[ATTACHMENTS] Further refactoring
Some key points: - Components and Plugins shouldn't extend Module directly - Avatars should be fetched via GSActor ID, not by nickname as that isn't unique - Avatar now is a separate Component - Common file utilities are now to be placed in Core\GSFile, this will handle storage and trigger validation - Some bug fixes
This commit is contained in:
parent
cdef6858ce
commit
0eaccc32fe
122
components/Avatar/Avatar.php
Normal file
122
components/Avatar/Avatar.php
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
<?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\Avatar;
|
||||||
|
|
||||||
|
use App\Core\Cache;
|
||||||
|
use App\Core\DB\DB;
|
||||||
|
use App\Core\Event;
|
||||||
|
use App\Core\GSFile;
|
||||||
|
use App\Core\Modules\Component;
|
||||||
|
use App\Util\Common;
|
||||||
|
use Component\Avatar\Exception\NoAvatarException;
|
||||||
|
use Exception;
|
||||||
|
use Symfony\Component\Asset\Package;
|
||||||
|
use Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy;
|
||||||
|
|
||||||
|
class Avatar extends Component
|
||||||
|
{
|
||||||
|
public function onAddRoute($r)
|
||||||
|
{
|
||||||
|
$r->connect('avatar', '/{gsactor_id<\d+>}/avatar/{size<full|big|medium|small>?full}', [Controller\Avatar::class, 'avatar']);
|
||||||
|
return Event::next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onEndTwigPopulateVars(array &$vars)
|
||||||
|
{
|
||||||
|
if (Common::user() != null) {
|
||||||
|
$vars['user_avatar'] = self::getAvatarUrl();
|
||||||
|
}
|
||||||
|
return Event::next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onGetAvatarUrl(int $gsactor_id, ?string &$url)
|
||||||
|
{
|
||||||
|
$url = self::getAvatarUrl($gsactor_id);
|
||||||
|
return Event::next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onDeleteCachedAvatar(int $gsactor_id)
|
||||||
|
{
|
||||||
|
Cache::delete('avatar-' . $gsactor_id);
|
||||||
|
Cache::delete('avatar-url-' . $gsactor_id);
|
||||||
|
Cache::delete('avatar-file-info-' . $gsactor_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTILS ----------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the avatar associated with the given nickname
|
||||||
|
*/
|
||||||
|
public static function getAvatar(?int $gsactor_id = null): \App\Entity\Avatar
|
||||||
|
{
|
||||||
|
$gsactor_id = $gsactor_id ?: Common::userNickname();
|
||||||
|
return GSFile::error(NoAvatarException::class,
|
||||||
|
$gsactor_id,
|
||||||
|
Cache::get("avatar-{$gsactor_id}",
|
||||||
|
function () use ($gsactor_id) {
|
||||||
|
return DB::dql('select a from App\Entity\Avatar a ' .
|
||||||
|
'where a.gsactor_id = :gsactor_id',
|
||||||
|
['gsactor_id' => $gsactor_id]);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cached avatar associated with the given nickname, or the current user if not given
|
||||||
|
*/
|
||||||
|
public static function getAvatarUrl(?int $gsactor_id = null): string
|
||||||
|
{
|
||||||
|
$gsactor_id = $gsactor_id ?: Common::userId();
|
||||||
|
return Cache::get("avatar-url-{$gsactor_id}", function () use ($gsactor_id) {
|
||||||
|
try {
|
||||||
|
return self::getAvatar($gsactor_id)->getUrl();
|
||||||
|
} catch (NoAvatarException $e) {
|
||||||
|
}
|
||||||
|
$package = new Package(new EmptyVersionStrategy());
|
||||||
|
return $package->getUrl(Common::config('avatar', 'default'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cached avatar file info associated with the given GSActor id
|
||||||
|
*
|
||||||
|
* Returns the avatar file's hash, mimetype, title and path.
|
||||||
|
* Ensures exactly one cached value exists
|
||||||
|
*/
|
||||||
|
public static function getAvatarFileInfo(int $gsactor_id): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$res = GSFile::error(NoAvatarException::class,
|
||||||
|
$gsactor_id,
|
||||||
|
Cache::get("avatar-file-info-{$gsactor_id}",
|
||||||
|
function () use ($gsactor_id) {
|
||||||
|
return DB::dql('select f.file_hash, f.mimetype, f.title ' .
|
||||||
|
'from App\Entity\Attachment f ' .
|
||||||
|
'join App\Entity\Avatar a with f.id = a.attachment_id ' .
|
||||||
|
'where a.gsactor_id = :gsactor_id',
|
||||||
|
['gsactor_id' => $gsactor_id]);
|
||||||
|
}));
|
||||||
|
$res['file_path'] = \App\Entity\Avatar::getFilePathStatic($res['file_hash']);
|
||||||
|
return $res;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$filepath = INSTALLDIR . '/public/assets/default-avatar.svg';
|
||||||
|
return ['file_path' => $filepath, 'mimetype' => 'image/svg+xml', 'title' => null];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
components/Avatar/Controller/Avatar.php
Normal file
44
components/Avatar/Controller/Avatar.php
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<?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\Avatar\Controller;
|
||||||
|
|
||||||
|
use App\Core\Controller;
|
||||||
|
use App\Core\GSFile as M;
|
||||||
|
use Exception;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
class Avatar extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function avatar(Request $request, int $gsactor_id, string $size)
|
||||||
|
{
|
||||||
|
switch ($size) {
|
||||||
|
case 'full':
|
||||||
|
$res = \Component\Avatar\Avatar::getAvatarFileInfo($gsactor_id);
|
||||||
|
return M::sendFile($res['file_path'], $res['mimetype'], $res['title']);
|
||||||
|
default:
|
||||||
|
throw new Exception('Not implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
// }}}
|
// }}}
|
||||||
|
|
||||||
namespace Component\Media\Exception;
|
namespace Component\Avatar\Exception;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
|
@ -19,8 +19,8 @@
|
||||||
|
|
||||||
namespace Component\Bridge;
|
namespace Component\Bridge;
|
||||||
|
|
||||||
use App\Core\Modules\Module;
|
use App\Core\Modules\Component;
|
||||||
|
|
||||||
class Bridge extends Module
|
class Bridge extends Component
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,11 +21,11 @@ namespace Component\Left;
|
||||||
|
|
||||||
use App\Core\Event;
|
use App\Core\Event;
|
||||||
use App\Core\Log;
|
use App\Core\Log;
|
||||||
use App\Core\Modules\Module;
|
use App\Core\Modules\Component;
|
||||||
use App\Util\Common;
|
use App\Util\Common;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class Left extends Module
|
class Left extends Component
|
||||||
{
|
{
|
||||||
public function onEndTwigPopulateVars(array &$vars)
|
public function onEndTwigPopulateVars(array &$vars)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
<?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;
|
|
||||||
|
|
||||||
use App\Core\Cache;
|
|
||||||
use App\Core\Event;
|
|
||||||
use App\Core\Modules\Module;
|
|
||||||
use App\Util\Common;
|
|
||||||
use App\Util\Nickname;
|
|
||||||
|
|
||||||
class Media extends Module
|
|
||||||
{
|
|
||||||
public static function __callStatic(string $name, array $args)
|
|
||||||
{
|
|
||||||
return Utils::{$name}(...$args);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onAddRoute($r)
|
|
||||||
{
|
|
||||||
$r->connect('avatar', '/{nickname<' . Nickname::DISPLAY_FMT . '>}/avatar/{size<full|big|medium|small>?full}', [Controller\Media::class, 'avatar']);
|
|
||||||
$r->connect('attachment_inline', '/attachment/{id<\d+>}', [Controller\Media::class, 'attachment_inline']);
|
|
||||||
return Event::next;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onEndTwigPopulateVars(array &$vars)
|
|
||||||
{
|
|
||||||
if (Common::user() != null) {
|
|
||||||
$vars['user_avatar'] = self::getAvatarUrl();
|
|
||||||
}
|
|
||||||
return Event::next;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onGetAvatarUrl(string $nickname, ?string &$url)
|
|
||||||
{
|
|
||||||
$url = self::getAvatarUrl($nickname);
|
|
||||||
return Event::next;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onDeleteCachedAvatar(string $nickname)
|
|
||||||
{
|
|
||||||
Cache::delete('avatar-' . $nickname);
|
|
||||||
Cache::delete('avatar-url-' . $nickname);
|
|
||||||
Cache::delete('avatar-file-info-' . $nickname);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,239 +0,0 @@
|
||||||
<?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;
|
|
||||||
|
|
||||||
use App\Core\Cache;
|
|
||||||
use App\Core\DB\DB;
|
|
||||||
use function App\Core\I18n\_m;
|
|
||||||
use App\Core\Log;
|
|
||||||
use App\Entity\Attachment;
|
|
||||||
use App\Entity\Avatar;
|
|
||||||
use App\Util\Common;
|
|
||||||
use App\Util\Exception\ClientException;
|
|
||||||
use Component\Media\Exception\NoAvatarException;
|
|
||||||
use Exception;
|
|
||||||
use Symfony\Component\Asset\Package;
|
|
||||||
use Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy;
|
|
||||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\File\File as SymfonyFile;
|
|
||||||
use Symfony\Component\HttpFoundation\HeaderUtils;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
|
|
||||||
abstract class Utils
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Perform file validation (checks and normalization) and store the given file
|
|
||||||
*/
|
|
||||||
public static function validateAndStoreAttachment(SymfonyFile $sfile,
|
|
||||||
string $dest_dir,
|
|
||||||
?string $title = null,
|
|
||||||
bool $is_local = true,
|
|
||||||
?int $actor_id = null): Attachment
|
|
||||||
{
|
|
||||||
// The following properly gets the mimetype with `file` or other
|
|
||||||
// available methods, so should be safe
|
|
||||||
$hash = hash_file(Attachment::FILEHASH_ALGO, $sfile->getPathname());
|
|
||||||
$file = Attachment::create([
|
|
||||||
'file_hash' => $hash,
|
|
||||||
'actor_id' => $actor_id,
|
|
||||||
'mimetype' => $sfile->getMimeType(),
|
|
||||||
'title' => $title ?: _m('Untitled attachment'),
|
|
||||||
'filename' => $hash,
|
|
||||||
'is_local' => $is_local,
|
|
||||||
]);
|
|
||||||
$sfile->move($dest_dir, $hash);
|
|
||||||
// TODO Normalize file types
|
|
||||||
return $file;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Include $filepath in the response, for viewing or downloading.
|
|
||||||
*
|
|
||||||
* @throws ServerException
|
|
||||||
*/
|
|
||||||
public static function sendFile(string $filepath, string $mimetype, ?string $output_filename, string $disposition = 'inline'): Response
|
|
||||||
{
|
|
||||||
$response = new BinaryFileResponse(
|
|
||||||
$filepath,
|
|
||||||
Response::HTTP_OK,
|
|
||||||
[
|
|
||||||
'Content-Description' => 'File Transfer',
|
|
||||||
'Content-Type' => $mimetype,
|
|
||||||
'Content-Disposition' => HeaderUtils::makeDisposition($disposition, $output_filename ?: _m('Untitled attachment')),
|
|
||||||
'Cache-Control' => 'public',
|
|
||||||
],
|
|
||||||
$public = true,
|
|
||||||
$disposition = null,
|
|
||||||
$add_etag = true,
|
|
||||||
$add_last_modified = true
|
|
||||||
);
|
|
||||||
if (Common::config('site', 'x_static_delivery')) {
|
|
||||||
$response->trustXSendfileTypeHeader();
|
|
||||||
}
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Throw a client exception if the cache key $id doesn't contain
|
|
||||||
* exactly one entry
|
|
||||||
*
|
|
||||||
* @param mixed $except
|
|
||||||
* @param mixed $id
|
|
||||||
*/
|
|
||||||
private static function error($except, $id, array $res)
|
|
||||||
{
|
|
||||||
switch (count($res)) {
|
|
||||||
case 0:
|
|
||||||
throw new $except();
|
|
||||||
case 1:
|
|
||||||
return $res[0];
|
|
||||||
default:
|
|
||||||
Log::error('Media query returned more than one result for identifier: \"' . $id . '\"');
|
|
||||||
throw new ClientException(_m('Internal server error'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the file info by id
|
|
||||||
*
|
|
||||||
* Returns the file's hash, mimetype and title
|
|
||||||
*/
|
|
||||||
public static function getFileInfo(int $id)
|
|
||||||
{
|
|
||||||
return self::error(NoSuchFileException::class,
|
|
||||||
$id,
|
|
||||||
Cache::get("file-info-{$id}",
|
|
||||||
function () use ($id) {
|
|
||||||
return DB::dql('select at.file_hash, at.mimetype, at.title ' .
|
|
||||||
'from App\\Entity\\Attachment at ' .
|
|
||||||
'where at.id = :id',
|
|
||||||
['id' => $id]);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- Attachment ------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the attachment file info by id
|
|
||||||
*
|
|
||||||
* Returns the attachment file's hash, mimetype, title and path
|
|
||||||
*/
|
|
||||||
public static function getAttachmentFileInfo(int $id): array
|
|
||||||
{
|
|
||||||
$res = self::getFileInfo($id);
|
|
||||||
$res['file_path'] = Common::config('attachments', 'dir') . $res['file_hash'];
|
|
||||||
return $res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- Avatar ------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the avatar associated with the given nickname
|
|
||||||
*/
|
|
||||||
public static function getAvatar(?string $nickname = null): Avatar
|
|
||||||
{
|
|
||||||
$nickname = $nickname ?: Common::userNickname();
|
|
||||||
return self::error(NoAvatarException::class,
|
|
||||||
$nickname,
|
|
||||||
Cache::get("avatar-{$nickname}",
|
|
||||||
function () use ($nickname) {
|
|
||||||
return DB::dql('select a from App\\Entity\\Avatar a ' .
|
|
||||||
'join App\Entity\GSActor g with a.gsactor_id = g.id ' .
|
|
||||||
'where g.nickname = :nickname',
|
|
||||||
['nickname' => $nickname]);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the cached avatar associated with the given nickname, or the current user if not given
|
|
||||||
*/
|
|
||||||
public static function getAvatarUrl(?string $nickname = null): string
|
|
||||||
{
|
|
||||||
$nickname = $nickname ?: Common::userNickname();
|
|
||||||
return Cache::get("avatar-url-{$nickname}", function () use ($nickname) {
|
|
||||||
try {
|
|
||||||
return self::getAvatar($nickname)->getUrl();
|
|
||||||
} catch (NoAvatarException $e) {
|
|
||||||
}
|
|
||||||
$package = new Package(new EmptyVersionStrategy());
|
|
||||||
return $package->getUrl(Common::config('avatar', 'default'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the cached avatar file info associated with the given nickname
|
|
||||||
*
|
|
||||||
* Returns the avatar file's hash, mimetype, title and path.
|
|
||||||
* Ensures exactly one cached value exists
|
|
||||||
*/
|
|
||||||
public static function getAvatarFileInfo(string $nickname): array
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$res = self::error(NoAvatarException::class,
|
|
||||||
$nickname,
|
|
||||||
Cache::get("avatar-file-info-{$nickname}",
|
|
||||||
function () use ($nickname) {
|
|
||||||
return DB::dql('select f.file_hash, f.mimetype, f.title ' .
|
|
||||||
'from App\\Entity\\Attachment f ' .
|
|
||||||
'join App\\Entity\\Avatar a with f.id = a.file_id ' .
|
|
||||||
'join App\\Entity\\GSActor g with g.id = a.gsactor_id ' .
|
|
||||||
'where g.nickname = :nickname',
|
|
||||||
['nickname' => $nickname]);
|
|
||||||
}));
|
|
||||||
$res['file_path'] = Avatar::getFilePathStatic($res['file_hash']);
|
|
||||||
return $res;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$filepath = INSTALLDIR . '/public/assets/default-avatar.svg';
|
|
||||||
return ['file_path' => $filepath, 'mimetype' => 'image/svg+xml', 'title' => null];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the minor part of a mimetype. image/webp -> image
|
|
||||||
*/
|
|
||||||
public static function mimetypeMajor(string $mime)
|
|
||||||
{
|
|
||||||
return explode('/', self::mimeBare($mime))[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the minor part of a mimetype. image/webp -> webp
|
|
||||||
*/
|
|
||||||
public static function mimetypeMinor(string $mime)
|
|
||||||
{
|
|
||||||
return explode('/', self::mimeBare($mime))[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get only the mimetype and not additional info (separated from bare mime with semi-colon)
|
|
||||||
*/
|
|
||||||
public static function mimeBare(string $mimetype)
|
|
||||||
{
|
|
||||||
$mimetype = mb_strtolower($mimetype);
|
|
||||||
if (($semicolon = mb_strpos($mimetype, ';')) !== false) {
|
|
||||||
$mimetype = mb_substr($mimetype, 0, $semicolon);
|
|
||||||
}
|
|
||||||
return trim($mimetype);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -25,21 +25,21 @@ use App\Core\Cache;
|
||||||
use App\Core\DB\DB;
|
use App\Core\DB\DB;
|
||||||
use App\Core\Event;
|
use App\Core\Event;
|
||||||
use App\Core\Form;
|
use App\Core\Form;
|
||||||
|
use App\Core\GSFile;
|
||||||
use function App\Core\I18n\_m;
|
use function App\Core\I18n\_m;
|
||||||
use App\Core\Modules\Module;
|
use App\Core\Modules\Component;
|
||||||
use App\Core\Security;
|
use App\Core\Security;
|
||||||
use App\Entity\AttachmentToNote;
|
use App\Entity\AttachmentToNote;
|
||||||
use App\Entity\Note;
|
use App\Entity\Note;
|
||||||
use App\Util\Common;
|
use App\Util\Common;
|
||||||
use App\Util\Exceptiion\InvalidFormException;
|
use App\Util\Exception\InvalidFormException;
|
||||||
use App\Util\Exception\RedirectException;
|
use App\Util\Exception\RedirectException;
|
||||||
use Component\Media\Media;
|
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||||
|
|
||||||
class Posting extends Module
|
class Posting extends Component
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* HTML render event handler responsible for adding and handling
|
* HTML render event handler responsible for adding and handling
|
||||||
|
@ -105,7 +105,7 @@ class Posting extends Module
|
||||||
]);
|
]);
|
||||||
$processed_attachments = [];
|
$processed_attachments = [];
|
||||||
foreach ($attachments as $f) {
|
foreach ($attachments as $f) {
|
||||||
$na = Media::validateAndStoreAttachment(
|
$na = GSFile::validateAndStoreAttachment(
|
||||||
$f, Common::config('attachments', 'dir'),
|
$f, Common::config('attachments', 'dir'),
|
||||||
Security::sanitize($title = $f->getClientOriginalName()),
|
Security::sanitize($title = $f->getClientOriginalName()),
|
||||||
$is_local = true, $actor_id
|
$is_local = true, $actor_id
|
||||||
|
|
|
@ -29,8 +29,8 @@ use App\Util\Common;
|
||||||
use App\Util\Exception\ClientException;
|
use App\Util\Exception\ClientException;
|
||||||
use App\Util\Exception\RedirectException;
|
use App\Util\Exception\RedirectException;
|
||||||
use App\Util\Exception\ServerException;
|
use App\Util\Exception\ServerException;
|
||||||
use Component\Media\Media;
|
use Component\Media\Attachment;
|
||||||
use Component\Media\Media as M;
|
use Component\Media\Attachment as M;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
|
@ -97,7 +97,7 @@ class Cover
|
||||||
if (explode('/',$sfile->getMimeType())[0] != 'image') {
|
if (explode('/',$sfile->getMimeType())[0] != 'image') {
|
||||||
throw new ServerException('Invalid file type');
|
throw new ServerException('Invalid file type');
|
||||||
}
|
}
|
||||||
$file = Media::validateAndStoreFile($sfile, Common::config('cover', 'dir'), $title = null, $is_local = true, $use_unique = $actor_id);
|
$file = Attachment::validateAndStoreFile($sfile, Common::config('cover', 'dir'), $title = null, $is_local = true, $use_unique = $actor_id);
|
||||||
$old_file = null;
|
$old_file = null;
|
||||||
$cover = DB::find('cover', ['gsactor_id' => $actor_id]);
|
$cover = DB::find('cover', ['gsactor_id' => $actor_id]);
|
||||||
// Must get old id before inserting another one
|
// Must get old id before inserting another one
|
||||||
|
|
|
@ -23,9 +23,9 @@ namespace Plugin\ImageThumbnail\Controller;
|
||||||
|
|
||||||
use App\Core\Controller;
|
use App\Core\Controller;
|
||||||
use App\Core\DB\DB;
|
use App\Core\DB\DB;
|
||||||
|
use App\Core\GSFile;
|
||||||
use App\Entity\AttachmentThumbnail;
|
use App\Entity\AttachmentThumbnail;
|
||||||
use App\Util\Common;
|
use App\Util\Common;
|
||||||
use Component\Media\Media;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class ImageThumbnail extends Controller
|
class ImageThumbnail extends Controller
|
||||||
|
@ -57,6 +57,6 @@ class ImageThumbnail extends Controller
|
||||||
$filename = $thumbnail->getFilename();
|
$filename = $thumbnail->getFilename();
|
||||||
$path = $thumbnail->getPath();
|
$path = $thumbnail->getPath();
|
||||||
|
|
||||||
return Media::sendFile(filepath: $path, mimetype: $attachment->getMimetype(), output_filename: $filename, disposition: 'inline');
|
return GSFile::sendFile(filepath: $path, mimetype: $attachment->getMimetype(), output_filename: $filename, disposition: 'inline');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
0
src/Controller/.gitignore
vendored
0
src/Controller/.gitignore
vendored
|
@ -19,26 +19,14 @@
|
||||||
|
|
||||||
// }}}
|
// }}}
|
||||||
|
|
||||||
namespace Component\Media\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Core\Controller;
|
use App\Core\Controller;
|
||||||
use Component\Media\Media as M;
|
use App\Core\GSFile as M;
|
||||||
use Exception;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class Media extends Controller
|
class Attachment extends Controller
|
||||||
{
|
{
|
||||||
public function avatar(Request $request, string $nickname, string $size)
|
|
||||||
{
|
|
||||||
switch ($size) {
|
|
||||||
case 'full':
|
|
||||||
$res = M::getAvatarFileInfo($nickname);
|
|
||||||
return M::sendFile($res['file_path'], $res['mimetype'], $res['title']);
|
|
||||||
default:
|
|
||||||
throw new Exception('Not implemented');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function attachment_inline(Request $request, int $id)
|
public function attachment_inline(Request $request, int $id)
|
||||||
{
|
{
|
||||||
$res = M::getAttachmentFileInfo($id);
|
$res = M::getAttachmentFileInfo($id);
|
|
@ -38,13 +38,13 @@ namespace App\Controller;
|
||||||
use App\Core\DB\DB;
|
use App\Core\DB\DB;
|
||||||
use App\Core\Event;
|
use App\Core\Event;
|
||||||
use App\Core\Form;
|
use App\Core\Form;
|
||||||
|
use App\Core\GSFile;
|
||||||
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\Avatar;
|
||||||
use App\Util\ClientException;
|
use App\Util\ClientException;
|
||||||
use App\Util\Common;
|
use App\Util\Common;
|
||||||
use App\Util\Form\ArrayTransformer;
|
use App\Util\Form\ArrayTransformer;
|
||||||
use Component\Media\Media;
|
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Functional as F;
|
use Functional as F;
|
||||||
|
@ -144,11 +144,11 @@ class UserPanel extends AbstractController
|
||||||
} else {
|
} else {
|
||||||
throw new ClientException('Invalid form');
|
throw new ClientException('Invalid form');
|
||||||
}
|
}
|
||||||
$user = Common::user();
|
$user = Common::user();
|
||||||
$actor_id = $user->getId();
|
$gsactor_id = $user->getId();
|
||||||
$file = Media::validateAndStoreAttachment($sfile, Common::config('avatar', 'dir'), $title = null, $is_local = true, $use_unique = $actor_id);
|
$file = GSFile::validateAndStoreAttachment($sfile, Common::config('avatar', 'dir'), $title = null, $is_local = true, $use_unique = $gsactor_id);
|
||||||
$old_file = null;
|
$old_file = null;
|
||||||
$avatar = DB::find('avatar', ['gsactor_id' => $actor_id]);
|
$avatar = DB::find('avatar', ['gsactor_id' => $gsactor_id]);
|
||||||
// Must get old id before inserting another one
|
// Must get old id before inserting another one
|
||||||
if ($avatar != null) {
|
if ($avatar != null) {
|
||||||
$old_file = $avatar->delete();
|
$old_file = $avatar->delete();
|
||||||
|
@ -156,13 +156,13 @@ class UserPanel extends AbstractController
|
||||||
DB::persist($file);
|
DB::persist($file);
|
||||||
// Can only get new id after inserting
|
// Can only get new id after inserting
|
||||||
DB::flush();
|
DB::flush();
|
||||||
DB::persist(Avatar::create(['gsactor_id' => $actor_id, 'file_id' => $file->getId()]));
|
DB::persist(Avatar::create(['gsactor_id' => $gsactor_id, 'attachment_id' => $file->getId()]));
|
||||||
DB::flush();
|
DB::flush();
|
||||||
// Only delete files if the commit went through
|
// Only delete files if the commit went through
|
||||||
if ($old_file != null) {
|
if ($old_file != null) {
|
||||||
@unlink($old_file);
|
@unlink($old_file);
|
||||||
}
|
}
|
||||||
Event::handle('DeleteCachedAvatar', [$user->getNickname()]);
|
Event::handle('DeleteCachedAvatar', [$user->getId()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['_template' => 'settings/avatar.html.twig', 'avatar' => $form->createView()];
|
return ['_template' => 'settings/avatar.html.twig', 'avatar' => $form->createView()];
|
||||||
|
|
170
src/Core/GSFile.php
Normal file
170
src/Core/GSFile.php
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
<?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 App\Core;
|
||||||
|
|
||||||
|
use App\Core\DB\DB;
|
||||||
|
use function App\Core\I18n\_m;
|
||||||
|
use App\Entity\Attachment;
|
||||||
|
use App\Util\Common;
|
||||||
|
use App\Util\Exception\ClientException;
|
||||||
|
use App\Util\Exception\NoSuchFileException;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\File\File as SymfonyFile;
|
||||||
|
use Symfony\Component\HttpFoundation\HeaderUtils;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class GSFile
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Perform file validation (checks and normalization) and store the given file
|
||||||
|
*/
|
||||||
|
public static function validateAndStoreAttachment(SymfonyFile $sfile,
|
||||||
|
string $dest_dir,
|
||||||
|
?string $title = null,
|
||||||
|
bool $is_local = true,
|
||||||
|
int $actor_id = null): Attachment
|
||||||
|
{
|
||||||
|
// The following properly gets the mimetype with `file` or other
|
||||||
|
// available methods, so should be safe
|
||||||
|
$hash = hash_file(Attachment::FILEHASH_ALGO, $sfile->getPathname());
|
||||||
|
$file = Attachment::create([
|
||||||
|
'file_hash' => $hash,
|
||||||
|
'gsactor_id' => $actor_id,
|
||||||
|
'mimetype' => $sfile->getMimeType(),
|
||||||
|
'title' => $title ?: _m('Untitled attachment'),
|
||||||
|
'filename' => $hash,
|
||||||
|
'is_local' => $is_local,
|
||||||
|
]);
|
||||||
|
$sfile->move($dest_dir, $hash);
|
||||||
|
// TODO Normalize file types
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Include $filepath in the response, for viewing or downloading.
|
||||||
|
*
|
||||||
|
* @throws ServerException
|
||||||
|
*/
|
||||||
|
public static function sendFile(string $filepath, string $mimetype, ?string $output_filename, string $disposition = 'inline'): Response
|
||||||
|
{
|
||||||
|
$response = new BinaryFileResponse(
|
||||||
|
$filepath,
|
||||||
|
Response::HTTP_OK,
|
||||||
|
[
|
||||||
|
'Content-Description' => 'File Transfer',
|
||||||
|
'Content-Type' => $mimetype,
|
||||||
|
'Content-Disposition' => HeaderUtils::makeDisposition($disposition, $output_filename ?: _m('Untitled attachment')),
|
||||||
|
'Cache-Control' => 'public',
|
||||||
|
],
|
||||||
|
$public = true,
|
||||||
|
$disposition = null,
|
||||||
|
$add_etag = true,
|
||||||
|
$add_last_modified = true
|
||||||
|
);
|
||||||
|
if (Common::config('site', 'x_static_delivery')) {
|
||||||
|
$response->trustXSendfileTypeHeader();
|
||||||
|
}
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw a client exception if the cache key $id doesn't contain
|
||||||
|
* exactly one entry
|
||||||
|
*
|
||||||
|
* @param mixed $except
|
||||||
|
* @param mixed $id
|
||||||
|
*/
|
||||||
|
public static function error($except, $id, array $res)
|
||||||
|
{
|
||||||
|
switch (count($res)) {
|
||||||
|
case 0:
|
||||||
|
throw new $except();
|
||||||
|
case 1:
|
||||||
|
return $res[0];
|
||||||
|
default:
|
||||||
|
Log::error('Media query returned more than one result for identifier: \"' . $id . '\"');
|
||||||
|
throw new ClientException(_m('Internal server error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the file info by id
|
||||||
|
*
|
||||||
|
* Returns the file's hash, mimetype and title
|
||||||
|
*/
|
||||||
|
public static function getFileInfo(int $id)
|
||||||
|
{
|
||||||
|
return self::error(NoSuchFileException::class,
|
||||||
|
$id,
|
||||||
|
Cache::get("file-info-{$id}",
|
||||||
|
function () use ($id) {
|
||||||
|
return DB::dql('select at.file_hash, at.mimetype, at.title ' .
|
||||||
|
'from App\\Entity\\Attachment at ' .
|
||||||
|
'where at.id = :id',
|
||||||
|
['id' => $id]);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Attachment ------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attachment file info by id
|
||||||
|
*
|
||||||
|
* Returns the attachment file's hash, mimetype, title and path
|
||||||
|
*/
|
||||||
|
public static function getAttachmentFileInfo(int $id): array
|
||||||
|
{
|
||||||
|
$res = self::getFileInfo($id);
|
||||||
|
$res['file_path'] = Common::config('attachments', 'dir') . $res['file_hash'];
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the minor part of a mimetype. image/webp -> image
|
||||||
|
*/
|
||||||
|
public static function mimetypeMajor(string $mime)
|
||||||
|
{
|
||||||
|
return explode('/', self::mimeBare($mime))[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the minor part of a mimetype. image/webp -> webp
|
||||||
|
*/
|
||||||
|
public static function mimetypeMinor(string $mime)
|
||||||
|
{
|
||||||
|
return explode('/', self::mimeBare($mime))[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get only the mimetype and not additional info (separated from bare mime with semi-colon)
|
||||||
|
*/
|
||||||
|
public static function mimeBare(string $mimetype)
|
||||||
|
{
|
||||||
|
$mimetype = mb_strtolower($mimetype);
|
||||||
|
if (($semicolon = mb_strpos($mimetype, ';')) !== false) {
|
||||||
|
$mimetype = mb_substr($mimetype, 0, $semicolon);
|
||||||
|
}
|
||||||
|
return trim($mimetype);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
namespace App\Core\Modules;
|
namespace App\Core\Modules;
|
||||||
|
|
||||||
// an interface that specifies how thumbs shall be created, allowing for generation of image, video, pdf, wtv by plugin
|
// an interface that specifies how thumbs shall be created, allowing for generation of image, video, pdf, wtv by plugin
|
||||||
|
|
||||||
abstract class Thumbnail
|
abstract class AttachmentThumbnail
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,12 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
namespace App\Core\Modules;
|
namespace App\Core\Modules;
|
||||||
|
|
||||||
// Remote Download allow to create a file or to download directly as a thumbnail; same plugins as for upload will validate
|
// Remote Download allow to create a file or to download directly as a thumbnail; same plugins as for upload will validate
|
||||||
// as validation event will be the same as that of upload - same logic
|
// as validation event will be the same as that of upload - same logic
|
||||||
|
|
||||||
abstract class RemoteAttachmentDownload
|
abstract class AttachmentValidate
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
7
src/Core/Modules/Component.php
Normal file
7
src/Core/Modules/Component.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Modules;
|
||||||
|
|
||||||
|
abstract class Component extends Module
|
||||||
|
{
|
||||||
|
}
|
7
src/Core/Modules/Plugin.php
Normal file
7
src/Core/Modules/Plugin.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Modules;
|
||||||
|
|
||||||
|
abstract class Plugin extends Module
|
||||||
|
{
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
|
|
||||||
namespace App\Core\Modules;
|
|
||||||
|
|
||||||
// used by avatar and attachment in note creation; plugins will be able to validate certain filetypes such as images
|
|
||||||
|
|
||||||
abstract class Upload
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
|
@ -201,12 +201,12 @@ class Attachment 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::findBy('avatar', ['file_id' => $this->id]);
|
$avatar = DB::findBy('avatar', ['attachment_id' => $this->id]);
|
||||||
foreach ($avatar as $a) {
|
foreach ($avatar as $a) {
|
||||||
$files[] = $a->getFilePath();
|
$files[] = $a->getFilePath();
|
||||||
$a->delete($flush, $delete_files_now, $cascading = true);
|
$a->delete($flush, $delete_files_now, $cascading = true);
|
||||||
}
|
}
|
||||||
foreach (DB::findBy('file_thumbnail', ['file_id' => $this->id]) as $ft) {
|
foreach (DB::findBy('attachment_thumbnail', ['attachment_id' => $this->id]) as $ft) {
|
||||||
$files[] = $ft->delete($flush, $delete_files_now, $cascading);
|
$files[] = $ft->delete($flush, $delete_files_now, $cascading);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,11 +25,11 @@ use App\Core\Cache;
|
||||||
use App\Core\DB\DB;
|
use App\Core\DB\DB;
|
||||||
use App\Core\Entity;
|
use App\Core\Entity;
|
||||||
use App\Core\Event;
|
use App\Core\Event;
|
||||||
|
use App\Core\GSFile;
|
||||||
use App\Core\Log;
|
use App\Core\Log;
|
||||||
use App\Util\Common;
|
use App\Util\Common;
|
||||||
use App\Util\Exception\NotFoundException;
|
use App\Util\Exception\NotFoundException;
|
||||||
use App\Util\Exception\ServerException;
|
use App\Util\Exception\ServerException;
|
||||||
use Component\Media\Media;
|
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -126,7 +126,7 @@ class AttachmentThumbnail extends Entity
|
||||||
} catch (NotFoundException $e) {
|
} catch (NotFoundException $e) {
|
||||||
$thumbnail = self::create(['attachment_id' => $attachment->getId(), 'width' => $width, 'height' => $height, 'attachment' => $attachment]);
|
$thumbnail = self::create(['attachment_id' => $attachment->getId(), 'width' => $width, 'height' => $height, 'attachment' => $attachment]);
|
||||||
$event_map = ['image' => 'ResizeImage', 'video' => 'ResizeVideo'];
|
$event_map = ['image' => 'ResizeImage', 'video' => 'ResizeVideo'];
|
||||||
$major_mime = Media::mimetypeMajor($attachment->getMimetype());
|
$major_mime = GSFile::mimetypeMajor($attachment->getMimetype());
|
||||||
if (in_array($major_mime, array_keys($event_map))) {
|
if (in_array($major_mime, array_keys($event_map))) {
|
||||||
Event::handle($event_map[$major_mime], [$attachment, $thumbnail, $width, $height, $crop]);
|
Event::handle($event_map[$major_mime], [$attachment, $thumbnail, $width, $height, $crop]);
|
||||||
return $thumbnail;
|
return $thumbnail;
|
||||||
|
|
|
@ -45,7 +45,7 @@ class Avatar extends Entity
|
||||||
{
|
{
|
||||||
// {{{ Autocode
|
// {{{ Autocode
|
||||||
private int $gsactor_id;
|
private int $gsactor_id;
|
||||||
private int $file_id;
|
private int $attachment_id;
|
||||||
private DateTimeInterface $created;
|
private DateTimeInterface $created;
|
||||||
private DateTimeInterface $modified;
|
private DateTimeInterface $modified;
|
||||||
|
|
||||||
|
@ -60,15 +60,15 @@ class Avatar extends Entity
|
||||||
return $this->gsactor_id;
|
return $this->gsactor_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setFileId(int $file_id): self
|
public function setAttachmentId(int $attachment_id): self
|
||||||
{
|
{
|
||||||
$this->file_id = $file_id;
|
$this->attachment_id = $attachment_id;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFileId(): int
|
public function getAttachmentId(): int
|
||||||
{
|
{
|
||||||
return $this->file_id;
|
return $this->attachment_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setCreated(DateTimeInterface $created): self
|
public function setCreated(DateTimeInterface $created): self
|
||||||
|
@ -95,17 +95,17 @@ class Avatar extends Entity
|
||||||
|
|
||||||
// }}} Autocode
|
// }}} Autocode
|
||||||
|
|
||||||
private ?File $file = null;
|
private ?Attachment $attachment = null;
|
||||||
|
|
||||||
public function getUrl(): string
|
public function getUrl(): string
|
||||||
{
|
{
|
||||||
return Router::url('avatar', ['nickname' => GSActor::getNicknameFromId($this->gsactor_id)]);
|
return Router::url('avatar', ['gsactor_id' => $this->gsactor_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFile(): File
|
public function getAttachment(): Attachment
|
||||||
{
|
{
|
||||||
$this->file = $this->file ?: DB::find('file', ['id' => $this->file_id]);
|
$this->attachment = $this->attachment ?: DB::find('attachment', ['id' => $this->attachment_id]);
|
||||||
return $this->file;
|
return $this->attachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getFilePathStatic(string $filename): string
|
public static function getFilePathStatic(string $filename): string
|
||||||
|
@ -115,7 +115,7 @@ class Avatar extends Entity
|
||||||
|
|
||||||
public function getFilePath(): string
|
public function getFilePath(): string
|
||||||
{
|
{
|
||||||
return Common::config('avatar', 'dir') . $this->getFile()->getFileName();
|
return Common::config('avatar', 'dir') . $this->getAttachment()->getFileName();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -125,7 +125,7 @@ class Avatar extends Entity
|
||||||
{
|
{
|
||||||
// Don't go into a loop if we're deleting from File
|
// Don't go into a loop if we're deleting from File
|
||||||
if (!$cascading) {
|
if (!$cascading) {
|
||||||
$files = $this->getFile()->delete($cascade = true, $file_flush = false, $delete_files_now);
|
$files = $this->getAttachment()->delete($cascade = true, $file_flush = false, $delete_files_now);
|
||||||
} else {
|
} else {
|
||||||
DB::remove(DB::getReference('avatar', ['gsactor_id' => $this->gsactor_id]));
|
DB::remove(DB::getReference('avatar', ['gsactor_id' => $this->gsactor_id]));
|
||||||
$file_path = $this->getFilePath();
|
$file_path = $this->getFilePath();
|
||||||
|
@ -143,14 +143,14 @@ class Avatar extends Entity
|
||||||
return [
|
return [
|
||||||
'name' => 'avatar',
|
'name' => 'avatar',
|
||||||
'fields' => [
|
'fields' => [
|
||||||
'gsactor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'GSActor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'foreign key to gsactor table'],
|
'gsactor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'GSActor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'foreign key to gsactor table'],
|
||||||
'file_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'File.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'foreign key to file table'],
|
'attachment_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'File.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'foreign key to file table'],
|
||||||
'created' => ['type' => 'datetime', 'not null' => true, 'description' => 'date this record was created', 'default' => 'CURRENT_TIMESTAMP'],
|
'created' => ['type' => 'datetime', 'not null' => true, 'description' => 'date this record was created', 'default' => 'CURRENT_TIMESTAMP'],
|
||||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified', 'default' => 'CURRENT_TIMESTAMP'],
|
'modified' => ['type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified', 'default' => 'CURRENT_TIMESTAMP'],
|
||||||
],
|
],
|
||||||
'primary key' => ['gsactor_id'],
|
'primary key' => ['gsactor_id'],
|
||||||
'indexes' => [
|
'indexes' => [
|
||||||
'avatar_file_id_idx' => ['file_id'],
|
'avatar_attachment_id_idx' => ['attachment_id'],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -196,7 +196,7 @@ class Note extends Entity
|
||||||
public function getAvatarUrl()
|
public function getAvatarUrl()
|
||||||
{
|
{
|
||||||
$url = null;
|
$url = null;
|
||||||
Event::handle('GetAvatarUrl', [$this->getActorNickname(), &$url]);
|
Event::handle('GetAvatarUrl', [$this->getGSActorId(), &$url]);
|
||||||
return $url;
|
return $url;
|
||||||
}
|
}
|
||||||
public static function getAllNotes(int $noteScope): array
|
public static function getAllNotes(int $noteScope): array
|
||||||
|
|
|
@ -69,5 +69,8 @@ abstract class Main
|
||||||
foreach (['personal_info', 'avatar', 'notifications', 'account'] as $s) {
|
foreach (['personal_info', 'avatar', 'notifications', 'account'] as $s) {
|
||||||
$r->connect('settings_' . $s, '/settings/' . $s, [C\UserPanel::class, $s]);
|
$r->connect('settings_' . $s, '/settings/' . $s, [C\UserPanel::class, $s]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attachments
|
||||||
|
$r->connect('attachment_inline', '/attachment/{id<\d+>}', [C\Attachment::class, 'attachment_inline']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,6 +94,11 @@ abstract class Common
|
||||||
return self::ensureLoggedIn()->getNickname();
|
return self::ensureLoggedIn()->getNickname();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function userId(): ?string
|
||||||
|
{
|
||||||
|
return self::ensureLoggedIn()->getId();
|
||||||
|
}
|
||||||
|
|
||||||
public static function ensureLoggedIn(): LocalUser
|
public static function ensureLoggedIn(): LocalUser
|
||||||
{
|
{
|
||||||
if (($user = self::user()) == null) {
|
if (($user = self::user()) == null) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user