Link.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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 Component\Link\Entity;
  20. use App\Core\DB\DB;
  21. use App\Core\Entity;
  22. use App\Core\Event;
  23. use App\Core\GSFile;
  24. use App\Core\HTTPClient;
  25. use App\Core\Log;
  26. use App\Util\Common;
  27. use App\Util\Exception\DuplicateFoundException;
  28. use App\Util\Exception\NotFoundException;
  29. use DateTimeInterface;
  30. use Exception;
  31. use InvalidArgumentException;
  32. /**
  33. * Entity for representing a Link
  34. *
  35. * @category DB
  36. * @package GNUsocial
  37. *
  38. * @author Diogo Peralta Cordeiro <mail@diogo.site>
  39. * @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
  40. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  41. */
  42. class Link extends Entity
  43. {
  44. // {{{ Autocode
  45. // @codeCoverageIgnoreStart
  46. private int $id;
  47. private ?string $url = null;
  48. private ?string $url_hash = null;
  49. private ?string $mimetype = null;
  50. private DateTimeInterface $modified;
  51. public function setId(int $id): self
  52. {
  53. $this->id = $id;
  54. return $this;
  55. }
  56. public function getId(): int
  57. {
  58. return $this->id;
  59. }
  60. public function setUrl(?string $url): self
  61. {
  62. $this->url = $url;
  63. return $this;
  64. }
  65. public function getUrl(): ?string
  66. {
  67. return $this->url;
  68. }
  69. public function setUrlHash(?string $url_hash): self
  70. {
  71. $this->url_hash = \is_null($url_hash) ? null : mb_substr($url_hash, 0, 64);
  72. return $this;
  73. }
  74. public function getUrlHash(): ?string
  75. {
  76. return $this->url_hash;
  77. }
  78. public function setMimetype(?string $mimetype): self
  79. {
  80. $this->mimetype = \is_null($mimetype) ? null : mb_substr($mimetype, 0, 50);
  81. return $this;
  82. }
  83. public function getMimetype(): ?string
  84. {
  85. return $this->mimetype;
  86. }
  87. public function setModified(DateTimeInterface $modified): self
  88. {
  89. $this->modified = $modified;
  90. return $this;
  91. }
  92. public function getModified(): DateTimeInterface
  93. {
  94. return $this->modified;
  95. }
  96. // @codeCoverageIgnoreEnd
  97. // }}} Autocode
  98. public const URLHASH_ALGO = 'sha256';
  99. /**
  100. * Create an attachment for the given URL, fetching the mimetype
  101. *
  102. * @throws DuplicateFoundException
  103. *@throws InvalidArgumentException
  104. *
  105. * @return Link
  106. */
  107. public static function getOrCreate(string $url): self
  108. {
  109. if (Common::isValidHttpUrl($url)) {
  110. // If the URL is a local one, do not create a Link to it
  111. if (parse_url($url, \PHP_URL_HOST) === $_ENV['SOCIAL_DOMAIN']) {
  112. Log::warning("It was attempted to create a Link to a local location {$url}.");
  113. // Forbidden
  114. throw new InvalidArgumentException(message: "A Link can't point to a local location ({$url}), it must be a remote one", code: 400);
  115. }
  116. try {
  117. $head = HTTPClient::head($url);
  118. // This must come before getInfo given that Symfony HTTPClient is lazy (thus forcing curl exec)
  119. $headers = $head->getHeaders();
  120. // @codeCoverageIgnoreStart
  121. } catch (Exception $e) {
  122. throw new InvalidArgumentException(previous: $e);
  123. // @codeCoverageIgnoreEnd
  124. }
  125. $url = $head->getInfo('url'); // The last effective url (after getHeaders, so it follows redirects)
  126. $url_hash = hash(self::URLHASH_ALGO, $url);
  127. try {
  128. return DB::findOneBy('link', ['url_hash' => $url_hash]);
  129. } catch (NotFoundException) {
  130. $headers = array_change_key_case($headers, \CASE_LOWER);
  131. $link = self::create([
  132. 'url' => $url,
  133. 'url_hash' => $url_hash,
  134. 'mimetype' => $headers['content-type'][0] ?? null,
  135. ]);
  136. DB::wrapInTransaction(fn () => DB::persist($link));
  137. Event::handle('LinkStoredNew', [&$link]);
  138. return $link;
  139. }
  140. } else {
  141. throw new InvalidArgumentException();
  142. }
  143. }
  144. public function getMimetypeMajor(): ?string
  145. {
  146. $mime = $this->getMimetype();
  147. return \is_null($mime) ? $mime : GSFile::mimetypeMajor($mime);
  148. }
  149. public function getMimetypeMinor(): ?string
  150. {
  151. $mime = $this->getMimetype();
  152. return \is_null($mime) ? $mime : GSFile::mimetypeMinor($mime);
  153. }
  154. public static function schemaDef(): array
  155. {
  156. return [
  157. 'name' => 'link',
  158. 'fields' => [
  159. 'id' => ['type' => 'serial', 'not null' => true],
  160. 'url' => ['type' => 'text', 'description' => 'URL after following possible redirections'],
  161. 'url_hash' => ['type' => 'varchar', 'length' => 64, 'description' => 'sha256 of destination URL (url field)'],
  162. 'mimetype' => ['type' => 'varchar', 'length' => 50, 'description' => 'mime type of resource'],
  163. 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
  164. ],
  165. 'primary key' => ['id'],
  166. 'indexes' => [
  167. 'actor_url_hash_idx' => ['url_hash'],
  168. ],
  169. ];
  170. }
  171. }