[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
No known key found for this signature in database
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\Entity\File;
use App\Util\Common;
use App\Util\Nickname;
use Symfony\Component\HttpFoundation\File\File as SymfonyFile;
class Media extends Module
@ -42,4 +44,50 @@ class Media extends Module
// TODO Normalize file types
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 function App\Core\I18n\_m;
use App\Core\Log;
use App\Entity\Avatar;
use App\Entity\File;
use App\Util\ClientException;
use App\Util\Common;
@ -102,16 +103,16 @@ class UserPanel extends AbstractController
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.')]],
['hidden', HiddenType::class, []],
['save', SubmitType::class, ['label' => _m('Submit')]],
]);
$avatar->handleRequest($request);
$form->handleRequest($request);
if ($avatar->isSubmitted() && $avatar->isValid()) {
$data = $avatar->getData();
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$sfile = $file_title = null;
if (isset($data['hidden'])) {
// Cropped client side
@ -120,13 +121,13 @@ class UserPanel extends AbstractController
list(, $mimetype_user, , $encoding_user, $data_user) = $matches;
if ($encoding_user == 'base64') {
$data_user = base64_decode($data_user);
$tmp_file = tmpfile();
fwrite($tmp_file, $data_user);
$filename = tempnam('/tmp/', 'avatar');
file_put_contents($filename, $data_user);
try {
$sfile = new SymfonyFile(stream_get_meta_data($tmp_file)['uri']);
$sfile = new SymfonyFile($filename);
$file_title = $data['avatar']->getFilename();
} finally {
fclose($tmp_file);
// fclose($tmp_file);
}
} else {
Log::info('Avatar upload got an invalid encoding, something\'s fishy and/or wrong');
@ -138,21 +139,25 @@ class UserPanel extends AbstractController
} else {
throw new ClientException('Invalid form');
}
try {
$profile_id = Common::user()->getProfile()->getId();
$profile_id = Common::profile()->getId();
$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()]));
$avatar = null;
try {
$avatar = DB::find('avatar', ['profile_id' => $profile_id]);
} catch (Exception $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);
} catch (Exception $e) {
throw $e;
}
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)

View File

@ -32,6 +32,8 @@
namespace App\Core;
use App\Core\DB\DB;
use App\Util\Common;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
@ -60,7 +62,13 @@ class Controller extends AbstractController implements EventSubscriberInterface
$controller = $event->getController();
$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]);
return $event;

View File

@ -23,6 +23,7 @@ namespace App\Core;
use App\Core\DB\DB;
use App\Util\Formatting;
use DateTime;
class Entity
{
@ -33,7 +34,8 @@ class Entity
$args['created'] = $args['modified'] = new DateTime();
foreach ($args as $prop => $val) {
if (property_exists($class, $prop)) {
$obj->{$prop} = $val;
$set = 'set' . Formatting::snakeCaseToCamelCase($prop);
$obj->{$set}($val);
} else {
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\Mailer\MailerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Security as SSecurity;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
@ -72,6 +73,7 @@ class GNUsocial implements EventSubscriberInterface
protected TranslatorInterface $translator;
protected EntityManagerInterface $entity_manager;
protected RouterInterface $router;
protected UrlGeneratorInterface $url_generator;
protected FormFactoryInterface $form_factory;
protected MessageBusInterface $message_bus;
protected EventDispatcherInterface $event_dispatcher;
@ -87,6 +89,7 @@ class GNUsocial implements EventSubscriberInterface
TranslatorInterface $trans,
EntityManagerInterface $em,
RouterInterface $router,
UrlGeneratorInterface $url_gen,
FormFactoryInterface $ff,
MessageBusInterface $mb,
EventDispatcherInterface $ed,
@ -99,6 +102,7 @@ class GNUsocial implements EventSubscriberInterface
$this->translator = $trans;
$this->entity_manager = $em;
$this->router = $router;
$this->url_generator = $url_gen;
$this->form_factory = $ff;
$this->message_bus = $mb;
$this->event_dispatcher = $ed;
@ -126,6 +130,7 @@ class GNUsocial implements EventSubscriberInterface
Queue::setMessageBus($this->message_bus);
Security::setHelper($this->security);
Mailer::setMailer($this->mailer);
Router::setRouter($this->router, $this->url_generator);
DefaultSettings::setDefaults();
@ -134,8 +139,6 @@ class GNUsocial implements EventSubscriberInterface
// Events are proloaded on compilation, but set at runtime
$this->module_manager->loadModules();
Router::setRouter($this->router);
$this->initialized = true;
}
}

View File

@ -30,15 +30,46 @@
namespace App\Core\Router;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\Router as SRouter;
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::$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)

