StoreRemoteMedia.php 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. <?php
  2. declare(strict_types = 1);
  3. // {{{ License
  4. // This file is part of GNU social - https://www.gnu.org/software/social
  5. //
  6. // GNU social is free software: you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // GNU social is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  18. // }}}
  19. namespace Plugin\StoreRemoteMedia;
  20. use App\Core\DB\DB;
  21. use App\Core\Event;
  22. use App\Core\GSFile;
  23. use App\Core\HTTPClient;
  24. use function App\Core\I18n\_m;
  25. use App\Core\Log;
  26. use App\Core\Modules\Plugin;
  27. use App\Entity\Note;
  28. use App\Util\Common;
  29. use App\Util\Exception\DuplicateFoundException;
  30. use App\Util\Exception\ServerException;
  31. use App\Util\Exception\TemporaryFileException;
  32. use App\Util\TemporaryFile;
  33. use Component\Attachment\Entity\AttachmentThumbnail;
  34. use Component\Attachment\Entity\AttachmentToLink;
  35. use Component\Attachment\Entity\AttachmentToNote;
  36. use Component\Link\Entity\Link;
  37. /**
  38. * The StoreRemoteMedia plugin downloads remotely attached files to local server.
  39. *
  40. * @package GNUsocial
  41. *
  42. * @author Mikael Nordfeldth
  43. * @author Stephen Paul Weber
  44. * @author Mikael Nordfeldth
  45. * @author Miguel Dantas
  46. * @author Diogo Peralta Cordeiro
  47. * @copyright 2015-2016, 2019-2021 Free Software Foundation, Inc http://www.fsf.org
  48. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  49. */
  50. class StoreRemoteMedia extends Plugin
  51. {
  52. public function version(): string
  53. {
  54. return '3.0.0';
  55. }
  56. /**
  57. * Settings which can be set in social.local.yaml
  58. * WARNING, these are _regexps_ (slashes added later). Always escape your dots and end ('$') your strings
  59. */
  60. public bool $check_whitelist = false;
  61. public bool $check_blacklist = false;
  62. public array $domain_whitelist = [
  63. // hostname
  64. '.*', // Default to allowing any host
  65. ];
  66. public array $domain_blacklist = [];
  67. // Whether to maintain a copy of the original media or only a thumbnail of it
  68. private function getStoreOriginal(): bool
  69. {
  70. return Common::config('plugin_store_remote_media', 'store_original');
  71. }
  72. private function getThumbnailWidth(): int
  73. {
  74. return Common::config('thumbnail', 'width');
  75. }
  76. private function getThumbnailHeight(): int
  77. {
  78. return Common::config('thumbnail', 'height');
  79. }
  80. private function getMaxFileSize(): int
  81. {
  82. return Common::config('plugin_store_remote_media', 'max_file_size');
  83. }
  84. private function getSmartCrop(): bool
  85. {
  86. return Common::config('plugin_store_remote_media', 'smart_crop');
  87. }
  88. /**
  89. * @throws DuplicateFoundException
  90. * @throws ServerException
  91. * @throws TemporaryFileException
  92. */
  93. public function onNewLinkFromNote(Link $link, Note $note): bool
  94. {
  95. // Embed is the plugin to handle these
  96. if ($link->getMimetypeMajor() === 'text') {
  97. return Event::next;
  98. }
  99. // Is this URL trusted?
  100. if (!$this->allowedLink($link->getUrl())) {
  101. Log::info("Blocked URL ({$link->getUrl()}) in StoreRemoteMedia->onNewLinkFromNote.");
  102. return Event::next;
  103. }
  104. // Have we handled it already?
  105. /** @var AttachmentToLink */
  106. $attachment_to_link = DB::find(
  107. 'attachment_to_link',
  108. ['link_id' => $link->getId()],
  109. );
  110. // If it was handled already
  111. // XXX: Maybe it would be interesting to have retroactive application of $this->getOriginal here
  112. if (!\is_null($attachment_to_link)) {
  113. // Relate the note with the existing attachment
  114. DB::persist(AttachmentToNote::create([
  115. 'attachment_id' => $attachment_to_link->getAttachmentId(),
  116. 'note_id' => $note->getId(),
  117. ]));
  118. DB::flush();
  119. return Event::stop;
  120. } else {
  121. // Retrieve media
  122. $get_response = HTTPClient::get($link->getUrl());
  123. $media = $get_response->getContent();
  124. $mimetype = $get_response->getHeaders()['content-type'][0] ?? null;
  125. unset($get_response);
  126. // Ensure we still want to handle it
  127. if ($mimetype != $link->getMimetype()) {
  128. $link->setMimetype($mimetype);
  129. DB::persist($link);
  130. DB::flush();
  131. if ($link->getMimetypeMajor() === 'text') {
  132. return Event::next;
  133. }
  134. }
  135. // We can ignore empty files safely, the user can guess them (:
  136. if (!empty($media)) {
  137. // Create an attachment for this
  138. $temp_file = new TemporaryFile();
  139. $temp_file->write($media);
  140. $attachment = GSFile::storeFileAsAttachment($temp_file);
  141. // Relate the link with the attachment
  142. // TODO: Create a function that gets the title from content disposition or URL when such header isn't available
  143. DB::persist(AttachmentToLink::create([
  144. 'link_id' => $link->getId(),
  145. 'attachment_id' => $attachment->getId(),
  146. ]));
  147. // Relate the note with the attachment
  148. DB::persist(AttachmentToNote::create([
  149. 'attachment_id' => $attachment->getId(),
  150. 'note_id' => $note->getId(),
  151. ]));
  152. DB::flush();
  153. // Should we create a thumb and delete the original file?
  154. if (!$this->getStoreOriginal()) {
  155. $thumbnail = AttachmentThumbnail::getOrCreate(
  156. attachment: $attachment,
  157. size: 'medium',
  158. crop: $this->getSmartCrop(),
  159. );
  160. $attachment->deleteStorage();
  161. }
  162. }
  163. return Event::stop;
  164. }
  165. }
  166. /**
  167. * @return bool true if allowed by the lists, false otherwise
  168. */
  169. private function allowedLink(string $url): bool
  170. {
  171. $passed_whitelist = !$this->check_whitelist;
  172. $passed_blacklist = !$this->check_blacklist;
  173. if ($this->check_whitelist) {
  174. $passed_whitelist = false; // don't trust be default
  175. $host = parse_url($url, \PHP_URL_HOST);
  176. foreach ($this->domain_whitelist as $regex => $provider) {
  177. if (preg_match("/{$regex}/", $host)) {
  178. $passed_whitelist = true; // we trust this source
  179. }
  180. }
  181. }
  182. if ($this->check_blacklist) {
  183. // assume it passed by default
  184. $host = parse_url($url, \PHP_URL_HOST);
  185. foreach ($this->domain_blacklist as $regex => $provider) {
  186. if (preg_match("/{$regex}/", $host)) {
  187. $passed_blacklist = false; // we blocked this source
  188. }
  189. }
  190. }
  191. return $passed_whitelist && $passed_blacklist;
  192. }
  193. /**
  194. * Event raised when GNU social polls the plugin for information about it.
  195. * Adds this plugin's version information to $versions array
  196. *
  197. * @param array $versions inherited from parent
  198. *
  199. * @return bool true hook value
  200. */
  201. public function onPluginVersion(array &$versions): bool
  202. {
  203. $versions[] = [
  204. 'name' => 'StoreRemoteMedia',
  205. 'version' => $this->version(),
  206. 'author' => 'Mikael Nordfeldth, Diogo Peralta Cordeiro',
  207. 'homepage' => GNUSOCIAL_PROJECT_URL,
  208. 'description', // TRANS: Plugin description. => _m('Plugin for downloading remotely attached files to local server.'),
  209. ];
  210. return Event::next;
  211. }
  212. }