Add avatar uploading.

This commit is contained in:
flash 2018-03-24 05:31:42 +01:00
parent a23b2729cc
commit 073389c964
21 changed files with 450 additions and 74 deletions

View file

@ -0,0 +1,7 @@
.mio__input__upload {
display: none;
&__label {
.mio__input__button();
}
}

View file

@ -0,0 +1,20 @@
.mio__settings__avatar {
display: flex;
min-height: 200px;
justify-content: space-between;
&__form {
display: block;
margin-bottom: 2px;
}
&__forms {
text-align: center;
flex-grow: 1;
}
&__preview {
flex-grow: 1;
flex-shrink: 0;
}
}

View file

@ -2,4 +2,8 @@
&--account { &--account {
margin: 1px; margin: 1px;
} }
&--avatar {
margin: 2px;
}
} }

View file

@ -36,6 +36,7 @@ body {
@import "classes/input/button"; @import "classes/input/button";
@import "classes/input/text"; @import "classes/input/text";
@import "classes/input/textarea"; @import "classes/input/textarea";
@import "classes/input/upload";
// Base styles // Base styles
@import "classes/avatar"; @import "classes/avatar";
@ -52,6 +53,7 @@ body {
@import "classes/settings/content"; @import "classes/settings/content";
@import "classes/settings/errors"; @import "classes/settings/errors";
@import "classes/settings/account"; @import "classes/settings/account";
@import "classes/settings/avatar";
// Forums // Forums
@import "classes/forum/listing"; @import "classes/forum/listing";

View file

@ -9,7 +9,9 @@
"require": { "require": {
"php": ">=7.2", "php": ">=7.2",
"ext-bcmath": "*", "ext-bcmath": "*",
"ext-imagick": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"ext-redis": "*",
"twig/twig": "~2.4", "twig/twig": "~2.4",
"nesbot/carbon": "~1.22", "nesbot/carbon": "~1.22",
"illuminate/database": "~5.5", "illuminate/database": "~5.5",

62
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "bfc5b8cbdbf22514c4b51ae1af8c333b", "content-hash": "2a199e9d03d4a7caedecbe72d7ea6b35",
"packages": [ "packages": [
{ {
"name": "composer/ca-bundle", "name": "composer/ca-bundle",
@ -696,7 +696,7 @@
}, },
{ {
"name": "illuminate/container", "name": "illuminate/container",
"version": "v5.6.11", "version": "v5.6.12",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/illuminate/container.git", "url": "https://github.com/illuminate/container.git",
@ -740,7 +740,7 @@
}, },
{ {
"name": "illuminate/contracts", "name": "illuminate/contracts",
"version": "v5.6.11", "version": "v5.6.12",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/illuminate/contracts.git", "url": "https://github.com/illuminate/contracts.git",
@ -784,16 +784,16 @@
}, },
{ {
"name": "illuminate/database", "name": "illuminate/database",
"version": "v5.6.11", "version": "v5.6.12",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/illuminate/database.git", "url": "https://github.com/illuminate/database.git",
"reference": "4d2fc3c816ed402fcac290e6ca7bc855d5313000" "reference": "104cd99c17d46e6f96eafd4f1469ea921a289279"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/illuminate/database/zipball/4d2fc3c816ed402fcac290e6ca7bc855d5313000", "url": "https://api.github.com/repos/illuminate/database/zipball/104cd99c17d46e6f96eafd4f1469ea921a289279",
"reference": "4d2fc3c816ed402fcac290e6ca7bc855d5313000", "reference": "104cd99c17d46e6f96eafd4f1469ea921a289279",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -839,11 +839,11 @@
"orm", "orm",
"sql" "sql"
], ],
"time": "2018-03-09T13:55:05+00:00" "time": "2018-03-14T12:21:13+00:00"
}, },
{ {
"name": "illuminate/filesystem", "name": "illuminate/filesystem",
"version": "v5.6.11", "version": "v5.6.12",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/illuminate/filesystem.git", "url": "https://github.com/illuminate/filesystem.git",
@ -894,7 +894,7 @@
}, },
{ {
"name": "illuminate/pagination", "name": "illuminate/pagination",
"version": "v5.6.11", "version": "v5.6.12",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/illuminate/pagination.git", "url": "https://github.com/illuminate/pagination.git",
@ -938,30 +938,30 @@
}, },
{ {
"name": "illuminate/support", "name": "illuminate/support",
"version": "v5.6.11", "version": "v5.6.12",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/illuminate/support.git", "url": "https://github.com/illuminate/support.git",
"reference": "259f6f17a11b0379340ec5311fcba27bc2a04070" "reference": "f0776f5bbfeeb9d4c4cac8f64d96f8f0cbe4f3f7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/illuminate/support/zipball/259f6f17a11b0379340ec5311fcba27bc2a04070", "url": "https://api.github.com/repos/illuminate/support/zipball/f0776f5bbfeeb9d4c4cac8f64d96f8f0cbe4f3f7",
"reference": "259f6f17a11b0379340ec5311fcba27bc2a04070", "reference": "f0776f5bbfeeb9d4c4cac8f64d96f8f0cbe4f3f7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"doctrine/inflector": "~1.1", "doctrine/inflector": "~1.1",
"ext-mbstring": "*", "ext-mbstring": "*",
"illuminate/contracts": "5.6.*", "illuminate/contracts": "5.6.*",
"nesbot/carbon": "^1.20", "nesbot/carbon": "^1.24.1",
"php": "^7.1.3" "php": "^7.1.3"
}, },
"conflict": { "conflict": {
"tightenco/collect": "<5.5.33" "tightenco/collect": "<5.5.33"
}, },
"suggest": { "suggest": {
"illuminate/filesystem": "Required to use the composer class (5.2.*).", "illuminate/filesystem": "Required to use the composer class (5.6.*).",
"symfony/process": "Required to use the composer class (~4.0).", "symfony/process": "Required to use the composer class (~4.0).",
"symfony/var-dumper": "Required to use the dd function (~4.0)." "symfony/var-dumper": "Required to use the dd function (~4.0)."
}, },
@ -991,7 +991,7 @@
], ],
"description": "The Illuminate Support package.", "description": "The Illuminate Support package.",
"homepage": "https://laravel.com", "homepage": "https://laravel.com",
"time": "2018-03-09T16:52:54+00:00" "time": "2018-03-14T12:56:14+00:00"
}, },
{ {
"name": "maxmind-db/reader", "name": "maxmind-db/reader",
@ -1097,16 +1097,16 @@
}, },
{ {
"name": "nesbot/carbon", "name": "nesbot/carbon",
"version": "1.24.2", "version": "1.25.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/briannesbitt/Carbon.git", "url": "https://github.com/briannesbitt/Carbon.git",
"reference": "bba6c6e410c6b4317e37a9474aeaa753808c3875" "reference": "cbcf13da0b531767e39eb86e9687f5deba9857b4"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/bba6c6e410c6b4317e37a9474aeaa753808c3875", "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cbcf13da0b531767e39eb86e9687f5deba9857b4",
"reference": "bba6c6e410c6b4317e37a9474aeaa753808c3875", "reference": "cbcf13da0b531767e39eb86e9687f5deba9857b4",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1146,7 +1146,7 @@
"datetime", "datetime",
"time" "time"
], ],
"time": "2018-03-10T10:10:14+00:00" "time": "2018-03-19T15:50:49+00:00"
}, },
{ {
"name": "psr/container", "name": "psr/container",
@ -1478,16 +1478,16 @@
}, },
{ {
"name": "twig/twig", "name": "twig/twig",
"version": "v2.4.6", "version": "v2.4.7",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/twigphp/Twig.git", "url": "https://github.com/twigphp/Twig.git",
"reference": "d2117ec118c1ff3d28ccddca8212d82787a4809f" "reference": "69aacd44dbbaa3199d5afb68605c996d577896fc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/d2117ec118c1ff3d28ccddca8212d82787a4809f", "url": "https://api.github.com/repos/twigphp/Twig/zipball/69aacd44dbbaa3199d5afb68605c996d577896fc",
"reference": "d2117ec118c1ff3d28ccddca8212d82787a4809f", "reference": "69aacd44dbbaa3199d5afb68605c996d577896fc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1496,8 +1496,8 @@
}, },
"require-dev": { "require-dev": {
"psr/container": "^1.0", "psr/container": "^1.0",
"symfony/debug": "~2.7", "symfony/debug": "^2.7",
"symfony/phpunit-bridge": "~3.3@dev" "symfony/phpunit-bridge": "^3.3"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@ -1540,7 +1540,7 @@
"keywords": [ "keywords": [
"templating" "templating"
], ],
"time": "2018-03-03T16:23:01+00:00" "time": "2018-03-20T04:31:17+00:00"
} }
], ],
"packages-dev": [ "packages-dev": [
@ -3010,7 +3010,9 @@
"platform": { "platform": {
"php": ">=7.2", "php": ">=7.2",
"ext-bcmath": "*", "ext-bcmath": "*",
"ext-mbstring": "*" "ext-imagick": "*",
"ext-mbstring": "*",
"ext-redis": "*"
}, },
"platform-dev": [] "platform-dev": []
} }

