[ATTACHMENTS] Even more further refactoring

Introduce Encoder plugins Instead of abstract upload and thumb modules

Ported attachment routes. In v3 thumbnail depends on existing attachment
so route updated accordingly.
This commit is contained in:
Diogo Peralta Cordeiro 2021-04-18 05:47:16 +01:00 committed by Hugo Sales
parent 0eaccc32fe
commit e385a9ac29
Signed by: someonewithpc
GPG Key ID: 7D0C7EAFC9D835A0
25 changed files with 113 additions and 2236 deletions

View File

@ -19,7 +19,7 @@
// }}}
namespace Plugin\ImageThumbnail\Controller;
namespace Plugin\ImageEncoder\Controller;
use App\Core\Controller;
use App\Core\DB\DB;

View File

@ -17,37 +17,75 @@
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
// }}}
namespace Plugin\ImageThumbnail;
namespace Plugin\ImageEncoder;
use App\Core\Event;
use function App\Core\I18n\_m;
use App\Core\Modules\Module;
use App\Core\Router\RouteLoader;
use App\Core\Log;
use App\Core\Modules\Plugin;
use App\Entity\Attachment;
use App\Entity\AttachmentThumbnail;
use App\Util\Common;
use Exception;
use Jcupitt\Vips;
use Symfony\Component\HttpFoundation\File\File as SymfonyFile;
class ImageThumbnail extends Module
class ImageEncoder extends Plugin
{
public function onAddRoute(RouteLoader $r)
/**
* Several obscure file types should be normalized to WebP on resize.
*
* Keeps only GIF (if animated) and WebP formats
*
* @return int
*/
public function preferredType(): int
{
$r->connect('thumbnail', '/thumbnail/{id<\d+>}', [Controller\ImageThumbnail::class, 'thumbnail']);
return Event::next;
if ($this->type == IMAGETYPE_GIF && $this->animated) {
return $this->type;
}
return IMAGETYPE_WEBP;
}
/**
* Resizes an image. It will reencode the image in the
* `self::prefferedType()` format. This only applies henceforward,
* Encodes the image to self::preferredType() format ensuring it's valid.
*
* @param SymfonyFile $sfile i/o
* @param null|string $mimetype out
*
* @return bool
*/
public function onAttachmentValidation(SymfonyFile &$sfile, ?string &$mimetype = null): bool
{
$original_mimetype = $mimetype ?? $sfile->getMimeType();
// TODO: Encode in place
$mimetype = self::preferredType();
return Event::stop;
}
/**
* Resizes an image. It will encode the image in the
* `self::preferredType()` format. This only applies henceforward,
* not retroactively
*
* Increases the 'memory_limit' to the one in the 'attachments' section in the config, to
* enable the handling of bigger images, which can cause a peak of memory consumption, while
* encoding
*
* @param Attachment $attachment
* @param AttachmentThumbnail $thumbnail
* @param int $width
* @param int $height
* @param bool $crop
*
* @throws Exception
* @throws Vips\Exception
*
* @return bool
*
*/
public function onResizeImage(Attachment $attachment, AttachmentThumbnail $thumbnail, int $width, int $height, bool $crop)
public function onResizeImage(Attachment $attachment, AttachmentThumbnail $thumbnail, int $width, int $height, bool $crop): bool
{
$old_limit = ini_set('memory_limit', Common::config('attachments', 'memory_limit'));

View File

@ -1,58 +0,0 @@
<?php
// 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\Media\Controller;
/**
* Download notice attachment
*
* @category Personal
* @package GNUsocial
*
* @author Mikael Nordfeldth <mmn@hethane.se>
* @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
*
* @see https:/gnu.io/social
*/
class AttachmentDownload extends Attachment
{
public function showPage(): void
{
// Disable errors, to not mess with the file contents (suppress errors in case access to this
// function is blocked, like in some shared hosts). Automatically reset at the end of the
// script execution, and we don't want to have any more errors until then, so don't reset it
@ini_set('display_errors', 0);
if ($this->attachment->isLocal()) {
try {
$this->filepath = $this->attachment->getFileOrThumbnailPath();
} catch (Exception $e) {
$this->clientError(
_m('Requested local URL for a file that is not stored locally.'),
404
);
}
common_send_file(
$this->filepath,
$this->mimetype,
$this->filename,
'attachment'
);
} else {
common_redirect($this->attachment->getUrl(), 303);
}
}
}

View File

@ -1,80 +0,0 @@
<?php
// 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\Media\Controller;
/**
* Show notice attachments
*
* @category Personal
* @package GNUsocial
*
* @author Evan Prodromou <evan@status.net>
* @copyright 2008-2009 StatusNet, Inc.
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class AttachmentThumbnail extends AttachmentView
{
protected $thumb_w; // max width
protected $thumb_h; // max height
protected $thumb_c; // crop?
protected function doPreparation()
{
parent::doPreparation();
$this->thumb_w = $this->int('w');
$this->thumb_h = $this->int('h');
$this->thumb_c = $this->boolean('c');
}
/**
* Show an inline representation of an attachment of the size
* requested in the GET variables (read in the constructor). Tries
* to send the most appropriate file with the correct size and
* headers or displays an error if it's not possible.
*
* @throws ClientException
* @throws ReflectionException
* @throws ServerException
*/
public function showPage(): void
{
// Returns a File_thumbnail object or throws exception if not available
$filename = $this->filename;
$filepath = $this->filepath;
try {
$thumbnail = $this->attachment->getThumbnail($this->thumb_w, $this->thumb_h, $this->thumb_c);
$file = $thumbnail->getFile();
} catch (UseFileAsThumbnailException $e) {
// With this exception, the file exists locally $e->file;
} catch (FileNotFoundException $e) {
$this->clientError(_m('No such attachment'), 404);
} catch (Exception $e) {
if (is_null($filepath)) {
$this->clientError(_m('No such thumbnail'), 404);
}
// Remote file
}
// Disable errors, to not mess with the file contents (suppress errors in case access to this
// function is blocked, like in some shared hosts). Automatically reset at the end of the
// script execution, and we don't want to have any more errors until then, so don't reset it
@ini_set('display_errors', 0);
common_send_file($filepath, image_type_to_mime_type(IMAGETYPE_WEBP), $filename, 'inline');
}
}

View File

@ -1,55 +0,0 @@
<?php
// 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\Media\Controller;
/**
* View notice attachment
*
* @package GNUsocial
*
* @author Miguel Dantas <biodantasgs@gmail.com>
* @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
*/
class AttachmentView extends Attachment
{
/** Placeholder */
public function showPage(): void
{
// Disable errors, to not mess with the file contents (suppress errors in case access to this
// function is blocked, like in some shared hosts). Automatically reset at the end of the
// script execution, and we don't want to have any more errors until then, so don't reset it
@ini_set('display_errors', 0);
if ($this->attachment->isLocal() || $this->attachment->isFetchedRemoteFile()) {
try {
$this->filepath = $this->attachment->getFileOrThumbnailPath();
} catch (Exception $e) {
$this->clientError(
_m('Requested local URL for a file that is not stored locally.'),
404
);
}
$disposition = 'attachment';
if (in_array(common_get_mime_media($this->mimetype), ['image', 'video'])) {
$disposition = 'inline';
}
common_send_file($this->filepath, $this->mimetype, $this->filename, $disposition);
} else {
common_redirect($this->attachment->getUrl(), 303);
}
}
}

View File

@ -1,48 +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\Media;
use App\Core\Event;
use App\Core\Modules\Module;
use App\Core\Router\RouteLoader;
class Media extends Module
{
/**
* Map URLs to Controllers
*/
public function onAddRoute(RouteLoader $r)
{
// foreach (['' => 'attachment',
// '/view' => 'attachment_view',
// '/download' => 'attachment_download',
// '/thumbnail' => 'attachment_thumbnail'] as $postfix => $action) {
// foreach (['filehash' => '[A-Za-z0-9._-]{64}',
// 'attachment' => '[0-9]+'] as $type => $match) {
// $r->connect($action, "attachment/:{$type}{$postfix}",
// ['action' => $action],
// [$type => $match]);
// }
// }
$r->connect('attachment', '/attachment/{filehash<[A-Za-z0-9._-]{64}>}', Controller\Attachment::class);
return Event::next;
}
}

View File

@ -1,685 +0,0 @@
<?php
// 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/>.
/**
* Abstraction for an image file
*
* @category Image
* @package GNUsocial
*
* @author Evan Prodromou <evan@status.net>
* @author Zach Copley <zach@status.net>
* @author Mikael Nordfeldth <mmn@hethane.se>
* @author Miguel Dantas <biodantasgs@gmail.com>
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @copyright 2008, 2019-2020 Free Software Foundation http://fsf.org
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
*/
namespace Plugin\Media\Util;
use Intervention\Image\ImageManagerStatic as Image;
/**
* A wrapper on uploaded images
*
* Makes it slightly easier to accept an image file from upload.
*
* @category Image
* @package GNUsocial
*
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @author Evan Prodromou <evan@status.net>
* @author Zach Copley <zach@status.net>
*
* @see https://www.gnu.org/software/social/
*/
class ImageFile extends MediaFile
{
public $type;
public $height;
public $width;
public $rotate = 0; // degrees to rotate for properly oriented image (extrapolated from EXIF etc.)
public $animated; // Animated image? (has more than 1 frame). null means untested
public $mimetype; // The _ImageFile_ mimetype, _not_ the originating File object
// /**
// * ImageFile constructor.
// *
// * @param int|null $id The DB id of the file. Int if known, null if not.
// * If null, it searches for it. If -1, it skips all DB
// * interactions (useful for temporary objects)
// * @param string $filepath The path of the file this media refers to. Required
// * @param string|null $filehash The hash of the file, if known. Optional
// *
// * @throws ClientException
// * @throws NoResultException
// * @throws ServerException
// * @throws UnsupportedMediaException
// */
// public function __construct(?int $id = null, string $filepath, ?string $filehash = null)
// {
// $old_limit = ini_set('memory_limit', common_config('attachments', 'memory_limit'));
// // These do not have to be the same as fileRecord->filename for example,
// // since we may have generated an image source file from something else!
// $this->filepath = $filepath;
// $this->filename = basename($filepath);
// $img = Image::make($this->filepath);
// $this->mimetype = $img->mime();
// $cmp = function ($obj, $type) {
// if ($obj->mimetype == image_type_to_mime_type($type)) {
// $obj->type = $type;
// return true;
// }
// return false;
// };
// if (!(($cmp($this, IMAGETYPE_GIF) && function_exists('imagecreatefromgif')) ||
// ($cmp($this, IMAGETYPE_JPEG) && function_exists('imagecreatefromjpeg')) ||
// ($cmp($this, IMAGETYPE_BMP) && function_exists('imagecreatefrombmp')) ||
// ($cmp($this, IMAGETYPE_WBMP) && function_exists('imagecreatefromwbmp')) ||
// ($cmp($this, IMAGETYPE_XBM) && function_exists('imagecreatefromxbm')) ||
// ($cmp($this, IMAGETYPE_PNG) && function_exists('imagecreatefrompng')) ||
// ($cmp($this, IMAGETYPE_WEBP) && function_exists('imagecreatefromwebp'))
// )
// ) {
// common_debug("Mimetype '{$this->mimetype}' was not recognized as a supported format");
// // TRANS: Exception thrown when trying to upload an unsupported image file format.
// throw new UnsupportedMediaException(_m('Unsupported image format.'), $this->filepath);
// }
// $this->width = $img->width();
// $this->height = $img->height();
// parent::__construct(
// $filepath,
// $this->mimetype,
// $filehash,
// $id
// );
// if ($this->type === IMAGETYPE_JPEG) {
// // Orientation value to rotate thumbnails properly
// $exif = @$img->exif();
// if (is_array($exif) && isset($exif['Orientation'])) {
// switch ((int)($exif['Orientation'])) {
// case 1: // top is top
// $this->rotate = 0;
// break;
// case 3: // top is bottom
// $this->rotate = 180;
// break;
// case 6: // top is right
// $this->rotate = -90;
// break;
// case 8: // top is left
// $this->rotate = 90;
// break;
// }
// // If we ever write this back, Orientation should be set to '1'
// }
// } elseif ($this->type === IMAGETYPE_GIF) {
// $this->animated = $this->isAnimatedGif();
// }
// Event::handle('FillImageFileMetadata', [$this]);
// $img->destroy();
// ini_set('memory_limit', $old_limit); // Restore the old memory limit
// }
/**
* Shortcut method to get an ImageFile from a File
*
* @param File $file
*
* @throws ClientException
* @throws FileNotFoundException
* @throws NoResultException
* @throws ServerException
* @throws UnsupportedMediaException
* @throws UseFileAsThumbnailException
*
* @return ImageFile
* @return ImageFile
*/
public static function fromFileObject(File $file)
{
$imgPath = null;
$media = common_get_mime_media($file->mimetype);
if (Event::handle('CreateFileImageThumbnailSource', [$file, &$imgPath, $media])) {
if (empty($file->filename) && !file_exists($imgPath)) {
throw new FileNotFoundException($imgPath);
}
// First some mimetype specific exceptions
switch ($file->mimetype) {
case 'image/svg+xml':
throw new UseFileAsThumbnailException($file);
}
// And we'll only consider it an image if it has such a media type
if ($media !== 'image') {
throw new UnsupportedMediaException(_m('Unsupported media format.'), $file->getPath());
}
$filepath = $file->getPath();
return new self($file->getID(), $filepath, $file->filehash);
}
}
public function getPath()
{
if (!file_exists($this->filepath)) {
throw new FileNotFoundException($this->filepath);
}
return $this->filepath;
}
/**
* Process a file upload
*
* Uses MediaFile's `fromUpload` to do the majority of the work
* and ensures the uploaded file is in fact an image.
*
* @param string $param
* @param null|Profile $scoped
*
* @throws NoResultException
* @throws NoUploadedMediaException
* @throws ServerException
* @throws UnsupportedMediaException
* @throws UseFileAsThumbnailException
* @throws ClientException
*
* @return ImageFile
*
*/
public static function fromUpload(string $param = 'upload', ?Profile $scoped = null): self
{
$mediafile = parent::fromUpload($param, $scoped);
if ($mediafile instanceof self) {
return $mediafile;
} else {
$mediafile->delete();
// We can conclude that we have failed to get the MIME type
// TRANS: Client exception thrown trying to upload an invalid image type.
// TRANS: %s is the file type that was denied
$hint = sprintf(_m('"%s" is not a supported file type on this server. ' .
'Try using another image format.'), $mediafile->mimetype);
throw new ClientException($hint);
}
}
/**
* Create a new ImageFile object from an url
*
* Uses MediaFile's `fromUrl` to do the majority of the work
* and ensures the uploaded file is in fact an image.
*
* @param string $url Remote image URL
* @param null|Profile $scoped
* @param null|string $name
* @param null|int $file_id same as in this class constructor
*
* @throws ClientException
* @throws HTTP_Request2_Exception
* @throws InvalidFilenameException
* @throws NoResultException
* @throws ServerException
* @throws UnsupportedMediaException
* @throws UseFileAsThumbnailException
*
* @return ImageFile
* @return ImageFile
*/
public static function fromUrl(string $url, ?Profile $scoped = null, ?string $name = null, ?int $file_id = null): self
{
$mediafile = parent::fromUrl($url, $scoped, $name, $file_id);
if ($mediafile instanceof self) {
return $mediafile;
} else {
$mediafile->delete();
// We can conclude that we have failed to get the MIME type
// TRANS: Client exception thrown trying to upload an invalid image type.
// TRANS: %s is the file type that was denied
$hint = sprintf(_m('"%s" is not a supported file type on this server. ' .
'Try using another image format.'), $mediafile->mimetype);
throw new ClientException($hint);
}
}
/**
* Several obscure file types should be normalized to WebP on resize.
*
* Keeps only GIF (if animated) and WebP formats
*
* @return int
*/
public function preferredType()
{
if ($this->type == IMAGETYPE_GIF && $this->animated) {
return $this->type;
}
return IMAGETYPE_WEBP;
}
/**
* Copy the image file to the given destination.
*
* This function may modify the resulting file. Please use the
* returned ImageFile object to read metadata (width, height etc.)
*
* @param string $outpath
*
* @throws NoResultException
* @throws ServerException
* @throws UnsupportedMediaException
* @throws UseFileAsThumbnailException
* @throws ClientException
*
* @return ImageFile the image stored at target path
*
*/
public function copyTo($outpath)
{
return new self(null, $this->resizeTo($outpath));
}
/**
* Create and save a thumbnail image.
*
* @param string $outpath
* @param array $box width, height, boundary box (x,y,w,h) defaults to full image
*
* @throws UnsupportedMediaException
* @throws UseFileAsThumbnailException
*
* @return string full local filesystem filename
* @return string full local filesystem filename
*
*/
public function resizeTo($outpath, array $box = [])
{
$box['width'] = isset($box['width']) ? (int) ($box['width']) : $this->width;
$box['height'] = isset($box['height']) ? (int) ($box['height']) : $this->height;
$box['x'] = isset($box['x']) ? (int) ($box['x']) : 0;
$box['y'] = isset($box['y']) ? (int) ($box['y']) : 0;
$box['w'] = isset($box['w']) ? (int) ($box['w']) : $this->width;
$box['h'] = isset($box['h']) ? (int) ($box['h']) : $this->height;
if (!file_exists($this->filepath)) {
// TRANS: Exception thrown during resize when image has been registered as present,
// but is no longer there.
throw new FileNotFoundException($this->filepath);
}
// Don't rotate/crop/scale if it isn't necessary
if ($box['width'] === $this->width
&& $box['height'] === $this->height
&& $box['x'] === 0
&& $box['y'] === 0
&& $box['w'] === $this->width
&& $box['h'] === $this->height
&& $this->type === $this->preferredType()) {
if (abs($this->rotate) == 90) {
// Box is rotated 90 degrees in either direction,
// so we have to redefine x to y and vice versa.
$tmp = $box['width'];
$box['width'] = $box['height'];
$box['height'] = $tmp;
$tmp = $box['x'];
$box['x'] = $box['y'];
$box['y'] = $tmp;
$tmp = $box['w'];
$box['w'] = $box['h'];
$box['h'] = $tmp;
}
}
$this->height = $box['h'];
$this->width = $box['w'];
if (Event::handle('StartResizeImageFile', [$this, $outpath, $box])) {
$outpath = $this->resizeToFile($outpath, $box);
}
if (!file_exists($outpath)) {
if ($this->fileRecord instanceof File) {
throw new UseFileAsThumbnailException($this->fileRecord);
} else {
throw new UnsupportedMediaException('No local File object exists for ImageFile.');
}
}
return $outpath;
}
/**
* Resizes a file. If $box is omitted, the size is not changed, but this is still useful,
* because it will reencode the image in the `self::prefferedType()` format. This only
* applies henceforward, not retroactively
*
* Increases the 'memory_limit' to the one in the 'attachments' section in the config, to
* enable the handling of bigger images, which can cause a peak of memory consumption, while
* encoding
*
* @param $outpath
* @param array $box
*
* @throws Exception
*/
protected function resizeToFile(string $outpath, array $box): string
{
$old_limit = ini_set('memory_limit', common_config('attachments', 'memory_limit'));
try {
$img = Image::make($this->filepath);
} catch (Exception $e) {
common_log(LOG_ERR, __METHOD__ . ' encountered exception: ' . print_r($e, true));
// TRANS: Exception thrown when trying to resize an unknown file type.
throw new Exception(_m('Unknown file type'));
}
if ($this->filepath === $outpath) {
@unlink($outpath);
}
if ($this->rotate != 0) {
$img = $img->orientate();
}
$img->fit(
$box['width'],
$box['height'],
function ($constraint) {
if (common_config('attachments', 'upscale') !== true) {
$constraint->upsize(); // Prevent upscaling
}
}
);
// Ensure we save in the correct format and allow customization based on type
$type = $this->preferredType();
switch ($type) {
case IMAGETYPE_WEBP:
$img->save($outpath, 100, 'webp');
break;
case IMAGETYPE_GIF:
$img->save($outpath, 100, 'gif');
break;
default:
// TRANS: Exception thrown when trying resize an unknown file type.
throw new Exception(_m('Unknown file type'));
}
$img->destroy();
ini_set('memory_limit', $old_limit); // Restore the old memory limit
return $outpath;
}
public function scaleToFit($maxWidth = null, $maxHeight = null, $crop = null)
{
return self::getScalingValues(
$this->width,
$this->height,
$maxWidth,
$maxHeight,
$crop,
$this->rotate
);
}
/**
* Gets scaling values for images of various types. Cropping can be enabled.
*
* Values will scale _up_ to fit max values if cropping is enabled!
* With cropping disabled, the max value of each axis will be respected.
*
* @param $width int Original width
* @param $height int Original height
* @param $maxW int Resulting max width
* @param $maxH int Resulting max height
* @param $crop int Crop to the size (not preserving aspect ratio)
* @param int $rotate
*
* @throws ServerException
*
* @return array
*
*/
public static function getScalingValues(
$width,
$height,
$maxW = null,
$maxH = null,
$crop = null,
$rotate = 0
) {
$maxW = $maxW ?: common_config('thumbnail', 'width');
$maxH = $maxH ?: common_config('thumbnail', 'height');
if ($maxW < 1 || ($maxH !== null && $maxH < 1)) {
throw new ServerException('Bad parameters for ImageFile::getScalingValues');
}
if ($maxH === null) {
// if maxH is null, we set maxH to equal maxW and enable crop
$maxH = $maxW;
$crop = true;
}
// Because GD doesn't understand EXIF orientation etc.
if (abs($rotate) == 90) {
$tmp = $width;
$width = $height;
$height = $tmp;
}
// Cropping data (for original image size). Default values, 0 and null,
// imply no cropping and with preserved aspect ratio (per axis).
$cx = 0; // crop x
$cy = 0; // crop y
$cw = null; // crop area width
$ch = null; // crop area height
if ($crop) {
$s_ar = $width / $height;
$t_ar = $maxW / $maxH;
$rw = $maxW;
$rh = $maxH;
// Source aspect ratio differs from target, recalculate crop points!
if ($s_ar > $t_ar) {
$cx = floor($width / 2 - $height * $t_ar / 2);
$cw = ceil($height * $t_ar);
} elseif ($s_ar < $t_ar) {
$cy = floor($height / 2 - $width / $t_ar / 2);
$ch = ceil($width / $t_ar);
}
} else {
$rw = $maxW;
$rh = ceil($height * $rw / $width);
// Scaling caused too large height, decrease to max accepted value
if ($rh > $maxH) {
$rh = $maxH;
$rw = ceil($width * $rh / $height);
}
}
return [(int) $rw, (int) $rh,
(int) $cx, (int) $cy,
is_null($cw) ? $width : (int) $cw,
is_null($ch) ? $height : (int) $ch, ];
}
/**
* Animated GIF test, courtesy of frank at huddler dot com et al:
* http://php.net/manual/en/function.imagecreatefromgif.php#104473
* Modified so avoid landing inside of a header (and thus not matching our regexp).
*/
protected function isAnimatedGif()
{
if (!($fh = @fopen($this->filepath, 'rb'))) {
return false;
}
$count = 0;
//an animated gif contains multiple "frames", with each frame having a
//header made up of:
// * a static 4-byte sequence (\x00\x21\xF9\x04)
// * 4 variable bytes
// * a static 2-byte sequence (\x00\x2C)
// In total the header is maximum 10 bytes.
// We read through the file til we reach the end of the file, or we've found
// at least 2 frame headers
while (!feof($fh) && $count < 2) {
$chunk = fread($fh, 1024 * 100); //read 100kb at a time
$count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00\x2C#s', $chunk, $matches);
// rewind in case we ended up in the middle of the header, but avoid
// infinite loop (i.e. don't rewind if we're already in the end).
if (!feof($fh) && ftell($fh) >= 9) {
fseek($fh, -9, SEEK_CUR);
}
}
fclose($fh);
return $count >= 1; // number of animated frames apart from the original image
}
/**
* @param $width
* @param $height
* @param $crop
* @param false $upscale
*
* @throws ClientException
* @throws FileNotFoundException
* @throws FileNotStoredLocallyException
* @throws InvalidFilenameException
* @throws ServerException
* @throws UnsupportedMediaException
* @throws UseFileAsThumbnailException
*
* @return File_thumbnail
*/
public function getFileThumbnail($width = null, $height = null, $crop = null, $upscale = false)
{
if (!$this->fileRecord instanceof File) {
throw new ServerException('No File object attached to this ImageFile object.');
}
// Throws FileNotFoundException or FileNotStoredLocallyException
$this->filepath = $this->fileRecord->getFileOrThumbnailPath();
$filename = basename($this->filepath);
if ($width === null) {
$width = common_config('thumbnail', 'width');
$height = common_config('thumbnail', 'height');
$crop = common_config('thumbnail', 'crop');
}
if (!$upscale) {
if ($width > $this->width) {
$width = $this->width;
}
if (!is_null($height) && $height > $this->height) {
$height = $this->height;
}
}
if ($height === null) {
$height = $width;
$crop = true;
}
// Get proper aspect ratio width and height before lookup
// We have to do it through an ImageFile object because of orientation etc.
// Only other solution would've been to rotate + rewrite uploaded files
// which we don't want to do because we like original, untouched data!
list($width, $height, $x, $y, $w, $h) = $this->scaleToFit($width, $height, $crop);
$thumb = File_thumbnail::pkeyGet([
'file_id' => $this->fileRecord->getID(),
'width' => $width,
'height' => $height,
]);
if ($thumb instanceof File_thumbnail) {
$this->height = $height;
$this->width = $width;
return $thumb;
}
$type = $this->preferredType();
$ext = image_type_to_extension($type, true);
// Decoding returns null if the file is in the old format
$filename = MediaFile::decodeFilename(basename($this->filepath));
// Encoding null makes the file use 'untitled', and also replaces the extension
$outfilename = MediaFile::encodeFilename($filename, $this->filehash, $ext);
// The boundary box for our resizing
$box = [
'width' => $width, 'height' => $height,
'x' => $x, 'y' => $y,
'w' => $w, 'h' => $h,
];
$outpath = File_thumbnail::path(
"thumb-{$this->fileRecord->id}-{$box['width']}x{$box['height']}-{$outfilename}"
);
// Doublecheck that parameters are sane and integers.
if ($box['width'] < 1 || $box['width'] > common_config('thumbnail', 'maxsize')
|| $box['height'] < 1 || $box['height'] > common_config('thumbnail', 'maxsize')
|| $box['w'] < 1 || $box['x'] >= $this->width
|| $box['h'] < 1 || $box['y'] >= $this->height) {
// Fail on bad width parameter. If this occurs, it's due to algorithm in ImageFile->scaleToFit
common_debug("Boundary box parameters for resize of {$this->filepath} : " . var_export($box, true));
throw new ServerException('Bad thumbnail size parameters.');
}
common_debug(sprintf(
'Generating a thumbnail of File id=%u of size %ux%u',
$this->fileRecord->getID(),
$width,
$height
));
$this->height = $box['height'];
$this->width = $box['width'];
// Perform resize and store into file
$outpath = $this->resizeTo($outpath, $box);
$outname = basename($outpath);
return File_thumbnail::saveThumbnail(
$this->fileRecord->getID(),
$this->fileRecord->getUrl(false),
$width,
$height,
$outname
);
}
}

View File

@ -1,934 +0,0 @@
<?php
// 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/>.
/**
* Abstraction for media files
*
* @category Media
* @package GNUsocial
*
* @author Robin Millette <robin@millette.info>
* @author Miguel Dantas <biodantas@gmail.com>
* @author Zach Copley <zach@status.net>
* @author Mikael Nordfeldth <mmn@hethane.se>
* @author Diogo Peralta Cordeiro <mail+gnusocial@diogo.site>
* @copyright 2008-2009, 2019-2021 Free Software Foundation http://fsf.org
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
*/
namespace Plugin\Media\Util;
/**
* Class responsible for abstracting media files
*/
class MediaFile
{
public $id;
public $filepath;
public $filename;
public $fileRecord;
public $fileurl;
public $short_fileurl;
public $mimetype;
// /**
// * MediaFile constructor.
// *
// * @param string $filepath The path of the file this media refers to. Required
// * @param string $mimetype The mimetype of the file. Required
// * @param string|null $filehash The hash of the file, if known. Optional
// * @param int|null $id The DB id of the file. Int if known, null if not.
// * If null, it searches for it. If -1, it skips all DB
// * interactions (useful for temporary objects)
// *
// * @throws ClientException
// * @throws NoResultException
// * @throws ServerException
// */
// public function __construct(string $filepath, string $mimetype, ?string $filehash = null, ?int $id = null)
// {
// $this->filepath = $filepath;
// $this->filename = basename($this->filepath);
// $this->mimetype = $mimetype;
// $this->filehash = self::getHashOfFile($this->filepath, $filehash);
// $this->id = $id;
// // If id is -1, it means we're dealing with a temporary object and don't want to store it in the DB,
// // or add redirects
// if ($this->id !== -1) {
// if (!empty($this->id)) {
// // If we have an id, load it
// $this->fileRecord = new File();
// $this->fileRecord->id = $this->id;
// if (!$this->fileRecord->find(true)) {
// // If we have set an ID, we need that ID to exist!
// throw new NoResultException($this->fileRecord);
// }
// } else {
// // Otherwise, store it
// $this->fileRecord = $this->storeFile();
// }
// $this->fileurl = common_local_url(
// 'attachment',
// ['attachment' => $this->fileRecord->id]
// );
// $this->short_fileurl = common_shorten_url($this->fileurl);
// }
// }
/**
* Shortcut method to get a MediaFile from a File
*
* @param File $file
*
* @throws ClientException
* @throws FileNotFoundException
* @throws NoResultException
* @throws ServerException
*
* @return ImageFile|MediaFile
*/
public static function fromFileObject(File $file)
{
$filepath = null;
try {
$filepath = $file->getPath();
} catch (Exception $e) {
}
return new self($filepath, common_get_mime_media($file->mimetype), $file->filehash, $file->getID());
}
public function attachToNotice(Notice $notice)
{
File_to_post::processNew($this->fileRecord, $notice);
}
public function getPath()
{
return File::path($this->filename);
}
/**
* @param null|bool $use_local true means require local, null means prefer original, false means use whatever is stored
*
* @return string
*/
public function getUrl(?bool $use_local = null): ?string
{
if ($use_local !== false) {
if (empty($this->fileurl)) {
// A locally stored file, so let's generate a URL for our instance.
return common_local_url('attachment_view', ['filehash' => $this->filehash]);
}
}
// The original file's URL
return $this->fileurl;
}
public function shortUrl()
{
return common_shorten_url($this->getUrl());
}
public function getEnclosure()
{
return $this->getFile()->getEnclosure();
}
public function delete($useWhere = false)
{
if (!is_null($this->fileRecord)) {
$this->fileRecord->delete($useWhere);
}
@unlink($this->filepath);
}
/**
* Remove the file from filesystem
*/
public function unlink()
{
$this->filename = null;
// Delete the file, if it exists locally
if (!empty($this->filepath) && file_exists($this->filepath)) {
$deleted = @unlink($this->filepath);
if (!$deleted) {
common_log(LOG_ERR, sprintf('Could not unlink existing file: "%s"', $this->filepath));
}
}
$this->fileRecord->unlink();
}
public function getFile()
{
if (!$this->fileRecord instanceof File) {
throw new ServerException('File record did not exist for MediaFile');
}
return $this->fileRecord;
}
/**
* Calculate the hash of a file.
*
* This won't work for files >2GiB because PHP uses only 32bit.
*
* @param string $filepath
* @param null|string $filehash
*
* @throws ServerException
*
* @return string
*
*/
public static function getHashOfFile(string $filepath, $filehash = null)
{
assert(!empty($filepath), __METHOD__ . ': filepath cannot be null');
if ($filehash === null) {
// Calculate if we have an older upload method somewhere (Qvitter) that
// doesn't do this before calling new MediaFile on its local files...
$filehash = hash_file(File::FILEHASH_ALG, $filepath);
if ($filehash === false) {
throw new ServerException('Could not read file for hashing');
}
}
return $filehash;
}
/**
* Retrieve or insert as a file in the DB
*
* @throws ServerException
* @throws ClientException
*
* @return object File
*
*/
protected function storeFile()
{
try {
$file = File::getByHash($this->filehash);
if (is_null($this->fileurl) && is_null($file->getUrl(false))) {
// An already existing local file is being re-added, return it
return $file;
}
} catch (NoResultException $e) {
// Well, let's just continue below.
}
$file = new File;
$file->filename = $this->filename;
$file->url = $this->fileurl;
$file->urlhash = is_null($file->url) ? null : File::hashurl($file->url);
$file->filehash = $this->filehash;
$file->size = filesize($this->filepath);
if ($file->size === false) {
throw new ServerException('Could not read file to get its size');
}
$file->date = time();
$file->mimetype = $this->mimetype;
$file_id = $file->insert();
if ($file_id === false) {
common_log_db_error($file, 'INSERT', __FILE__);
// TRANS: Client exception thrown when a database error was thrown during a file upload operation.
throw new ClientException(_m('There was a database error while saving your file. Please try again.'));
}
// Set file geometrical properties if available
try {
$image = ImageFile::fromFileObject($file);
$orig = clone $file;
$file->width = $image->width;
$file->height = $image->height;
$file->update($orig);
// We have to cleanup after ImageFile, since it
// may have generated a temporary file from a
// video support plugin or something.
// FIXME: Do this more automagically.
// Honestly, I think this is unlikely these days,
// but better be safe than sorry, I guess
if ($image->getPath() != $file->getPath()) {
$image->unlink();
}
} catch (ServerException $e) {
// We just couldn't make out an image from the file. This
// does not have to be UnsupportedMediaException, as we can
// also get ServerException from files not existing etc.
}
return $file;
}
/**
* The maximum allowed file size, as a string
*/
public static function maxFileSize()
{
$value = self::maxFileSizeInt();
if ($value > 1024 * 1024) {
$value = $value / (1024 * 1024);
// TRANS: Number of megabytes. %d is the number.
return sprintf(_m('%dMB', '%dMB', $value), $value);
} elseif ($value > 1024) {
$value = $value / 1024;
// TRANS: Number of kilobytes. %d is the number.
return sprintf(_m('%dkB', '%dkB', $value), $value);
} else {
// TRANS: Number of bytes. %d is the number.
return sprintf(_m('%dB', '%dB', $value), $value);
}
}
/**
* The maximum allowed file size, as an int
*/
public static function maxFileSizeInt(): int
{
return common_config('attachments', 'file_quota');
}
/**
* Encodes a file name and a file hash in the new file format, which is used to avoid
* having an extension in the file, removing trust in extensions, while keeping the original name
*
* @param null|string $original_name
* @param string $filehash
* @param null|bool|string $ext from File::getSafeExtension
*
* @throws ClientException
* @throws ServerException
*
* @return string
*/
public static function encodeFilename($original_name, string $filehash, $ext = null): string
{
if (empty($original_name)) {
$original_name = _m('Untitled attachment');
}
// If we're given an extension explicitly, use it, otherwise...
$ext = $ext ?:
// get a replacement extension if configured, returns false if it's blocked,
// null if no extension
File::getSafeExtension($original_name);
if ($ext === false) {
throw new ClientException(_m('Blacklisted file extension.'));
}
if (!empty($ext)) {
// Remove dots if we have them (make sure they're not repeated)
$ext = preg_replace('/^\.+/', '', $ext);
$original_name = preg_replace('/\.+.+$/i', ".{$ext}", $original_name);
}
// Avoid unnecessarily large file names
$pretty_name = substr(trim($original_name), 0, 30); // 30 seems like a sensible limit for a file name
$enc_name = bin2hex($pretty_name);
return "{$enc_name}-{$filehash}";
}
/**
* Decode the new filename format
*
* @return false | null | string on failure, no match (old format) or original file name, respectively
*/
public static function decodeFilename(string $encoded_filename)
{
// Should match:
// hex-hash
// thumb-id-widthxheight-hex-hash
// And return the `hex` part
$ret = preg_match('/^(.*-)?([^-]+)-[^-]+$/', $encoded_filename, $matches);
if ($ret === false) {
return false;
} elseif ($ret === 0 || !ctype_xdigit($matches[2])) {
return null; // No match
} else {
$filename = hex2bin($matches[2]);
// Matches extension
if (preg_match('/^(.+?)\.(.+)$/', $filename, $sub_matches) === 1) {
$ext = $sub_matches[2];
// Previously, there was a blacklisted extension array, which could have an alternative
// extension, such as phps, to replace php. We want to turn it back (this is deprecated,
// as it no longer makes sense, since we don't trust trust files based on extension,
// but keep the feature)
$blacklist = common_config('attachments', 'extblacklist');
if (is_array($blacklist)) {
foreach ($blacklist as $upload_ext => $safe_ext) {
if ($ext === $safe_ext) {
$ext = $upload_ext;
break;
}
}
}
return "{$sub_matches[1]}.{$ext}";
} else {
// No extension, don't bother trying to replace it
return $filename;
}
}
}
/**
* Create a new MediaFile or ImageFile object from an upload
*
* Tries to set the mimetype correctly, using the most secure method available and rejects the file otherwise.
* In case the upload is an image, this function returns an new ImageFile (which extends MediaFile)
* The filename has a new format:
* bin2hex("{$original_name}.{$ext}")."-{$filehash}"
* This format should be respected. Notice the dash, which is important to distinguish it from the previous
* format ("{$hash}.{$ext}")
*
* @param string $param Form name
* @param null|Profile $scoped
*
* @throws ClientException
* @throws InvalidFilenameException
* @throws NoResultException
* @throws NoUploadedMediaException
* @throws ServerException
* @throws UnsupportedMediaException
* @throws UseFileAsThumbnailException
*
* @return ImageFile|MediaFile
*/
public static function fromUpload(string $param = 'media', ?Profile $scoped = null)
{
// The existence of the "error" element means PHP has processed it properly even if it was ok.
if (!(isset($_FILES[$param], $_FILES[$param]['error']))) {
throw new NoUploadedMediaException($param);
}
switch ($_FILES[$param]['error']) {
case UPLOAD_ERR_OK: // success, jump out
break;
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
// TRANS: Exception thrown when too large a file is uploaded.
// TRANS: %s is the maximum file size, for example "500b", "10kB" or "2MB".
throw new ClientException(sprintf(
_m('That file is too big. The maximum file size is %s.'),
self::maxFileSize()
));
case UPLOAD_ERR_PARTIAL:
@unlink($_FILES[$param]['tmp_name']);
// TRANS: Client exception.
throw new ClientException(_m('The uploaded file was only partially uploaded.'));
case UPLOAD_ERR_NO_FILE:
// No file; probably just a non-AJAX submission.
throw new NoUploadedMediaException($param);
case UPLOAD_ERR_NO_TMP_DIR:
// TRANS: Client exception thrown when a temporary folder is not present to store a file upload.
throw new ClientException(_m('Missing a temporary folder.'));
case UPLOAD_ERR_CANT_WRITE:
// TRANS: Client exception thrown when writing to disk is not possible during a file upload operation.
throw new ClientException(_m('Failed to write file to disk.'));
case UPLOAD_ERR_EXTENSION:
// TRANS: Client exception thrown when a file upload operation has been stopped by an extension.
throw new ClientException(_m('File upload stopped by extension.'));
default:
common_log(LOG_ERR, __METHOD__ . ': Unknown upload error ' . $_FILES[$param]['error']);
// TRANS: Client exception thrown when a file upload operation has failed with an unknown reason.
throw new ClientException(_m('System error uploading file.'));
}
$filehash = strtolower(self::getHashOfFile($_FILES[$param]['tmp_name']));
$fileid = null;
try {
$file = File::getByHash($filehash);
// There can be more than one file for the same filehash IF the url are different (due to different metadata).
while ($file->fetch()) {
if ($file->getUrl(false)) {
// Files uploaded by Actors of this instance won't have an url, skip.
continue;
}
try {
return ImageFile::fromFileObject($file);
} catch (UnsupportedMediaException $e) {
return self::fromFileObject($file);
}
}
// Assert: If we got to this line, then we only traversed URLs on the while loop above.
if (common_config('attachments', 'prefer_remote')) {
// Was this file imported from a remote source already?
$filepath = $file->getPath(); // This function will throw FileNotFoundException if not.
// Assert: If we got to this line, then we can use this file and just add redirections.
$mimetype = $file->mimetype;
$fileid = $file->getID();
} else {
throw new FileNotFoundException('This isn\'t a path.'); // A bit of dadaist art.
// It's natural that a sysadmin prefers to not add redirections to the first remote link of an
// attachment, it's not a very consistent thing to do. On the other hand, lack of space can drive
// a person crazy.
// Also note that if one configured StoreRemoteMedia to not save original images (very likely), then
// having prefer_remote enabled will never store the original attachment (sort of the idea here).
}
} catch (FileNotFoundException | NoResultException $e) {
// We have to save the upload as a new local file. This is the normal course of action.
if ($scoped instanceof Profile) {
// Throws exception if additional size does not respect quota
// This test is only needed, of course, if we're uploading something new.
File::respectsQuota($scoped, $_FILES[$param]['size']);
}
$mimetype = self::getUploadedMimeType($_FILES[$param]['tmp_name'], $_FILES[$param]['name']);
$media = common_get_mime_media($mimetype);
$basename = basename($_FILES[$param]['name']);
if ($media == 'image') {
// Use -1 for the id to avoid adding this temporary file to the DB
$img = new ImageFile(-1, $_FILES[$param]['tmp_name']);
// Validate the image by re-encoding it. Additionally normalizes old formats to WebP,
// keeping GIF untouched if animated
$outpath = $img->resizeTo($img->filepath);
$ext = image_type_to_extension($img->preferredType(), false);
}
$filename = self::encodeFilename($basename, $filehash, isset($ext) ? $ext : File::getSafeExtension($basename));
$filepath = File::path($filename);
if ($media == 'image') {
$result = rename($outpath, $filepath);
} else {
$result = move_uploaded_file($_FILES[$param]['tmp_name'], $filepath);
}
if (!$result) {
// TRANS: Client exception thrown when a file upload operation fails because the file could
// TRANS: not be moved from the temporary folder to the permanent file location.
// UX: too specific
throw new ClientException(_m('File could not be moved to destination directory.'));
}
if ($media == 'image') {
return new ImageFile(null, $filepath, $filehash);
}
}
return new self($filepath, $mimetype, $filehash, $fileid);
}
/**
* Create a new MediaFile or ImageFile object from an url
*
* Tries to set the mimetype correctly, using the most secure method available and rejects the file otherwise.
* In case the url is an image, this function returns an new ImageFile (which extends MediaFile)
* The filename has the following format: bin2hex("{$original_name}.{$ext}")."-{$filehash}"
*
* @param string $url Remote media URL
* @param null|Profile $scoped
* @param null|string $name
* @param null|int $file_id same as in this class constructor
*
* @throws ClientException
* @throws HTTP_Request2_Exception
* @throws InvalidFilenameException
* @throws NoResultException
* @throws ServerException
* @throws UnsupportedMediaException
* @throws UseFileAsThumbnailException
*
* @return ImageFile|MediaFile
* @return ImageFile|MediaFile
*/
public static function fromUrl(string $url, ?Profile $scoped = null, ?string $name = null, ?int $file_id = null)
{
if (!common_valid_http_url($url)) {
// TRANS: Server exception. %s is a URL.
throw new ServerException(sprintf('Invalid remote media URL %s.', $url));
}
$http = new HTTPClient();
common_debug(sprintf('Performing HEAD request for incoming activity to avoid ' .
'unnecessarily downloading too large files. URL: %s',
$url));
$head = $http->head($url);
$url = $head->getEffectiveUrl(); // to avoid going through redirects again
if (empty($url)) {
throw new ServerException(sprintf('URL after redirects is somehow empty, for URL %s.', $url));
}
$headers = $head->getHeader();
$headers = array_change_key_case($headers, CASE_LOWER);
if (array_key_exists('content-length', $headers)) {
$fileQuota = common_config('attachments', 'file_quota');
$fileSize = $headers['content-length'];
if ($fileSize > $fileQuota) {
// TRANS: Message used to be inserted as %2$s in the text "No file may
// TRANS: be larger than %1$d byte and the file you sent was %2$s.".
// TRANS: %1$d is the number of bytes of an uploaded file.
$fileSizeText = sprintf(_m('%1$d byte', '%1$d bytes', $fileSize), $fileSize);
// TRANS: Message given if an upload is larger than the configured maximum.
// TRANS: %1$d (used for plural) is the byte limit for uploads,
// TRANS: %2$s is the proper form of "n bytes". This is the only ways to have
// TRANS: gettext support multiple plurals in the same message, unfortunately...
throw new ClientException(
sprintf(
_m(
'No file may be larger than %1$d byte and the file you sent was %2$s. Try to upload a smaller version.',
'No file may be larger than %1$d bytes and the file you sent was %2$s. Try to upload a smaller version.',
$fileQuota
),
$fileQuota,
$fileSizeText
)
);
}
} else {
throw new ServerException(sprintf('Invalid remote media URL headers %s.', $url));
}
unset($head, $headers);
$tempfile = new TemporaryFile('gs-mediafile');
fwrite($tempfile->getResource(), HTTPClient::quickGet($url));
fflush($tempfile->getResource());
$filehash = strtolower(self::getHashOfFile($tempfile->getRealPath()));
try {
$file = File::getByUrl($url);
/*
* If no exception is thrown the file exists locally, so we'll use
* that and just add redirections.
* But if the _actual_ locally stored file doesn't exist, getPath
* will throw FileNotFoundException.
*/
$filepath = $file->getPath();
$mimetype = $file->mimetype;
} catch (FileNotFoundException | NoResultException $e) {
// We have to save the downloaded as a new local file.
// This is the normal course of action.
if ($scoped instanceof Profile) {
// Throws exception if additional size does not respect quota
// This test is only needed, of course, if something new is uploaded.
File::respectsQuota($scoped, filesize($tempfile->getRealPath()));
}
$mimetype = self::getUploadedMimeType(
$tempfile->getRealPath(),
$name ?? false
);
$media = common_get_mime_media($mimetype);
$basename = basename($name ?? ('media' . common_timestamp()));
if ($media === 'image') {
// Use -1 for the id to avoid adding this temporary file to the DB.
$img = new ImageFile(-1, $tempfile->getRealPath());
// Validate the image by re-encoding it.
// Additionally normalises old formats to PNG,
// keeping JPEG and GIF untouched.
$outpath = $img->resizeTo($img->filepath);
$ext = image_type_to_extension($img->preferredType(), false);
}
$filename = self::encodeFilename(
$basename,
$filehash,
$ext ?? File::getSafeExtension($basename)
);
$filepath = File::path($filename);
if ($media === 'image') {
$result = rename($outpath, $filepath);
} else {
try {
$tempfile->commit($filepath);
$result = true;
} catch (TemporaryFileException $e) {
$result = false;
}
}
if (!$result) {
// TRANS: Server exception thrown when a file upload operation fails because the file could
// TRANS: not be moved from the temporary directory to the permanent file location.
throw new ServerException(_m('File could not be moved to destination directory.'));
}
if ($media === 'image') {
return new ImageFile($file_id, $filepath, $filehash, $url);
}
}
return new self($filepath, $mimetype, $filehash, $file_id, $url);
}
/**
* Construct media fiile from an upload
*/
public static function fromFileInfo(SplFileInfo $finfo, Profile $scoped = null)
{
$filehash = hash_file(File::FILEHASH_ALG, $finfo->getRealPath());
try {
$file = File::getByHash($filehash);
$file->fetch();
// Already have it, so let's reuse the locally stored File
// by using getPath we also check whether the file exists
// and throw a FileNotFoundException with the path if it doesn't.
$filename = basename($file->getPath());
$mimetype = $file->mimetype;
} catch (FileNotFoundException $e) {
// This happens if the file we have uploaded has disappeared
// from the local filesystem for some reason. Since we got the
// File object from a sha256 check in fromFileInfo, it's safe
// to just copy the uploaded data to disk!
// dump the contents of our filehandle to the path from our exception
// and report error if it failed.
if (file_put_contents($e->path, file_get_contents($finfo->getRealPath())) === false) {
// TRANS: Client exception thrown when a file upload operation fails because the file could
// TRANS: not be moved from the temporary folder to the permanent file location.
throw new ClientException(_m('File could not be moved to destination directory.'));
}
if (!chmod($e->path, 0664)) {
common_log(LOG_ERR, 'Could not chmod uploaded file: ' . _ve($e->path));
}
$filename = basename($file->getPath());
$mimetype = $file->mimetype;
} catch (NoResultException $e) {
if ($scoped instanceof Profile) {
File::respectsQuota($scoped, filesize($finfo->getRealPath()));
}
$mimetype = self::getUploadedMimeType($finfo->getRealPath());
$filename = strtolower($filehash) . '.' . File::guessMimeExtension($mimetype);
$filepath = File::path($filename);
$result = copy($finfo->getRealPath(), $filepath) && chmod($filepath, 0664);
if (!$result) {
common_log(LOG_ERR, 'File could not be moved (or chmodded) from ' . _ve($stream['uri']) . ' to ' . _ve($filepath));
// TRANS: Client exception thrown when a file upload operation fails because the file could
// TRANS: not be moved from the temporary folder to the permanent file location.
throw new ClientException(_m('File could not be moved to destination directory.'));
}
}
return new self($filename, $mimetype, $filehash);
}
/**
* Attempt to identify the content type of a given file.
*
* @param string $filepath filesystem path as string (file must exist)
* @param bool $originalFilename (optional) for extension-based detection
*
* @throws ServerException
* @throws ClientException if type is known, but not supported for local uploads
*
* @return string
*
* @fixme this seems to tie a front-end error message in, kinda confusing
*
*
*/
public static function getUploadedMimeType(string $filepath, $originalFilename = false)
{
// We only accept filenames to existing files
$mimetype = null;
// From CodeIgniter
// We'll need this to validate the MIME info string (e.g. text/plain; charset=us-ascii)
$regexp = '/^([a-z\-]+\/[a-z0-9\-\.\+]+)(;\s[^\/]+)?$/';
/**
* Fileinfo extension - most reliable method
*
* Apparently XAMPP, CentOS, cPanel and who knows what
* other PHP distribution channels EXPLICITLY DISABLE
* ext/fileinfo, which is otherwise enabled by default
* since PHP 5.3 ...
*/
if (function_exists('finfo_file')) {
$finfo = @finfo_open(FILEINFO_MIME);
// It is possible that a FALSE value is returned, if there is no magic MIME database
// file found on the system
if (is_resource($finfo)) {
$mime = @finfo_file($finfo, $filepath);
finfo_close($finfo);
/* According to the comments section of the PHP manual page,
* it is possible that this function returns an empty string
* for some files (e.g. if they don't exist in the magic MIME database)
*/
if (is_string($mime) && preg_match($regexp, $mime, $matches)) {
$mimetype = $matches[1];
}
}
}
/* This is an ugly hack, but UNIX-type systems provide a "native" way to detect the file type,
* which is still more secure than depending on the value of $_FILES[$field]['type'], and as it
* was reported in issue #750 (https://github.com/EllisLab/CodeIgniter/issues/750) - it's better
* than mime_content_type() as well, hence the attempts to try calling the command line with
* three different functions.
*
* Notes:
* - the DIRECTORY_SEPARATOR comparison ensures that we're not on a Windows system
* - many system admins would disable the exec(), shell_exec(), popen() and similar functions
* due to security concerns, hence the function_usable() checks
*/
if (DIRECTORY_SEPARATOR !== '\\') {
$cmd = 'file --brief --mime ' . escapeshellarg($filepath) . ' 2>&1';
if (empty($mimetype) && function_exists('exec')) {
/* This might look confusing, as $mime is being populated with all of the output
* when set in the second parameter. However, we only need the last line, which is
* the actual return value of exec(), and as such - it overwrites anything that could
* already be set for $mime previously. This effectively makes the second parameter a
* dummy value, which is only put to allow us to get the return status code.
*/
$mime = @exec($cmd, $mime, $return_status);
if ($return_status === 0 && is_string($mime) && preg_match($regexp, $mime, $matches)) {
$mimetype = $matches[1];
}
}
if (empty($mimetype) && function_exists('shell_exec')) {
$mime = @shell_exec($cmd);
if (strlen($mime) > 0) {
$mime = explode("\n", trim($mime));
if (preg_match($regexp, $mime[(count($mime) - 1)], $matches)) {
$mimetype = $matches[1];
}
}
}
if (empty($mimetype) && function_exists('popen')) {
$proc = @popen($cmd, 'r');
if (is_resource($proc)) {
$mime = @fread($proc, 512);
@pclose($proc);
if ($mime !== false) {
$mime = explode("\n", trim($mime));
if (preg_match($regexp, $mime[(count($mime) - 1)], $matches)) {
$mimetype = $matches[1];
}
}
}
}
}
// Fall back to mime_content_type(), if available (still better than $_FILES[$field]['type'])
if (empty($mimetype) && function_exists('mime_content_type')) {
$mimetype = @mime_content_type($filepath);
// It's possible that mime_content_type() returns FALSE or an empty string
if ($mimetype == false && strlen($mimetype) > 0) {
throw new ServerException(_m('Could not determine file\'s MIME type.'));
}
}
// Unclear types are such that we can't really tell by the auto
// detect what they are (.bin, .exe etc. are just "octet-stream")
$unclearTypes = ['application/octet-stream',
'application/vnd.ms-office',
'application/zip',
'text/plain',
'text/html', // Ironically, Wikimedia Commons' SVG_logo.svg is identified as text/html
// TODO: for XML we could do better content-based sniffing too
'text/xml', ];
$supported = common_config('attachments', 'supported');
// If we didn't match, or it is an unclear match
if ($originalFilename && (!$mimetype || in_array($mimetype, $unclearTypes))) {
try {
$type = common_supported_filename_to_mime($originalFilename);
return $type;
} catch (UnknownExtensionMimeException $e) {
// FIXME: I think we should keep the file extension here (supported should be === true here)
} catch (Exception $e) {
// Extension parsed but no connected mimetype, so $mimetype is our best guess
}
}
// If $config['attachments']['supported'] equals boolean true, accept any mimetype
if ($supported === true || array_key_exists($mimetype, $supported)) {
// FIXME: Don't know if it always has a mimetype here because
// finfo->file CAN return false on error: http://php.net/finfo_file
// so if $supported === true, this may return something unexpected.
return $mimetype;
}
// We can conclude that we have failed to get the MIME type
$media = common_get_mime_media($mimetype);
if ('application' !== $media) {
// TRANS: Client exception thrown trying to upload a forbidden MIME type.
// TRANS: %1$s is the file type that was denied, %2$s is the application part of
// TRANS: the MIME type that was denied.
$hint = sprintf(_m('"%1$s" is not a supported file type on this server. ' .
'Try using another %2$s format.'), $mimetype, $media);
} else {
// TRANS: Client exception thrown trying to upload a forbidden MIME type.
// TRANS: %s is the file type that was denied.
$hint = sprintf(_m('"%s" is not a supported file type on this server.'), $mimetype);
}
throw new ClientException($hint);
}
/**
* Title for a file, to display in the interface (if there's no better title) and
* for download filenames
*
* @param $file File object
* @returns string
*/
public static function getDisplayName(File $file): string
{
if (empty($file->filename)) {
return _m('Untitled attachment');
}
// New file name format is "{bin2hex(original_name.ext)}-{$hash}"
$filename = self::decodeFilename($file->filename);
// If there was an error in the match, something's wrong with some piece
// of code (could be a file with utf8 chars in the name)
$log_error_msg = "Invalid file name for File with id={$file->id} " .
"({$file->filename}). Some plugin probably did something wrong.";
if ($filename === false) {
common_log(LOG_ERR, $log_error_msg);
} elseif ($filename === null) {
// The old file name format was "{hash}.{ext}" so we didn't have a name
// This extracts the extension
$ret = preg_match('/^.+?\.+?(.+)$/', $file->filename, $matches);
if ($ret !== 1) {
common_log(LOG_ERR, $log_error_msg);
return _m('Untitled attachment');
}
$ext = $matches[1];
// There's a blacklisted extension array, which could have an alternative
// extension, such as phps, to replace php. We want to turn it back
// (currently defaulted to empty, but let's keep the feature)
$blacklist = common_config('attachments', 'extblacklist');
if (is_array($blacklist)) {
foreach ($blacklist as $upload_ext => $safe_ext) {
if ($ext === $safe_ext) {
$ext = $upload_ext;
break;
}
}
}
$filename = "untitled.{$ext}";
}
return $filename;
}
}

View File

@ -1,66 +0,0 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* widget for displaying a list of notice attachments
*
* PHP version 5
*
* LICENCE: This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category UI
* @package StatusNet
*
* @author Evan Prodromou <evan@status.net>
* @author Sarven Capadisli <csarven@status.net>
* @copyright 2008 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
*
* @see http://status.net/
*/
namespace Plugin\Media\media;
/**
* used for one-off attachment action
*/
class Attachment extends AttachmentListItem
{
public function showNoticeAttachment()
{
if (Event::handle('StartShowAttachmentLink', [$this->out, $this->attachment])) {
$this->out->elementStart('div', ['id' => 'attachment_view',
'class' => 'h-entry', ]);
$this->out->elementStart('div', 'entry-title');
$this->out->element('a', $this->linkAttr(), _m('Download link'));
$this->out->elementEnd('div');
$this->out->elementStart('article', 'e-content');
$this->showRepresentation();
$this->out->elementEnd('article');
Event::handle('EndShowAttachmentLink', [$this->out, $this->attachment]);
$this->out->elementEnd('div');
}
}
public function show()
{
$this->showNoticeAttachment();
}
public function linkAttr()
{
return ['rel' => 'external', 'href' => $this->attachment->getAttachmentDownloadUrl()];
}
}

View File

@ -1,131 +0,0 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* widget for displaying a list of notice attachments
*
* PHP version 5
*
* LICENCE: This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category UI
* @package StatusNet
*
* @author Evan Prodromou <evan@status.net>
* @author Sarven Capadisli <csarven@status.net>
* @copyright 2008 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
*
* @see http://status.net/
*/
namespace Plugin\Media\media;
/**
* widget for displaying a list of notice attachments
*
* There are a number of actions that display a list of notices, in
* reverse chronological order. This widget abstracts out most of the
* code for UI for notice lists. It's overridden to hide some
* data for e.g. the profile page.
*
* @category UI
* @package StatusNet
*
* @author Evan Prodromou <evan@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
*
* @see http://status.net/
* @see Notice
* @see NoticeListItem
* @see ProfileNoticeList
*/
class AttachmentList
{
/** the current stream of notices being displayed. */
public $notice;
// /**
// * constructor
// *
// * @param Notice $notice stream of notices from DB_DataObject
// */
// function __construct(Notice $notice, $out=null)
// {
// // parent::__construct($out);
// $this->notice = $notice;
// }
/**
* show the list of attachments
*
* "Uses up" the stream by looping through it. So, probably can't
* be called twice on the same list.
*
* @return int count of items listed.
*/
public function show()
{
$attachments = $this->notice->attachments();
foreach ($attachments as $key => $att) {
// Remove attachments which are not representable with neither a title nor thumbnail
if ($att->getTitle() === _('Untitled attachment') && !$att->hasThumbnail()) {
unset($attachments[$key]);
}
}
if (!count($attachments)) {
return 0;
}
if ($this->notice->getProfile()->isSilenced()) {
// TRANS: Message for inline attachments list in notices when the author has been silenced.
$this->element('div', ['class' => 'error'],
_('Attachments are hidden because this profile has been silenced.'));
return 0;
}
$this->showListStart();
foreach ($attachments as $att) {
$item = $this->newListItem($att);
$item->show();
}
$this->showListEnd();
return count($attachments);
}
public function showListStart()
{
$this->out->elementStart('ol', ['class' => 'attachments']);
}
public function showListEnd()
{
$this->out->elementEnd('ol');
}
/**
* returns a new list item for the current attachment
*
* @param File $attachment the current attachment
*
* @return AttachmentListItem a list item for displaying the attachment
*/
public function newListItem(File $attachment)
{
return new AttachmentListItem($attachment, $this->out);
}
}

View File

@ -95,11 +95,12 @@ class AttachmentListItem
$this->showEnd();
}
function linkAttr() {
public function linkAttr()
{
return [
'class' => 'u-url',
'href' => $this->attachment->getAttachmentDownloadUrl(),
'title' => $this->linkTitle()
'title' => $this->linkTitle(),
];
}
@ -128,7 +129,7 @@ class AttachmentListItem
try {
// Tell getThumbnail that we can show an animated image if it has one (4th arg, "force_still")
$thumb = File_thumbnail::fromFileObject($this->attachment, null, null, false, false);
} catch (UseFileAsThumbnailException|UnsupportedMediaException|FileNotFoundException|ServerException $e) {
} catch (UseFileAsThumbnailException | UnsupportedMediaException | FileNotFoundException | ServerException $e) {
common_debug("AttachmentListItem couldn't find a thumbnail for {$this->attachment->getID()} because {$e->getMessage()}");
// This remote file has no local thumbnail.
$thumb = null;
@ -157,10 +158,10 @@ class AttachmentListItem
try {
// getUrl(true) because we don't want to hotlink, could be made configurable
$this->out->element('img', ['class' => 'u-photo',
'src' => $this->attachment->getUrl(true),
'alt' => $this->attachment->getTitle()]);
'src' => $this->attachment->getUrl(true),
'alt' => $this->attachment->getTitle(), ]);
} catch (FileNotStoredLocallyException $e) {
$url = $e->file->getUrl(false);
//$url = $e->file->getUrl(false);
$this->out->element('a', ['href' => $url, 'rel' => 'external'], $url);
}
}
@ -178,12 +179,12 @@ class AttachmentListItem
}
$this->out->elementStart($mediatype,
array('class' => "attachment_player u-{$mediatype}",
'poster' => $poster,
'controls' => 'controls'));
['class' => "attachment_player u-{$mediatype}",
'poster' => $poster,
'controls' => 'controls', ]);
$this->out->element('source',
array('src' => $this->attachment->getUrl(),
'type' => $this->attachment->mimetype));
['src' => $this->attachment->getUrl(),
'type' => $this->attachment->mimetype, ]);
$this->out->elementEnd($mediatype);
break;
@ -202,12 +203,13 @@ class AttachmentListItem
break;
}
// Fall through to default if it wasn't a _local_ text/html File object
// no break
default:
Event::handle('ShowUnsupportedAttachmentRepresentation', array($this->out, $this->attachment));
Event::handle('ShowUnsupportedAttachmentRepresentation', [$this->out, $this->attachment]);
}
}
} else {
Event::handle('ShowUnsupportedAttachmentRepresentation', array($this->out, $this->attachment));
Event::handle('ShowUnsupportedAttachmentRepresentation', [$this->out, $this->attachment]);
}
} catch (FileNotFoundException $e) {
if (!$this->attachment->isLocal()) {

View File

@ -1,53 +0,0 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* widget for displaying notice attachments thumbnails
*
* PHP version 5
*
* LICENCE: This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category UI
* @package StatusNet
*
* @author Brion Vibber <brion@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
*
* @see http://status.net/
*/
namespace Plugin\Media\media;
class InlineAttachmentList extends AttachmentList
{
public function showListStart()
{
$this->out->element('h4', 'attachments-title', _('Attachments'));
parent::showListStart();
}
/**
* returns a new list item for the current attachment
*
* @param File $notice the current attachment
*
* @return ListItem a list item for displaying the attachment
*/
public function newListItem(File $attachment)
{
return new InlineAttachmentListItem($attachment, $this->out);
}
}

View File

@ -1,64 +0,0 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* widget for displaying notice attachments thumbnails
*
* PHP version 5
*
* LICENCE: This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category UI
* @package StatusNet
*
* @author Brion Vibber <brion@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
*
* @see http://status.net/
*/
namespace Plugin\Media\media;
class InlineAttachmentListItem extends AttachmentListItem
{
/**
* start a single notice.
*
* @return void
*/
public function showStart()
{
// XXX: RDFa
// TODO: add notice_type class e.g., notice_video, notice_image
$this->out->elementStart('li',
[
'class' => 'inline-attachment',
'id' => 'attachment-' . $this->attachment->getID(),
]
);
}
/**
* finish the notice
*
* Close the last elements in the notice list item
*
* @return void
*/
public function showEnd()
{
$this->out->elementEnd('li');
}
}

View File

@ -26,12 +26,11 @@
* @see http://www.gnu.org/software/social/
*/
namespace Plugin\VideoThumbnail;
namespace Plugin\VideoEncoder;
use App\Core\Modules\Module;
use Plugin\Media\Util\ImageFile;
use App\Core\Modules\Plugin;
class VideoThumbnail extends Module
class VideoEncoder extends Plugin
{
const PLUGIN_VERSION = '0.1.0';
@ -39,7 +38,7 @@ class VideoThumbnail extends Module
* Handle resizing GIF files
*/
public function onStartResizeImageFile(
ImageFile $imagefile,
ImageValidate $imagefile,
string $outpath,
array $box
): bool {
@ -60,7 +59,7 @@ class VideoThumbnail extends Module
* @see http://blog.pkh.me/p/21-high-quality-gif-with-ffmpeg.html
* @see https://github.com/PHP-FFMpeg/PHP-FFMpeg/pull/592
*/
public function resizeImageFileAnimatedGif(ImageFile $imagefile, string $outpath, array $box): bool
public function resizeImageFileAnimatedGif(ImageValidate $imagefile, string $outpath, array $box): bool
{
// Create FFMpeg instance
// Need to explictly tell the drivers location or it won't find them

View File

@ -27,9 +27,23 @@ use Symfony\Component\HttpFoundation\Request;
class Attachment extends Controller
{
public function attachment_inline(Request $request, int $id)
public function attachment_show(Request $request, int $id)
{
}
public function attachment_view(Request $request, int $id)
{
$res = M::getAttachmentFileInfo($id);
return M::sendFile($res['file_path'], $res['mimetype'], $res['title']);
return M::sendFile($res['file_path'], $res['mimetype'], $res['title'], 'inline');
}
public function attachment_download(Request $request, int $id)
{
$res = M::getAttachmentFileInfo($id);
return M::sendFile($res['file_path'], $res['mimetype'], $res['title'], 'attachment');
}
public function attachment_thumbnail(Request $request, int $id)
{
}
}

View File

@ -45,18 +45,32 @@ class GSFile
{
// 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([
$hash = hash_file(Attachment::FILEHASH_ALGO, $sfile->getPathname());
$mimetype = $sfile->getMimeType();
Event::handle('AttachmentValidation', [&$sfile, &$mimetype]);
$attachment = Attachment::create([
'file_hash' => $hash,
'gsactor_id' => $actor_id,
'mimetype' => $sfile->getMimeType(),
'mimetype' => $mimetype,
'title' => $title ?: _m('Untitled attachment'),
'filename' => $hash,
'is_local' => $is_local,
]);
$sfile->move($dest_dir, $hash);
// TODO Normalize file types
return $file;
return $attachment;
}
/**
* Perform file validation (checks and normalization) and store the given file
*/
public static function validateAndStoreAttachmentThumbnail(SymfonyFile $sfile,
string $dest_dir,
?string $title = null,
bool $is_local = true,
int $actor_id = null): Attachment//Thumbnail
{
$attachment = self::validateAndStoreAttachment($sfile,$dest_dir,$title,$is_local,$actor_id);
return $attachment;
}
/**

View File

@ -1,9 +0,0 @@
<?php
namespace App\Core\Modules;
// an interface that specifies how thumbs shall be created, allowing for generation of image, video, pdf, wtv by plugin
abstract class AttachmentThumbnail
{
}

View File

@ -1,10 +0,0 @@
<?php
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
// as validation event will be the same as that of upload - same logic
abstract class AttachmentValidate
{
}

View File

@ -71,6 +71,9 @@ abstract class Main
}
// Attachments
$r->connect('attachment_inline', '/attachment/{id<\d+>}', [C\Attachment::class, 'attachment_inline']);
$r->connect('attachment_show', '/attachment/{id<\d+>}', [C\Attachment::class, 'attachment_show']);
$r->connect('attachment_view', '/attachment/{id<\d+>}/view', [C\Attachment::class, 'attachment_view']);
$r->connect('attachment_download', '/attachment/{id<\d+>}/download', [C\Attachment::class, 'attachment_download']);
$r->connect('attachment_thumbnail', '/attachment/{id<\d+>}/thumbnail', [C\Attachment::class, 'attachment_thumbnail']);
}
}

View File

@ -26,19 +26,19 @@
{% if attachment.mimetype starts with 'image/' %}
<div>
<figure>
<img src="{{ path('attachment_inline', {'id': attachment.getId()}) }}" alt="{{ attachment.getTitle() }}">
<figcaption> {{ attachment.getTitle() }} </figcaption>
<img src="{{ path('attachment_view', {'id': attachment.getId()}) }}" alt="{{ attachment.getTitle() }}">
<figcaption> <a href="{{ path('attachment_show', {'id': attachment.getId()}) }}">{{ attachment.getTitle() }}</a> </figcaption>
</figure>
</div>
{% elseif attachment.mimetype starts with 'video/' %}
<div>
<video src="{{ path('attachment_inline', {'id': attachment.getId()}) }}">
<i> {{ attachment.getTitle() }} </i>
<video src="{{ path('attachment_view', {'id': attachment.getId()}) }}">
<i> <a href="{{ path('attachment_show', {'id': attachment.getId()}) }}">{{ attachment.getTitle() }}</a> </i>
</video>
</div>
{% else %}
<div>
<i> {{ attachment.getTitle() }} </i>
<i> <a href="{{ path('attachment_show', {'id': attachment.getId()}) }}">{{ attachment.getTitle() }}</a> </i>
</div>
{% endif %}
{% endfor %}