2010-02-09 20:37:37 +00:00
< ? php
/*
* StatusNet - the distributed open - source microblogging tool
* Copyright ( C ) 2010 , StatusNet , Inc .
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < http :// www . gnu . org / licenses />.
*/
/**
* @ package OStatusPlugin
* @ author James Walker < james @ status . net >
*/
2014-05-05 18:06:22 +01:00
if ( ! defined ( 'GNUSOCIAL' )) { exit ( 1 ); }
2010-02-09 20:37:37 +00:00
class SalmonAction extends Action
{
2014-05-31 10:29:55 +01:00
protected $needPost = true ;
2014-06-28 19:33:09 +01:00
protected $oprofile = null ; // Ostatus_profile of the actor
protected $actor = null ; // Profile object of the actor
2010-02-12 05:43:16 +00:00
var $xml = null ;
var $activity = null ;
2010-12-27 18:51:59 +00:00
var $target = null ;
2010-02-09 20:37:37 +00:00
2014-05-05 18:06:22 +01:00
protected function prepare ( array $args = array ())
2010-02-09 20:37:37 +00:00
{
2015-02-27 11:44:15 +00:00
GNUsocial :: setApi ( true ); // Send smaller error pages
2010-02-20 00:21:17 +00:00
2010-02-18 18:20:48 +00:00
parent :: prepare ( $args );
2015-06-06 16:14:01 +01:00
if ( ! isset ( $_SERVER [ 'CONTENT_TYPE' ])) {
// TRANS: Client error. Do not translate "Content-type"
$this -> clientError ( _m ( 'Salmon requires a Content-type header.' ));
}
$envxml = null ;
switch ( $_SERVER [ 'CONTENT_TYPE' ]) {
case 'application/magic-envelope+xml' :
$envxml = file_get_contents ( 'php://input' );
break ;
case 'application/x-www-form-urlencoded' :
$envxml = Magicsig :: base64_url_decode ( $this -> trimmed ( 'xml' ));
break ;
default :
// TRANS: Client error. Do not translate the quoted "application/[type]" strings.
$this -> clientError ( _m ( 'Salmon requires "application/magic-envelope+xml". For Diaspora we also accept "application/x-www-form-urlencoded" with an "xml" parameter.' , 415 ));
2010-02-12 05:43:16 +00:00
}
2010-02-09 20:37:37 +00:00
2014-05-31 11:51:51 +01:00
try {
2015-06-06 16:14:01 +01:00
if ( empty ( $envxml )) {
throw new ClientException ( 'No magic envelope supplied in POST.' );
}
2014-06-02 13:20:58 +01:00
$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 );
2015-01-10 01:07:39 +00:00
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.' ));
}
2015-01-13 12:43:18 +00:00
// ensureProfiles sets $this->actor and $this->oprofile
$this -> ensureProfiles ();
2014-06-02 13:20:58 +01:00
} catch ( Exception $e ) {
common_debug ( 'Salmon envelope parsing failed with: ' . $e -> getMessage ());
$this -> clientError ( $e -> getMessage ());
2014-05-31 11:51:51 +01:00
}
2014-06-02 13:20:58 +01:00
// Cryptographic verification test
2015-01-13 12:43:18 +00:00
if ( ! $magic_env -> verify ( $this -> actor )) {
2010-02-26 20:39:30 +00:00
common_log ( LOG_DEBUG , " Salmon signature verification failed. " );
2010-09-19 14:17:36 +01:00
// TRANS: Client error.
2010-02-26 20:39:30 +00:00
$this -> clientError ( _m ( 'Salmon signature verification failed.' ));
}
2010-02-18 11:36:32 +00:00
return true ;
2010-02-12 05:43:16 +00:00
}
2010-02-18 21:22:21 +00:00
/**
2010-02-19 20:08:07 +00:00
* Check the posted activity type and break out to appropriate processing .
2010-02-18 21:22:21 +00:00
*/
2010-02-19 03:18:14 +00:00
2014-05-05 18:06:22 +01:00
protected function handle ()
2010-02-12 05:43:16 +00:00
{
2014-05-05 18:06:22 +01:00
parent :: handle ();
2010-02-12 05:43:16 +00:00
2010-08-13 21:07:25 +01:00
common_log ( LOG_DEBUG , " Got a " . $this -> activity -> verb );
2014-06-02 12:44:08 +01:00
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 ));
2010-02-18 11:36:32 +00:00
}
2014-06-02 12:44:08 +01:00
} 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.' );
2010-02-18 11:36:32 +00:00
}
}
function handlePost ()
{
2010-09-19 14:17:36 +01:00
// TRANS: Client exception.
2011-04-10 23:39:27 +01:00
throw new ClientException ( _m ( 'This target does not understand posts.' ));
2010-02-18 11:36:32 +00:00
}
function handleFollow ()
{
2010-09-19 14:17:36 +01:00
// TRANS: Client exception.
2011-04-10 23:39:27 +01:00
throw new ClientException ( _m ( 'This target does not understand follows.' ));
2010-02-19 20:08:07 +00:00
}
2010-02-19 21:37:07 +00:00
2010-02-19 20:08:07 +00:00
function handleUnfollow ()
{
2010-09-19 14:17:36 +01:00
// TRANS: Client exception.
2011-04-10 23:39:27 +01:00
throw new ClientException ( _m ( 'This target does not understand unfollows.' ));
2010-02-18 11:36:32 +00:00
}
function handleShare ()
{
2010-09-19 14:17:36 +01:00
// TRANS: Client exception.
2011-04-10 23:39:27 +01:00
throw new ClientException ( _m ( 'This target does not understand share events.' ));
2010-02-20 16:12:43 +00:00
}
function handleJoin ()
{
2010-09-19 14:17:36 +01:00
// TRANS: Client exception.
2011-04-10 23:39:27 +01:00
throw new ClientException ( _m ( 'This target does not understand joins.' ));
2010-02-24 23:28:01 +00:00
}
function handleLeave ()
{
2010-09-19 14:17:36 +01:00
// TRANS: Client exception.
2011-04-10 23:39:27 +01:00
throw new ClientException ( _m ( 'This target does not understand leave events.' ));
2010-02-18 11:36:32 +00:00
}
2011-03-06 19:15:34 +00:00
function handleTag ()
{
2011-04-10 23:39:27 +01:00
// TRANS: Client exception.
2011-08-20 19:30:37 +01:00
throw new ClientException ( _m ( 'This target does not understand list events.' ));
2011-03-06 19:15:34 +00:00
}
function handleUntag ()
{
2011-04-10 23:39:27 +01:00
// TRANS: Client exception.
2011-08-20 19:30:37 +01:00
throw new ClientException ( _m ( 'This target does not understand unlist events.' ));
2011-03-06 19:15:34 +00:00
}
2010-02-22 17:43:27 +00:00
/**
2010-02-24 23:28:01 +00:00
* Remote user sent us an update to their profile .
* If we already know them , accept the updates .
2010-02-22 17:43:27 +00:00
*/
2010-02-24 23:28:01 +00:00
function handleUpdateProfile ()
2010-02-22 17:43:27 +00:00
{
2010-08-13 21:07:25 +01:00
$oprofile = Ostatus_profile :: getActorProfile ( $this -> activity );
2014-05-05 22:57:41 +01:00
if ( $oprofile instanceof Ostatus_profile ) {
2010-02-24 23:28:01 +00:00
common_log ( LOG_INFO , " Got a profile-update ping from $oprofile->uri " );
2010-08-13 21:07:25 +01:00
$oprofile -> updateFromActivityObject ( $this -> activity -> actor );
2010-02-24 23:28:01 +00:00
} else {
2010-08-13 21:07:25 +01:00
common_log ( LOG_INFO , " Ignoring profile-update ping from unknown " . $this -> activity -> actor -> id );
2010-02-24 23:28:01 +00:00
}
2010-02-22 17:43:27 +00:00
}
2015-01-13 12:43:18 +00:00
function ensureProfiles ()
2010-02-18 11:36:32 +00:00
{
2015-01-10 01:07:39 +00:00
try {
2015-01-13 12:43:18 +00:00
$this -> oprofile = Ostatus_profile :: getActorProfile ( $this -> activity );
if ( ! $this -> oprofile instanceof Ostatus_profile ) {
throw new UnknownUriException ( $this -> activity -> actor -> id );
}
2015-01-10 01:07:39 +00:00
} 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 ;
}
2010-02-18 11:36:32 +00:00
2015-01-10 01:07:39 +00:00
// 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 )) {
2015-02-17 16:35:45 +00:00
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 );
2015-02-17 20:31:35 +00:00
$oprofile -> updateWithKeys ( $orig , 'uri' ); // 'uri' is the primary key column
2015-02-17 16:35:45 +00:00
unset ( $orig );
2015-01-13 12:43:18 +00:00
$this -> oprofile = $oprofile ;
2015-01-10 01:07:39 +00:00
break ; // don't iterate through aliases anymore
}
}
2015-01-13 12:43:18 +00:00
// 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 );
}
2015-01-10 01:07:39 +00:00
}
2015-01-13 12:43:18 +00:00
assert ( $this -> oprofile instanceof Ostatus_profile );
$this -> actor = $this -> oprofile -> localProfile ();
2010-02-18 11:36:32 +00:00
}
2010-02-21 14:16:27 +00:00
function saveNotice ()
{
2015-01-13 12:43:18 +00:00
if ( ! $this -> oprofile instanceof Ostatus_profile ) {
common_debug ( 'Ostatus_profile missing in ' . get_class () . ' profile: ' . var_export ( $this -> profile ));
}
2015-01-12 01:01:26 +00:00
return $this -> oprofile -> processPost ( $this -> activity , 'salmon' );
2010-02-21 14:16:27 +00:00
}
2010-02-09 20:37:37 +00:00
}