forked from GNUsocial/gnu-social
		
	[MEDIA] ImageFile fromUpload method wasn't ensuring uploaded file was an image
This commit is contained in:
		| @@ -1,34 +1,32 @@ | ||||
| <?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 | ||||
|  * | ||||
|  * 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 | ||||
|  * @package   StatusNet | ||||
|  * @package   GNUsocial | ||||
|  * | ||||
|  * @author    Evan Prodromou <evan@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 | ||||
|  * @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 <evan@status.net> | ||||
|  * @author   Zach Copley <zach@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 | ||||
|  * @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', | ||||
|         $this->elementStart('form', ['enctype' => 'multipart/form-data', | ||||
|             'method' => 'post', | ||||
|             'id' => 'form_settings_avatar', | ||||
|             'class' => 'form_settings', | ||||
|                                           'action' => | ||||
|                                           common_local_url('avatarsettings'))); | ||||
|             '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(), | ||||
|                 $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())); | ||||
|                     'alt' => $this->scoped->getNickname(),]); | ||||
|                 $this->elementEnd('div'); | ||||
|                 $this->elementEnd('li'); | ||||
|             } catch (NoAvatarException $e) { | ||||
| @@ -139,15 +144,15 @@ 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(), | ||||
|                 $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())); | ||||
|                     'alt' => $this->scoped->getNickname(),]); | ||||
|                 $this->elementEnd('div'); | ||||
|                 if (!empty($avatar->filename)) { | ||||
|                     // TRANS: Button on avatar upload page to delete current avatar. | ||||
| @@ -158,14 +163,14 @@ class AvatarsettingsAction extends SettingsAction | ||||
|                 // No previously uploaded avatar to preview. | ||||
|             } | ||||
|  | ||||
|             $this->elementStart('li', array ('id' => 'settings_attach')); | ||||
|             $this->element('input', array('name' => 'MAX_FILE_SIZE', | ||||
|             $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', array('name' => 'avatarfile', | ||||
|                 'value' => ImageFile::maxFileSizeInt(),]); | ||||
|             $this->element('input', ['name' => 'avatarfile', | ||||
|                 'type' => 'file', | ||||
|                                           'id' => 'avatarfile')); | ||||
|                 'id' => 'avatarfile',]); | ||||
|             $this->elementEnd('li'); | ||||
|             $this->elementEnd('ul'); | ||||
|  | ||||
| @@ -176,56 +181,59 @@ class AvatarsettingsAction extends SettingsAction | ||||
|             $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', | ||||
|         $this->elementStart('form', ['method' => 'post', | ||||
|             'id' => 'form_settings_avatar', | ||||
|             'class' => 'form_settings', | ||||
|                                           'action' => | ||||
|                                           common_local_url('avatarsettings'))); | ||||
|             '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']), | ||||
|         $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())); | ||||
|             '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']), | ||||
|         $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())); | ||||
|             '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, | ||||
|         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)); | ||||
|                 'id' => $crop_info,]); | ||||
|         } | ||||
|  | ||||
|         // 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'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @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(); | ||||
|             } elseif ($this->trimmed('crop')) { | ||||
| @@ -248,9 +267,9 @@ class AvatarsettingsAction extends SettingsAction | ||||
|                 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(), | ||||
|         $filename = Avatar::filename( | ||||
|             $this->scoped->getID(), | ||||
|             image_type_to_extension($type), | ||||
|             null, | ||||
|                                      'tmp'.common_timestamp()); | ||||
|             'tmp' . common_timestamp() | ||||
|         ); | ||||
|  | ||||
|         $filepath = Avatar::path($filename); | ||||
|         $imagefile = $imagefile->copyTo($filepath); | ||||
|  | ||||
|         $filedata = array('filename' => $filename, | ||||
|         $filedata = [ | ||||
|             'filename' => $filename, | ||||
|             'filepath' => $filepath, | ||||
|             'width' => $imagefile->width, | ||||
|             'height' => $imagefile->height, | ||||
|                           'type' => $type); | ||||
|             '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,7 +339,7 @@ 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']); | ||||
| @@ -313,15 +348,19 @@ class AvatarsettingsAction extends SettingsAction | ||||
|         $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'))); | ||||
|         $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, | ||||
|                      'w' => $dest_w,   'h' => $dest_h); | ||||
|             '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) { | ||||
| @@ -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,8 +403,7 @@ 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'); | ||||
| @@ -374,7 +414,7 @@ class AvatarsettingsAction extends SettingsAction | ||||
|      * | ||||
|      * @return void | ||||
|      */ | ||||
|     function showScripts() | ||||
|     public function showScripts() | ||||
|     { | ||||
|         parent::showScripts(); | ||||
|  | ||||
|   | ||||
| @@ -24,10 +24,9 @@ | ||||
|  * @author    Zach Copley <zach@status.net> | ||||
|  * @author    Mikael Nordfeldth <mmn@hethane.se> | ||||
|  * @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 | ||||
|  * | ||||
|  * @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 <evan@status.net> | ||||
|  * @author   Zach Copley <zach@status.net> | ||||
| @@ -52,8 +52,8 @@ class ImageFile extends MediaFile | ||||
|     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 $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) | ||||
|     { | ||||
| @@ -74,12 +74,7 @@ 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); | ||||
| @@ -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 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, | ||||
| @@ -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,7 +365,9 @@ class ImageFile extends MediaFile | ||||
|             $img = $img->orientate(); | ||||
|         } | ||||
|  | ||||
|         $img->fit($box['width'], $box['height'], | ||||
|         $img->fit( | ||||
|             $box['width'], | ||||
|             $box['height'], | ||||
|             function ($constraint) { | ||||
|                 if (common_config('attachments', 'upscale') !== true) { | ||||
|                     $constraint->upsize(); // Prevent upscaling | ||||
| @@ -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, | ||||
| @@ -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,]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -588,7 +596,8 @@ class ImageFile extends MediaFile | ||||
|         ]; | ||||
|  | ||||
|         $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') | ||||
|   | ||||
| @@ -1,33 +1,33 @@ | ||||
| <?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 | ||||
|  * | ||||
|  * 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 | ||||
|  * @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> | ||||
|  * @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 | ||||
|  * @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. | ||||
|      * @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 | ||||
| @@ -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,7 +166,7 @@ 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; | ||||
|  | ||||
| @@ -179,7 +184,7 @@ class MediaFile | ||||
|         $file_id = $file->insert(); | ||||
|  | ||||
|         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. | ||||
|             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); | ||||
| @@ -239,6 +244,10 @@ 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 | ||||
| @@ -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) | ||||
|     { | ||||
|         // 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,7 +429,7 @@ 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) | ||||
| @@ -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 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) | ||||
|     { | ||||
| @@ -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', | ||||
|         $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'); | ||||
|             'text/xml',]; | ||||
|  | ||||
|         $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 | ||||
|      * 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'); | ||||
|         } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user