View file

@ -10,11 +10,20 @@ $app = Application::start(
$app->startDatabase(); $app->startDatabase();
if (PHP_SAPI !== 'cli') { if (PHP_SAPI !== 'cli') {
$storage_dir = $app->getStoragePath();
if (!$storage_dir->isReadable()
|| !$storage_dir->isWritable()) {
echo 'Cannot access storage directory.';
exit;
}
if (isset($_COOKIE['msz_uid'], $_COOKIE['msz_sid'])) { if (isset($_COOKIE['msz_uid'], $_COOKIE['msz_sid'])) {
$app->startSession((int)$_COOKIE['msz_uid'], $_COOKIE['msz_sid']); $app->startSession((int)$_COOKIE['msz_uid'], $_COOKIE['msz_sid']);
} }
//ob_start('ob_gzhandler'); if (!$app->inDebugMode()) {
ob_start('ob_gzhandler');
}
$app->startTemplating(); $app->startTemplating();
} }

BIN
public/images/no-avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View file

@ -1,16 +1,56 @@
<?php <?php
use Misuzu\IO\File;
use Misuzu\IO\FileStream;
use Misuzu\Users\User; use Misuzu\Users\User;
require_once __DIR__ . '/../misuzu.php'; require_once __DIR__ . '/../misuzu.php';
$user_id = (int)($_GET['u'] ?? 0); $user_id = (int)($_GET['u'] ?? 0);
$mode = (string)($_GET['m'] ?? 'view');
$profile_user = User::find($user_id);
try { switch ($mode) {
$app->templating->vars(['profile' => User::findOrFail($user_id)]); case 'avatar':
} catch (Exception $ex) { $avatar_filename = $app->getPath(
$app->config->get('Avatar', 'default_path', 'string', 'public/images/no-avatar.png')
);
if ($profile_user !== null) {
$user_avatar = "{$profile_user->user_id}.msz";
$cropped_avatar = $app->getStore('avatars/200x200')->filename($user_avatar);
if (File::exists($cropped_avatar)) {
$avatar_filename = $cropped_avatar;
} else {
$original_avatar = $app->getStore('avatars/original')->filename($user_avatar);
if (File::exists($original_avatar)) {
try {
File::writeAll(
$cropped_avatar,
crop_image_centred_path($original_avatar, 200, 200)->getImagesBlob()
);
$avatar_filename = $cropped_avatar;
} catch (Exception $ex) {
}
}
}
}
header('Content-Type: ' . mime_content_type($avatar_filename));
echo File::readToEnd($avatar_filename);
break;
case 'view':
default:
if ($profile_user === null) {
http_response_code(404); http_response_code(404);
echo $app->templating->render('user.notfound'); echo $app->templating->render('user.notfound');
return; break;
} }
echo $app->templating->render('user.view'); $app->templating->var('profile', $profile_user);
echo $app->templating->render('user.view');
break;
}

