[MEDIA] ImageFile fromUpload method wasn't ensuring uploaded file was an image

This commit is contained in:
Diogo Cordeiro 2020-06-20 13:49:37 +01:00 committed by Diogo Peralta Cordeiro
parent d01f44ee99
commit 5439ff3ec5
3 changed files with 379 additions and 314 deletions

View File

@ -1,34 +1,32 @@
<?php <?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/>.
/** /**
* StatusNet, the distributed open-source microblogging tool
*
* Upload an avatar * 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 <http://www.gnu.org/licenses/>.
*
* @category Settings * @category Settings
* @package StatusNet * @package GNUsocial
*
* @author Evan Prodromou <evan@status.net> * @author Evan Prodromou <evan@status.net>
* @author Zach Copley <zach@status.net> * @author Zach Copley <zach@status.net>
* @copyright 2008-2009 StatusNet, Inc. * @author Diogo Cordeiro <diogo@fc.up.pt>
* @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 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/ */
defined('GNUSOCIAL') || die;
if (!defined('GNUSOCIAL')) { exit(1); }
/** /**
* Upload an avatar * Upload an avatar
@ -37,24 +35,26 @@ if (!defined('GNUSOCIAL')) { exit(1); }
* *
* @category Settings * @category Settings
* @package StatusNet * @package StatusNet
*
* @author Evan Prodromou <evan@status.net> * @author Evan Prodromou <evan@status.net>
* @author Zach Copley <zach@status.net> * @author Zach Copley <zach@status.net>
* @author Sarven Capadisli <csarven@status.net> * @author Sarven Capadisli <csarven@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @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 class AvatarsettingsAction extends SettingsAction
{ {
var $mode = null; public $mode;
var $imagefile = null; public $imagefile;
var $filename = null; public $filename;
function prepare(array $args=array()) public function prepare(array $args = [])
{ {
$avatarpath = Avatar::path(''); $avatarpath = Avatar::path('');
if (!is_writable($avatarpath)) { 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 add write permissions on the avatar upload folder before
you're able to set one.")); you're able to set one."));
} }
@ -67,24 +67,30 @@ class AvatarsettingsAction extends SettingsAction
* Title of the page * Title of the page
* *
* @return string Title of the page * @return string Title of the page
* @throws Exception
*
*/ */
function title() public function title()
{ {
// TRANS: Title for avatar upload page. // TRANS: Title for avatar upload page.
return _('Avatar'); return _m('Avatar');
} }
/** /**
* Instructions for use * 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: Instruction for avatar upload page.
// TRANS: %s is the maximum file size, for example "500b", "10kB" or "2MB". // 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.'), return sprintf(
ImageFile::maxFileSize()); _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 * @return void
*/ */
function showContent() public function showContent()
{ {
if ($this->mode == 'crop') { if ($this->mode == 'crop') {
$this->showCropForm(); $this->showCropForm();
@ -104,33 +110,32 @@ class AvatarsettingsAction extends SettingsAction
} }
} }
function showUploadForm() public function showUploadForm()
{ {
$this->elementStart('form', array('enctype' => 'multipart/form-data', $this->elementStart('form', ['enctype' => 'multipart/form-data',
'method' => 'post', 'method' => 'post',
'id' => 'form_settings_avatar', 'id' => 'form_settings_avatar',
'class' => 'form_settings', 'class' => 'form_settings',
'action' => 'action' => common_local_url('avatarsettings'),]);
common_local_url('avatarsettings')));
$this->elementStart('fieldset'); $this->elementStart('fieldset');
// TRANS: Avatar upload page form legend. // 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()); $this->hidden('token', common_session_token());
if (Event::handle('StartAvatarFormData', array($this))) { if (Event::handle('StartAvatarFormData', [$this])) {
$this->elementStart('ul', 'form_data'); $this->elementStart('ul', 'form_data');
try { try {
$original = Avatar::getUploaded($this->scoped); $original = Avatar::getUploaded($this->scoped);
$this->elementStart('li', array('id' => 'avatar_original', $this->elementStart('li', ['id' => 'avatar_original',
'class' => 'avatar_view')); 'class' => 'avatar_view',]);
// TRANS: Header on avatar upload page for thumbnail of originally uploaded avatar (h2). // TRANS: Header on avatar upload page for thumbnail of originally uploaded avatar (h2).
$this->element('h2', null, _("Original")); $this->element('h2', null, _m('Original'));
$this->elementStart('div', array('id'=>'avatar_original_view')); $this->elementStart('div', ['id' => 'avatar_original_view']);
$this->element('img', array('src' => $original->displayUrl(), $this->element('img', ['src' => $original->displayUrl(),
'width' => $original->width, 'width' => $original->width,
'height' => $original->height, 'height' => $original->height,
'alt' => $this->scoped->getNickname())); 'alt' => $this->scoped->getNickname(),]);
$this->elementEnd('div'); $this->elementEnd('div');
$this->elementEnd('li'); $this->elementEnd('li');
} catch (NoAvatarException $e) { } catch (NoAvatarException $e) {
@ -139,15 +144,15 @@ class AvatarsettingsAction extends SettingsAction
try { try {
$avatar = $this->scoped->getAvatar(AVATAR_PROFILE_SIZE); $avatar = $this->scoped->getAvatar(AVATAR_PROFILE_SIZE);
$this->elementStart('li', array('id' => 'avatar_preview', $this->elementStart('li', ['id' => 'avatar_preview',
'class' => 'avatar_view')); 'class' => 'avatar_view',]);
// TRANS: Header on avatar upload page for thumbnail of to be used rendition of uploaded avatar (h2). // TRANS: Header on avatar upload page for thumbnail of to be used rendition of uploaded avatar (h2).
$this->element('h2', null, _("Preview")); $this->element('h2', null, _m('Preview'));
$this->elementStart('div', array('id'=>'avatar_preview_view')); $this->elementStart('div', ['id' => 'avatar_preview_view']);
$this->element('img', array('src' => $avatar->displayUrl(), $this->element('img', ['src' => $avatar->displayUrl(),
'width' => AVATAR_PROFILE_SIZE, 'width' => AVATAR_PROFILE_SIZE,
'height' => AVATAR_PROFILE_SIZE, 'height' => AVATAR_PROFILE_SIZE,
'alt' => $this->scoped->getNickname())); 'alt' => $this->scoped->getNickname(),]);
$this->elementEnd('div'); $this->elementEnd('div');
if (!empty($avatar->filename)) { if (!empty($avatar->filename)) {
// TRANS: Button on avatar upload page to delete current avatar. // TRANS: Button on avatar upload page to delete current avatar.
@ -158,14 +163,14 @@ class AvatarsettingsAction extends SettingsAction
// No previously uploaded avatar to preview. // No previously uploaded avatar to preview.
} }
$this->elementStart('li', array ('id' => 'settings_attach')); $this->elementStart('li', ['id' => 'settings_attach']);
$this->element('input', array('name' => 'MAX_FILE_SIZE', $this->element('input', ['name' => 'MAX_FILE_SIZE',
'type' => 'hidden', 'type' => 'hidden',
'id' => 'MAX_FILE_SIZE', 'id' => 'MAX_FILE_SIZE',
'value' => ImageFile::maxFileSizeInt())); 'value' => ImageFile::maxFileSizeInt(),]);
$this->element('input', array('name' => 'avatarfile', $this->element('input', ['name' => 'avatarfile',
'type' => 'file', 'type' => 'file',
'id' => 'avatarfile')); 'id' => 'avatarfile',]);
$this->elementEnd('li'); $this->elementEnd('li');
$this->elementEnd('ul'); $this->elementEnd('ul');
@ -176,56 +181,59 @@ class AvatarsettingsAction extends SettingsAction
$this->elementEnd('li'); $this->elementEnd('li');
$this->elementEnd('ul'); $this->elementEnd('ul');
} }
Event::handle('EndAvatarFormData', array($this)); Event::handle('EndAvatarFormData', [$this]);
$this->elementEnd('fieldset'); $this->elementEnd('fieldset');
$this->elementEnd('form'); $this->elementEnd('form');
} }
function showCropForm() public function showCropForm()
{ {
$this->elementStart('form', array('method' => 'post', $this->elementStart('form', ['method' => 'post',
'id' => 'form_settings_avatar', 'id' => 'form_settings_avatar',
'class' => 'form_settings', 'class' => 'form_settings',
'action' => 'action' => common_local_url('avatarsettings'),]);
common_local_url('avatarsettings')));
$this->elementStart('fieldset'); $this->elementStart('fieldset');
// TRANS: Avatar upload page crop form legend. // 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->hidden('token', common_session_token());
$this->elementStart('ul', 'form_data'); $this->elementStart('ul', 'form_data');
$this->elementStart('li', $this->elementStart(
array('id' => 'avatar_original', 'li',
'class' => 'avatar_view')); ['id' => 'avatar_original',
'class' => 'avatar_view',]
);
// TRANS: Header on avatar upload crop form for thumbnail of originally uploaded avatar (h2). // TRANS: Header on avatar upload crop form for thumbnail of originally uploaded avatar (h2).
$this->element('h2', null, _('Original')); $this->element('h2', null, _m('Original'));
$this->elementStart('div', array('id'=>'avatar_original_view')); $this->elementStart('div', ['id' => 'avatar_original_view']);
$this->element('img', array('src' => Avatar::url($this->filedata['filename']), $this->element('img', ['src' => Avatar::url($this->filedata['filename']),
'width' => $this->filedata['width'], 'width' => $this->filedata['width'],
'height' => $this->filedata['height'], 'height' => $this->filedata['height'],
'alt' => $this->scoped->getNickname())); 'alt' => $this->scoped->getNickname(),]);
$this->elementEnd('div'); $this->elementEnd('div');
$this->elementEnd('li'); $this->elementEnd('li');
$this->elementStart('li', $this->elementStart(
array('id' => 'avatar_preview', 'li',
'class' => 'avatar_view')); ['id' => 'avatar_preview',
'class' => 'avatar_view',]
);
// TRANS: Header on avatar upload crop form for thumbnail of to be used rendition of uploaded avatar (h2). // TRANS: Header on avatar upload crop form for thumbnail of to be used rendition of uploaded avatar (h2).
$this->element('h2', null, _('Preview')); $this->element('h2', null, _m('Preview'));
$this->elementStart('div', array('id'=>'avatar_preview_view')); $this->elementStart('div', ['id' => 'avatar_preview_view']);
$this->element('img', array('src' => Avatar::url($this->filedata['filename']), $this->element('img', ['src' => Avatar::url($this->filedata['filename']),
'width' => AVATAR_PROFILE_SIZE, 'width' => AVATAR_PROFILE_SIZE,
'height' => AVATAR_PROFILE_SIZE, 'height' => AVATAR_PROFILE_SIZE,
'alt' => $this->scoped->getNickname())); 'alt' => $this->scoped->getNickname(),]);
$this->elementEnd('div'); $this->elementEnd('div');
foreach (array('avatar_crop_x', 'avatar_crop_y', foreach (['avatar_crop_x', 'avatar_crop_y',
'avatar_crop_w', 'avatar_crop_h') as $crop_info) { 'avatar_crop_w', 'avatar_crop_h',] as $crop_info) {
$this->element('input', array('name' => $crop_info, $this->element('input', ['name' => $crop_info,
'type' => 'hidden', 'type' => 'hidden',
'id' => $crop_info)); 'id' => $crop_info,]);
} }
// TRANS: Button on avatar upload crop form to confirm a selected crop as avatar. // TRANS: Button on avatar upload crop form to confirm a selected crop as avatar.
@ -237,9 +245,20 @@ class AvatarsettingsAction extends SettingsAction
$this->elementEnd('form'); $this->elementEnd('form');
} }
/**
* @return string
* @throws NoResultException
* @throws NoUploadedMediaException
* @throws ServerException
* @throws UnsupportedMediaException
* @throws UseFileAsThumbnailException
* @throws Exception
*
* @throws ClientException
*/
protected function doPost() protected function doPost()
{ {
if (Event::handle('StartAvatarSaveForm', array($this))) { if (Event::handle('StartAvatarSaveForm', [$this])) {
if ($this->trimmed('upload')) { if ($this->trimmed('upload')) {
return $this->uploadAvatar(); return $this->uploadAvatar();
} elseif ($this->trimmed('crop')) { } elseif ($this->trimmed('crop')) {
@ -248,9 +267,9 @@ class AvatarsettingsAction extends SettingsAction
return $this->deleteAvatar(); return $this->deleteAvatar();
} else { } else {
// TRANS: Unexpected validation error on avatar upload form. // 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 * Does all the magic for handling an image upload, and crops the
* image by default. * 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 // ImageFile throws exception if something goes wrong, which we'll
// pick up and show as an error message above the form. // pick up and show as an error message above the form.
$imagefile = ImageFile::fromUpload('avatarfile'); $imagefile = ImageFile::fromUpload('avatarfile');
$type = $imagefile->preferredType(); $type = $imagefile->preferredType();
$filename = Avatar::filename($this->scoped->getID(), $filename = Avatar::filename(
$this->scoped->getID(),
image_type_to_extension($type), image_type_to_extension($type),
null, null,
'tmp'.common_timestamp()); 'tmp' . common_timestamp()
);
$filepath = Avatar::path($filename); $filepath = Avatar::path($filename);
$imagefile = $imagefile->copyTo($filepath); $imagefile = $imagefile->copyTo($filepath);
$filedata = array('filename' => $filename, $filedata = [
'filename' => $filename,
'filepath' => $filepath, 'filepath' => $filepath,
'width' => $imagefile->width, 'width' => $imagefile->width,
'height' => $imagefile->height, 'height' => $imagefile->height,
'type' => $type); 'type' => $type,
];
$_SESSION['FILEDATA'] = $filedata; $_SESSION['FILEDATA'] = $filedata;
@ -290,13 +320,18 @@ class AvatarsettingsAction extends SettingsAction
$this->mode = 'crop'; $this->mode = 'crop';
// TRANS: Avatar upload form instruction after uploading a file. // 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. * Handle the results of jcrop.
* *
* @return void * @return string
* @throws NoResultException
* @throws ServerException
* @throws UnsupportedMediaException
*
* @throws ClientException
*/ */
public function cropAvatar() public function cropAvatar()
{ {
@ -304,7 +339,7 @@ class AvatarsettingsAction extends SettingsAction
if (empty($filedata)) { if (empty($filedata)) {
// TRANS: Server error displayed if an avatar upload went wrong somehow server side. // 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']);
@ -313,15 +348,19 @@ class AvatarsettingsAction extends SettingsAction
$dest_y = $this->arg('avatar_crop_y') ? $this->arg('avatar_crop_y') : 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_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; $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'))); $size = (int)(min($dest_w, $dest_h, common_config('avatar', 'maxsize')));
$box = array('width' => $size, 'height' => $size, $box = ['width' => $size, 'height' => $size,
'x' => $dest_x, 'y' => $dest_y, 'x' => $dest_x, 'y' => $dest_y,
'w' => $dest_w, 'h' => $dest_h); 'w' => $dest_w, 'h' => $dest_h,];
$imagefile = new ImageFile(null, $filedata['filepath']); $imagefile = new ImageFile(null, $filedata['filepath']);
$filename = Avatar::filename($this->scoped->getID(), image_type_to_extension($imagefile->preferredType()), $filename = Avatar::filename(
$size, common_timestamp()); $this->scoped->getID(),
image_type_to_extension($imagefile->preferredType()),
$size,
common_timestamp()
);
try { try {
$imagefile->resizeTo(Avatar::path($filename), $box); $imagefile->resizeTo(Avatar::path($filename), $box);
} catch (UseFileAsThumbnailException $e) { } catch (UseFileAsThumbnailException $e) {
@ -337,24 +376,26 @@ class AvatarsettingsAction extends SettingsAction
unset($_SESSION['FILEDATA']); unset($_SESSION['FILEDATA']);
$this->mode = 'upload'; $this->mode = 'upload';
// TRANS: Success message for having updated a user avatar. // 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. // 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. * Get rid of the current avatar.
* *
* @return void * @return string
* @throws Exception
*
*/ */
function deleteAvatar() public function deleteAvatar()
{ {
Avatar::deleteFromProfile($this->scoped); Avatar::deleteFromProfile($this->scoped);
// TRANS: Success message for deleting a user avatar. // TRANS: Success message for deleting a user avatar.
return _('Avatar deleted.'); return _m('Avatar deleted.');
} }
/** /**
@ -362,8 +403,7 @@ class AvatarsettingsAction extends SettingsAction
* *
* @return void * @return void
*/ */
public function showStylesheets()
function showStylesheets()
{ {
parent::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 * @return void
*/ */
function showScripts() public function showScripts()
{ {
parent::showScripts(); parent::showScripts();

View File

@ -24,10 +24,9 @@
* @author Zach Copley <zach@status.net> * @author Zach Copley <zach@status.net>
* @author Mikael Nordfeldth <mmn@hethane.se> * @author Mikael Nordfeldth <mmn@hethane.se>
* @author Miguel Dantas <biodantasgs@gmail.com> * @author Miguel Dantas <biodantasgs@gmail.com>
* @copyright 2008, 2019 Free Software Foundation http://fsf.org * @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 * @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(); defined('GNUSOCIAL') || die();
@ -40,6 +39,7 @@ use Intervention\Image\ImageManagerStatic as Image;
* *
* @category Image * @category Image
* @package GNUsocial * @package GNUsocial
*
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @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 Evan Prodromou <evan@status.net>
* @author Zach Copley <zach@status.net> * @author Zach Copley <zach@status.net>
@ -52,8 +52,8 @@ class ImageFile extends MediaFile
public $height; public $height;
public $width; public $width;
public $rotate = 0; // degrees to rotate for properly oriented image (extrapolated from EXIF etc.) 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 $animated; // Animated image? (has more than 1 frame). null means untested
public $mimetype = null; // The _ImageFile_ mimetype, _not_ the originating File object public $mimetype; // The _ImageFile_ mimetype, _not_ the originating File object
public function __construct($id, string $filepath) public function __construct($id, string $filepath)
{ {
@ -74,12 +74,7 @@ class ImageFile extends MediaFile
} }
return false; return false;
}; };
if (!(($cmp($this, IMAGETYPE_GIF) && function_exists('imagecreatefromgif')) || 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_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"); common_debug("Mimetype '{$this->mimetype}' was not recognized as a supported format");
// TRANS: Exception thrown when trying to upload an unsupported image file format. // TRANS: Exception thrown when trying to upload an unsupported image file format.
throw new UnsupportedMediaException(_m('Unsupported image format.'), $this->filepath); throw new UnsupportedMediaException(_m('Unsupported image format.'), $this->filepath);
@ -198,24 +193,34 @@ class ImageFile extends MediaFile
/** /**
* Process a file upload * Process a file upload
* *
* Uses MediaFile's `fromUpload` to do the majority of the work and reencodes the image, * Uses MediaFile's `fromUpload` to do the majority of the work
* to mitigate injection attacks. * and ensures the uploaded file is in fact an image.
* *
* @param string $param * @param string $param
* @param null|Profile $scoped * @param null|Profile $scoped
* *
* @throws ClientException * @return ImageFile
* @throws NoResultException * @throws NoResultException
* @throws NoUploadedMediaException * @throws NoUploadedMediaException
* @throws ServerException * @throws ServerException
* @throws UnsupportedMediaException * @throws UnsupportedMediaException
* @throws UseFileAsThumbnailException * @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() 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) { if ($this->type === IMAGETYPE_JPEG || $this->type === IMAGETYPE_GIF) {
return $this->type; return $this->type;
} }
@ -245,13 +250,13 @@ class ImageFile extends MediaFile
* *
* @param string $outpath * @param string $outpath
* *
* @throws ClientException * @return ImageFile the image stored at target path
* @throws NoResultException * @throws NoResultException
* @throws ServerException * @throws ServerException
* @throws UnsupportedMediaException * @throws UnsupportedMediaException
* @throws UseFileAsThumbnailException * @throws UseFileAsThumbnailException
* *
* @return ImageFile the image stored at target path * @throws ClientException
*/ */
public function copyTo($outpath) public function copyTo($outpath)
{ {
@ -263,20 +268,21 @@ class ImageFile extends MediaFile
* *
* @param string $outpath * @param string $outpath
* @param array $box width, height, boundary box (x,y,w,h) defaults to full image * @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 * @return string full local filesystem filename
* @throws UnsupportedMediaException * @throws UnsupportedMediaException
* @throws UseFileAsThumbnailException * @throws UseFileAsThumbnailException
* *
* @return string full local filesystem filename
*/ */
public function resizeTo($outpath, array $box = []) public function resizeTo($outpath, array $box = [])
{ {
$box['width'] = isset($box['width']) ? intval($box['width']) : $this->width; $box['width'] = isset($box['width']) ? (int)($box['width']) : $this->width;
$box['height'] = isset($box['height']) ? intval($box['height']) : $this->height; $box['height'] = isset($box['height']) ? (int)($box['height']) : $this->height;
$box['x'] = isset($box['x']) ? intval($box['x']) : 0; $box['x'] = isset($box['x']) ? (int)($box['x']) : 0;
$box['y'] = isset($box['y']) ? intval($box['y']) : 0; $box['y'] = isset($box['y']) ? (int)($box['y']) : 0;
$box['w'] = isset($box['w']) ? intval($box['w']) : $this->width; $box['w'] = isset($box['w']) ? (int)($box['w']) : $this->width;
$box['h'] = isset($box['h']) ? intval($box['h']) : $this->height; $box['h'] = isset($box['h']) ? (int)($box['h']) : $this->height;
if (!file_exists($this->filepath)) { if (!file_exists($this->filepath)) {
// TRANS: Exception thrown during resize when image has been registered as present, // TRANS: Exception thrown during resize when image has been registered as present,
@ -346,7 +352,7 @@ class ImageFile extends MediaFile
try { try {
$img = Image::make($this->filepath); $img = Image::make($this->filepath);
} catch (Exception $e) { } 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. // TRANS: Exception thrown when trying to resize an unknown file type.
throw new Exception(_m('Unknown file type')); throw new Exception(_m('Unknown file type'));
} }
@ -359,7 +365,9 @@ class ImageFile extends MediaFile
$img = $img->orientate(); $img = $img->orientate();
} }
$img->fit($box['width'], $box['height'], $img->fit(
$box['width'],
$box['height'],
function ($constraint) { function ($constraint) {
if (common_config('attachments', 'upscale') !== true) { if (common_config('attachments', 'upscale') !== true) {
$constraint->upsize(); // Prevent upscaling $constraint->upsize(); // Prevent upscaling
@ -421,9 +429,9 @@ class ImageFile extends MediaFile
* @param $crop int Crop to the size (not preserving aspect ratio) * @param $crop int Crop to the size (not preserving aspect ratio)
* @param int $rotate * @param int $rotate
* *
* @return array
* @throws ServerException * @throws ServerException
* *
* @return array
*/ */
public static function getScalingValues( public static function getScalingValues(
$width, $width,
@ -484,10 +492,10 @@ class ImageFile extends MediaFile
$rw = ceil($width * $rh / $height); $rw = ceil($width * $rh / $height);
} }
} }
return array(intval($rw), intval($rh), return [(int)$rw, (int)$rh,
intval($cx), intval($cy), (int)$cx, (int)$cy,
is_null($cw) ? $width : intval($cw), is_null($cw) ? $width : (int)$cw,
is_null($ch) ? $height : intval($ch)); is_null($ch) ? $height : (int)$ch,];
} }
/** /**
@ -588,7 +596,8 @@ class ImageFile extends MediaFile
]; ];
$outpath = File_thumbnail::path( $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. // Doublecheck that parameters are sane and integers.
if ($box['width'] < 1 || $box['width'] > common_config('thumbnail', 'maxsize') if ($box['width'] < 1 || $box['width'] > common_config('thumbnail', 'maxsize')

View File

@ -1,33 +1,33 @@
<?php <?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/>.
/** /**
* GNU social - a federating social network
*
* Abstraction for media files * 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 <http://www.gnu.org/licenses/>.
*
* @category Media * @category Media
* @package GNUsocial * @package GNUsocial
*
* @author Robin Millette <robin@millette.info> * @author Robin Millette <robin@millette.info>
* @author Miguel Dantas <biodantas@gmail.com> * @author Miguel Dantas <biodantas@gmail.com>
* @author Zach Copley <zach@status.net> * @author Zach Copley <zach@status.net>
* @author Mikael Nordfeldth <mmn@hethane.se> * @author Mikael Nordfeldth <mmn@hethane.se>
* @copyright 2008-2009, 2019 Free Software Foundation http://fsf.org * @author Diogo Cordeiro <diogo@fc.up.pt>
* @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 * @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(); defined('GNUSOCIAL') || die();
/** /**
@ -35,21 +35,22 @@ defined('GNUSOCIAL') || die();
*/ */
class MediaFile class MediaFile
{ {
public $id = null; public $id;
public $filepath = null; public $filepath;
public $filename = null; public $filename;
public $fileRecord = null; public $fileRecord;
public $fileurl = null; public $fileurl;
public $short_fileurl = null; public $short_fileurl;
public $mimetype = null; public $mimetype;
/** /**
* @param string $filepath The path of the file this media refers to. Required * @param string $filepath The path of the file this media refers to. Required
* @param string $mimetype The mimetype of the file. Required * @param string $mimetype The mimetype of the file. Required
* @param $filehash The hash of the file, if known. Optional * @param string $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. * @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 * If null, it searches for it. If -1, it skips all DB
* interactions (useful for temporary objects) * interactions (useful for temporary objects)
*
* @throws ClientException * @throws ClientException
* @throws NoResultException * @throws NoResultException
* @throws ServerException * @throws ServerException
@ -80,7 +81,7 @@ class MediaFile
$this->fileurl = common_local_url( $this->fileurl = common_local_url(
'attachment', 'attachment',
array('attachment' => $this->fileRecord->id) ['attachment' => $this->fileRecord->id]
); );
$this->short_fileurl = common_shorten_url($this->fileurl); $this->short_fileurl = common_shorten_url($this->fileurl);
@ -125,14 +126,17 @@ class MediaFile
* Calculate the hash of a file. * Calculate the hash of a file.
* *
* This won't work for files >2GiB because PHP uses only 32bit. * This won't work for files >2GiB because PHP uses only 32bit.
*
* @param string $filepath * @param string $filepath
* @param string|null $filehash * @param null|string $filehash
*
* @return string * @return string
* @throws ServerException * @throws ServerException
*
*/ */
public static function getHashOfFile(string $filepath, $filehash = null) 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) { if ($filehash === null) {
// Calculate if we have an older upload method somewhere (Qvitter) that // Calculate if we have an older upload method somewhere (Qvitter) that
// doesn't do this before calling new MediaFile on its local files... // 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 * Retrieve or insert as a file in the DB
* *
* @return object File * @return object File
* @throws ClientException
* @throws ServerException * @throws ServerException
*
* @throws ClientException
*/ */
protected function storeFile() protected function storeFile()
{ {
@ -161,7 +166,7 @@ class MediaFile
// Well, let's just continue below. // 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 = new File;
@ -179,7 +184,7 @@ class MediaFile
$file_id = $file->insert(); $file_id = $file->insert();
if ($file_id === false) { if ($file_id === false) {
common_log_db_error($file, "INSERT", __FILE__); common_log_db_error($file, 'INSERT', __FILE__);
// TRANS: Client exception thrown when a database error was thrown during a file upload operation. // 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.')); 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 // Set file geometrical properties if available
try { try {
$image = ImageFile::fromFileObject($file); $image = ImageFile::fromFileObject($file);
$orig = clone($file); $orig = clone $file;
$file->width = $image->width; $file->width = $image->width;
$file->height = $image->height; $file->height = $image->height;
$file->update($orig); $file->update($orig);
@ -239,6 +244,10 @@ class MediaFile
/** /**
* Encodes a file name and a file hash in the new file format, which is used to avoid * 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 * 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 * @throws ClientException
*/ */
public static function encodeFilename($original_name, string $filehash, $ext = null): string public static function encodeFilename($original_name, string $filehash, $ext = null): string
@ -268,6 +277,7 @@ class MediaFile
/** /**
* Decode the new filename format * Decode the new filename format
*
* @return false | null | string on failure, no match (old format) or original file name, respectively * @return false | null | string on failure, no match (old format) or original file name, respectively
*/ */
public static function decodeFilename(string $encoded_filename) public static function decodeFilename(string $encoded_filename)
@ -319,19 +329,21 @@ class MediaFile
* format ("{$hash}.{$ext}") * format ("{$hash}.{$ext}")
* *
* @param string $param Form name * @param string $param Form name
* @param Profile|null $scoped * @param null|Profile $scoped
*
* @return ImageFile|MediaFile * @return ImageFile|MediaFile
* @throws ClientException
* @throws NoResultException * @throws NoResultException
* @throws NoUploadedMediaException * @throws NoUploadedMediaException
* @throws ServerException * @throws ServerException
* @throws UnsupportedMediaException * @throws UnsupportedMediaException
* @throws UseFileAsThumbnailException * @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. // 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); 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. // TRANS: Client exception thrown when a file upload operation has been stopped by an extension.
throw new ClientException(_m('File upload stopped by extension.')); throw new ClientException(_m('File upload stopped by extension.'));
default: 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. // TRANS: Client exception thrown when a file upload operation has failed with an unknown reason.
throw new ClientException(_m('System error uploading file.')); throw new ClientException(_m('System error uploading file.'));
} }
@ -417,7 +429,7 @@ class MediaFile
return new ImageFile(null, $filepath); 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)
@ -474,7 +486,7 @@ class MediaFile
} }
} }
return new MediaFile($filename, $mimetype, $filehash); return new self($filename, $mimetype, $filehash);
} }
/** /**
@ -482,12 +494,14 @@ class MediaFile
* *
* @param string $filepath filesystem path as string (file must exist) * @param string $filepath filesystem path as string (file must exist)
* @param bool $originalFilename (optional) for extension-based detection * @param bool $originalFilename (optional) for extension-based detection
*
* @return string * @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 * @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)
{ {
@ -581,13 +595,13 @@ class MediaFile
// Unclear types are such that we can't really tell by the auto // Unclear types are such that we can't really tell by the auto
// detect what they are (.bin, .exe etc. are just "octet-stream") // detect what they are (.bin, .exe etc. are just "octet-stream")
$unclearTypes = array('application/octet-stream', $unclearTypes = ['application/octet-stream',
'application/vnd.ms-office', 'application/vnd.ms-office',
'application/zip', 'application/zip',
'text/plain', 'text/plain',
'text/html', // Ironically, Wikimedia Commons' SVG_logo.svg is identified as text/html 'text/html', // Ironically, Wikimedia Commons' SVG_logo.svg is identified as text/html
// TODO: for XML we could do better content-based sniffing too // TODO: for XML we could do better content-based sniffing too
'text/xml'); 'text/xml',];
$supported = common_config('attachments', 'supported'); $supported = common_config('attachments', 'supported');
@ -630,10 +644,12 @@ class MediaFile
/** /**
* Title for a file, to display in the interface (if there's no better title) and * Title for a file, to display in the interface (if there's no better title) and
* for download filenames * for download filenames
*
* @param $file File object * @param $file File object
* @returns string * @returns string
*/ */
public static function getDisplayName(File $file) : string { public static function getDisplayName(File $file): string
{
if (empty($file->filename)) { if (empty($file->filename)) {
return _m('Untitled attachment'); return _m('Untitled attachment');
} }