StoreRemoteMedia.php 7.9 KB

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