[AVATAR] Fixed avatar upload, added avatar inline download and updated template and base controller

This commit is contained in:
Hugo Sales 2020-08-08 16:10:25 +00:00 committed by Hugo Sales
parent 2bf914f96f
commit bd8f4bd277
Signed by: someonewithpc
GPG Key ID: 7D0C7EAFC9D835A0
13 changed files with 303 additions and 97 deletions

View File

@ -0,0 +1,60 @@
<?php
// {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
// }}}
namespace Component\Media\Controller;
use App\Core\Controller;
use App\Core\DB\DB;
use function App\Core\I18n\_m;
use App\Core\Log;
use App\Entity\Avatar as EAvatar;
use Component\Media\Media;
use Exception;
use Symfony\Component\HttpFoundation\Request;
class Avatar extends Controller
{
public function send(Request $request, string $nickname, string $size)
{
switch ($size) {
case 'full':
$result = DB::createQuery('select f.file_hash, f.mimetype, f.title from ' .
'App\\Entity\\File f join App\\Entity\\Avatar a with f.id = a.file_id ' .
'join App\\Entity\\Profile p with p.id = a.profile_id ' .
'where p.nickname = :nickname')
->setParameter('nickname', $nickname)
->getResult();
if (count($result) != 1) {
Log::error('Avatar query returned more than one result for nickname ' . $nickname);
throw new Exception(_m('Internal server error'));
}
$res = $result[0];
Media::sendFile(EAvatar::getFilePath($res['file_hash']), $res['mimetype'], $res['title']);
die();
// TODO FIX THIS
break;
default:
throw new Exception('Not implemented');
}
}
}

View File

