. * * @category Plugin * @package GNUsocial * @author Diogo Cordeiro * @author Daniel Supernault * @copyright 2018 Free Software Foundation http://fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link https://www.gnu.org/software/social/ */ if (!defined('GNUSOCIAL')) { exit(1); } /** * ActivityPub's own Explorer * * Allows to discovery new (or the same) ActivityPub profiles * * @category Plugin * @package GNUsocial * @author Diogo Cordeiro * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://www.gnu.org/software/social/ */ class Activitypub_explorer { private $discovered_actor_profiles = array(); /** * Get every profile from the given URL * This function cleans the $this->discovered_actor_profiles array * so that there is no erroneous data * * @author Diogo Cordeiro * @param string $url User's url * @return array of Profile objects */ public function lookup($url) { $this->discovered_actor_profiles = array(); return $this->_lookup($url); } /** * Get every profile from the given URL * This is a recursive function that will accumulate the results on * $discovered_actor_profiles array * * @author Diogo Cordeiro * @param string $url User's url * @return array of Profile objects */ private function _lookup($url) { // First check if we already have it locally and, if so, return it // If the local fetch fails: grab it remotely, store locally and return if (! ($this->grab_local_user($url) || $this->grab_remote_user($url))) { throw new Exception("User not found."); } return $this->discovered_actor_profiles; } /** * This ensures that we are using a valid ActivityPub URI * * @author Diogo Cordeiro * @param string $url * @return boolean success state (related to the response) * @throws Exception (If the HTTP request fails) */ private function ensure_proper_remote_uri($url) { $client = new HTTPClient(); $headers = []; $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; $headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; $response = $client->get($url, $headers); if (!$response->isOk()) { throw new Exception("Invalid Actor URL."); } $res = json_decode($response->getBody(), JSON_UNESCAPED_SLASHES); if (self::validate_remote_response($res)) { $this->temp_res = $res; return true; } return false; } /** * Get a local user profiles from its URL and joins it on * $this->discovered_actor_profiles * * @author Diogo Cordeiro * @param string $uri Actor's uri * @return boolean success state */ private function grab_local_user($uri, $online = false) { // Ensure proper remote URI // If an exception occurs here it's better to just leave everything // break than to continue processing if ($online && $this->ensure_proper_remote_uri($uri)) { $uri = $this->temp_res["id"]; } // Try standard ActivityPub route // Is this a known filthy little mudblood? $aprofile = Activitypub_profile::getKV("uri", $uri); if ($aprofile instanceof Activitypub_profile) { $profile = $aprofile->local_profile(); // We found something! $this->discovered_actor_profiles[]= $profile; unset($this->temp_res); // IMPORTANT to avoid _dangerous_ noise in the Explorer system return true; } // If offline grabbing failed, attempt again with online resources if (!$online) { return $this->grab_local_user($uri, true); } return false; } /** * Get a remote user(s) profile(s) from its URL and joins it on * $this->discovered_actor_profiles * * @author Diogo Cordeiro * @param string $url User's url * @return boolean success state */ private function grab_remote_user($url) { if (!isset($this->temp_res)) { $client = new HTTPClient(); $headers = array(); $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; $headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; $response = $client->get($url, $headers); if (!$response->isOk()) { throw new Exception("Invalid Actor URL."); } $res = json_decode($response->getBody(), JSON_UNESCAPED_SLASHES); } else { $res = $this->temp_res; unset($this->temp_res); } if (isset($res["orderedItems"])) { // It's a potential collection of actors!!! foreach ($res["orderedItems"] as $profile) { if ($this->_lookup($profile) == false) { // XXX: Invalid actor found, not sure how we handle those } } // Go through entire collection if (!is_null($res["next"])) { $this->_lookup($res["next"]); } return true; } elseif (self::validate_remote_response($res)) { $this->discovered_actor_profiles[]= $this->store_profile($res); return true; } return false; } /** * Save remote user profile in local instance * * @author Diogo Cordeiro * @param array $res remote response * @return Profile remote Profile object */ private function store_profile($res) { // ActivityPub Profile $aprofile = new Activitypub_profile; $aprofile->uri = $res['id']; $aprofile->nickname = $res['preferredUsername']; $aprofile->fullname = $res['name']; $aprofile->bio = substr($res['summary'], 0, 1000); $aprofile->inboxuri = $res['inbox']; $aprofile->sharedInboxuri = isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : $res['inbox']; $aprofile->do_insert(); $profile = $aprofile->local_profile(); // Public Key $apRSA = new Activitypub_rsa(); $apRSA->profile_id = $profile->getID(); $apRSA->public_key = $res['publicKey']['publicKeyPem']; $apRSA->store_keys(); return $profile; } /** * Validates a remote response in order to determine whether this * response is a valid profile or not * * @author Diogo Cordeiro * @param array $res remote response * @return boolean success state */ public static function validate_remote_response($res) { if (!isset($res['id'], $res['preferredUsername'], $res['name'], $res['summary'], $res['inbox'], $res['publicKey']['publicKeyPem'])) { return false; } return true; } /** * Given a valid actor profile url returns its inboxes * * @author Diogo Cordeiro * @param string $url of Actor profile * @return boolean|array false if fails | array with inbox and shared inbox if successful */ public static function get_actor_inboxes_uri($url) { $client = new HTTPClient(); $headers = array(); $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; $headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; $response = $client->get($url, $headers); if (!$response->isOk()) { throw new Exception('Invalid Actor URL.'); } $res = json_decode($response->getBody(), JSON_UNESCAPED_SLASHES); if (self::validate_remote_response($res)) { return [ 'inbox' => $res['inbox'], 'sharedInbox' => isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : $res['inbox'] ]; } return false; } }