View File

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

View File

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

View File

@ -20,8 +20,8 @@
namespace App\Entity;
use App\Core\DB\DB;
use App\Core\Entity;
use App\Core\UserRoles;
use DateTime;
use DateTimeInterface;
use Functional as F;
@ -39,7 +39,7 @@ use Functional as F;
* @copyright 2020 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class Profile
class Profile extends Entity
{
// {{{ Autocode
@ -62,6 +62,7 @@ class Profile
$this->id = $id;
return $this;
}
public function getId(): int
{
return $this->id;
@ -72,6 +73,7 @@ class Profile
$this->nickname = $nickname;
return $this;
}
public function getNickname(): string
{
return $this->nickname;
@ -82,6 +84,7 @@ class Profile
$this->fullname = $fullname;
return $this;
}
public function getFullname(): ?string
{
return $this->fullname;
@ -92,6 +95,7 @@ class Profile
$this->roles = $roles;
return $this;
}
public function getRoles(): int
{
return $this->roles;
@ -102,6 +106,7 @@ class Profile
$this->homepage = $homepage;
return $this;
}
public function getHomepage(): ?string
{
return $this->homepage;
@ -112,6 +117,7 @@ class Profile
$this->bio = $bio;
return $this;
}
public function getBio(): ?string
{
return $this->bio;
@ -122,6 +128,7 @@ class Profile
$this->location = $location;
return $this;
}
public function getLocation(): ?string
{
return $this->location;
@ -132,6 +139,7 @@ class Profile
$this->lat = $lat;
return $this;
}
public function getLat(): ?float
{
return $this->lat;
@ -142,6 +150,7 @@ class Profile
$this->lon = $lon;
return $this;
}
public function getLon(): ?float
{
return $this->lon;
@ -152,6 +161,7 @@ class Profile
$this->location_id = $location_id;
return $this;
}
public function getLocationId(): ?int
{
return $this->location_id;
@ -162,6 +172,7 @@ class Profile
$this->location_service = $location_service;
return $this;
}
public function getLocationService(): ?int
{
return $this->location_service;
@ -172,6 +183,7 @@ class Profile
$this->created = $created;
return $this;
}
public function getCreated(): DateTimeInterface
{
return $this->created;
@ -182,6 +194,7 @@ class Profile
$this->modified = $modified;
return $this;
}
public function getModified(): DateTimeInterface
{
return $this->modified;
@ -189,13 +202,39 @@ class Profile
// }}} 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
$this->created = new DateTime();
$this->modified = new DateTime();
public function getFromNickname(string $nickname): ?self
{
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
@ -230,24 +269,4 @@ class Profile
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\Router;
use App\Core\Security;
use App\Entity\LocalUser;
use App\Entity\Profile;
use Functional as F;
use Symfony\Component\Security\Core\User\UserInterface;
abstract class Common
{
@ -69,11 +70,16 @@ abstract class Common
DB::flush();
}
public static function user(): UserInterface
public static function user(): LocalUser
{
return Security::getUser();
}
public static function profile(): Profile
{
return self::user()->getProfile();
}
/**
* Is the given string identical to a system path or route?
* This could probably be put in some other class, but at

View File

@ -17,7 +17,7 @@
<div class='navbar'>
<div class="left-nav">
<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">
<b id="nick">{{ app.user.username }}</b>
<div class="tags">