[Embed][StoreRemoteMedia][Media] Copy and cleanup plugins from v2

This commit is contained in:
Hugo Sales 2021-04-14 15:27:37 +00:00
parent fe478c6104
commit 3b901745d5
Signed by untrusted user: someonewithpc
GPG Key ID: 7D0C7EAFC9D835A0
9 changed files with 947 additions and 263 deletions

5
config/preload.php Normal file
View File

@ -0,0 +1,5 @@
<?php
if (file_exists(dirname(__DIR__) . '/var/cache/prod/App_KernelProdContainer.preload.php')) {
require dirname(__DIR__) . '/var/cache/prod/App_KernelProdContainer.preload.php';
}

View File

@ -18,6 +18,7 @@
* OEmbed and OpenGraph implementation for GNU social
*
* @package GNUsocial
*
* @author Mikael Nordfeldth
* @author Stephen Paul Weber
* @author hannes
@ -29,9 +30,9 @@
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
defined('GNUSOCIAL') || die();
namespace Plugin\Embed;
use Embed\Embed;
use App\Core\Module;
/**
* Base class for the Embed plugin that does most of the heavy lifting to get
@ -40,7 +41,7 @@ use Embed\Embed;
* @copyright 2014-2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class EmbedPlugin extends Plugin
class Embed extends Module
{
const PLUGIN_VERSION = '2.1.0';
@ -49,16 +50,16 @@ class EmbedPlugin extends Plugin
public $domain_whitelist = [
// hostname => service provider
'^i\d*\.ytimg\.com$' => 'YouTube',
'^i\d*\.ytimg\.com$' => 'YouTube',
'^i\d*\.vimeocdn\.com$' => 'Vimeo',
];
public $append_whitelist = []; // fill this array as domain_whitelist to add more trusted sources
public $check_whitelist = false; // security/abuse precaution
public $thumbnail_width = 128;
public $thumbnail_width = 128;
public $thumbnail_height = 128;
public $crop = true;
public $max_size = null;
public $crop = true;
public $max_size;
protected $imgData = [];
@ -74,10 +75,10 @@ class EmbedPlugin extends Plugin
$this->domain_whitelist = array_merge($this->domain_whitelist, $this->append_whitelist);
// Load global configuration if specific not provided
$this->thumbnail_width = $this->thumbnail_width ?? common_config('thumbnail', 'width');
$this->thumbnail_width = $this->thumbnail_width ?? common_config('thumbnail', 'width');
$this->thumbnail_height = $this->thumbnail_height ?? common_config('thumbnail', 'height');
$this->max_size = $this->max_size ?? common_config('attachments', 'file_quota');
$this->crop = $this->crop ?? common_config('thumbnail', 'crop');
$this->max_size = $this->max_size ?? common_config('attachments', 'file_quota');
$this->crop = $this->crop ?? common_config('thumbnail', 'crop');
}
/**
@ -106,8 +107,10 @@ class EmbedPlugin extends Plugin
* on this event to add our action handler for Embed.
*
* @param $m URLMapper the router that was initialized.
* @return void true if successful, the exception object if it isn't.
*
* @throws Exception
*
* @return void true if successful, the exception object if it isn't.
*/
public function onRouterInitialized(URLMapper $m)
{
@ -122,35 +125,36 @@ class EmbedPlugin extends Plugin
* @param $url string the remote URL we're looking at
* @param $dom DOMDocument the document we're getting metadata from
* @param $metadata stdClass class representing the metadata
*
* @return bool true if successful, the exception object if it isn't.
*/
public function onGetRemoteUrlMetadataFromDom(string $url, DOMDocument $dom, stdClass &$metadata)
{
try {
common_log(LOG_INFO, "Trying to find Embed data for $url with 'oscarotero/Embed'");
$info = Embed::create($url);
common_log(LOG_INFO, "Trying to find Embed data for {$url} with 'oscarotero/Embed'");
$info = self::create($url);
$metadata->version = '1.0'; // Yes.
$metadata->provider_name = $info->authorName;
$metadata->title = $info->title;
$metadata->html = common_purify($info->description);
$metadata->type = $info->type;
$metadata->url = $info->url;
$metadata->version = '1.0'; // Yes.
$metadata->provider_name = $info->authorName;
$metadata->title = $info->title;
$metadata->html = common_purify($info->description);
$metadata->type = $info->type;
$metadata->url = $info->url;
$metadata->thumbnail_height = $info->imageHeight;
$metadata->thumbnail_width = $info->imageWidth;
$metadata->thumbnail_width = $info->imageWidth;
if (substr($info->image, 0, 4) === 'data') {
// Inline image
$imgData = base64_decode(substr($info->image, stripos($info->image, 'base64,') + 7));
list($filename, , ) = $this->validateAndWriteImage($imgData);
$imgData = base64_decode(substr($info->image, stripos($info->image, 'base64,') + 7));
list($filename) = $this->validateAndWriteImage($imgData);
// Use a file URI for images, as file_embed can't store a filename
$metadata->thumbnail_url = 'file://' . File_thumbnail::path($filename);
} else {
$metadata->thumbnail_url = $info->image;
}
} catch (Exception $e) {
common_log(LOG_INFO, "Failed to find Embed data for $url with 'oscarotero/Embed'" .
", got exception: " . get_class($e));
common_log(LOG_INFO, "Failed to find Embed data for {$url} with 'oscarotero/Embed'" .
', got exception: ' . get_class($e));
}
if (isset($metadata->thumbnail_url)) {
@ -158,15 +162,15 @@ class EmbedPlugin extends Plugin
// let's "be liberal in what you accept from others"!
// add protocol and host if the thumbnail_url starts with /
if ($metadata->thumbnail_url[0] == '/') {
$thumbnail_url_parsed = parse_url($metadata->url);
$metadata->thumbnail_url = "{$thumbnail_url_parsed['scheme']}://".
$thumbnail_url_parsed = parse_url($metadata->url);
$metadata->thumbnail_url = "{$thumbnail_url_parsed['scheme']}://" .
"{$thumbnail_url_parsed['host']}$metadata->thumbnail_url";
}
// some wordpress opengraph implementations sometimes return a white blank image
// no need for us to save that!
if ($metadata->thumbnail_url == 'https://s0.wp.com/i/blank.jpg') {
unset($metadata->thumbnail_url);
$metadata->thumbnail_url = null;
}
// FIXME: this is also true of locally-installed wordpress so we should watch out for that.
@ -174,6 +178,7 @@ class EmbedPlugin extends Plugin
return true;
}
/** Placeholder */
public function onEndShowHeadElements(Action $action)
{
switch ($action->getActionName()) {
@ -199,10 +204,10 @@ class EmbedPlugin extends Plugin
$action->element(
'link',
[
'rel' =>'alternate',
'type' => "application/$format+oembed",
'rel' => 'alternate',
'type' => "application/{$format}+oembed",
'href' => common_local_url('oembed', [], ['format' => $format, 'url' => $url]),
'title' => 'oEmbed'
'title' => 'oEmbed',
]
);
}
@ -210,6 +215,7 @@ class EmbedPlugin extends Plugin
return true;
}
/** Placeholder */
public function onEndShowStylesheets(Action $action)
{
$action->cssLink($this->path('css/embed.css'));
@ -221,9 +227,9 @@ class EmbedPlugin extends Plugin
*
* Normally this event is called through File::saveNew()
*
* @param File $file The newly inserted File object.
* @param File $file The newly inserted File object.
*
* @return boolean success
* @return bool success
*/
public function onEndFileSaveNew(File $file)
{
@ -234,17 +240,16 @@ class EmbedPlugin extends Plugin
}
if (isset($file->mimetype)
&& (('text/html' === substr($file->mimetype, 0, 9) ||
'application/xhtml+xml' === substr($file->mimetype, 0, 21)))) {
&& (('text/html' === substr($file->mimetype, 0, 9) || 'application/xhtml+xml' === substr($file->mimetype, 0, 21)))) {
try {
$embed_data = File_embed::getEmbed($file->url);
if ($embed_data === false) {
throw new Exception("Did not get Embed data from URL $file->url");
throw new Exception("Did not get Embed data from URL {$file->url}");
}
$file->setTitle($embed_data->title);
} catch (Exception $e) {
common_log(LOG_WARNING, sprintf(
__METHOD__.': %s thrown when getting embed data: %s',
__METHOD__ . ': %s thrown when getting embed data: %s',
get_class($e),
_ve($e->getMessage())
));
@ -256,25 +261,26 @@ class EmbedPlugin extends Plugin
return true;
}
/** Placeholder */
public function onEndShowAttachmentLink(HTMLOutputter $out, File $file)
{
$embed = File_embed::getKV('file_id', $file->getID());
if (empty($embed->author_name) && empty($embed->provider)) {
return true;
}
$out->elementStart('div', ['id'=>'oembed_info', 'class'=>'e-content']);
$out->elementStart('div', ['id' => 'oembed_info', 'class' => 'e-content']);
foreach (['author_name' => ['class' => ' author', 'url' => 'author_url'],
'provider' => ['class' => '', 'url' => 'provider_url']]
'provider' => ['class' => '', 'url' => 'provider_url'], ]
as $field => $options) {
if (!empty($embed->{$field})) {
$out->elementStart('div', "fn vcard" . $options['class']);
$out->elementStart('div', 'fn vcard' . $options['class']);
if (empty($embed->{$options['url']})) {
$out->text($embed->{$field});
} else {
$out->element(
'a',
['href' => $embed->{$options['url']},
'class' => 'url'],
['href' => $embed->{$options['url']},
'class' => 'url', ],
$embed->{$field}
);
}
@ -284,6 +290,7 @@ class EmbedPlugin extends Plugin
return false;
}
/** Placeholder */
public function onFileEnclosureMetadata(File $file, &$enclosure)
{
// Never treat generic HTML links as an enclosure type!
@ -301,6 +308,7 @@ class EmbedPlugin extends Plugin
return true;
}
/** Placeholder */
public function onStartShowAttachmentRepresentation(HTMLOutputter $out, File $file)
{
try {
@ -314,29 +322,29 @@ class EmbedPlugin extends Plugin
return true;
}
$out->elementStart('article', ['class'=>'h-entry embed']);
$out->elementStart('article', ['class' => 'h-entry embed']);
$out->elementStart('header');
try {
$thumb = $file->getThumbnail($this->thumbnail_width, $this->thumbnail_height);
$out->element('img', $thumb->getHtmlAttrs(['class'=>'u-photo embed']));
$out->element('img', $thumb->getHtmlAttrs(['class' => 'u-photo embed']));
unset($thumb);
} catch (FileNotFoundException $e) {
// Nothing to show
} catch (Exception $e) {
$out->element('div', ['class'=>'error'], $e->getMessage());
$out->element('div', ['class' => 'error'], $e->getMessage());
}
$out->elementStart('h5', ['class'=>'p-name embed']);
$out->element('a', ['class'=>'u-url', 'href'=>$file->getUrl()], common_strip_html($embed->title));
$out->elementStart('h5', ['class' => 'p-name embed']);
$out->element('a', ['class' => 'u-url', 'href' => $file->getUrl()], common_strip_html($embed->title));
$out->elementEnd('h5');
$out->elementStart('div', ['class'=>'p-author embed']);
$out->elementStart('div', ['class' => 'p-author embed']);
if (!empty($embed->author_name)) {
// TRANS: text before the author name of embed attachment representation
// FIXME: The whole "By x from y" should be i18n because of different language constructions.
$out->text(_('By '));
$attrs = ['class'=>'h-card p-author'];
$attrs = ['class' => 'h-card p-author'];
if (!empty($embed->author_url)) {
$attrs['href'] = $embed->author_url;
$tag = 'a';
$tag = 'a';
} else {
$tag = 'span';
}
@ -346,10 +354,10 @@ class EmbedPlugin extends Plugin
// TRANS: text between the embed author name and provider url
// FIXME: The whole "By x from y" should be i18n because of different language constructions.
$out->text(_(' from '));
$attrs = ['class'=>'h-card'];
$attrs = ['class' => 'h-card'];
if (!empty($embed->provider_url)) {
$attrs['href'] = $embed->provider_url;
$tag = 'a';
$tag = 'a';
} else {
$tag = 'span';
}
@ -357,7 +365,7 @@ class EmbedPlugin extends Plugin
}
$out->elementEnd('div');
$out->elementEnd('header');
$out->elementStart('div', ['class'=>'p-summary embed']);
$out->elementStart('div', ['class' => 'p-summary embed']);
$out->raw(common_purify($embed->html));
$out->elementEnd('div');
$out->elementStart('footer');
@ -367,6 +375,7 @@ class EmbedPlugin extends Plugin
return false;
}
/** Placeholder */
public function onShowUnsupportedAttachmentRepresentation(HTMLOutputter $out, File $file)
{
try {
@ -400,12 +409,14 @@ class EmbedPlugin extends Plugin
* @param $file File the file of the created thumbnail
* @param &$imgPath null|string = the path to the created thumbnail (output)
* @param $media string = media type
* @return bool true if it succeeds (including non-action
* states where it isn't oEmbed data, so it doesn't mess up the event handle
* for other things hooked into it), or the exception if it fails.
*
* @throws FileNotFoundException
* @throws NoResultException
* @throws ServerException
*
* @return bool true if it succeeds (including non-action
* states where it isn't oEmbed data, so it doesn't mess up the event handle
* for other things hooked into it), or the exception if it fails.
*/
public function onCreateFileImageThumbnailSource(File $file, ?string &$imgPath, string $media): bool
{
@ -417,7 +428,7 @@ class EmbedPlugin extends Plugin
// All our remote Embed images lack a local filename property in the File object
if ($file->isLocal()) {
common_debug(sprintf('File of id==%d is local (filename: %s), so nothing Embed '.
common_debug(sprintf('File of id==%d is local (filename: %s), so nothing Embed ' .
'should handle.', $file->getID(), _ve($file->filename)));
return true;
}
@ -428,7 +439,7 @@ class EmbedPlugin extends Plugin
$thumbnail = File_thumbnail::byFile($file);
} catch (NoResultException $e) {
// Not Embed data, or at least nothing we either can or want to use.
common_debug('No Embed data found for file id=='.$file->getID());
common_debug('No Embed data found for file id==' . $file->getID());
return true;
}
@ -459,8 +470,11 @@ class EmbedPlugin extends Plugin
}
/**
* @return bool false on no check made, provider name on success
* @throws ServerException if check is made but fails
* @param mixed $url
*
* @throws ServerException if check is made but fails
*
* @return bool false on no check made, provider name on success
*/
protected function checkWhitelist($url)
{
@ -470,7 +484,7 @@ class EmbedPlugin extends Plugin
$host = parse_url($url, PHP_URL_HOST);
foreach ($this->domain_whitelist as $regex => $provider) {
if (preg_match("/$regex/", $host)) {
if (preg_match("/{$regex}/", $host)) {
return $provider; // we trust this source, return provider name
}
}
@ -483,24 +497,27 @@ class EmbedPlugin extends Plugin
* the content-length variable returned. This isn't 100% foolproof but is
* reliable enough for our purposes.
*
* @return string|bool the file size if it succeeds, false otherwise.
* @param mixed $url
* @param null|mixed $headers
*
* @return bool|string the file size if it succeeds, false otherwise.
*/
private function getRemoteFileSize($url, $headers = null)
{
try {
if ($headers === null) {
if (!common_valid_http_url($url)) {
common_log(LOG_ERR, "Invalid URL in Embed::getRemoteFileSize()");
common_log(LOG_ERR, 'Invalid URL in Embed::getRemoteFileSize()');
return false;
}
$head = (new HTTPClient())->head($url);
$head = (new HTTPClient())->head($url);
$headers = $head->getHeader();
$headers = array_change_key_case($headers, CASE_LOWER);
}
return $headers['content-length'] ?? false;
} catch (Exception $err) {
common_log(LOG_ERR, __CLASS__.': getRemoteFileSize on URL : '._ve($url).
' threw exception: '.$err->getMessage());
common_log(LOG_ERR, __CLASS__ . ': getRemoteFileSize on URL : ' . _ve($url) .
' threw exception: ' . $err->getMessage());
return false;
}
}
@ -509,16 +526,19 @@ class EmbedPlugin extends Plugin
* A private helper function that uses a CURL lookup to check the mime type
* of a remote URL to see it it's an image.
*
* @param mixed $url
* @param null|mixed $headers
*
* @return bool true if the remote URL is an image, or false otherwise.
*/
private function isRemoteImage($url, $headers = null)
{
if (empty($headers)) {
if (!common_valid_http_url($url)) {
common_log(LOG_ERR, "Invalid URL in Embed::isRemoteImage()");
common_log(LOG_ERR, 'Invalid URL in Embed::isRemoteImage()');
return false;
}
$head = (new HTTPClient())->head($url);
$head = (new HTTPClient())->head($url);
$headers = $head->getHeader();
$headers = array_change_key_case($headers, CASE_LOWER);
}
@ -531,11 +551,11 @@ class EmbedPlugin extends Plugin
* by $this->thumbnail_height
*
* @param $imgData - The image data to validate. Taken by reference to avoid copying
* @param string|null $url - The url where the image came from, to fetch metadata
* @param array|null $headers - The headers possible previous request to $url
* @param int|null $file_id - The id of the file this image belongs to, used for logging
* @param null|string $url - The url where the image came from, to fetch metadata
* @param null|array $headers - The headers possible previous request to $url
* @param null|int $file_id - The id of the file this image belongs to, used for logging
*/
protected function validateAndWriteImage(&$imgData, ?string $url = null, ?array $headers = null, ?int $file_id = null) : array
protected function validateAndWriteImage(&$imgData, ?string $url = null, ?array $headers = null, ?int $file_id = null): array
{
$info = @getimagesizefromstring($imgData);
// array indexes documented on php.net:
@ -546,8 +566,8 @@ class EmbedPlugin extends Plugin
throw new UnsupportedMediaException(_('Image file had impossible geometry (0 width or height)'));
}
$width = min($info[0], $this->thumbnail_width);
$height = min($info[1], $this->thumbnail_height);
$width = min($info[0], $this->thumbnail_width);
$height = min($info[1], $this->thumbnail_height);
$filehash = hash(File::FILEHASH_ALG, $imgData);
try {
@ -556,8 +576,8 @@ class EmbedPlugin extends Plugin
}
$filename = MediaFile::encodeFilename($original_name ?? _m('Untitled attachment'), $filehash);
} catch (Exception $err) {
common_log(LOG_ERR, "Went to write a thumbnail to disk in StoreRemoteMediaPlugin::storeRemoteThumbnail " .
"but encountered error: $err");
common_log(LOG_ERR, 'Went to write a thumbnail to disk in StoreRemoteMediaPlugin::storeRemoteThumbnail ' .
"but encountered error: {$err}");
throw $err;
}
@ -581,21 +601,21 @@ class EmbedPlugin extends Plugin
if ($this->crop && ($info[0] > $this->thumbnail_width || $info[1] > $this->thumbnail_height)) {
try {
// Temporary object, not stored in DB
$img = new ImageFile(-1, $fullpath);
$img = new ImageFile(-1, $fullpath);
list($width, $height, $x, $y, $w, $h) = $img->scaleToFit($this->thumbnail_width, $this->thumbnail_height, $this->crop);
// The boundary box for our resizing
$box = [
'width' => $width, 'height' => $height,
'x' => $x, 'y' => $y,
'w' => $w, 'h' => $h,
'x' => $x, 'y' => $y,
'w' => $w, 'h' => $h,
];
$width = $box['width'];
$width = $box['width'];
$height = $box['height'];
$img->resizeTo($fullpath, $box);
} catch (\Intervention\Image\Exception\NotReadableException $e) {
common_log(LOG_ERR, "StoreRemoteMediaPlugin::storeRemoteThumbnail was unable to decode image with Intervention: $e");
common_log(LOG_ERR, "StoreRemoteMediaPlugin::storeRemoteThumbnail was unable to decode image with Intervention: {$e}");
// No need to interrupt processing
}
}
@ -606,8 +626,8 @@ class EmbedPlugin extends Plugin
} catch (AlreadyFulfilledException $e) {
// Carry on
} catch (Exception $err) {
common_log(LOG_ERR, "Went to write a thumbnail to disk in EmbedPlugin::storeRemoteThumbnail " .
"but encountered error: $err");
common_log(LOG_ERR, 'Went to write a thumbnail to disk in EmbedPlugin::storeRemoteThumbnail ' .
"but encountered error: {$err}");
throw $err;
} finally {
unset($imgData);
@ -620,8 +640,9 @@ class EmbedPlugin extends Plugin
* Function to create and store a thumbnail representation of a remote image
*
* @param $thumbnail File_thumbnail object containing the file thumbnail
*
* @return bool true if it succeeded, the exception if it fails, or false if it
* is limited by system limits (ie the file is too large.)
* is limited by system limits (ie the file is too large.)
*/
protected function storeRemoteFileThumbnail(File_thumbnail $thumbnail)
{
@ -635,13 +656,13 @@ class EmbedPlugin extends Plugin
if (substr($url, 0, 7) == 'file://') {
$filename = substr($url, 7);
$info = getimagesize($filename);
$info = getimagesize($filename);
$filename = basename($filename);
$width = $info[0];
$height = $info[1];
$width = $info[0];
$height = $info[1];
} else {
$this->checkWhitelist($url);
$head = (new HTTPClient())->head($url);
$head = (new HTTPClient())->head($url);
$headers = $head->getHeader();
$headers = array_change_key_case($headers, CASE_LOWER);
@ -649,16 +670,16 @@ class EmbedPlugin extends Plugin
$is_image = $this->isRemoteImage($url, $headers);
if ($is_image == true) {
$file_size = $this->getRemoteFileSize($url, $headers);
if (($file_size!=false) && ($file_size > $this->max_size)) {
common_debug("Went to store remote thumbnail of size " . $file_size .
" but the upload limit is " . $this->max_size . " so we aborted.");
if (($file_size != false) && ($file_size > $this->max_size)) {
common_debug('Went to store remote thumbnail of size ' . $file_size .
' but the upload limit is ' . $this->max_size . ' so we aborted.');
return false;
}
} else {
return false;
}
} catch (Exception $err) {
common_debug("Could not determine size of remote image, aborted local storage.");
common_debug('Could not determine size of remote image, aborted local storage.');
throw $err;
}
@ -683,22 +704,22 @@ class EmbedPlugin extends Plugin
}
} catch (UnsupportedMediaException $e) {
// Couldn't find anything that looks like an image, nothing to do
common_debug("Embed was not able to find an image for URL `$url`: " . $e->getMessage());
common_debug("Embed was not able to find an image for URL `{$url}`: " . $e->getMessage());
return false;
}
}
try {
// Update our database for the thumbnail record
$orig = clone($thumbnail);
$orig = clone $thumbnail;
$thumbnail->filename = $filename;
$thumbnail->width = $width;
$thumbnail->height = $height;
$thumbnail->width = $width;
$thumbnail->height = $height;
// Throws exception on failure.
$thumbnail->updateWithKeys($orig);
} catch (Exception $err) {
common_log(LOG_ERR, "Went to write a thumbnail entry to the database in " .
"EmbedPlugin::storeRemoteThumbnail but encountered error: ".$err);
common_log(LOG_ERR, 'Went to write a thumbnail entry to the database in ' .
'EmbedPlugin::storeRemoteThumbnail but encountered error: ' . $err);
throw $err;
}
return true;
@ -709,17 +730,17 @@ class EmbedPlugin extends Plugin
* Adds this plugin's version information to $versions array
*
* @param &$versions array inherited from parent
*
* @return bool true hook value
*/
public function onPluginVersion(array &$versions): bool
{
$versions[] = ['name' => 'Embed',
'version' => self::PLUGIN_VERSION,
'author' => 'Mikael Nordfeldth',
'homepage' => GNUSOCIAL_ENGINE_URL,
'description' =>
// TRANS: Plugin description.
_m('Plugin for using and representing oEmbed, OpenGraph and other data.')];
'version' => self::PLUGIN_VERSION,
'author' => 'Mikael Nordfeldth',
'homepage' => GNUSOCIAL_ENGINE_URL,
'description' => // TRANS: Plugin description.
_m('Plugin for using and representing oEmbed, OpenGraph and other data.'), ];
return true;
}
}

View File

@ -18,6 +18,7 @@
* OembedPlugin implementation for GNU social
*
* @package GNUsocial
*
* @author Craig Andrews <candrews@integralblue.com>
* @author Mikael Nordfeldth <mmn@hethane.se>
* @author hannes
@ -26,7 +27,7 @@
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
defined('GNUSOCIAL') || die();
namespace Plguin\Embed\actions;
/**
* Oembed provider implementation
@ -38,12 +39,13 @@ defined('GNUSOCIAL') || die();
*/
class OEmbedAction extends Action
{
/** Placeholder */
protected function handle()
{
parent::handle();
$url = $this->trimmed('url');
$tls = parse_url($url, PHP_URL_SCHEME) == 'https';
$url = $this->trimmed('url');
$tls = parse_url($url, PHP_URL_SCHEME) == 'https';
$root_url = common_root_url($tls);
if (substr(strtolower($url), 0, mb_strlen($root_url)) !== strtolower($root_url)) {
@ -58,20 +60,20 @@ class OEmbedAction extends Action
// $r->map will throw ClientException 404 if it fails to find a mapping
$proxy_args = $r->map($path);
$oembed=array();
$oembed['version']='1.0';
$oembed['provider_name']=common_config('site', 'name');
$oembed['provider_url']=common_root_url();
$oembed = [];
$oembed['version'] = '1.0';
$oembed['provider_name'] = common_config('site', 'name');
$oembed['provider_url'] = common_root_url();
switch ($proxy_args['action']) {
case 'shownotice':
$oembed['type']='link';
$oembed['type'] = 'link';
try {
$notice = Notice::getByID($proxy_args['notice']);
} catch (NoResultException $e) {
throw new ClientException($e->getMessage(), 404);
}
$profile = $notice->getProfile();
$profile = $notice->getProfile();
$authorname = $profile->getFancyName();
// TRANS: oEmbed title. %1$s is the author name, %2$s is the creation date.
$oembed['title'] = sprintf(
@ -79,36 +81,34 @@ class OEmbedAction extends Action
$authorname,
common_exact_date($notice->created)
);
$oembed['author_name']=$authorname;
$oembed['author_url']=$profile->profileurl;
$oembed['url']=$notice->getUrl();
$oembed['html']=$notice->getRendered();
$oembed['author_name'] = $authorname;
$oembed['author_url'] = $profile->profileurl;
$oembed['url'] = $notice->getUrl();
$oembed['html'] = $notice->getRendered();
// maybe add thumbnail
foreach ($notice->attachments() as $attachment) {
if (!$attachment instanceof File) {
common_debug('ATTACHMENTS array entry from notice id=='._ve($notice->getID()).
' is something else than a File dataobject: '._ve($attachment));
common_debug('ATTACHMENTS array entry from notice id==' . _ve($notice->getID()) .
' is something else than a File dataobject: ' . _ve($attachment));
continue;
}
try {
$thumb = $attachment->getThumbnail();
$thumb_url = $thumb->getUrl();
$thumb = $attachment->getThumbnail();
$thumb_url = $thumb->getUrl();
$oembed['thumbnail_url'] = $thumb_url;
break; // only first one
} catch (UseFileAsThumbnailException $e) {
$oembed['thumbnail_url'] = $attachment->getUrl();
break; // we're happy with that
} catch (ServerException $e) {
//
} catch (ClientException $e) {
//
}
}
break;
case 'attachment':
$id = $proxy_args['attachment'];
$id = $proxy_args['attachment'];
$attachment = File::getKV($id);
if (empty($attachment)) {
// TRANS: Client error displayed in oEmbed action when attachment not found.
@ -123,47 +123,47 @@ class OEmbedAction extends Action
))
) {
// Proxy the existing oembed information
$oembed['type']=$file_oembed->type;
$oembed['provider']=$file_oembed->provider;
$oembed['provider_url']=$file_oembed->provider_url;
$oembed['width']=$file_oembed->width;
$oembed['height']=$file_oembed->height;
$oembed['html']=$file_oembed->html;
$oembed['title']=$file_oembed->title;
$oembed['author_name']=$file_oembed->author_name;
$oembed['author_url']=$file_oembed->author_url;
$oembed['url']=$file_oembed->getUrl();
} elseif (substr($attachment->mimetype, 0, strlen('image/'))==='image/') {
$oembed['type']='photo';
$oembed['type'] = $file_oembed->type;
$oembed['provider'] = $file_oembed->provider;
$oembed['provider_url'] = $file_oembed->provider_url;
$oembed['width'] = $file_oembed->width;
$oembed['height'] = $file_oembed->height;
$oembed['html'] = $file_oembed->html;
$oembed['title'] = $file_oembed->title;
$oembed['author_name'] = $file_oembed->author_name;
$oembed['author_url'] = $file_oembed->author_url;
$oembed['url'] = $file_oembed->getUrl();
} elseif (substr($attachment->mimetype, 0, strlen('image/')) === 'image/') {
$oembed['type'] = 'photo';
if ($attachment->filename) {
$filepath = File::path($attachment->filename);
$gis = @getimagesize($filepath);
$gis = @getimagesize($filepath);
if ($gis) {
$oembed['width'] = $gis[0];
$oembed['width'] = $gis[0];
$oembed['height'] = $gis[1];
} else {
// TODO Either throw an error or find a fallback?
}
}
$oembed['url']=$attachment->getUrl();
$oembed['url'] = $attachment->getUrl();
try {
$thumb = $attachment->getThumbnail();
$oembed['thumbnail_url'] = $thumb->getUrl();
$oembed['thumbnail_width'] = $thumb->width;
$thumb = $attachment->getThumbnail();
$oembed['thumbnail_url'] = $thumb->getUrl();
$oembed['thumbnail_width'] = $thumb->width;
$oembed['thumbnail_height'] = $thumb->height;
unset($thumb);
} catch (UnsupportedMediaException $e) {
// No thumbnail data available
}
} else {
$oembed['type']='link';
$oembed['url']=common_local_url(
$oembed['type'] = 'link';
$oembed['url'] = common_local_url(
'attachment',
array('attachment' => $attachment->id)
['attachment' => $attachment->id]
);
}
if ($attachment->title) {
$oembed['title']=$attachment->title;
$oembed['title'] = $attachment->title;
}
break;
default:
@ -176,14 +176,14 @@ class OEmbedAction extends Action
case 'xml':
$this->init_document('xml');
$this->elementStart('oembed');
foreach (array(
'version', 'type', 'provider_name',
'provider_url', 'title', 'author_name',
'author_url', 'url', 'html', 'width',
'height', 'cache_age', 'thumbnail_url',
'thumbnail_width', 'thumbnail_height',
) as $key) {
if (isset($oembed[$key]) && $oembed[$key]!='') {
foreach ([
'version', 'type', 'provider_name',
'provider_url', 'title', 'author_name',
'author_url', 'url', 'html', 'width',
'height', 'cache_age', 'thumbnail_url',
'thumbnail_width', 'thumbnail_height',
] as $key) {
if (isset($oembed[$key]) && $oembed[$key] != '') {
$this->element($key, null, $oembed[$key]);
}
}
@ -203,6 +203,7 @@ class OEmbedAction extends Action
}
}
/** Placeholder */
public function init_document($type)
{
switch ($type) {
@ -216,7 +217,7 @@ class OEmbedAction extends Action
// Check for JSONP callback
$callback = $this->arg('callback');
if ($callback) {
print $callback . '(';
echo $callback . '(';
}
break;
default:
@ -226,6 +227,7 @@ class OEmbedAction extends Action
}
}
/** Placeholder */
public function end_document($type)
{
switch ($type) {
@ -236,7 +238,7 @@ class OEmbedAction extends Action
// Check for JSONP callback
$callback = $this->arg('callback');
if ($callback) {
print ')';
echo ')';
}
break;
default:
@ -244,7 +246,6 @@ class OEmbedAction extends Action
$this->serverError(_('Not a supported data format.'), 501);
break;
}
return;
}
/**
@ -252,7 +253,7 @@ class OEmbedAction extends Action
*
* @param array $args other arguments
*
* @return boolean is read only action?
* @return bool is read only action?
*/
public function isReadOnly($args)
{

View File

@ -21,11 +21,12 @@ namespace Plugin\Media\Controller;
*
* @category Personal
* @package GNUsocial
*
* @author Evan Prodromou <evan@status.net>
* @copyright 2008-2009 StatusNet, Inc.
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class Attachment_thumbnailAction extends Attachment_viewAction
class AttachmentThumbnail extends AttachmentView
{
protected $thumb_w; // max width
protected $thumb_h; // max height

View File

@ -26,6 +26,7 @@ namespace Plugin\Media\Controller;
*/
class AttachmentView extends Attachment
{
/** Placeholder */
public function showPage(): void
{
// Disable errors, to not mess with the file contents (suppress errors in case access to this

View File

@ -147,7 +147,7 @@ class ImageFile extends MediaFile
* Shortcut method to get an ImageFile from a File
*
* @param File $file
* @return ImageFile
*
* @throws ClientException
* @throws FileNotFoundException
* @throws NoResultException
@ -156,6 +156,7 @@ class ImageFile extends MediaFile
* @throws UseFileAsThumbnailException
*
* @return ImageFile
* @return ImageFile
*/
public static function fromFileObject(File $file)
{
@ -172,14 +173,15 @@ class ImageFile extends MediaFile
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());
// 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());
}
$filepath = $file->getPath();
return new self($file->getID(), $filepath, $file->filehash);
}
$filepath = $file->getPath();
return new self($file->getID(), $filepath, $file->filehash);
}
public function getPath()
@ -232,11 +234,11 @@ class ImageFile extends MediaFile
* 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
* @param Profile|null $scoped
* @param string|null $name
* @param int|null $file_id same as in this class constructor
* @return ImageFile
* @param string $url Remote image URL
* @param null|Profile $scoped
* @param null|string $name
* @param null|int $file_id same as in this class constructor
*
* @throws ClientException
* @throws HTTP_Request2_Exception
* @throws InvalidFilenameException
@ -246,6 +248,7 @@ class ImageFile extends MediaFile
* @throws UseFileAsThumbnailException
*
* @return ImageFile
* @return ImageFile
*/
public static function fromUrl(string $url, ?Profile $scoped = null, ?string $name = null, ?int $file_id = null): self
{
@ -524,10 +527,10 @@ class ImageFile extends MediaFile
$rw = ceil($width * $rh / $height);
}
}
return [(int)$rw, (int)$rh,
(int)$cx, (int)$cy,
is_null($cw) ? $width : (int)$cw,
is_null($ch) ? $height : (int)$ch];
return [(int) $rw, (int) $rh,
(int) $cx, (int) $cy,
is_null($cw) ? $width : (int) $cw,
is_null($ch) ? $height : (int) $ch, ];
}
/**
@ -570,7 +573,7 @@ class ImageFile extends MediaFile
* @param $height
* @param $crop
* @param false $upscale
* @return File_thumbnail
*
* @throws ClientException
* @throws FileNotFoundException
* @throws FileNotStoredLocallyException
@ -578,6 +581,8 @@ class ImageFile extends MediaFile
* @throws ServerException
* @throws UnsupportedMediaException
* @throws UseFileAsThumbnailException
*
* @return File_thumbnail
*/
public function getFileThumbnail($width = null, $height = null, $crop = null, $upscale = false)
{

View File

@ -95,18 +95,21 @@ class MediaFile
* Shortcut method to get a MediaFile from a File
*
* @param File $file
* @return MediaFile|ImageFile
*
* @throws ClientException
* @throws FileNotFoundException
* @throws NoResultException
* @throws ServerException
*
* @return ImageFile|MediaFile
*/
public static function fromFileObject(File $file)
{
$filepath = null;
try {
$filepath = $file->getPath();
} catch (Exception $e) {}
} catch (Exception $e) {
}
return new self($filepath, common_get_mime_media($file->mimetype), $file->filehash, $file->getID());
}
@ -121,10 +124,11 @@ class MediaFile
}
/**
* @param bool|null $use_local true means require local, null means prefer original, false means use whatever is stored
* @param null|bool $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
public function getUrl(?bool $use_local = null): ?string
{
if ($use_local !== false) {
if (empty($this->fileurl)) {
@ -147,7 +151,7 @@ class MediaFile
return $this->getFile()->getEnclosure();
}
public function delete($useWhere=false)
public function delete($useWhere = false)
{
if (!is_null($this->fileRecord)) {
$this->fileRecord->delete($useWhere);
@ -155,6 +159,9 @@ class MediaFile
@unlink($this->filepath);
}
/**
* Remove the file from filesystem
*/
public function unlink()
{
$this->filename = null;
@ -228,8 +235,8 @@ class MediaFile
$file = new File;
$file->filename = $this->filename;
$file->url = $this->fileurl;
$file->urlhash = is_null($file->url) ? null : File::hashurl($file->url);
$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) {
@ -248,9 +255,9 @@ class MediaFile
// Set file geometrical properties if available
try {
$image = ImageFile::fromFileObject($file);
$orig = clone($file);
$file->width = $image->width;
$image = ImageFile::fromFileObject($file);
$orig = clone $file;
$file->width = $image->width;
$file->height = $image->height;
$file->update($orig);
@ -448,7 +455,7 @@ class MediaFile
}
$filehash = strtolower(self::getHashOfFile($_FILES[$param]['tmp_name']));
$fileid = null;
$fileid = null;
try {
$file = File::getByHash($filehash);
// There can be more than one file for the same filehash IF the url are different (due to different metadata).
@ -460,7 +467,7 @@ class MediaFile
try {
return ImageFile::fromFileObject($file);
} catch (UnsupportedMediaException $e) {
return MediaFile::fromFileObject($file);
return self::fromFileObject($file);
}
}
// Assert: If we got to this line, then we only traversed URLs on the while loop above.
@ -469,7 +476,7 @@ class MediaFile
$filepath = $file->getPath(); // This function will throw FileNotFoundException if not.
// Assert: If we got to this line, then we can use this file and just add redirections.
$mimetype = $file->mimetype;
$fileid = $file->getID();
$fileid = $file->getID();
} else {
throw new FileNotFoundException('This isn\'t a path.'); // A bit of dadaist art.
// It's natural that a sysadmin prefers to not add redirections to the first remote link of an
@ -530,11 +537,11 @@ class MediaFile
* 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
* @param string|null $name
* @param int|null $file_id same as in this class constructor
* @return ImageFile|MediaFile
* @param string $url Remote media URL
* @param null|Profile $scoped
* @param null|string $name
* @param null|int $file_id same as in this class constructor
*
* @throws ClientException
* @throws HTTP_Request2_Exception
* @throws InvalidFilenameException
@ -544,6 +551,7 @@ class MediaFile
* @throws UseFileAsThumbnailException
*
* @return ImageFile|MediaFile
* @return ImageFile|MediaFile
*/
public static function fromUrl(string $url, ?Profile $scoped = null, ?string $name = null, ?int $file_id = null)
{
@ -557,7 +565,7 @@ class MediaFile
'unnecessarily downloading too large files. URL: %s',
$url));
$head = $http->head($url);
$url = $head->getEffectiveUrl(); // to avoid going through redirects again
$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));
}
@ -565,7 +573,7 @@ class MediaFile
$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'];
$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.".
@ -591,8 +599,7 @@ class MediaFile
} else {
throw new ServerException(sprintf('Invalid remote media URL headers %s.', $url));
}
unset($head);
unset($headers);
unset($head, $headers);
$tempfile = new TemporaryFile('gs-mediafile');
fwrite($tempfile->getResource(), HTTPClient::quickGet($url));

View File

@ -14,10 +14,15 @@
// 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/>.
namespace Plugin\StoreRemoteMedia;
use App\Core\Module;
/**
* The StoreRemoteMedia plugin downloads remotely attached files to local server.
*
* @package GNUsocial
*
* @author Mikael Nordfeldth
* @author Stephen Paul Weber
* @author Mikael Nordfeldth
@ -26,7 +31,7 @@
* @copyright 2015-2016, 2019-2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class StoreRemoteMediaPlugin extends Plugin
class StoreRemoteMedia extends Module
{
const PLUGIN_VERSION = '3.0.0';
@ -41,11 +46,11 @@ class StoreRemoteMediaPlugin extends Plugin
public $append_whitelist = []; // fill this array as domain_whitelist to add more trusted sources
public $check_whitelist = false; // security/abuse precaution
public $store_original = false; // Whether to maintain a copy of the original media or only a thumbnail of it
public $thumbnail_width = null;
public $thumbnail_height = null;
public $crop = null;
public $max_size = null;
public $store_original = false; // Whether to maintain a copy of the original media or only a thumbnail of it
public $thumbnail_width;
public $thumbnail_height;
public $crop;
public $max_size;
/**
* Initialize the StoreRemoteMedia plugin and set up the environment it needs for it.
@ -59,10 +64,10 @@ class StoreRemoteMediaPlugin extends Plugin
$this->domain_whitelist = array_merge($this->domain_whitelist, $this->append_whitelist);
// Load global configuration if specific not provided
$this->thumbnail_width = $this->thumbnail_width ?? common_config('thumbnail', 'width');
$this->thumbnail_width = $this->thumbnail_width ?? common_config('thumbnail', 'width');
$this->thumbnail_height = $this->thumbnail_height ?? common_config('thumbnail', 'height');
$this->max_size = $this->max_size ?? common_config('attachments', 'file_quota');
$this->crop = $this->crop ?? common_config('thumbnail', 'crop');
$this->max_size = $this->max_size ?? common_config('attachments', 'file_quota');
$this->crop = $this->crop ?? common_config('thumbnail', 'crop');
}
/**
@ -72,14 +77,16 @@ class StoreRemoteMediaPlugin extends Plugin
* @param $file File the file of the created thumbnail
* @param &$imgPath null|string = out the path to the created thumbnail (output parameter)
* @param $media string = media type (unused)
* @return bool
*
* @throws AlreadyFulfilledException
* @throws FileNotFoundException
* @throws FileNotStoredLocallyException
* @throws HTTP_Request2_Exception
* @throws ServerException
*
* @return bool
*/
public function onCreateFileImageThumbnailSource(File $file, ?string &$imgPath = null, ?string $media=null): bool
public function onCreateFileImageThumbnailSource(File $file, ?string &$imgPath = null, ?string $media = null): bool
{
// If we are on a private node, we won't do any remote calls (just as a precaution until
// we can configure this from config.php for the private nodes)
@ -89,7 +96,7 @@ class StoreRemoteMediaPlugin extends Plugin
// If there is a local filename, it is either a local file already or has already been downloaded.
if (!$file->isStoredRemotely()) {
common_debug(sprintf('File id==%d isn\'t a non-fetched remote file (%s), so nothing StoreRemoteMedia '.
common_debug(sprintf('File id==%d isn\'t a non-fetched remote file (%s), so nothing StoreRemoteMedia ' .
'should handle.', $file->getID(), _ve($file->filename)));
return true;
}
@ -106,13 +113,13 @@ class StoreRemoteMediaPlugin extends Plugin
if (substr($url, 0, 7) == 'file://') {
$filename = substr($url, 7);
$info = getimagesize($filename);
$info = getimagesize($filename);
$filename = basename($filename);
$width = $info[0];
$height = $info[1];
$width = $info[0];
$height = $info[1];
} else {
$this->checkWhitelist($url);
$head = (new HTTPClient())->head($url);
$head = (new HTTPClient())->head($url);
$headers = $head->getHeader();
$headers = array_change_key_case($headers, CASE_LOWER);
@ -120,16 +127,16 @@ class StoreRemoteMediaPlugin extends Plugin
$is_image = $this->isRemoteImage($url, $headers);
if ($is_image == true) {
$file_size = $this->getRemoteFileSize($url, $headers);
if (($file_size!=false) && ($file_size > $this->max_size)) {
common_debug("Went to store remote thumbnail of size " . $file_size .
" but the upload limit is " . $this->max_size . " so we aborted.");
if (($file_size != false) && ($file_size > $this->max_size)) {
common_debug('Went to store remote thumbnail of size ' . $file_size .
' but the upload limit is ' . $this->max_size . ' so we aborted.');
return false;
}
} else {
return false;
}
} catch (Exception $err) {
common_debug("Could not determine size of remote image, aborted local storage.");
common_debug('Could not determine size of remote image, aborted local storage.');
throw $err;
}
@ -154,7 +161,7 @@ class StoreRemoteMediaPlugin extends Plugin
}
} catch (UnsupportedMediaException $e) {
// Couldn't find anything that looks like an image, nothing to do
common_debug("StoreRemoteMedia was not able to find an image for URL `$url`: " . $e->getMessage());
common_debug("StoreRemoteMedia was not able to find an image for URL `{$url}`: " . $e->getMessage());
return false;
}
}
@ -163,33 +170,33 @@ class StoreRemoteMediaPlugin extends Plugin
if ($this->store_original) {
try {
// Update our database for the file record
$orig = clone($file);
$orig = clone $file;
$file->filename = $filename;
$file->filehash = $filehash;
$file->width = $width;
$file->height = $height;
$file->width = $width;
$file->height = $height;
// Throws exception on failure.
$file->updateWithKeys($orig);
} catch (Exception $err) {
common_log(LOG_ERR, "Went to update a file entry on the database in " .
"StoreRemoteMediaPlugin::storeRemoteThumbnail but encountered error: " . $err);
common_log(LOG_ERR, 'Went to update a file entry on the database in ' .
'StoreRemoteMediaPlugin::storeRemoteThumbnail but encountered error: ' . $err);
throw $err;
}
} else {
try {
// Insert a thumbnail record for this file
$data = new stdClass();
$data->thumbnail_url = $url;
$data->thumbnail_width = $width;
$data = new stdClass();
$data->thumbnail_url = $url;
$data->thumbnail_width = $width;
$data->thumbnail_height = $height;
File_thumbnail::saveNew($data, $file->getID());
$ft = File_thumbnail::byFile($file);
$orig = clone($ft);
$ft = File_thumbnail::byFile($file);
$orig = clone $ft;
$ft->filename = $filename;
$ft->updateWithKeys($orig);
} catch (Exception $err) {
common_log(LOG_ERR, "Went to write a thumbnail entry to the database in " .
"StoreRemoteMediaPlugin::storeRemoteThumbnail but encountered error: " . $err);
common_log(LOG_ERR, 'Went to write a thumbnail entry to the database in ' .
'StoreRemoteMediaPlugin::storeRemoteThumbnail but encountered error: ' . $err);
throw $err;
}
}
@ -208,24 +215,27 @@ class StoreRemoteMediaPlugin extends Plugin
* the content-length variable returned. This isn't 100% foolproof but is
* reliable enough for our purposes.
*
* @return string|bool the file size if it succeeds, false otherwise.
* @param mixed $url
* @param null|mixed $headers
*
* @return bool|string the file size if it succeeds, false otherwise.
*/
private function getRemoteFileSize($url, $headers = null)
{
try {
if ($headers === null) {
if (!common_valid_http_url($url)) {
common_log(LOG_ERR, "Invalid URL in StoreRemoteMedia::getRemoteFileSize()");
common_log(LOG_ERR, 'Invalid URL in StoreRemoteMedia::getRemoteFileSize()');
return false;
}
$head = (new HTTPClient())->head($url);
$head = (new HTTPClient())->head($url);
$headers = $head->getHeader();
$headers = array_change_key_case($headers, CASE_LOWER);
}
return $headers['content-length'] ?? false;
} catch (Exception $err) {
common_log(LOG_ERR, __CLASS__.': getRemoteFileSize on URL : '._ve($url).
' threw exception: '.$err->getMessage());
common_log(LOG_ERR, __CLASS__ . ': getRemoteFileSize on URL : ' . _ve($url) .
' threw exception: ' . $err->getMessage());
return false;
}
}
@ -234,16 +244,19 @@ class StoreRemoteMediaPlugin extends Plugin
* A private helper function that uses a CURL lookup to check the mime type
* of a remote URL to see it it's an image.
*
* @param mixed $url
* @param null|mixed $headers
*
* @return bool true if the remote URL is an image, or false otherwise.
*/
private function isRemoteImage($url, $headers = null): bool
{
if (empty($headers)) {
if (!common_valid_http_url($url)) {
common_log(LOG_ERR, "Invalid URL in StoreRemoteMedia::isRemoteImage()");
common_log(LOG_ERR, 'Invalid URL in StoreRemoteMedia::isRemoteImage()');
return false;
}
$head = (new HTTPClient())->head($url);
$head = (new HTTPClient())->head($url);
$headers = $head->getHeader();
$headers = array_change_key_case($headers, CASE_LOWER);
}
@ -256,11 +269,11 @@ class StoreRemoteMediaPlugin extends Plugin
* by $this->thumbnail_height
*
* @param $imgData - The image data to validate. Taken by reference to avoid copying
* @param string|null $url - The url where the image came from, to fetch metadata
* @param array|null $headers - The headers possible previous request to $url
* @param int|null $file_id - The id of the file this image belongs to, used for logging
* @param null|string $url - The url where the image came from, to fetch metadata
* @param null|array $headers - The headers possible previous request to $url
* @param null|int $file_id - The id of the file this image belongs to, used for logging
*/
protected function validateAndWriteImage(&$imgData, ?string $url = null, ?array $headers = null, ?int $file_id = null) : array
protected function validateAndWriteImage(&$imgData, ?string $url = null, ?array $headers = null, ?int $file_id = null): array
{
$info = @getimagesizefromstring($imgData);
// array indexes documented on php.net:
@ -271,8 +284,8 @@ class StoreRemoteMediaPlugin extends Plugin
throw new UnsupportedMediaException(_m('Image file had impossible geometry (0 width or height)'));
}
$width = min($info[0], $this->thumbnail_width);
$height = min($info[1], $this->thumbnail_height);
$width = min($info[0], $this->thumbnail_width);
$height = min($info[1], $this->thumbnail_height);
$filehash = hash(File::FILEHASH_ALG, $imgData);
try {
@ -281,8 +294,8 @@ class StoreRemoteMediaPlugin extends Plugin
}
$filename = MediaFile::encodeFilename($original_name ?? _m('Untitled attachment'), $filehash);
} catch (Exception $err) {
common_log(LOG_ERR, "Went to write a thumbnail to disk in StoreRemoteMediaPlugin::storeRemoteThumbnail " .
"but encountered error: $err");
common_log(LOG_ERR, 'Went to write a thumbnail to disk in StoreRemoteMediaPlugin::storeRemoteThumbnail ' .
"but encountered error: {$err}");
throw $err;
}
@ -306,21 +319,21 @@ class StoreRemoteMediaPlugin extends Plugin
if (!$this->store_original && $this->crop && ($info[0] > $this->thumbnail_width || $info[1] > $this->thumbnail_height)) {
try {
// Temporary object, not stored in DB
$img = new ImageFile(-1, $fullpath);
$img = new ImageFile(-1, $fullpath);
list($width, $height, $x, $y, $w, $h) = $img->scaleToFit($this->thumbnail_width, $this->thumbnail_height, $this->crop);
// The boundary box for our resizing
$box = [
'width' => $width, 'height' => $height,
'x' => $x, 'y' => $y,
'w' => $w, 'h' => $h,
'x' => $x, 'y' => $y,
'w' => $w, 'h' => $h,
];
$width = $box['width'];
$width = $box['width'];
$height = $box['height'];
$img->resizeTo($fullpath, $box);
} catch (\Intervention\Image\Exception\NotReadableException $e) {
common_log(LOG_ERR, "StoreRemoteMediaPlugin::storeRemoteThumbnail was unable to decode image with Intervention: $e");
common_log(LOG_ERR, "StoreRemoteMediaPlugin::storeRemoteThumbnail was unable to decode image with Intervention: {$e}");
// No need to interrupt processing
}
}
@ -331,8 +344,8 @@ class StoreRemoteMediaPlugin extends Plugin
} catch (AlreadyFulfilledException $e) {
// Carry on
} catch (Exception $err) {
common_log(LOG_ERR, "Went to write a thumbnail to disk in StoreRemoteMediaPlugin::storeRemoteThumbnail " .
"but encountered error: $err");
common_log(LOG_ERR, 'Went to write a thumbnail to disk in StoreRemoteMediaPlugin::storeRemoteThumbnail ' .
"but encountered error: {$err}");
throw $err;
} finally {
unset($imgData);
@ -342,8 +355,11 @@ class StoreRemoteMediaPlugin extends Plugin
}
/**
* @return bool false on no check made, provider name on success
* @throws ServerException if check is made but fails
* @param mixed $url
*
* @throws ServerException if check is made but fails
*
* @return bool false on no check made, provider name on success
*/
protected function checkWhitelist($url)
{
@ -353,7 +369,7 @@ class StoreRemoteMediaPlugin extends Plugin
$host = parse_url($url, PHP_URL_HOST);
foreach ($this->domain_whitelist as $regex => $provider) {
if (preg_match("/$regex/", $host)) {
if (preg_match("/{$regex}/", $host)) {
return $provider; // we trust this source, return provider name
}
}
@ -366,17 +382,17 @@ class StoreRemoteMediaPlugin extends Plugin
* Adds this plugin's version information to $versions array
*
* @param &$versions array inherited from parent
*
* @return bool true hook value
*/
public function onPluginVersion(array &$versions): bool
{
$versions[] = ['name' => 'StoreRemoteMedia',
'version' => self::PLUGIN_VERSION,
'author' => 'Mikael Nordfeldth, Diogo Peralta Cordeiro',
'homepage' => GNUSOCIAL_ENGINE_URL,
'description' =>
// TRANS: Plugin description.
_m('Plugin for downloading remotely attached files to local server.')];
'version' => self::PLUGIN_VERSION,
'author' => 'Mikael Nordfeldth, Diogo Peralta Cordeiro',
'homepage' => GNUSOCIAL_ENGINE_URL,
'description' => // TRANS: Plugin description.
_m('Plugin for downloading remotely attached files to local server.'), ];
return true;
}
}
}

View File

@ -1,19 +1,189 @@
{
"alchemy/resource-component": {
"version": "0.1.1"
},
"alchemy/zippy": {
"version": "v0.5.x-dev"
},
"beberlei/assert": {
"version": "v2.9.9"
},
"composer/semver": {
"version": "3.2.4"
},
"composer/xdebug-handler": {
"version": "1.4.6"
},
"doctrine/annotations": {
"version": "1.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "1.0",
"ref": "a2759dd6123694c8d901d0ec80006e044c2e6457"
},
"files": [
"config/routes/annotations.yaml"
]
},
"doctrine/cache": {
"version": "1.10.2"
},
"doctrine/collections": {
"version": "1.6.7"
},
"doctrine/common": {
"version": "3.1.2"
},
"doctrine/dbal": {
"version": "2.13.0"
},
"doctrine/deprecations": {
"version": "v0.5.3"
},
"doctrine/doctrine-bundle": {
"version": "2.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "2.3",
"ref": "6e0f582596a8f1c865aaa0d3cceb9bf0daf609b4"
},
"files": [
"config/packages/doctrine.yaml",
"config/packages/prod/doctrine.yaml",
"config/packages/test/doctrine.yaml",
"src/Entity/.gitignore",
"src/Repository/.gitignore"
]
},
"doctrine/doctrine-migrations-bundle": {
"version": "3.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "3.1",
"ref": "ee609429c9ee23e22d6fa5728211768f51ed2818"
},
"files": [
"config/packages/doctrine_migrations.yaml",
"migrations/.gitignore"
]
},
"doctrine/event-manager": {
"version": "1.1.1"
},
"doctrine/inflector": {
"version": "2.0.3"
},
"doctrine/instantiator": {
"version": "1.4.0"
},
"doctrine/lexer": {
"version": "1.2.1"
},
"doctrine/migrations": {
"version": "3.1.1"
},
"doctrine/orm": {
"version": "2.8.4"
},
"doctrine/persistence": {
"version": "2.1.0"
},
"doctrine/sql-formatter": {
"version": "1.1.1"
},
"egulias/email-validator": {
"version": "3.1.1"
},
"erusev/parsedown": {
"version": "1.7.4"
},
"friendsofphp/php-cs-fixer": {
"version": "2.17",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "2.17",
"ref": "f0d4b4ddb4e2bb73b227e3987b6ecc53cd8be431"
},
"files": [
".php_cs.dist"
]
},
"friendsofphp/proxy-manager-lts": {
"version": "v1.0.3"
},
"giggsey/libphonenumber-for-php": {
"version": "8.12.21"
},
"giggsey/locale": {
"version": "1.9"
},
"jchook/phpunit-assert-throws": {
"version": "v1.0.3"
},
"laminas/laminas-code": {
"version": "4.1.0"
},
"laminas/laminas-eventmanager": {
"version": "3.3.1"
},
"laminas/laminas-zendframework-bridge": {
"version": "1.2.0"
},
"league/uri-parser": {
"version": "1.4.1"
},
"lstrojny/functional-php": {
"version": "1.17.0"
},
"masterminds/html5": {
"version": "2.7.4"
},
"monolog/monolog": {
"version": "2.2.0"
},
"myclabs/deep-copy": {
"version": "1.10.2"
},
"niels-de-blaauw/php-doc-check": {
"version": "v0.2.2"
},
"nikic/php-parser": {
"version": "v4.10.4"
},
"odolbeau/phone-number-bundle": {
"version": "3.0",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "master",
"version": "3.0",
"ref": "4388686329b81291918a948cd42891829fb1de71"
}
},
"phar-io/manifest": {
"version": "2.0.1"
},
"phar-io/version": {
"version": "3.1.0"
},
"php-cs-fixer/diff": {
"version": "v1.3.1"
},
"php-ds/php-ds": {
"version": "v1.3.0"
},
"phpdocumentor/reflection-common": {
"version": "2.2.0"
},
"phpdocumentor/reflection-docblock": {
"version": "5.2.2"
},
"phpdocumentor/type-resolver": {
"version": "1.4.0"
},
"phpspec/prophecy": {
"version": "1.13.0"
},
@ -46,6 +216,21 @@
"tests/bootstrap.php"
]
},
"psr/cache": {
"version": "1.0.1"
},
"psr/container": {
"version": "1.1.1"
},
"psr/event-dispatcher": {
"version": "1.0.0"
},
"psr/link": {
"version": "1.1.1"
},
"psr/log": {
"version": "1.1.3"
},
"sebastian/cli-parser": {
"version": "1.0.1"
},
@ -94,7 +279,449 @@
"sebastian/version": {
"version": "3.0.2"
},
"sensio/framework-extra-bundle": {
"version": "5.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "5.2",
"ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b"
},
"files": [
"config/packages/sensio_framework_extra.yaml"
]
},
"someonewithpc/memcached-polyfill": {
"version": "1.0.1"
},
"someonewithpc/redis-polyfill": {
"version": "dev-master"
},
"symfony/amqp-messenger": {
"version": "v5.2.4"
},
"symfony/asset": {
"version": "v5.2.4"
},
"symfony/browser-kit": {
"version": "v5.2.4"
},
"symfony/cache": {
"version": "v5.2.6"
},
"symfony/cache-contracts": {
"version": "v2.2.0"
},
"symfony/config": {
"version": "v5.2.4"
},
"symfony/console": {
"version": "5.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "5.1",
"ref": "c6d02bdfba9da13c22157520e32a602dbee8a75c"
},
"files": [
"bin/console"
]
},
"symfony/css-selector": {
"version": "v5.2.4"
},
"symfony/debug-bundle": {
"version": "4.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "4.1",
"ref": "f8863cbad2f2e58c4b65fa1eac892ab189971bea"
},
"files": [
"config/packages/dev/debug.yaml"
]
},
"symfony/debug-pack": {
"version": "v1.0.9"
},
"symfony/dependency-injection": {
"version": "v5.2.6"
},
"symfony/deprecation-contracts": {
"version": "v2.2.0"
},
"symfony/doctrine-bridge": {
"version": "v5.2.6"
},
"symfony/doctrine-messenger": {
"version": "v5.2.5"
},
"symfony/dom-crawler": {
"version": "v5.2.4"
},
"symfony/dotenv": {
"version": "v5.2.4"
},
"symfony/error-handler": {
"version": "v5.2.6"
},
"symfony/event-dispatcher": {
"version": "v5.2.4"
},
"symfony/event-dispatcher-contracts": {
"version": "v2.2.0"
},
"symfony/expression-language": {
"version": "v5.2.4"
},
"symfony/filesystem": {
"version": "v5.2.6"
},
"symfony/finder": {
"version": "v5.2.4"
},
"symfony/flex": {
"version": "1.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "1.0",
"ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e"
},
"files": [
".env"
]
},
"symfony/form": {
"version": "v5.2.6"
},
"symfony/framework-bundle": {
"version": "5.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "5.2",
"ref": "6ec87563dcc85cd0c48856dcfbfc29610506d250"
},
"files": [
"config/packages/cache.yaml",
"config/packages/framework.yaml",
"config/packages/test/framework.yaml",
"config/preload.php",
"config/routes/dev/framework.yaml",
"config/services.yaml",
"public/index.php",
"src/Controller/.gitignore",
"src/Kernel.php"
]
},
"symfony/http-client": {
"version": "v5.2.6"
},
"symfony/http-client-contracts": {
"version": "v2.3.1"
},
"symfony/http-foundation": {
"version": "v5.2.4"
},
"symfony/http-kernel": {
"version": "v5.2.6"
},
"symfony/intl": {
"version": "v5.2.4"
},
"symfony/mailer": {
"version": "4.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "4.3",
"ref": "15658c2a0176cda2e7dba66276a2030b52bd81b2"
},
"files": [
"config/packages/mailer.yaml"
]
},
"symfony/maker-bundle": {
"version": "1.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "1.0",
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
}
},
"symfony/messenger": {
"version": "4.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "4.3",
"ref": "e9a414b113ceadbf4e52abe37bf8f1b443f06ccb"
},
"files": [
"config/packages/messenger.yaml"
]
},
"symfony/mime": {
"version": "v5.2.6"
},
"symfony/monolog-bridge": {
"version": "v5.2.5"
},
"symfony/monolog-bundle": {
"version": "3.7",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "3.7",
"ref": "f4adb4379ee437f91ecb1bd5a41c1de6286b9a04"
},
"files": [
"config/packages/dev/monolog.yaml",
"config/packages/prod/deprecations.yaml",
"config/packages/prod/monolog.yaml",
"config/packages/test/monolog.yaml"
]
},
"symfony/notifier": {
"version": "5.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "5.0",
"ref": "c31585e252b32fe0e1f30b1f256af553f4a06eb9"
},
"files": [
"config/packages/notifier.yaml"
]
},
"symfony/options-resolver": {
"version": "v5.2.4"
},
"symfony/orm-pack": {
"version": "v2.1.0"
},
"symfony/phpunit-bridge": {
"version": "5.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "5.1",
"ref": "bf16921ef8309a81d9f046e9b6369c46bcbd031f"
},
"files": [
".env.test",
"bin/phpunit",
"phpunit.xml.dist",
"tests/bootstrap.php"
]
},
"symfony/polyfill-intl-grapheme": {
"version": "v1.22.1"
},
"symfony/polyfill-intl-icu": {
"version": "v1.22.1"
},
"symfony/polyfill-intl-idn": {
"version": "v1.22.1"
},
"symfony/polyfill-intl-normalizer": {
"version": "v1.22.1"
},
"symfony/polyfill-mbstring": {
"version": "v1.22.1"
},
"symfony/polyfill-php73": {
"version": "v1.22.1"
},
"symfony/polyfill-php80": {
"version": "v1.22.1"
},
"symfony/process": {
"version": "v5.2.4"
},
"symfony/profiler-pack": {
"version": "v1.0.5"
},
"symfony/property-access": {
"version": "v5.2.4"
},
"symfony/property-info": {
"version": "v5.2.4"
},
"symfony/proxy-manager-bridge": {
"version": "v5.2.4"
},
"symfony/redis-messenger": {
"version": "v5.2.4"
},
"symfony/routing": {
"version": "5.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "5.1",
"ref": "b4f3e7c95e38b606eef467e8a42a8408fc460c43"
},
"files": [
"config/packages/prod/routing.yaml",
"config/packages/routing.yaml",
"config/routes.yaml"
]
},
"symfony/security-bundle": {
"version": "5.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "5.1",
"ref": "0a4bae19389d3b9cba1ca0102e3b2bccea724603"
},
"files": [
"config/packages/security.yaml"
]
},
"symfony/security-core": {
"version": "v5.2.6"
},
"symfony/security-csrf": {
"version": "v5.2.4"
},
"symfony/security-guard": {
"version": "v5.2.4"
},
"symfony/security-http": {
"version": "v5.2.6"
},
"symfony/serializer": {
"version": "v5.2.4"
},
"symfony/serializer-pack": {
"version": "v1.0.4"
},
"symfony/service-contracts": {
"version": "v2.2.0"
},
"symfony/stopwatch": {
"version": "v5.2.4"
},
"symfony/string": {
"version": "v5.2.6"
},
"symfony/test-pack": {
"version": "v1.0.7"
},
"symfony/translation": {
"version": "3.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "3.3",
"ref": "2ad9d2545bce8ca1a863e50e92141f0b9d87ffcd"
},
"files": [
"config/packages/translation.yaml",
"translations/.gitignore"
]
},
"symfony/translation-contracts": {
"version": "v2.3.0"
},
"symfony/twig-bridge": {
"version": "v5.2.6"
},
"symfony/twig-bundle": {
"version": "5.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "5.0",
"ref": "fab9149bbaa4d5eca054ed93f9e1b66cc500895d"
},
"files": [
"config/packages/test/twig.yaml",
"config/packages/twig.yaml",
"templates/base.html.twig"
]
},
"symfony/twig-pack": {
"version": "v1.0.1"
},
"symfony/validator": {
"version": "4.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "4.3",
"ref": "d902da3e4952f18d3bf05aab29512eb61cabd869"
},
"files": [
"config/packages/test/validator.yaml",
"config/packages/validator.yaml"
]
},
"symfony/var-dumper": {
"version": "v5.2.6"
},
"symfony/var-exporter": {
"version": "v5.2.4"
},
"symfony/web-link": {
"version": "v5.2.5"
},
"symfony/web-profiler-bundle": {
"version": "3.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "3.3",
"ref": "6bdfa1a95f6b2e677ab985cd1af2eae35d62e0f6"
},
"files": [
"config/packages/dev/web_profiler.yaml",
"config/packages/test/web_profiler.yaml",
"config/routes/dev/web_profiler.yaml"
]
},
"symfony/yaml": {
"version": "v5.2.5"
},
"symfonycasts/verify-email-bundle": {
"version": "v1.3.0"
},
"tgalopin/html-sanitizer": {
"version": "1.4.0"
},
"tgalopin/html-sanitizer-bundle": {
"version": "1.0",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "master",
"version": "1.0",
"ref": "95b935177db9abb65356fe19e57fe5abd908b5b8"
}
},
"theseer/tokenizer": {
"version": "1.2.0"
},
"twig/extra-bundle": {
"version": "v3.3.0"
},
"twig/markdown-extra": {
"version": "v3.3.0"
},
"twig/twig": {
"version": "v3.3.0"
},
"ulrichsg/getopt-php": {
"version": "v3.4.0"
},
"webmozart/assert": {
"version": "1.10.0"
},
"wp-cli/php-cli-tools": {
"version": "v0.11.12"
}
}