View file

@ -1,5 +1,6 @@
<?php <?php
use Misuzu\Application; use Misuzu\Application;
use Misuzu\IO\File;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\Session; use Misuzu\Users\Session;
@ -12,6 +13,8 @@ if ($settings_session === null) {
return; return;
} }
$csrf_error_str = "Couldn't verify you, please refresh the page and retry.";
$settings_profile_fields = [ $settings_profile_fields = [
'twitter' => [ 'twitter' => [
'name' => 'Twitter', 'name' => 'Twitter',
@ -91,11 +94,17 @@ if (!array_key_exists($settings_mode, $settings_modes)) {
$settings_errors = []; $settings_errors = [];
$avatar_filename = "{$settings_user->user_id}.msz";
$avatar_max_width = $app->config->get('Avatar', 'max_width', 'int', 4000);
$avatar_max_height = $app->config->get('Avatar', 'max_height', 'int', 4000);
$avatar_max_filesize = $app->config->get('Avatar', 'max_filesize', 'int', 1000000);
$avatar_max_filesize_human = byte_symbol($avatar_max_filesize, true);
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
switch ($settings_mode) { switch ($settings_mode) {
case 'account': case 'account':
if (!tmp_csrf_verify($_POST['csrf'] ?? '')) { if (!tmp_csrf_verify($_POST['csrf'] ?? '')) {
$settings_errors[] = "Couldn't verify you, please refresh the page and retry."; $settings_errors[] = $csrf_error_str;
break; break;
} }
@ -193,6 +202,110 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$settings_user->save(); $settings_user->save();
} }
break; break;
case 'avatar':
if (isset($_POST['import'])
&& !File::exists($app->getStore('avatars/original')->filename($avatar_filename))) {
if (!tmp_csrf_verify($_POST['import'])) {
$settings_errors[] = $csrf_error_str;
break;
}
$old_avatar_url = trim(file_get_contents(
"https://secret.flashii.net/avatar-serve.php?id={$settings_user->user_id}&r"
));
if (empty($old_avatar_url)) {
$settings_errors[] = 'No old avatar was found for you.';
break;
}
File::writeAll(
$app->getStore('avatars/original')->filename($avatar_filename),
file_get_contents($old_avatar_url)
);
break;
}
if (isset($_POST['delete'])) {
if (!tmp_csrf_verify($_POST['delete'])) {
$settings_errors[] = $csrf_error_str;
break;
}
File::delete($app->getStore('avatars/original')->filename($avatar_filename));
File::delete($app->getStore('avatars/200x200')->filename($avatar_filename));
break;
}
if (isset($_POST['upload'])) {
if (!tmp_csrf_verify($_POST['upload'])) {
$settings_errors[] = $csrf_error_str;
break;
}
switch ($_FILES['avatar']['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_PARTIAL:
$settings_errors[] = 'The upload was interrupted, please try again!';
break;
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
$settings_errors[] = "Your avatar is not allowed to be larger in filesize than {$avatar_max_filesize_human}!";
break;
case UPLOAD_ERR_NO_TMP_DIR:
case UPLOAD_ERR_CANT_WRITE:
$settings_errors[] = 'Unable to save your avatar, contact an administator!';
break;
case UPLOAD_ERR_EXTENSION:
default:
$settings_errors[] = 'Something happened?';
break;
}
if (count($settings_errors) > 0) {
break;
}
$upload_path = $_FILES['avatar']['tmp_name'];
$upload_meta = getimagesize($upload_path);
if (!$upload_meta
|| !in_array($upload_meta[2], [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG], true)
|| $upload_meta[0] < 1
|| $upload_meta[1] < 1) {
$settings_errors[] = 'Please provide a valid image.';
break;
}
if ($upload_meta[0] > $avatar_max_width || $upload_meta[1] > $avatar_max_height) {
$settings_errors[] = "Your avatar can't be larger than {$avatar_max_width}x{$avatar_max_height}, yours was {$upload_meta[0]}x{$upload_meta[1]}";
break;
}
if (filesize($upload_path) > $avatar_max_filesize) {
$settings_errors[] = "Your avatar is not allowed to be larger in filesize than {$avatar_max_filesize_human}!";
break;
}
$avatar_path = $app->getStore('avatars/original')->filename($avatar_filename);
move_uploaded_file($upload_path, $avatar_path);
$crop_path = $app->getStore('avatars/200x200')->filename($avatar_filename);
if (File::exists($crop_path)) {
File::delete($crop_path);
}
break;
}
$settings_errors[] = "You shouldn't have done that.";
break;
} }
} }
@ -204,6 +317,13 @@ switch ($settings_mode) {
$app->templating->vars(compact('settings_profile_fields')); $app->templating->vars(compact('settings_profile_fields'));
break; break;
case 'avatar':
$app->templating->var(
'can_import_old_avatar',
!File::exists($app->getStore('avatars/original')->filename($avatar_filename))
);
break;
case 'sessions': case 'sessions':
$app->templating->var('user_sessions', $settings_user->sessions->reverse()); $app->templating->var('user_sessions', $settings_user->sessions->reverse());
break; break;

View file

@ -2,6 +2,7 @@
namespace Misuzu; namespace Misuzu;
use Misuzu\Config\ConfigManager; use Misuzu\Config\ConfigManager;
use Misuzu\IO\Directory;
use Misuzu\Users\Session; use Misuzu\Users\Session;
use UnexpectedValueException; use UnexpectedValueException;
use InvalidArgumentException; use InvalidArgumentException;
@ -48,6 +49,48 @@ class Application extends ApplicationBase
ExceptionHandler::unregister(); ExceptionHandler::unregister();
} }
public function inDebugMode(): bool
{
return $this->debugMode;
}
public function getPath(string $path): string
{
if (!starts_with($path, '/')) {
$path = __DIR__ . '/../' . $path;
}
return Directory::fixSlashes(rtrim($path, '/'));
}
public function getStoragePath(string $append = ''): Directory
{
$path = '';
if (starts_with($append, '/')) {
$path = $append;
} else {
$path = $this->config->get('Storage', 'path', 'string', __DIR__ . '/../store');
if (!empty($append)) {
$path .= '/' . $append;
}
}
return Directory::createOrOpen($this->getPath($path));
}
public function getStore(string $purpose): Directory
{
$override_key = "override_{$purpose}";
if ($this->config->contains('Storage', $override_key)) {
return new Directory($this->config->get('Storage', $override_key));
}
return $this->getStoragePath($purpose);
}
public function startSession(int $user_id, string $session_key): void public function startSession(int $user_id, string $session_key): void
{ {
$session = Session::where('session_key', $session_key) $session = Session::where('session_key', $session_key)

View file

@ -12,7 +12,22 @@ class Directory
* Path to this directory. * Path to this directory.
* @var string * @var string
*/ */
public $path; private $path;
public function getPath(): string
{
return $this->path;
}
public function isReadable(): bool
{
return is_readable($this->getPath());
}
public function isWritable(): bool
{
return is_writable($this->getPath());
}
/** /**
* Fixes the path, sets proper slashes and checks if the directory exists. * Fixes the path, sets proper slashes and checks if the directory exists.
@ -21,11 +36,13 @@ class Directory
*/ */
public function __construct(string $path) public function __construct(string $path)
{ {
$this->path = static::fixSlashes(rtrim($path, '/\\')); $path = static::fixSlashes(rtrim($path, '/\\'));
if (!static::exists($this->path)) { if (!static::exists($path)) {
throw new DirectoryDoesNotExistException; throw new DirectoryDoesNotExistException;
} }
$this->path = realpath($path);
} }
/** /**
@ -44,6 +61,11 @@ class Directory
}, glob($this->path . '/' . $pattern)); }, glob($this->path . '/' . $pattern));
} }
public function filename(string $filename): string
{
return $this->getPath() . '/' . $filename;
}
/** /**
* Creates a directory if it doesn't already exist. * Creates a directory if it doesn't already exist.
* @param string $path * @param string $path
@ -52,17 +74,33 @@ class Directory
*/ */
public static function create(string $path): Directory public static function create(string $path): Directory
{ {
$path = static::fixSlashes($path);
if (static::exists($path)) { if (static::exists($path)) {
throw new DirectoryExistsException; throw new DirectoryExistsException;
} }
mkdir($path); $split_path = explode('/', $path);
$existing_path = '/';
foreach ($split_path as $path_part) {
$existing_path .= $path_part . '/';
if (!Directory::exists($existing_path)) {
mkdir($existing_path);
}
}
return new static($path); return new static($path);
} }
public static function createOrOpen(string $path): Directory
{
if (static::exists($path)) {
return new Directory($path);
} else {
return Directory::create($path);
}
}
/** /**
* Deletes a directory, recursively if requested. Use $purge with care! * Deletes a directory, recursively if requested. Use $purge with care!
* @param string $path * @param string $path

View file

@ -1,6 +1,8 @@
<?php <?php
namespace Misuzu\IO; namespace Misuzu\IO;
use Exception;
/** /**
* Static file meta functions. * Static file meta functions.
* @package Misuzu\IO * @package Misuzu\IO
@ -13,6 +15,27 @@ class File
return new FileStream($filename, FileStream::MODE_READ_WRITE, true); return new FileStream($filename, FileStream::MODE_READ_WRITE, true);
} }
public static function readToEnd(string $filename, bool $lock = false): string
{
$output = '';
try {
$file = new FileStream($filename, FileStream::MODE_READ, $lock);
$output = $file->read($file->getLength());
$file->close();
} catch (Exception $ex) {
}
return $output;
}
public static function writeAll(string $filename, string $data): void
{
$file = new FileStream($filename, FileStream::MODE_TRUNCATE, true);
$file->write($data);
$file->close();
}
/** /**
* Creates an instance of a temporary file. * Creates an instance of a temporary file.
* @param string $prefix * @param string $prefix

View file

@ -124,44 +124,44 @@ class FileStream extends Stream
} }
} }
protected function getCanRead(): bool public function getCanRead(): bool
{ {
return ($this->fileMode & static::MODE_READ) > 0 && is_readable($this->filePath); return ($this->fileMode & static::MODE_READ) > 0 && is_readable($this->filePath);
} }
protected function getCanSeek(): bool public function getCanSeek(): bool
{ {
return ($this->fileMode & static::MODE_APPEND_RAW) == 0 && $this->getCanRead(); return ($this->fileMode & static::MODE_APPEND_RAW) == 0 && $this->getCanRead();
} }
protected function getCanTimeout(): bool public function getCanTimeout(): bool
{ {
return false; return false;
} }
protected function getCanWrite(): bool public function getCanWrite(): bool
{ {
return ($this->fileMode & static::MODE_WRITE) > 0 && is_writable($this->filePath); return ($this->fileMode & static::MODE_WRITE) > 0 && is_writable($this->filePath);
} }
protected function getLength(): int public function getLength(): int
{ {
$this->ensureHandleActive(); $this->ensureHandleActive();
return fstat($this->fileHandle)['size']; return fstat($this->fileHandle)['size'];
} }
protected function getPosition(): int public function getPosition(): int
{ {
$this->ensureHandleActive(); $this->ensureHandleActive();
return ftell($this->fileHandle); return ftell($this->fileHandle);
} }
protected function getReadTimeout(): int public function getReadTimeout(): int
{ {
return -1; return -1;
} }
protected function getWriteTimeout(): int public function getWriteTimeout(): int
{ {
return -1; return -1;
} }

View file

@ -49,42 +49,42 @@ class NetworkStream extends Stream
} }
} }
protected function getCanRead(): bool public function getCanRead(): bool
{ {
return true; return true;
} }
protected function getCanSeek(): bool public function getCanSeek(): bool
{ {
return false; return false;
} }
protected function getCanTimeout(): bool public function getCanTimeout(): bool
{ {
return true; return true;
} }
protected function getCanWrite(): bool public function getCanWrite(): bool
{ {
return true; return true;
} }
protected function getLength(): int public function getLength(): int
{ {
return -1; return -1;
} }
protected function getPosition(): int public function getPosition(): int
{ {
return -1; return -1;
} }
protected function getReadTimeout(): int public function getReadTimeout(): int
{ {
return -1; return -1;
} }
protected function getWriteTimeout(): int public function getWriteTimeout(): int
{ {
return -1; return -1;
} }

View file

@ -20,14 +20,14 @@ abstract class Stream
throw new InvalidArgumentException; throw new InvalidArgumentException;
} }
abstract protected function getCanRead(): bool; abstract public function getCanRead(): bool;
abstract protected function getCanSeek(): bool; abstract public function getCanSeek(): bool;
abstract protected function getCanTimeout(): bool; abstract public function getCanTimeout(): bool;
abstract protected function getCanWrite(): bool; abstract public function getCanWrite(): bool;
abstract protected function getLength(): int; abstract public function getLength(): int;
abstract protected function getPosition(): int; abstract public function getPosition(): int;
abstract protected function getReadTimeout(): int; abstract public function getReadTimeout(): int;
abstract protected function getWriteTimeout(): int; abstract public function getWriteTimeout(): int;
abstract public function flush(): void; abstract public function flush(): void;
abstract public function close(): void; abstract public function close(): void;

View file

@ -150,6 +150,53 @@ function tmp_csrf_token(?\Misuzu\Users\Session $session = null): string
return md5($session->session_key); return md5($session->session_key);
} }
function crop_image_centred_path(string $filename, int $target_width, int $target_height): \Imagick
{
return crop_image_centred(new \Imagick($filename), $target_width, $target_height);
}
function crop_image_centred(Imagick $image, int $target_width, int $target_height): Imagick
{
$image->setImageFormat($image->getNumberImages() > 1 ? 'gif' : 'png');
$image = $image->coalesceImages();
$width = $image->getImageWidth();
$height = $image->getImageHeight();
if ($width > $height) {
$resize_width = $width * $target_height / $height;
$resize_height = $target_height;
} else {
$resize_width = $target_width;
$resize_height = $height * $target_width / $width;
}
do {
$image->resizeImage(
$resize_width,
$resize_height,
Imagick::FILTER_LANCZOS,
0.9
);
$image->cropImage(
$target_width,
$target_height,
($resize_width - $target_width) / 2,
($resize_height - $target_height) / 2
);
$image->setImagePage(
$target_width,
$target_height,
0,
0
);
} while ($image->nextImage());
return $image->deconstructImages();
}
function is_int_ex($value, int $boundary_low, int $boundary_high): bool function is_int_ex($value, int $boundary_low, int $boundary_high): bool
{ {
return is_int($value) && $value >= $boundary_low && $value <= $boundary_high; return is_int($value) && $value >= $boundary_low && $value <= $boundary_high;

View file

@ -28,7 +28,7 @@
<div class="mio__container mio__header__user"> <div class="mio__container mio__header__user">
<div class="mio__container__title">Hey, {{ app.session.user.username }}!</div> <div class="mio__container__title">Hey, {{ app.session.user.username }}!</div>
<div class="mio__container__content mio__header__user__content"> <div class="mio__container__content mio__header__user__content">
<div class="mio__avatar mio__header__user__avatar" style="background-image:url('https://secret.flashii.net/avatar-serve.php?id={{ app.session.user.user_id }}');"></div> <div class="mio__avatar mio__header__user__avatar" style="background-image:url('/profile.php?u={{ app.session.user.user_id }}&amp;m=avatar');"></div>
<div class="mio__header__user__links__container"> <div class="mio__header__user__links__container">
<ul class="mio__header__user__links"> <ul class="mio__header__user__links">
<li class="mio__header__user__option"><a class="mio__header__user__link" href="/profile.php?u={{ app.session.user.user_id }}">Profile</a></li> <li class="mio__header__user__option"><a class="mio__header__user__link" href="/profile.php?u={{ app.session.user.user_id }}">Profile</a></li>

View file

@ -1,5 +1,24 @@
{% extends '@mio/settings/master.twig' %} {% extends '@mio/settings/master.twig' %}
{% block settings_content %} {% block settings_content %}
<p>Sorry for getting your hopes up with the button, but not yet! Read the front page while logged in.</p> <div class="mio__settings__avatar">
<div class="mio__settings__avatar__forms">
<form class="mio__settings__avatar__form" method="post" action="?m=avatar" enctype="multipart/form-data">
<label class="mio__input__upload__label">
<input type="file" name="avatar" class="mio__input__upload">
Pick file...
</label>
<button class="mio__input__button" name="upload" value="{{ csrf_token() }}">Upload</button>
</form>
<form class="mio__settings__avatar__form" method="post" action="?m=avatar">
<button class="mio__input__button" name="delete" value="{{ csrf_token() }}">Delete</button>
</form>
{% if can_import_old_avatar %}
<form class="mio__settings__avatar__form" method="post" action="?m=avatar">
<button class="mio__input__button" name="import" value="{{ csrf_token() }}">Import old avatar</button>
</form>
{% endif %}
</div>
<div class="mio__avatar mio__settings__avatar__preview" style="background-image:url('/profile.php?u={{ settings_user.user_id }}&amp;m=avatar')"></div>
</div>
{% endblock %} {% endblock %}

View file

@ -115,7 +115,7 @@
</div> </div>
{% endspaceless %} {% endspaceless %}
</div> </div>
<div class="mio__avatar mio__profile__avatar" style="background-image:url('https://secret.flashii.net/avatar-serve.php?id={{ profile.user_id }}');"></div> <div class="mio__avatar mio__profile__avatar" style="background-image:url('/profile.php?u={{ profile.user_id }}&amp;m=avatar');"></div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -55,7 +55,7 @@
<div class="profile__content"> <div class="profile__content">
{% spaceless %} {% spaceless %}
<div class="profile__container profile__container--left"> <div class="profile__container profile__container--left">
<div class="profile__avatar" style="background-image: url('https://secret.flashii.net/avatar-serve.php?id={{ profile.user_id }}');"></div> <div class="profile__avatar" style="background-image: url('/profile.php?u={{ profile.user_id }}&amp;m=avatar');"></div>
<div class="platform profile__platform profile__hierarchies"> <div class="platform profile__platform profile__hierarchies">
{% for id, data in hierarchies %} {% for id, data in hierarchies %}
{% if data.display %} {% if data.display %}