diff --git a/assets/less/mio/classes/input/upload.less b/assets/less/mio/classes/input/upload.less new file mode 100644 index 00000000..a8d8bb71 --- /dev/null +++ b/assets/less/mio/classes/input/upload.less @@ -0,0 +1,7 @@ +.mio__input__upload { + display: none; + + &__label { + .mio__input__button(); + } +} diff --git a/assets/less/mio/classes/settings/avatar.less b/assets/less/mio/classes/settings/avatar.less new file mode 100644 index 00000000..a0705aee --- /dev/null +++ b/assets/less/mio/classes/settings/avatar.less @@ -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; + } +} diff --git a/assets/less/mio/classes/settings/content.less b/assets/less/mio/classes/settings/content.less index 27acb44f..70ca7989 100644 --- a/assets/less/mio/classes/settings/content.less +++ b/assets/less/mio/classes/settings/content.less @@ -2,4 +2,8 @@ &--account { margin: 1px; } + + &--avatar { + margin: 2px; + } } diff --git a/assets/less/mio/main.less b/assets/less/mio/main.less index d3550205..0c4e79ad 100644 --- a/assets/less/mio/main.less +++ b/assets/less/mio/main.less @@ -36,6 +36,7 @@ body { @import "classes/input/button"; @import "classes/input/text"; @import "classes/input/textarea"; +@import "classes/input/upload"; // Base styles @import "classes/avatar"; @@ -52,6 +53,7 @@ body { @import "classes/settings/content"; @import "classes/settings/errors"; @import "classes/settings/account"; +@import "classes/settings/avatar"; // Forums @import "classes/forum/listing"; diff --git a/composer.json b/composer.json index b6cac299..09dffd1c 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,9 @@ "require": { "php": ">=7.2", "ext-bcmath": "*", + "ext-imagick": "*", "ext-mbstring": "*", + "ext-redis": "*", "twig/twig": "~2.4", "nesbot/carbon": "~1.22", "illuminate/database": "~5.5", diff --git a/composer.lock b/composer.lock index 43ec0abb..9a5474fe 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "bfc5b8cbdbf22514c4b51ae1af8c333b", + "content-hash": "2a199e9d03d4a7caedecbe72d7ea6b35", "packages": [ { "name": "composer/ca-bundle", @@ -696,7 +696,7 @@ }, { "name": "illuminate/container", - "version": "v5.6.11", + "version": "v5.6.12", "source": { "type": "git", "url": "https://github.com/illuminate/container.git", @@ -740,7 +740,7 @@ }, { "name": "illuminate/contracts", - "version": "v5.6.11", + "version": "v5.6.12", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", @@ -784,16 +784,16 @@ }, { "name": "illuminate/database", - "version": "v5.6.11", + "version": "v5.6.12", "source": { "type": "git", "url": "https://github.com/illuminate/database.git", - "reference": "4d2fc3c816ed402fcac290e6ca7bc855d5313000" + "reference": "104cd99c17d46e6f96eafd4f1469ea921a289279" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/database/zipball/4d2fc3c816ed402fcac290e6ca7bc855d5313000", - "reference": "4d2fc3c816ed402fcac290e6ca7bc855d5313000", + "url": "https://api.github.com/repos/illuminate/database/zipball/104cd99c17d46e6f96eafd4f1469ea921a289279", + "reference": "104cd99c17d46e6f96eafd4f1469ea921a289279", "shasum": "" }, "require": { @@ -839,11 +839,11 @@ "orm", "sql" ], - "time": "2018-03-09T13:55:05+00:00" + "time": "2018-03-14T12:21:13+00:00" }, { "name": "illuminate/filesystem", - "version": "v5.6.11", + "version": "v5.6.12", "source": { "type": "git", "url": "https://github.com/illuminate/filesystem.git", @@ -894,7 +894,7 @@ }, { "name": "illuminate/pagination", - "version": "v5.6.11", + "version": "v5.6.12", "source": { "type": "git", "url": "https://github.com/illuminate/pagination.git", @@ -938,30 +938,30 @@ }, { "name": "illuminate/support", - "version": "v5.6.11", + "version": "v5.6.12", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "259f6f17a11b0379340ec5311fcba27bc2a04070" + "reference": "f0776f5bbfeeb9d4c4cac8f64d96f8f0cbe4f3f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/259f6f17a11b0379340ec5311fcba27bc2a04070", - "reference": "259f6f17a11b0379340ec5311fcba27bc2a04070", + "url": "https://api.github.com/repos/illuminate/support/zipball/f0776f5bbfeeb9d4c4cac8f64d96f8f0cbe4f3f7", + "reference": "f0776f5bbfeeb9d4c4cac8f64d96f8f0cbe4f3f7", "shasum": "" }, "require": { "doctrine/inflector": "~1.1", "ext-mbstring": "*", "illuminate/contracts": "5.6.*", - "nesbot/carbon": "^1.20", + "nesbot/carbon": "^1.24.1", "php": "^7.1.3" }, "conflict": { "tightenco/collect": "<5.5.33" }, "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/var-dumper": "Required to use the dd function (~4.0)." }, @@ -991,7 +991,7 @@ ], "description": "The Illuminate Support package.", "homepage": "https://laravel.com", - "time": "2018-03-09T16:52:54+00:00" + "time": "2018-03-14T12:56:14+00:00" }, { "name": "maxmind-db/reader", @@ -1097,16 +1097,16 @@ }, { "name": "nesbot/carbon", - "version": "1.24.2", + "version": "1.25.0", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "bba6c6e410c6b4317e37a9474aeaa753808c3875" + "reference": "cbcf13da0b531767e39eb86e9687f5deba9857b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/bba6c6e410c6b4317e37a9474aeaa753808c3875", - "reference": "bba6c6e410c6b4317e37a9474aeaa753808c3875", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cbcf13da0b531767e39eb86e9687f5deba9857b4", + "reference": "cbcf13da0b531767e39eb86e9687f5deba9857b4", "shasum": "" }, "require": { @@ -1146,7 +1146,7 @@ "datetime", "time" ], - "time": "2018-03-10T10:10:14+00:00" + "time": "2018-03-19T15:50:49+00:00" }, { "name": "psr/container", @@ -1478,16 +1478,16 @@ }, { "name": "twig/twig", - "version": "v2.4.6", + "version": "v2.4.7", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "d2117ec118c1ff3d28ccddca8212d82787a4809f" + "reference": "69aacd44dbbaa3199d5afb68605c996d577896fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/d2117ec118c1ff3d28ccddca8212d82787a4809f", - "reference": "d2117ec118c1ff3d28ccddca8212d82787a4809f", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/69aacd44dbbaa3199d5afb68605c996d577896fc", + "reference": "69aacd44dbbaa3199d5afb68605c996d577896fc", "shasum": "" }, "require": { @@ -1496,8 +1496,8 @@ }, "require-dev": { "psr/container": "^1.0", - "symfony/debug": "~2.7", - "symfony/phpunit-bridge": "~3.3@dev" + "symfony/debug": "^2.7", + "symfony/phpunit-bridge": "^3.3" }, "type": "library", "extra": { @@ -1540,7 +1540,7 @@ "keywords": [ "templating" ], - "time": "2018-03-03T16:23:01+00:00" + "time": "2018-03-20T04:31:17+00:00" } ], "packages-dev": [ @@ -3010,7 +3010,9 @@ "platform": { "php": ">=7.2", "ext-bcmath": "*", - "ext-mbstring": "*" + "ext-imagick": "*", + "ext-mbstring": "*", + "ext-redis": "*" }, "platform-dev": [] } diff --git a/misuzu.php b/misuzu.php index 61db05fa..af98739a 100644 --- a/misuzu.php +++ b/misuzu.php @@ -10,11 +10,20 @@ $app = Application::start( $app->startDatabase(); 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'])) { $app->startSession((int)$_COOKIE['msz_uid'], $_COOKIE['msz_sid']); } - //ob_start('ob_gzhandler'); + if (!$app->inDebugMode()) { + ob_start('ob_gzhandler'); + } $app->startTemplating(); } diff --git a/public/images/no-avatar.png b/public/images/no-avatar.png new file mode 100644 index 00000000..f08da9e1 Binary files /dev/null and b/public/images/no-avatar.png differ diff --git a/public/profile.php b/public/profile.php index bc3bca12..decc0126 100644 --- a/public/profile.php +++ b/public/profile.php @@ -1,16 +1,56 @@ templating->vars(['profile' => User::findOrFail($user_id)]); -} catch (Exception $ex) { - http_response_code(404); - echo $app->templating->render('user.notfound'); - return; +switch ($mode) { + case 'avatar': + $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); + echo $app->templating->render('user.notfound'); + break; + } + + $app->templating->var('profile', $profile_user); + echo $app->templating->render('user.view'); + break; } - -echo $app->templating->render('user.view'); diff --git a/public/settings.php b/public/settings.php index b0c26b64..0a3d7f6d 100644 --- a/public/settings.php +++ b/public/settings.php @@ -1,5 +1,6 @@ [ 'name' => 'Twitter', @@ -91,11 +94,17 @@ if (!array_key_exists($settings_mode, $settings_modes)) { $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') { switch ($settings_mode) { case 'account': if (!tmp_csrf_verify($_POST['csrf'] ?? '')) { - $settings_errors[] = "Couldn't verify you, please refresh the page and retry."; + $settings_errors[] = $csrf_error_str; break; } @@ -193,6 +202,110 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $settings_user->save(); } 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')); break; + case 'avatar': + $app->templating->var( + 'can_import_old_avatar', + !File::exists($app->getStore('avatars/original')->filename($avatar_filename)) + ); + break; + case 'sessions': $app->templating->var('user_sessions', $settings_user->sessions->reverse()); break; diff --git a/src/Application.php b/src/Application.php index 19402425..59bd32e7 100644 --- a/src/Application.php +++ b/src/Application.php @@ -2,6 +2,7 @@ namespace Misuzu; use Misuzu\Config\ConfigManager; +use Misuzu\IO\Directory; use Misuzu\Users\Session; use UnexpectedValueException; use InvalidArgumentException; @@ -48,6 +49,48 @@ class Application extends ApplicationBase 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 { $session = Session::where('session_key', $session_key) diff --git a/src/IO/Directory.php b/src/IO/Directory.php index 940f5283..26f3e7fb 100644 --- a/src/IO/Directory.php +++ b/src/IO/Directory.php @@ -12,7 +12,22 @@ class Directory * Path to this directory. * @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. @@ -21,11 +36,13 @@ class Directory */ 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; } + + $this->path = realpath($path); } /** @@ -44,6 +61,11 @@ class Directory }, glob($this->path . '/' . $pattern)); } + public function filename(string $filename): string + { + return $this->getPath() . '/' . $filename; + } + /** * Creates a directory if it doesn't already exist. * @param string $path @@ -52,17 +74,33 @@ class Directory */ public static function create(string $path): Directory { - $path = static::fixSlashes($path); - if (static::exists($path)) { 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); } + 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! * @param string $path diff --git a/src/IO/File.php b/src/IO/File.php index 10b3304b..e2dbc057 100644 --- a/src/IO/File.php +++ b/src/IO/File.php @@ -1,6 +1,8 @@ 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. * @param string $prefix diff --git a/src/IO/FileStream.php b/src/IO/FileStream.php index 0ee15b14..5777d67e 100644 --- a/src/IO/FileStream.php +++ b/src/IO/FileStream.php @@ -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); } - protected function getCanSeek(): bool + public function getCanSeek(): bool { return ($this->fileMode & static::MODE_APPEND_RAW) == 0 && $this->getCanRead(); } - protected function getCanTimeout(): bool + public function getCanTimeout(): bool { return false; } - protected function getCanWrite(): bool + public function getCanWrite(): bool { return ($this->fileMode & static::MODE_WRITE) > 0 && is_writable($this->filePath); } - protected function getLength(): int + public function getLength(): int { $this->ensureHandleActive(); return fstat($this->fileHandle)['size']; } - protected function getPosition(): int + public function getPosition(): int { $this->ensureHandleActive(); return ftell($this->fileHandle); } - protected function getReadTimeout(): int + public function getReadTimeout(): int { return -1; } - protected function getWriteTimeout(): int + public function getWriteTimeout(): int { return -1; } diff --git a/src/IO/NetworkStream.php b/src/IO/NetworkStream.php index 59e85221..f1d3a8b6 100644 --- a/src/IO/NetworkStream.php +++ b/src/IO/NetworkStream.php @@ -49,42 +49,42 @@ class NetworkStream extends Stream } } - protected function getCanRead(): bool + public function getCanRead(): bool { return true; } - protected function getCanSeek(): bool + public function getCanSeek(): bool { return false; } - protected function getCanTimeout(): bool + public function getCanTimeout(): bool { return true; } - protected function getCanWrite(): bool + public function getCanWrite(): bool { return true; } - protected function getLength(): int + public function getLength(): int { return -1; } - protected function getPosition(): int + public function getPosition(): int { return -1; } - protected function getReadTimeout(): int + public function getReadTimeout(): int { return -1; } - protected function getWriteTimeout(): int + public function getWriteTimeout(): int { return -1; } diff --git a/src/IO/Stream.php b/src/IO/Stream.php index b0b2aaa0..96aed6eb 100644 --- a/src/IO/Stream.php +++ b/src/IO/Stream.php @@ -20,14 +20,14 @@ abstract class Stream throw new InvalidArgumentException; } - abstract protected function getCanRead(): bool; - abstract protected function getCanSeek(): bool; - abstract protected function getCanTimeout(): bool; - abstract protected function getCanWrite(): bool; - abstract protected function getLength(): int; - abstract protected function getPosition(): int; - abstract protected function getReadTimeout(): int; - abstract protected function getWriteTimeout(): int; + abstract public function getCanRead(): bool; + abstract public function getCanSeek(): bool; + abstract public function getCanTimeout(): bool; + abstract public function getCanWrite(): bool; + abstract public function getLength(): int; + abstract public function getPosition(): int; + abstract public function getReadTimeout(): int; + abstract public function getWriteTimeout(): int; abstract public function flush(): void; abstract public function close(): void; diff --git a/utility.php b/utility.php index a545e8f8..37111786 100644 --- a/utility.php +++ b/utility.php @@ -150,6 +150,53 @@ function tmp_csrf_token(?\Misuzu\Users\Session $session = null): string 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 { return is_int($value) && $value >= $boundary_low && $value <= $boundary_high; diff --git a/views/mio/master.twig b/views/mio/master.twig index c171544a..c151379e 100644 --- a/views/mio/master.twig +++ b/views/mio/master.twig @@ -28,7 +28,7 @@
Hey, {{ app.session.user.username }}!
-
+
{% endspaceless %}
-
+
diff --git a/views/nova/user/view.twig b/views/nova/user/view.twig index d15f0826..cb99ec5b 100644 --- a/views/nova/user/view.twig +++ b/views/nova/user/view.twig @@ -55,7 +55,7 @@
{% spaceless %}
-
+
{% for id, data in hierarchies %} {% if data.display %}