Explorer.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  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. /**
  20. * ActivityPub implementation for GNU social
  21. *
  22. * @package GNUsocial
  23. * @category ActivityPub
  24. *
  25. * @author Diogo Peralta Cordeiro <@diogo.site>
  26. * @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
  27. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  28. */
  29. namespace Plugin\ActivityPub\Util;
  30. use App\Core\HTTPClient;
  31. use App\Core\Log;
  32. use App\Util\Exception\NoSuchActorException;
  33. use Exception;
  34. use const JSON_UNESCAPED_SLASHES;
  35. use Plugin\ActivityPub\ActivityPub;
  36. use Plugin\ActivityPub\Entity\ActivitypubActor;
  37. use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
  38. use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
  39. use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
  40. use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
  41. /**
  42. * ActivityPub's own Explorer
  43. *
  44. * Allows to discovery new remote actors
  45. *
  46. * @copyright 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 Explorer
  50. {
  51. private array $discovered_activitypub_actor_profiles = [];
  52. /**
  53. * Shortcut function to get a single profile from its URL.
  54. *
  55. * @param bool $grab_online whether to try online grabbing, defaults to true
  56. *
  57. * @throws ClientExceptionInterface
  58. * @throws NoSuchActorException
  59. * @throws RedirectionExceptionInterface
  60. * @throws ServerExceptionInterface
  61. * @throws TransportExceptionInterface
  62. */
  63. public static function get_profile_from_url(string $url, bool $grab_online = true): ActivitypubActor
  64. {
  65. $discovery = new self();
  66. // Get valid Actor object
  67. $actor_profile = $discovery->lookup($url, $grab_online);
  68. if (!empty($actor_profile)) {
  69. return $actor_profile[0];
  70. }
  71. throw new NoSuchActorException('Invalid Actor.');
  72. }
  73. /**
  74. * Get every profile from the given URL
  75. * This function cleans the $this->discovered_actor_profiles array
  76. * so that there is no erroneous data
  77. *
  78. * @param string $url User's url
  79. * @param bool $grab_online whether to try online grabbing, defaults to true
  80. *
  81. * @throws ClientExceptionInterface
  82. * @throws NoSuchActorException
  83. * @throws RedirectionExceptionInterface
  84. * @throws ServerExceptionInterface
  85. * @throws TransportExceptionInterface
  86. *
  87. * @return array of Actor objects
  88. */
  89. public function lookup(string $url, bool $grab_online = true)
  90. {
  91. if (\in_array($url, ActivityPub::PUBLIC_TO)) {
  92. return [];
  93. }
  94. Log::debug('ActivityPub Explorer: Started now looking for ' . $url);
  95. $this->discovered_activitypub_actor_profiles = [];
  96. return $this->_lookup($url, $grab_online);
  97. }
  98. /**
  99. * Get every profile from the given URL
  100. * This is a recursive function that will accumulate the results on
  101. * $discovered_actor_profiles array
  102. *
  103. * @param string $url User's url
  104. * @param bool $grab_online whether to try online grabbing, defaults to true
  105. *
  106. * @throws ClientExceptionInterface
  107. * @throws NoSuchActorException
  108. * @throws RedirectionExceptionInterface
  109. * @throws ServerExceptionInterface
  110. * @throws TransportExceptionInterface
  111. *
  112. * @return array of ActivityPub Actor objects
  113. */
  114. private function _lookup(string $url, bool $grab_online = true): array
  115. {
  116. $grab_known = $this->grab_known_user($url);
  117. // First check if we already have it locally and, if so, return it.
  118. // If the known fetch fails and remote grab is required: store locally and return.
  119. if (!$grab_known && (!$grab_online || !$this->grab_remote_user($url))) {
  120. throw new NoSuchActorException('Actor not found.');
  121. }
  122. return $this->discovered_activitypub_actor_profiles;
  123. }
  124. /**
  125. * Get a known user profile from its URL and joins it on
  126. * $this->discovered_actor_profiles
  127. *
  128. * @param string $uri Actor's uri
  129. *
  130. * @throws Exception
  131. * @throws NoSuchActorException
  132. *
  133. * @return bool success state
  134. */
  135. private function grab_known_user(string $uri): bool
  136. {
  137. Log::debug('ActivityPub Explorer: Searching locally for ' . $uri . ' offline.');
  138. // Try standard ActivityPub route
  139. // Is this a known filthy little mudblood?
  140. $aprofile = self::get_aprofile_by_url($uri);
  141. if ($aprofile instanceof ActivitypubActor) {
  142. Log::debug('ActivityPub Explorer: Found a known Aprofile for ' . $uri);
  143. // We found something!
  144. $this->discovered_activitypub_actor_profiles[] = $aprofile;
  145. return true;
  146. } else {
  147. Log::debug('ActivityPub Explorer: Unable to find a known Aprofile for ' . $uri);
  148. }
  149. return false;
  150. }
  151. /**
  152. * Get a remote user(s) profile(s) from its URL and joins it on
  153. * $this->discovered_actor_profiles
  154. *
  155. * @param string $url User's url
  156. *
  157. * @throws ClientExceptionInterface
  158. * @throws NoSuchActorException
  159. * @throws RedirectionExceptionInterface
  160. * @throws ServerExceptionInterface
  161. * @throws TransportExceptionInterface
  162. *
  163. * @return bool success state
  164. */
  165. private function grab_remote_user(string $url): bool
  166. {
  167. Log::debug('ActivityPub Explorer: Trying to grab a remote actor for ' . $url);
  168. $response = HTTPClient::get($url, ['headers' => ACTIVITYPUB::HTTP_CLIENT_HEADERS]);
  169. $res = json_decode($response->getContent(), true);
  170. if ($response->getStatusCode() == 410) { // If it was deleted
  171. return true; // Nothing to add.
  172. } elseif (!HTTPClient::statusCodeIsOkay($response)) { // If it is unavailable
  173. return false; // Try to add at another time.
  174. }
  175. if (\is_null($res)) {
  176. Log::debug('ActivityPub Explorer: Invalid response returned from given Actor URL: ' . $res);
  177. return true; // Nothing to add.
  178. }
  179. if ($res['type'] === 'OrderedCollection') { // It's a potential collection of actors!!!
  180. Log::debug('ActivityPub Explorer: Found a collection of actors for ' . $url);
  181. $this->travel_collection($res['first']);
  182. return true;
  183. } else {
  184. try {
  185. $this->discovered_activitypub_actor_profiles[] = Model\Actor::fromJson(json_encode($res));
  186. return true;
  187. } catch (Exception $e) {
  188. Log::debug(
  189. 'ActivityPub Explorer: Invalid potential remote actor while grabbing remotely: ' . $url
  190. . '. He returned the following: ' . json_encode($res, JSON_UNESCAPED_SLASHES)
  191. . ' and the following exception: ' . $e->getMessage(),
  192. );
  193. return false;
  194. }
  195. }
  196. return false;
  197. }
  198. /**
  199. * Get a ActivityPub Profile from it's uri
  200. *
  201. * @param string $v URL
  202. *
  203. * @return ActivitypubActor|bool false if fails | Aprofile object if successful
  204. */
  205. public static function get_aprofile_by_url(string $v): ActivitypubActor|bool
  206. {
  207. $aprofile = ActivitypubActor::getByPK(['uri' => $v]);
  208. return \is_null($aprofile) ? false : ActivitypubActor::getByPK(['uri' => $v]);
  209. }
  210. /**
  211. * Allows the Explorer to transverse a collection of persons.
  212. *
  213. * @throws ClientExceptionInterface
  214. * @throws NoSuchActorException
  215. * @throws RedirectionExceptionInterface
  216. * @throws ServerExceptionInterface
  217. * @throws TransportExceptionInterface
  218. */
  219. private function travel_collection(string $url): bool
  220. {
  221. $response = HTTPClient::get($url, ['headers' => ACTIVITYPUB::HTTP_CLIENT_HEADERS]);
  222. $res = json_decode($response->getContent(), true);
  223. if (!isset($res['orderedItems'])) {
  224. return false;
  225. }
  226. foreach ($res['orderedItems'] as $profile) {
  227. if ($this->_lookup($profile) == false) {
  228. Log::debug('ActivityPub Explorer: Found an invalid actor for ' . $profile);
  229. }
  230. }
  231. // Go through entire collection
  232. if (!\is_null($res['next'])) {
  233. $this->travel_collection($res['next']);
  234. }
  235. return true;
  236. }
  237. /**
  238. * Get a remote user array from its URL (this function is only used for
  239. * profile updating and shall not be used for anything else)
  240. *
  241. * @param string $url User's url
  242. *
  243. * @throws ClientExceptionInterface
  244. * @throws Exception
  245. * @throws RedirectionExceptionInterface
  246. * @throws ServerExceptionInterface
  247. * @throws TransportExceptionInterface
  248. *
  249. * @return null|string If it is able to fetch, false if it's gone
  250. * // Exceptions when network issues or unsupported Activity format
  251. */
  252. public static function get_remote_user_activity(string $url): string|null
  253. {
  254. $response = HTTPClient::get($url, ['headers' => ACTIVITYPUB::HTTP_CLIENT_HEADERS]);
  255. // If it was deleted
  256. if ($response->getStatusCode() == 410) {
  257. return null;
  258. } elseif (!HTTPClient::statusCodeIsOkay($response)) { // If it is unavailable
  259. throw new Exception('Non Ok Status Code for given Actor URL.');
  260. }
  261. return $response->getContent();
  262. }
  263. }