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..61e611a124 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
@@ -71,20 +80,20 @@ class File extends Managed_DataObject
);
}
- public static function isProtected($url) {
+ public static function isProtected($url)
+ {
+ $protected_urls_exps = array(
+ 'https://www.facebook.com/login.php',
+ common_path('main/login')
+ );
- $protected_urls_exps = array(
- 'https://www.facebook.com/login.php',
- common_path('main/login')
- );
+ foreach ($protected_urls_exps as $protected_url_exp) {
+ if (preg_match('!^'.preg_quote($protected_url_exp).'(.*)$!i', $url) === 1) {
+ return true;
+ }
+ }
- foreach ($protected_urls_exps as $protected_url_exp) {
- if (preg_match('!^'.preg_quote($protected_url_exp).'(.*)$!i', $url) === 1) {
- return true;
- }
- }
-
- return false;
+ return false;
}
/**
@@ -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)
{
@@ -142,16 +152,27 @@ class File extends Managed_DataObject
$file = new File;
$file->url = $given_url;
- if (!empty($redir_data['protected'])) $file->protected = $redir_data['protected'];
- if (!empty($redir_data['title'])) $file->title = $redir_data['title'];
- if (!empty($redir_data['type'])) $file->mimetype = $redir_data['type'];
- if (!empty($redir_data['size'])) $file->size = intval($redir_data['size']);
- if (isset($redir_data['time']) && $redir_data['time'] > 0) $file->date = intval($redir_data['time']);
+ if (!empty($redir_data['protected'])) {
+ $file->protected = $redir_data['protected'];
+ }
+ if (!empty($redir_data['title'])) {
+ $file->title = $redir_data['title'];
+ }
+ if (!empty($redir_data['type'])) {
+ $file->mimetype = $redir_data['type'];
+ }
+ if (!empty($redir_data['size'])) {
+ $file->size = intval($redir_data['size']);
+ }
+ if (isset($redir_data['time']) && $redir_data['time'] > 0) {
+ $file->date = intval($redir_data['time']);
+ }
$file->saveFile();
return $file;
}
- public function saveFile() {
+ public function saveFile()
+ {
$this->urlhash = self::hashurl($this->url);
if (!Event::handle('StartFileSaveNew', array(&$this))) {
@@ -183,7 +204,8 @@ class File extends Managed_DataObject
*
* @throws ServerException on failure
*/
- public static function processNew($given_url, Notice $notice=null, $followRedirects=true) {
+ public static function processNew($given_url, Notice $notice=null, $followRedirects=true)
+ {
if (empty($given_url)) {
throw new ServerException('No given URL to process');
}
@@ -212,12 +234,13 @@ class File extends Managed_DataObject
return $file;
}
- public static function respectsQuota(Profile $scoped, $fileSize) {
+ public static function respectsQuota(Profile $scoped, $fileSize)
+ {
if ($fileSize > common_config('attachments', 'file_quota')) {
// TRANS: Message used to be inserted as %2$s in the text "No file may
// TRANS: be larger than %1$d byte and the file you sent was %2$s.".
// TRANS: %1$d is the number of bytes of an uploaded file.
- $fileSizeText = sprintf(_m('%1$d byte','%1$d bytes',$fileSize),$fileSize);
+ $fileSizeText = sprintf(_m('%1$d byte', '%1$d bytes', $fileSize), $fileSize);
$fileQuota = common_config('attachments', 'file_quota');
// TRANS: Message given if an upload is larger than the configured maximum.
@@ -225,10 +248,16 @@ class File extends Managed_DataObject
// TRANS: %2$s is the proper form of "n bytes". This is the only ways to have
// TRANS: gettext support multiple plurals in the same message, unfortunately...
throw new ClientException(
- sprintf(_m('No file may be larger than %1$d byte and the file you sent was %2$s. Try to upload a smaller version.',
- 'No file may be larger than %1$d bytes and the file you sent was %2$s. Try to upload a smaller version.',
- $fileQuota),
- $fileQuota, $fileSizeText));
+ sprintf(
+ _m(
+ 'No file may be larger than %1$d byte and the file you sent was %2$s. Try to upload a smaller version.',
+ 'No file may be larger than %1$d bytes and the file you sent was %2$s. Try to upload a smaller version.',
+ $fileQuota
+ ),
+ $fileQuota,
+ $fileSizeText
+ )
+ );
}
$file = new File;
@@ -241,10 +270,15 @@ class File extends Managed_DataObject
// TRANS: Message given if an upload would exceed user quota.
// TRANS: %d (number) is the user quota in bytes and is used for plural.
throw new ClientException(
- sprintf(_m('A file this large would exceed your user quota of %d byte.',
- 'A file this large would exceed your user quota of %d bytes.',
- common_config('attachments', 'user_quota')),
- common_config('attachments', 'user_quota')));
+ sprintf(
+ _m(
+ 'A file this large would exceed your user quota of %d byte.',
+ 'A file this large would exceed your user quota of %d bytes.',
+ common_config('attachments', 'user_quota')
+ ),
+ common_config('attachments', 'user_quota')
+ )
+ );
}
$query .= ' AND EXTRACT(month FROM file.modified) = EXTRACT(month FROM now()) and EXTRACT(year FROM file.modified) = EXTRACT(year FROM now())';
$file->query($query);
@@ -254,10 +288,15 @@ class File extends Managed_DataObject
// TRANS: Message given id an upload would exceed a user's monthly quota.
// TRANS: $d (number) is the monthly user quota in bytes and is used for plural.
throw new ClientException(
- sprintf(_m('A file this large would exceed your monthly quota of %d byte.',
- 'A file this large would exceed your monthly quota of %d bytes.',
- common_config('attachments', 'monthly_quota')),
- common_config('attachments', 'monthly_quota')));
+ sprintf(
+ _m(
+ 'A file this large would exceed your monthly quota of %d byte.',
+ 'A file this large would exceed your monthly quota of %d bytes.',
+ common_config('attachments', 'monthly_quota')
+ ),
+ common_config('attachments', 'monthly_quota')
+ )
+ );
}
return true;
}
@@ -274,7 +313,7 @@ class File extends Managed_DataObject
// where should the file go?
- static function filename(Profile $profile, $origname, $mimetype)
+ public static function filename(Profile $profile, $origname, $mimetype)
{
$ext = self::guessMimeExtension($mimetype, $origname);
@@ -298,10 +337,12 @@ 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)
+ public static function guessMimeExtension($mimetype, $filename=null)
{
try {
// first see if we know the extension for our mimetype
@@ -349,16 +390,17 @@ class File extends Managed_DataObject
/**
* Validation for as-saved base filenames
+ * @param $filename
+ * @return false|int
*/
- static function validFilename($filename)
+ public static function validFilename($filename)
{
return preg_match('/^[A-Za-z0-9._-]+$/', $filename);
}
- static function tryFilename($filename)
+ public static function tryFilename($filename)
{
- if (!self::validFilename($filename))
- {
+ if (!self::validFilename($filename)) {
throw new InvalidFilenameException($filename);
}
// if successful, return the filename for easy if-statementing
@@ -366,9 +408,11 @@ class File extends Managed_DataObject
}
/**
- * @throws ClientException on invalid filename
+ * @param $filename
+ * @return string
+ * @throws InvalidFilenameException
*/
- static function path($filename)
+ public static function path($filename)
{
self::tryFilename($filename);
@@ -381,19 +425,18 @@ class File extends Managed_DataObject
return $dir . $filename;
}
- static function url($filename)
+ public static function url($filename)
{
self::tryFilename($filename);
- if (common_config('site','private')) {
-
- return common_local_url('getfile',
- array('filename' => $filename));
-
+ if (common_config('site', 'private')) {
+ return common_local_url(
+ 'getfile',
+ array('filename' => $filename)
+ );
}
if (GNUsocial::useHTTPS()) {
-
$sslserver = common_config('attachments', 'sslserver');
if (empty($sslserver)) {
@@ -402,7 +445,7 @@ class File extends Managed_DataObject
if (is_string(common_config('site', 'sslserver')) &&
mb_strlen(common_config('site', 'sslserver')) > 0) {
$server = common_config('site', 'sslserver');
- } else if (common_config('site', 'server')) {
+ } elseif (common_config('site', 'server')) {
$server = common_config('site', 'server');
}
$path = common_config('site', 'path') . '/file/';
@@ -439,9 +482,10 @@ class File extends Managed_DataObject
return $protocol.'://'.$server.$path.$filename;
}
- static $_enclosures = array();
+ public static $_enclosures = array();
- function getEnclosure(){
+ public function getEnclosure()
+ {
if (isset(self::$_enclosures[$this->getID()])) {
return self::$_enclosures[$this->getID()];
}
@@ -515,8 +559,12 @@ class File extends Managed_DataObject
}
}
- return $image->getFileThumbnail($width, $height, $crop,
- !is_null($upscale) ? $upscale : common_config('thumbnail', 'upscale'));
+ return $image->getFileThumbnail(
+ $width,
+ $height,
+ $crop,
+ !is_null($upscale) ? $upscale : common_config('thumbnail', 'upscale')
+ );
}
public function getPath()
@@ -534,7 +582,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)
{
@@ -554,7 +604,7 @@ class File extends Managed_DataObject
return $this->url;
}
- static public function getByUrl($url)
+ public static function getByUrl($url)
{
$file = new File();
$file->urlhash = self::hashurl($url);
@@ -565,9 +615,11 @@ 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)
+ public static function getByHash($hashstr)
{
$file = new File();
$file->filehash = strtolower($hashstr);
@@ -584,10 +636,13 @@ class File extends Managed_DataObject
throw new ServerException('URL already exists in DB');
}
$sql = 'UPDATE %1$s SET urlhash=%2$s, url=%3$s WHERE urlhash=%4$s;';
- $result = $this->query(sprintf($sql, $this->tableName(),
- $this->_quote((string)self::hashurl($url)),
- $this->_quote((string)$url),
- $this->_quote((string)$this->urlhash)));
+ $result = $this->query(sprintf(
+ $sql,
+ $this->tableName(),
+ $this->_quote((string)self::hashurl($url)),
+ $this->_quote((string)$url),
+ $this->_quote((string)$this->urlhash)
+ ));
if ($result === false) {
common_log_db_error($this, 'UPDATE', __FILE__);
throw new ServerException("Could not UPDATE {$this->tableName()}.url");
@@ -604,7 +659,7 @@ class File extends Managed_DataObject
* @return void
*/
- function blowCache($last=false)
+ public function blowCache($last=false)
{
self::blow('file:notice-ids:%s', $this->id);
if ($last) {
@@ -624,7 +679,7 @@ class File extends Managed_DataObject
* @return array ids of notices that link to this file
*/
- function stream($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
+ public function stream($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
{
// FIXME: Try to get the Profile::current() here in some other way to avoid mixing
// the current session user with possibly background/queue processing.
@@ -632,14 +687,13 @@ class File extends Managed_DataObject
return $stream->getNotices($offset, $limit, $since_id, $max_id);
}
- function noticeCount()
+ public function noticeCount()
{
$cacheKey = sprintf('file:notice-count:%d', $this->id);
$count = self::cacheGet($cacheKey);
if ($count === false) {
-
$f2p = new File_to_post();
$f2p->file_id = $this->id;
@@ -647,7 +701,7 @@ class File extends Managed_DataObject
$count = $f2p->count();
self::cacheSet($cacheKey, $count);
- }
+ }
return $count;
}
@@ -704,7 +758,7 @@ class File extends Managed_DataObject
return $this->update($orig);
}
- static public function hashurl($url)
+ public static function hashurl($url)
{
if (empty($url)) {
throw new Exception('No URL provided to hash algorithm.');
@@ -712,7 +766,7 @@ class File extends Managed_DataObject
return hash(self::URLHASH_ALG, $url);
}
- static public function beforeSchemaUpdate()
+ public static function beforeSchemaUpdate()
{
$table = strtolower(get_called_class());
$schema = Schema::get();
@@ -745,7 +799,7 @@ class File extends Managed_DataObject
$dupfile->update($orig);
print "\nDeleting duplicate entries of too long URL on $table id: {$file->id} [";
// only start deleting with this fetch.
- while($dupfile->fetch()) {
+ while ($dupfile->fetch()) {
common_log(LOG_INFO, sprintf('Deleting duplicate File entry of %1$d: %2$d (original URL: %3$s collides with these first 191 characters: %4$s', $dupfile->id, $file->id, $origurl, $file->shortenedurl));
print ".";
$dupfile->delete();
@@ -761,13 +815,13 @@ class File extends Managed_DataObject
echo "\n...now running hacky pre-schemaupdate change for $table:";
// We have to create a urlhash that is _not_ the primary key,
// transfer data and THEN run checkSchema
- $schemadef['fields']['urlhash'] = array (
+ $schemadef['fields']['urlhash'] = array(
'type' => 'varchar',
'length' => 64,
'not null' => false, // this is because when adding column, all entries will _be_ NULL!
'description' => 'sha256 of destination URL (url field)',
);
- $schemadef['fields']['url'] = array (
+ $schemadef['fields']['url'] = array(
'type' => 'text',
'description' => 'destination URL after following possible redirections',
);
@@ -780,11 +834,13 @@ class File extends Managed_DataObject
// urlhash is hash('sha256', $url) in the File table
echo "Updating urlhash fields in $table table...";
// Maybe very MySQL specific :(
- $tablefix->query(sprintf('UPDATE %1$s SET %2$s=%3$s;',
- $schema->quoteIdentifier($table),
- 'urlhash',
+ $tablefix->query(sprintf(
+ 'UPDATE %1$s SET %2$s=%3$s;',
+ $schema->quoteIdentifier($table),
+ 'urlhash',
// The line below is "result of sha256 on column `url`"
- 'SHA2(url, 256)'));
+ 'SHA2(url, 256)'
+ ));
echo "DONE.\n";
echo "Resuming core schema upgrade...";
}
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..6794971fbb 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,14 @@ 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 +175,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,8 +222,13 @@ 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)
+ public function copyTo($outpath)
{
return new ImageFile(null, $this->resizeTo($outpath));
}
@@ -234,36 +237,34 @@ 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())
+ public 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 +279,6 @@ class ImageFile
}
}
-
if (Event::handle('StartResizeImageFile', array($this, $outpath, $box))) {
$this->resizeToFile($outpath, $box);
}
@@ -294,8 +294,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 +331,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 +340,45 @@ 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 +390,32 @@ 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);
+ return self::getScalingValues(
+ $this->width,
+ $this->height,
+ $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 +426,21 @@ 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)
- {
+ public static function getScalingValues(
+ $width,
+ $height,
+ $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 +455,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 +489,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));
}
/**
@@ -539,7 +515,7 @@ class ImageFile
// We read through the file til we reach the end of the file, or we've found
// at least 2 frame headers
- while(!feof($fh) && $count < 2) {
+ while (!feof($fh) && $count < 2) {
$chunk = fread($fh, 1024 * 100); //read 100kb at a time
$count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00\x2C#s', $chunk, $matches);
// rewind in case we ended up in the middle of the header, but avoid
@@ -560,9 +536,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 +565,7 @@ class ImageFile
'file_id'=> $this->fileRecord->getID(),
'width' => $width,
'height' => $height,
- ));
+ ));
if ($thumb instanceof File_thumbnail) {
return $thumb;
}
@@ -610,11 +586,16 @@ class ImageFile
|| $box['w'] < 1 || $box['x'] >= $this->width
|| $box['h'] < 1 || $box['y'] >= $this->height) {
// Fail on bad width parameter. If this occurs, it's due to algorithm in ImageFile->scaleToFit
- common_debug("Boundary box parameters for resize of {$this->filepath} : ".var_export($box,true));
+ common_debug("Boundary box parameters for resize of {$this->filepath} : ".var_export($box, true));
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);
@@ -628,106 +609,14 @@ class ImageFile
// $this->getPath() says the file doesn't exist anyway, so no point in trying to delete it!
}
- return File_thumbnail::saveThumbnail($this->fileRecord->getID(),
- null, // no url since we generated it ourselves and can dynamically generate the url
- $width, $height,
- $outname);
+ return File_thumbnail::saveThumbnail(
+ $this->fileRecord->getID(),
+ // 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..dd8f82720c 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); }
+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)
@@ -65,20 +102,19 @@ class MediaFile
return File::path($this->filename);
}
- function shortUrl()
+ public function shortUrl()
{
return $this->short_fileurl;
}
- function getEnclosure()
+ public function getEnclosure()
{
return $this->getFile()->getEnclosure();
}
- function delete()
+ public function delete()
{
- $filepath = File::path($this->filename);
- @unlink($filepath);
+ @unlink($this->filepath);
}
public function getFile()
@@ -90,18 +126,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 +174,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) {
@@ -158,15 +213,24 @@ class MediaFile
return $file;
}
- function rememberFile($file, $short)
+ public function rememberFile($file, $short)
{
$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 +244,80 @@ 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
+ */
+ public 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);
+ } elseif ($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
+ */
+ public 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;
+ // no break
+ case 'M':
+ $num *= 1024;
+ // no break
+ 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 +328,17 @@ 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 +352,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,24 +376,38 @@ 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) {
+ public static function fromFilehandle($fh, Profile $scoped=null)
+ {
$stream = stream_get_meta_data($fh);
// So far we're only handling filehandles originating from tmpfile(),
// so we can always do hash_file on $stream['uri'] as far as I can tell!
@@ -310,7 +440,6 @@ class MediaFile
$filename = basename($file->getPath());
$mimetype = $file->mimetype;
-
} catch (NoResultException $e) {
if ($scoped instanceof Profile) {
File::respectsQuota($scoped, filesize($stream['uri']));
@@ -327,7 +456,7 @@ class MediaFile
common_log(LOG_ERR, 'File could not be moved (or chmodded) from '._ve($stream['uri']) . ' to ' . _ve($filepath));
// 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.' ));
+ throw new ClientException(_('File could not be moved to destination directory.'));
}
}
@@ -336,19 +465,105 @@ 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) {
+ public 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")