[Media] Fix issues with database file storage
Fixed file quota as well. There can be more than one file for the same filehash IF the url are different. Possible states: - A file with no url and with filename is a local file. - A file with an url but no filename is a remote file that wasn't fetched, not even the thumbnail. - A file with an url and filename is a fetched remote file (maybe just a thumbnail of it). - A file with no filename nor url is a redirect. Routes: Given these states, updated routes so that an attachment can only be retrieved by id and a file by filehash. Major API changes: File::getByHash now returns a yield of files Major UI changes: - Now remote non stored files are presented. - /view became preferred - Redirects to remote originals are preferred. Many other minor bug fixes...
This commit is contained in:
parent
11fa4b617a
commit
8bbbb890e3
@ -20,10 +20,10 @@ defined('GNUSOCIAL') || die();
|
||||
* Show notice attachments
|
||||
*
|
||||
* @category Personal
|
||||
* @package StatusNet
|
||||
* @package GNUsocial
|
||||
* @author Evan Prodromou <evan@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/
|
||||
* @copyright 2008-2009 StatusNet, Inc.
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class AttachmentAction extends ManagedAction
|
||||
{
|
||||
@ -58,9 +58,11 @@ class AttachmentAction extends ManagedAction
|
||||
|
||||
try {
|
||||
if (!empty($id = $this->trimmed('attachment'))) {
|
||||
$this->attachment = File::getByID($id);
|
||||
$this->attachment = File::getByID((int) $id);
|
||||
} elseif (!empty($this->filehash = $this->trimmed('filehash'))) {
|
||||
$this->attachment = File::getByHash($this->filehash);
|
||||
$file = File::getByHash($this->filehash);
|
||||
$file->fetch();
|
||||
$this->attachment = $file;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Not found
|
||||
@ -70,13 +72,22 @@ class AttachmentAction extends ManagedAction
|
||||
$this->clientError(_m('No such attachment.'), 404);
|
||||
}
|
||||
|
||||
$this->filesize = $this->attachment->size;
|
||||
$this->mimetype = $this->attachment->mimetype;
|
||||
$this->filename = $this->attachment->filename;
|
||||
|
||||
if ($this->attachment->isLocal()) {
|
||||
$this->filepath = $this->attachment->getFileOrThumbnailPath();
|
||||
if (empty($this->filepath)) {
|
||||
$this->clientError(_m('Requested local URL for a file that is not stored locally.'), 404);
|
||||
$this->clientError(
|
||||
_m('Requested local URL for a file that is not stored locally.'),
|
||||
404
|
||||
);
|
||||
}
|
||||
$this->filesize = $this->attachment->getFileOrThumbnailSize();
|
||||
$this->mimetype = $this->attachment->getFileOrThumbnailMimetype();
|
||||
$this->filename = MediaFile::getDisplayName($this->attachment);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -104,8 +115,12 @@ class AttachmentAction extends ManagedAction
|
||||
|
||||
public function showPage(): void
|
||||
{
|
||||
if (empty($this->filepath)) {
|
||||
// if it's not a local file, gtfo
|
||||
if (
|
||||
!$this->attachment->isLocal()
|
||||
|| empty($this->filepath)
|
||||
|| !file_exists($this->filepath)
|
||||
) {
|
||||
// If it's not a locally stored file, get lost
|
||||
common_redirect($this->attachment->getUrl(), 303);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,20 @@
|
||||
<?php
|
||||
// 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/>.
|
||||
|
||||
if (!defined('GNUSOCIAL')) { exit(1); }
|
||||
defined('GNUSOCIAL') || die();
|
||||
|
||||
/**
|
||||
* Download notice attachment
|
||||
@ -8,8 +22,8 @@ if (!defined('GNUSOCIAL')) { exit(1); }
|
||||
* @category Personal
|
||||
* @package GNUsocial
|
||||
* @author Mikael Nordfeldth <mmn@hethane.se>
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link https:/gnu.io/social
|
||||
* @copyright 2016 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or late
|
||||
*/
|
||||
class Attachment_downloadAction extends AttachmentAction
|
||||
{
|
||||
@ -20,6 +34,15 @@ class Attachment_downloadAction extends AttachmentAction
|
||||
// script execution, and we don't want to have any more errors until then, so don't reset it
|
||||
@ini_set('display_errors', 0);
|
||||
|
||||
common_send_file($this->filepath, $this->mimetype, $this->filename, 'attachment');
|
||||
if ($this->attachment->isLocal()) {
|
||||
common_send_file(
|
||||
$this->filepath,
|
||||
$this->mimetype,
|
||||
$this->filename,
|
||||
'attachment'
|
||||
);
|
||||
} else {
|
||||
common_redirect($this->attachment->getUrl(), 303);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,12 +20,12 @@ defined('GNUSOCIAL') || die();
|
||||
* Show notice attachments
|
||||
*
|
||||
* @category Personal
|
||||
* @package StatusNet
|
||||
* @package GNUsocial
|
||||
* @author Evan Prodromou <evan@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/
|
||||
* @copyright 2008-2009 StatusNet, Inc.
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class Attachment_thumbnailAction extends AttachmentAction
|
||||
class Attachment_thumbnailAction extends Attachment_viewAction
|
||||
{
|
||||
protected $thumb_w = null; // max width
|
||||
protected $thumb_h = null; // max height
|
||||
@ -52,14 +52,21 @@ class Attachment_thumbnailAction extends AttachmentAction
|
||||
public function showPage(): void
|
||||
{
|
||||
// Returns a File_thumbnail object or throws exception if not available
|
||||
$filename = $this->filename;
|
||||
$filepath = $this->filepath;
|
||||
try {
|
||||
$thumbnail = $this->attachment->getThumbnail($this->thumb_w, $this->thumb_h, $this->thumb_c);
|
||||
$file = $thumbnail->getFile();
|
||||
$filename = $thumbnail->getFilename();
|
||||
$filepath = $thumbnail->getPath();
|
||||
} catch (UseFileAsThumbnailException $e) {
|
||||
// With this exception, the file exists locally
|
||||
$file = $e->file;
|
||||
// With this exception, the file exists locally $e->file;
|
||||
} catch (FileNotFoundException $e) {
|
||||
$this->clientError(_m('No such attachment'), 404);
|
||||
} catch (Exception $e) {
|
||||
if (is_null($filepath)) {
|
||||
$this->clientError(_m('No such thumbnail'), 404);
|
||||
}
|
||||
// Remote file
|
||||
}
|
||||
|
||||
// Disable errors, to not mess with the file contents (suppress errors in case access to this
|
||||
@ -67,6 +74,6 @@ class Attachment_thumbnailAction extends AttachmentAction
|
||||
// script execution, and we don't want to have any more errors until then, so don't reset it
|
||||
@ini_set('display_errors', 0);
|
||||
|
||||
common_send_file($this->filepath, $this->mimetype, $this->filename, 'inline');
|
||||
common_send_file($filepath, $this->mimetype, $filename, 'inline');
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,8 @@ defined('GNUSOCIAL') || die();
|
||||
*
|
||||
* @package GNUsocial
|
||||
* @author Miguel Dantas <biodantasgs@gmail.com>
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class Attachment_viewAction extends AttachmentAction
|
||||
{
|
||||
@ -32,7 +33,15 @@ class Attachment_viewAction extends AttachmentAction
|
||||
// script execution, and we don't want to have any more errors until then, so don't reset it
|
||||
@ini_set('display_errors', 0);
|
||||
|
||||
$disposition = in_array(common_get_mime_media($this->mimetype), ['image', 'video']) ? 'inline' : 'attachment';
|
||||
common_send_file($this->filepath, $this->mimetype, $this->filename, $disposition);
|
||||
if ($this->attachment->isLocal()) {
|
||||
$disposition = 'attachment';
|
||||
if (in_array(common_get_mime_media($this->mimetype), ['image', 'video'])) {
|
||||
$disposition = 'inline';
|
||||
}
|
||||
common_send_file($this->filepath, $this->mimetype,
|
||||
$this->filename, $disposition);
|
||||
} else {
|
||||
common_redirect($this->attachment->getUrl(), 303);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +1,43 @@
|
||||
<?php
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* StatusNet, the distributed open-source microblogging tool
|
||||
*
|
||||
* Handler for posting new notices
|
||||
*
|
||||
* 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
|
||||
* (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
|
||||
* 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/>.
|
||||
*
|
||||
* @category Personal
|
||||
* @package StatusNet
|
||||
* @package GNUsocial
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @author Sarven Capadisli <csarven@status.net>
|
||||
* @copyright 2008-2009 StatusNet, Inc.
|
||||
* @copyright 2013 Free Software Foundation, Inc.
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link http://status.net/
|
||||
* @copyright 2013 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
|
||||
if (!defined('GNUSOCIAL')) { exit(1); }
|
||||
defined('GNUSOCIAL') || die();
|
||||
|
||||
/**
|
||||
* Action for posting new notices
|
||||
*
|
||||
* @category Personal
|
||||
* @package StatusNet
|
||||
* @package GNUsocial
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @author Sarven Capadisli <csarven@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/
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class NewnoticeAction extends FormAction
|
||||
{
|
||||
@ -56,7 +52,7 @@ class NewnoticeAction extends FormAction
|
||||
*
|
||||
* @return string page title
|
||||
*/
|
||||
function title()
|
||||
public function title()
|
||||
{
|
||||
if ($this->getInfo() && $this->stored instanceof Notice) {
|
||||
// TRANS: Page title after sending a notice.
|
||||
@ -71,7 +67,7 @@ class NewnoticeAction extends FormAction
|
||||
|
||||
protected function doPreparation()
|
||||
{
|
||||
foreach(array('inreplyto') as $opt) {
|
||||
foreach (['inreplyto'] as $opt) {
|
||||
if ($this->trimmed($opt)) {
|
||||
$this->formOpts[$opt] = $this->trimmed($opt);
|
||||
}
|
||||
@ -106,26 +102,6 @@ class NewnoticeAction extends FormAction
|
||||
$options = array('source' => 'web');
|
||||
Event::handle('StartSaveNewNoticeWeb', array($this, $user, &$content, &$options));
|
||||
|
||||
$upload = null;
|
||||
try {
|
||||
// throws exception on failure
|
||||
$upload = MediaFile::fromUpload('attach', $this->scoped);
|
||||
if (Event::handle('StartSaveNewNoticeAppendAttachment', array($this, $upload, &$content, &$options))) {
|
||||
$content .= ($content==='' ? '' : ' ') . $upload->shortUrl();
|
||||
}
|
||||
Event::handle('EndSaveNewNoticeAppendAttachment', array($this, $upload, &$content, &$options));
|
||||
|
||||
// We could check content length here if the URL was added, but I'll just let it slide for now...
|
||||
|
||||
$act->enclosures[] = $upload->getEnclosure();
|
||||
} catch (NoUploadedMediaException $e) {
|
||||
// simply no attached media to the new notice
|
||||
if (empty($content)) {
|
||||
// TRANS: Client error displayed trying to send a notice without content.
|
||||
throw new ClientException(_('No content!'));
|
||||
}
|
||||
}
|
||||
|
||||
$inter = new CommandInterpreter();
|
||||
|
||||
$cmd = $inter->handle_command($user, $content);
|
||||
@ -144,15 +120,37 @@ class NewnoticeAction extends FormAction
|
||||
$act->time = time();
|
||||
$act->actor = $this->scoped->asActivityObject();
|
||||
|
||||
$upload = null;
|
||||
try {
|
||||
// throws exception on failure
|
||||
$upload = MediaFile::fromUpload('attach', $this->scoped);
|
||||
if (Event::handle('StartSaveNewNoticeAppendAttachment', array($this, $upload, &$content, &$options))) {
|
||||
$content .= ($content==='' ? '' : ' ') . $upload->shortUrl();
|
||||
}
|
||||
Event::handle('EndSaveNewNoticeAppendAttachment', array($this, $upload, &$content, &$options));
|
||||
|
||||
// We could check content length here if the URL was added, but I'll just let it slide for now...
|
||||
|
||||
$act->enclosures[] = $upload->getEnclosure();
|
||||
} catch (NoUploadedMediaException $e) {
|
||||
// simply no attached media to the new notice
|
||||
if (empty($content)) {
|
||||
// TRANS: Client error displayed trying to send a notice without content.
|
||||
throw new ClientException(_m('No content!'));
|
||||
}
|
||||
}
|
||||
|
||||
// Reject notice if it is too long (without the HTML)
|
||||
// This is done after MediaFile::fromUpload etc. just to act the same as the ApiStatusesUpdateAction
|
||||
if (Notice::contentTooLong($content)) {
|
||||
// TRANS: Client error displayed when the parameter "status" is missing.
|
||||
// TRANS: %d is the maximum number of character for a notice.
|
||||
throw new ClientException(sprintf(_m('That\'s too long. Maximum notice size is %d character.',
|
||||
throw new ClientException(sprintf(
|
||||
_m('That\'s too long. Maximum notice size is %d character.',
|
||||
'That\'s too long. Maximum notice size is %d characters.',
|
||||
Notice::maxContent()),
|
||||
Notice::maxContent()));
|
||||
Notice::maxContent()
|
||||
));
|
||||
}
|
||||
|
||||
$act->context = new ActivityContext();
|
||||
@ -165,17 +163,21 @@ class NewnoticeAction extends FormAction
|
||||
if ($this->scoped->shareLocation()) {
|
||||
// use browser data if checked; otherwise profile data
|
||||
if ($this->arg('notice_data-geo')) {
|
||||
$locOptions = Notice::locationOptions($this->trimmed('lat'),
|
||||
$locOptions = Notice::locationOptions(
|
||||
$this->trimmed('lat'),
|
||||
$this->trimmed('lon'),
|
||||
$this->trimmed('location_id'),
|
||||
$this->trimmed('location_ns'),
|
||||
$this->scoped);
|
||||
$this->scoped
|
||||
);
|
||||
} else {
|
||||
$locOptions = Notice::locationOptions(null,
|
||||
$locOptions = Notice::locationOptions(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
$this->scoped);
|
||||
null,
|
||||
$this->scoped
|
||||
);
|
||||
}
|
||||
|
||||
$act->context->location = Location::fromOptions($locOptions);
|
||||
@ -202,9 +204,7 @@ class NewnoticeAction extends FormAction
|
||||
|
||||
$this->stored = Notice::saveActivity($act, $this->scoped, $options);
|
||||
|
||||
if ($upload instanceof MediaFile) {
|
||||
$upload->attachToNotice($this->stored);
|
||||
}
|
||||
|
||||
Event::handle('EndNoticeSaveWeb', array($this, $this->stored));
|
||||
}
|
||||
@ -216,7 +216,7 @@ class NewnoticeAction extends FormAction
|
||||
common_redirect($url, 303);
|
||||
}
|
||||
|
||||
return _('Saved the notice!');
|
||||
return _m('Saved the notice!');
|
||||
}
|
||||
|
||||
protected function showContent()
|
||||
@ -240,7 +240,7 @@ class NewnoticeAction extends FormAction
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function showNotice(Notice $notice)
|
||||
public function showNotice(Notice $notice)
|
||||
{
|
||||
$nli = new NoticeListItem($notice, $this);
|
||||
$nli->show();
|
||||
|
@ -56,7 +56,7 @@ class RedirecturlAction extends ManagedAction
|
||||
|
||||
public function showPage()
|
||||
{
|
||||
common_redirect($this->file->getUrl(false), 301);
|
||||
common_redirect($this->file->getUrl(true), 301);
|
||||
}
|
||||
|
||||
function isReadOnly($args)
|
||||
|
129
classes/File.php
129
classes/File.php
@ -53,7 +53,7 @@ class File extends Managed_DataObject
|
||||
return array(
|
||||
'fields' => array(
|
||||
'id' => array('type' => 'serial', 'not null' => true),
|
||||
'urlhash' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'sha256 of destination URL (url field)'),
|
||||
'urlhash' => array('type' => 'varchar', 'length' => 64, 'description' => 'sha256 of destination URL (url field)'),
|
||||
'url' => array('type' => 'text', 'description' => 'destination URL after following possible redirections'),
|
||||
'filehash' => array('type' => 'varchar', 'length' => 64, 'not null' => false, 'description' => 'sha256 of the file contents, only for locally stored files of course'),
|
||||
'mimetype' => array('type' => 'varchar', 'length' => 50, 'description' => 'mime type of resource'),
|
||||
@ -112,21 +112,26 @@ class File extends Managed_DataObject
|
||||
// We don't have the file's URL since before, so let's continue.
|
||||
}
|
||||
|
||||
// if the given url is an local attachment url and the id already exists, don't
|
||||
// save a new file record. This should never happen, but let's make it foolproof
|
||||
// FIXME: how about attachments servers?
|
||||
$u = parse_url($given_url);
|
||||
if (isset($u['host']) && $u['host'] === common_config('site', 'server')) {
|
||||
// If the given url is a local attachment url, don't save a new file record.
|
||||
$uh = parse_url($given_url, PHP_URL_HOST);
|
||||
$up = parse_url($given_url, PHP_URL_PATH);
|
||||
if ($uh == common_config('site', 'server') || $uh == common_config('attachments', 'server')) {
|
||||
unset($uh);
|
||||
$r = Router::get();
|
||||
// Skip the / in the beginning or $r->map won't match
|
||||
try {
|
||||
$args = $r->map(mb_substr($u['path'], 1));
|
||||
if ($args['action'] === 'attachment') {
|
||||
$args = $r->map(mb_substr($up, 1));
|
||||
if ($args['action'] === 'attachment' ||
|
||||
$args['action'] === 'attachment_view' ||
|
||||
$args['action'] === 'attachment_download' ||
|
||||
$args['action'] === 'attachment_thumbnail' ) {
|
||||
try {
|
||||
if (!empty($args['attachment'])) {
|
||||
return File::getByID($args['attachment']);
|
||||
} elseif ($args['filehash']) {
|
||||
return File::getByHash($args['filehash']);
|
||||
if (array_key_exists('attachment', $args)) {
|
||||
return File::getByID((int)$args['attachment']);
|
||||
} elseif (array_key_exists('filehash', $args)) {
|
||||
$file = File::getByHash($args['filehash']);
|
||||
$file->fetch();
|
||||
return $file;
|
||||
}
|
||||
} catch (NoResultException $e) {
|
||||
// apparently this link goes to us, but is _not_ an existing attachment (File) ID?
|
||||
@ -158,10 +163,10 @@ class File extends Managed_DataObject
|
||||
$file->mimetype = $redir_data['type'];
|
||||
}
|
||||
if (!empty($redir_data['size'])) {
|
||||
$file->size = intval($redir_data['size']);
|
||||
$file->size = (int)$redir_data['size'];
|
||||
}
|
||||
if (isset($redir_data['time']) && $redir_data['time'] > 0) {
|
||||
$file->date = intval($redir_data['time']);
|
||||
$file->date = (int)$redir_data['time'];
|
||||
}
|
||||
$file->saveFile();
|
||||
return $file;
|
||||
@ -169,7 +174,7 @@ class File extends Managed_DataObject
|
||||
|
||||
public function saveFile()
|
||||
{
|
||||
$this->urlhash = self::hashurl($this->url);
|
||||
$this->urlhash = is_null($this->url) ? null : self::hashurl($this->url);
|
||||
|
||||
if (!Event::handle('StartFileSaveNew', array(&$this))) {
|
||||
throw new ServerException('File not saved due to an aborted StartFileSaveNew event.');
|
||||
@ -193,14 +198,14 @@ class File extends Managed_DataObject
|
||||
* - return the File object with the full reference
|
||||
*
|
||||
* @param string $given_url the URL we're looking at
|
||||
* @param Notice $notice (optional)
|
||||
* @param Notice|null $notice (optional)
|
||||
* @param bool $followRedirects defaults to true
|
||||
*
|
||||
* @return mixed File on success, -1 on some errors
|
||||
*
|
||||
* @throws ServerException on failure
|
||||
*/
|
||||
public static function processNew($given_url, Notice $notice=null, $followRedirects=true)
|
||||
public static function processNew($given_url, ?Notice $notice=null, bool $followRedirects=true)
|
||||
{
|
||||
if (empty($given_url)) {
|
||||
throw new ServerException('No given URL to process');
|
||||
@ -265,7 +270,8 @@ class File extends Managed_DataObject
|
||||
INNER JOIN notice
|
||||
ON file_to_post.post_id = notice.id
|
||||
WHERE profile_id = {$scoped->id} AND
|
||||
file.url LIKE '%/notice/%/file'";
|
||||
filename IS NULL AND
|
||||
file.url IS NOT NULL";
|
||||
$file->query($query);
|
||||
$file->fetch();
|
||||
$total = $file->total + $fileSize;
|
||||
@ -460,6 +466,14 @@ class File extends Managed_DataObject
|
||||
return $dir . $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't use for attachments, only for assets.
|
||||
*
|
||||
* @param $filename
|
||||
* @return mixed|string
|
||||
* @throws InvalidFilenameException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public static function url($filename)
|
||||
{
|
||||
self::tryFilename($filename);
|
||||
@ -534,7 +548,7 @@ class File extends Managed_DataObject
|
||||
|
||||
$needMoreMetadataMimetypes = array(null, 'application/xhtml+xml', 'text/html');
|
||||
|
||||
if (!isset($this->filename) && in_array(common_bare_mime($enclosure->mimetype), $needMoreMetadataMimetypes)) {
|
||||
if (isset($enclosure->url) && in_array(common_bare_mime($enclosure->mimetype), $needMoreMetadataMimetypes)) {
|
||||
// This fetches enclosure metadata for non-local links with unset/HTML mimetypes,
|
||||
// which may be enriched through oEmbed or similar (implemented as plugins)
|
||||
Event::handle('FileEnclosureMetadata', array($this, &$enclosure));
|
||||
@ -561,45 +575,28 @@ class File extends Managed_DataObject
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment's thumbnail record, if any.
|
||||
* Make sure you supply proper 'int' typed variables (or null).
|
||||
* Get the attachment's thumbnail record, if any or generate one.
|
||||
*
|
||||
* @param $width int Max width of thumbnail in pixels. (if null, use common_config values)
|
||||
* @param $height int Max height of thumbnail in pixels. (if null, square-crop to $width)
|
||||
* @param $crop bool Crop to the max-values' aspect ratio
|
||||
* @param $force_still bool Don't allow fallback to showing original (such as animated GIF)
|
||||
* @param $upscale mixed Whether or not to scale smaller images up to larger thumbnail sizes. (null = site default)
|
||||
* @param int|null $width Max width of thumbnail in pixels. (if null, use common_config values)
|
||||
* @param int|null $height Max height of thumbnail in pixels. (if null, square-crop to $width)
|
||||
* @param bool $crop Crop to the max-values' aspect ratio
|
||||
* @param bool $force_still Don't allow fallback to showing original (such as animated GIF)
|
||||
* @param bool|null $upscale Whether or not to scale smaller images up to larger thumbnail sizes. (null = site default)
|
||||
*
|
||||
* @return File_thumbnail
|
||||
*
|
||||
* @throws UseFileAsThumbnailException if the file is considered an image itself and should be itself as thumbnail
|
||||
* @throws UnsupportedMediaException if, despite trying, we can't understand how to make a thumbnail for this format
|
||||
* @throws ClientException
|
||||
* @throws FileNotFoundException
|
||||
* @throws FileNotStoredLocallyException
|
||||
* @throws InvalidFilenameException
|
||||
* @throws NoResultException
|
||||
* @throws ServerException on various other errors
|
||||
* @throws UnsupportedMediaException if, despite trying, we can't understand how to make a thumbnail for this format
|
||||
* @throws UseFileAsThumbnailException if the file is considered an image itself and should be itself as thumbnail
|
||||
*/
|
||||
public function getThumbnail($width = null, $height = null, $crop = false, $force_still = true, $upscale = null): File_thumbnail
|
||||
public function getThumbnail (?int $width = null, ?int $height = null, bool $crop = false, bool $force_still = true, ?bool $upscale = null): File_thumbnail
|
||||
{
|
||||
// Get some more information about this file through our ImageFile class
|
||||
$image = ImageFile::fromFileObject($this);
|
||||
if ($image->animated && !common_config('thumbnail', 'animated')) {
|
||||
// null means "always use file as thumbnail"
|
||||
// false means you get choice between frozen frame or original when calling getThumbnail
|
||||
if (is_null(common_config('thumbnail', 'animated')) || !$force_still) {
|
||||
try {
|
||||
// remote files with animated GIFs as thumbnails will match this
|
||||
return File_thumbnail::byFile($this);
|
||||
} catch (NoResultException $e) {
|
||||
// and if it's not a remote file, it'll be safe to use the locally stored File
|
||||
throw new UseFileAsThumbnailException($this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $image->getFileThumbnail(
|
||||
$width,
|
||||
$height,
|
||||
$crop,
|
||||
!is_null($upscale) ? $upscale : common_config('thumbnail', 'upscale')
|
||||
);
|
||||
return File_thumbnail::fromFileObject($this, $width, $height, $crop, $force_still, $upscale);
|
||||
}
|
||||
|
||||
public function getPath()
|
||||
@ -698,34 +695,33 @@ class File extends Managed_DataObject
|
||||
|
||||
public function getAttachmentDownloadUrl()
|
||||
{
|
||||
return common_local_url('attachment_download', array('attachment'=>$this->getID()));
|
||||
return common_local_url('attachment_download', ['filehash' => $this->filehash]);
|
||||
}
|
||||
|
||||
public function getAttachmentViewUrl()
|
||||
{
|
||||
return common_local_url('attachment_view', array('attachment'=>$this->getID()));
|
||||
return common_local_url('attachment_view', ['filehash' => $this->filehash]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $use_local true means require local, null means prefer local, false means use whatever is stored
|
||||
* @param bool|null $use_local true means require local, null means prefer original, false means use whatever is stored
|
||||
* @return string
|
||||
* @throws FileNotStoredLocallyException
|
||||
*/
|
||||
public function getUrl($use_local=null)
|
||||
public function getUrl(?bool $use_local=null): ?string
|
||||
{
|
||||
if ($use_local !== false) {
|
||||
if (is_string($this->filename) || !empty($this->filename)) {
|
||||
if (empty($this->url)) {
|
||||
// A locally stored file, so let's generate a URL for our instance.
|
||||
return $this->getAttachmentViewUrl();
|
||||
}
|
||||
if ($use_local) {
|
||||
// if the file wasn't stored locally (has filename) and we require a local URL
|
||||
// if the file isn't ours but and we require a local URL anyway
|
||||
throw new FileNotStoredLocallyException($this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// No local filename available, return the URL we have stored
|
||||
// The original file's URL
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
@ -748,7 +744,7 @@ class File extends Managed_DataObject
|
||||
{
|
||||
$file = new File();
|
||||
$file->filehash = strtolower($hashstr);
|
||||
if (!$file->find(true)) {
|
||||
if (!$file->find()) {
|
||||
throw new NoResultException($file);
|
||||
}
|
||||
return $file;
|
||||
@ -836,11 +832,10 @@ class File extends Managed_DataObject
|
||||
|
||||
public function isLocal()
|
||||
{
|
||||
return !empty($this->filename);
|
||||
return empty($this->url) && !empty($this->filename);
|
||||
}
|
||||
|
||||
public function delete($useWhere=false)
|
||||
{
|
||||
public function unlink() {
|
||||
// Delete the file, if it exists locally
|
||||
if (!empty($this->filename) && file_exists(self::path($this->filename))) {
|
||||
$deleted = @unlink(self::path($this->filename));
|
||||
@ -848,12 +843,18 @@ class File extends Managed_DataObject
|
||||
common_log(LOG_ERR, sprintf('Could not unlink existing file: "%s"', self::path($this->filename)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($useWhere=false)
|
||||
{
|
||||
// Delete the file, if it exists locally
|
||||
$this->unlink();
|
||||
|
||||
// Clear out related things in the database and filesystem, such as thumbnails
|
||||
$related = [
|
||||
'File_redirection',
|
||||
'File_thumbnail',
|
||||
'File_to_post',
|
||||
'File_to_post'
|
||||
];
|
||||
Event::handle('FileDeleteRelated', [$this, &$related]);
|
||||
|
||||
|
@ -59,6 +59,69 @@ class File_thumbnail extends Managed_DataObject
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment's thumbnail record, if any or generate one.
|
||||
*
|
||||
* @param File $file
|
||||
* @param int|null $width Max width of thumbnail in pixels. (if null, use common_config values)
|
||||
* @param int|null $height Max height of thumbnail in pixels. (if null, square-crop to $width)
|
||||
* @param bool $crop Crop to the max-values' aspect ratio
|
||||
* @param bool $force_still Don't allow fallback to showing original (such as animated GIF)
|
||||
* @param bool|null $upscale Whether or not to scale smaller images up to larger thumbnail sizes. (null = site default)
|
||||
*
|
||||
* @return File_thumbnail
|
||||
*
|
||||
* @throws ClientException
|
||||
* @throws FileNotFoundException
|
||||
* @throws FileNotStoredLocallyException
|
||||
* @throws InvalidFilenameException
|
||||
* @throws NoResultException
|
||||
* @throws ServerException on various other errors
|
||||
* @throws UnsupportedMediaException if, despite trying, we can't understand how to make a thumbnail for this format
|
||||
* @throws UseFileAsThumbnailException if the file is considered an image itself and should be itself as thumbnail
|
||||
*/
|
||||
public static function fromFileObject (File $file, ?int $width = null, ?int $height = null, bool $crop = false, bool $force_still = true, ?bool $upscale = null): File_thumbnail
|
||||
{
|
||||
if (is_null($file->filename)) {
|
||||
throw new FileNotFoundException("This remote file has no local thumbnail.");
|
||||
}
|
||||
$image = ImageFile::fromFileObject($file);
|
||||
$imgPath = $image->getPath();
|
||||
$media = common_get_mime_media($file->mimetype);
|
||||
if (Event::handle('CreateFileImageThumbnailSource', [$file, &$imgPath, $media])) {
|
||||
if (!file_exists($imgPath)) {
|
||||
throw new FileNotFoundException($imgPath);
|
||||
}
|
||||
|
||||
// First some mimetype specific exceptions
|
||||
switch ($file->mimetype) {
|
||||
case 'image/svg+xml':
|
||||
throw new UseFileAsThumbnailException($file);
|
||||
}
|
||||
}
|
||||
|
||||
if ($image->animated && !common_config('thumbnail', 'animated')) {
|
||||
// null means "always use file as thumbnail"
|
||||
// false means you get choice between frozen frame or original when calling getThumbnail
|
||||
if (is_null(common_config('thumbnail', 'animated')) || !$force_still) {
|
||||
try {
|
||||
// remote files with animated GIFs as thumbnails will match this
|
||||
return File_thumbnail::byFile($file);
|
||||
} catch (NoResultException $e) {
|
||||
// and if it's not a remote file, it'll be safe to use the locally stored File
|
||||
throw new UseFileAsThumbnailException($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $image->getFileThumbnail(
|
||||
$width,
|
||||
$height,
|
||||
$crop,
|
||||
!is_null($upscale) ? $upscale : common_config('thumbnail', 'upscale')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save oEmbed-provided thumbnail data
|
||||
*
|
||||
@ -128,8 +191,8 @@ class File_thumbnail extends Managed_DataObject
|
||||
$tn->file_id = $file_id;
|
||||
$tn->url = $url;
|
||||
$tn->filename = $filename;
|
||||
$tn->width = intval($width);
|
||||
$tn->height = intval($height);
|
||||
$tn->width = (int)$width;
|
||||
$tn->height = (int)$height;
|
||||
$tn->insert();
|
||||
return $tn;
|
||||
}
|
||||
@ -148,30 +211,6 @@ class File_thumbnail extends Managed_DataObject
|
||||
return $dir . $filename;
|
||||
}
|
||||
|
||||
public static function url($filename)
|
||||
{
|
||||
File::tryFilename($filename);
|
||||
|
||||
// FIXME: private site thumbnails?
|
||||
|
||||
$path = common_config('thumbnail', 'path');
|
||||
if (empty($path)) {
|
||||
return File::url('thumb')."/{$filename}";
|
||||
}
|
||||
|
||||
$protocol = (GNUsocial::useHTTPS() ? 'https' : 'http');
|
||||
$server = common_config('thumbnail', 'server') ?: common_config('site', 'server');
|
||||
|
||||
if ($path[mb_strlen($path)-1] != '/') {
|
||||
$path .= '/';
|
||||
}
|
||||
if ($path[0] != '/') {
|
||||
$path = '/'.$path;
|
||||
}
|
||||
|
||||
return $protocol.'://'.$server.$path.$filename;
|
||||
}
|
||||
|
||||
public function getFilename()
|
||||
{
|
||||
return File::tryFilename($this->filename);
|
||||
@ -222,17 +261,11 @@ class File_thumbnail extends Managed_DataObject
|
||||
|
||||
public function getUrl()
|
||||
{
|
||||
if (!empty($this->filename) || $this->getFile()->isLocal()) {
|
||||
// A locally stored File, so we can dynamically generate a URL.
|
||||
$url = common_local_url('attachment_thumbnail', array('attachment'=>$this->file_id));
|
||||
$url = common_local_url('attachment_thumbnail', ['filehash' => $this->getFile()->filehash]);
|
||||
if (strpos($url, '?') === false) {
|
||||
$url .= '?';
|
||||
}
|
||||
return $url . http_build_query(array('w'=>$this->width, 'h'=>$this->height));
|
||||
}
|
||||
|
||||
// No local filename available, return the remote URL we have stored
|
||||
return $this->url;
|
||||
return $url . http_build_query(['w'=>$this->width, 'h'=>$this->height]);
|
||||
}
|
||||
|
||||
public function getHeight()
|
||||
@ -272,7 +305,7 @@ class File_thumbnail extends Managed_DataObject
|
||||
return parent::delete($useWhere);
|
||||
}
|
||||
|
||||
public function getFile()
|
||||
public function getFile(): File
|
||||
{
|
||||
return File::getByID($this->file_id);
|
||||
}
|
||||
|
@ -61,6 +61,6 @@ class Attachment extends AttachmentListItem
|
||||
|
||||
public function linkAttr()
|
||||
{
|
||||
return array('rel' => 'external', 'href' => $this->attachment->getAttachmentDownloadUrl());
|
||||
return ['rel' => 'external', 'href' => $this->attachment->getUrl(null)];
|
||||
}
|
||||
}
|
||||
|
@ -75,12 +75,6 @@ class AttachmentList extends Widget
|
||||
function show()
|
||||
{
|
||||
$attachments = $this->notice->attachments();
|
||||
foreach ($attachments as $key=>$att) {
|
||||
// Remove attachments which are not representable with neither a title nor thumbnail
|
||||
if ($att->getTitle() === _('Untitled attachment') && !$att->hasThumbnail()) {
|
||||
unset($attachments[$key]);
|
||||
}
|
||||
}
|
||||
if (!count($attachments)) {
|
||||
return 0;
|
||||
}
|
||||
|
@ -91,10 +91,11 @@ class AttachmentListItem extends Widget
|
||||
}
|
||||
|
||||
function linkAttr() {
|
||||
return array(
|
||||
return [
|
||||
'class' => 'u-url',
|
||||
'href' => $this->attachment->getAttachmentUrl(),
|
||||
'title' => $this->linkTitle());
|
||||
'href' => $this->attachment->getAttachmentDownloadUrl(),
|
||||
'title' => $this->linkTitle()
|
||||
];
|
||||
}
|
||||
|
||||
function showNoticeAttachment()
|
||||
@ -105,25 +106,29 @@ class AttachmentListItem extends Widget
|
||||
function showRepresentation() {
|
||||
$enclosure = $this->attachment->getEnclosure();
|
||||
|
||||
if (Event::handle('StartShowAttachmentRepresentation', array($this->out, $this->attachment))) {
|
||||
if (Event::handle('StartShowAttachmentRepresentation', [$this->out, $this->attachment])) {
|
||||
|
||||
$this->out->elementStart('label');
|
||||
$this->out->element('a', $this->linkAttr(), $this->title());
|
||||
$this->out->element('a', ['rel' => 'external', 'href' => $this->attachment->getAttachmentUrl()], $this->title());
|
||||
$this->out->elementEnd('label');
|
||||
|
||||
$this->out->element('br');
|
||||
|
||||
try {
|
||||
if (!empty($enclosure->mimetype)) {
|
||||
// First, prepare a thumbnail if it exists.
|
||||
$thumb = null;
|
||||
try {
|
||||
// Tell getThumbnail that we can show an animated image if it has one (4th arg, "force_still")
|
||||
$thumb = $this->attachment->getThumbnail(null, null, false, false);
|
||||
$thumb = File_thumbnail::fromFileObject($this->attachment, null, null, false, false);
|
||||
} catch (UseFileAsThumbnailException $e) {
|
||||
$thumb = null;
|
||||
} catch (UnsupportedMediaException $e) {
|
||||
// FIXME: Show a good representation of unsupported/unshowable images
|
||||
$thumb = null;
|
||||
} catch (FileNotFoundException $e) {
|
||||
// Remote file
|
||||
$thumb = null;
|
||||
}
|
||||
|
||||
// Then get the kind of mediatype we're dealing with
|
||||
@ -201,6 +206,11 @@ class AttachmentListItem extends Widget
|
||||
} else {
|
||||
Event::handle('ShowUnsupportedAttachmentRepresentation', array($this->out, $this->attachment));
|
||||
}
|
||||
} catch (FileNotFoundException $e) {
|
||||
if (!$this->attachment->isLocal()) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::handle('EndShowAttachmentRepresentation', array($this->out, $this->attachment));
|
||||
}
|
||||
|
@ -63,13 +63,13 @@ class ImageFile extends MediaFile
|
||||
* interactions (useful for temporary objects)
|
||||
* @param string $filepath The path of the file this media refers to. Required
|
||||
* @param string|null $filehash The hash of the file, if known. Optional
|
||||
*
|
||||
* @param string|null $fileurl
|
||||
* @throws ClientException
|
||||
* @throws NoResultException
|
||||
* @throws ServerException
|
||||
* @throws UnsupportedMediaException
|
||||
*/
|
||||
public function __construct(?int $id = null, string $filepath, ?string $filehash = null)
|
||||
public function __construct(?int $id = null, string $filepath, ?string $filehash = null, ?string $fileurl = null)
|
||||
{
|
||||
$old_limit = ini_set('memory_limit', common_config('attachments', 'memory_limit'));
|
||||
|
||||
@ -109,7 +109,8 @@ class ImageFile extends MediaFile
|
||||
$filepath,
|
||||
$this->mimetype,
|
||||
$filehash,
|
||||
$id
|
||||
$id,
|
||||
$fileurl
|
||||
);
|
||||
|
||||
if ($this->type === IMAGETYPE_JPEG) {
|
||||
@ -143,69 +144,28 @@ class ImageFile extends MediaFile
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a thumbnail from a file object
|
||||
* Shortcut method to get an ImageFile from a File
|
||||
*
|
||||
* @param File $file
|
||||
* @return ImageFile
|
||||
* @throws ClientException
|
||||
* @throws FileNotFoundException
|
||||
* @throws NoResultException
|
||||
* @throws ServerException
|
||||
* @throws UnsupportedMediaException
|
||||
* @throws UseFileAsThumbnailException
|
||||
*/
|
||||
public static function fromFileObject(File $file)
|
||||
{
|
||||
$imgPath = null;
|
||||
$media = common_get_mime_media($file->mimetype);
|
||||
if (Event::handle('CreateFileImageThumbnailSource', [$file, &$imgPath, $media])) {
|
||||
if (empty($file->filename) && !file_exists($imgPath)) {
|
||||
throw new FileNotFoundException($imgPath);
|
||||
}
|
||||
|
||||
// First some mimetype specific exceptions
|
||||
switch ($file->mimetype) {
|
||||
case 'image/svg+xml':
|
||||
throw new UseFileAsThumbnailException($file);
|
||||
}
|
||||
|
||||
// And we'll only consider it an image if it has such a media type
|
||||
if ($media !== 'image') {
|
||||
throw new UnsupportedMediaException(_m('Unsupported media format.'), $file->getPath());
|
||||
}
|
||||
|
||||
if (!empty($file->filename)) {
|
||||
$imgPath = $file->getPath();
|
||||
}
|
||||
}
|
||||
$filepath = $file->getPath();
|
||||
|
||||
if (!file_exists($imgPath)) {
|
||||
throw new FileNotFoundException($imgPath);
|
||||
}
|
||||
|
||||
try {
|
||||
$image = new self($file->getID(), $imgPath);
|
||||
} catch (Exception $e) {
|
||||
// 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));
|
||||
@unlink($imgPath);
|
||||
}
|
||||
} catch (FileNotFoundException $e) {
|
||||
// File reported (via getPath) that the original file
|
||||
// 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())
|
||||
));
|
||||
throw $e;
|
||||
}
|
||||
return $image;
|
||||
return new self($file->getID(), $filepath, $file->filehash);
|
||||
}
|
||||
|
||||
public function getPath()
|
||||
@ -251,9 +211,9 @@ class ImageFile extends MediaFile
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a file upload
|
||||
* Create a new ImageFile object from an url
|
||||
*
|
||||
* Uses MediaFile's `fromURL` to do the majority of the work
|
||||
* Uses MediaFile's `fromUrl` to do the majority of the work
|
||||
* and ensures the uploaded file is in fact an image.
|
||||
*
|
||||
* @param string $url Remote image URL
|
||||
@ -453,11 +413,6 @@ class ImageFile extends MediaFile
|
||||
return $outpath;
|
||||
}
|
||||
|
||||
public function unlink()
|
||||
{
|
||||
@unlink($this->filepath);
|
||||
}
|
||||
|
||||
public function scaleToFit($maxWidth = null, $maxHeight = null, $crop = null)
|
||||
{
|
||||
return self::getScalingValues(
|
||||
@ -587,7 +542,21 @@ class ImageFile extends MediaFile
|
||||
return $count >= 1; // number of animated frames apart from the original image
|
||||
}
|
||||
|
||||
public function getFileThumbnail($width, $height, $crop, $upscale = false)
|
||||
/**
|
||||
* @param $width
|
||||
* @param $height
|
||||
* @param $crop
|
||||
* @param false $upscale
|
||||
* @return File_thumbnail
|
||||
* @throws ClientException
|
||||
* @throws FileNotFoundException
|
||||
* @throws FileNotStoredLocallyException
|
||||
* @throws InvalidFilenameException
|
||||
* @throws ServerException
|
||||
* @throws UnsupportedMediaException
|
||||
* @throws UseFileAsThumbnailException
|
||||
*/
|
||||
public function getFileThumbnail($width = null, $height = null, $crop = null, $upscale = false)
|
||||
{
|
||||
if (!$this->fileRecord instanceof File) {
|
||||
throw new ServerException('No File object attached to this ImageFile object.');
|
||||
@ -679,9 +648,7 @@ class ImageFile extends MediaFile
|
||||
|
||||
return File_thumbnail::saveThumbnail(
|
||||
$this->fileRecord->getID(),
|
||||
// no url since we generated it ourselves and can dynamically
|
||||
// generate the url
|
||||
null,
|
||||
$this->fileRecord->getUrl(false),
|
||||
$width,
|
||||
$height,
|
||||
$outname
|
||||
|
@ -41,9 +41,11 @@ class InlineAttachmentListItem extends AttachmentListItem
|
||||
// XXX: RDFa
|
||||
// TODO: add notice_type class e.g., notice_video, notice_image
|
||||
$this->out->elementStart('li',
|
||||
array('class' => 'inline-attachment',
|
||||
[
|
||||
'class' => 'inline-attachment',
|
||||
'id' => 'attachment-' . $this->attachment->getID(),
|
||||
));
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -48,24 +48,25 @@ class MediaFile
|
||||
/**
|
||||
* MediaFile constructor.
|
||||
*
|
||||
* @param string $filepath The path of the file this media refers to. Required
|
||||
* @param string|null $filepath The path of the file this media refers to. Required
|
||||
* @param string $mimetype The mimetype of the file. Required
|
||||
* @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.
|
||||
* If null, it searches for it. If -1, it skips all DB
|
||||
* interactions (useful for temporary objects)
|
||||
*
|
||||
* @param string|null $fileurl Provide if remote
|
||||
* @throws ClientException
|
||||
* @throws NoResultException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function __construct(string $filepath, string $mimetype, ?string $filehash = null, ?int $id = null)
|
||||
public function __construct(?string $filepath = null, string $mimetype, ?string $filehash = null, ?int $id = null, ?string $fileurl = null)
|
||||
{
|
||||
$this->filepath = $filepath;
|
||||
$this->filename = basename($this->filepath);
|
||||
$this->mimetype = $mimetype;
|
||||
$this->filehash = self::getHashOfFile($this->filepath, $filehash);
|
||||
$this->filehash = is_null($filepath) ? null : self::getHashOfFile($this->filepath, $filehash);
|
||||
$this->id = $id;
|
||||
$this->fileurl = $fileurl;
|
||||
|
||||
// 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
|
||||
@ -82,16 +83,28 @@ class MediaFile
|
||||
// Otherwise, store it
|
||||
$this->fileRecord = $this->storeFile();
|
||||
}
|
||||
|
||||
$this->fileurl = common_local_url(
|
||||
'attachment',
|
||||
['attachment' => $this->fileRecord->id]
|
||||
);
|
||||
|
||||
$this->short_fileurl = common_shorten_url($this->fileurl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
|
||||
public function attachToNotice(Notice $notice)
|
||||
{
|
||||
File_to_post::processNew($this->fileRecord, $notice);
|
||||
@ -102,9 +115,26 @@ class MediaFile
|
||||
return File::path($this->filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
public function shortUrl()
|
||||
{
|
||||
return $this->short_fileurl;
|
||||
return common_shorten_url($this->getUrl());
|
||||
}
|
||||
|
||||
public function getEnclosure()
|
||||
@ -112,11 +142,27 @@ class MediaFile
|
||||
return $this->getFile()->getEnclosure();
|
||||
}
|
||||
|
||||
public function delete()
|
||||
public function delete($useWhere=false)
|
||||
{
|
||||
if (!is_null($this->fileRecord)) {
|
||||
$this->fileRecord->delete($useWhere);
|
||||
}
|
||||
@unlink($this->filepath);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public function getFile()
|
||||
{
|
||||
if (!$this->fileRecord instanceof File) {
|
||||
@ -164,19 +210,19 @@ class MediaFile
|
||||
{
|
||||
try {
|
||||
$file = File::getByHash($this->filehash);
|
||||
// We're done here. Yes. Already. We assume sha256 won't collide on us anytime soon.
|
||||
if (is_null($this->fileurl) && is_null($file->getUrl(false))) {
|
||||
// An already existing local file is being re-added, return it
|
||||
return $file;
|
||||
}
|
||||
} catch (NoResultException $e) {
|
||||
// Well, let's just continue below.
|
||||
}
|
||||
|
||||
$fileurl = common_local_url('attachment_view', ['filehash' => $this->filehash]);
|
||||
|
||||
$file = new File;
|
||||
|
||||
$file->filename = $this->filename;
|
||||
$file->urlhash = File::hashurl($fileurl);
|
||||
$file->url = $fileurl;
|
||||
$file->url = $this->fileurl;
|
||||
$file->urlhash = is_null($file->url) ? null : File::hashurl($file->url);
|
||||
$file->filehash = $this->filehash;
|
||||
$file->size = filesize($this->filepath);
|
||||
if ($file->size === false) {
|
||||
@ -196,7 +242,7 @@ class MediaFile
|
||||
// Set file geometrical properties if available
|
||||
try {
|
||||
$image = ImageFile::fromFileObject($file);
|
||||
$orig = clone $file;
|
||||
$orig = clone($file);
|
||||
$file->width = $image->width;
|
||||
$file->height = $image->height;
|
||||
$file->update($orig);
|
||||
@ -205,6 +251,8 @@ class MediaFile
|
||||
// may have generated a temporary file from a
|
||||
// video support plugin or something.
|
||||
// FIXME: Do this more automagically.
|
||||
// Honestly, I think this is unlikely these days,
|
||||
// but better be safe than sure, I guess
|
||||
if ($image->getPath() != $file->getPath()) {
|
||||
$image->unlink();
|
||||
}
|
||||
@ -390,6 +438,16 @@ class MediaFile
|
||||
|
||||
try {
|
||||
$file = File::getByHash($filehash);
|
||||
while ($file->fetch()) {
|
||||
if ($file->getUrl(false)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
return ImageFile::fromFileObject($file);
|
||||
} catch (UnsupportedMediaException $e) {
|
||||
return MediaFile::fromFileObject($file);
|
||||
}
|
||||
}
|
||||
// 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();
|
||||
@ -432,10 +490,10 @@ class MediaFile
|
||||
}
|
||||
|
||||
if ($media == 'image') {
|
||||
return new ImageFile(null, $filepath);
|
||||
return new ImageFile(null, $filepath, $filehash);
|
||||
}
|
||||
}
|
||||
return new self($filepath, $mimetype, $filehash);
|
||||
return new self($filepath, $mimetype, $filehash, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -464,6 +522,48 @@ class MediaFile
|
||||
throw new ServerException(sprintf('Invalid remote media URL %s.', $url));
|
||||
}
|
||||
|
||||
$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);
|
||||
|
||||
$tempfile = new TemporaryFile('gs-mediafile');
|
||||
fwrite($tempfile->getResource(), HTTPClient::quickGet($url));
|
||||
fflush($tempfile->getResource());
|
||||
@ -471,7 +571,7 @@ class MediaFile
|
||||
$filehash = strtolower(self::getHashOfFile($tempfile->getRealPath()));
|
||||
|
||||
try {
|
||||
$file = File::getByHash($filehash);
|
||||
$file = File::getByUrl($url);
|
||||
/*
|
||||
* If no exception is thrown the file exists locally, so we'll use
|
||||
* that and just add redirections.
|
||||
@ -531,10 +631,10 @@ class MediaFile
|
||||
}
|
||||
|
||||
if ($media === 'image') {
|
||||
return new ImageFile(null, $filepath);
|
||||
return new ImageFile(null, $filepath, $filehash, $url);
|
||||
}
|
||||
}
|
||||
return new self($filepath, $mimetype, $filehash);
|
||||
return new self($filepath, $mimetype, $filehash, null, $url);
|
||||
}
|
||||
|
||||
public static function fromFileInfo(SplFileInfo $finfo, Profile $scoped = null)
|
||||
@ -543,6 +643,7 @@ class MediaFile
|
||||
|
||||
try {
|
||||
$file = File::getByHash($filehash);
|
||||
$file->fetch();
|
||||
// Already have it, so let's reuse the locally stored File
|
||||
// by using getPath we also check whether the file exists
|
||||
// and throw a FileNotFoundException with the path if it doesn't.
|
||||
|
@ -223,16 +223,18 @@ class Router
|
||||
['q' => '.+']);
|
||||
$m->connect('search/notice/rss', ['action' => 'noticesearchrss']);
|
||||
|
||||
foreach (['' => 'attachment',
|
||||
'/view' => 'attachment_view',
|
||||
// Attachment page for file
|
||||
$m->connect("attachment/:attachment",
|
||||
['action' => 'attachment'],
|
||||
['attachment' => '[0-9]+']);
|
||||
|
||||
// Retrieve local file
|
||||
foreach (['/view' => 'attachment_view',
|
||||
'/download' => 'attachment_download',
|
||||
'/thumbnail' => 'attachment_thumbnail'] as $postfix => $action) {
|
||||
foreach (['filehash' => '[A-Za-z0-9._-]{64}',
|
||||
'attachment' => '[0-9]+'] as $type => $match) {
|
||||
$m->connect("attachment/:{$type}{$postfix}",
|
||||
$m->connect("attachment/:filehash{$postfix}",
|
||||
['action' => $action],
|
||||
[$type => $match]);
|
||||
}
|
||||
['filehash' => '[A-Za-z0-9._-]{64}']);
|
||||
}
|
||||
|
||||
$m->connect('notice/new?replyto=:replyto&inreplyto=:inreplyto',
|
||||
|
@ -189,14 +189,12 @@ class EmbedPlugin extends Plugin
|
||||
foreach (['xml', 'json'] as $format) {
|
||||
$action->element(
|
||||
'link',
|
||||
['rel' =>'alternate',
|
||||
[
|
||||
'rel' =>'alternate',
|
||||
'type' => "application/{$format}+oembed",
|
||||
'href' => common_local_url(
|
||||
'oembed',
|
||||
[],
|
||||
['format' => $format, 'url' => $url]
|
||||
),
|
||||
'title' => 'oEmbed']
|
||||
'href' => common_local_url('oembed', [], ['format' => $format, 'url' => $url]),
|
||||
'title' => 'oEmbed'
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ class OEmbedAction extends Action
|
||||
}
|
||||
try {
|
||||
$thumb = $attachment->getThumbnail();
|
||||
$thumb_url = File_thumbnail::url($thumb->filename);
|
||||
$thumb_url = $thumb->getUrl();
|
||||
$oembed['thumbnail_url'] = $thumb_url;
|
||||
break; // only first one
|
||||
} catch (UseFileAsThumbnailException $e) {
|
||||
|
@ -917,7 +917,7 @@ class Ostatus_profile extends Managed_DataObject
|
||||
}
|
||||
|
||||
// ImageFile throws exception if something goes wrong, which we'll let go on its merry way
|
||||
$imagefile = ImageFile::fromURL($url);
|
||||
$imagefile = ImageFile::fromUrl($url);
|
||||
|
||||
$self = $this->localProfile();
|
||||
|
||||
|
@ -138,6 +138,7 @@ class StoreRemoteMediaPlugin extends Plugin
|
||||
try {
|
||||
// Exception will be thrown before $file is set to anything, so old $file value will be kept
|
||||
$file = File::getByHash($filehash);
|
||||
$file->fetch();
|
||||
|
||||
//FIXME: Add some code so we don't have to store duplicate File rows for same hash files.
|
||||
} catch (NoResultException $e) {
|
||||
|
Loading…
Reference in New Issue
Block a user