Merge branch 'nightly' of biodantas/gnu-social into nightly

This commit is contained in:
Diogo Cordeiro 2019-06-09 23:44:43 +00:00 committed by Gogs
commit abfd691fda
7 changed files with 657 additions and 490 deletions

View File

@ -649,7 +649,16 @@ detection.
* `supported`: an array of mime types you accept to store and distribute, * `supported`: an array of mime types you accept to store and distribute,
like 'image/gif', 'video/mpeg', 'audio/mpeg', etc. Make sure you like 'image/gif', 'video/mpeg', 'audio/mpeg', etc. Make sure you
setup your server to properly recognize the types you want to 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). * `uploads`: false to disable uploading files with notices (true by default).

View File

@ -1,4 +1,4 @@
# GNU social 1.19.x # GNU social 1.20.x
(c) 2010-2019 Free Software Foundation, Inc (c) 2010-2019 Free Software Foundation, Inc
This is the README file for GNU social, the free This is the README file for GNU social, the free

View File

@ -1,23 +1,32 @@
<?php <?php
/* /**
* StatusNet - the distributed open-source microblogging tool * GNU social - a federating social network
* Copyright (C) 2008, 2009, StatusNet, Inc.
* *
* This program is free software: you can redistribute it and/or modify * Abstraction for files
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by * 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 * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * 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. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * 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 * 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( foreach ($protected_urls_exps as $protected_url_exp) {
'https://www.facebook.com/login.php', if (preg_match('!^'.preg_quote($protected_url_exp).'(.*)$!i', $url) === 1) {
common_path('main/login') return true;
); }
}
foreach ($protected_urls_exps as $protected_url_exp) { return false;
if (preg_match('!^'.preg_quote($protected_url_exp).'(.*)$!i', $url) === 1) {
return true;
}
}
return false;
} }
/** /**
@ -93,6 +102,7 @@ class File extends Managed_DataObject
* @param array $redir_data lookup data eg from File_redirection::where() * @param array $redir_data lookup data eg from File_redirection::where()
* @param string $given_url * @param string $given_url
* @return File * @return File
* @throws ServerException
*/ */
public static function saveNew(array $redir_data, $given_url) public static function saveNew(array $redir_data, $given_url)
{ {
@ -142,16 +152,27 @@ class File extends Managed_DataObject
$file = new File; $file = new File;
$file->url = $given_url; $file->url = $given_url;
if (!empty($redir_data['protected'])) $file->protected = $redir_data['protected']; if (!empty($redir_data['protected'])) {
if (!empty($redir_data['title'])) $file->title = $redir_data['title']; $file->protected = $redir_data['protected'];
if (!empty($redir_data['type'])) $file->mimetype = $redir_data['type']; }
if (!empty($redir_data['size'])) $file->size = intval($redir_data['size']); if (!empty($redir_data['title'])) {
if (isset($redir_data['time']) && $redir_data['time'] > 0) $file->date = intval($redir_data['time']); $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(); $file->saveFile();
return $file; return $file;
} }
public function saveFile() { public function saveFile()
{
$this->urlhash = self::hashurl($this->url); $this->urlhash = self::hashurl($this->url);
if (!Event::handle('StartFileSaveNew', array(&$this))) { if (!Event::handle('StartFileSaveNew', array(&$this))) {
@ -183,7 +204,8 @@ class File extends Managed_DataObject
* *
* @throws ServerException on failure * @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)) { if (empty($given_url)) {
throw new ServerException('No given URL to process'); throw new ServerException('No given URL to process');
} }
@ -212,12 +234,13 @@ class File extends Managed_DataObject
return $file; return $file;
} }
public static function respectsQuota(Profile $scoped, $fileSize) { public static function respectsQuota(Profile $scoped, $fileSize)
{
if ($fileSize > common_config('attachments', 'file_quota')) { if ($fileSize > common_config('attachments', 'file_quota')) {
// TRANS: Message used to be inserted as %2$s in the text "No file may // 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: 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. // 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'); $fileQuota = common_config('attachments', 'file_quota');
// TRANS: Message given if an upload is larger than the configured maximum. // 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: %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... // TRANS: gettext support multiple plurals in the same message, unfortunately...
throw new ClientException( 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.', sprintf(
'No file may be larger than %1$d bytes and the file you sent was %2$s. Try to upload a smaller version.', _m(
$fileQuota), 'No file may be larger than %1$d byte and the file you sent was %2$s. Try to upload a smaller version.',
$fileQuota, $fileSizeText)); '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; $file = new File;
@ -241,10 +270,15 @@ class File extends Managed_DataObject
// TRANS: Message given if an upload would exceed user quota. // TRANS: Message given if an upload would exceed user quota.
// TRANS: %d (number) is the user quota in bytes and is used for plural. // TRANS: %d (number) is the user quota in bytes and is used for plural.
throw new ClientException( throw new ClientException(
sprintf(_m('A file this large would exceed your user quota of %d byte.', sprintf(
'A file this large would exceed your user quota of %d bytes.', _m(
common_config('attachments', 'user_quota')), 'A file this large would exceed your user quota of %d byte.',
common_config('attachments', 'user_quota'))); '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())'; $query .= ' AND EXTRACT(month FROM file.modified) = EXTRACT(month FROM now()) and EXTRACT(year FROM file.modified) = EXTRACT(year FROM now())';
$file->query($query); $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: 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. // TRANS: $d (number) is the monthly user quota in bytes and is used for plural.
throw new ClientException( throw new ClientException(
sprintf(_m('A file this large would exceed your monthly quota of %d byte.', sprintf(
'A file this large would exceed your monthly quota of %d bytes.', _m(
common_config('attachments', 'monthly_quota')), 'A file this large would exceed your monthly quota of %d byte.',
common_config('attachments', 'monthly_quota'))); 'A file this large would exceed your monthly quota of %d bytes.',
common_config('attachments', 'monthly_quota')
),
common_config('attachments', 'monthly_quota')
)
);
} }
return true; return true;
} }
@ -274,7 +313,7 @@ class File extends Managed_DataObject
// where should the file go? // 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); $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 $mimetype string The mimetype we've discovered for this file.
* @param $filename An optional filename which we can use on failure. * @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 { try {
// first see if we know the extension for our mimetype // 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 * 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); 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); throw new InvalidFilenameException($filename);
} }
// if successful, return the filename for easy if-statementing // 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); self::tryFilename($filename);
@ -381,19 +425,18 @@ class File extends Managed_DataObject
return $dir . $filename; return $dir . $filename;
} }
static function url($filename) public static function url($filename)
{ {
self::tryFilename($filename); self::tryFilename($filename);
if (common_config('site','private')) { if (common_config('site', 'private')) {
return common_local_url(
return common_local_url('getfile', 'getfile',
array('filename' => $filename)); array('filename' => $filename)
);
} }
if (GNUsocial::useHTTPS()) { if (GNUsocial::useHTTPS()) {
$sslserver = common_config('attachments', 'sslserver'); $sslserver = common_config('attachments', 'sslserver');
if (empty($sslserver)) { if (empty($sslserver)) {
@ -402,7 +445,7 @@ class File extends Managed_DataObject
if (is_string(common_config('site', 'sslserver')) && if (is_string(common_config('site', 'sslserver')) &&
mb_strlen(common_config('site', 'sslserver')) > 0) { mb_strlen(common_config('site', 'sslserver')) > 0) {
$server = common_config('site', 'sslserver'); $server = common_config('site', 'sslserver');
} else if (common_config('site', 'server')) { } elseif (common_config('site', 'server')) {
$server = common_config('site', 'server'); $server = common_config('site', 'server');
} }
$path = common_config('site', 'path') . '/file/'; $path = common_config('site', 'path') . '/file/';
@ -439,9 +482,10 @@ class File extends Managed_DataObject
return $protocol.'://'.$server.$path.$filename; return $protocol.'://'.$server.$path.$filename;
} }
static $_enclosures = array(); public static $_enclosures = array();
function getEnclosure(){ public function getEnclosure()
{
if (isset(self::$_enclosures[$this->getID()])) { if (isset(self::$_enclosures[$this->getID()])) {
return self::$_enclosures[$this->getID()]; return self::$_enclosures[$this->getID()];
} }
@ -515,8 +559,12 @@ class File extends Managed_DataObject
} }
} }
return $image->getFileThumbnail($width, $height, $crop, return $image->getFileThumbnail(
!is_null($upscale) ? $upscale : common_config('thumbnail', 'upscale')); $width,
$height,
$crop,
!is_null($upscale) ? $upscale : common_config('thumbnail', 'upscale')
);
} }
public function getPath() 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) public function getUrl($use_local=null)
{ {
@ -554,7 +604,7 @@ class File extends Managed_DataObject
return $this->url; return $this->url;
} }
static public function getByUrl($url) public static function getByUrl($url)
{ {
$file = new File(); $file = new File();
$file->urlhash = self::hashurl($url); $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 = new File();
$file->filehash = strtolower($hashstr); $file->filehash = strtolower($hashstr);
@ -584,10 +636,13 @@ class File extends Managed_DataObject
throw new ServerException('URL already exists in DB'); throw new ServerException('URL already exists in DB');
} }
$sql = 'UPDATE %1$s SET urlhash=%2$s, url=%3$s WHERE urlhash=%4$s;'; $sql = 'UPDATE %1$s SET urlhash=%2$s, url=%3$s WHERE urlhash=%4$s;';
$result = $this->query(sprintf($sql, $this->tableName(), $result = $this->query(sprintf(
$this->_quote((string)self::hashurl($url)), $sql,
$this->_quote((string)$url), $this->tableName(),
$this->_quote((string)$this->urlhash))); $this->_quote((string)self::hashurl($url)),
$this->_quote((string)$url),
$this->_quote((string)$this->urlhash)
));
if ($result === false) { if ($result === false) {
common_log_db_error($this, 'UPDATE', __FILE__); common_log_db_error($this, 'UPDATE', __FILE__);
throw new ServerException("Could not UPDATE {$this->tableName()}.url"); throw new ServerException("Could not UPDATE {$this->tableName()}.url");
@ -604,7 +659,7 @@ class File extends Managed_DataObject
* @return void * @return void
*/ */
function blowCache($last=false) public function blowCache($last=false)
{ {
self::blow('file:notice-ids:%s', $this->id); self::blow('file:notice-ids:%s', $this->id);
if ($last) { if ($last) {
@ -624,7 +679,7 @@ class File extends Managed_DataObject
* @return array ids of notices that link to this file * @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 // FIXME: Try to get the Profile::current() here in some other way to avoid mixing
// the current session user with possibly background/queue processing. // 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); return $stream->getNotices($offset, $limit, $since_id, $max_id);
} }
function noticeCount() public function noticeCount()
{ {
$cacheKey = sprintf('file:notice-count:%d', $this->id); $cacheKey = sprintf('file:notice-count:%d', $this->id);
$count = self::cacheGet($cacheKey); $count = self::cacheGet($cacheKey);
if ($count === false) { if ($count === false) {
$f2p = new File_to_post(); $f2p = new File_to_post();
$f2p->file_id = $this->id; $f2p->file_id = $this->id;
@ -647,7 +701,7 @@ class File extends Managed_DataObject
$count = $f2p->count(); $count = $f2p->count();
self::cacheSet($cacheKey, $count); self::cacheSet($cacheKey, $count);
} }
return $count; return $count;
} }
@ -704,7 +758,7 @@ class File extends Managed_DataObject
return $this->update($orig); return $this->update($orig);
} }
static public function hashurl($url) public static function hashurl($url)
{ {
if (empty($url)) { if (empty($url)) {
throw new Exception('No URL provided to hash algorithm.'); throw new Exception('No URL provided to hash algorithm.');
@ -712,7 +766,7 @@ class File extends Managed_DataObject
return hash(self::URLHASH_ALG, $url); return hash(self::URLHASH_ALG, $url);
} }
static public function beforeSchemaUpdate() public static function beforeSchemaUpdate()
{ {
$table = strtolower(get_called_class()); $table = strtolower(get_called_class());
$schema = Schema::get(); $schema = Schema::get();
@ -745,7 +799,7 @@ class File extends Managed_DataObject
$dupfile->update($orig); $dupfile->update($orig);
print "\nDeleting duplicate entries of too long URL on $table id: {$file->id} ["; print "\nDeleting duplicate entries of too long URL on $table id: {$file->id} [";
// only start deleting with this fetch. // 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)); 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 "."; print ".";
$dupfile->delete(); $dupfile->delete();
@ -761,13 +815,13 @@ class File extends Managed_DataObject
echo "\n...now running hacky pre-schemaupdate change for $table:"; echo "\n...now running hacky pre-schemaupdate change for $table:";
// We have to create a urlhash that is _not_ the primary key, // We have to create a urlhash that is _not_ the primary key,
// transfer data and THEN run checkSchema // transfer data and THEN run checkSchema
$schemadef['fields']['urlhash'] = array ( $schemadef['fields']['urlhash'] = array(
'type' => 'varchar', 'type' => 'varchar',
'length' => 64, 'length' => 64,
'not null' => false, // this is because when adding column, all entries will _be_ NULL! 'not null' => false, // this is because when adding column, all entries will _be_ NULL!
'description' => 'sha256 of destination URL (url field)', 'description' => 'sha256 of destination URL (url field)',
); );
$schemadef['fields']['url'] = array ( $schemadef['fields']['url'] = array(
'type' => 'text', 'type' => 'text',
'description' => 'destination URL after following possible redirections', '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 // urlhash is hash('sha256', $url) in the File table
echo "Updating urlhash fields in $table table..."; echo "Updating urlhash fields in $table table...";
// Maybe very MySQL specific :( // Maybe very MySQL specific :(
$tablefix->query(sprintf('UPDATE %1$s SET %2$s=%3$s;', $tablefix->query(sprintf(
$schema->quoteIdentifier($table), 'UPDATE %1$s SET %2$s=%3$s;',
'urlhash', $schema->quoteIdentifier($table),
'urlhash',
// The line below is "result of sha256 on column `url`" // The line below is "result of sha256 on column `url`"
'SHA2(url, 256)')); 'SHA2(url, 256)'
));
echo "DONE.\n"; echo "DONE.\n";
echo "Resuming core schema upgrade..."; echo "Resuming core schema upgrade...";
} }

View File

@ -1,10 +1,7 @@
<?php <?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 * 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 * it under the terms of the GNU Affero General Public License as published by
@ -22,9 +19,9 @@
* @category Config * @category Config
* @package GNUsocial * @package GNUsocial
* @author Evan Prodromou <evan@status.net> * @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 * @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 = $default =
@ -253,11 +250,11 @@ $default =
'application/x-go-sgf' => 'sgf', 'application/x-go-sgf' => 'sgf',
'application/xml' => 'xml', 'application/xml' => 'xml',
'application/gpx+xml' => 'gpx', 'application/gpx+xml' => 'gpx',
'image/png' => 'png', image_type_to_mime_type(IMAGETYPE_PNG) => image_type_to_extension(IMAGETYPE_PNG),
'image/jpeg' => 'jpg', image_type_to_mime_type(IMAGETYPE_JPEG) => image_type_to_extension(IMAGETYPE_JPEG),
'image/gif' => 'gif', image_type_to_mime_type(IMAGETYPE_GIF) => image_type_to_extension(IMAGETYPE_GIF),
'image/svg+xml' => 'svg', 'image/svg+xml' => 'svg', // No built-in constant
'image/vnd.microsoft.icon' => 'ico', image_type_to_mime_type(IMAGETYPE_ICO) => image_type_to_extension(IMAGETYPE_ICO),
'audio/ogg' => 'ogg', 'audio/ogg' => 'ogg',
'audio/mpeg' => 'mpg', 'audio/mpeg' => 'mpg',
'audio/x-speex' => 'spx', 'audio/x-speex' => 'spx',
@ -280,6 +277,7 @@ $default =
'php' => 'phps', // this turns .php into .phps 'php' => 'phps', // this turns .php into .phps
'exe' => false, // this would deny any uploads to keep the "exe" file extension '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' => [ 'thumbnail' => [
'dir' => null, // falls back to File::path('thumb') (equivalent to ['attachments']['dir'] . '/thumb/') 'dir' => null, // falls back to File::path('thumb') (equivalent to ['attachments']['dir'] . '/thumb/')

View File

@ -22,7 +22,7 @@ if (!defined('GNUSOCIAL')) { exit(1); }
define('GNUSOCIAL_ENGINE', 'GNU social'); define('GNUSOCIAL_ENGINE', 'GNU social');
define('GNUSOCIAL_ENGINE_URL', 'https://www.gnu.org/software/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_LIFECYCLE', 'rc0'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
define('GNUSOCIAL_VERSION', GNUSOCIAL_BASE_VERSION . '-' . GNUSOCIAL_LIFECYCLE); define('GNUSOCIAL_VERSION', GNUSOCIAL_BASE_VERSION . '-' . GNUSOCIAL_LIFECYCLE);

View File

@ -1,11 +1,9 @@
<?php <?php
/** /**
* StatusNet, the distributed open-source microblogging tool * GNU social - a federating social network
* *
* Abstraction for an image file * Abstraction for an image file
* *
* PHP version 5
*
* LICENCE: 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 * 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 * the Free Software Foundation, either version 3 of the License, or
@ -20,55 +18,41 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
* @category Image * @category Image
* @package StatusNet * @package GNUsocial
* @author Evan Prodromou <evan@status.net> * @author Evan Prodromou <evan@status.net>
* @author Zach Copley <zach@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 * @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. * Makes it slightly easier to accept an image file from upload.
* *
* @category Image * @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 Evan Prodromou <evan@status.net>
* @author Zach Copley <zach@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 https://www.gnu.org/software/social/
* @link http://status.net/
*/ */
class ImageFile extends MediaFile
class ImageFile
{ {
var $id; public $type;
var $filepath; public $height;
var $filename; public $width;
var $type; public $rotate = 0; // degrees to rotate for properly oriented image (extrapolated from EXIF etc.)
var $height; public $animated = null; // Animated image? (has more than 1 frame). null means untested
var $width; public $mimetype = null; // The _ImageFile_ mimetype, _not_ the originating File object
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; public function __construct($id, string $filepath)
function __construct($id, $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, // 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! // since we may have generated an image source file from something else!
$this->filepath = $filepath; $this->filepath = $filepath;
@ -76,16 +60,14 @@ class ImageFile
$info = @getimagesize($this->filepath); $info = @getimagesize($this->filepath);
if (!( if (!(($info[2] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) ||
($info[2] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) || ($info[2] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) ||
($info[2] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) || ($info[2] == IMAGETYPE_BMP && function_exists('imagecreatefrombmp')) ||
$info[2] == IMAGETYPE_BMP || ($info[2] == IMAGETYPE_WBMP && function_exists('imagecreatefromwbmp')) ||
($info[2] == IMAGETYPE_WBMP && function_exists('imagecreatefromwbmp')) || ($info[2] == IMAGETYPE_XBM && function_exists('imagecreatefromxbm')) ||
($info[2] == IMAGETYPE_XBM && function_exists('imagecreatefromxbm')) || ($info[2] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')))) {
($info[2] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')))) {
// TRANS: Exception thrown when trying to upload an unsupported image file format. // 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->width = $info[0];
@ -93,11 +75,18 @@ class ImageFile
$this->type = $info[2]; $this->type = $info[2];
$this->mimetype = $info['mime']; $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')) { if ($this->type === IMAGETYPE_JPEG && function_exists('exif_read_data')) {
// Orientation value to rotate thumbnails properly // Orientation value to rotate thumbnails properly
$exif = @exif_read_data($this->filepath); $exif = @exif_read_data($this->filepath);
if (is_array($exif) && isset($exif['Orientation'])) { if (is_array($exif) && isset($exif['Orientation'])) {
switch ((int)$exif['Orientation']) { switch (intval($exif['Orientation'])) {
case 1: // top is top case 1: // top is top
$this->rotate = 0; $this->rotate = 0;
break; break;
@ -126,7 +115,7 @@ class ImageFile
$media = common_get_mime_media($file->mimetype); $media = common_get_mime_media($file->mimetype);
if (Event::handle('CreateFileImageThumbnailSource', array($file, &$imgPath, $media))) { if (Event::handle('CreateFileImageThumbnailSource', array($file, &$imgPath, $media))) {
if (empty($file->filename) && !file_exists($imgPath)) { 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 // First some mimetype specific exceptions
@ -141,7 +130,7 @@ class ImageFile
$imgPath = $file->getPath(); $imgPath = $file->getPath();
break; break;
default: 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 // Avoid deleting the original
try { try {
if (strlen($imgPath) > 0 && $imgPath !== $file->getPath()) { 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); @unlink($imgPath);
} }
} catch (FileNotFoundException $e) { } catch (FileNotFoundException $e) {
@ -163,7 +153,14 @@ class ImageFile
// doesn't exist anyway, so it's safe to delete $imgPath // doesn't exist anyway, so it's safe to delete $imgPath
@unlink($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; throw $e;
} }
return $image; return $image;
@ -178,42 +175,43 @@ class ImageFile
return $this->filepath; 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']) { return parent::fromUpload($param, $scoped);
case UPLOAD_ERR_OK: // success, jump out }
break;
case UPLOAD_ERR_INI_SIZE: /**
case UPLOAD_ERR_FORM_SIZE: * Several obscure file types should be normalized to PNG on resize.
// TRANS: Exception thrown when too large a file is uploaded. *
// TRANS: %s is the maximum file size, for example "500b", "10kB" or "2MB". * Keeps only PNG, JPEG and GIF
throw new Exception(sprintf(_('That file is too big. The maximum file size is %s.'), ImageFile::maxFileSize())); *
* @return int
case UPLOAD_ERR_PARTIAL: */
@unlink($_FILES[$param]['tmp_name']); public function preferredType()
// TRANS: Exception thrown when uploading an image and that action could not be completed. {
throw new Exception(_('Partial upload.')); // Keep only JPEG and GIF in their orignal format
if ($this->type === IMAGETYPE_JPEG || $this->type === IMAGETYPE_GIF) {
case UPLOAD_ERR_NO_FILE: return $this->type;
// 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.'));
} }
// We don't want to save some formats as they are rare, inefficient and antiquated
$info = @getimagesize($_FILES[$param]['tmp_name']); // thus we can't guarantee clients will support
// So just save it as PNG
if (!$info) { return IMAGETYPE_PNG;
@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']);
} }
/** /**
@ -224,8 +222,13 @@ class ImageFile
* *
* @param string $outpath * @param string $outpath
* @return ImageFile the image stored at target path * @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)); return new ImageFile(null, $this->resizeTo($outpath));
} }
@ -234,36 +237,34 @@ class ImageFile
* Create and save a thumbnail image. * Create and save a thumbnail image.
* *
* @param string $outpath * @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 * @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['height'] = isset($box['height']) ? intval($box['height']) : $this->height;
$box['x'] = isset($box['x']) ? intval($box['x']) : 0; $box['x'] = isset($box['x']) ? intval($box['x']) : 0;
$box['y'] = isset($box['y']) ? intval($box['y']) : 0; $box['y'] = isset($box['y']) ? intval($box['y']) : 0;
$box['w'] = isset($box['w']) ? intval($box['w']) : $this->width; $box['w'] = isset($box['w']) ? intval($box['w']) : $this->width;
$box['h'] = isset($box['h']) ? intval($box['h']) : $this->height; $box['h'] = isset($box['h']) ? intval($box['h']) : $this->height;
if (!file_exists($this->filepath)) { if (!file_exists($this->filepath)) {
// TRANS: Exception thrown during resize when image has been registered as present, but is no longer there. // 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 // Don't rotate/crop/scale if it isn't necessary
if ($box['width'] === $this->width if ($box['width'] === $this->width
&& $box['height'] === $this->height && $box['height'] === $this->height
&& $box['x'] === 0 && $box['x'] === 0
&& $box['y'] === 0 && $box['y'] === 0
&& $box['w'] === $this->width && $box['w'] === $this->width
&& $box['h'] === $this->height && $box['h'] === $this->height
&& $this->type == $this->preferredType()) { && $this->type === $this->preferredType()) {
if ($this->rotate == 0) { if (abs($this->rotate) == 90) {
// No rotational difference, just copy it as-is
@copy($this->filepath, $outpath);
return $outpath;
} elseif (abs($this->rotate) == 90) {
// Box is rotated 90 degrees in either direction, // Box is rotated 90 degrees in either direction,
// so we have to redefine x to y and vice versa. // so we have to redefine x to y and vice versa.
$tmp = $box['width']; $tmp = $box['width'];
@ -278,7 +279,6 @@ class ImageFile
} }
} }
if (Event::handle('StartResizeImageFile', array($this, $outpath, $box))) { if (Event::handle('StartResizeImageFile', array($this, $outpath, $box))) {
$this->resizeToFile($outpath, $box); $this->resizeToFile($outpath, $box);
} }
@ -294,8 +294,22 @@ class ImageFile
return $outpath; 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) protected function resizeToFile($outpath, array $box)
{ {
$old_limit = ini_set('memory_limit', common_config('attachments', 'memory_limit'));
$image_src = null;
switch ($this->type) { switch ($this->type) {
case IMAGETYPE_GIF: case IMAGETYPE_GIF:
$image_src = imagecreatefromgif($this->filepath); $image_src = imagecreatefromgif($this->filepath);
@ -317,7 +331,7 @@ class ImageFile
break; break;
default: default:
// TRANS: Exception thrown when trying to resize an unknown file type. // 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) { if ($this->rotate != 0) {
@ -326,30 +340,45 @@ class ImageFile
$image_dest = imagecreatetruecolor($box['width'], $box['height']); $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); $transparent_idx = imagecolortransparent($image_src);
if ($transparent_idx >= 0) { if ($transparent_idx >= 0 && $transparent_idx < 255) {
$transparent_color = imagecolorsforindex($image_src, $transparent_idx); $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); imagefill($image_dest, 0, 0, $transparent_idx);
imagecolortransparent($image_dest, $transparent_idx); imagecolortransparent($image_dest, $transparent_idx);
} elseif ($this->type == IMAGETYPE_PNG) { } elseif ($this->type == IMAGETYPE_PNG) {
imagealphablending($image_dest, false); imagealphablending($image_dest, false);
$transparent = imagecolorallocatealpha($image_dest, 0, 0, 0, 127); $transparent = imagecolorallocatealpha($image_dest, 0, 0, 0, 127);
imagefill($image_dest, 0, 0, $transparent); imagefill($image_dest, 0, 0, $transparent);
imagesavealpha($image_dest, true); 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: case IMAGETYPE_GIF:
imagegif($image_dest, $outpath); imagegif($image_dest, $outpath);
break; break;
@ -361,92 +390,32 @@ class ImageFile
break; break;
default: default:
// TRANS: Exception thrown when trying resize an unknown file type. // 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_src);
imagedestroy($image_dest); imagedestroy($image_dest);
ini_set('memory_limit', $old_limit); // Restore the old memory limit
} }
public function unlink()
/**
* 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()
{ {
@unlink($this->filepath); @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) public function scaleToFit($maxWidth=null, $maxHeight=null, $crop=null)
{ {
return self::getScalingValues($this->width, $this->height, return self::getScalingValues(
$maxWidth, $maxHeight, $crop, $this->rotate); $this->width,
$this->height,
$maxWidth,
$maxHeight,
$crop,
$this->rotate
);
} }
/* /**
* Gets scaling values for images of various types. Cropping can be enabled. * Gets scaling values for images of various types. Cropping can be enabled.
* *
* Values will scale _up_ to fit max values if cropping is 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 $maxW int Resulting max width
* @param $maxH int Resulting max height * @param $maxH int Resulting max height
* @param $crop int Crop to the size (not preserving aspect ratio) * @param $crop int Crop to the size (not preserving aspect ratio)
* @param int $rotate
* @return array
* @throws ServerException
*/ */
public static function getScalingValues($width, $height, public static function getScalingValues(
$maxW=null, $maxH=null, $width,
$crop=null, $rotate=0) $height,
{ $maxW=null,
$maxH=null,
$crop=null,
$rotate=0
) {
$maxW = $maxW ?: common_config('thumbnail', 'width'); $maxW = $maxW ?: common_config('thumbnail', 'width');
$maxH = $maxH ?: common_config('thumbnail', 'height'); $maxH = $maxH ?: common_config('thumbnail', 'height');
if ($maxW < 1 || ($maxH !== null && $maxH < 1)) { if ($maxW < 1 || ($maxH !== null && $maxH < 1)) {
throw new ServerException('Bad parameters for ImageFile::getScalingValues'); throw new ServerException('Bad parameters for ImageFile::getScalingValues');
} elseif ($maxH === null) { } elseif ($maxH === null) {
@ -479,14 +455,14 @@ class ImageFile
$width = $height; $width = $height;
$height = $tmp; $height = $tmp;
} }
// Cropping data (for original image size). Default values, 0 and null, // Cropping data (for original image size). Default values, 0 and null,
// imply no cropping and with preserved aspect ratio (per axis). // imply no cropping and with preserved aspect ratio (per axis).
$cx = 0; // crop x $cx = 0; // crop x
$cy = 0; // crop y $cy = 0; // crop y
$cw = null; // crop area width $cw = null; // crop area width
$ch = null; // crop area height $ch = null; // crop area height
if ($crop) { if ($crop) {
$s_ar = $width / $height; $s_ar = $width / $height;
$t_ar = $maxW / $maxH; $t_ar = $maxW / $maxH;
@ -513,9 +489,9 @@ class ImageFile
} }
} }
return array(intval($rw), intval($rh), return array(intval($rw), intval($rh),
intval($cx), intval($cy), intval($cx), intval($cy),
is_null($cw) ? $width : intval($cw), is_null($cw) ? $width : intval($cw),
is_null($ch) ? $height : intval($ch)); 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 // We read through the file til we reach the end of the file, or we've found
// at least 2 frame headers // 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 $chunk = fread($fh, 1024 * 100); //read 100kb at a time
$count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00\x2C#s', $chunk, $matches); $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 // rewind in case we ended up in the middle of the header, but avoid
@ -560,9 +536,9 @@ class ImageFile
} }
if ($width === null) { if ($width === null) {
$width = common_config('thumbnail', 'width'); $width = common_config('thumbnail', 'width');
$height = common_config('thumbnail', 'height'); $height = common_config('thumbnail', 'height');
$crop = common_config('thumbnail', 'crop'); $crop = common_config('thumbnail', 'crop');
} }
if (!$upscale) { if (!$upscale) {
@ -589,7 +565,7 @@ class ImageFile
'file_id'=> $this->fileRecord->getID(), 'file_id'=> $this->fileRecord->getID(),
'width' => $width, 'width' => $width,
'height' => $height, 'height' => $height,
)); ));
if ($thumb instanceof File_thumbnail) { if ($thumb instanceof File_thumbnail) {
return $thumb; return $thumb;
} }
@ -610,11 +586,16 @@ class ImageFile
|| $box['w'] < 1 || $box['x'] >= $this->width || $box['w'] < 1 || $box['x'] >= $this->width
|| $box['h'] < 1 || $box['y'] >= $this->height) { || $box['h'] < 1 || $box['y'] >= $this->height) {
// Fail on bad width parameter. If this occurs, it's due to algorithm in ImageFile->scaleToFit // 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.'); 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 // Perform resize and store into file
$this->resizeTo($outpath, $box); $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! // $this->getPath() says the file doesn't exist anyway, so no point in trying to delete it!
} }
return File_thumbnail::saveThumbnail($this->fileRecord->getID(), return File_thumbnail::saveThumbnail(
null, // no url since we generated it ourselves and can dynamically generate the url $this->fileRecord->getID(),
$width, $height, // no url since we generated it ourselves and can dynamically
$outname); // generate the url
null,
$width,
$height,
$outname
);
} }
} }
//PHP doesn't (as of 2/24/2010) have an imagecreatefrombmp so conditionally define one
if(!function_exists('imagecreatefrombmp')){
//taken shamelessly from http://www.php.net/manual/en/function.imagecreatefromwbmp.php#86214
function imagecreatefrombmp($p_sFile)
{
// Load the image into a string
$file = fopen($p_sFile,"rb");
$read = fread($file,10);
while(!feof($file)&&($read<>""))
$read .= fread($file,1024);
$temp = unpack("H*",$read);
$hex = $temp[1];
$header = substr($hex,0,108);
// Process the header
// Structure: http://www.fastgraph.com/help/bmp_header_format.html
if (substr($header,0,4)=="424d")
{
// Cut it in parts of 2 bytes
$header_parts = str_split($header,2);
// Get the width 4 bytes
$width = hexdec($header_parts[19].$header_parts[18]);
// Get the height 4 bytes
$height = hexdec($header_parts[23].$header_parts[22]);
// Unset the header params
unset($header_parts);
}
// Define starting X and Y
$x = 0;
$y = 1;
// Create newimage
$image = imagecreatetruecolor($width,$height);
// Grab the body from the image
$body = substr($hex,108);
// Calculate if padding at the end-line is needed
// Divided by two to keep overview.
// 1 byte = 2 HEX-chars
$body_size = (strlen($body)/2);
$header_size = ($width*$height);
// Use end-line padding? Only when needed
$usePadding = ($body_size>($header_size*3)+4);
// Using a for-loop with index-calculation instaid of str_split to avoid large memory consumption
// Calculate the next DWORD-position in the body
for ($i=0;$i<$body_size;$i+=3)
{
// Calculate line-ending and padding
if ($x>=$width)
{
// If padding needed, ignore image-padding
// Shift i to the ending of the current 32-bit-block
if ($usePadding)
$i += $width%4;
// Reset horizontal position
$x = 0;
// Raise the height-position (bottom-up)
$y++;
// Reached the image-height? Break the for-loop
if ($y>$height)
break;
}
// Calculation of the RGB-pixel (defined as BGR in image-data)
// Define $i_pos as absolute position in the body
$i_pos = $i*2;
$r = hexdec($body[$i_pos+4].$body[$i_pos+5]);
$g = hexdec($body[$i_pos+2].$body[$i_pos+3]);
$b = hexdec($body[$i_pos].$body[$i_pos+1]);
// Calculate and draw the pixel
$color = imagecolorallocate($image,$r,$g,$b);
imagesetpixel($image,$x,$height-$y,$color);
// Raise the horizontal position
$x++;
}
// Unset the body / free the memory
unset($body);
// Return image-object
return $image;
}
} // if(!function_exists('imagecreatefrombmp'))

View File

@ -1,12 +1,8 @@
<?php <?php
/** /**
* StatusNet, the distributed open-source microblogging tool * GNU social - a federating social network
* *
* Abstraction for media files in general * Abstraction for media files
*
* TODO: combine with ImageFile?
*
* PHP version 5
* *
* LICENCE: 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 * it under the terms of the GNU Affero General Public License as published by
@ -22,37 +18,78 @@
* 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 Media * @category Media
* @package StatusNet * @package GNUsocial
* @author Robin Millette <robin@millette.info> * @author Robin Millette <robin@millette.info>
* @author Miguel Dantas <biodantas@gmail.com>
* @author Zach Copley <zach@status.net> * @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 * @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 class MediaFile
{ {
var $filename = null; public $id = null;
var $fileRecord = null; public $filepath = null;
var $fileurl = null; public $filename = null;
var $short_fileurl = null; public $fileRecord = null;
var $mimetype = 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->filepath = $filepath;
$this->mimetype = $mimetype; $this->filename = basename($this->filepath);
$this->filehash = $filehash; $this->mimetype = $mimetype;
$this->fileRecord = $this->storeFile(); $this->filehash = self::getHashOfFile($this->filepath, $filehash);
$this->id = $id;
$this->fileurl = common_local_url('attachment', // If id is -1, it means we're dealing with a temporary object and don't want to store it in the DB,
array('attachment' => $this->fileRecord->id)); // 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->fileurl = common_local_url(
$this->short_fileurl = common_shorten_url($this->fileurl); 'attachment',
$this->maybeAddRedir($this->fileRecord->id, $this->short_fileurl); 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) public function attachToNotice(Notice $notice)
@ -65,20 +102,19 @@ class MediaFile
return File::path($this->filename); return File::path($this->filename);
} }
function shortUrl() public function shortUrl()
{ {
return $this->short_fileurl; return $this->short_fileurl;
} }
function getEnclosure() public function getEnclosure()
{ {
return $this->getFile()->getEnclosure(); return $this->getFile()->getEnclosure();
} }
function delete() public function delete()
{ {
$filepath = File::path($this->filename); @unlink($this->filepath);
@unlink($filepath);
} }
public function getFile() public function getFile()
@ -90,18 +126,38 @@ class MediaFile
return $this->fileRecord; 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); assert(!empty($filepath), __METHOD__ . ": filepath cannot be null");
if (!empty($this->filename) && $this->filehash === null) { if ($filehash === null) {
// Calculate if we have an older upload method somewhere (Qvitter) that // Calculate if we have an older upload method somewhere (Qvitter) that
// doesn't do this before calling new MediaFile on its local files... // doesn't do this before calling new MediaFile on its local files...
$this->filehash = hash_file(File::FILEHASH_ALG, $filepath); $filehash = hash_file(File::FILEHASH_ALG, $filepath);
if ($this->filehash === false) { if ($filehash === false) {
throw new ServerException('Could not read file for hashing'); 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 { try {
$file = File::getByHash($this->filehash); $file = File::getByHash($this->filehash);
// We're done here. Yes. Already. We assume sha256 won't collide on us anytime soon. // 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->urlhash = File::hashurl($fileurl);
$file->url = $fileurl; $file->url = $fileurl;
$file->filehash = $this->filehash; $file->filehash = $this->filehash;
$file->size = filesize($filepath); $file->size = filesize($this->filepath);
if ($file->size === false) { if ($file->size === false) {
throw new ServerException('Could not read file to get its size'); throw new ServerException('Could not read file to get its size');
} }
$file->date = time(); $file->date = time();
$file->mimetype = $this->mimetype; $file->mimetype = $this->mimetype;
$file_id = $file->insert(); $file_id = $file->insert();
if ($file_id===false) { if ($file_id===false) {
@ -158,15 +213,24 @@ class MediaFile
return $file; return $file;
} }
function rememberFile($file, $short) public function rememberFile($file, $short)
{ {
$this->maybeAddRedir($file->id, $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 { try {
$file_redir = File_redirection::getByUrl($url); File_redirection::getByUrl($url);
return false;
} catch (NoResultException $e) { } catch (NoResultException $e) {
$file_redir = new File_redirection; $file_redir = new File_redirection;
$file_redir->urlhash = File::hashurl($url); $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. // 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.')); 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. // The existence of the "error" element means PHP has processed it properly even if it was ok.
if (!isset($_FILES[$param]) || !isset($_FILES[$param]['error'])) { if (!isset($_FILES[$param]) || !isset($_FILES[$param]['error'])) {
@ -194,19 +328,17 @@ class MediaFile
case UPLOAD_ERR_OK: // success, jump out case UPLOAD_ERR_OK: // success, jump out
break; break;
case UPLOAD_ERR_INI_SIZE: 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: case UPLOAD_ERR_FORM_SIZE:
throw new ClientException( // TRANS: Exception thrown when too large a file is uploaded.
// TRANS: Client exception. // TRANS: %s is the maximum file size, for example "500b", "10kB" or "2MB".
_('The uploaded file exceeds the MAX_FILE_SIZE directive' . throw new ClientException(sprintf(
' that was specified in the HTML form.')); _('That file is too big. The maximum file size is %s.'),
self::maxFileSize()
));
case UPLOAD_ERR_PARTIAL: case UPLOAD_ERR_PARTIAL:
@unlink($_FILES[$param]['tmp_name']); @unlink($_FILES[$param]['tmp_name']);
// TRANS: Client exception. // TRANS: Client exception.
throw new ClientException(_('The uploaded file was only' . throw new ClientException(_('The uploaded file was only partially uploaded.'));
' partially uploaded.'));
case UPLOAD_ERR_NO_FILE: case UPLOAD_ERR_NO_FILE:
// No file; probably just a non-AJAX submission. // No file; probably just a non-AJAX submission.
throw new NoUploadedMediaException($param); 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. // TRANS: Client exception thrown when a file upload operation has been stopped by an extension.
throw new ClientException(_('File upload stopped by extension.')); throw new ClientException(_('File upload stopped by extension.'));
default: default:
common_log(LOG_ERR, __METHOD__ . ": Unknown upload error " . common_log(LOG_ERR, __METHOD__ . ": Unknown upload error " . $_FILES[$param]['error']);
$_FILES[$param]['error']);
// TRANS: Client exception thrown when a file upload operation has failed with an unknown reason. // TRANS: Client exception thrown when a file upload operation has failed with an unknown reason.
throw new ClientException(_('System error uploading file.')); throw new ClientException(_('System error uploading file.'));
} }
// TODO: Make documentation clearer that this won't work for files >2GiB because $filehash = strtolower(self::getHashOfFile($_FILES[$param]['tmp_name']));
// 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']);
try { try {
$file = File::getByHash($filehash); $file = File::getByHash($filehash);
// If no exception is thrown the file exists locally, so we'll use that and just add redirections. // 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 // but if the _actual_ locally stored file doesn't exist, getPath will throw FileNotFoundException
$filename = basename($file->getPath()); $filepath = $file->getPath();
$mimetype = $file->mimetype; $mimetype = $file->mimetype;
// XXX PHP: Upgrade to PHP 7.1
} catch (FileNotFoundException $e) { // catch (FileNotFoundException | NoResultException $e)
// The file does not exist in our local filesystem, so store this upload. } catch (Exception $e) {
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) {
// We have to save the upload as a new local file. This is the normal course of action. // We have to save the upload as a new local file. This is the normal course of action.
if ($scoped instanceof Profile) { if ($scoped instanceof Profile) {
// Throws exception if additional size does not respect quota // Throws exception if additional size does not respect quota
// This test is only needed, of course, if we're uploading something new. // 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']); $mimetype = self::getUploadedMimeType($_FILES[$param]['tmp_name'], $_FILES[$param]['name']);
$media = common_get_mime_media($mimetype);
$basename = basename($_FILES[$param]['name']); $basename = basename($_FILES[$param]['name']);
$filename = $filehash . '.' . File::guessMimeExtension($mimetype, $basename);
$filename = strtolower($filehash) . '.' . File::guessMimeExtension($mimetype, $basename);
$filepath = File::path($filename); $filepath = File::path($filename);
$result = move_uploaded_file($_FILES[$param]['tmp_name'], $filepath); $result = move_uploaded_file($_FILES[$param]['tmp_name'], $filepath);
if (!$result) { if (!$result) {
// TRANS: Client exception thrown when a file upload operation fails because the file could // 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. // 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.')); 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); $stream = stream_get_meta_data($fh);
// So far we're only handling filehandles originating from tmpfile(), // 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! // 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()); $filename = basename($file->getPath());
$mimetype = $file->mimetype; $mimetype = $file->mimetype;
} catch (NoResultException $e) { } catch (NoResultException $e) {
if ($scoped instanceof Profile) { if ($scoped instanceof Profile) {
File::respectsQuota($scoped, filesize($stream['uri'])); 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)); 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: 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. // 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. * Attempt to identify the content type of a given file.
* *
* @param string $filepath filesystem path as string (file must exist) * @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 * @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 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 // 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 // Unclear types are such that we can't really tell by the auto
// detect what they are (.bin, .exe etc. are just "octet-stream") // detect what they are (.bin, .exe etc. are just "octet-stream")