forked from GNUsocial/gnu-social
Merge branch 'nightly' of biodantas/gnu-social into nightly
This commit is contained in:
commit
abfd691fda
@ -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).
|
||||
|
||||
|
@ -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
|
||||
|
224
classes/File.php
224
classes/File.php
@ -1,23 +1,32 @@
|
||||
<?php
|
||||
/*
|
||||
* StatusNet - the distributed open-source microblogging tool
|
||||
* Copyright (C) 2008, 2009, StatusNet, Inc.
|
||||
/**
|
||||
* GNU social - a federating social network
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* Abstraction for files
|
||||
*
|
||||
* LICENCE: This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category Files
|
||||
* @package GNUsocial
|
||||
* @author Mikael Nordfeldth <mmn@hethane.se>
|
||||
* @author Miguel Dantas <biodantas@gmail.com>
|
||||
* @copyright 2008-2009, 2019 Free Software Foundation http://fsf.org
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link https://www.gnu.org/software/social/
|
||||
*/
|
||||
|
||||
if (!defined('GNUSOCIAL')) { exit(1); }
|
||||
defined('GNUSOCIAL') || die();
|
||||
|
||||
/**
|
||||
* Table Definition for file
|
||||
@ -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...";
|
||||
}
|
||||
|
@ -1,10 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet, the distributed open-source microblogging tool
|
||||
* GNU social - a federating social network
|
||||
*
|
||||
* Default settings for core configuration
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* LICENCE: This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@ -22,9 +19,9 @@
|
||||
* @category Config
|
||||
* @package GNUsocial
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @copyright 2008-9 StatusNet, Inc.
|
||||
* @copyright 2008-2009, 2019 Free Software Foundation http://fsf.org
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link http://www.gnu.org/software/social/
|
||||
* @link https://www.gnu.org/software/social/
|
||||
*/
|
||||
|
||||
$default =
|
||||
@ -253,11 +250,11 @@ $default =
|
||||
'application/x-go-sgf' => 'sgf',
|
||||
'application/xml' => 'xml',
|
||||
'application/gpx+xml' => 'gpx',
|
||||
'image/png' => 'png',
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/gif' => 'gif',
|
||||
'image/svg+xml' => 'svg',
|
||||
'image/vnd.microsoft.icon' => 'ico',
|
||||
image_type_to_mime_type(IMAGETYPE_PNG) => image_type_to_extension(IMAGETYPE_PNG),
|
||||
image_type_to_mime_type(IMAGETYPE_JPEG) => image_type_to_extension(IMAGETYPE_JPEG),
|
||||
image_type_to_mime_type(IMAGETYPE_GIF) => image_type_to_extension(IMAGETYPE_GIF),
|
||||
'image/svg+xml' => 'svg', // No built-in constant
|
||||
image_type_to_mime_type(IMAGETYPE_ICO) => image_type_to_extension(IMAGETYPE_ICO),
|
||||
'audio/ogg' => 'ogg',
|
||||
'audio/mpeg' => 'mpg',
|
||||
'audio/x-speex' => 'spx',
|
||||
@ -280,6 +277,7 @@ $default =
|
||||
'php' => 'phps', // this turns .php into .phps
|
||||
'exe' => false, // this would deny any uploads to keep the "exe" file extension
|
||||
],
|
||||
'memory_limit' => '1024M' // PHP's memory limit to use temporarily when handling images
|
||||
),
|
||||
'thumbnail' => [
|
||||
'dir' => null, // falls back to File::path('thumb') (equivalent to ['attachments']['dir'] . '/thumb/')
|
||||
|
@ -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);
|
||||
|
@ -1,11 +1,9 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet, the distributed open-source microblogging tool
|
||||
* GNU social - a federating social network
|
||||
*
|
||||
* Abstraction for an image file
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* LICENCE: This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
@ -20,55 +18,41 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category Image
|
||||
* @package StatusNet
|
||||
* @package GNUsocial
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @copyright 2008-2009 StatusNet, Inc.
|
||||
* @author Mikael Nordfeldth <mmn@hethane.se>
|
||||
* @author Miguel Dantas <biodantasgs@gmail.com>
|
||||
* @copyright 2008, 2019 Free Software Foundation http://fsf.org
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link http://status.net/
|
||||
* @link https://www.gnu.org/software/social/
|
||||
*/
|
||||
|
||||
if (!defined('GNUSOCIAL')) { exit(1); }
|
||||
defined('GNUSOCIAL') || die();
|
||||
|
||||
/**
|
||||
* A wrapper on uploaded files
|
||||
* A wrapper on uploaded images
|
||||
*
|
||||
* Makes it slightly easier to accept an image file from upload.
|
||||
*
|
||||
* @category Image
|
||||
* @package StatusNet
|
||||
* @package GNUsocial
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link http://status.net/
|
||||
* @link https://www.gnu.org/software/social/
|
||||
*/
|
||||
|
||||
class ImageFile
|
||||
class ImageFile extends MediaFile
|
||||
{
|
||||
var $id;
|
||||
var $filepath;
|
||||
var $filename;
|
||||
var $type;
|
||||
var $height;
|
||||
var $width;
|
||||
var $rotate=0; // degrees to rotate for properly oriented image (extrapolated from EXIF etc.)
|
||||
var $animated = null; // Animated image? (has more than 1 frame). null means untested
|
||||
var $mimetype = null; // The _ImageFile_ mimetype, _not_ the originating File object
|
||||
public $type;
|
||||
public $height;
|
||||
public $width;
|
||||
public $rotate = 0; // degrees to rotate for properly oriented image (extrapolated from EXIF etc.)
|
||||
public $animated = null; // Animated image? (has more than 1 frame). null means untested
|
||||
public $mimetype = null; // The _ImageFile_ mimetype, _not_ the originating File object
|
||||
|
||||
protected $fileRecord = null;
|
||||
|
||||
function __construct($id, $filepath)
|
||||
public function __construct($id, string $filepath)
|
||||
{
|
||||
$this->id = $id;
|
||||
if (!empty($this->id)) {
|
||||
$this->fileRecord = new File();
|
||||
$this->fileRecord->id = $this->id;
|
||||
if (!$this->fileRecord->find(true)) {
|
||||
// If we have set an ID, we need that ID to exist!
|
||||
throw new NoResultException($this->fileRecord);
|
||||
}
|
||||
}
|
||||
|
||||
// These do not have to be the same as fileRecord->filename for example,
|
||||
// since we may have generated an image source file from something else!
|
||||
$this->filepath = $filepath;
|
||||
@ -76,16 +60,14 @@ class ImageFile
|
||||
|
||||
$info = @getimagesize($this->filepath);
|
||||
|
||||
if (!(
|
||||
($info[2] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) ||
|
||||
($info[2] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) ||
|
||||
$info[2] == IMAGETYPE_BMP ||
|
||||
($info[2] == IMAGETYPE_WBMP && function_exists('imagecreatefromwbmp')) ||
|
||||
($info[2] == IMAGETYPE_XBM && function_exists('imagecreatefromxbm')) ||
|
||||
($info[2] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')))) {
|
||||
|
||||
if (!(($info[2] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) ||
|
||||
($info[2] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) ||
|
||||
($info[2] == IMAGETYPE_BMP && function_exists('imagecreatefrombmp')) ||
|
||||
($info[2] == IMAGETYPE_WBMP && function_exists('imagecreatefromwbmp')) ||
|
||||
($info[2] == IMAGETYPE_XBM && function_exists('imagecreatefromxbm')) ||
|
||||
($info[2] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')))) {
|
||||
// TRANS: Exception thrown when trying to upload an unsupported image file format.
|
||||
throw new UnsupportedMediaException(_('Unsupported image format.'), $this->filepath);
|
||||
throw new UnsupportedMediaException(_m('Unsupported image format.'), $this->filepath);
|
||||
}
|
||||
|
||||
$this->width = $info[0];
|
||||
@ -93,11 +75,18 @@ class ImageFile
|
||||
$this->type = $info[2];
|
||||
$this->mimetype = $info['mime'];
|
||||
|
||||
parent::__construct(
|
||||
$filepath,
|
||||
$this->mimetype,
|
||||
null /* filehash, MediaFile will calculate it */,
|
||||
$id
|
||||
);
|
||||
|
||||
if ($this->type === IMAGETYPE_JPEG && function_exists('exif_read_data')) {
|
||||
// Orientation value to rotate thumbnails properly
|
||||
$exif = @exif_read_data($this->filepath);
|
||||
if (is_array($exif) && isset($exif['Orientation'])) {
|
||||
switch ((int)$exif['Orientation']) {
|
||||
switch (intval($exif['Orientation'])) {
|
||||
case 1: // top is top
|
||||
$this->rotate = 0;
|
||||
break;
|
||||
@ -126,7 +115,7 @@ class ImageFile
|
||||
$media = common_get_mime_media($file->mimetype);
|
||||
if (Event::handle('CreateFileImageThumbnailSource', array($file, &$imgPath, $media))) {
|
||||
if (empty($file->filename) && !file_exists($imgPath)) {
|
||||
throw new UnsupportedMediaException(_('File without filename could not get a thumbnail source.'));
|
||||
throw new UnsupportedMediaException(_m('File without filename could not get a thumbnail source.'));
|
||||
}
|
||||
|
||||
// First some mimetype specific exceptions
|
||||
@ -141,7 +130,7 @@ class ImageFile
|
||||
$imgPath = $file->getPath();
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedMediaException(_('Unsupported media format.'), $file->getPath());
|
||||
throw new UnsupportedMediaException(_m('Unsupported media format.'), $file->getPath());
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,7 +144,8 @@ class ImageFile
|
||||
// Avoid deleting the original
|
||||
try {
|
||||
if (strlen($imgPath) > 0 && $imgPath !== $file->getPath()) {
|
||||
common_debug(__METHOD__.': Deleting temporary file that was created as image file thumbnail source: '._ve($imgPath));
|
||||
common_debug(__METHOD__.': Deleting temporary file that was created as image file' .
|
||||
'thumbnail source: '._ve($imgPath));
|
||||
@unlink($imgPath);
|
||||
}
|
||||
} catch (FileNotFoundException $e) {
|
||||
@ -163,7 +153,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'))
|
||||
|
@ -1,12 +1,8 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet, the distributed open-source microblogging tool
|
||||
* GNU social - a federating social network
|
||||
*
|
||||
* Abstraction for media files in general
|
||||
*
|
||||
* TODO: combine with ImageFile?
|
||||
*
|
||||
* PHP version 5
|
||||
* Abstraction for media files
|
||||
*
|
||||
* LICENCE: This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@ -22,37 +18,78 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category Media
|
||||
* @package StatusNet
|
||||
* @package GNUsocial
|
||||
* @author Robin Millette <robin@millette.info>
|
||||
* @author Miguel Dantas <biodantas@gmail.com>
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @copyright 2008-2009 StatusNet, Inc.
|
||||
* @author Mikael Nordfeldth <mmn@hethane.se>
|
||||
* @copyright 2008-2009, 2019 Free Software Foundation http://fsf.org
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link http://status.net/
|
||||
* @link https://www.gnu.org/software/social/
|
||||
*/
|
||||
|
||||
if (!defined('GNUSOCIAL')) { exit(1); }
|
||||
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")
|
||||
|
Loading…
Reference in New Issue
Block a user