2009-10-28 02:11:18 +00:00
|
|
|
<?php
|
2020-06-20 13:49:37 +01:00
|
|
|
// This file is part of GNU social - https://www.gnu.org/software/social
|
|
|
|
//
|
|
|
|
// GNU social 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.
|
|
|
|
//
|
|
|
|
// GNU social 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
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
|
|
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2009-10-28 02:11:18 +00:00
|
|
|
/**
|
2019-06-07 14:08:27 +01:00
|
|
|
* Abstraction for media files
|
2009-10-28 02:11:18 +00:00
|
|
|
*
|
|
|
|
* @category Media
|
2019-06-07 14:08:27 +01:00
|
|
|
* @package GNUsocial
|
2020-06-20 13:49:37 +01:00
|
|
|
*
|
2009-10-29 00:47:14 +00:00
|
|
|
* @author Robin Millette <robin@millette.info>
|
2019-06-07 14:08:27 +01:00
|
|
|
* @author Miguel Dantas <biodantas@gmail.com>
|
2009-10-28 02:11:18 +00:00
|
|
|
* @author Zach Copley <zach@status.net>
|
2019-06-07 14:08:27 +01:00
|
|
|
* @author Mikael Nordfeldth <mmn@hethane.se>
|
2020-06-20 13:49:37 +01:00
|
|
|
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
|
|
|
* @copyright 2008-2009, 2019-2020 Free Software Foundation http://fsf.org
|
2009-10-28 02:11:18 +00:00
|
|
|
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
|
|
|
*/
|
[MEDIA] File downloader now in PHP, added proper name in the UI and changed the format for new attachment file names
The file downloader was changed from a simple redirect to the file to one
implemented in PHP, which should make it safer, by making it possible disallow
direct access to the file, to prevent executing of atttachments
The filename has a new format:
bin2hex("{$original_name}")."-{$filehash}"
This format should be respected. Notice the dash, which is important to distinguish it from the previous
format, which was "{$hash}.{$ext}"
This change was made to both make the experience more user friendly, by
providing a readable name for files, as opposed to it's hash. This name is taken
from the upload filename, but, clearly, as this wasn't done before, it's
impossible to have a proper name for older files, so those are displayed as
"untitled.{$ext}".
This new name is displayed in the UI, instead of the previous name.
2019-06-11 02:42:33 +01:00
|
|
|
defined('GNUSOCIAL') || die();
|
2019-06-07 14:08:27 +01:00
|
|
|
|
2020-09-04 11:15:23 +01:00
|
|
|
require_once INSTALLDIR . '/lib/util/tempfile.php';
|
|
|
|
|
2019-06-07 14:08:27 +01:00
|
|
|
/**
|
|
|
|
* Class responsible for abstracting media files
|
|
|
|
*/
|
2009-10-28 04:45:56 +00:00
|
|
|
class MediaFile
|
2009-10-28 02:11:18 +00:00
|
|
|
{
|
2020-06-20 13:49:37 +01:00
|
|
|
public $id;
|
|
|
|
public $filepath;
|
|
|
|
public $filename;
|
|
|
|
public $fileRecord;
|
|
|
|
public $fileurl;
|
|
|
|
public $short_fileurl;
|
|
|
|
public $mimetype;
|
2009-10-28 02:11:18 +00:00
|
|
|
|
2019-06-07 14:08:27 +01:00
|
|
|
/**
|
2020-06-21 00:09:32 +01:00
|
|
|
* MediaFile constructor.
|
|
|
|
*
|
2020-09-21 21:54:23 +01:00
|
|
|
* @param string|null $filepath The path of the file this media refers to. Required
|
2019-06-07 14:08:27 +01:00
|
|
|
* @param string $mimetype The mimetype of the file. Required
|
2020-06-21 00:09:32 +01:00
|
|
|
* @param string|null $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.
|
2020-06-20 13:49:37 +01:00
|
|
|
* If null, it searches for it. If -1, it skips all DB
|
|
|
|
* interactions (useful for temporary objects)
|
2020-09-21 21:54:23 +01:00
|
|
|
* @param string|null $fileurl Provide if remote
|
2019-06-07 14:08:27 +01:00
|
|
|
* @throws ClientException
|
|
|
|
* @throws NoResultException
|
|
|
|
* @throws ServerException
|
|
|
|
*/
|
2020-09-21 21:54:23 +01:00
|
|
|
public function __construct(?string $filepath = null, string $mimetype, ?string $filehash = null, ?int $id = null, ?string $fileurl = null)
|
2009-10-28 02:11:18 +00:00
|
|
|
{
|
2019-06-07 14:08:27 +01:00
|
|
|
$this->filepath = $filepath;
|
|
|
|
$this->filename = basename($this->filepath);
|
|
|
|
$this->mimetype = $mimetype;
|
2020-09-21 21:54:23 +01:00
|
|
|
$this->filehash = is_null($filepath) ? null : self::getHashOfFile($this->filepath, $filehash);
|
|
|
|
$this->id = $id;
|
|
|
|
$this->fileurl = $fileurl;
|
2019-06-07 14:08:27 +01:00
|
|
|
|
|
|
|
// 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();
|
|
|
|
}
|
|
|
|
}
|
2009-10-28 02:11:18 +00:00
|
|
|
}
|
2009-10-28 04:45:56 +00:00
|
|
|
|
2020-09-21 21:54:23 +01:00
|
|
|
/**
|
|
|
|
* Shortcut method to get a MediaFile from a File
|
|
|
|
*
|
|
|
|
* @param File $file
|
|
|
|
* @return MediaFile|ImageFile
|
|
|
|
* @throws ClientException
|
|
|
|
* @throws FileNotFoundException
|
|
|
|
* @throws NoResultException
|
|
|
|
* @throws ServerException
|
|
|
|
*/
|
|
|
|
public static function fromFileObject(File $file)
|
|
|
|
{
|
|
|
|
$filepath = null;
|
|
|
|
try {
|
|
|
|
$filepath = $file->getPath();
|
|
|
|
} catch (Exception $e) {}
|
|
|
|
return new self($filepath, common_get_mime_media($file->mimetype), $file->filehash, $file->getID());
|
|
|
|
}
|
|
|
|
|
2014-04-16 22:17:27 +01:00
|
|
|
public function attachToNotice(Notice $notice)
|
2009-10-28 02:11:18 +00:00
|
|
|
{
|
2015-06-04 16:36:11 +01:00
|
|
|
File_to_post::processNew($this->fileRecord, $notice);
|
2009-10-28 02:11:18 +00:00
|
|
|
}
|
2009-10-28 04:45:56 +00:00
|
|
|
|
2014-04-16 18:14:26 +01:00
|
|
|
public function getPath()
|
|
|
|
{
|
|
|
|
return File::path($this->filename);
|
|
|
|
}
|
|
|
|
|
2020-09-21 21:54:23 +01:00
|
|
|
/**
|
|
|
|
* @param bool|null $use_local true means require local, null means prefer original, false means use whatever is stored
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getUrl(?bool $use_local=null): ?string
|
|
|
|
{
|
|
|
|
if ($use_local !== false) {
|
|
|
|
if (empty($this->fileurl)) {
|
|
|
|
// A locally stored file, so let's generate a URL for our instance.
|
|
|
|
return common_local_url('attachment_view', ['filehash' => $this->filehash]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// The original file's URL
|
|
|
|
return $this->fileurl;
|
|
|
|
}
|
|
|
|
|
2019-06-09 23:26:48 +01:00
|
|
|
public function shortUrl()
|
2009-10-28 02:11:18 +00:00
|
|
|
{
|
2020-09-21 21:54:23 +01:00
|
|
|
return common_shorten_url($this->getUrl());
|
2009-10-28 02:11:18 +00:00
|
|
|
}
|
|
|
|
|
2019-06-09 23:26:48 +01:00
|
|
|
public function getEnclosure()
|
2016-01-01 19:18:54 +00:00
|
|
|
{
|
|
|
|
return $this->getFile()->getEnclosure();
|
|
|
|
}
|
|
|
|
|
2020-09-21 21:54:23 +01:00
|
|
|
public function delete($useWhere=false)
|
2009-10-28 02:11:18 +00:00
|
|
|
{
|
2020-09-21 21:54:23 +01:00
|
|
|
if (!is_null($this->fileRecord)) {
|
|
|
|
$this->fileRecord->delete($useWhere);
|
|
|
|
}
|
2019-06-07 14:08:27 +01:00
|
|
|
@unlink($this->filepath);
|
2009-10-28 02:11:18 +00:00
|
|
|
}
|
|
|
|
|
2020-09-21 21:54:23 +01:00
|
|
|
public function unlink()
|
|
|
|
{
|
|
|
|
$this->filename = null;
|
|
|
|
// Delete the file, if it exists locally
|
|
|
|
if (!empty($this->filepath) && file_exists($this->filepath)) {
|
|
|
|
$deleted = @unlink($this->filepath);
|
|
|
|
if (!$deleted) {
|
|
|
|
common_log(LOG_ERR, sprintf('Could not unlink existing file: "%s"', $this->filepath));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$this->fileRecord->unlink();
|
|
|
|
}
|
|
|
|
|
2015-01-23 13:46:47 +00:00
|
|
|
public function getFile()
|
|
|
|
{
|
|
|
|
if (!$this->fileRecord instanceof File) {
|
|
|
|
throw new ServerException('File record did not exist for MediaFile');
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->fileRecord;
|
|
|
|
}
|
|
|
|
|
2019-06-07 14:08:27 +01:00
|
|
|
/**
|
|
|
|
* Calculate the hash of a file.
|
|
|
|
*
|
|
|
|
* This won't work for files >2GiB because PHP uses only 32bit.
|
2020-06-20 13:49:37 +01:00
|
|
|
*
|
2019-06-07 14:08:27 +01:00
|
|
|
* @param string $filepath
|
2020-06-20 13:49:37 +01:00
|
|
|
* @param null|string $filehash
|
|
|
|
*
|
2019-06-07 14:08:27 +01:00
|
|
|
* @return string
|
|
|
|
* @throws ServerException
|
2020-06-20 13:49:37 +01:00
|
|
|
*
|
2019-06-07 14:08:27 +01:00
|
|
|
*/
|
|
|
|
public static function getHashOfFile(string $filepath, $filehash = null)
|
2014-04-22 11:09:24 +01:00
|
|
|
{
|
2020-06-20 13:49:37 +01:00
|
|
|
assert(!empty($filepath), __METHOD__ . ': filepath cannot be null');
|
2019-06-07 14:08:27 +01:00
|
|
|
if ($filehash === null) {
|
2015-02-24 20:11:25 +00:00
|
|
|
// Calculate if we have an older upload method somewhere (Qvitter) that
|
|
|
|
// doesn't do this before calling new MediaFile on its local files...
|
2019-06-07 14:08:27 +01:00
|
|
|
$filehash = hash_file(File::FILEHASH_ALG, $filepath);
|
|
|
|
if ($filehash === false) {
|
2015-02-24 20:11:25 +00:00
|
|
|
throw new ServerException('Could not read file for hashing');
|
|
|
|
}
|
|
|
|
}
|
2019-06-07 14:08:27 +01:00
|
|
|
return $filehash;
|
|
|
|
}
|
2015-02-24 20:11:25 +00:00
|
|
|
|
2019-06-07 14:08:27 +01:00
|
|
|
/**
|
|
|
|
* Retrieve or insert as a file in the DB
|
|
|
|
*
|
|
|
|
* @return object File
|
|
|
|
* @throws ServerException
|
2020-06-20 13:49:37 +01:00
|
|
|
*
|
|
|
|
* @throws ClientException
|
2019-06-07 14:08:27 +01:00
|
|
|
*/
|
|
|
|
protected function storeFile()
|
|
|
|
{
|
2015-02-24 20:11:25 +00:00
|
|
|
try {
|
|
|
|
$file = File::getByHash($this->filehash);
|
2020-09-21 21:54:23 +01:00
|
|
|
if (is_null($this->fileurl) && is_null($file->getUrl(false))) {
|
|
|
|
// An already existing local file is being re-added, return it
|
|
|
|
return $file;
|
|
|
|
}
|
2015-02-24 20:11:25 +00:00
|
|
|
} catch (NoResultException $e) {
|
|
|
|
// Well, let's just continue below.
|
|
|
|
}
|
|
|
|
|
2009-10-28 02:11:18 +00:00
|
|
|
$file = new File;
|
|
|
|
|
2009-10-29 00:12:22 +00:00
|
|
|
$file->filename = $this->filename;
|
2020-09-21 21:54:23 +01:00
|
|
|
$file->url = $this->fileurl;
|
|
|
|
$file->urlhash = is_null($file->url) ? null : File::hashurl($file->url);
|
2015-02-24 20:11:25 +00:00
|
|
|
$file->filehash = $this->filehash;
|
2020-06-20 13:49:37 +01:00
|
|
|
$file->size = filesize($this->filepath);
|
2015-02-24 20:11:25 +00:00
|
|
|
if ($file->size === false) {
|
|
|
|
throw new ServerException('Could not read file to get its size');
|
|
|
|
}
|
2020-06-20 13:49:37 +01:00
|
|
|
$file->date = time();
|
2009-10-28 02:11:18 +00:00
|
|
|
$file->mimetype = $this->mimetype;
|
|
|
|
|
|
|
|
$file_id = $file->insert();
|
|
|
|
|
2020-06-20 13:49:37 +01:00
|
|
|
if ($file_id === false) {
|
|
|
|
common_log_db_error($file, 'INSERT', __FILE__);
|
2010-09-12 17:24:44 +01:00
|
|
|
// TRANS: Client exception thrown when a database error was thrown during a file upload operation.
|
2019-08-17 02:33:31 +01:00
|
|
|
throw new ClientException(_m('There was a database error while saving your file. Please try again.'));
|
2009-10-28 02:11:18 +00:00
|
|
|
}
|
2009-10-28 04:45:56 +00:00
|
|
|
|
2014-04-22 11:09:24 +01:00
|
|
|
// Set file geometrical properties if available
|
|
|
|
try {
|
|
|
|
$image = ImageFile::fromFileObject($file);
|
2020-09-21 21:54:23 +01:00
|
|
|
$orig = clone($file);
|
2014-04-22 11:09:24 +01:00
|
|
|
$file->width = $image->width;
|
|
|
|
$file->height = $image->height;
|
|
|
|
$file->update($orig);
|
|
|
|
|
|
|
|
// We have to cleanup after ImageFile, since it
|
|
|
|
// may have generated a temporary file from a
|
|
|
|
// video support plugin or something.
|
|
|
|
// FIXME: Do this more automagically.
|
2020-09-21 21:54:23 +01:00
|
|
|
// Honestly, I think this is unlikely these days,
|
2021-02-16 18:30:21 +00:00
|
|
|
// but better be safe than sorry, I guess
|
2014-04-22 11:09:24 +01:00
|
|
|
if ($image->getPath() != $file->getPath()) {
|
|
|
|
$image->unlink();
|
|
|
|
}
|
|
|
|
} catch (ServerException $e) {
|
|
|
|
// We just couldn't make out an image from the file. This
|
|
|
|
// does not have to be UnsupportedMediaException, as we can
|
|
|
|
// also get ServerException from files not existing etc.
|
|
|
|
}
|
|
|
|
|
2009-10-28 02:11:18 +00:00
|
|
|
return $file;
|
|
|
|
}
|
|
|
|
|
2019-06-07 14:08:27 +01:00
|
|
|
/**
|
|
|
|
* The maximum allowed file size, as a string
|
|
|
|
*/
|
2019-06-09 23:26:48 +01:00
|
|
|
public static function maxFileSize()
|
2019-06-07 14:08:27 +01:00
|
|
|
{
|
|
|
|
$value = self::maxFileSizeInt();
|
|
|
|
if ($value > 1024 * 1024) {
|
2020-06-20 13:49:37 +01:00
|
|
|
$value = $value / (1024 * 1024);
|
2019-06-07 14:08:27 +01:00
|
|
|
// TRANS: Number of megabytes. %d is the number.
|
2019-06-09 23:26:48 +01:00
|
|
|
return sprintf(_m('%dMB', '%dMB', $value), $value);
|
|
|
|
} elseif ($value > 1024) {
|
2020-06-20 13:49:37 +01:00
|
|
|
$value = $value / 1024;
|
2019-06-07 14:08:27 +01:00
|
|
|
// TRANS: Number of kilobytes. %d is the number.
|
2019-06-09 23:26:48 +01:00
|
|
|
return sprintf(_m('%dkB', '%dkB', $value), $value);
|
2019-06-07 14:08:27 +01:00
|
|
|
} else {
|
|
|
|
// TRANS: Number of bytes. %d is the number.
|
2019-06-09 23:26:48 +01:00
|
|
|
return sprintf(_m('%dB', '%dB', $value), $value);
|
2019-06-07 14:08:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The maximum allowed file size, as an int
|
|
|
|
*/
|
2020-06-20 13:49:37 +01:00
|
|
|
public static function maxFileSizeInt(): int
|
2019-06-07 14:08:27 +01:00
|
|
|
{
|
2019-06-15 15:21:05 +01:00
|
|
|
return common_config('attachments', 'file_quota');
|
2009-10-28 02:11:18 +00:00
|
|
|
}
|
|
|
|
|
2019-06-30 13:36:33 +01:00
|
|
|
/**
|
|
|
|
* Encodes a file name and a file hash in the new file format, which is used to avoid
|
|
|
|
* having an extension in the file, removing trust in extensions, while keeping the original name
|
2020-06-20 13:49:37 +01:00
|
|
|
*
|
2020-07-05 01:25:11 +01:00
|
|
|
* @param null|string $original_name
|
|
|
|
* @param string $filehash
|
|
|
|
* @param null|string|bool $ext from File::getSafeExtension
|
2020-06-20 13:49:37 +01:00
|
|
|
*
|
2020-07-05 01:25:11 +01:00
|
|
|
* @return string
|
2019-06-30 13:36:33 +01:00
|
|
|
* @throws ClientException
|
2020-07-05 01:25:11 +01:00
|
|
|
* @throws ServerException
|
2019-06-30 13:36:33 +01:00
|
|
|
*/
|
2020-06-20 13:49:37 +01:00
|
|
|
public static function encodeFilename($original_name, string $filehash, $ext = null): string
|
2019-06-30 13:36:33 +01:00
|
|
|
{
|
|
|
|
if (empty($original_name)) {
|
2019-08-17 02:33:31 +01:00
|
|
|
$original_name = _m('Untitled attachment');
|
2019-06-30 13:36:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// If we're given an extension explicitly, use it, otherwise...
|
|
|
|
$ext = $ext ?:
|
2020-06-20 13:49:37 +01:00
|
|
|
// get a replacement extension if configured, returns false if it's blocked,
|
|
|
|
// null if no extension
|
|
|
|
File::getSafeExtension($original_name);
|
2019-06-30 13:36:33 +01:00
|
|
|
if ($ext === false) {
|
2019-08-17 02:33:31 +01:00
|
|
|
throw new ClientException(_m('Blacklisted file extension.'));
|
2019-06-30 13:36:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!empty($ext)) {
|
|
|
|
// Remove dots if we have them (make sure they're not repeated)
|
|
|
|
$ext = preg_replace('/^\.+/', '', $ext);
|
|
|
|
$original_name = preg_replace('/\.+.+$/i', ".{$ext}", $original_name);
|
|
|
|
}
|
|
|
|
|
|
|
|
$enc_name = bin2hex($original_name);
|
|
|
|
return "{$enc_name}-{$filehash}";
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Decode the new filename format
|
2020-06-20 13:49:37 +01:00
|
|
|
*
|
2019-06-30 13:36:33 +01:00
|
|
|
* @return false | null | string on failure, no match (old format) or original file name, respectively
|
|
|
|
*/
|
|
|
|
public static function decodeFilename(string $encoded_filename)
|
|
|
|
{
|
2019-09-12 22:11:45 +01:00
|
|
|
// Should match:
|
|
|
|
// hex-hash
|
|
|
|
// thumb-id-widthxheight-hex-hash
|
|
|
|
// And return the `hex` part
|
|
|
|
$ret = preg_match('/^(.*-)?([^-]+)-[^-]+$/', $encoded_filename, $matches);
|
2019-06-30 13:36:33 +01:00
|
|
|
if ($ret === false) {
|
|
|
|
return false;
|
2020-01-07 14:30:18 +00:00
|
|
|
} elseif ($ret === 0 || !ctype_xdigit($matches[2])) {
|
2019-06-30 13:36:33 +01:00
|
|
|
return null; // No match
|
|
|
|
} else {
|
2019-09-12 22:11:45 +01:00
|
|
|
$filename = hex2bin($matches[2]);
|
2019-06-30 13:36:33 +01:00
|
|
|
|
|
|
|
// Matches extension
|
|
|
|
if (preg_match('/^(.+?)\.(.+)$/', $filename, $sub_matches) === 1) {
|
|
|
|
$ext = $sub_matches[2];
|
|
|
|
// Previously, there was a blacklisted extension array, which could have an alternative
|
|
|
|
// extension, such as phps, to replace php. We want to turn it back (this is deprecated,
|
|
|
|
// as it no longer makes sense, since we don't trust trust files based on extension,
|
|
|
|
// but keep the feature)
|
|
|
|
$blacklist = common_config('attachments', 'extblacklist');
|
|
|
|
if (is_array($blacklist)) {
|
|
|
|
foreach ($blacklist as $upload_ext => $safe_ext) {
|
|
|
|
if ($ext === $safe_ext) {
|
|
|
|
$ext = $upload_ext;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "{$sub_matches[1]}.{$ext}";
|
|
|
|
} else {
|
|
|
|
// No extension, don't bother trying to replace it
|
|
|
|
return $filename;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-07 14:08:27 +01:00
|
|
|
/**
|
|
|
|
* 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)
|
[MEDIA] File downloader now in PHP, added proper name in the UI and changed the format for new attachment file names
The file downloader was changed from a simple redirect to the file to one
implemented in PHP, which should make it safer, by making it possible disallow
direct access to the file, to prevent executing of atttachments
The filename has a new format:
bin2hex("{$original_name}")."-{$filehash}"
This format should be respected. Notice the dash, which is important to distinguish it from the previous
format, which was "{$hash}.{$ext}"
This change was made to both make the experience more user friendly, by
providing a readable name for files, as opposed to it's hash. This name is taken
from the upload filename, but, clearly, as this wasn't done before, it's
impossible to have a proper name for older files, so those are displayed as
"untitled.{$ext}".
This new name is displayed in the UI, instead of the previous name.
2019-06-11 02:42:33 +01:00
|
|
|
* The filename has a new format:
|
|
|
|
* bin2hex("{$original_name}.{$ext}")."-{$filehash}"
|
|
|
|
* This format should be respected. Notice the dash, which is important to distinguish it from the previous
|
|
|
|
* format ("{$hash}.{$ext}")
|
|
|
|
*
|
2019-08-17 02:33:31 +01:00
|
|
|
* @param string $param Form name
|
2020-07-05 01:25:11 +01:00
|
|
|
* @param Profile|null $scoped
|
2019-06-07 14:08:27 +01:00
|
|
|
* @return ImageFile|MediaFile
|
2020-07-05 01:25:11 +01:00
|
|
|
* @throws ClientException
|
|
|
|
* @throws InvalidFilenameException
|
2019-06-07 14:08:27 +01:00
|
|
|
* @throws NoResultException
|
|
|
|
* @throws NoUploadedMediaException
|
|
|
|
* @throws ServerException
|
|
|
|
* @throws UnsupportedMediaException
|
|
|
|
* @throws UseFileAsThumbnailException
|
|
|
|
*/
|
2020-07-05 01:25:11 +01:00
|
|
|
public static function fromUpload(string $param = 'media', ?Profile $scoped = null)
|
2009-10-28 02:11:18 +00:00
|
|
|
{
|
2015-01-21 16:32:57 +00:00
|
|
|
// The existence of the "error" element means PHP has processed it properly even if it was ok.
|
2020-06-20 13:49:37 +01:00
|
|
|
if (!(isset($_FILES[$param], $_FILES[$param]['error']))) {
|
2015-01-21 16:32:57 +00:00
|
|
|
throw new NoUploadedMediaException($param);
|
2009-10-28 02:11:18 +00:00
|
|
|
}
|
2009-10-28 04:45:56 +00:00
|
|
|
|
2009-10-28 02:11:18 +00:00
|
|
|
switch ($_FILES[$param]['error']) {
|
2014-04-22 11:09:24 +01:00
|
|
|
case UPLOAD_ERR_OK: // success, jump out
|
|
|
|
break;
|
|
|
|
case UPLOAD_ERR_INI_SIZE:
|
|
|
|
case UPLOAD_ERR_FORM_SIZE:
|
2019-06-07 14:08:27 +01:00
|
|
|
// TRANS: Exception thrown when too large a file is uploaded.
|
|
|
|
// TRANS: %s is the maximum file size, for example "500b", "10kB" or "2MB".
|
2019-06-09 23:26:48 +01:00
|
|
|
throw new ClientException(sprintf(
|
2019-08-17 02:33:31 +01:00
|
|
|
_m('That file is too big. The maximum file size is %s.'),
|
2019-06-09 23:26:48 +01:00
|
|
|
self::maxFileSize()
|
|
|
|
));
|
2014-04-22 11:09:24 +01:00
|
|
|
case UPLOAD_ERR_PARTIAL:
|
|
|
|
@unlink($_FILES[$param]['tmp_name']);
|
2010-09-12 17:24:44 +01:00
|
|
|
// TRANS: Client exception.
|
2019-08-17 02:33:31 +01:00
|
|
|
throw new ClientException(_m('The uploaded file was only partially uploaded.'));
|
2014-04-22 11:09:24 +01:00
|
|
|
case UPLOAD_ERR_NO_FILE:
|
|
|
|
// No file; probably just a non-AJAX submission.
|
2015-01-22 11:38:57 +00:00
|
|
|
throw new NoUploadedMediaException($param);
|
2014-04-22 11:09:24 +01:00
|
|
|
case UPLOAD_ERR_NO_TMP_DIR:
|
|
|
|
// TRANS: Client exception thrown when a temporary folder is not present to store a file upload.
|
2019-08-17 02:33:31 +01:00
|
|
|
throw new ClientException(_m('Missing a temporary folder.'));
|
2014-04-22 11:09:24 +01:00
|
|
|
case UPLOAD_ERR_CANT_WRITE:
|
|
|
|
// TRANS: Client exception thrown when writing to disk is not possible during a file upload operation.
|
2019-08-17 02:33:31 +01:00
|
|
|
throw new ClientException(_m('Failed to write file to disk.'));
|
2014-04-22 11:09:24 +01:00
|
|
|
case UPLOAD_ERR_EXTENSION:
|
|
|
|
// TRANS: Client exception thrown when a file upload operation has been stopped by an extension.
|
2019-08-17 02:33:31 +01:00
|
|
|
throw new ClientException(_m('File upload stopped by extension.'));
|
2014-04-22 11:09:24 +01:00
|
|
|
default:
|
2020-06-20 13:49:37 +01:00
|
|
|
common_log(LOG_ERR, __METHOD__ . ': Unknown upload error ' . $_FILES[$param]['error']);
|
2014-04-22 11:09:24 +01:00
|
|
|
// TRANS: Client exception thrown when a file upload operation has failed with an unknown reason.
|
2019-08-17 02:33:31 +01:00
|
|
|
throw new ClientException(_m('System error uploading file.'));
|
2009-10-28 02:11:18 +00:00
|
|
|
}
|
2009-10-28 04:45:56 +00:00
|
|
|
|
2019-06-07 14:08:27 +01:00
|
|
|
$filehash = strtolower(self::getHashOfFile($_FILES[$param]['tmp_name']));
|
2021-02-18 17:49:10 +00:00
|
|
|
$fileid = null;
|
2015-02-24 20:11:25 +00:00
|
|
|
try {
|
|
|
|
$file = File::getByHash($filehash);
|
2021-02-18 17:49:10 +00:00
|
|
|
// There can be more than one file for the same filehash IF the url are different (due to different metadata).
|
2020-09-21 21:54:23 +01:00
|
|
|
while ($file->fetch()) {
|
|
|
|
if ($file->getUrl(false)) {
|
2021-02-18 17:49:10 +00:00
|
|
|
// Files uploaded by Actors of this instance won't have an url, skip.
|
2020-09-21 21:54:23 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
return ImageFile::fromFileObject($file);
|
|
|
|
} catch (UnsupportedMediaException $e) {
|
|
|
|
return MediaFile::fromFileObject($file);
|
|
|
|
}
|
|
|
|
}
|
2021-02-18 17:49:10 +00:00
|
|
|
// If no exception is thrown then this file was already uploaded by a local actor once, so we'll use that and just add redirections.
|
2016-03-05 11:05:12 +00:00
|
|
|
// but if the _actual_ locally stored file doesn't exist, getPath will throw FileNotFoundException
|
2019-06-07 14:08:27 +01:00
|
|
|
$filepath = $file->getPath();
|
2015-02-24 20:11:25 +00:00
|
|
|
$mimetype = $file->mimetype;
|
2021-02-18 17:49:10 +00:00
|
|
|
$fileid = $file->getID();
|
2019-07-25 01:29:20 +01:00
|
|
|
} catch (FileNotFoundException | NoResultException $e) {
|
2015-02-24 20:11:25 +00:00
|
|
|
// We have to save the upload as a new local file. This is the normal course of action.
|
2016-03-05 10:59:46 +00:00
|
|
|
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.
|
|
|
|
File::respectsQuota($scoped, $_FILES[$param]['size']);
|
|
|
|
}
|
2009-10-28 04:45:56 +00:00
|
|
|
|
2015-02-24 20:11:25 +00:00
|
|
|
$mimetype = self::getUploadedMimeType($_FILES[$param]['tmp_name'], $_FILES[$param]['name']);
|
2019-06-07 14:08:27 +01:00
|
|
|
$media = common_get_mime_media($mimetype);
|
2009-10-28 04:45:56 +00:00
|
|
|
|
2019-06-16 00:41:54 +01:00
|
|
|
$basename = basename($_FILES[$param]['name']);
|
[MEDIA] File downloader now in PHP, added proper name in the UI and changed the format for new attachment file names
The file downloader was changed from a simple redirect to the file to one
implemented in PHP, which should make it safer, by making it possible disallow
direct access to the file, to prevent executing of atttachments
The filename has a new format:
bin2hex("{$original_name}")."-{$filehash}"
This format should be respected. Notice the dash, which is important to distinguish it from the previous
format, which was "{$hash}.{$ext}"
This change was made to both make the experience more user friendly, by
providing a readable name for files, as opposed to it's hash. This name is taken
from the upload filename, but, clearly, as this wasn't done before, it's
impossible to have a proper name for older files, so those are displayed as
"untitled.{$ext}".
This new name is displayed in the UI, instead of the previous name.
2019-06-11 02:42:33 +01:00
|
|
|
|
2019-08-17 02:33:31 +01:00
|
|
|
if ($media == 'image') {
|
[MEDIA] File downloader now in PHP, added proper name in the UI and changed the format for new attachment file names
The file downloader was changed from a simple redirect to the file to one
implemented in PHP, which should make it safer, by making it possible disallow
direct access to the file, to prevent executing of atttachments
The filename has a new format:
bin2hex("{$original_name}")."-{$filehash}"
This format should be respected. Notice the dash, which is important to distinguish it from the previous
format, which was "{$hash}.{$ext}"
This change was made to both make the experience more user friendly, by
providing a readable name for files, as opposed to it's hash. This name is taken
from the upload filename, but, clearly, as this wasn't done before, it's
impossible to have a proper name for older files, so those are displayed as
"untitled.{$ext}".
This new name is displayed in the UI, instead of the previous name.
2019-06-11 02:42:33 +01:00
|
|
|
// Use -1 for the id to avoid adding this temporary file to the DB
|
|
|
|
$img = new ImageFile(-1, $_FILES[$param]['tmp_name']);
|
2020-08-13 18:37:31 +01:00
|
|
|
// Validate the image by re-encoding it. Additionally normalizes old formats to WebP,
|
|
|
|
// keeping GIF untouched if animated
|
[MEDIA] File downloader now in PHP, added proper name in the UI and changed the format for new attachment file names
The file downloader was changed from a simple redirect to the file to one
implemented in PHP, which should make it safer, by making it possible disallow
direct access to the file, to prevent executing of atttachments
The filename has a new format:
bin2hex("{$original_name}")."-{$filehash}"
This format should be respected. Notice the dash, which is important to distinguish it from the previous
format, which was "{$hash}.{$ext}"
This change was made to both make the experience more user friendly, by
providing a readable name for files, as opposed to it's hash. This name is taken
from the upload filename, but, clearly, as this wasn't done before, it's
impossible to have a proper name for older files, so those are displayed as
"untitled.{$ext}".
This new name is displayed in the UI, instead of the previous name.
2019-06-11 02:42:33 +01:00
|
|
|
$outpath = $img->resizeTo($img->filepath);
|
|
|
|
$ext = image_type_to_extension($img->preferredType(), false);
|
|
|
|
}
|
2019-08-17 02:33:31 +01:00
|
|
|
$filename = self::encodeFilename($basename, $filehash, isset($ext) ? $ext : File::getSafeExtension($basename));
|
[MEDIA] File downloader now in PHP, added proper name in the UI and changed the format for new attachment file names
The file downloader was changed from a simple redirect to the file to one
implemented in PHP, which should make it safer, by making it possible disallow
direct access to the file, to prevent executing of atttachments
The filename has a new format:
bin2hex("{$original_name}")."-{$filehash}"
This format should be respected. Notice the dash, which is important to distinguish it from the previous
format, which was "{$hash}.{$ext}"
This change was made to both make the experience more user friendly, by
providing a readable name for files, as opposed to it's hash. This name is taken
from the upload filename, but, clearly, as this wasn't done before, it's
impossible to have a proper name for older files, so those are displayed as
"untitled.{$ext}".
This new name is displayed in the UI, instead of the previous name.
2019-06-11 02:42:33 +01:00
|
|
|
|
2015-02-24 20:11:25 +00:00
|
|
|
$filepath = File::path($filename);
|
|
|
|
|
2019-08-17 02:33:31 +01:00
|
|
|
if ($media == 'image') {
|
[MEDIA] File downloader now in PHP, added proper name in the UI and changed the format for new attachment file names
The file downloader was changed from a simple redirect to the file to one
implemented in PHP, which should make it safer, by making it possible disallow
direct access to the file, to prevent executing of atttachments
The filename has a new format:
bin2hex("{$original_name}")."-{$filehash}"
This format should be respected. Notice the dash, which is important to distinguish it from the previous
format, which was "{$hash}.{$ext}"
This change was made to both make the experience more user friendly, by
providing a readable name for files, as opposed to it's hash. This name is taken
from the upload filename, but, clearly, as this wasn't done before, it's
impossible to have a proper name for older files, so those are displayed as
"untitled.{$ext}".
This new name is displayed in the UI, instead of the previous name.
2019-06-11 02:42:33 +01:00
|
|
|
$result = rename($outpath, $filepath);
|
|
|
|
} else {
|
|
|
|
$result = move_uploaded_file($_FILES[$param]['tmp_name'], $filepath);
|
|
|
|
}
|
2015-02-24 20:11:25 +00:00
|
|
|
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.
|
2019-06-07 14:08:27 +01:00
|
|
|
// UX: too specific
|
2019-08-17 02:33:31 +01:00
|
|
|
throw new ClientException(_m('File could not be moved to destination directory.'));
|
2015-02-24 20:11:25 +00:00
|
|
|
}
|
2009-10-28 04:45:56 +00:00
|
|
|
|
2019-08-17 02:33:31 +01:00
|
|
|
if ($media == 'image') {
|
2020-09-21 21:54:23 +01:00
|
|
|
return new ImageFile(null, $filepath, $filehash);
|
2019-06-07 14:08:27 +01:00
|
|
|
}
|
|
|
|
}
|
2021-02-18 17:49:10 +00:00
|
|
|
return new self($filepath, $mimetype, $filehash, $fileid);
|
2009-10-28 02:11:18 +00:00
|
|
|
}
|
|
|
|
|
2020-07-05 01:25:11 +01:00
|
|
|
/**
|
|
|
|
* Create a new MediaFile or ImageFile object from an url
|
|
|
|
*
|
|
|
|
* Tries to set the mimetype correctly, using the most secure method available and rejects the file otherwise.
|
|
|
|
* In case the url is an image, this function returns an new ImageFile (which extends MediaFile)
|
|
|
|
* The filename has the following format: bin2hex("{$original_name}.{$ext}")."-{$filehash}"
|
|
|
|
*
|
|
|
|
* @param string $url Remote media URL
|
|
|
|
* @param Profile|null $scoped
|
2020-08-05 17:51:43 +01:00
|
|
|
* @param string|null $name
|
2021-02-16 18:30:21 +00:00
|
|
|
* @param int|null $file_id same as in this class constructor
|
2020-07-05 01:25:11 +01:00
|
|
|
* @return ImageFile|MediaFile
|
2020-08-05 17:51:43 +01:00
|
|
|
* @throws ClientException
|
2021-02-16 18:30:21 +00:00
|
|
|
* @throws HTTP_Request2_Exception
|
2020-08-05 17:51:43 +01:00
|
|
|
* @throws InvalidFilenameException
|
|
|
|
* @throws NoResultException
|
2020-07-05 01:25:11 +01:00
|
|
|
* @throws ServerException
|
2020-08-05 17:51:43 +01:00
|
|
|
* @throws UnsupportedMediaException
|
|
|
|
* @throws UseFileAsThumbnailException
|
2020-07-05 01:25:11 +01:00
|
|
|
*/
|
2021-02-16 18:30:21 +00:00
|
|
|
public static function fromUrl(string $url, ?Profile $scoped = null, ?string $name = null, ?int $file_id = null)
|
2020-07-05 01:25:11 +01:00
|
|
|
{
|
|
|
|
if (!common_valid_http_url($url)) {
|
|
|
|
// TRANS: Server exception. %s is a URL.
|
|
|
|
throw new ServerException(sprintf('Invalid remote media URL %s.', $url));
|
|
|
|
}
|
|
|
|
|
2020-09-21 21:54:23 +01:00
|
|
|
$http = new HTTPClient();
|
|
|
|
common_debug(sprintf('Performing HEAD request for incoming activity to avoid ' .
|
|
|
|
'unnecessarily downloading too large files. URL: %s',
|
|
|
|
$url));
|
|
|
|
$head = $http->head($url);
|
|
|
|
$url = $head->getEffectiveUrl(); // to avoid going through redirects again
|
|
|
|
if (empty($url)) {
|
|
|
|
throw new ServerException(sprintf('URL after redirects is somehow empty, for URL %s.', $url));
|
|
|
|
}
|
|
|
|
$headers = $head->getHeader();
|
|
|
|
$headers = array_change_key_case($headers, CASE_LOWER);
|
|
|
|
if (array_key_exists('content-length', $headers)) {
|
|
|
|
$fileQuota = common_config('attachments', 'file_quota');
|
|
|
|
$fileSize = $headers['content-length'];
|
|
|
|
if ($fileSize > $fileQuota) {
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
// TRANS: Message given if an upload is larger than the configured maximum.
|
|
|
|
// TRANS: %1$d (used for plural) is the byte limit for uploads,
|
|
|
|
// 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
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new ServerException(sprintf('Invalid remote media URL headers %s.', $url));
|
|
|
|
}
|
|
|
|
unset($head);
|
|
|
|
unset($headers);
|
|
|
|
|
2020-09-04 11:15:23 +01:00
|
|
|
$tempfile = new TemporaryFile('gs-mediafile');
|
|
|
|
fwrite($tempfile->getResource(), HTTPClient::quickGet($url));
|
|
|
|
fflush($tempfile->getResource());
|
|
|
|
|
|
|
|
$filehash = strtolower(self::getHashOfFile($tempfile->getRealPath()));
|
2020-07-05 01:25:11 +01:00
|
|
|
|
|
|
|
try {
|
2020-09-21 21:54:23 +01:00
|
|
|
$file = File::getByUrl($url);
|
2020-09-04 11:15:23 +01:00
|
|
|
/*
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
$filepath = $file->getPath();
|
|
|
|
$mimetype = $file->mimetype;
|
|
|
|
} catch (FileNotFoundException | NoResultException $e) {
|
|
|
|
// We have to save the downloaded 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 something new is uploaded.
|
|
|
|
File::respectsQuota($scoped, filesize($tempfile->getRealPath()));
|
|
|
|
}
|
2020-07-05 01:25:11 +01:00
|
|
|
|
2020-09-04 11:15:23 +01:00
|
|
|
$mimetype = self::getUploadedMimeType(
|
|
|
|
$tempfile->getRealPath(),
|
|
|
|
$name ?? false
|
|
|
|
);
|
|
|
|
$media = common_get_mime_media($mimetype);
|
2020-07-05 01:25:11 +01:00
|
|
|
|
2020-09-04 11:15:23 +01:00
|
|
|
$basename = basename($name ?? ('media' . common_timestamp()));
|
2020-07-05 01:25:11 +01:00
|
|
|
|
2020-09-04 11:15:23 +01:00
|
|
|
if ($media === 'image') {
|
|
|
|
// Use -1 for the id to avoid adding this temporary file to the DB.
|
|
|
|
$img = new ImageFile(-1, $tempfile->getRealPath());
|
|
|
|
// Validate the image by re-encoding it.
|
|
|
|
// Additionally normalises old formats to PNG,
|
|
|
|
// keeping JPEG and GIF untouched.
|
|
|
|
$outpath = $img->resizeTo($img->filepath);
|
|
|
|
$ext = image_type_to_extension($img->preferredType(), false);
|
|
|
|
}
|
|
|
|
$filename = self::encodeFilename(
|
|
|
|
$basename,
|
|
|
|
$filehash,
|
|
|
|
$ext ?? File::getSafeExtension($basename)
|
|
|
|
);
|
2020-07-05 01:25:11 +01:00
|
|
|
|
2020-09-04 11:15:23 +01:00
|
|
|
$filepath = File::path($filename);
|
2020-07-05 01:25:11 +01:00
|
|
|
|
2020-09-04 11:15:23 +01:00
|
|
|
if ($media === 'image') {
|
|
|
|
$result = rename($outpath, $filepath);
|
|
|
|
} else {
|
2020-09-14 18:37:48 +01:00
|
|
|
try {
|
|
|
|
$tempfile->commit($filepath);
|
|
|
|
$result = true;
|
|
|
|
} catch (TemporaryFileException $e) {
|
|
|
|
$result = false;
|
|
|
|
}
|
2020-09-04 11:15:23 +01:00
|
|
|
}
|
|
|
|
if (!$result) {
|
|
|
|
// TRANS: Server exception thrown when a file upload operation fails because the file could
|
|
|
|
// TRANS: not be moved from the temporary directory to the permanent file location.
|
|
|
|
throw new ServerException(_m('File could not be moved to destination directory.'));
|
|
|
|
}
|
2020-07-05 01:25:11 +01:00
|
|
|
|
2020-09-04 11:15:23 +01:00
|
|
|
if ($media === 'image') {
|
2021-02-16 18:30:21 +00:00
|
|
|
return new ImageFile($file_id, $filepath, $filehash, $url);
|
2020-07-05 01:25:11 +01:00
|
|
|
}
|
|
|
|
}
|
2021-02-16 18:30:21 +00:00
|
|
|
return new self($filepath, $mimetype, $filehash, $file_id, $url);
|
2020-07-05 01:25:11 +01:00
|
|
|
}
|
|
|
|
|
2020-09-04 11:15:23 +01:00
|
|
|
public static function fromFileInfo(SplFileInfo $finfo, Profile $scoped = null)
|
2019-06-09 23:26:48 +01:00
|
|
|
{
|
2020-09-04 11:15:23 +01:00
|
|
|
$filehash = hash_file(File::FILEHASH_ALG, $finfo->getRealPath());
|
2009-10-28 02:11:18 +00:00
|
|
|
|
2015-02-24 20:11:25 +00:00
|
|
|
try {
|
|
|
|
$file = File::getByHash($filehash);
|
2020-09-21 21:54:23 +01:00
|
|
|
$file->fetch();
|
2015-02-24 20:11:25 +00:00
|
|
|
// Already have it, so let's reuse the locally stored File
|
2016-03-05 00:26:34 +00:00
|
|
|
// by using getPath we also check whether the file exists
|
|
|
|
// and throw a FileNotFoundException with the path if it doesn't.
|
|
|
|
$filename = basename($file->getPath());
|
2015-02-24 20:11:25 +00:00
|
|
|
$mimetype = $file->mimetype;
|
2016-03-05 00:26:34 +00:00
|
|
|
} catch (FileNotFoundException $e) {
|
|
|
|
// This happens if the file we have uploaded has disappeared
|
|
|
|
// from the local filesystem for some reason. Since we got the
|
2020-09-04 11:15:23 +01:00
|
|
|
// File object from a sha256 check in fromFileInfo, it's safe
|
2016-03-05 00:26:34 +00:00
|
|
|
// to just copy the uploaded data to disk!
|
|
|
|
|
|
|
|
// dump the contents of our filehandle to the path from our exception
|
|
|
|
// and report error if it failed.
|
2020-09-04 11:15:23 +01:00
|
|
|
if (file_put_contents($e->path, file_get_contents($finfo->getRealPath())) === false) {
|
2016-03-05 00:26:34 +00:00
|
|
|
// 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.
|
2019-08-17 02:33:31 +01:00
|
|
|
throw new ClientException(_m('File could not be moved to destination directory.'));
|
2016-03-05 00:26:34 +00:00
|
|
|
}
|
|
|
|
if (!chmod($e->path, 0664)) {
|
2020-06-20 13:49:37 +01:00
|
|
|
common_log(LOG_ERR, 'Could not chmod uploaded file: ' . _ve($e->path));
|
2016-03-05 00:26:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
$filename = basename($file->getPath());
|
|
|
|
$mimetype = $file->mimetype;
|
2015-02-24 20:11:25 +00:00
|
|
|
} catch (NoResultException $e) {
|
2016-03-05 10:59:46 +00:00
|
|
|
if ($scoped instanceof Profile) {
|
2020-09-04 11:15:23 +01:00
|
|
|
File::respectsQuota($scoped, filesize($finfo->getRealPath()));
|
2016-03-05 10:59:46 +00:00
|
|
|
}
|
2009-10-28 04:45:56 +00:00
|
|
|
|
2020-09-04 11:15:23 +01:00
|
|
|
$mimetype = self::getUploadedMimeType($finfo->getRealPath());
|
2009-10-28 02:11:18 +00:00
|
|
|
|
2016-03-05 10:59:46 +00:00
|
|
|
$filename = strtolower($filehash) . '.' . File::guessMimeExtension($mimetype);
|
2015-02-24 20:11:25 +00:00
|
|
|
$filepath = File::path($filename);
|
2009-10-28 04:45:56 +00:00
|
|
|
|
2020-09-04 11:15:23 +01:00
|
|
|
$result = copy($finfo->getRealPath(), $filepath) && chmod($filepath, 0664);
|
2009-10-28 04:45:56 +00:00
|
|
|
|
2015-02-24 20:11:25 +00:00
|
|
|
if (!$result) {
|
2020-06-20 13:49:37 +01:00
|
|
|
common_log(LOG_ERR, 'File could not be moved (or chmodded) from ' . _ve($stream['uri']) . ' to ' . _ve($filepath));
|
2015-02-24 20:11:25 +00:00
|
|
|
// 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.
|
2019-08-17 02:33:31 +01:00
|
|
|
throw new ClientException(_m('File could not be moved to destination directory.'));
|
2015-02-24 20:11:25 +00:00
|
|
|
}
|
2009-10-28 02:11:18 +00:00
|
|
|
}
|
2009-10-28 04:45:56 +00:00
|
|
|
|
2020-06-20 13:49:37 +01:00
|
|
|
return new self($filename, $mimetype, $filehash);
|
2009-10-28 02:11:18 +00:00
|
|
|
}
|
|
|
|
|
2010-05-11 00:18:29 +01:00
|
|
|
/**
|
|
|
|
* Attempt to identify the content type of a given file.
|
2019-06-07 14:08:27 +01:00
|
|
|
*
|
2014-03-08 02:03:04 +00:00
|
|
|
* @param string $filepath filesystem path as string (file must exist)
|
2019-06-07 14:08:27 +01:00
|
|
|
* @param bool $originalFilename (optional) for extension-based detection
|
2020-06-20 13:49:37 +01:00
|
|
|
*
|
2010-05-11 00:18:29 +01:00
|
|
|
* @return string
|
2019-06-07 14:08:27 +01:00
|
|
|
*
|
|
|
|
* @fixme this seems to tie a front-end error message in, kinda confusing
|
|
|
|
*
|
2020-06-20 13:49:37 +01:00
|
|
|
* @throws ServerException
|
|
|
|
*
|
|
|
|
* @throws ClientException if type is known, but not supported for local uploads
|
2010-05-11 00:18:29 +01:00
|
|
|
*/
|
2020-06-20 13:49:37 +01:00
|
|
|
public static function getUploadedMimeType(string $filepath, $originalFilename = false)
|
2019-06-09 23:26:48 +01:00
|
|
|
{
|
2014-03-08 00:42:24 +00:00
|
|
|
// We only accept filenames to existing files
|
2019-06-07 14:08:27 +01:00
|
|
|
|
|
|
|
$mimetype = null;
|
|
|
|
|
|
|
|
// From CodeIgniter
|
|
|
|
// We'll need this to validate the MIME info string (e.g. text/plain; charset=us-ascii)
|
2019-07-12 22:22:51 +01:00
|
|
|
$regexp = '/^([a-z\-]+\/[a-z0-9\-\.\+]+)(;\s[^\/]+)?$/';
|
2019-06-07 14:08:27 +01:00
|
|
|
/**
|
|
|
|
* 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 ...
|
|
|
|
*/
|
2019-06-09 23:26:48 +01:00
|
|
|
if (function_exists('finfo_file')) {
|
2019-06-07 14:08:27 +01:00
|
|
|
$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
|
2019-06-09 23:26:48 +01:00
|
|
|
if (is_resource($finfo)) {
|
2019-06-07 14:08:27 +01:00
|
|
|
$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)
|
|
|
|
*/
|
2019-06-09 23:26:48 +01:00
|
|
|
if (is_string($mime) && preg_match($regexp, $mime, $matches)) {
|
2019-06-07 14:08:27 +01:00
|
|
|
$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 !== '\\') {
|
2020-06-20 13:49:37 +01:00
|
|
|
$cmd = 'file --brief --mime ' . escapeshellarg($filepath) . ' 2>&1';
|
2019-07-12 22:22:51 +01:00
|
|
|
if (empty($mimetype) && function_exists('exec')) {
|
2019-06-07 14:08:27 +01:00
|
|
|
/* 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];
|
|
|
|
}
|
|
|
|
}
|
2019-07-12 22:22:51 +01:00
|
|
|
if (empty($mimetype) && function_exists('shell_exec')) {
|
2019-06-07 14:08:27 +01:00
|
|
|
$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];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-07-12 22:22:51 +01:00
|
|
|
if (empty($mimetype) && function_exists('popen')) {
|
2019-06-07 14:08:27 +01:00
|
|
|
$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'])
|
2019-07-12 22:22:51 +01:00
|
|
|
if (empty($mimetype) && function_exists('mime_content_type')) {
|
2019-06-07 14:08:27 +01:00
|
|
|
$mimetype = @mime_content_type($filepath);
|
|
|
|
// It's possible that mime_content_type() returns FALSE or an empty string
|
2019-06-09 23:26:48 +01:00
|
|
|
if ($mimetype == false && strlen($mimetype) > 0) {
|
2019-06-07 14:08:27 +01:00
|
|
|
throw new ServerException(_m('Could not determine file\'s MIME type.'));
|
|
|
|
}
|
|
|
|
}
|
2009-10-28 04:45:56 +00:00
|
|
|
|
2014-03-08 02:03:04 +00:00
|
|
|
// Unclear types are such that we can't really tell by the auto
|
|
|
|
// detect what they are (.bin, .exe etc. are just "octet-stream")
|
2020-06-20 13:49:37 +01:00
|
|
|
$unclearTypes = ['application/octet-stream',
|
|
|
|
'application/vnd.ms-office',
|
|
|
|
'application/zip',
|
|
|
|
'text/plain',
|
|
|
|
'text/html', // Ironically, Wikimedia Commons' SVG_logo.svg is identified as text/html
|
|
|
|
// TODO: for XML we could do better content-based sniffing too
|
|
|
|
'text/xml',];
|
2010-05-11 00:18:29 +01:00
|
|
|
|
2014-03-08 02:03:04 +00:00
|
|
|
$supported = common_config('attachments', 'supported');
|
|
|
|
|
|
|
|
// If we didn't match, or it is an unclear match
|
|
|
|
if ($originalFilename && (!$mimetype || in_array($mimetype, $unclearTypes))) {
|
2014-03-08 02:51:47 +00:00
|
|
|
try {
|
2016-07-06 08:34:09 +01:00
|
|
|
$type = common_supported_filename_to_mime($originalFilename);
|
2014-03-08 02:03:04 +00:00
|
|
|
return $type;
|
2016-07-06 08:34:09 +01:00
|
|
|
} catch (UnknownExtensionMimeException $e) {
|
|
|
|
// FIXME: I think we should keep the file extension here (supported should be === true here)
|
2014-03-08 02:51:47 +00:00
|
|
|
} catch (Exception $e) {
|
2016-07-06 08:34:09 +01:00
|
|
|
// Extension parsed but no connected mimetype, so $mimetype is our best guess
|
2010-05-11 00:18:29 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-03-08 02:03:04 +00:00
|
|
|
// If $config['attachments']['supported'] equals boolean true, accept any mimetype
|
|
|
|
if ($supported === true || array_key_exists($mimetype, $supported)) {
|
|
|
|
// FIXME: Don't know if it always has a mimetype here because
|
|
|
|
// finfo->file CAN return false on error: http://php.net/finfo_file
|
|
|
|
// so if $supported === true, this may return something unexpected.
|
|
|
|
return $mimetype;
|
2009-10-28 02:11:18 +00:00
|
|
|
}
|
2014-03-08 02:03:04 +00:00
|
|
|
|
|
|
|
// We can conclude that we have failed to get the MIME type
|
|
|
|
$media = common_get_mime_media($mimetype);
|
2009-10-28 02:11:18 +00:00
|
|
|
if ('application' !== $media) {
|
2010-09-12 17:11:28 +01:00
|
|
|
// TRANS: Client exception thrown trying to upload a forbidden MIME type.
|
|
|
|
// TRANS: %1$s is the file type that was denied, %2$s is the application part of
|
|
|
|
// TRANS: the MIME type that was denied.
|
2019-08-17 02:33:31 +01:00
|
|
|
$hint = sprintf(_m('"%1$s" is not a supported file type on this server. ' .
|
2020-06-20 13:49:37 +01:00
|
|
|
'Try using another %2$s format.'), $mimetype, $media);
|
2009-10-28 02:11:18 +00:00
|
|
|
} else {
|
2010-09-12 17:11:28 +01:00
|
|
|
// TRANS: Client exception thrown trying to upload a forbidden MIME type.
|
|
|
|
// TRANS: %s is the file type that was denied.
|
2019-08-17 02:33:31 +01:00
|
|
|
$hint = sprintf(_m('"%s" is not a supported file type on this server.'), $mimetype);
|
2009-10-28 02:11:18 +00:00
|
|
|
}
|
2010-09-12 17:11:28 +01:00
|
|
|
throw new ClientException($hint);
|
2009-10-28 02:11:18 +00:00
|
|
|
}
|
[MEDIA] File downloader now in PHP, added proper name in the UI and changed the format for new attachment file names
The file downloader was changed from a simple redirect to the file to one
implemented in PHP, which should make it safer, by making it possible disallow
direct access to the file, to prevent executing of atttachments
The filename has a new format:
bin2hex("{$original_name}")."-{$filehash}"
This format should be respected. Notice the dash, which is important to distinguish it from the previous
format, which was "{$hash}.{$ext}"
This change was made to both make the experience more user friendly, by
providing a readable name for files, as opposed to it's hash. This name is taken
from the upload filename, but, clearly, as this wasn't done before, it's
impossible to have a proper name for older files, so those are displayed as
"untitled.{$ext}".
This new name is displayed in the UI, instead of the previous name.
2019-06-11 02:42:33 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Title for a file, to display in the interface (if there's no better title) and
|
|
|
|
* for download filenames
|
2020-06-20 13:49:37 +01:00
|
|
|
*
|
[MEDIA] File downloader now in PHP, added proper name in the UI and changed the format for new attachment file names
The file downloader was changed from a simple redirect to the file to one
implemented in PHP, which should make it safer, by making it possible disallow
direct access to the file, to prevent executing of atttachments
The filename has a new format:
bin2hex("{$original_name}")."-{$filehash}"
This format should be respected. Notice the dash, which is important to distinguish it from the previous
format, which was "{$hash}.{$ext}"
This change was made to both make the experience more user friendly, by
providing a readable name for files, as opposed to it's hash. This name is taken
from the upload filename, but, clearly, as this wasn't done before, it's
impossible to have a proper name for older files, so those are displayed as
"untitled.{$ext}".
This new name is displayed in the UI, instead of the previous name.
2019-06-11 02:42:33 +01:00
|
|
|
* @param $file File object
|
|
|
|
* @returns string
|
|
|
|
*/
|
2020-06-20 13:49:37 +01:00
|
|
|
public static function getDisplayName(File $file): string
|
|
|
|
{
|
2019-06-28 00:18:27 +01:00
|
|
|
if (empty($file->filename)) {
|
2019-08-17 02:33:31 +01:00
|
|
|
return _m('Untitled attachment');
|
2019-06-28 00:18:27 +01:00
|
|
|
}
|
|
|
|
|
[MEDIA] File downloader now in PHP, added proper name in the UI and changed the format for new attachment file names
The file downloader was changed from a simple redirect to the file to one
implemented in PHP, which should make it safer, by making it possible disallow
direct access to the file, to prevent executing of atttachments
The filename has a new format:
bin2hex("{$original_name}")."-{$filehash}"
This format should be respected. Notice the dash, which is important to distinguish it from the previous
format, which was "{$hash}.{$ext}"
This change was made to both make the experience more user friendly, by
providing a readable name for files, as opposed to it's hash. This name is taken
from the upload filename, but, clearly, as this wasn't done before, it's
impossible to have a proper name for older files, so those are displayed as
"untitled.{$ext}".
This new name is displayed in the UI, instead of the previous name.
2019-06-11 02:42:33 +01:00
|
|
|
// New file name format is "{bin2hex(original_name.ext)}-{$hash}"
|
2019-06-30 13:36:33 +01:00
|
|
|
$filename = self::decodeFilename($file->filename);
|
|
|
|
|
[MEDIA] File downloader now in PHP, added proper name in the UI and changed the format for new attachment file names
The file downloader was changed from a simple redirect to the file to one
implemented in PHP, which should make it safer, by making it possible disallow
direct access to the file, to prevent executing of atttachments
The filename has a new format:
bin2hex("{$original_name}")."-{$filehash}"
This format should be respected. Notice the dash, which is important to distinguish it from the previous
format, which was "{$hash}.{$ext}"
This change was made to both make the experience more user friendly, by
providing a readable name for files, as opposed to it's hash. This name is taken
from the upload filename, but, clearly, as this wasn't done before, it's
impossible to have a proper name for older files, so those are displayed as
"untitled.{$ext}".
This new name is displayed in the UI, instead of the previous name.
2019-06-11 02:42:33 +01:00
|
|
|
// If there was an error in the match, something's wrong with some piece
|
|
|
|
// of code (could be a file with utf8 chars in the name)
|
2019-06-16 00:33:12 +01:00
|
|
|
$log_error_msg = "Invalid file name for File with id={$file->id} " .
|
2020-06-20 13:49:37 +01:00
|
|
|
"({$file->filename}). Some plugin probably did something wrong.";
|
2019-06-30 13:36:33 +01:00
|
|
|
if ($filename === false) {
|
[MEDIA] File downloader now in PHP, added proper name in the UI and changed the format for new attachment file names
The file downloader was changed from a simple redirect to the file to one
implemented in PHP, which should make it safer, by making it possible disallow
direct access to the file, to prevent executing of atttachments
The filename has a new format:
bin2hex("{$original_name}")."-{$filehash}"
This format should be respected. Notice the dash, which is important to distinguish it from the previous
format, which was "{$hash}.{$ext}"
This change was made to both make the experience more user friendly, by
providing a readable name for files, as opposed to it's hash. This name is taken
from the upload filename, but, clearly, as this wasn't done before, it's
impossible to have a proper name for older files, so those are displayed as
"untitled.{$ext}".
This new name is displayed in the UI, instead of the previous name.
2019-06-11 02:42:33 +01:00
|
|
|
common_log(LOG_ERR, $log_error_msg);
|
2019-06-30 13:36:33 +01:00
|
|
|
} elseif ($filename === null) {
|
|
|
|
// The old file name format was "{hash}.{ext}" so we didn't have a name
|
|
|
|
// This extracts the extension
|
2019-07-11 23:49:16 +01:00
|
|
|
$ret = preg_match('/^.+?\.+?(.+)$/', $file->filename, $matches);
|
[MEDIA] File downloader now in PHP, added proper name in the UI and changed the format for new attachment file names
The file downloader was changed from a simple redirect to the file to one
implemented in PHP, which should make it safer, by making it possible disallow
direct access to the file, to prevent executing of atttachments
The filename has a new format:
bin2hex("{$original_name}")."-{$filehash}"
This format should be respected. Notice the dash, which is important to distinguish it from the previous
format, which was "{$hash}.{$ext}"
This change was made to both make the experience more user friendly, by
providing a readable name for files, as opposed to it's hash. This name is taken
from the upload filename, but, clearly, as this wasn't done before, it's
impossible to have a proper name for older files, so those are displayed as
"untitled.{$ext}".
This new name is displayed in the UI, instead of the previous name.
2019-06-11 02:42:33 +01:00
|
|
|
if ($ret !== 1) {
|
|
|
|
common_log(LOG_ERR, $log_error_msg);
|
2019-08-17 02:33:31 +01:00
|
|
|
return _m('Untitled attachment');
|
[MEDIA] File downloader now in PHP, added proper name in the UI and changed the format for new attachment file names
The file downloader was changed from a simple redirect to the file to one
implemented in PHP, which should make it safer, by making it possible disallow
direct access to the file, to prevent executing of atttachments
The filename has a new format:
bin2hex("{$original_name}")."-{$filehash}"
This format should be respected. Notice the dash, which is important to distinguish it from the previous
format, which was "{$hash}.{$ext}"
This change was made to both make the experience more user friendly, by
providing a readable name for files, as opposed to it's hash. This name is taken
from the upload filename, but, clearly, as this wasn't done before, it's
impossible to have a proper name for older files, so those are displayed as
"untitled.{$ext}".
This new name is displayed in the UI, instead of the previous name.
2019-06-11 02:42:33 +01:00
|
|
|
}
|
|
|
|
$ext = $matches[1];
|
2019-06-30 13:36:33 +01:00
|
|
|
// There's a blacklisted extension array, which could have an alternative
|
[MEDIA] File downloader now in PHP, added proper name in the UI and changed the format for new attachment file names
The file downloader was changed from a simple redirect to the file to one
implemented in PHP, which should make it safer, by making it possible disallow
direct access to the file, to prevent executing of atttachments
The filename has a new format:
bin2hex("{$original_name}")."-{$filehash}"
This format should be respected. Notice the dash, which is important to distinguish it from the previous
format, which was "{$hash}.{$ext}"
This change was made to both make the experience more user friendly, by
providing a readable name for files, as opposed to it's hash. This name is taken
from the upload filename, but, clearly, as this wasn't done before, it's
impossible to have a proper name for older files, so those are displayed as
"untitled.{$ext}".
This new name is displayed in the UI, instead of the previous name.
2019-06-11 02:42:33 +01:00
|
|
|
// extension, such as phps, to replace php. We want to turn it back
|
2019-06-30 13:36:33 +01:00
|
|
|
// (currently defaulted to empty, but let's keep the feature)
|
[MEDIA] File downloader now in PHP, added proper name in the UI and changed the format for new attachment file names
The file downloader was changed from a simple redirect to the file to one
implemented in PHP, which should make it safer, by making it possible disallow
direct access to the file, to prevent executing of atttachments
The filename has a new format:
bin2hex("{$original_name}")."-{$filehash}"
This format should be respected. Notice the dash, which is important to distinguish it from the previous
format, which was "{$hash}.{$ext}"
This change was made to both make the experience more user friendly, by
providing a readable name for files, as opposed to it's hash. This name is taken
from the upload filename, but, clearly, as this wasn't done before, it's
impossible to have a proper name for older files, so those are displayed as
"untitled.{$ext}".
This new name is displayed in the UI, instead of the previous name.
2019-06-11 02:42:33 +01:00
|
|
|
$blacklist = common_config('attachments', 'extblacklist');
|
|
|
|
if (is_array($blacklist)) {
|
|
|
|
foreach ($blacklist as $upload_ext => $safe_ext) {
|
|
|
|
if ($ext === $safe_ext) {
|
|
|
|
$ext = $upload_ext;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$filename = "untitled.{$ext}";
|
|
|
|
}
|
|
|
|
return $filename;
|
|
|
|
}
|
2010-01-10 11:26:24 +00:00
|
|
|
}
|