From 2cc2b5b856b0c8cc669d129f930bc68bc5579ea0 Mon Sep 17 00:00:00 2001 From: Diogo Cordeiro Date: Sat, 20 Jun 2020 13:49:37 +0100 Subject: [PATCH] [MEDIA] ImageFile fromUpload method wasn't ensuring uploaded file was an image --- actions/avatarsettings.php | 328 +++++++++++++++++++++---------------- lib/media/imagefile.php | 203 ++++++++++++----------- lib/media/mediafile.php | 162 +++++++++--------- 3 files changed, 379 insertions(+), 314 deletions(-) diff --git a/actions/avatarsettings.php b/actions/avatarsettings.php index 6c9f3fc3d0..4c94d192a6 100644 --- a/actions/avatarsettings.php +++ b/actions/avatarsettings.php @@ -1,34 +1,32 @@ . + /** - * StatusNet, the distributed open-source microblogging tool - * * Upload an avatar * - * 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 . - * * @category Settings - * @package StatusNet + * @package GNUsocial + * * @author Evan Prodromou * @author Zach Copley - * @copyright 2008-2009 StatusNet, Inc. + * @author Diogo Cordeiro + * @copyright 2008-2009, 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 - * @link http://status.net/ */ - -if (!defined('GNUSOCIAL')) { exit(1); } +defined('GNUSOCIAL') || die; /** * Upload an avatar @@ -37,24 +35,26 @@ if (!defined('GNUSOCIAL')) { exit(1); } * * @category Settings * @package StatusNet + * * @author Evan Prodromou * @author Zach Copley * @author Sarven Capadisli * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://status.net/ + * + * @see http://status.net/ */ class AvatarsettingsAction extends SettingsAction { - var $mode = null; - var $imagefile = null; - var $filename = null; + public $mode; + public $imagefile; + public $filename; - function prepare(array $args=array()) + public function prepare(array $args = []) { $avatarpath = Avatar::path(''); if (!is_writable($avatarpath)) { - throw new Exception(_("The administrator of your site needs to + throw new Exception(_m("The administrator of your site needs to add write permissions on the avatar upload folder before you're able to set one.")); } @@ -67,24 +67,30 @@ class AvatarsettingsAction extends SettingsAction * Title of the page * * @return string Title of the page + * @throws Exception + * */ - function title() + public function title() { // TRANS: Title for avatar upload page. - return _('Avatar'); + return _m('Avatar'); } /** * Instructions for use * - * @return instructions for use + * @return string instructions for use + * @throws Exception + * */ - function getInstructions() + public function getInstructions() { // TRANS: Instruction for avatar upload page. // TRANS: %s is the maximum file size, for example "500b", "10kB" or "2MB". - return sprintf(_('You can upload your personal avatar. The maximum file size is %s.'), - ImageFile::maxFileSize()); + return sprintf( + _m('You can upload your personal avatar. The maximum file size is %s.'), + ImageFile::maxFileSize() + ); } /** @@ -95,7 +101,7 @@ class AvatarsettingsAction extends SettingsAction * * @return void */ - function showContent() + public function showContent() { if ($this->mode == 'crop') { $this->showCropForm(); @@ -104,33 +110,32 @@ class AvatarsettingsAction extends SettingsAction } } - function showUploadForm() + public function showUploadForm() { - $this->elementStart('form', array('enctype' => 'multipart/form-data', - 'method' => 'post', - 'id' => 'form_settings_avatar', - 'class' => 'form_settings', - 'action' => - common_local_url('avatarsettings'))); + $this->elementStart('form', ['enctype' => 'multipart/form-data', + 'method' => 'post', + 'id' => 'form_settings_avatar', + 'class' => 'form_settings', + 'action' => common_local_url('avatarsettings'),]); $this->elementStart('fieldset'); // TRANS: Avatar upload page form legend. - $this->element('legend', null, _('Avatar settings')); + $this->element('legend', null, _m('Avatar settings')); $this->hidden('token', common_session_token()); - if (Event::handle('StartAvatarFormData', array($this))) { + if (Event::handle('StartAvatarFormData', [$this])) { $this->elementStart('ul', 'form_data'); try { $original = Avatar::getUploaded($this->scoped); - $this->elementStart('li', array('id' => 'avatar_original', - 'class' => 'avatar_view')); + $this->elementStart('li', ['id' => 'avatar_original', + 'class' => 'avatar_view',]); // TRANS: Header on avatar upload page for thumbnail of originally uploaded avatar (h2). - $this->element('h2', null, _("Original")); - $this->elementStart('div', array('id'=>'avatar_original_view')); - $this->element('img', array('src' => $original->displayUrl(), - 'width' => $original->width, - 'height' => $original->height, - 'alt' => $this->scoped->getNickname())); + $this->element('h2', null, _m('Original')); + $this->elementStart('div', ['id' => 'avatar_original_view']); + $this->element('img', ['src' => $original->displayUrl(), + 'width' => $original->width, + 'height' => $original->height, + 'alt' => $this->scoped->getNickname(),]); $this->elementEnd('div'); $this->elementEnd('li'); } catch (NoAvatarException $e) { @@ -139,97 +144,100 @@ class AvatarsettingsAction extends SettingsAction try { $avatar = $this->scoped->getAvatar(AVATAR_PROFILE_SIZE); - $this->elementStart('li', array('id' => 'avatar_preview', - 'class' => 'avatar_view')); + $this->elementStart('li', ['id' => 'avatar_preview', + 'class' => 'avatar_view',]); // TRANS: Header on avatar upload page for thumbnail of to be used rendition of uploaded avatar (h2). - $this->element('h2', null, _("Preview")); - $this->elementStart('div', array('id'=>'avatar_preview_view')); - $this->element('img', array('src' => $avatar->displayUrl(), - 'width' => AVATAR_PROFILE_SIZE, - 'height' => AVATAR_PROFILE_SIZE, - 'alt' => $this->scoped->getNickname())); + $this->element('h2', null, _m('Preview')); + $this->elementStart('div', ['id' => 'avatar_preview_view']); + $this->element('img', ['src' => $avatar->displayUrl(), + 'width' => AVATAR_PROFILE_SIZE, + 'height' => AVATAR_PROFILE_SIZE, + 'alt' => $this->scoped->getNickname(),]); $this->elementEnd('div'); if (!empty($avatar->filename)) { // TRANS: Button on avatar upload page to delete current avatar. - $this->submit('delete', _m('BUTTON','Delete')); + $this->submit('delete', _m('BUTTON', 'Delete')); } $this->elementEnd('li'); } catch (NoAvatarException $e) { // No previously uploaded avatar to preview. } - $this->elementStart('li', array ('id' => 'settings_attach')); - $this->element('input', array('name' => 'MAX_FILE_SIZE', - 'type' => 'hidden', - 'id' => 'MAX_FILE_SIZE', - 'value' => ImageFile::maxFileSizeInt())); - $this->element('input', array('name' => 'avatarfile', - 'type' => 'file', - 'id' => 'avatarfile')); + $this->elementStart('li', ['id' => 'settings_attach']); + $this->element('input', ['name' => 'MAX_FILE_SIZE', + 'type' => 'hidden', + 'id' => 'MAX_FILE_SIZE', + 'value' => ImageFile::maxFileSizeInt(),]); + $this->element('input', ['name' => 'avatarfile', + 'type' => 'file', + 'id' => 'avatarfile',]); $this->elementEnd('li'); $this->elementEnd('ul'); $this->elementStart('ul', 'form_actions'); $this->elementStart('li'); - // TRANS: Button on avatar upload page to upload an avatar. - $this->submit('upload', _m('BUTTON','Upload')); + // TRANS: Button on avatar upload page to upload an avatar. + $this->submit('upload', _m('BUTTON', 'Upload')); $this->elementEnd('li'); $this->elementEnd('ul'); } - Event::handle('EndAvatarFormData', array($this)); + Event::handle('EndAvatarFormData', [$this]); $this->elementEnd('fieldset'); $this->elementEnd('form'); } - function showCropForm() + public function showCropForm() { - $this->elementStart('form', array('method' => 'post', - 'id' => 'form_settings_avatar', - 'class' => 'form_settings', - 'action' => - common_local_url('avatarsettings'))); + $this->elementStart('form', ['method' => 'post', + 'id' => 'form_settings_avatar', + 'class' => 'form_settings', + 'action' => common_local_url('avatarsettings'),]); $this->elementStart('fieldset'); // TRANS: Avatar upload page crop form legend. - $this->element('legend', null, _('Avatar settings')); + $this->element('legend', null, _m('Avatar settings')); $this->hidden('token', common_session_token()); $this->elementStart('ul', 'form_data'); - $this->elementStart('li', - array('id' => 'avatar_original', - 'class' => 'avatar_view')); + $this->elementStart( + 'li', + ['id' => 'avatar_original', + 'class' => 'avatar_view',] + ); // TRANS: Header on avatar upload crop form for thumbnail of originally uploaded avatar (h2). - $this->element('h2', null, _('Original')); - $this->elementStart('div', array('id'=>'avatar_original_view')); - $this->element('img', array('src' => Avatar::url($this->filedata['filename']), - 'width' => $this->filedata['width'], - 'height' => $this->filedata['height'], - 'alt' => $this->scoped->getNickname())); + $this->element('h2', null, _m('Original')); + $this->elementStart('div', ['id' => 'avatar_original_view']); + $this->element('img', ['src' => Avatar::url($this->filedata['filename']), + 'width' => $this->filedata['width'], + 'height' => $this->filedata['height'], + 'alt' => $this->scoped->getNickname(),]); $this->elementEnd('div'); $this->elementEnd('li'); - $this->elementStart('li', - array('id' => 'avatar_preview', - 'class' => 'avatar_view')); + $this->elementStart( + 'li', + ['id' => 'avatar_preview', + 'class' => 'avatar_view',] + ); // TRANS: Header on avatar upload crop form for thumbnail of to be used rendition of uploaded avatar (h2). - $this->element('h2', null, _('Preview')); - $this->elementStart('div', array('id'=>'avatar_preview_view')); - $this->element('img', array('src' => Avatar::url($this->filedata['filename']), - 'width' => AVATAR_PROFILE_SIZE, - 'height' => AVATAR_PROFILE_SIZE, - 'alt' => $this->scoped->getNickname())); + $this->element('h2', null, _m('Preview')); + $this->elementStart('div', ['id' => 'avatar_preview_view']); + $this->element('img', ['src' => Avatar::url($this->filedata['filename']), + 'width' => AVATAR_PROFILE_SIZE, + 'height' => AVATAR_PROFILE_SIZE, + 'alt' => $this->scoped->getNickname(),]); $this->elementEnd('div'); - foreach (array('avatar_crop_x', 'avatar_crop_y', - 'avatar_crop_w', 'avatar_crop_h') as $crop_info) { - $this->element('input', array('name' => $crop_info, - 'type' => 'hidden', - 'id' => $crop_info)); + foreach (['avatar_crop_x', 'avatar_crop_y', + 'avatar_crop_w', 'avatar_crop_h',] as $crop_info) { + $this->element('input', ['name' => $crop_info, + 'type' => 'hidden', + 'id' => $crop_info,]); } // TRANS: Button on avatar upload crop form to confirm a selected crop as avatar. - $this->submit('crop', _m('BUTTON','Crop')); + $this->submit('crop', _m('BUTTON', 'Crop')); $this->elementEnd('li'); $this->elementEnd('ul'); @@ -237,20 +245,31 @@ class AvatarsettingsAction extends SettingsAction $this->elementEnd('form'); } + /** + * @return string + * @throws NoResultException + * @throws NoUploadedMediaException + * @throws ServerException + * @throws UnsupportedMediaException + * @throws UseFileAsThumbnailException + * @throws Exception + * + * @throws ClientException + */ protected function doPost() { - if (Event::handle('StartAvatarSaveForm', array($this))) { + if (Event::handle('StartAvatarSaveForm', [$this])) { if ($this->trimmed('upload')) { return $this->uploadAvatar(); - } else if ($this->trimmed('crop')) { + } elseif ($this->trimmed('crop')) { return $this->cropAvatar(); - } else if ($this->trimmed('delete')) { + } elseif ($this->trimmed('delete')) { return $this->deleteAvatar(); } else { // TRANS: Unexpected validation error on avatar upload form. - throw new ClientException(_('Unexpected form submission.')); + throw new ClientException(_m('Unexpected form submission.')); } - Event::handle('EndAvatarSaveForm', array($this)); + Event::handle('EndAvatarSaveForm', [$this]); } } @@ -260,28 +279,39 @@ class AvatarsettingsAction extends SettingsAction * Does all the magic for handling an image upload, and crops the * image by default. * - * @return void + * @return string + * @throws NoResultException + * @throws NoUploadedMediaException + * @throws ServerException + * @throws UnsupportedMediaException + * @throws UseFileAsThumbnailException + * + * @throws ClientException */ - function uploadAvatar() + public function uploadAvatar(): string { // ImageFile throws exception if something goes wrong, which we'll // pick up and show as an error message above the form. $imagefile = ImageFile::fromUpload('avatarfile'); $type = $imagefile->preferredType(); - $filename = Avatar::filename($this->scoped->getID(), - image_type_to_extension($type), - null, - 'tmp'.common_timestamp()); + $filename = Avatar::filename( + $this->scoped->getID(), + image_type_to_extension($type), + null, + 'tmp' . common_timestamp() + ); $filepath = Avatar::path($filename); $imagefile = $imagefile->copyTo($filepath); - $filedata = array('filename' => $filename, - 'filepath' => $filepath, - 'width' => $imagefile->width, - 'height' => $imagefile->height, - 'type' => $type); + $filedata = [ + 'filename' => $filename, + 'filepath' => $filepath, + 'width' => $imagefile->width, + 'height' => $imagefile->height, + 'type' => $type, + ]; $_SESSION['FILEDATA'] = $filedata; @@ -290,13 +320,18 @@ class AvatarsettingsAction extends SettingsAction $this->mode = 'crop'; // TRANS: Avatar upload form instruction after uploading a file. - return _('Pick a square area of the image to be your avatar.'); + return _m('Pick a square area of the image to be your avatar.'); } /** * Handle the results of jcrop. * - * @return void + * @return string + * @throws NoResultException + * @throws ServerException + * @throws UnsupportedMediaException + * + * @throws ClientException */ public function cropAvatar() { @@ -304,30 +339,34 @@ class AvatarsettingsAction extends SettingsAction if (empty($filedata)) { // TRANS: Server error displayed if an avatar upload went wrong somehow server side. - throw new ServerException(_('Lost our file data.')); + throw new ServerException(_m('Lost our file data.')); } - $file_d = min($filedata['width'], $filedata['height']); + $file_d = min($filedata['width'], $filedata['height']); - $dest_x = $this->arg('avatar_crop_x') ? $this->arg('avatar_crop_x'):0; - $dest_y = $this->arg('avatar_crop_y') ? $this->arg('avatar_crop_y'):0; - $dest_w = $this->arg('avatar_crop_w') ? $this->arg('avatar_crop_w'):$file_d; - $dest_h = $this->arg('avatar_crop_h') ? $this->arg('avatar_crop_h'):$file_d; - $size = intval(min($dest_w, $dest_h, common_config('avatar', 'maxsize'))); + $dest_x = $this->arg('avatar_crop_x') ? $this->arg('avatar_crop_x') : 0; + $dest_y = $this->arg('avatar_crop_y') ? $this->arg('avatar_crop_y') : 0; + $dest_w = $this->arg('avatar_crop_w') ? $this->arg('avatar_crop_w') : $file_d; + $dest_h = $this->arg('avatar_crop_h') ? $this->arg('avatar_crop_h') : $file_d; + $size = (int)(min($dest_w, $dest_h, common_config('avatar', 'maxsize'))); - $box = array('width' => $size, 'height' => $size, - 'x' => $dest_x, 'y' => $dest_y, - 'w' => $dest_w, 'h' => $dest_h); + $box = ['width' => $size, 'height' => $size, + 'x' => $dest_x, 'y' => $dest_y, + 'w' => $dest_w, 'h' => $dest_h,]; $imagefile = new ImageFile(null, $filedata['filepath']); - $filename = Avatar::filename($this->scoped->getID(), image_type_to_extension($imagefile->preferredType()), - $size, common_timestamp()); + $filename = Avatar::filename( + $this->scoped->getID(), + image_type_to_extension($imagefile->preferredType()), + $size, + common_timestamp() + ); try { $imagefile->resizeTo(Avatar::path($filename), $box); } catch (UseFileAsThumbnailException $e) { - common_debug('Using uploaded avatar directly without resizing, copying it to: '.$filename); + common_debug('Using uploaded avatar directly without resizing, copying it to: ' . $filename); if (!copy($filedata['filepath'], Avatar::path($filename))) { - common_debug('Tried to copy image file '.$filedata['filepath'].' to destination '.Avatar::path($filename)); + common_debug('Tried to copy image file ' . $filedata['filepath'] . ' to destination ' . Avatar::path($filename)); throw new ServerException('Could not copy file to destination.'); } } @@ -337,24 +376,26 @@ class AvatarsettingsAction extends SettingsAction unset($_SESSION['FILEDATA']); $this->mode = 'upload'; // TRANS: Success message for having updated a user avatar. - return _('Avatar updated.'); + return _m('Avatar updated.'); } // TRANS: Error displayed on the avatar upload page if the avatar could not be updated for an unknown reason. - throw new ServerException(_('Failed updating avatar.')); + throw new ServerException(_m('Failed updating avatar.')); } /** * Get rid of the current avatar. * - * @return void + * @return string + * @throws Exception + * */ - function deleteAvatar() + public function deleteAvatar() { Avatar::deleteFromProfile($this->scoped); // TRANS: Success message for deleting a user avatar. - return _('Avatar deleted.'); + return _m('Avatar deleted.'); } /** @@ -362,11 +403,10 @@ class AvatarsettingsAction extends SettingsAction * * @return void */ - - function showStylesheets() + public function showStylesheets() { parent::showStylesheets(); - $this->cssLink('js/extlib/jquery-jcrop/css/jcrop.css','base','screen, projection, tv'); + $this->cssLink('js/extlib/jquery-jcrop/css/jcrop.css', 'base', 'screen, projection, tv'); } /** @@ -374,7 +414,7 @@ class AvatarsettingsAction extends SettingsAction * * @return void */ - function showScripts() + public function showScripts() { parent::showScripts(); diff --git a/lib/media/imagefile.php b/lib/media/imagefile.php index ae0bd0ec1a..821a0776d2 100644 --- a/lib/media/imagefile.php +++ b/lib/media/imagefile.php @@ -24,10 +24,9 @@ * @author Zach Copley * @author Mikael Nordfeldth * @author Miguel Dantas - * @copyright 2008, 2019 Free Software Foundation http://fsf.org + * @author Diogo Cordeiro + * @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 - * - * @see https://www.gnu.org/software/social/ */ defined('GNUSOCIAL') || die(); @@ -40,6 +39,7 @@ use Intervention\Image\ImageManagerStatic as Image; * * @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 * @author Zach Copley @@ -51,9 +51,9 @@ 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 = null; // Animated image? (has more than 1 frame). null means untested - public $mimetype = null; // The _ImageFile_ mimetype, _not_ the originating File object + 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 public function __construct($id, string $filepath) { @@ -64,7 +64,7 @@ class ImageFile extends MediaFile $this->filepath = $filepath; $this->filename = basename($filepath); - $img = Image::make($this->filepath); + $img = Image::make($this->filepath); $this->mimetype = $img->mime(); $cmp = function ($obj, $type) { @@ -74,18 +74,13 @@ class ImageFile extends MediaFile } 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')))) { + 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')))) { 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->width = $img->width(); $this->height = $img->height(); parent::__construct( @@ -99,19 +94,19 @@ class ImageFile extends MediaFile // 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; + 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' } @@ -133,7 +128,7 @@ class ImageFile extends MediaFile public static function fromFileObject(File $file) { $imgPath = null; - $media = common_get_mime_media($file->mimetype); + $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); @@ -141,8 +136,8 @@ class ImageFile extends MediaFile // First some mimetype specific exceptions switch ($file->mimetype) { - case 'image/svg+xml': - throw new UseFileAsThumbnailException($file); + case 'image/svg+xml': + throw new UseFileAsThumbnailException($file); } // And we'll only consider it an image if it has such a media type @@ -165,7 +160,7 @@ class ImageFile extends MediaFile try { if (strlen($imgPath) > 0 && $imgPath !== $file->getPath()) { common_debug(__METHOD__ . ': Deleting temporary file that was created as image file' . - 'thumbnail source: ' . _ve($imgPath)); + 'thumbnail source: ' . _ve($imgPath)); @unlink($imgPath); } } catch (FileNotFoundException $e) { @@ -175,7 +170,7 @@ class ImageFile extends MediaFile } common_debug(sprintf( 'Exception %s caught when creating ImageFile for File id==%s ' . - 'and imgPath==%s: %s', + 'and imgPath==%s: %s', get_class($e), _ve($file->id), _ve($imgPath), @@ -198,24 +193,34 @@ class ImageFile extends MediaFile /** * Process a file upload * - * Uses MediaFile's `fromUpload` to do the majority of the work and reencodes the image, - * to mitigate injection attacks. + * 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 string $param * @param null|Profile $scoped * - * @throws ClientException + * @return ImageFile * @throws NoResultException * @throws NoUploadedMediaException * @throws ServerException * @throws UnsupportedMediaException * @throws UseFileAsThumbnailException * - * @return ImageFile|MediaFile + * @throws ClientException */ - public static function fromUpload(string $param = 'upload', Profile $scoped = null) + public static function fromUpload(string $param = 'upload', ?Profile $scoped = null): self { - return parent::fromUpload($param, $scoped); + $mediafile = parent::fromUpload($param, $scoped); + if ($mediafile instanceof self) { + return $mediafile; + } else { + // 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); + } } /** @@ -227,7 +232,7 @@ class ImageFile extends MediaFile */ public function preferredType() { - // Keep only JPEG and GIF in their orignal format + // Keep only JPEG and GIF in their original format if ($this->type === IMAGETYPE_JPEG || $this->type === IMAGETYPE_GIF) { return $this->type; } @@ -245,13 +250,13 @@ class ImageFile extends MediaFile * * @param string $outpath * - * @throws ClientException + * @return ImageFile the image stored at target path * @throws NoResultException * @throws ServerException * @throws UnsupportedMediaException * @throws UseFileAsThumbnailException * - * @return ImageFile the image stored at target path + * @throws ClientException */ public function copyTo($outpath) { @@ -263,20 +268,21 @@ class ImageFile extends MediaFile * * @param string $outpath * @param array $box width, height, boundary box (x,y,w,h) defaults to full image + * + * @return string full local filesystem filename * @return string full local filesystem filename * @throws UnsupportedMediaException * @throws UseFileAsThumbnailException * - * @return string full local filesystem filename */ public function resizeTo($outpath, array $box = []) { - $box['width'] = isset($box['width']) ? intval($box['width']) : $this->width; - $box['height'] = isset($box['height']) ? intval($box['height']) : $this->height; - $box['x'] = isset($box['x']) ? intval($box['x']) : 0; - $box['y'] = isset($box['y']) ? intval($box['y']) : 0; - $box['w'] = isset($box['w']) ? intval($box['w']) : $this->width; - $box['h'] = isset($box['h']) ? intval($box['h']) : $this->height; + $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, @@ -285,13 +291,13 @@ class ImageFile extends MediaFile } // Don't rotate/crop/scale if it isn't necessary - if ($box['width'] === $this->width + 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()) { + && $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. @@ -308,7 +314,7 @@ class ImageFile extends MediaFile } $this->height = $box['h']; - $this->width = $box['w']; + $this->width = $box['w']; if (Event::handle('StartResizeImageFile', [$this, $outpath, $box])) { $outpath = $this->resizeToFile($outpath, $box); @@ -346,7 +352,7 @@ class ImageFile extends MediaFile try { $img = Image::make($this->filepath); } catch (Exception $e) { - common_log(LOG_ERR, __METHOD__ . ' ecountered exception: ' . print_r($e, true)); + 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')); } @@ -359,29 +365,31 @@ class ImageFile extends MediaFile $img = $img->orientate(); } - $img->fit($box['width'], $box['height'], - function ($constraint) { - if (common_config('attachments', 'upscale') !== true) { - $constraint->upsize(); // Prevent upscaling - } - } + $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_GIF: - $img->save($outpath, 100, 'gif'); - break; - case IMAGETYPE_PNG: - $img->save($outpath, 100, 'png'); - break; - case IMAGETYPE_JPEG: - $img->save($outpath, common_config('image', 'jpegquality'), 'jpg'); - break; - default: - // TRANS: Exception thrown when trying resize an unknown file type. - throw new Exception(_m('Unknown file type')); + case IMAGETYPE_GIF: + $img->save($outpath, 100, 'gif'); + break; + case IMAGETYPE_PNG: + $img->save($outpath, 100, 'png'); + break; + case IMAGETYPE_JPEG: + $img->save($outpath, common_config('image', 'jpegquality'), 'jpg'); + break; + default: + // TRANS: Exception thrown when trying resize an unknown file type. + throw new Exception(_m('Unknown file type')); } $img->destroy(); @@ -421,9 +429,9 @@ class ImageFile extends MediaFile * @param $crop int Crop to the size (not preserving aspect ratio) * @param int $rotate * + * @return array * @throws ServerException * - * @return array */ public static function getScalingValues( $width, @@ -447,8 +455,8 @@ class ImageFile extends MediaFile // Because GD doesn't understand EXIF orientation etc. if (abs($rotate) == 90) { - $tmp = $width; - $width = $height; + $tmp = $width; + $width = $height; $height = $tmp; } @@ -461,7 +469,7 @@ class ImageFile extends MediaFile if ($crop) { $s_ar = $width / $height; - $t_ar = $maxW / $maxH; + $t_ar = $maxW / $maxH; $rw = $maxW; $rh = $maxH; @@ -484,10 +492,10 @@ class ImageFile extends MediaFile $rw = ceil($width * $rh / $height); } } - return array(intval($rw), intval($rh), - intval($cx), intval($cy), - is_null($cw) ? $width : intval($cw), - is_null($ch) ? $height : intval($ch)); + return [(int)$rw, (int)$rh, + (int)$cx, (int)$cy, + is_null($cw) ? $width : (int)$cw, + is_null($ch) ? $height : (int)$ch,]; } /** @@ -536,9 +544,9 @@ class ImageFile extends MediaFile $filename = basename($this->filepath); if ($width === null) { - $width = common_config('thumbnail', 'width'); + $width = common_config('thumbnail', 'width'); $height = common_config('thumbnail', 'height'); - $crop = common_config('thumbnail', 'crop'); + $crop = common_config('thumbnail', 'crop'); } if (!$upscale) { @@ -552,7 +560,7 @@ class ImageFile extends MediaFile if ($height === null) { $height = $width; - $crop = true; + $crop = true; } // Get proper aspect ratio width and height before lookup @@ -563,18 +571,18 @@ class ImageFile extends MediaFile $thumb = File_thumbnail::pkeyGet([ 'file_id' => $this->fileRecord->getID(), - 'width' => $width, - 'height' => $height, + 'width' => $width, + 'height' => $height, ]); if ($thumb instanceof File_thumbnail) { $this->height = $height; - $this->width = $width; + $this->width = $width; return $thumb; } $type = $this->preferredType(); - $ext = image_type_to_extension($type, true); + $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 @@ -583,18 +591,19 @@ class ImageFile extends MediaFile // The boundary box for our resizing $box = [ 'width' => $width, 'height' => $height, - 'x' => $x, 'y' => $y, - 'w' => $w, 'h' => $h, + 'x' => $x, 'y' => $y, + 'w' => $w, 'h' => $h, ]; $outpath = File_thumbnail::path( - "thumb-{$this->fileRecord->id}-{$box['width']}x{$box['height']}-{$outfilename}"); + "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) { + || $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.'); @@ -608,7 +617,7 @@ class ImageFile extends MediaFile )); $this->height = $box['height']; - $this->width = $box['width']; + $this->width = $box['width']; // Perform resize and store into file $outpath = $this->resizeTo($outpath, $box); diff --git a/lib/media/mediafile.php b/lib/media/mediafile.php index 11086775ca..224de175be 100644 --- a/lib/media/mediafile.php +++ b/lib/media/mediafile.php @@ -1,33 +1,33 @@ . + /** - * GNU social - a federating social network - * * Abstraction for media files * - * 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 . - * * @category Media * @package GNUsocial + * * @author Robin Millette * @author Miguel Dantas * @author Zach Copley * @author Mikael Nordfeldth - * @copyright 2008-2009, 2019 Free Software Foundation http://fsf.org + * @author Diogo Cordeiro + * @copyright 2008-2009, 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 - * @link https://www.gnu.org/software/social/ */ - defined('GNUSOCIAL') || die(); /** @@ -35,21 +35,22 @@ defined('GNUSOCIAL') || die(); */ class MediaFile { - public $id = null; - public $filepath = null; - public $filename = null; - public $fileRecord = null; - public $fileurl = null; - public $short_fileurl = null; - public $mimetype = null; + public $id; + public $filepath; + public $filename; + public $fileRecord; + public $fileurl; + public $short_fileurl; + public $mimetype; /** * @param string $filepath The path of the file this media refers to. Required * @param string $mimetype The mimetype of the file. Required - * @param $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) + * @param string $filehash The hash of the file, if known. Optional + * @param null|int $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 @@ -60,7 +61,7 @@ class MediaFile $this->filename = basename($this->filepath); $this->mimetype = $mimetype; $this->filehash = self::getHashOfFile($this->filepath, $filehash); - $this->id = $id; + $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 @@ -80,7 +81,7 @@ class MediaFile $this->fileurl = common_local_url( 'attachment', - array('attachment' => $this->fileRecord->id) + ['attachment' => $this->fileRecord->id] ); $this->short_fileurl = common_shorten_url($this->fileurl); @@ -125,14 +126,17 @@ class MediaFile * Calculate the hash of a file. * * This won't work for files >2GiB because PHP uses only 32bit. + * * @param string $filepath - * @param string|null $filehash + * @param null|string $filehash + * * @return string * @throws ServerException + * */ public static function getHashOfFile(string $filepath, $filehash = null) { - assert(!empty($filepath), __METHOD__ . ": filepath cannot be 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... @@ -148,8 +152,9 @@ class MediaFile * Retrieve or insert as a file in the DB * * @return object File - * @throws ClientException * @throws ServerException + * + * @throws ClientException */ protected function storeFile() { @@ -161,25 +166,25 @@ class MediaFile // Well, let's just continue below. } - $fileurl = common_local_url('attachment_view', array('filehash' => $this->filehash)); + $fileurl = common_local_url('attachment_view', ['filehash' => $this->filehash]); $file = new File; $file->filename = $this->filename; - $file->urlhash = File::hashurl($fileurl); - $file->url = $fileurl; + $file->urlhash = File::hashurl($fileurl); + $file->url = $fileurl; $file->filehash = $this->filehash; - $file->size = filesize($this->filepath); + $file->size = filesize($this->filepath); if ($file->size === false) { throw new ServerException('Could not read file to get its size'); } - $file->date = time(); + $file->date = time(); $file->mimetype = $this->mimetype; $file_id = $file->insert(); - if ($file_id===false) { - common_log_db_error($file, "INSERT", __FILE__); + 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.')); } @@ -187,7 +192,7 @@ class MediaFile // Set file geometrical properties if available try { $image = ImageFile::fromFileObject($file); - $orig = clone($file); + $orig = clone $file; $file->width = $image->width; $file->height = $image->height; $file->update($orig); @@ -215,11 +220,11 @@ class MediaFile { $value = self::maxFileSizeInt(); if ($value > 1024 * 1024) { - $value = $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; + $value = $value / 1024; // TRANS: Number of kilobytes. %d is the number. return sprintf(_m('%dkB', '%dkB', $value), $value); } else { @@ -231,7 +236,7 @@ class MediaFile /** * The maximum allowed file size, as an int */ - public static function maxFileSizeInt() : int + public static function maxFileSizeInt(): int { return common_config('attachments', 'file_quota'); } @@ -239,9 +244,13 @@ class MediaFile /** * 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 mixed $original_name + * @param null|mixed $ext + * * @throws ClientException */ - public static function encodeFilename($original_name, string $filehash, $ext = null) : string + public static function encodeFilename($original_name, string $filehash, $ext = null): string { if (empty($original_name)) { $original_name = _m('Untitled attachment'); @@ -249,9 +258,9 @@ class MediaFile // 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); + // 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.')); } @@ -268,6 +277,7 @@ class MediaFile /** * 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) @@ -319,19 +329,21 @@ class MediaFile * format ("{$hash}.{$ext}") * * @param string $param Form name - * @param Profile|null $scoped + * @param null|Profile $scoped + * * @return ImageFile|MediaFile - * @throws ClientException * @throws NoResultException * @throws NoUploadedMediaException * @throws ServerException * @throws UnsupportedMediaException * @throws UseFileAsThumbnailException + * + * @throws ClientException */ - public static function fromUpload(string $param='media', Profile $scoped=null) + 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]) && isset($_FILES[$param]['error']))) { + if (!(isset($_FILES[$param], $_FILES[$param]['error']))) { throw new NoUploadedMediaException($param); } @@ -363,7 +375,7 @@ class MediaFile // 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']); + 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.')); } @@ -417,10 +429,10 @@ class MediaFile return new ImageFile(null, $filepath); } } - return new MediaFile($filepath, $mimetype, $filehash); + return new self($filepath, $mimetype, $filehash); } - public static function fromFilehandle($fh, Profile $scoped=null) + public static function fromFilehandle($fh, Profile $scoped = null) { $stream = stream_get_meta_data($fh); // So far we're only handling filehandles originating from tmpfile(), @@ -449,7 +461,7 @@ class MediaFile 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)); + common_log(LOG_ERR, 'Could not chmod uploaded file: ' . _ve($e->path)); } $filename = basename($file->getPath()); @@ -467,14 +479,14 @@ class MediaFile $result = copy($stream['uri'], $filepath) && chmod($filepath, 0664); if (!$result) { - common_log(LOG_ERR, 'File could not be moved (or chmodded) from '._ve($stream['uri']) . ' to ' . _ve($filepath)); + 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 MediaFile($filename, $mimetype, $filehash); + return new self($filename, $mimetype, $filehash); } /** @@ -482,14 +494,16 @@ class MediaFile * * @param string $filepath filesystem path as string (file must exist) * @param bool $originalFilename (optional) for extension-based detection + * * @return string * - * @throws ClientException if type is known, but not supported for local uploads - * @throws ServerException * @fixme this seems to tie a front-end error message in, kinda confusing * + * @throws ServerException + * + * @throws ClientException if type is known, but not supported for local uploads */ - public static function getUploadedMimeType(string $filepath, $originalFilename=false) + public static function getUploadedMimeType(string $filepath, $originalFilename = false) { // We only accept filenames to existing files @@ -534,7 +548,7 @@ class MediaFile * due to security concerns, hence the function_usable() checks */ if (DIRECTORY_SEPARATOR !== '\\') { - $cmd = 'file --brief --mime '.escapeshellarg($filepath).' 2>&1'; + $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 @@ -581,13 +595,13 @@ class MediaFile // 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 = array('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'); + $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'); @@ -618,7 +632,7 @@ class MediaFile // 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); + '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. @@ -630,10 +644,12 @@ class MediaFile /** * 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 { + public static function getDisplayName(File $file): string + { if (empty($file->filename)) { return _m('Untitled attachment'); } @@ -644,7 +660,7 @@ class MediaFile // 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."; + "({$file->filename}). Some plugin probably did something wrong."; if ($filename === false) { common_log(LOG_ERR, $log_error_msg); } elseif ($filename === null) {