* `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).
-# 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
<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2008, 2009, StatusNet, Inc.
+/**
+ * GNU social - a federating social network
+ *
+ * Abstraction for files
*
- * This program is free software: you can redistribute it and/or modify
+ * 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
* @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)
{
}
/**
- * @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)
{
/**
* Validation for as-saved base filenames
+ * @param $filename
+ * @return false|int
*/
static function validFilename($filename)
{
}
/**
- * @throws ClientException on invalid filename
+ * @param $filename
+ * @return string
+ * @throws InvalidFilenameException
*/
static function path($filename)
{
}
/**
- * @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)
{
}
/**
- * @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)
{
<?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
* @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 =
'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',
'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/')
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);
<?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
* 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
-
- protected $fileRecord = null;
-
- function __construct($id, $filepath)
+ public $type;
+ public $height;
+ public $width;
+ public $rotate = 0; // degrees to rotate for properly oriented image (extrapolated from EXIF etc.)
+ public $animated = null; // Animated image? (has more than 1 frame). null means untested
+ public $mimetype = null; // The _ImageFile_ mimetype, _not_ the originating File object
+
+ public 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;
$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];
$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;
$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
$imgPath = $file->getPath();
break;
default:
- throw new UnsupportedMediaException(_('Unsupported media format.'), $file->getPath());
+ throw new UnsupportedMediaException(_m('Unsupported media format.'), $file->getPath());
}
}
// 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) {
// 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;
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;
-
- 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.'));
- }
-
- $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 parent::fromUpload($param, $scoped);
+ }
- return new ImageFile(null, $_FILES[$param]['tmp_name']);
+ /**
+ * 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;
+ }
+ // 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;
}
/**
*
* @param string $outpath
* @return ImageFile the image stored at target path
+ * @throws ClientException
+ * @throws NoResultException
+ * @throws ServerException
+ * @throws UnsupportedMediaException
+ * @throws UseFileAsThumbnailException
*/
function copyTo($outpath)
{
* 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'];
}
}
-
if (Event::handle('StartResizeImageFile', array($this, $outpath, $box))) {
$this->resizeToFile($outpath, $box);
}
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);
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) {
$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']);
+
+ $type = $this->preferredType();
+ $ext = image_type_to_extension($type, true);
+ $outpath = preg_replace("/\.[^\.]+$/", $ext, $outpath);
- switch ($this->preferredType()) {
+ switch ($type) {
case IMAGETYPE_GIF:
imagegif($image_dest, $outpath);
break;
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!
* @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) {
$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;
}
}
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));
}
/**
}
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) {
'file_id'=> $this->fileRecord->getID(),
'width' => $width,
'height' => $height,
- ));
+ ));
if ($thumb instanceof File_thumbnail) {
return $thumb;
}
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);
}
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'))
<?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
* 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;
+
+ // 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->fileurl = common_local_url('attachment',
- array('attachment' => $this->fileRecord->id));
+ $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);
+ $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)
function delete()
{
- $filepath = File::path($this->filename);
- @unlink($filepath);
+ @unlink($this->filepath);
}
public function getFile()
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.
$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) {
$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);
// 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;
+ }
+ }
+
+ /**
+ * 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);
}
}
- static function fromUpload($param='media', Profile $scoped=null)
+ /**
+ * 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'])) {
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);
// 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());
- $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());
+ $filepath = $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.
}
$mimetype = self::getUploadedMimeType($_FILES[$param]['tmp_name'], $_FILES[$param]['name']);
- $basename = basename($_FILES[$param]['name']);
+ $media = common_get_mime_media($mimetype);
- $filename = strtolower($filehash) . '.' . File::guessMimeExtension($mimetype, $basename);
+ $basename = basename($_FILES[$param]['name']);
+ $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) {
/**
* 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")