diff --git a/DOCUMENTATION/SYSTEM_ADMINISTRATORS/CONFIGURE.md b/DOCUMENTATION/SYSTEM_ADMINISTRATORS/CONFIGURE.md index 02f0db7a6d..5346f1f378 100644 --- a/DOCUMENTATION/SYSTEM_ADMINISTRATORS/CONFIGURE.md +++ b/DOCUMENTATION/SYSTEM_ADMINISTRATORS/CONFIGURE.md @@ -649,7 +649,16 @@ detection. * `supported`: an array of mime types you accept to store and distribute, like 'image/gif', 'video/mpeg', 'audio/mpeg', etc. Make sure you setup your server to properly recognize the types you want to - support. + support. It's important to use the result of calling `image_type_to_extension` + for the appropriate image type, in the case of images. This is so all parts of + the code see the same extension for each image type (jpg vs jpeg). + For example, to enable BMP uploads, add this to the config.php file: + $config['attachments']['supported'][image_type_to_mime_type(IMAGETYPE_GIF)] + = image_type_to_extension(IMAGETYPE_GIF); + See https://www.php.net/manual/en/function.image-type-to-mime-type.php for a + list of such constants. If a filetype is not listed there, it's possible to add + the mimetype and the extension by hand, but they need to match those returned by + the file command. * `uploads`: false to disable uploading files with notices (true by default). diff --git a/README.md b/README.md index 63a4d1abde..b04a465e04 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# GNU social 1.19.x +# GNU social 1.20.x (c) 2010-2019 Free Software Foundation, Inc This is the README file for GNU social, the free diff --git a/classes/File.php b/classes/File.php index 67b87efd0d..a9ef3125de 100644 --- a/classes/File.php +++ b/classes/File.php @@ -1,23 +1,32 @@ . + * along with this program. If not, see . + * + * @category Files + * @package GNUsocial + * @author Mikael Nordfeldth + * @author Miguel Dantas + * @copyright 2008-2009, 2019 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/ */ -if (!defined('GNUSOCIAL')) { exit(1); } +defined('GNUSOCIAL') || die(); /** * Table Definition for file @@ -93,6 +102,7 @@ class File extends Managed_DataObject * @param array $redir_data lookup data eg from File_redirection::where() * @param string $given_url * @return File + * @throws ServerException */ public static function saveNew(array $redir_data, $given_url) { @@ -298,8 +308,10 @@ class File extends Managed_DataObject } /** - * @param $mimetype The mimetype we've discovered for this file. - * @param $filename An optional filename which we can use on failure. + * @param $mimetype string The mimetype we've discovered for this file. + * @param $filename string An optional filename which we can use on failure. + * @return mixed|string + * @throws ClientException */ static function guessMimeExtension($mimetype, $filename=null) { @@ -349,6 +361,8 @@ class File extends Managed_DataObject /** * Validation for as-saved base filenames + * @param $filename + * @return false|int */ static function validFilename($filename) { @@ -366,7 +380,9 @@ class File extends Managed_DataObject } /** - * @throws ClientException on invalid filename + * @param $filename + * @return string + * @throws InvalidFilenameException */ static function path($filename) { @@ -534,7 +550,9 @@ class File extends Managed_DataObject } /** - * @param mixed $use_local true means require local, null means prefer local, false means use whatever is stored + * @param mixed $use_local true means require local, null means prefer local, false means use whatever is stored + * @return string + * @throws FileNotStoredLocallyException */ public function getUrl($use_local=null) { @@ -565,7 +583,9 @@ class File extends Managed_DataObject } /** - * @param string $hashstr String of (preferrably lower case) hexadecimal characters, same as result of 'hash_file(...)' + * @param string $hashstr String of (preferrably lower case) hexadecimal characters, same as result of 'hash_file(...)' + * @return File + * @throws NoResultException */ static public function getByHash($hashstr) { diff --git a/lib/default.php b/lib/default.php index cacb9d88cb..c73f8fccd2 100644 --- a/lib/default.php +++ b/lib/default.php @@ -1,10 +1,7 @@ - * @copyright 2008-9 StatusNet, Inc. + * @copyright 2008-2009, 2019 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://www.gnu.org/software/social/ + * @link https://www.gnu.org/software/social/ */ $default = @@ -253,11 +250,11 @@ $default = 'application/x-go-sgf' => 'sgf', 'application/xml' => 'xml', 'application/gpx+xml' => 'gpx', - 'image/png' => 'png', - 'image/jpeg' => 'jpg', - 'image/gif' => 'gif', - 'image/svg+xml' => 'svg', - 'image/vnd.microsoft.icon' => 'ico', + image_type_to_mime_type(IMAGETYPE_PNG) => image_type_to_extension(IMAGETYPE_PNG), + image_type_to_mime_type(IMAGETYPE_JPEG) => image_type_to_extension(IMAGETYPE_JPEG), + image_type_to_mime_type(IMAGETYPE_GIF) => image_type_to_extension(IMAGETYPE_GIF), + 'image/svg+xml' => 'svg', // No built-in constant + image_type_to_mime_type(IMAGETYPE_ICO) => image_type_to_extension(IMAGETYPE_ICO), 'audio/ogg' => 'ogg', 'audio/mpeg' => 'mpg', 'audio/x-speex' => 'spx', @@ -280,6 +277,7 @@ $default = 'php' => 'phps', // this turns .php into .phps 'exe' => false, // this would deny any uploads to keep the "exe" file extension ], + 'memory_limit' => '1024M' // PHP's memory limit to use temporarily when handling images ), 'thumbnail' => [ 'dir' => null, // falls back to File::path('thumb') (equivalent to ['attachments']['dir'] . '/thumb/') diff --git a/lib/framework.php b/lib/framework.php index 0d6a2c44c2..2d74695354 100644 --- a/lib/framework.php +++ b/lib/framework.php @@ -22,7 +22,7 @@ if (!defined('GNUSOCIAL')) { exit(1); } define('GNUSOCIAL_ENGINE', 'GNU social'); define('GNUSOCIAL_ENGINE_URL', 'https://www.gnu.org/software/social/'); -define('GNUSOCIAL_BASE_VERSION', '1.19.4'); +define('GNUSOCIAL_BASE_VERSION', '1.20.0'); define('GNUSOCIAL_LIFECYCLE', 'rc0'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release' define('GNUSOCIAL_VERSION', GNUSOCIAL_BASE_VERSION . '-' . GNUSOCIAL_LIFECYCLE); diff --git a/lib/imagefile.php b/lib/imagefile.php index 80bc90f125..bc5712056f 100644 --- a/lib/imagefile.php +++ b/lib/imagefile.php @@ -1,11 +1,9 @@ . * * @category Image - * @package StatusNet + * @package GNUsocial * @author Evan Prodromou * @author Zach Copley - * @copyright 2008-2009 StatusNet, Inc. + * @author Mikael Nordfeldth + * @author Miguel Dantas + * @copyright 2008, 2019 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/ + * @link https://www.gnu.org/software/social/ */ -if (!defined('GNUSOCIAL')) { exit(1); } +defined('GNUSOCIAL') || die(); /** - * A wrapper on uploaded files + * A wrapper on uploaded images * * Makes it slightly easier to accept an image file from upload. * * @category Image - * @package StatusNet + * @package GNUsocial + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @author Evan Prodromou * @author Zach Copley - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://status.net/ + * @link https://www.gnu.org/software/social/ */ - -class ImageFile +class ImageFile extends MediaFile { - var $id; - var $filepath; - var $filename; - var $type; - var $height; - var $width; - var $rotate=0; // degrees to rotate for properly oriented image (extrapolated from EXIF etc.) - var $animated = null; // Animated image? (has more than 1 frame). null means untested - var $mimetype = null; // The _ImageFile_ mimetype, _not_ the originating File object + public $type; + public $height; + public $width; + public $rotate = 0; // degrees to rotate for properly oriented image (extrapolated from EXIF etc.) + public $animated = null; // Animated image? (has more than 1 frame). null means untested + public $mimetype = null; // The _ImageFile_ mimetype, _not_ the originating File object - protected $fileRecord = null; - - function __construct($id, $filepath) + public function __construct($id, string $filepath) { - $this->id = $id; - if (!empty($this->id)) { - $this->fileRecord = new File(); - $this->fileRecord->id = $this->id; - if (!$this->fileRecord->find(true)) { - // If we have set an ID, we need that ID to exist! - throw new NoResultException($this->fileRecord); - } - } - // These do not have to be the same as fileRecord->filename for example, // since we may have generated an image source file from something else! $this->filepath = $filepath; @@ -76,16 +60,14 @@ class ImageFile $info = @getimagesize($this->filepath); - if (!( - ($info[2] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) || - ($info[2] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) || - $info[2] == IMAGETYPE_BMP || - ($info[2] == IMAGETYPE_WBMP && function_exists('imagecreatefromwbmp')) || - ($info[2] == IMAGETYPE_XBM && function_exists('imagecreatefromxbm')) || - ($info[2] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')))) { - + if (!(($info[2] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) || + ($info[2] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) || + ($info[2] == IMAGETYPE_BMP && function_exists('imagecreatefrombmp')) || + ($info[2] == IMAGETYPE_WBMP && function_exists('imagecreatefromwbmp')) || + ($info[2] == IMAGETYPE_XBM && function_exists('imagecreatefromxbm')) || + ($info[2] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')))) { // TRANS: Exception thrown when trying to upload an unsupported image file format. - throw new UnsupportedMediaException(_('Unsupported image format.'), $this->filepath); + throw new UnsupportedMediaException(_m('Unsupported image format.'), $this->filepath); } $this->width = $info[0]; @@ -93,11 +75,18 @@ class ImageFile $this->type = $info[2]; $this->mimetype = $info['mime']; + parent::__construct( + $filepath, + $this->mimetype, + null /* filehash, MediaFile will calculate it */, + $id + ); + if ($this->type === IMAGETYPE_JPEG && function_exists('exif_read_data')) { // Orientation value to rotate thumbnails properly $exif = @exif_read_data($this->filepath); if (is_array($exif) && isset($exif['Orientation'])) { - switch ((int)$exif['Orientation']) { + switch (intval($exif['Orientation'])) { case 1: // top is top $this->rotate = 0; break; @@ -126,7 +115,7 @@ class ImageFile $media = common_get_mime_media($file->mimetype); if (Event::handle('CreateFileImageThumbnailSource', array($file, &$imgPath, $media))) { if (empty($file->filename) && !file_exists($imgPath)) { - throw new UnsupportedMediaException(_('File without filename could not get a thumbnail source.')); + throw new UnsupportedMediaException(_m('File without filename could not get a thumbnail source.')); } // First some mimetype specific exceptions @@ -141,7 +130,7 @@ class ImageFile $imgPath = $file->getPath(); break; default: - throw new UnsupportedMediaException(_('Unsupported media format.'), $file->getPath()); + throw new UnsupportedMediaException(_m('Unsupported media format.'), $file->getPath()); } } @@ -155,7 +144,8 @@ class ImageFile // Avoid deleting the original try { if (strlen($imgPath) > 0 && $imgPath !== $file->getPath()) { - common_debug(__METHOD__.': Deleting temporary file that was created as image file thumbnail source: '._ve($imgPath)); + common_debug(__METHOD__.': Deleting temporary file that was created as image file' . + 'thumbnail source: '._ve($imgPath)); @unlink($imgPath); } } catch (FileNotFoundException $e) { @@ -163,7 +153,9 @@ class ImageFile // doesn't exist anyway, so it's safe to delete $imgPath @unlink($imgPath); } - common_debug(sprintf('Exception %s caught when creating ImageFile for File id==%s and imgPath==%s: %s', get_class($e), _ve($file->id), _ve($imgPath), _ve($e->getMessage()))); + common_debug(sprintf('Exception %s caught when creating ImageFile for File id==%s ' . + 'and imgPath==%s: %s', get_class($e), _ve($file->id), + _ve($imgPath), _ve($e->getMessage()))); throw $e; } return $image; @@ -178,42 +170,43 @@ class ImageFile return $this->filepath; } - static function fromUpload($param='upload') + /** + * Process a file upload + * + * Uses MediaFile's `fromUpload` to do the majority of the work and reencodes the image, + * to mitigate injection attacks. + * @param string $param + * @param Profile|null $scoped + * @return ImageFile|MediaFile + * @throws ClientException + * @throws NoResultException + * @throws NoUploadedMediaException + * @throws ServerException + * @throws UnsupportedMediaException + * @throws UseFileAsThumbnailException + */ + public static function fromUpload(string $param='upload', Profile $scoped = null) { - switch ($_FILES[$param]['error']) { - case UPLOAD_ERR_OK: // success, jump out - break; + return parent::fromUpload($param, $scoped); + } - case UPLOAD_ERR_INI_SIZE: - case UPLOAD_ERR_FORM_SIZE: - // TRANS: Exception thrown when too large a file is uploaded. - // TRANS: %s is the maximum file size, for example "500b", "10kB" or "2MB". - throw new Exception(sprintf(_('That file is too big. The maximum file size is %s.'), ImageFile::maxFileSize())); - - case UPLOAD_ERR_PARTIAL: - @unlink($_FILES[$param]['tmp_name']); - // TRANS: Exception thrown when uploading an image and that action could not be completed. - throw new Exception(_('Partial upload.')); - - case UPLOAD_ERR_NO_FILE: - // No file; probably just a non-AJAX submission. - throw new ClientException(_('No file uploaded.')); - - default: - common_log(LOG_ERR, __METHOD__ . ": Unknown upload error " . $_FILES[$param]['error']); - // TRANS: Exception thrown when uploading an image fails for an unknown reason. - throw new Exception(_('System error uploading file.')); + /** + * Several obscure file types should be normalized to PNG on resize. + * + * Keeps only PNG, JPEG and GIF + * + * @return int + */ + public function preferredType() + { + // Keep only JPEG and GIF in their orignal format + if ($this->type === IMAGETYPE_JPEG || $this->type === IMAGETYPE_GIF) { + return $this->type; } - - $info = @getimagesize($_FILES[$param]['tmp_name']); - - if (!$info) { - @unlink($_FILES[$param]['tmp_name']); - // TRANS: Exception thrown when uploading a file as image that is not an image or is a corrupt file. - throw new UnsupportedMediaException(_('Not an image or corrupt file.'), '[deleted]'); - } - - return new ImageFile(null, $_FILES[$param]['tmp_name']); + // We don't want to save some formats as they are rare, inefficient and antiquated + // thus we can't guarantee clients will support + // So just save it as PNG + return IMAGETYPE_PNG; } /** @@ -224,6 +217,11 @@ class ImageFile * * @param string $outpath * @return ImageFile the image stored at target path + * @throws ClientException + * @throws NoResultException + * @throws ServerException + * @throws UnsupportedMediaException + * @throws UseFileAsThumbnailException */ function copyTo($outpath) { @@ -234,36 +232,35 @@ class ImageFile * Create and save a thumbnail image. * * @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 + * @throws UnsupportedMediaException + * @throws UseFileAsThumbnailException */ function resizeTo($outpath, array $box=array()) { - $box['width'] = isset($box['width']) ? intval($box['width']) : $this->width; + $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['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; if (!file_exists($this->filepath)) { // TRANS: Exception thrown during resize when image has been registered as present, but is no longer there. - throw new Exception(_('Lost our file.')); + throw new Exception(_m('Lost our file.')); } // Don't rotate/crop/scale if it isn't necessary - if ($box['width'] === $this->width - && $box['height'] === $this->height - && $box['x'] === 0 - && $box['y'] === 0 - && $box['w'] === $this->width - && $box['h'] === $this->height - && $this->type == $this->preferredType()) { - if ($this->rotate == 0) { - // No rotational difference, just copy it as-is - @copy($this->filepath, $outpath); - return $outpath; - } elseif (abs($this->rotate) == 90) { + if ($box['width'] === $this->width + && $box['height'] === $this->height + && $box['x'] === 0 + && $box['y'] === 0 + && $box['w'] === $this->width + && $box['h'] === $this->height + && $this->type === $this->preferredType()) { + + if (abs($this->rotate) == 90) { // Box is rotated 90 degrees in either direction, // so we have to redefine x to y and vice versa. $tmp = $box['width']; @@ -278,7 +275,6 @@ class ImageFile } } - if (Event::handle('StartResizeImageFile', array($this, $outpath, $box))) { $this->resizeToFile($outpath, $box); } @@ -294,8 +290,22 @@ class ImageFile return $outpath; } + /** + * Resizes a file. If $box is omitted, the size is not changed, but this is still useful, + * because it will reencode the image in the `self::prefferedType()` format. This only + * applies henceforward, not retroactively + * + * Increases the 'memory_limit' to the one in the 'attachments' section in the config, to + * enable the handling of bigger images, which can cause a peak of memory consumption, while + * encoding + * @param $outpath + * @param array $box + * @throws Exception + */ protected function resizeToFile($outpath, array $box) { + $old_limit = ini_set('memory_limit', common_config('attachments', 'memory_limit')); + $image_src = null; switch ($this->type) { case IMAGETYPE_GIF: $image_src = imagecreatefromgif($this->filepath); @@ -317,7 +327,7 @@ class ImageFile break; default: // TRANS: Exception thrown when trying to resize an unknown file type. - throw new Exception(_('Unknown file type')); + throw new Exception(_m('Unknown file type')); } if ($this->rotate != 0) { @@ -326,30 +336,34 @@ class ImageFile $image_dest = imagecreatetruecolor($box['width'], $box['height']); - if ($this->type == IMAGETYPE_GIF || $this->type == IMAGETYPE_PNG || $this->type == IMAGETYPE_BMP) { + if ($this->type == IMAGETYPE_PNG || $this->type == IMAGETYPE_BMP) { $transparent_idx = imagecolortransparent($image_src); - if ($transparent_idx >= 0) { - + if ($transparent_idx >= 0 && $transparent_idx < 255) { $transparent_color = imagecolorsforindex($image_src, $transparent_idx); - $transparent_idx = imagecolorallocate($image_dest, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']); + $transparent_idx = imagecolorallocate($image_dest, $transparent_color['red'], + $transparent_color['green'], + $transparent_color['blue']); imagefill($image_dest, 0, 0, $transparent_idx); imagecolortransparent($image_dest, $transparent_idx); } elseif ($this->type == IMAGETYPE_PNG) { - imagealphablending($image_dest, false); $transparent = imagecolorallocatealpha($image_dest, 0, 0, 0, 127); imagefill($image_dest, 0, 0, $transparent); imagesavealpha($image_dest, true); - } } - imagecopyresampled($image_dest, $image_src, 0, 0, $box['x'], $box['y'], $box['width'], $box['height'], $box['w'], $box['h']); + imagecopyresampled($image_dest, $image_src, 0, 0, $box['x'], $box['y'], + $box['width'], $box['height'], $box['w'], $box['h']); - switch ($this->preferredType()) { + $type = $this->preferredType(); + $ext = image_type_to_extension($type, true); + $outpath = preg_replace("/\.[^\.]+$/", $ext, $outpath); + + switch ($type) { case IMAGETYPE_GIF: imagegif($image_dest, $outpath); break; @@ -361,92 +375,26 @@ class ImageFile break; default: // TRANS: Exception thrown when trying resize an unknown file type. - throw new Exception(_('Unknown file type')); + throw new Exception(_m('Unknown file type')); } imagedestroy($image_src); imagedestroy($image_dest); + ini_set('memory_limit', $old_limit); // Restore the old memory limit } - - /** - * Several obscure file types should be normalized to PNG on resize. - * - * @fixme consider flattening anything not GIF or JPEG to PNG - * @return int - */ - function preferredType() - { - if($this->type == IMAGETYPE_BMP) { - //we don't want to save BMP... it's an inefficient, rare, antiquated format - //save png instead - return IMAGETYPE_PNG; - } else if($this->type == IMAGETYPE_WBMP) { - //we don't want to save WBMP... it's a rare format that we can't guarantee clients will support - //save png instead - return IMAGETYPE_PNG; - } else if($this->type == IMAGETYPE_XBM) { - //we don't want to save XBM... it's a rare format that we can't guarantee clients will support - //save png instead - return IMAGETYPE_PNG; - } - return $this->type; - } - - function unlink() + public function unlink() { @unlink($this->filepath); } - static function maxFileSize() - { - $value = ImageFile::maxFileSizeInt(); - - if ($value > 1024 * 1024) { - $value = $value/(1024*1024); - // TRANS: Number of megabytes. %d is the number. - return sprintf(_m('%dMB','%dMB',$value),$value); - } else if ($value > 1024) { - $value = $value/1024; - // TRANS: Number of kilobytes. %d is the number. - return sprintf(_m('%dkB','%dkB',$value),$value); - } else { - // TRANS: Number of bytes. %d is the number. - return sprintf(_m('%dB','%dB',$value),$value); - } - } - - static function maxFileSizeInt() - { - return min(ImageFile::strToInt(ini_get('post_max_size')), - ImageFile::strToInt(ini_get('upload_max_filesize')), - ImageFile::strToInt(ini_get('memory_limit'))); - } - - static function strToInt($str) - { - $unit = substr($str, -1); - $num = substr($str, 0, -1); - - switch(strtoupper($unit)){ - case 'G': - $num *= 1024; - case 'M': - $num *= 1024; - case 'K': - $num *= 1024; - } - - return $num; - } - public function scaleToFit($maxWidth=null, $maxHeight=null, $crop=null) { return self::getScalingValues($this->width, $this->height, - $maxWidth, $maxHeight, $crop, $this->rotate); + $maxWidth, $maxHeight, $crop, $this->rotate); } - /* + /** * Gets scaling values for images of various types. Cropping can be enabled. * * Values will scale _up_ to fit max values if cropping is enabled! @@ -457,14 +405,17 @@ class ImageFile * @param $maxW int Resulting max width * @param $maxH int Resulting max height * @param $crop int Crop to the size (not preserving aspect ratio) + * @param int $rotate + * @return array + * @throws ServerException */ public static function getScalingValues($width, $height, - $maxW=null, $maxH=null, - $crop=null, $rotate=0) + $maxW=null, $maxH=null, + $crop=null, $rotate=0) { $maxW = $maxW ?: common_config('thumbnail', 'width'); $maxH = $maxH ?: common_config('thumbnail', 'height'); - + if ($maxW < 1 || ($maxH !== null && $maxH < 1)) { throw new ServerException('Bad parameters for ImageFile::getScalingValues'); } elseif ($maxH === null) { @@ -479,14 +430,14 @@ class ImageFile $width = $height; $height = $tmp; } - + // Cropping data (for original image size). Default values, 0 and null, // imply no cropping and with preserved aspect ratio (per axis). $cx = 0; // crop x $cy = 0; // crop y $cw = null; // crop area width $ch = null; // crop area height - + if ($crop) { $s_ar = $width / $height; $t_ar = $maxW / $maxH; @@ -513,9 +464,9 @@ class ImageFile } } return array(intval($rw), intval($rh), - intval($cx), intval($cy), - is_null($cw) ? $width : intval($cw), - is_null($ch) ? $height : intval($ch)); + intval($cx), intval($cy), + is_null($cw) ? $width : intval($cw), + is_null($ch) ? $height : intval($ch)); } /** @@ -560,9 +511,9 @@ class ImageFile } if ($width === null) { - $width = common_config('thumbnail', 'width'); + $width = common_config('thumbnail', 'width'); $height = common_config('thumbnail', 'height'); - $crop = common_config('thumbnail', 'crop'); + $crop = common_config('thumbnail', 'crop'); } if (!$upscale) { @@ -589,7 +540,7 @@ class ImageFile 'file_id'=> $this->fileRecord->getID(), 'width' => $width, 'height' => $height, - )); + )); if ($thumb instanceof File_thumbnail) { return $thumb; } @@ -614,7 +565,8 @@ class ImageFile throw new ServerException('Bad thumbnail size parameters.'); } - common_debug(sprintf('Generating a thumbnail of File id==%u of size %ux%u', $this->fileRecord->getID(), $width, $height)); + common_debug(sprintf('Generating a thumbnail of File id==%u of size %ux%u', + $this->fileRecord->getID(), $width, $height)); // Perform resize and store into file $this->resizeTo($outpath, $box); @@ -629,105 +581,9 @@ class ImageFile } return File_thumbnail::saveThumbnail($this->fileRecord->getID(), - null, // no url since we generated it ourselves and can dynamically generate the url - $width, $height, - $outname); + // no url since we generated it ourselves and can dynamically + // generate the url + null, + $width, $height, $outname); } } - -//PHP doesn't (as of 2/24/2010) have an imagecreatefrombmp so conditionally define one -if(!function_exists('imagecreatefrombmp')){ - //taken shamelessly from http://www.php.net/manual/en/function.imagecreatefromwbmp.php#86214 - function imagecreatefrombmp($p_sFile) - { - // Load the image into a string - $file = fopen($p_sFile,"rb"); - $read = fread($file,10); - while(!feof($file)&&($read<>"")) - $read .= fread($file,1024); - - $temp = unpack("H*",$read); - $hex = $temp[1]; - $header = substr($hex,0,108); - - // Process the header - // Structure: http://www.fastgraph.com/help/bmp_header_format.html - if (substr($header,0,4)=="424d") - { - // Cut it in parts of 2 bytes - $header_parts = str_split($header,2); - - // Get the width 4 bytes - $width = hexdec($header_parts[19].$header_parts[18]); - - // Get the height 4 bytes - $height = hexdec($header_parts[23].$header_parts[22]); - - // Unset the header params - unset($header_parts); - } - - // Define starting X and Y - $x = 0; - $y = 1; - - // Create newimage - $image = imagecreatetruecolor($width,$height); - - // Grab the body from the image - $body = substr($hex,108); - - // Calculate if padding at the end-line is needed - // Divided by two to keep overview. - // 1 byte = 2 HEX-chars - $body_size = (strlen($body)/2); - $header_size = ($width*$height); - - // Use end-line padding? Only when needed - $usePadding = ($body_size>($header_size*3)+4); - - // Using a for-loop with index-calculation instaid of str_split to avoid large memory consumption - // Calculate the next DWORD-position in the body - for ($i=0;$i<$body_size;$i+=3) - { - // Calculate line-ending and padding - if ($x>=$width) - { - // If padding needed, ignore image-padding - // Shift i to the ending of the current 32-bit-block - if ($usePadding) - $i += $width%4; - - // Reset horizontal position - $x = 0; - - // Raise the height-position (bottom-up) - $y++; - - // Reached the image-height? Break the for-loop - if ($y>$height) - break; - } - - // Calculation of the RGB-pixel (defined as BGR in image-data) - // Define $i_pos as absolute position in the body - $i_pos = $i*2; - $r = hexdec($body[$i_pos+4].$body[$i_pos+5]); - $g = hexdec($body[$i_pos+2].$body[$i_pos+3]); - $b = hexdec($body[$i_pos].$body[$i_pos+1]); - - // Calculate and draw the pixel - $color = imagecolorallocate($image,$r,$g,$b); - imagesetpixel($image,$x,$height-$y,$color); - - // Raise the horizontal position - $x++; - } - - // Unset the body / free the memory - unset($body); - - // Return image-object - return $image; - } -} // if(!function_exists('imagecreatefrombmp')) diff --git a/lib/mediafile.php b/lib/mediafile.php index 803cbe0a4c..bed3300d36 100644 --- a/lib/mediafile.php +++ b/lib/mediafile.php @@ -1,12 +1,8 @@ . * * @category Media - * @package StatusNet + * @package GNUsocial * @author Robin Millette + * @author Miguel Dantas * @author Zach Copley - * @copyright 2008-2009 StatusNet, Inc. + * @author Mikael Nordfeldth + * @copyright 2008-2009, 2019 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/ + * @link https://www.gnu.org/software/social/ */ if (!defined('GNUSOCIAL')) { exit(1); } + +/** + * Class responsible for abstracting media files + */ class MediaFile { - var $filename = null; - var $fileRecord = null; - var $fileurl = null; - var $short_fileurl = null; - var $mimetype = null; + public $id = null; + public $filepath = null; + public $filename = null; + public $fileRecord = null; + public $fileurl = null; + public $short_fileurl = null; + public $mimetype = null; - function __construct($filename = null, $mimetype = null, $filehash = null) + /** + * @param string $filepath The path of the file this media refers to. Required + * @param string $mimetype The mimetype of the file. Required + * @param $filehash The hash of the file, if known. Optional + * @param int|null $id The DB id of the file. Int if known, null if not. + * If null, it searches for it. If -1, it skips all DB + * interactions (useful for temporary objects) + * @throws ClientException + * @throws NoResultException + * @throws ServerException + */ + public function __construct(string $filepath, string $mimetype, $filehash = null, $id = null) { - $this->filename = $filename; - $this->mimetype = $mimetype; - $this->filehash = $filehash; - $this->fileRecord = $this->storeFile(); + $this->filepath = $filepath; + $this->filename = basename($this->filepath); + $this->mimetype = $mimetype; + $this->filehash = self::getHashOfFile($this->filepath, $filehash); + $this->id = $id; - $this->fileurl = common_local_url('attachment', - array('attachment' => $this->fileRecord->id)); + // If id is -1, it means we're dealing with a temporary object and don't want to store it in the DB, + // or add redirects + if ($this->id !== -1) { + if (!empty($this->id)) { + // If we have an id, load it + $this->fileRecord = new File(); + $this->fileRecord->id = $this->id; + if (!$this->fileRecord->find(true)) { + // If we have set an ID, we need that ID to exist! + throw new NoResultException($this->fileRecord); + } + } else { + // Otherwise, store it + $this->fileRecord = $this->storeFile(); + } - $this->maybeAddRedir($this->fileRecord->id, $this->fileurl); - $this->short_fileurl = common_shorten_url($this->fileurl); - $this->maybeAddRedir($this->fileRecord->id, $this->short_fileurl); + $this->fileurl = common_local_url( + 'attachment', + array('attachment' => $this->fileRecord->id) + ); + + $this->maybeAddRedir($this->fileRecord->id, $this->fileurl); + $this->short_fileurl = common_shorten_url($this->fileurl); + $this->maybeAddRedir($this->fileRecord->id, $this->short_fileurl); + } } public function attachToNotice(Notice $notice) @@ -77,8 +112,7 @@ class MediaFile function delete() { - $filepath = File::path($this->filename); - @unlink($filepath); + @unlink($this->filepath); } public function getFile() @@ -90,18 +124,38 @@ class MediaFile return $this->fileRecord; } - protected function storeFile() + /** + * 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 + * @return string + * @throws ServerException + */ + public static function getHashOfFile(string $filepath, $filehash = null) { - $filepath = File::path($this->filename); - if (!empty($this->filename) && $this->filehash === null) { + assert(!empty($filepath), __METHOD__ . ": filepath cannot be null"); + if ($filehash === null) { // Calculate if we have an older upload method somewhere (Qvitter) that // doesn't do this before calling new MediaFile on its local files... - $this->filehash = hash_file(File::FILEHASH_ALG, $filepath); - if ($this->filehash === false) { + $filehash = hash_file(File::FILEHASH_ALG, $filepath); + if ($filehash === false) { throw new ServerException('Could not read file for hashing'); } } + return $filehash; + } + /** + * Retrieve or insert as a file in the DB + * + * @return object File + * @throws ClientException + * @throws ServerException + */ + protected function storeFile() + { try { $file = File::getByHash($this->filehash); // We're done here. Yes. Already. We assume sha256 won't collide on us anytime soon. @@ -118,14 +172,13 @@ class MediaFile $file->urlhash = File::hashurl($fileurl); $file->url = $fileurl; $file->filehash = $this->filehash; - $file->size = filesize($filepath); + $file->size = filesize($this->filepath); if ($file->size === false) { throw new ServerException('Could not read file to get its size'); } $file->date = time(); $file->mimetype = $this->mimetype; - $file_id = $file->insert(); if ($file_id===false) { @@ -163,10 +216,19 @@ class MediaFile $this->maybeAddRedir($file->id, $short); } - function maybeAddRedir($file_id, $url) + /** + * Adds Redir if needed. + * + * @param $file_id + * @param $url + * @return bool false if no need to add, true if added + * @throws ClientException If failed adding + */ + public function maybeAddRedir($file_id, $url) { try { - $file_redir = File_redirection::getByUrl($url); + File_redirection::getByUrl($url); + return false; } catch (NoResultException $e) { $file_redir = new File_redirection; $file_redir->urlhash = File::hashurl($url); @@ -180,10 +242,76 @@ class MediaFile // TRANS: Client exception thrown when a database error was thrown during a file upload operation. throw new ClientException(_('There was a database error while saving your file. Please try again.')); } + return $result; } } - static function fromUpload($param='media', Profile $scoped=null) + /** + * The maximum allowed file size, as a string + */ + static function maxFileSize() + { + $value = self::maxFileSizeInt(); + if ($value > 1024 * 1024) { + $value = $value/(1024*1024); + // TRANS: Number of megabytes. %d is the number. + return sprintf(_m('%dMB','%dMB',$value),$value); + } else if ($value > 1024) { + $value = $value/1024; + // TRANS: Number of kilobytes. %d is the number. + return sprintf(_m('%dkB','%dkB',$value),$value); + } else { + // TRANS: Number of bytes. %d is the number. + return sprintf(_m('%dB','%dB',$value),$value); + } + } + + /** + * The maximum allowed file size, as an int + */ + static function maxFileSizeInt() + { + return min(self::sizeStrToInt(ini_get('post_max_size')), + self::sizeStrToInt(ini_get('upload_max_filesize')), + self::sizeStrToInt(ini_get('memory_limit'))); + } + + /** + * Convert a string representing a file size (with units), to an int + * @param $str + * @return bool|int|string + */ + public static function sizeStrToInt($str) + { + $unit = substr($str, -1); + $num = substr($str, 0, -1); + switch(strtoupper($unit)){ + case 'G': + $num *= 1024; + case 'M': + $num *= 1024; + case 'K': + $num *= 1024; + } + return $num; + } + + /** + * Create a new MediaFile or ImageFile object from an upload + * + * Tries to set the mimetype correctly, using the most secure method available and rejects the file otherwise. + * In case the upload is an image, this function returns an new ImageFile (which extends MediaFile) + * @param string $param + * @param Profile|null $scoped + * @return ImageFile|MediaFile + * @throws ClientException + * @throws NoResultException + * @throws NoUploadedMediaException + * @throws ServerException + * @throws UnsupportedMediaException + * @throws UseFileAsThumbnailException + */ + 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'])) { @@ -194,19 +322,15 @@ class MediaFile case UPLOAD_ERR_OK: // success, jump out break; case UPLOAD_ERR_INI_SIZE: - // TRANS: Client exception thrown when an uploaded file is larger than set in php.ini. - throw new ClientException(_('The uploaded file exceeds the ' . - 'upload_max_filesize directive in php.ini.')); case UPLOAD_ERR_FORM_SIZE: - throw new ClientException( - // TRANS: Client exception. - _('The uploaded file exceeds the MAX_FILE_SIZE directive' . - ' that was specified in the HTML form.')); + // TRANS: Exception thrown when too large a file is uploaded. + // TRANS: %s is the maximum file size, for example "500b", "10kB" or "2MB". + throw new ClientException(sprintf(_('That file is too big. The maximum file size is %s.'), + self::maxFileSize())); case UPLOAD_ERR_PARTIAL: @unlink($_FILES[$param]['tmp_name']); // TRANS: Client exception. - throw new ClientException(_('The uploaded file was only' . - ' partially uploaded.')); + throw new ClientException(_('The uploaded file was only partially uploaded.')); case UPLOAD_ERR_NO_FILE: // No file; probably just a non-AJAX submission. throw new NoUploadedMediaException($param); @@ -220,39 +344,23 @@ class MediaFile // TRANS: Client exception thrown when a file upload operation has been stopped by an extension. throw new ClientException(_('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(_('System error uploading file.')); } - // TODO: Make documentation clearer that this won't work for files >2GiB because - // PHP is stupid in its 32bit head. But noone accepts 2GiB files with PHP - // anyway... I hope. - $filehash = hash_file(File::FILEHASH_ALG, $_FILES[$param]['tmp_name']); + $filehash = strtolower(self::getHashOfFile($_FILES[$param]['tmp_name'])); try { $file = File::getByHash($filehash); // If no exception is thrown the file exists locally, so we'll use that and just add redirections. // but if the _actual_ locally stored file doesn't exist, getPath will throw FileNotFoundException - $filename = basename($file->getPath()); + $filepath = $file->getPath(); $mimetype = $file->mimetype; - - } catch (FileNotFoundException $e) { - // The file does not exist in our local filesystem, so store this upload. - - if (!move_uploaded_file($_FILES[$param]['tmp_name'], $e->path)) { - // TRANS: Client exception thrown when a file upload operation fails because the file could - // TRANS: not be moved from the temporary folder to the permanent file location. - throw new ClientException(_('File could not be moved to destination directory.')); - } - - $filename = basename($file->getPath()); - $mimetype = $file->mimetype; - - } catch (NoResultException $e) { + // XXX PHP: Upgrade to PHP 7.1 + // catch (FileNotFoundException | NoResultException $e) + } catch (Exception $e) { // We have to save the upload as a new local file. This is the normal course of action. - if ($scoped instanceof Profile) { // Throws exception if additional size does not respect quota // This test is only needed, of course, if we're uploading something new. @@ -260,21 +368,34 @@ class MediaFile } $mimetype = self::getUploadedMimeType($_FILES[$param]['tmp_name'], $_FILES[$param]['name']); + $media = common_get_mime_media($mimetype); + $basename = basename($_FILES[$param]['name']); - - $filename = strtolower($filehash) . '.' . File::guessMimeExtension($mimetype, $basename); + $filename = $filehash . '.' . File::guessMimeExtension($mimetype, $basename); $filepath = File::path($filename); - $result = move_uploaded_file($_FILES[$param]['tmp_name'], $filepath); if (!$result) { // TRANS: Client exception thrown when a file upload operation fails because the file could // TRANS: not be moved from the temporary folder to the permanent file location. + // UX: too specific throw new ClientException(_('File could not be moved to destination directory.')); } - } - return new MediaFile($filename, $mimetype, $filehash); + if ($media === 'image') { + // Use -1 for the id to avoid adding this temporary file to the DB + $img = new ImageFile(-1, $filepath); + // Validate the image by reencoding it. Additionally normalizes old formats to PNG, + // keeping JPEG and GIF untouched + $outpath = $img->resizeTo($img->filepath); + $ext = image_type_to_extension($img->preferredType()); + $filename = $filehash . $ext; + $filepath = File::path($filename); + $result = rename($outpath, $filepath); + return new ImageFile(null, $filepath); + } + } + return new MediaFile($filepath, $mimetype, $filehash); } static function fromFilehandle($fh, Profile $scoped=null) { @@ -336,19 +457,109 @@ class MediaFile /** * Attempt to identify the content type of a given file. - * + * * @param string $filepath filesystem path as string (file must exist) - * @param string $originalFilename (optional) for extension-based detection + * @param bool $originalFilename (optional) for extension-based detection * @return string - * - * @fixme this seems to tie a front-end error message in, kinda confusing - * + * * @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 + * */ - static function getUploadedMimeType($filepath, $originalFilename=false) { + static function getUploadedMimeType(string $filepath, $originalFilename=false) { // We only accept filenames to existing files - $mimelookup = new finfo(FILEINFO_MIME_TYPE); - $mimetype = $mimelookup->file($filepath); + + $mimetype = null; + + // From CodeIgniter + // We'll need this to validate the MIME info string (e.g. text/plain; charset=us-ascii) + $regexp = '/^([a-z\-]+\/[a-z0-9\-\.\+]+)(;\s.+)?$/'; + /** + * Fileinfo extension - most reliable method + * + * Apparently XAMPP, CentOS, cPanel and who knows what + * other PHP distribution channels EXPLICITLY DISABLE + * ext/fileinfo, which is otherwise enabled by default + * since PHP 5.3 ... + */ + if (function_exists('finfo_file')) + { + $finfo = @finfo_open(FILEINFO_MIME); + // It is possible that a FALSE value is returned, if there is no magic MIME database + // file found on the system + if (is_resource($finfo)) + { + $mime = @finfo_file($finfo, $filepath); + finfo_close($finfo); + /* According to the comments section of the PHP manual page, + * it is possible that this function returns an empty string + * for some files (e.g. if they don't exist in the magic MIME database) + */ + if (is_string($mime) && preg_match($regexp, $mime, $matches)) + { + $mimetype = $matches[1]; + } + } + } + /* This is an ugly hack, but UNIX-type systems provide a "native" way to detect the file type, + * which is still more secure than depending on the value of $_FILES[$field]['type'], and as it + * was reported in issue #750 (https://github.com/EllisLab/CodeIgniter/issues/750) - it's better + * than mime_content_type() as well, hence the attempts to try calling the command line with + * three different functions. + * + * Notes: + * - the DIRECTORY_SEPARATOR comparison ensures that we're not on a Windows system + * - many system admins would disable the exec(), shell_exec(), popen() and similar functions + * due to security concerns, hence the function_usable() checks + */ + if (DIRECTORY_SEPARATOR !== '\\') { + $cmd = 'file --brief --mime '.escapeshellarg($filepath).' 2>&1'; + if (function_exists('exec')) { + /* This might look confusing, as $mime is being populated with all of the output + * when set in the second parameter. However, we only need the last line, which is + * the actual return value of exec(), and as such - it overwrites anything that could + * already be set for $mime previously. This effectively makes the second parameter a + * dummy value, which is only put to allow us to get the return status code. + */ + $mime = @exec($cmd, $mime, $return_status); + if ($return_status === 0 && is_string($mime) && preg_match($regexp, $mime, $matches)) { + $mimetype = $matches[1]; + } + } + if (function_exists('shell_exec')) { + $mime = @shell_exec($cmd); + if (strlen($mime) > 0) { + $mime = explode("\n", trim($mime)); + if (preg_match($regexp, $mime[(count($mime) - 1)], $matches)) { + $mimetype = $matches[1]; + } + } + } + if (function_exists('popen')) { + $proc = @popen($cmd, 'r'); + if (is_resource($proc)) { + $mime = @fread($proc, 512); + @pclose($proc); + if ($mime !== false) { + $mime = explode("\n", trim($mime)); + if (preg_match($regexp, $mime[(count($mime) - 1)], $matches)) { + $mimetype = $matches[1]; + } + } + } + } + } + // Fall back to mime_content_type(), if available (still better than $_FILES[$field]['type']) + if (function_exists('mime_content_type')) + { + $mimetype = @mime_content_type($filepath); + // It's possible that mime_content_type() returns FALSE or an empty string + if ($mimetype == false && strlen($mimetype) > 0) + { + throw new ServerException(_m('Could not determine file\'s MIME type.')); + } + } // Unclear types are such that we can't really tell by the auto // detect what they are (.bin, .exe etc. are just "octet-stream")