@ -21,6 +21,8 @@ namespace Component\Media;
use App\Core\Module; use App\Core\Module;
use App\Entity\File; use App\Entity\File;
use App\Util\Common;
use App\Util\Nickname;
use Symfony\Component\HttpFoundation\File\File as SymfonyFile; use Symfony\Component\HttpFoundation\File\File as SymfonyFile;
class Media extends Module class Media extends Module
@ -42,4 +44,50 @@ class Media extends Module
// TODO Normalize file types // TODO Normalize file types
return $file; return $file;
} }
/**
* Include $filepath in the response, for viewing and downloading.
*
* @throws ServerException
*/
public static function sendFile(string $filepath, string $mimetype, string $output_filename, string $disposition = 'inline'): void
{
$x_delivery = Common::config('site', 'x_static_delivery');
if (is_string($x_delivery)) {
$tmp = explode(INSTALLDIR, $filepath);
$relative_path = end($tmp);
Log::debug("Using Static Delivery with header for: {$relative_path}");
header("{$x_delivery}: {$relative_path}");
} else {
if (file_exists($filepath)) {
header('Content-Description: File Transfer');
header("Content-Type: {$mimetype}");
header("Content-Disposition: {$disposition}; filename=\"{$output_filename}\"");
header('Expires: 0');
header('Content-Transfer-Encoding: binary');
$filesize = filesize($filepath);
http_response_code(200);
header("Content-Length: {$filesize}");
// header('Cache-Control: private, no-transform, no-store, must-revalidate');
$ret = @readfile($filepath);
if ($ret === false) {
http_response_code(404);
Log::error("Couldn't read file at {$filepath}.");
} elseif ($ret !== $filesize) {
http_response_code(500);
Log::error('The lengths of the file as recorded on the DB (or on disk) for the file ' .
"{$filepath} differ from what was sent to the user ({$filesize} vs {$ret}).");
}
}
}
}
public function onAddRoute($r)
{
$r->connect('avatar', '/{nickname<' . Nickname::DISPLAY_FMT . '>}/avatar/{size<full|big|medium|small>?full}', [Controller\Avatar::class, 'send']);
}
} }

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="141.11111mm" height="141.11111mm"
viewBox="0 0 499.99998 500.00005" id="svg2" version="1.1" inkscape:version="0.91 r13725"
inkscape:export-filename="/home/alberto/NewGS edited.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90"
sodipodi:docname="GS avatar Final.svg">
<defs id="defs4"/>
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.35355339" inkscape:cx="-611.42805" inkscape:cy="426.03658" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" inkscape:window-width="1366" inkscape:window-height="716" inkscape:window-x="0" inkscape:window-y="29" inkscape:window-maximized="1" showguides="false">
<inkscape:grid type="xygrid" id="grid4146"/>
</sodipodi:namedview>
<metadata id="metadata7">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g inkscape:label="Capa 1" inkscape:groupmode="layer" id="layer1" transform="translate(-157.14286,-79.50503)">
<rect style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect4685" width="500.00009" height="500.00009" x="157.14281" y="79.50502"/>
<g id="g5206" transform="translate(5.0010184,16.878838)">
<path id="path5202" transform="translate(157.14281,79.50503)" d="m 156.0957,58.869141 c -2.68025,0.0382 -5.40172,0.373422 -8.12109,0.976562 -4.27851,0.94895 -8.60777,2.588306 -12.59766,4.853516 -4.01887,2.281669 -7.73517,5.2222 -10.96484,8.724609 -3.23012,3.50289 -5.98354,7.539503 -8.00391,12.039063 -2.01857,4.495529 -3.38338,9.436829 -3.87695,14.667969 -0.50485,5.68236 -0.0729,12.79202 2.70703,20.16016 1.36562,3.61951 3.29345,7.34391 5.90625,10.94531 2.61146,3.59956 6.03849,7.0071 10.35742,10.1289 9.53878,6.89481 23.71189,11.49816 41.58399,13.83985 -5.91744,3.3848 -10.93941,6.88738 -14.68555,10.24804 -5.021,-4.706 -14.55133,-11.82251 -27.82031,-22.85937 -23.77204,-19.77309 -82.709577,-23.05953 -82.734377,-14.14063 -0.0278,10.01879 15.25625,50.60502 63.740237,43.14649 20.31295,-3.12485 31.72368,-0.31868 39.21484,1.29492 -19.37347,20.97225 -43.38563,52.45365 -31.125,87.50977 32.5947,93.19612 56.11218,146.97265 125.32617,146.97265 69.214,0 94.53219,-53.33948 125.32032,-146.97265 11.60063,-35.28002 -11.74959,-66.53752 -31.12305,-87.50977 7.49115,-1.6136 18.90189,-4.41977 39.21484,-1.29492 48.48399,7.45853 63.76615,-33.1277 63.73828,-43.14649 -0.0248,-8.9189 -58.96038,-5.63246 -82.73242,14.14063 -13.26898,11.03686 -22.80127,18.15337 -27.82226,22.85937 -2.58848,-3.42164 -7.1092,-7.00722 -12.95313,-10.4707 17.06416,-2.40798 30.62176,-6.9443 39.85352,-13.61719 4.31893,-3.1218 7.74401,-6.52934 10.35547,-10.1289 2.6128,-3.6014 4.54258,-7.3258 5.9082,-10.94531 2.77992,-7.36814 3.20993,-14.4778 2.70508,-20.16016 -0.49357,-5.23114 -1.85643,-10.17244 -3.875,-14.667969 -2.02037,-4.49956 -4.77379,-8.536173 -8.00391,-12.039063 -3.22967,-3.502409 -6.94793,-6.44294 -10.9668,-8.724609 -3.98989,-2.26521 -8.31719,-3.904566 -12.5957,-4.853516 -17.40396,-3.860099 -34.92622,3.313482 -40.78711,28.380859 1.6307,-4.282299 8.33903,-17.124026 24.52539,-17.878906 9.97855,-0.46537 17.73729,4.163357 22.68555,10.416016 4.89527,6.185699 6.47977,14.299742 5.63281,20.169918 -0.97936,6.7878 -5.99826,25.42473 -34.45508,24.58789 -18.69599,-0.5498 -35.3627,-18.64648 -58.92383,-18.64648 -7.42488,0 -12.3863,2.29126 -15.70312,5.42773 -3.31682,-3.13647 -8.27824,-5.42773 -15.70312,-5.42773 -23.56113,0 -40.22784,18.09668 -58.92383,18.64648 -28.45682,0.83684 -33.47572,-17.80009 -34.45508,-24.58789 -0.84696,-5.870176 0.73754,-13.984219 5.63281,-20.169918 4.94826,-6.252659 12.707,-10.881386 22.68555,-10.416016 16.18636,0.75488 22.89469,13.596607 24.52539,17.878906 C 183.81659,67.075965 170.56906,58.662871 156.0957,58.869141 Z" style="display:inline;fill:#a22430;fill-opacity:0.85098039;stroke:none" inkscape:connector-curvature="0"/>
<path inkscape:connector-curvature="0" id="path5157" d="m 313.23851,138.37417 c -2.68025,0.0382 -5.40172,0.37342 -8.12109,0.97656 -4.27851,0.94895 -8.60777,2.58831 -12.59766,4.85352 -4.01887,2.28167 -7.73517,5.2222 -10.96484,8.72461 -3.23012,3.50289 -5.98354,7.5395 -8.00391,12.03906 -2.01857,4.49553 -3.38338,9.43683 -3.87695,14.66797 -0.50485,5.68236 -0.0729,12.79202 2.70703,20.16016 1.36562,3.61951 3.29345,7.34391 5.90625,10.94531 2.61146,3.59956 6.03849,7.0071 10.35742,10.1289 14.838,10.72518 40.83724,15.93497 74.64063,15.65039 21.51239,-0.1811 32.86693,-6.81751 38.85742,-13.4746 5.99049,6.65709 17.34503,13.2935 38.85742,13.4746 33.80339,0.28458 59.80263,-4.92521 74.64063,-15.65039 4.31893,-3.1218 7.74401,-6.52934 10.35547,-10.1289 2.6128,-3.6014 4.54258,-7.3258 5.9082,-10.94531 2.77992,-7.36814 3.20993,-14.4778 2.70508,-20.16016 -0.49357,-5.23114 -1.85643,-10.17244 -3.875,-14.66797 -2.02037,-4.49956 -4.77379,-8.53617 -8.00391,-12.03906 -3.22967,-3.50241 -6.94793,-6.44294 -10.9668,-8.72461 -3.98989,-2.26521 -8.31719,-3.90457 -12.5957,-4.85352 -17.40396,-3.8601 -34.92622,3.31348 -40.78711,28.38086 1.6307,-4.2823 8.33903,-17.12402 24.52539,-17.8789 9.97855,-0.46537 17.73729,4.16335 22.68555,10.41601 4.89527,6.1857 6.47977,14.29974 5.63281,20.16992 -0.97936,6.7878 -5.99826,25.42473 -34.45508,24.58789 -18.696,-0.5498 -35.3627,-18.64648 -58.92383,-18.64648 -7.42489,0 -12.3863,2.29126 -15.70312,5.42773 -3.31682,-3.13647 -8.27823,-5.42773 -15.70312,-5.42773 -23.56113,0 -40.22783,18.09668 -58.92383,18.64648 -28.45682,0.83684 -33.47572,-17.80009 -34.45508,-24.58789 -0.84696,-5.87018 0.73754,-13.98422 5.63281,-20.16992 4.94826,-6.25266 12.707,-10.88138 22.68555,-10.41601 16.18636,0.75488 22.89469,13.5966 24.52539,17.8789 -4.94513,-21.1506 -18.19266,-29.56369 -32.66602,-29.35742 z" style="display:inline;fill:#a22430;fill-opacity:0.85098039;stroke:none"/>
<path sodipodi:nodetypes="csccc" style="opacity:1;fill:#a22430;fill-opacity:0.85098039;fill-rule:nonzero;stroke:none;stroke-width:3.67872214;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m 316.40417,339.90855 c 32.59469,93.19613 16.53711,145.08383 85.75112,145.08383 69.214,0 54.93762,-51.45065 85.72574,-145.08383 3.04616,-57.94864 -4.43738,-62.72475 -85.73722,-62.35823 -76.96659,-0.36652 -88.47651,3.8032 -85.73964,62.35823 z" id="path4152" inkscape:connector-curvature="0"/>
<ellipse ry="41.798668" rx="40.000877" cy="318.71783" cx="451.22238" id="path4139" style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
<ellipse ry="41.798668" rx="40.000877" cy="318.71783" cx="353.06296" id="ellipse4143" style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
<circle r="13.983542" cy="318.71783" cx="355.13037" id="path4671" style="opacity:1;fill:#a22430;fill-opacity:0.85022027;fill-rule:nonzero;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
<circle style="opacity:1;fill:#a22430;fill-opacity:0.85022027;fill-rule:nonzero;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="circle4673" cx="449.15485" cy="318.71783" r="13.983542"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -38,6 +38,7 @@ use App\Core\Event;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Log; use App\Core\Log;
use App\Entity\Avatar;
use App\Entity\File; use App\Entity\File;
use App\Util\ClientException; use App\Util\ClientException;
use App\Util\Common; use App\Util\Common;
@ -102,16 +103,16 @@ class UserPanel extends AbstractController
public function avatar(Request $request) public function avatar(Request $request)
{ {
$avatar = Form::create([ $form = Form::create([
['avatar', FileType::class, ['label' => _m('Avatar'), 'help' => _m('You can upload your personal avatar. The maximum file size is 2MB.')]], ['avatar', FileType::class, ['label' => _m('Avatar'), 'help' => _m('You can upload your personal avatar. The maximum file size is 2MB.')]],
['hidden', HiddenType::class, []], ['hidden', HiddenType::class, []],
['save', SubmitType::class, ['label' => _m('Submit')]], ['save', SubmitType::class, ['label' => _m('Submit')]],
]); ]);
$avatar->handleRequest($request); $form->handleRequest($request);
if ($avatar->isSubmitted() && $avatar->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$data = $avatar->getData(); $data = $form->getData();
$sfile = $file_title = null; $sfile = $file_title = null;
if (isset($data['hidden'])) { if (isset($data['hidden'])) {
// Cropped client side // Cropped client side
@ -120,13 +121,13 @@ class UserPanel extends AbstractController
list(, $mimetype_user, , $encoding_user, $data_user) = $matches; list(, $mimetype_user, , $encoding_user, $data_user) = $matches;
if ($encoding_user == 'base64') { if ($encoding_user == 'base64') {
$data_user = base64_decode($data_user); $data_user = base64_decode($data_user);
$tmp_file = tmpfile(); $filename = tempnam('/tmp/', 'avatar');
fwrite($tmp_file, $data_user); file_put_contents($filename, $data_user);
try { try {
$sfile = new SymfonyFile(stream_get_meta_data($tmp_file)['uri']); $sfile = new SymfonyFile($filename);
$file_title = $data['avatar']->getFilename(); $file_title = $data['avatar']->getFilename();
} finally { } finally {
fclose($tmp_file); // fclose($tmp_file);
} }
} else { } else {
Log::info('Avatar upload got an invalid encoding, something\'s fishy and/or wrong'); Log::info('Avatar upload got an invalid encoding, something\'s fishy and/or wrong');
@ -138,21 +139,25 @@ class UserPanel extends AbstractController
} else { } else {
throw new ClientException('Invalid form'); throw new ClientException('Invalid form');
} }
$profile_id = Common::profile()->getId();
$file = Media::validateAndStoreFile($sfile, Common::config('avatar', 'dir'), $file_title);
$avatar = null;
try { try {
$profile_id = Common::user()->getProfile()->getId(); $avatar = DB::find('avatar', ['profile_id' => $profile_id]);
$file = Media::validateAndStoreFile($sfile, Common::config('avatar', 'dir'), $file_title);
$fs_files_to_delete = DB::find('avatar', ['profile_id' => $profile_id])->delete();
DB::persist(Avatar::create(['profile_id' => $profile_id, 'file_id' => $file->getId()]));
DB::persist($file);
DB::flush();
// Only delete files if the commit went through
File::deleteFiles($fs_files_to_delete);
} catch (Exception $e) { } catch (Exception $e) {
throw $e;
} }
if ($avatar != null) {
$avatar->delete();
} else {
DB::persist($file);
DB::persist(Avatar::create(['profile_id' => $profile_id, 'file_id' => $file->getId()]));
}
DB::flush();
// Only delete files if the commit went through
File::deleteFiles($fs_files_to_delete ?? []);
} }
return ['_template' => 'settings/avatar.html.twig', 'avatar' => $avatar->createView()]; return ['_template' => 'settings/avatar.html.twig', 'avatar' => $form->createView()];
} }
public function notifications(Request $request) public function notifications(Request $request)

View File

@ -32,6 +32,8 @@
namespace App\Core; namespace App\Core;
use App\Core\DB\DB;
use App\Util\Common;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
@ -60,7 +62,13 @@ class Controller extends AbstractController implements EventSubscriberInterface
$controller = $event->getController(); $controller = $event->getController();
$request = $event->getRequest(); $request = $event->getRequest();
$this->vars = ['controler' => $controller, 'request' => $request]; if (($avatar = DB::find('avatar', ['profile_id' => Common::profile()->getId()])) != null) {
$avatar_filename = $avatar->getUrl();
} else {
$avatar_filename = '/public/assets/default_avatar.svg';
}
$this->vars = ['controler' => $controller, 'request' => $request, 'user_avatar' => $avatar_filename];
Event::handle('StartTwigPopulateVars', [&$this->vars]); Event::handle('StartTwigPopulateVars', [&$this->vars]);
return $event; return $event;

View File

@ -23,6 +23,7 @@ namespace App\Core;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Util\Formatting; use App\Util\Formatting;
use DateTime;
class Entity class Entity
{ {
@ -33,7 +34,8 @@ class Entity
$args['created'] = $args['modified'] = new DateTime(); $args['created'] = $args['modified'] = new DateTime();
foreach ($args as $prop => $val) { foreach ($args as $prop => $val) {
if (property_exists($class, $prop)) { if (property_exists($class, $prop)) {
$obj->{$prop} = $val; $set = 'set' . Formatting::snakeCaseToCamelCase($prop);
$obj->{$set}($val);
} else { } else {
Log::error("Property {$class}::{$prop} doesn't exist"); Log::error("Property {$class}::{$prop} doesn't exist");
} }

View File

@ -58,6 +58,7 @@ use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Security as SSecurity; use Symfony\Component\Security\Core\Security as SSecurity;
use Symfony\Component\Security\Http\Util\TargetPathTrait; use Symfony\Component\Security\Http\Util\TargetPathTrait;
@ -72,6 +73,7 @@ class GNUsocial implements EventSubscriberInterface
protected TranslatorInterface $translator; protected TranslatorInterface $translator;
protected EntityManagerInterface $entity_manager; protected EntityManagerInterface $entity_manager;
protected RouterInterface $router; protected RouterInterface $router;
protected UrlGeneratorInterface $url_generator;
protected FormFactoryInterface $form_factory; protected FormFactoryInterface $form_factory;
protected MessageBusInterface $message_bus; protected MessageBusInterface $message_bus;
protected EventDispatcherInterface $event_dispatcher; protected EventDispatcherInterface $event_dispatcher;
@ -87,6 +89,7 @@ class GNUsocial implements EventSubscriberInterface
TranslatorInterface $trans, TranslatorInterface $trans,
EntityManagerInterface $em, EntityManagerInterface $em,
RouterInterface $router, RouterInterface $router,
UrlGeneratorInterface $url_gen,
FormFactoryInterface $ff, FormFactoryInterface $ff,
MessageBusInterface $mb, MessageBusInterface $mb,
EventDispatcherInterface $ed, EventDispatcherInterface $ed,
@ -99,6 +102,7 @@ class GNUsocial implements EventSubscriberInterface
$this->translator = $trans; $this->translator = $trans;
$this->entity_manager = $em; $this->entity_manager = $em;
$this->router = $router; $this->router = $router;
$this->url_generator = $url_gen;
$this->form_factory = $ff; $this->form_factory = $ff;
$this->message_bus = $mb; $this->message_bus = $mb;
$this->event_dispatcher = $ed; $this->event_dispatcher = $ed;
@ -126,6 +130,7 @@ class GNUsocial implements EventSubscriberInterface
Queue::setMessageBus($this->message_bus); Queue::setMessageBus($this->message_bus);
Security::setHelper($this->security); Security::setHelper($this->security);
Mailer::setMailer($this->mailer); Mailer::setMailer($this->mailer);
Router::setRouter($this->router, $this->url_generator);
DefaultSettings::setDefaults(); DefaultSettings::setDefaults();
@ -134,8 +139,6 @@ class GNUsocial implements EventSubscriberInterface
// Events are proloaded on compilation, but set at runtime // Events are proloaded on compilation, but set at runtime
$this->module_manager->loadModules(); $this->module_manager->loadModules();
Router::setRouter($this->router);
$this->initialized = true; $this->initialized = true;
} }
} }

View File

@ -30,15 +30,46 @@
namespace App\Core\Router; namespace App\Core\Router;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\Router as SRouter; use Symfony\Component\Routing\Router as SRouter;
abstract class Router abstract class Router
{ {
public static ?SRouter $router = null; /**
* Generates an absolute URL, e.g. "http://example.com/dir/file".
*/
const ABSOLUTE_URL = UrlGeneratorInterface::ABSOLUTE_URL;
public static function setRouter($rtr): void /**
* Generates an absolute path, e.g. "/dir/file".
*/
const ABSOLUTE_PATH = UrlGeneratorInterface::ABSOLUTE_PATH;
/**
* Generates a relative path based on the current request path, e.g. "../parent-file".
*
* @see UrlGenerator::getRelativePath()
*/
const RELATIVE_PATH = UrlGeneratorInterface::RELATIVE_PATH;
/**
* Generates a network path, e.g. "//example.com/dir/file".
* Such reference reuses the current scheme but specifies the host.
*/
const NETWORK_PATH = UrlGeneratorInterface::NETWORK_PATH;
public static ?SRouter $router = null;
public static ?UrlGeneratorInterface $url_gen = null;
public static function setRouter($rtr, $gen): void
{ {
self::$router = $rtr; self::$router = $rtr;
self::$url_gen = $gen;
}
public static function url(string $id, array $args, int $type = self::ABSOLUTE_PATH): string
{
return self::$url_gen->generate($id, $args, $type);
} }
public static function __callStatic(string $name, array $args) public static function __callStatic(string $name, array $args)

View File

@ -21,6 +21,8 @@ namespace App\Entity;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Core\Router\Router;
use App\Util\Common;
use DateTimeInterface; use DateTimeInterface;
/** /**
@ -42,9 +44,6 @@ class Avatar extends Entity
// {{{ Autocode // {{{ Autocode
private int $profile_id; private int $profile_id;
private int $width;
private int $height;
private ?bool $is_original;
private int $file_id; private int $file_id;
private \DateTimeInterface $created; private \DateTimeInterface $created;
private \DateTimeInterface $modified; private \DateTimeInterface $modified;
@ -54,46 +53,18 @@ class Avatar extends Entity
$this->profile_id = $profile_id; $this->profile_id = $profile_id;
return $this; return $this;
} }
public function getProfileId(): int public function getProfileId(): int
{ {
return $this->profile_id; return $this->profile_id;
} }
public function setWidth(int $width): self
{
$this->width = $width;
return $this;
}
public function getWidth(): int
{
return $this->width;
}
public function setHeight(int $height): self
{
$this->height = $height;
return $this;
}
public function getHeight(): int
{
return $this->height;
}
public function setIsOriginal(?bool $is_original): self
{
$this->is_original = $is_original;
return $this;
}
public function getIsOriginal(): ?bool
{
return $this->is_original;
}
public function setFileId(int $file_id): self public function setFileId(int $file_id): self
{ {
$this->file_id = $file_id; $this->file_id = $file_id;
return $this; return $this;
} }
public function getFileId(): int public function getFileId(): int
{ {
return $this->file_id; return $this->file_id;
@ -104,6 +75,7 @@ class Avatar extends Entity
$this->created = $created; $this->created = $created;
return $this; return $this;
} }
public function getCreated(): DateTimeInterface public function getCreated(): DateTimeInterface
{ {
return $this->created; return $this->created;
@ -114,6 +86,7 @@ class Avatar extends Entity
$this->modified = $modified; $this->modified = $modified;
return $this; return $this;
} }
public function getModified(): DateTimeInterface public function getModified(): DateTimeInterface
{ {
return $this->modified; return $this->modified;
@ -123,18 +96,20 @@ class Avatar extends Entity
private ?File $file = null; private ?File $file = null;
public function getUrl(): string
{
return Router::url('avatar', ['nickname' => Profile::getNicknameFromId($this->profile_id)]);
}
public function getFile(): File public function getFile(): File
{ {
$this->file = $this->file ?: DB::find('file', ['id' => $this->file_id]); $this->file = $this->file ?: DB::find('file', ['id' => $this->file_id]);
return $this->file; return $this->file;
} }
public function getFilePath(): string public function getFilePath(?string $filename = null): string
{ {
$file_name = $this->getFile()->getFileName(); return Common::config('avatar', 'dir') . '/' . $filename ?: $this->getFile()->getFileName();
if ($this->is_original) {
return Common::config('avatar', 'dir') . '/' . $file_name;
}
} }
/** /**
@ -146,7 +121,7 @@ class Avatar extends Entity
if (!$cascading) { if (!$cascading) {
$files = $this->getFile()->delete($cascade = true, $file_flush = false, $delete_files_now); $files = $this->getFile()->delete($cascade = true, $file_flush = false, $delete_files_now);
} else { } else {
DB::remove(DB::getReference('avatar', ['profile_id' => $this->profile_id, 'width' => $this->width, 'height' => $this->height])); DB::remove(DB::getReference('avatar', ['profile_id' => $this->profile_id]));
$file_path = $this->getFilePath(); $file_path = $this->getFilePath();
$files[] = $file_path; $files[] = $file_path;
if ($flush) { if ($flush) {

View File

@ -19,6 +19,7 @@
namespace App\Entity; namespace App\Entity;
use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use DateTimeInterface; use DateTimeInterface;
@ -57,6 +58,7 @@ class File extends Entity
$this->id = $id; $this->id = $id;
return $this; return $this;
} }
public function getId(): int public function getId(): int
{ {
return $this->id; return $this->id;
@ -67,6 +69,7 @@ class File extends Entity
$this->url = $url; $this->url = $url;
return $this; return $this;
} }
public function getUrl(): ?string public function getUrl(): ?string
{ {
return $this->url; return $this->url;
@ -77,6 +80,7 @@ class File extends Entity
$this->is_url_protected = $is_url_protected; $this->is_url_protected = $is_url_protected;
return $this; return $this;
} }
public function getIsUrlProtected(): ?bool public function getIsUrlProtected(): ?bool
{ {
return $this->is_url_protected; return $this->is_url_protected;
@ -87,6 +91,7 @@ class File extends Entity
$this->url_hash = $url_hash; $this->url_hash = $url_hash;
return $this; return $this;
} }
public function getUrlHash(): ?string public function getUrlHash(): ?string
{ {
return $this->url_hash; return $this->url_hash;
@ -97,6 +102,7 @@ class File extends Entity
$this->file_hash = $file_hash; $this->file_hash = $file_hash;
return $this; return $this;
} }
public function getFileHash(): ?string public function getFileHash(): ?string
{ {
return $this->file_hash; return $this->file_hash;
@ -107,6 +113,7 @@ class File extends Entity
$this->mimetype = $mimetype; $this->mimetype = $mimetype;
return $this; return $this;
} }
public function getMimetype(): ?string public function getMimetype(): ?string
{ {
return $this->mimetype; return $this->mimetype;
@ -117,6 +124,7 @@ class File extends Entity
$this->size = $size; $this->size = $size;
return $this; return $this;
} }
public function getSize(): ?int public function getSize(): ?int
{ {
return $this->size; return $this->size;
@ -127,6 +135,7 @@ class File extends Entity
$this->title = $title; $this->title = $title;
return $this; return $this;
} }
public function getTitle(): ?string public function getTitle(): ?string
{ {
return $this->title; return $this->title;
@ -137,6 +146,7 @@ class File extends Entity
$this->timestamp = $timestamp; $this->timestamp = $timestamp;
return $this; return $this;
} }
public function getTimestamp(): ?int public function getTimestamp(): ?int
{ {
return $this->timestamp; return $this->timestamp;
@ -147,6 +157,7 @@ class File extends Entity
$this->is_local = $is_local; $this->is_local = $is_local;
return $this; return $this;
} }
public function getIsLocal(): ?bool public function getIsLocal(): ?bool
{ {
return $this->is_local; return $this->is_local;
@ -157,6 +168,7 @@ class File extends Entity
$this->modified = $modified; $this->modified = $modified;
return $this; return $this;
} }
public function getModified(): DateTimeInterface public function getModified(): DateTimeInterface
{ {
return $this->modified; return $this->modified;
@ -180,9 +192,11 @@ class File extends Entity
$files = []; $files = [];
if ($cascade) { if ($cascade) {
// An avatar can own a file, and it becomes invalid if the file is deleted // An avatar can own a file, and it becomes invalid if the file is deleted
$avatar = DB::find('avatar', ['file_id' => $this->id]); $avatar = DB::findBy('avatar', ['file_id' => $this->id]);
$files[] = $avatar->getFilePath(); foreach ($avatar as $a) {
$avatar->delete($flush, $delete_files_now, $cascading = true); $files[] = $a->getFilePath();
$a->delete($flush, $delete_files_now, $cascading = true);
}
foreach (DB::findBy('file_thumbnail', ['file_id' => $this->id]) as $ft) { foreach (DB::findBy('file_thumbnail', ['file_id' => $this->id]) as $ft) {
$files[] = $ft->delete($flush, $delete_files_now, $cascading); $files[] = $ft->delete($flush, $delete_files_now, $cascading);
} }

View File

@ -20,8 +20,8 @@
namespace App\Entity; namespace App\Entity;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Entity;
use App\Core\UserRoles; use App\Core\UserRoles;
use DateTime;
use DateTimeInterface; use DateTimeInterface;
use Functional as F; use Functional as F;
@ -39,7 +39,7 @@ use Functional as F;
* @copyright 2020 Free Software Foundation, Inc http://www.fsf.org * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/ */
class Profile class Profile extends Entity
{ {
// {{{ Autocode // {{{ Autocode
@ -62,6 +62,7 @@ class Profile
$this->id = $id; $this->id = $id;
return $this; return $this;
} }
public function getId(): int public function getId(): int
{ {
return $this->id; return $this->id;
@ -72,6 +73,7 @@ class Profile
$this->nickname = $nickname; $this->nickname = $nickname;
return $this; return $this;
} }
public function getNickname(): string public function getNickname(): string
{ {
return $this->nickname; return $this->nickname;
@ -82,6 +84,7 @@ class Profile
$this->fullname = $fullname; $this->fullname = $fullname;
return $this; return $this;
} }
public function getFullname(): ?string public function getFullname(): ?string
{ {
return $this->fullname; return $this->fullname;
@ -92,6 +95,7 @@ class Profile
$this->roles = $roles; $this->roles = $roles;
return $this; return $this;
} }
public function getRoles(): int public function getRoles(): int
{ {
return $this->roles; return $this->roles;
@ -102,6 +106,7 @@ class Profile
$this->homepage = $homepage; $this->homepage = $homepage;
return $this; return $this;
} }
public function getHomepage(): ?string public function getHomepage(): ?string
{ {
return $this->homepage; return $this->homepage;
@ -112,6 +117,7 @@ class Profile
$this->bio = $bio; $this->bio = $bio;
return $this; return $this;
} }
public function getBio(): ?string public function getBio(): ?string
{ {
return $this->bio; return $this->bio;
@ -122,6 +128,7 @@ class Profile
$this->location = $location; $this->location = $location;
return $this; return $this;
} }
public function getLocation(): ?string public function getLocation(): ?string
{ {
return $this->location; return $this->location;
@ -132,6 +139,7 @@ class Profile
$this->lat = $lat; $this->lat = $lat;
return $this; return $this;
} }
public function getLat(): ?float public function getLat(): ?float
{ {
return $this->lat; return $this->lat;
@ -142,6 +150,7 @@ class Profile
$this->lon = $lon; $this->lon = $lon;
return $this; return $this;
} }
public function getLon(): ?float public function getLon(): ?float
{ {
return $this->lon; return $this->lon;
@ -152,6 +161,7 @@ class Profile
$this->location_id = $location_id; $this->location_id = $location_id;
return $this; return $this;
} }
public function getLocationId(): ?int public function getLocationId(): ?int
{ {
return $this->location_id; return $this->location_id;
@ -162,6 +172,7 @@ class Profile
$this->location_service = $location_service; $this->location_service = $location_service;
return $this; return $this;
} }
public function getLocationService(): ?int public function getLocationService(): ?int
{ {
return $this->location_service; return $this->location_service;
@ -172,6 +183,7 @@ class Profile
$this->created = $created; $this->created = $created;
return $this; return $this;
} }
public function getCreated(): DateTimeInterface public function getCreated(): DateTimeInterface
{ {
return $this->created; return $this->created;
@ -182,6 +194,7 @@ class Profile
$this->modified = $modified; $this->modified = $modified;
return $this; return $this;
} }
public function getModified(): DateTimeInterface public function getModified(): DateTimeInterface
{ {
return $this->modified; return $this->modified;
@ -189,13 +202,39 @@ class Profile
// }}} Autocode // }}} Autocode
public function __construct(string $nickname) public function getFromId(int $id): ?self
{ {
$this->nickname = $nickname; return DB::find('profile', ['id' => $id]);
}
// TODO auto update created and modified public function getFromNickname(string $nickname): ?self
$this->created = new DateTime(); {
$this->modified = new DateTime(); return DB::findOneBy('profile', ['nickname' => $nickname]);
}
public static function getNicknameFromId(int $id): string
{
return self::getFromId($id)->getNickname();
}
public function getSelfTags(): array
{
return DB::findBy('profile_tag', ['tagger' => $this->id, 'tagged' => $this->id]);
}
public function setSelfTags(array $tags, array $pt_existing): void
{
$tag_existing = F\map($pt_existing, function ($pt) { return $pt->getTag(); });
$tag_to_add = array_diff($tags, $tag_existing);
$tag_to_remove = array_diff($tag_existing, $tags);
$pt_to_remove = F\filter($pt_existing, function ($pt) use ($tag_to_remove) { return in_array($pt->getTag(), $tag_to_remove); });
foreach ($tag_to_add as $tag) {
$pt = new ProfileTag($this->id, $this->id, $tag);
DB::persist($pt);
}
foreach ($pt_to_remove as $pt) {
DB::remove($pt);
}
} }
public static function schemaDef(): array public static function schemaDef(): array
@ -230,24 +269,4 @@ class Profile
return $def; return $def;
} }
public function getSelfTags(): array
{
return DB::findBy('profile_tag', ['tagger' => $this->id, 'tagged' => $this->id]);
}
public function setSelfTags(array $tags, array $pt_existing): void
{
$tag_existing = F\map($pt_existing, function ($pt) { return $pt->getTag(); });
$tag_to_add = array_diff($tags, $tag_existing);
$tag_to_remove = array_diff($tag_existing, $tags);
$pt_to_remove = F\filter($pt_existing, function ($pt) use ($tag_to_remove) { return in_array($pt->getTag(), $tag_to_remove); });
foreach ($tag_to_add as $tag) {
$pt = new ProfileTag($this->id, $this->id, $tag);
DB::persist($pt);
}
foreach ($pt_to_remove as $pt) {
DB::remove($pt);
}
}
} }

View File

@ -35,8 +35,9 @@ namespace App\Util;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Router; use App\Core\Router;
use App\Core\Security; use App\Core\Security;
use App\Entity\LocalUser;
use App\Entity\Profile;
use Functional as F; use Functional as F;
use Symfony\Component\Security\Core\User\UserInterface;
abstract class Common abstract class Common
{ {
@ -69,11 +70,16 @@ abstract class Common
DB::flush(); DB::flush();
} }
public static function user(): UserInterface public static function user(): LocalUser
{ {
return Security::getUser(); return Security::getUser();
} }
public static function profile(): Profile
{
return self::user()->getProfile();
}
/** /**
* Is the given string identical to a system path or route? * Is the given string identical to a system path or route?
* This could probably be put in some other class, but at * This could probably be put in some other class, but at

View File

@ -17,7 +17,7 @@
<div class='navbar'> <div class='navbar'>
<div class="left-nav"> <div class="left-nav">
<div class='profile'> <div class='profile'>
<img src='{{ asset('assets/image.png') }}' alt="Your avatar." class="icon icon-avatar"> <img src='{{ user_avatar }}' alt="Your avatar." class="icon icon-avatar">
<div class="info"> <div class="info">
<b id="nick">{{ app.user.username }}</b> <b id="nick">{{ app.user.username }}</b>
<div class="tags"> <div class="tags">