StoreRemoteMedia.php 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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. $attachment_to_link = DB::find('attachment_to_link',
  116. ['link_id' => $link->getId()]);
  117. // If it was handled already
  118. if (!is_null($attachment_to_link)) {
  119. // Relate the note with the existing attachment
  120. DB::persist(AttachmentToNote::create([
  121. 'attachment_id' => $attachment_to_link->getAttachmentId(),
  122. 'note_id' => $note->getId(),
  123. ]));
  124. DB::flush();
  125. return Event::stop;
  126. } else {
  127. // Retrieve media
  128. $get_response = HTTPClient::get($link->getUrl());
  129. $media = $get_response->getContent();
  130. $mimetype = $get_response->getHeaders()['content-type'][0];
  131. unset($get_response);
  132. // Ensure we still want to handle it
  133. if ($mimetype != $link->getMimetype()) {
  134. $link->setMimetype($mimetype);
  135. DB::persist($link);
  136. DB::flush();
  137. if ($link->getMimetypeMajor() === 'text') {
  138. return Event::next;
  139. }
  140. }
  141. // Create an attachment for this
  142. $temp_file = new TemporaryFile();
  143. $temp_file->write($media);
  144. $attachment = GSFile::sanitizeAndStoreFileAsAttachment($temp_file);
  145. // Relate the link with the attachment
  146. DB::persist(AttachmentToLink::create([
  147. 'link_id' => $link->getId(),
  148. 'attachment_id' => $attachment->getId(),
  149. ]));
  150. // Relate the note with the attachment
  151. DB::persist(AttachmentToNote::create([
  152. 'attachment_id' => $attachment->getId(),
  153. 'note_id' => $note->getId(),
  154. ]));
  155. DB::flush();
  156. // Should we create a thumb and delete the original file?
  157. if (!$this->getStoreOriginal()) {
  158. $thumbnail = AttachmentThumbnail::getOrCreate(
  159. attachment: $attachment,
  160. width: $this->getThumbnailWidth(),
  161. height: $this->getThumbnailHeight(),
  162. crop: $this->getSmartCrop()
  163. );
  164. $attachment->deleteStorage();
  165. }
  166. return Event::stop;
  167. }
  168. }
  169. /**
  170. * @param string $url
  171. *
  172. * @return bool true if allowed by the lists, false otherwise
  173. */
  174. private function allowedLink(string $url): bool
  175. {
  176. $passed_whitelist = !$this->check_whitelist;
  177. $passed_blacklist = !$this->check_blacklist;
  178. if ($this->check_whitelist) {
  179. $passed_whitelist = false; // don't trust be default
  180. $host = parse_url($url, PHP_URL_HOST);
  181. foreach ($this->domain_whitelist as $regex => $provider) {
  182. if (preg_match("/{$regex}/", $host)) {
  183. $passed_whitelist = true; // we trust this source
  184. }
  185. }
  186. }
  187. if ($this->check_blacklist) {
  188. // assume it passed by default
  189. $host = parse_url($url, PHP_URL_HOST);
  190. foreach ($this->domain_blacklist as $regex => $provider) {
  191. if (preg_match("/{$regex}/", $host)) {
  192. $passed_blacklist = false; // we blocked this source
  193. }
  194. }
  195. }
  196. return $passed_whitelist && $passed_blacklist;
  197. }
  198. /**
  199. * Event raised when GNU social polls the plugin for information about it.
  200. * Adds this plugin's version information to $versions array
  201. *
  202. * @param &$versions array inherited from parent
  203. *
  204. * @return bool true hook value
  205. */
  206. public function onPluginVersion(array &$versions): bool
  207. {
  208. $versions[] = [
  209. 'name' => 'StoreRemoteMedia',
  210. 'version' => $this->version(),
  211. 'author' => 'Mikael Nordfeldth, Diogo Peralta Cordeiro',
  212. 'homepage' => GNUSOCIAL_PROJECT_URL,
  213. 'description' => // TRANS: Plugin description.
  214. _m('Plugin for downloading remotely attached files to local server.'),
  215. ];
  216. return Event::next;
  217. }
  218. }