. */ /** * @package OStatusPlugin * @author James Walker */ if (!defined('GNUSOCIAL')) { exit(1); } class SalmonAction extends Action { protected $needPost = true; protected $oprofile = null; // Ostatus_profile of the actor protected $actor = null; // Profile object of the actor var $xml = null; var $activity = null; var $target = null; protected function prepare(array $args=array()) { GNUsocial::setApi(true); // Send smaller error pages parent::prepare($args); if (!isset($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != 'application/magic-envelope+xml') { // TRANS: Client error. Do not translate "application/magic-envelope+xml". $this->clientError(_m('Salmon requires "application/magic-envelope+xml".')); } try { $envxml = file_get_contents('php://input'); $magic_env = new MagicEnvelope($envxml); // parse incoming XML as a MagicEnvelope $entry = $magic_env->getPayload(); // Not cryptographically verified yet! $this->activity = new Activity($entry->documentElement); if (empty($this->activity->actor->id)) { common_log(LOG_ERR, "broken actor: " . var_export($this->activity->actor->id, true)); common_log(LOG_ERR, "activity with no actor: " . var_export($this->activity, true)); // TRANS: Exception. throw new Exception(_m('Received a salmon slap from unidentified actor.')); } // ensureProfiles sets $this->actor and $this->oprofile $this->ensureProfiles(); } catch (Exception $e) { common_debug('Salmon envelope parsing failed with: '.$e->getMessage()); $this->clientError($e->getMessage()); } // Cryptographic verification test if (!$magic_env->verify($this->actor)) { common_log(LOG_DEBUG, "Salmon signature verification failed."); // TRANS: Client error. $this->clientError(_m('Salmon signature verification failed.')); } return true; } /** * Check the posted activity type and break out to appropriate processing. */ protected function handle() { parent::handle(); common_log(LOG_DEBUG, "Got a " . $this->activity->verb); try { if (Event::handle('StartHandleSalmonTarget', array($this->activity, $this->target)) && Event::handle('StartHandleSalmon', array($this->activity))) { switch ($this->activity->verb) { case ActivityVerb::POST: $this->handlePost(); break; case ActivityVerb::SHARE: $this->handleShare(); break; case ActivityVerb::FOLLOW: case ActivityVerb::FRIEND: $this->handleFollow(); break; case ActivityVerb::UNFOLLOW: $this->handleUnfollow(); break; case ActivityVerb::JOIN: $this->handleJoin(); break; case ActivityVerb::LEAVE: $this->handleLeave(); break; case ActivityVerb::TAG: $this->handleTag(); break; case ActivityVerb::UNTAG: $this->handleUntag(); break; case ActivityVerb::UPDATE_PROFILE: $this->handleUpdateProfile(); break; default: // TRANS: Client exception. throw new ClientException(_m('Unrecognized activity type.')); } Event::handle('EndHandleSalmon', array($this->activity)); Event::handle('EndHandleSalmonTarget', array($this->activity, $this->target)); } } catch (AlreadyFulfilledException $e) { // The action's results are already fulfilled. Maybe it was a // duplicate? Maybe someone's database is out of sync? // Let's just accept it and move on. common_log(LOG_INFO, 'Salmon slap carried an event which had already been fulfilled.'); } } function handlePost() { // TRANS: Client exception. throw new ClientException(_m('This target does not understand posts.')); } function handleFollow() { // TRANS: Client exception. throw new ClientException(_m('This target does not understand follows.')); } function handleUnfollow() { // TRANS: Client exception. throw new ClientException(_m('This target does not understand unfollows.')); } function handleShare() { // TRANS: Client exception. throw new ClientException(_m('This target does not understand share events.')); } function handleJoin() { // TRANS: Client exception. throw new ClientException(_m('This target does not understand joins.')); } function handleLeave() { // TRANS: Client exception. throw new ClientException(_m('This target does not understand leave events.')); } function handleTag() { // TRANS: Client exception. throw new ClientException(_m('This target does not understand list events.')); } function handleUntag() { // TRANS: Client exception. throw new ClientException(_m('This target does not understand unlist events.')); } /** * Remote user sent us an update to their profile. * If we already know them, accept the updates. */ function handleUpdateProfile() { $oprofile = Ostatus_profile::getActorProfile($this->activity); if ($oprofile instanceof Ostatus_profile) { common_log(LOG_INFO, "Got a profile-update ping from $oprofile->uri"); $oprofile->updateFromActivityObject($this->activity->actor); } else { common_log(LOG_INFO, "Ignoring profile-update ping from unknown " . $this->activity->actor->id); } } function ensureProfiles() { try { $this->oprofile = Ostatus_profile::getActorProfile($this->activity); if (!$this->oprofile instanceof Ostatus_profile) { throw new UnknownUriException($this->activity->actor->id); } } catch (UnknownUriException $e) { // Apparently we didn't find the Profile object based on our URI, // so OStatus doesn't have it with this URI in ostatus_profile. // Try to look it up again, remote side may have changed from http to https // or maybe publish an acct: URI now instead of an http: URL. // // Steps: // 1. Check the newly received URI. Who does it say it is? // 2. Compare these alleged identities to our local database. // 3. If we found any locally stored identities, ask it about its aliases. // 4. Do any of the aliases from our known identity match the recently introduced one? // // Example: We have stored http://example.com/user/1 but this URI says https://example.com/user/1 common_debug('No local Profile object found for a magicsigned activity author URI: '.$e->object_uri); $disco = new Discovery(); $xrd = $disco->lookup($e->object_uri); // Step 1: We got a bunch of discovery data for https://example.com/user/1 which includes // aliases https://example.com/user and hopefully our original http://example.com/user/1 too $all_ids = array_merge(array($xrd->subject), $xrd->aliases); if (!in_array($e->object_uri, $all_ids)) { common_debug('The activity author URI we got was not listed itself when doing discovery on it.'); throw $e; } // Go through each reported alias from lookup to see if we know this already foreach ($all_ids as $aliased_uri) { $oprofile = Ostatus_profile::getKV('uri', $aliased_uri); if (!$oprofile instanceof Ostatus_profile) { continue; // unknown locally, check the next alias } // Step 2: We found the alleged http://example.com/user/1 URI in our local database, // but this can't be trusted yet because anyone can publish any alias. common_debug('Found a local Ostatus_profile for "'.$e->object_uri.'" with this URI: '.$aliased_uri); // We found an existing OStatus profile, but is it really the same? Do a callback to the URI's origin // Step 3: lookup our previously known http://example.com/user/1 webfinger etc. $xrd = $disco->lookup($oprofile->getUri()); // getUri returns ->uri, which we filtered on earlier $doublecheck_aliases = array_merge(array($xrd->subject), $xrd->aliases); common_debug('Trying to match known "'.$aliased_uri.'" against its returned aliases: '.implode(' ', $doublecheck_aliases)); // if we find our original URI here, it is a legitimate alias // Step 4: Is the newly introduced https://example.com/user/1 URI in the list of aliases // presented by http://example.com/user/1 (i.e. do they both say they are the same identity?) if (in_array($e->object_uri, $doublecheck_aliases)) { common_debug('URIFIX These identities both say they are each other: "'.$aliased_uri.'" and "'.$e->object_uri.'"'); $orig = clone($oprofile); $oprofile->uri = $e->object_uri; common_debug('URIFIX Updating Ostatus_profile URI for '.$aliased_uri.' to '.$oprofile->uri); $oprofile->updateWithKeys($orig, 'uri'); // 'uri' is the primary key column unset($orig); $this->oprofile = $oprofile; break; // don't iterate through aliases anymore } } // We might end up here after $all_ids is iterated through without a $this->oprofile value, if (!$this->oprofile instanceof Ostatus_profile) { common_debug("We do not have a local profile to connect to this activity's author. Let's create one."); // ensureActivityObjectProfile throws exception on failure $this->oprofile = Ostatus_profile::ensureActivityObjectProfile($this->activity->actor); } } assert($this->oprofile instanceof Ostatus_profile); $this->actor = $this->oprofile->localProfile(); } function saveNotice() { if (!$this->oprofile instanceof Ostatus_profile) { common_debug('Ostatus_profile missing in ' . get_class(). ' profile: '.var_export($this->profile)); } return $this->oprofile->processPost($this->activity, 'salmon'); } }