Add avatar uploading.
This commit is contained in:
parent
a23b2729cc
commit
073389c964
21 changed files with 450 additions and 74 deletions
7
assets/less/mio/classes/input/upload.less
Normal file
7
assets/less/mio/classes/input/upload.less
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.mio__input__upload {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
.mio__input__button();
|
||||||
|
}
|
||||||
|
}
|
20
assets/less/mio/classes/settings/avatar.less
Normal file
20
assets/less/mio/classes/settings/avatar.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,4 +2,8 @@
|
||||||
&--account {
|
&--account {
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--avatar {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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
62
composer.lock
generated
|
@ -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": []
|
||||||
}
|
}
|
||||||
|
|
11
misuzu.php
11
misuzu.php
|
@ -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
BIN
public/images/no-avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 71 KiB |
|
@ -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(
|
||||||
http_response_code(404);
|
$app->config->get('Avatar', 'default_path', 'string', 'public/images/no-avatar.png')
|
||||||
echo $app->templating->render('user.notfound');
|
);
|
||||||
return;
|
|
||||||
|
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');
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
47
utility.php
47
utility.php
|
@ -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;
|
||||||
|
|
|
@ -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 }}&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>
|
||||||
|
|
|
@ -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 }}&m=avatar')"></div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -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 }}&m=avatar');"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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 }}&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 %}
|
||||||
|
|
Loading…
Add table
Reference in a new issue