2015-10-01 21:18:47 +01:00
< ? php
2021-10-10 09:26:18 +01:00
declare ( strict_types = 1 );
2021-04-19 19:51:05 +01:00
// {{{ License
2021-02-19 10:34:21 +00:00
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
2021-04-19 19:51:05 +01:00
// }}}
2015-10-01 21:18:47 +01:00
2021-04-14 16:27:37 +01:00
namespace Plugin\StoreRemoteMedia ;
2022-03-27 15:19:09 +01:00
use App\Core\DB ;
2021-08-12 00:39:36 +01:00
use App\Core\Event ;
use App\Core\GSFile ;
use App\Core\HTTPClient ;
2021-08-18 14:15:30 +01:00
use function App\Core\I18n\_m ;
use App\Core\Log ;
2021-04-19 19:51:05 +01:00
use App\Core\Modules\Plugin ;
2021-08-12 00:39:36 +01:00
use App\Entity\Note ;
use App\Util\Common ;
2021-08-13 20:09:20 +01:00
use App\Util\Exception\DuplicateFoundException ;
use App\Util\Exception\ServerException ;
use App\Util\Exception\TemporaryFileException ;
2021-08-12 00:39:36 +01:00
use App\Util\TemporaryFile ;
2021-12-03 00:44:45 +00:00
use Component\Attachment\Entity\AttachmentThumbnail ;
use Component\Attachment\Entity\AttachmentToLink ;
use Component\Attachment\Entity\AttachmentToNote ;
use Component\Link\Entity\Link ;
2021-12-26 20:20:03 +00:00
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface ;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface ;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface ;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface ;
2021-04-14 16:27:37 +01:00
2021-02-19 10:34:21 +00:00
/**
* The StoreRemoteMedia plugin downloads remotely attached files to local server .
*
* @ package GNUsocial
2021-04-14 16:27:37 +01:00
*
2021-02-19 10:34:21 +00:00
* @ author Mikael Nordfeldth
* @ author Stephen Paul Weber
* @ author Mikael Nordfeldth
* @ author Miguel Dantas
* @ author Diogo Peralta Cordeiro
* @ 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
*/
2021-04-19 19:51:05 +01:00
class StoreRemoteMedia extends Plugin
2015-10-01 21:18:47 +01:00
{
2021-08-12 00:39:36 +01:00
public function version () : string
2015-10-01 21:18:47 +01:00
{
2021-08-12 00:39:36 +01:00
return '3.0.0' ;
}
2015-10-01 21:18:47 +01:00
2021-08-18 14:15:30 +01:00
/**
* Settings which can be set in social . local . yaml
* WARNING , these are _regexps_ ( slashes added later ) . Always escape your dots and end ( '$' ) your strings
*/
public bool $check_whitelist = false ;
public bool $check_blacklist = false ;
public array $domain_whitelist = [
// hostname
'.*' , // Default to allowing any host
];
public array $domain_blacklist = [];
2021-02-21 10:35:02 +00:00
2021-12-03 03:13:28 +00:00
// Whether to maintain a copy of the original media or only a thumbnail of it
2021-08-12 00:39:36 +01:00
private function getStoreOriginal () : bool
{
2021-12-03 03:13:28 +00:00
return Common :: config ( 'plugin_store_remote_media' , 'store_original' );
2015-10-01 21:18:47 +01:00
}
2021-08-18 14:15:30 +01:00
2021-12-03 03:13:28 +00:00
private function getMaxFileSize () : int
2015-10-01 21:18:47 +01:00
{
2021-12-26 20:20:03 +00:00
return min ( Common :: config ( 'plugin_store_remote_media' , 'max_file_size' ), Common :: config ( 'attachments' , 'file_quota' ));
2021-02-16 18:30:21 +00:00
}
2021-08-12 00:39:36 +01:00
private function getSmartCrop () : bool
2021-02-16 18:30:21 +00:00
{
2021-12-03 03:13:28 +00:00
return Common :: config ( 'plugin_store_remote_media' , 'smart_crop' );
2021-02-16 18:30:21 +00:00
}
2016-06-24 14:56:14 +01:00
2021-02-16 18:30:21 +00:00
/**
2021-10-10 09:26:18 +01:00
* @ throws DuplicateFoundException
2021-08-13 20:09:20 +01:00
* @ throws ServerException
* @ throws TemporaryFileException
2021-02-16 18:30:21 +00:00
*/
2021-08-13 20:09:20 +01:00
public function onNewLinkFromNote ( Link $link , Note $note ) : bool
2021-02-16 18:30:21 +00:00
{
2021-08-12 00:39:36 +01:00
// Embed is the plugin to handle these
2021-08-13 20:09:20 +01:00
if ( $link -> getMimetypeMajor () === 'text' ) {
2021-08-12 00:39:36 +01:00
return Event :: next ;
2021-02-16 18:30:21 +00:00
}
2021-08-18 14:15:30 +01:00
// Is this URL trusted?
if ( ! $this -> allowedLink ( $link -> getUrl ())) {
Log :: info ( " Blocked URL ( { $link -> getUrl () } ) in StoreRemoteMedia->onNewLinkFromNote. " );
return Event :: next ;
}
2021-08-12 00:39:36 +01:00
// Have we handled it already?
2021-10-10 09:26:18 +01:00
$attachment_to_link = DB :: find (
'attachment_to_link' ,
[ 'link_id' => $link -> getId ()],
);
2021-08-12 00:39:36 +01:00
// If it was handled already
2021-12-03 02:22:50 +00:00
// XXX: Maybe it would be interesting to have retroactive application of $this->getOriginal here
2021-10-10 09:26:18 +01:00
if ( ! \is_null ( $attachment_to_link )) {
2021-08-12 00:39:36 +01:00
// Relate the note with the existing attachment
DB :: persist ( AttachmentToNote :: create ([
2021-08-13 20:09:20 +01:00
'attachment_id' => $attachment_to_link -> getAttachmentId (),
2021-08-12 00:39:36 +01:00
'note_id' => $note -> getId (),
]));
DB :: flush ();
return Event :: stop ;
} else {
2021-12-26 20:20:03 +00:00
// Validate if the URL really does point to a remote image
$head = HTTPClient :: head ( $link -> getUrl ());
try {
$headers = $head -> getHeaders ();
} catch ( ClientExceptionInterface | RedirectionExceptionInterface | ServerExceptionInterface | TransportExceptionInterface $e ) {
Log :: debug ( 'StoreRemoteMedia->onNewLinkFromNote@HTTPHead->getHeaders: ' . $e -> getMessage (), [ $e ]);
return Event :: next ;
}
// Does it respect the file quota?
$file_size = $headers [ 'content-length' ][ 0 ] ? ? null ;
$max_size = $this -> getMaxFileSize ();
if ( \is_null ( $file_size ) || $file_size > $max_size ) {
Log :: debug ( " Went to download remote media of size { $file_size } but the plugin's filesize limit is { $max_size } so we aborted in StoreRemoteMedia->onNewLinkFromNote. " );
return Event :: next ;
}
2021-08-12 00:39:36 +01:00
// Retrieve media
2021-08-13 20:09:20 +01:00
$get_response = HTTPClient :: get ( $link -> getUrl ());
2021-08-12 00:39:36 +01:00
$media = $get_response -> getContent ();
2021-12-03 00:44:45 +00:00
$mimetype = $get_response -> getHeaders ()[ 'content-type' ][ 0 ] ? ? null ;
2021-08-12 00:39:36 +01:00
unset ( $get_response );
2021-12-26 20:20:03 +00:00
// TODO: Add functionality to specify allowed content types to retrieve here
2021-08-12 00:39:36 +01:00
// Ensure we still want to handle it
2021-08-13 20:09:20 +01:00
if ( $mimetype != $link -> getMimetype ()) {
$link -> setMimetype ( $mimetype );
DB :: persist ( $link );
2021-08-12 00:39:36 +01:00
DB :: flush ();
2021-08-13 20:09:20 +01:00
if ( $link -> getMimetypeMajor () === 'text' ) {
2021-08-12 00:39:36 +01:00
return Event :: next ;
2021-02-16 18:30:21 +00:00
}
}
2015-10-01 21:18:47 +01:00
2021-12-03 01:16:57 +00:00
// We can ignore empty files safely, the user can guess them (:
if ( ! empty ( $media )) {
// Create an attachment for this
$temp_file = new TemporaryFile ();
$temp_file -> write ( $media );
$attachment = GSFile :: storeFileAsAttachment ( $temp_file );
// Relate the link with the attachment
// TODO: Create a function that gets the title from content disposition or URL when such header isn't available
DB :: persist ( AttachmentToLink :: create ([
'link_id' => $link -> getId (),
'attachment_id' => $attachment -> getId (),
]));
// Relate the note with the attachment
DB :: persist ( AttachmentToNote :: create ([
'attachment_id' => $attachment -> getId (),
'note_id' => $note -> getId (),
]));
2021-08-12 00:39:36 +01:00
2021-12-03 01:16:57 +00:00
DB :: flush ();
2021-08-12 00:39:36 +01:00
2021-12-03 01:16:57 +00:00
// Should we create a thumb and delete the original file?
if ( ! $this -> getStoreOriginal ()) {
$thumbnail = AttachmentThumbnail :: getOrCreate (
attachment : $attachment ,
size : 'medium' ,
crop : $this -> getSmartCrop (),
);
$attachment -> deleteStorage ();
}
2015-10-01 21:18:47 +01:00
}
2021-08-12 00:39:36 +01:00
return Event :: stop ;
}
2015-10-01 21:18:47 +01:00
}
2021-08-18 14:15:30 +01:00
/**
* @ return bool true if allowed by the lists , false otherwise
*/
private function allowedLink ( string $url ) : bool
{
$passed_whitelist = ! $this -> check_whitelist ;
$passed_blacklist = ! $this -> check_blacklist ;
if ( $this -> check_whitelist ) {
$passed_whitelist = false ; // don't trust be default
2021-10-10 09:26:18 +01:00
$host = parse_url ( $url , \PHP_URL_HOST );
2021-08-18 14:15:30 +01:00
foreach ( $this -> domain_whitelist as $regex => $provider ) {
if ( preg_match ( " / { $regex } / " , $host )) {
$passed_whitelist = true ; // we trust this source
}
}
}
if ( $this -> check_blacklist ) {
// assume it passed by default
2021-10-10 09:26:18 +01:00
$host = parse_url ( $url , \PHP_URL_HOST );
2021-08-18 14:15:30 +01:00
foreach ( $this -> domain_blacklist as $regex => $provider ) {
if ( preg_match ( " / { $regex } / " , $host )) {
$passed_blacklist = false ; // we blocked this source
}
}
}
return $passed_whitelist && $passed_blacklist ;
}
2021-02-16 18:30:21 +00:00
/**
* Event raised when GNU social polls the plugin for information about it .
* Adds this plugin ' s version information to $versions array
*
2021-09-06 23:47:28 +01:00
* @ param array $versions inherited from parent
2021-04-14 16:27:37 +01:00
*
2021-02-16 18:30:21 +00:00
* @ return bool true hook value
*/
2019-08-12 15:03:30 +01:00
public function onPluginVersion ( array & $versions ) : bool
2015-10-01 21:18:47 +01:00
{
2021-08-12 00:39:36 +01:00
$versions [] = [
2021-10-10 09:26:18 +01:00
'name' => 'StoreRemoteMedia' ,
'version' => $this -> version (),
'author' => 'Mikael Nordfeldth, Diogo Peralta Cordeiro' ,
'homepage' => GNUSOCIAL_PROJECT_URL ,
'description' , // TRANS: Plugin description. => _m('Plugin for downloading remotely attached files to local server.'),
2021-08-12 00:39:36 +01:00
];
return Event :: next ;
2015-10-01 21:18:47 +01:00
}
2021-04-14 16:27:37 +01:00
}