[MEDIA] ImageFile now extends MediaFile and validates images more aggressively.

Default supported files need to use consistent names. Bumped version to 1.20.0

ImageFile has been changed to extend MediaFile and rely on it to partially
validate files. This validation has been extended to not rely solely on
Fileinfo, as it is disabled on some places. Now it'll try to use the shell
command `file`, if Fileinfo isn't available.

ImageFile now converts every new upload to PNG, except JPEG and GIF, which
are kept, but still resized (to the same size), to remove possible scripts
embedded therein.

MediaFile::fromUpload will return an ImageFile if the uploaded file is an image
or a MediaFile otherwise.

MediaFile can be constructed with an id with value -1 to denote a temporary
object, which is not added to the DB. This is useful to create a temporary
object for representing images, so it can be used to rescale them.

The supported attachment array needs to be populated with the result of calling
`image_type_to_extension` for the appropriate image type, in the case of images.
This is important so all parts of the code see the same extension for each image
type (jpg vs jpeg).

Added documentation to classes/File.php and to lib/MediaFile and lib/ImageFile
This commit is contained in:
Miguel Dantas 2019-06-07 14:08:27 +01:00
parent 719bf065ca
commit b224d93098
7 changed files with 494 additions and 400 deletions

View File

@ -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).

View File

@ -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

View File

@ -1,23 +1,32 @@
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2008, 2009, StatusNet, Inc.
/**
* GNU social - a federating social network
*
* This program is free software: you can redistribute it and/or modify
* Abstraction for 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
* 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/>.
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Files
* @package GNUsocial
* @author Mikael Nordfeldth <mmn@hethane.se>
* @author Miguel Dantas <biodantas@gmail.com>
* @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)
{

View File

@ -1,10 +1,7 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
* GNU social - a federating social network
*
* Default settings for core configuration
*
* 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
@ -22,9 +19,9 @@
* @category Config
* @package GNUsocial
* @author Evan Prodromou <evan@status.net>
* @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/')

View File

@ -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);

View File

@ -1,11 +1,9 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
* GNU social - a federating social network
*
* Abstraction for an image file
*
* 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
@ -20,55 +18,41 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Image
* @package StatusNet
* @package GNUsocial
* @author Evan Prodromou <evan@status.net>
* @author Zach Copley <zach@status.net>
* @copyright 2008-2009 StatusNet, Inc.
* @author Mikael Nordfeldth <mmn@hethane.se>
* @author Miguel Dantas <biodantasgs@gmail.com>
* @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 <evan@status.net>
* @author Zach Copley <zach@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/
* @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'))

View File

@ -1,12 +1,8 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
* GNU social - a federating social network
*
* Abstraction for media files in general
*
* TODO: combine with ImageFile?
*
* PHP version 5
* 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
@ -22,37 +18,76 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Media
* @package StatusNet
* @package GNUsocial
* @author Robin Millette <robin@millette.info>
* @author Miguel Dantas <biodantas@gmail.com>
* @author Zach Copley <zach@status.net>
* @copyright 2008-2009 StatusNet, Inc.
* @author Mikael Nordfeldth <mmn@hethane.se>
* @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")