[GSFile] Fix sanitize configuration

This commit is contained in:
Diogo Peralta Cordeiro 2021-09-22 15:04:45 +01:00
parent c1e7d486a3
commit 7beb5c2995
Signed by: diogo
GPG Key ID: 18D2D35001FBFAB0
10 changed files with 96 additions and 98 deletions

View File

@ -109,7 +109,7 @@ class Avatar extends Component
* Returns the avatar file's hash, mimetype, title and path. * Returns the avatar file's hash, mimetype, title and path.
* Ensures exactly one cached value exists * Ensures exactly one cached value exists
*/ */
public static function getAvatarFileInfo(int $actor_id): array public static function getAvatarFileInfo(int $actor_id, int $size = 0): array
{ {
$res = Cache::get("avatar-file-info-{$actor_id}", $res = Cache::get("avatar-file-info-{$actor_id}",
function () use ($actor_id) { function () use ($actor_id) {

View File

@ -101,7 +101,7 @@ class Avatar extends Controller
$data_user = base64_decode($data_user); $data_user = base64_decode($data_user);
$tempfile = new TemporaryFile(['prefix' => 'gs-avatar']); $tempfile = new TemporaryFile(['prefix' => 'gs-avatar']);
$tempfile->write($data_user); $tempfile->write($data_user);
$attachment = GSFile::sanitizeAndStoreFileAsAttachment($tempfile); $attachment = GSFile::storeFileAsAttachment($tempfile);
} 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');
} }
@ -109,7 +109,7 @@ class Avatar extends Controller
} elseif (isset($data['avatar'])) { } elseif (isset($data['avatar'])) {
// Cropping failed (e.g. disabled js), use file as uploaded // Cropping failed (e.g. disabled js), use file as uploaded
$file = $data['avatar']; $file = $data['avatar'];
$attachment = GSFile::sanitizeAndStoreFileAsAttachment($file); $attachment = GSFile::storeFileAsAttachment($file);
} else { } else {
throw new ClientException('Invalid form'); throw new ClientException('Invalid form');
} }

View File

@ -96,7 +96,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 = GSFile::sanitizeAndStoreFileAsAttachment($sfile); $file = GSFile::storeFileAsAttachment($sfile);
$old_file = null; $old_file = null;
$cover = DB::find('cover', ['gctor_id' => $actor_id]); $cover = DB::find('cover', ['gctor_id' => $actor_id]);
// Must get old id before inserting another one // Must get old id before inserting another one

View File

@ -1,60 +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 Plugin\ImageEncoder\Controller;
use App\Core\Controller;
use App\Core\DB\DB;
use App\Core\GSFile;
use App\Entity\AttachmentThumbnail;
use App\Util\Common;
use Symfony\Component\HttpFoundation\Request;
class ImageThumbnail extends Controller
{
/**
* Get a thumbnail for the attachment with id $id
*/
public function thumbnail(Request $request, int $id)
{
$attachment = DB::findOneBy('attachment', ['id' => $id]);
if (!is_null($attachment->getScope())) {
// && ($attachment->scope | VisibilityScope::PUBLIC) != 0
// $user = Common::ensureLoggedIn();
assert(false, 'Attachment scope not implemented');
}
// TODO rate limit
$max_width = Common::config('thumbnail', 'width');
$max_height = Common::config('thumbnail', 'height');
$width = Common::clamp($this->int('w') ?: $max_width, min: 0, max: $max_width);
$height = Common::clamp($this->int('h') ?: $max_height, min: 0, max: $max_height);
$crop = $this->bool('c') ?: false;
$thumbnail = AttachmentThumbnail::getOrCreate(attachment: $attachment, width: $width, height: $height, crop: $crop);
$filename = $thumbnail->getFilename();
$path = $thumbnail->getPath();
return GSFile::sendFile(filepath: $path, mimetype: $attachment->getMimetype(), output_filename: $filename, disposition: 'inline');
}
}

View File

@ -53,6 +53,21 @@ class ImageEncoder extends Plugin
return '3.0.0'; return '3.0.0';
} }
/**
* @param array $event_map
* @param string $mimetype
*
* @return bool
*/
public function onFileMetaAvailable(array &$event_map, string $mimetype): bool
{
if (GSFile::mimetypeMajor($mimetype) !== 'image') {
return Event::next;
}
$event_map['image'][] = [$this, 'fileMeta'];
return Event::next;
}
/** /**
* @param array $event_map * @param array $event_map
* @param string $mimetype * @param string $mimetype
@ -83,8 +98,29 @@ class ImageEncoder extends Plugin
return Event::next; return Event::next;
} }
public function fileMeta(SplFileInfo &$file, ?string &$mimetype, ?int &$width, ?int &$height): bool
{
$original_mimetype = $mimetype;
if (GSFile::mimetypeMajor($original_mimetype) !== 'image') {
// Nothing concerning us
return false;
}
try {
$image = Vips\Image::newFromFile($file->getRealPath(), ['access' => 'sequential']);
} catch (Vips\Exception $e) {
Log::debug("ImageEncoder's Vips couldn't handle the image file, failed with {$e}.");
throw new UnsupportedFileTypeException(_m("Unsupported image file with {$mimetype}.", previous: $e));
}
$width = $image->width;
$height = $image->height;
// Only one plugin can handle meta
return true;
}
/** /**
* Re-encodes the image ensuring it's valid. * Re-encodes the image ensuring it is valid.
* Also ensures that the image is not greater than the max width and height configured. * Also ensures that the image is not greater than the max width and height configured.
* *
* @param SplFileInfo $file * @param SplFileInfo $file
@ -103,7 +139,7 @@ class ImageEncoder extends Plugin
public function fileSanitize(SplFileInfo &$file, ?string &$mimetype, ?int &$width, ?int &$height): bool public function fileSanitize(SplFileInfo &$file, ?string &$mimetype, ?int &$width, ?int &$height): bool
{ {
$original_mimetype = $mimetype; $original_mimetype = $mimetype;
if (GSFile::mimetypeMajor($original_mimetype) != 'image') { if (GSFile::mimetypeMajor($original_mimetype) !== 'image') {
// Nothing concerning us // Nothing concerning us
return false; return false;
} }

View File

@ -55,6 +55,22 @@ class VideoEncoder extends Plugin
return '1.0.0'; return '1.0.0';
} }
/**
* @param array $event_map
* @param string $mimetype
*
* @return bool
*/
public function onFileMetaAvailable(array &$event_map, string $mimetype): bool
{
if (GSFile::mimetypeMajor($mimetype) !== 'video' && $mimetype !== 'image/gif') {
return Event::next;
}
$event_map['video'][] = [$this, 'fileMeta'];
$event_map['image/gif'][] = [$this, 'fileMeta'];
return Event::next;
}
/** /**
* @param array $event_map * @param array $event_map
* @param string $mimetype * @param string $mimetype
@ -66,8 +82,8 @@ class VideoEncoder extends Plugin
if (GSFile::mimetypeMajor($mimetype) !== 'video' && $mimetype !== 'image/gif') { if (GSFile::mimetypeMajor($mimetype) !== 'video' && $mimetype !== 'image/gif') {
return Event::next; return Event::next;
} }
$event_map['video'][] = [$this, 'fileSanitize']; $event_map['video'][] = [$this, 'fileMeta'];
$event_map['image/gif'][] = [$this, 'fileSanitize']; $event_map['image/gif'][] = [$this, 'fileMeta'];
return Event::next; return Event::next;
} }
@ -97,7 +113,7 @@ class VideoEncoder extends Plugin
* *
* @return bool true if sanitized * @return bool true if sanitized
*/ */
public function fileSanitize(SplFileInfo &$file, ?string &$mimetype, ?int &$width, ?int &$height): bool public function fileMeta(SplFileInfo &$file, ?string &$mimetype, ?int &$width, ?int &$height): bool
{ {
if (//GSFile::mimetypeMajor($mimetype) !== 'video' && if (//GSFile::mimetypeMajor($mimetype) !== 'video' &&
$mimetype !== 'image/gif') { $mimetype !== 'image/gif') {

View File

@ -1,6 +1,6 @@
<div> <div>
<figure> <figure>
<video class="u-video" src="{{ attachment.getUrl() }}" controls poster="{{ attachment.getThumbnailUrl()}}"> <video class="u-video" src="{{ attachment.getUrl() }}" controls poster="{{ attachment.getThumbnailUrl('medium')}}">
</video> </video>
<figcaption><a <figcaption><a
href="{{ path('attachment_show', {'id': attachment.getId()}) }}">{{ attachment.getBestTitle(note) }}</a> href="{{ path('attachment_show', {'id': attachment.getId()}) }}">{{ attachment.getBestTitle(note) }}</a>

View File

@ -59,7 +59,7 @@ class GSFile
* *
* @return Attachment * @return Attachment
*/ */
public static function sanitizeAndStoreFileAsAttachment(TemporaryFile|SymfonyFile $file): Attachment public static function storeFileAsAttachment(TemporaryFile|SymfonyFile $file): Attachment
{ {
$hash = null; $hash = null;
Event::handle('HashFile', [$file->getPathname(), &$hash]); Event::handle('HashFile', [$file->getPathname(), &$hash]);
@ -67,21 +67,25 @@ class GSFile
$attachment = DB::findOneBy('attachment', ['filehash' => $hash]); $attachment = DB::findOneBy('attachment', ['filehash' => $hash]);
// Attachment Exists // Attachment Exists
$attachment->livesIncrementAndGet(); $attachment->livesIncrementAndGet();
// We had this attachment, but not the file, thus no filename, update meta
if (is_null($attachment->getFilename())) { if (is_null($attachment->getFilename())) {
$mimetype = $attachment->getMimetype(); $mimetype = $attachment->getMimetype();
$width = $attachment->getWidth(); $width = $attachment->getWidth();
$height = $attachment->getHeight(); $height = $attachment->getHeight();
if (Common::config('attachments', 'sanitize')) {
$event_map[$mimetype] = []; $event_map[$mimetype] = [];
$major_mime = self::mimetypeMajor($mimetype); $major_mime = self::mimetypeMajor($mimetype);
$event_map[$major_mime] = []; $event_map[$major_mime] = [];
if (Common::config('attachments', 'sanitize')) {
Event::handle('FileSanitizerAvailable', [&$event_map, $mimetype]); Event::handle('FileSanitizerAvailable', [&$event_map, $mimetype]);
} else {
Event::handle('FileMetaAvailable', [&$event_map, $mimetype]);
}
// Always prefer specific encoders // Always prefer specific encoders
$encoders = array_merge($event_map[$mimetype], $event_map[$major_mime]); $encoders = array_merge($event_map[$mimetype], $event_map[$major_mime]);
foreach ($encoders as $encoder) { foreach ($encoders as $encoder) {
// These are all I/O params
if ($encoder($file, $mimetype, $width, $height)) { if ($encoder($file, $mimetype, $width, $height)) {
break; // One successful sanitizer is enough break; // One successful File type handler plugin is enough
}
} }
} }
$attachment->setFilename($hash); $attachment->setFilename($hash);
@ -97,11 +101,14 @@ class GSFile
// available methods, so should be safe // available methods, so should be safe
$mimetype = mb_substr($file->getMimeType(), 0, 64); $mimetype = mb_substr($file->getMimeType(), 0, 64);
$width = $height = null; $width = $height = null;
if (Common::config('attachments', 'sanitize')) {
$event_map[$mimetype] = []; $event_map[$mimetype] = [];
$major_mime = self::mimetypeMajor($mimetype); $major_mime = self::mimetypeMajor($mimetype);
$event_map[$major_mime] = []; $event_map[$major_mime] = [];
if (Common::config('attachments', 'sanitize')) {
Event::handle('FileSanitizerAvailable', [&$event_map, $mimetype]); Event::handle('FileSanitizerAvailable', [&$event_map, $mimetype]);
} else {
Event::handle('FileMetaAvailable', [&$event_map, $mimetype]);
}
// Always prefer specific encoders // Always prefer specific encoders
$encoders = array_merge($event_map[$mimetype], $event_map[$major_mime]); $encoders = array_merge($event_map[$mimetype], $event_map[$major_mime]);
foreach ($encoders as $encoder) { foreach ($encoders as $encoder) {
@ -109,7 +116,6 @@ class GSFile
break; // One successful sanitizer is enough break; // One successful sanitizer is enough
} }
} }
}
$attachment = Attachment::create([ $attachment = Attachment::create([
'filehash' => $hash, 'filehash' => $hash,
'mimetype' => $mimetype, 'mimetype' => $mimetype,

View File

@ -20,7 +20,7 @@ class MediaFixtures extends Fixture
$file = new TemporaryFile(); $file = new TemporaryFile();
$file->write(file_get_contents($filepath)); $file->write(file_get_contents($filepath));
try { try {
GSFile::sanitizeAndStoreFileAsAttachment($file); GSFile::storeFileAsAttachment($file);
} catch (Exception $e) { } catch (Exception $e) {
echo "Could not save file {$filepath}, failed with {$e}\n"; echo "Could not save file {$filepath}, failed with {$e}\n";
} finally { } finally {

View File

@ -39,7 +39,7 @@ class AttachmentTest extends GNUsocialTestCase
// Setup first attachment // Setup first attachment
$file = new TemporaryFile(); $file = new TemporaryFile();
$attachment = GSFile::sanitizeAndStoreFileAsAttachment($file); $attachment = GSFile::storeFileAsAttachment($file);
$path = $attachment->getPath(); $path = $attachment->getPath();
$hash = $attachment->getFilehash(); $hash = $attachment->getFilehash();
static::assertTrue(file_exists($attachment->getPath())); static::assertTrue(file_exists($attachment->getPath()));
@ -55,7 +55,7 @@ class AttachmentTest extends GNUsocialTestCase
// Setup the second attachment, re-adding the backed store // Setup the second attachment, re-adding the backed store
$file = new TemporaryFile(); $file = new TemporaryFile();
$repeated_attachment = GSFile::sanitizeAndStoreFileAsAttachment($file); $repeated_attachment = GSFile::storeFileAsAttachment($file);
$path = $attachment->getPath(); $path = $attachment->getPath();
static::assertSame(2, $repeated_attachment->getLives()); static::assertSame(2, $repeated_attachment->getLives());
static::assertTrue(file_exists($path)); static::assertTrue(file_exists($path));
@ -84,7 +84,7 @@ class AttachmentTest extends GNUsocialTestCase
DB::flush(); DB::flush();
$file = new File($temp_file->getRealPath()); $file = new File($temp_file->getRealPath());
GSFile::sanitizeAndStoreFileAsAttachment($file); GSFile::storeFileAsAttachment($file);
static::assertNotNull($attachment->getFilename()); static::assertNotNull($attachment->getFilename());
static::assertTrue(file_exists($attachment->getPath())); static::assertTrue(file_exists($attachment->getPath()));
}; };