From 2df3bbc80b6c9dd44134bcf3b5b4a09ea1a803d1 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sat, 20 Feb 2010 11:12:43 -0500 Subject: Move some salmon processing to a superclass Moved some salmon processing to a superclass so we could handle group salmon posts, too. --- plugins/OStatus/lib/salmonaction.php | 231 +++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 plugins/OStatus/lib/salmonaction.php (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php new file mode 100644 index 000000000..7085e4583 --- /dev/null +++ b/plugins/OStatus/lib/salmonaction.php @@ -0,0 +1,231 @@ +. + */ + +/** + * @package OStatusPlugin + * @author James Walker + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class SalmonAction extends Action +{ + var $xml = null; + var $activity = null; + + function prepare($args) + { + StatusNet::setApi(true); // Send smaller error pages + + parent::prepare($args); + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + $this->clientError(_('This method requires a POST.')); + } + + if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') { + $this->clientError(_('Salmon requires application/atom+xml')); + } + + $xml = file_get_contents('php://input'); + + $dom = DOMDocument::loadXML($xml); + + if ($dom->documentElement->namespaceURI != Activity::ATOM || + $dom->documentElement->localName != 'entry') { + $this->clientError(_m('Salmon post must be an Atom entry.')); + } + // XXX: check the signature + + $this->act = new Activity($dom->documentElement); + return true; + } + + /** + * Check the posted activity type and break out to appropriate processing. + */ + + function handle($args) + { + StatusNet::setApi(true); // Send smaller error pages + + // TODO : Insert new $xml -> notice code + + if (Event::handle('StartHandleSalmon', array($this->activity))) { + switch ($this->act->verb) + { + case ActivityVerb::POST: + $this->handlePost(); + break; + case ActivityVerb::SHARE: + $this->handleShare(); + break; + case ActivityVerb::FAVORITE: + $this->handleFavorite(); + break; + case ActivityVerb::FOLLOW: + case ActivityVerb::FRIEND: + $this->handleFollow(); + break; + case ActivityVerb::UNFOLLOW: + $this->handleUnfollow(); + break; + case ActivityVerb::JOIN: + $this->handleJoin(); + break; + default: + throw new ClientException(_("Unimplemented.")); + } + Event::handle('EndHandleSalmon', array($this->activity)); + } + } + + function handlePost() + { + throw new ClientException(_("Unimplemented!")); + } + + function handleFollow() + { + throw new ClientException(_("Unimplemented!")); + } + + function handleUnfollow() + { + throw new ClientException(_("Unimplemented!")); + } + + function handleFavorite() + { + throw new ClientException(_("Unimplemented!")); + } + + /** + * Remote user doesn't like one of our posts after all! + * Confirm the post is ours, and delete a local favorite event. + */ + + function handleUnfavorite() + { + throw new ClientException(_("Unimplemented!")); + } + + /** + * Hmmmm + */ + function handleShare() + { + throw new ClientException(_("Unimplemented!")); + } + + /** + * Hmmmm + */ + function handleJoin() + { + throw new ClientException(_("Unimplemented!")); + } + + /** + * @return Ostatus_profile + */ + function ensureProfile() + { + $actor = $this->act->actor; + if (empty($actor->id)) { + common_log(LOG_ERR, "broken actor: " . var_export($actor, true)); + throw new Exception("Received a salmon slap from unidentified actor."); + } + + return Ostatus_profile::ensureActorProfile($this->act); + } + + /** + * @fixme merge into Ostatus_profile::ensureActorProfile and friends + */ + function createProfile() + { + $actor = $this->act->actor; + + $profile = new Profile(); + + $profile->nickname = $this->nicknameFromURI($actor->id); + + if (empty($profile->nickname)) { + $profile->nickname = common_nicknamize($actor->title); + } + + $profile->fullname = $actor->title; + $profile->bio = $actor->summary; // XXX: is that right? + $profile->profileurl = $actor->link; // XXX: is that right? + $profile->created = common_sql_now(); + + $id = $profile->insert(); + + if (empty($id)) { + common_log_db_error($profile, 'INSERT', __FILE__); + throw new Exception("Couldn't save new profile for $actor->id\n"); + } + + // XXX: add avatars + + $op = new Ostatus_profile(); + + $op->profile_id = $id; + $op->homeuri = $actor->id; + $op->created = $profile->created; + + // XXX: determine feed URI from source or Webfinger or whatever + + $id = $op->insert(); + + if (empty($id)) { + common_log_db_error($op, 'INSERT', __FILE__); + throw new Exception("Couldn't save new ostatus profile for $actor->id\n"); + } + + return $profile; + } + + /** + * @fixme should be merged into Ostatus_profile + */ + function nicknameFromURI($uri) + { + preg_match('/(\w+):/', $uri, $matches); + + $protocol = $matches[1]; + + switch ($protocol) { + case 'acct': + case 'mailto': + if (preg_match("/^$protocol:(.*)?@.*\$/", $uri, $matches)) { + return common_canonical_nickname($matches[1]); + } + return null; + case 'http': + return common_url_to_nickname($uri); + break; + default: + return null; + } + } +} -- cgit v1.2.3-54-g00ecf From 145a19954f6f993714cb8b65aaf9d54996503664 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Sat, 20 Feb 2010 16:45:30 -0800 Subject: OStatus: Salmon favorite & unfavorite events now handled --- plugins/OStatus/actions/usersalmon.php | 77 ++++++++++++++++++++-------------- plugins/OStatus/lib/salmonaction.php | 3 ++ 2 files changed, 48 insertions(+), 32 deletions(-) (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/plugins/OStatus/actions/usersalmon.php b/plugins/OStatus/actions/usersalmon.php index 4363488dd..20c6c2942 100644 --- a/plugins/OStatus/actions/usersalmon.php +++ b/plugins/OStatus/actions/usersalmon.php @@ -131,10 +131,51 @@ class UsersalmonAction extends SalmonAction function handleFavorite() { - // WORST VARIABLE NAME EVER - $object = $this->act->object; + $notice = $this->getNotice($this->act->object); + $profile = $this->ensureProfile()->localProfile(); - switch ($this->act->object->type) { + $old = Fave::pkeyGet(array('user_id' => $profile->id, + 'notice_id' => $notice->id)); + + if (!empty($old)) { + throw new ClientException("We already know that's a fave!"); + } + + if (!Fave::addNew($profile, $notice)) { + throw new ClientException("Could not save new favorite."); + } + } + + /** + * Remote user doesn't like one of our posts after all! + * Confirm the post is ours, and save a local favorite event. + */ + function handleUnfavorite() + { + $notice = $this->getNotice($this->act->object); + $profile = $this->ensureProfile()->localProfile(); + + $fave = Fave::pkeyGet(array('user_id' => $profile->id, + 'notice_id' => $notice->id)); + if (empty($fave)) { + throw new ClientException("Notice wasn't favorited!"); + } + + $fave->delete(); + } + + /** + * @param ActivityObject $object + * @return Notice + * @throws ClientException on invalid input + */ + function getNotice($object) + { + if (!$object) { + throw new ClientException("Can't favorite/unfavorite without an object."); + } + + switch ($object->type) { case ActivityObject::ARTICLE: case ActivityObject::BLOGENTRY: case ActivityObject::NOTE: @@ -155,35 +196,7 @@ class UsersalmonAction extends SalmonAction throw new ClientException("Notice with ID $object->id not posted by $this->user->id."); } - $profile = $this->ensureProfile(); - - $old = Fave::pkeyGet(array('user_id' => $profile->id, - 'notice_id' => $notice->id)); - - if (!empty($old)) { - throw new ClientException("We already know that's a fave!"); - } - - $fave = new Fave(); - - // @fixme need to change this attribute name, maybe references - $fave->user_id = $profile->id; - $fave->notice_id = $notice->id; - - $result = $fave->insert(); - - if (!$result) { - common_log_db_error($fave, 'INSERT', __FILE__); - throw new ServerException('Could not save new favorite.'); - } - } - - /** - * Remote user doesn't like one of our posts after all! - * Confirm the post is ours, and save a local favorite event. - */ - function handleUnfavorite() - { + return $notice; } } diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 7085e4583..c83890507 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -81,6 +81,9 @@ class SalmonAction extends Action case ActivityVerb::FAVORITE: $this->handleFavorite(); break; + case ActivityVerb::UNFAVORITE: + $this->handleUnfavorite(); + break; case ActivityVerb::FOLLOW: case ActivityVerb::FRIEND: $this->handleFollow(); -- cgit v1.2.3-54-g00ecf From 6169d8a877fb0e76ee17187f74a6747fe7f2c97d Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 21 Feb 2010 09:16:27 -0500 Subject: saving notices in salmon actions --- plugins/OStatus/lib/salmonaction.php | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index c83890507..41e8322e8 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -231,4 +231,49 @@ class SalmonAction extends Action return null; } } + + function saveNotice() + { + $oprofile = $this->ensureProfile(); + + // Get (safe!) HTML and text versions of the content + + require_once(INSTALLDIR.'/extlib/HTMLPurifier/HTMLPurifier.auto.php'); + + $html = $this->act->object->content; + + $rendered = HTMLPurifier::purify($html); + $content = html_entity_decode(strip_tags($rendered)); + + $options = array('is_local' => Notice::REMOTE_OMB, + 'uri' => $this->act->object->id, + 'url' => $this->act->object->link, + 'rendered' => $rendered); + + if (!empty($this->act->context->location)) { + $options['lat'] = $location->lat; + $options['lon'] = $location->lon; + if ($location->location_id) { + $options['location_ns'] = $location->location_ns; + $options['location_id'] = $location->location_id; + } + } + + if (!empty($this->act->context->replyToID)) { + $orig = Notice::staticGet('uri', + $this->act->context->replyToID); + if (!empty($orig)) { + $options['reply_to'] = $orig->id; + } + } + + if (!empty($this->act->time)) { + $options['created'] = common_sql_time($this->act->time); + } + + return Notice::saveNew($oprofile->profile_id, + $content, + 'ostatus+salmon', + $options); + } } -- cgit v1.2.3-54-g00ecf From 09e2d181be3b72b1d8134273b9fdfa280895627c Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 21 Feb 2010 12:54:52 -0500 Subject: remove unused profile code from salmonaction --- plugins/OStatus/lib/salmonaction.php | 71 ------------------------------------ 1 file changed, 71 deletions(-) (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 41e8322e8..abd8d4c83 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -161,77 +161,6 @@ class SalmonAction extends Action return Ostatus_profile::ensureActorProfile($this->act); } - /** - * @fixme merge into Ostatus_profile::ensureActorProfile and friends - */ - function createProfile() - { - $actor = $this->act->actor; - - $profile = new Profile(); - - $profile->nickname = $this->nicknameFromURI($actor->id); - - if (empty($profile->nickname)) { - $profile->nickname = common_nicknamize($actor->title); - } - - $profile->fullname = $actor->title; - $profile->bio = $actor->summary; // XXX: is that right? - $profile->profileurl = $actor->link; // XXX: is that right? - $profile->created = common_sql_now(); - - $id = $profile->insert(); - - if (empty($id)) { - common_log_db_error($profile, 'INSERT', __FILE__); - throw new Exception("Couldn't save new profile for $actor->id\n"); - } - - // XXX: add avatars - - $op = new Ostatus_profile(); - - $op->profile_id = $id; - $op->homeuri = $actor->id; - $op->created = $profile->created; - - // XXX: determine feed URI from source or Webfinger or whatever - - $id = $op->insert(); - - if (empty($id)) { - common_log_db_error($op, 'INSERT', __FILE__); - throw new Exception("Couldn't save new ostatus profile for $actor->id\n"); - } - - return $profile; - } - - /** - * @fixme should be merged into Ostatus_profile - */ - function nicknameFromURI($uri) - { - preg_match('/(\w+):/', $uri, $matches); - - $protocol = $matches[1]; - - switch ($protocol) { - case 'acct': - case 'mailto': - if (preg_match("/^$protocol:(.*)?@.*\$/", $uri, $matches)) { - return common_canonical_nickname($matches[1]); - } - return null; - case 'http': - return common_url_to_nickname($uri); - break; - default: - return null; - } - } - function saveNotice() { $oprofile = $this->ensureProfile(); -- cgit v1.2.3-54-g00ecf From 588fe5d603abe40c45a1147eba18c8b5143babc4 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Sun, 21 Feb 2010 10:48:48 -0800 Subject: OStatus: debug aid - log the received Salmon post when it can't be parsed properly as an --- plugins/OStatus/lib/salmonaction.php | 1 + 1 file changed, 1 insertion(+) (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index abd8d4c83..4e5ed7fe6 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -51,6 +51,7 @@ class SalmonAction extends Action if ($dom->documentElement->namespaceURI != Activity::ATOM || $dom->documentElement->localName != 'entry') { + common_log(LOG_DEBUG, "Got invalid Salmon post: $xml"); $this->clientError(_m('Salmon post must be an Atom entry.')); } // XXX: check the signature -- cgit v1.2.3-54-g00ecf From ad3406a919d950315ed1381ffb4dd8d47baf2c24 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 21 Feb 2010 14:17:37 -0500 Subject: use Ostatus_profile::ensureActivityObjectProfile() in SalmonAction::ensureProfile() --- plugins/OStatus/lib/salmonaction.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index abd8d4c83..87e98ad35 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -155,10 +155,11 @@ class SalmonAction extends Action $actor = $this->act->actor; if (empty($actor->id)) { common_log(LOG_ERR, "broken actor: " . var_export($actor, true)); + common_log(LOG_ERR, "activity with no actor: " . var_export($this->act, true)); throw new Exception("Received a salmon slap from unidentified actor."); } - return Ostatus_profile::ensureActorProfile($this->act); + return Ostatus_profile::ensureActivityObjectProfile($actor); } function saveNotice() -- cgit v1.2.3-54-g00ecf From 5207783765328a3d6f0101f143edb8807247bcfe Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Sun, 21 Feb 2010 19:51:11 -0800 Subject: OStatus: record source profile & saving method in ostatus_source table; this allows us to distinguish posts that have come through an unverified group feed --- plugins/OStatus/OStatusPlugin.php | 1 + plugins/OStatus/classes/Ostatus_profile.php | 4 +- plugins/OStatus/classes/Ostatus_source.php | 114 ++++++++++++++++++++++++++++ plugins/OStatus/lib/salmonaction.php | 13 +++- 4 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 plugins/OStatus/classes/Ostatus_source.php (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 641765dae..5b9e3be2b 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -305,6 +305,7 @@ class OStatusPlugin extends Plugin function onCheckSchema() { $schema = Schema::get(); $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef()); + $schema->ensureTable('ostatus_source', Ostatus_source::schemaDef()); $schema->ensureTable('feedsub', FeedSub::schemaDef()); $schema->ensureTable('hubsub', HubSub::schemaDef()); return true; diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 3bed1c2aa..71885bcdc 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -508,13 +508,15 @@ class Ostatus_profile extends Memcached_DataObject } } - // @fixme save detailed ostatus source info // @fixme ensure that groups get handled correctly $saved = Notice::saveNew($oprofile->localProfile()->id, $content, 'ostatus', $params); + + // Record which feed this came through... + Ostatus_source::saveNew($saved, $this, 'push'); } /** diff --git a/plugins/OStatus/classes/Ostatus_source.php b/plugins/OStatus/classes/Ostatus_source.php new file mode 100644 index 000000000..e6ce7d442 --- /dev/null +++ b/plugins/OStatus/classes/Ostatus_source.php @@ -0,0 +1,114 @@ +. + */ + +/** + * @package OStatusPlugin + * @maintainer Brion Vibber + */ + +class Ostatus_source extends Memcached_DataObject +{ + public $__table = 'ostatus_source'; + + public $notice_id; // notice we're referring to + public $profile_uri; // uri of the ostatus_profile this came through -- may be a group feed + public $method; // push or salmon + + public /*static*/ function staticGet($k, $v=null) + { + return parent::staticGet(__CLASS__, $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array('notice_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'profile_uri' => DB_DATAOBJECT_STR, + 'method' => DB_DATAOBJECT_STR); + } + + static function schemaDef() + { + return array(new ColumnDef('notice_id', 'integer', + null, false, 'PRI'), + new ColumnDef('profile_uri', 'varchar', + 255, false), + new ColumnDef('method', "ENUM('push','salmon')", + null, false)); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has; this function + * defines them. + * + * @return array key definitions + */ + + function keys() + { + return array_keys($this->keyTypes()); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. + * + * @return array key definitions + */ + + function keyTypes() + { + return array('notice_id' => 'K'); + } + + function sequenceKey() + { + return array(false, false, false); + } + + /** + * Save a remote notice source record; this helps indicate how trusted we are. + * @param string $method + */ + public static function saveNew(Notice $notice, Ostatus_profile $oprofile, $method) + { + $osource = new Ostatus_source(); + $osource->notice_id = $notice->id; + $osource->profile_uri = $oprofile->uri; + $osource->method = $method; + if ($osource->insert()) { + return true; + } else { + common_log_db_error($osource, 'INSERT', __FILE__); + return false; + } + } +} diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 11c411c7d..d93cc9aa4 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -202,9 +202,14 @@ class SalmonAction extends Action $options['created'] = common_sql_time($this->act->time); } - return Notice::saveNew($oprofile->profile_id, - $content, - 'ostatus+salmon', - $options); + $saved = Notice::saveNew($oprofile->profile_id, + $content, + 'ostatus+salmon', + $options); + + // Record that this was saved through a validated Salmon source + // @fixme actually do the signature validation! + Ostatus_source::saveNew($saved, $oprofile, 'salmon'); + return $saved; } } -- cgit v1.2.3-54-g00ecf From 17c329ba89f575c7fcbf05522a0cf50db6d6a74a Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 21 Feb 2010 23:07:46 -0500 Subject: add HTMLPurifier config --- plugins/OStatus/lib/salmonaction.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 11c411c7d..8734223c6 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -173,7 +173,10 @@ class SalmonAction extends Action $html = $this->act->object->content; - $rendered = HTMLPurifier::purify($html); + $htmlConfig = HTMLPurifier_Config::createDefault(); + + $rendered = HTMLPurifier::purify($html, $htmlConfig); + $content = html_entity_decode(strip_tags($rendered)); $options = array('is_local' => Notice::REMOTE_OMB, -- cgit v1.2.3-54-g00ecf From e39e6cdcc590a5330ab5a20a2519346bfc7965e0 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 21 Feb 2010 23:16:42 -0500 Subject: was using HTMLPurifier::purify() as a static method, which it is not --- plugins/OStatus/lib/salmonaction.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 273be588b..08bfa332f 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -173,9 +173,9 @@ class SalmonAction extends Action $html = $this->act->object->content; - $htmlConfig = HTMLPurifier_Config::createDefault(); + $purifier = new HTMLPurifier(); - $rendered = HTMLPurifier::purify($html, $htmlConfig); + $rendered = $purifier->purify($html); $content = html_entity_decode(strip_tags($rendered)); -- cgit v1.2.3-54-g00ecf From 48839a1fcf0c94b877c7c3232cf3e34eb0a9f23e Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 21 Feb 2010 23:19:29 -0500 Subject: change erroneous common_sql_time() to common_sql_date() --- plugins/OStatus/lib/salmonaction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 08bfa332f..b128cbd13 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -202,7 +202,7 @@ class SalmonAction extends Action } if (!empty($this->act->time)) { - $options['created'] = common_sql_time($this->act->time); + $options['created'] = common_sql_date($this->act->time); } $saved = Notice::saveNew($oprofile->profile_id, -- cgit v1.2.3-54-g00ecf From 891e0028838e51788e917d947cc280dbd53c1792 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 21 Feb 2010 23:56:48 -0500 Subject: don't calculate replies for remote notices --- classes/Notice.php | 27 +++++++++++++++++++++++++++ lib/distribqueuehandler.php | 4 ++-- plugins/OStatus/lib/salmonaction.php | 3 ++- 3 files changed, 31 insertions(+), 3 deletions(-) (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/classes/Notice.php b/classes/Notice.php index 6f1ef81fc..a12839d72 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -333,8 +333,15 @@ class Notice extends Memcached_DataObject # Clear the cache for subscribed users, so they'll update at next request # XXX: someone clever could prepend instead of clearing the cache + $notice->blowOnInsert(); + if (isset($replies)) { + $notice->saveKnownReplies($replies); + } else { + $notice->saveReplies(); + } + $notice->distribute(); return $notice; @@ -817,6 +824,26 @@ class Notice extends Memcached_DataObject return true; } + function saveKnownReplies($uris) + { + foreach ($uris as $uri) { + + $user = User::staticGet('uri', $uri); + + if (!empty($user)) { + + $reply = new Reply(); + + $reply->notice_id = $this->id; + $reply->profile_id = $user->id; + + $id = $reply->insert(); + } + } + + return; + } + /** * @return array of integer profile IDs */ diff --git a/lib/distribqueuehandler.php b/lib/distribqueuehandler.php index 4477468d0..c31b675c1 100644 --- a/lib/distribqueuehandler.php +++ b/lib/distribqueuehandler.php @@ -75,7 +75,7 @@ class DistribQueueHandler } try { - $recipients = $notice->saveReplies(); + $recipients = $notice->getReplies(); } catch (Exception $e) { $this->logit($notice, $e); } @@ -107,7 +107,7 @@ class DistribQueueHandler return true; } - + protected function logit($notice, $e) { common_log(LOG_ERR, "Distrib queue exception saving notice $notice->id: " . diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index b128cbd13..4aba20cc4 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -182,7 +182,8 @@ class SalmonAction extends Action $options = array('is_local' => Notice::REMOTE_OMB, 'uri' => $this->act->object->id, 'url' => $this->act->object->link, - 'rendered' => $rendered); + 'rendered' => $rendered, + 'replies' => $this->act->context->attention); if (!empty($this->act->context->location)) { $options['lat'] = $location->lat; -- cgit v1.2.3-54-g00ecf From 2f65fa646acc9a0739e779de9e472b9957c2e7eb Mon Sep 17 00:00:00 2001 From: James Walker Date: Mon, 22 Feb 2010 09:05:52 -0500 Subject: wiring in magicsig --- plugins/OStatus/lib/salmon.php | 17 ++++++++++++++--- plugins/OStatus/lib/salmonaction.php | 8 +++++++- 2 files changed, 21 insertions(+), 4 deletions(-) (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php index df17a7006..53925dc3f 100644 --- a/plugins/OStatus/lib/salmon.php +++ b/plugins/OStatus/lib/salmon.php @@ -34,6 +34,8 @@ class Salmon return FALSE; } + $xml = $this->createMagicEnv($xml); + $headers = array('Content-type: application/atom+xml'); try { @@ -52,16 +54,25 @@ class Salmon } - public function createMagicEnv($text, $userid) + public function createMagicEnv($text) { + $magic_env = new MagicEnvelope(); + // TODO: Should probably be getting the signer uri as an argument? + $signer_uri = $magic_env->getAuthor($text); + $env = $magic_env->signMessage($text, 'application/atom+xml', $signer_uri); + + return $magic_env->unfold($env); } - public function verifyMagicEnv($env) + public function verifyMagicEnv($dom) { + $magic_env = new MagicEnvelope(); + + $env = $magic_env->fromDom($dom); - + return $magic_env->verify($env); } } diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 4aba20cc4..09a042975 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -54,8 +54,14 @@ class SalmonAction extends Action common_log(LOG_DEBUG, "Got invalid Salmon post: $xml"); $this->clientError(_m('Salmon post must be an Atom entry.')); } - // XXX: check the signature + // Check the signature + $salmon = new Salmon; + if (!$salmon->verifyMagicEnv($dom)) { + common_log(LOG_DEBUG, "Salmon signature verification failed."); + $this->clientError(_m('Salmon signature verification failed.')); + } + $this->act = new Activity($dom->documentElement); return true; } -- cgit v1.2.3-54-g00ecf From 06f155c02df91ae81eb4401c738815ee46b802a6 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 22 Feb 2010 09:43:27 -0800 Subject: OStatus: initial hookup of remote group membership (notice delivery not yet working quite right) - added a temp config var to disable salmon magic signatures until they're working consistently --- plugins/OStatus/OStatusPlugin.php | 93 ++++++++++++++- plugins/OStatus/actions/groupsalmon.php | 77 ++++++++++++- plugins/OStatus/actions/ostatussub.php | 2 +- plugins/OStatus/classes/Ostatus_profile.php | 169 +++++++++++++++++++++------- plugins/OStatus/lib/activity.php | 6 + plugins/OStatus/lib/salmon.php | 21 +++- plugins/OStatus/lib/salmonaction.php | 23 +++- 7 files changed, 336 insertions(+), 55 deletions(-) (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 7c6c0c69f..061ed4bd1 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -211,7 +211,7 @@ class OStatusPlugin extends Plugin // FIXME: this needs to go out in a queue handler - $xml = ''; + $xml = ''; $xml .= $notice->asAtomEntry(true, true); $salmon = new Salmon(); @@ -402,6 +402,97 @@ class OStatusPlugin extends Plugin return true; } + /** + * When one of our local users tries to join a remote group, + * notify the remote server. If the notification is rejected, + * deny the join. + * + * @param User_group $group + * @param User $user + * + * @return mixed hook return value + */ + + function onStartJoinGroup($group, $user) + { + $oprofile = Ostatus_profile::staticGet('group_id', $group->id); + if ($oprofile) { + $member = Profile::staticGet($user->id); + + $act = new Activity(); + $act->id = TagURI::mint('join:%d:%d:%s', + $member->id, + $group->id, + common_date_iso8601(time())); + + $act->actor = ActivityObject::fromProfile($member); + $act->verb = ActivityVerb::JOIN; + $act->object = $oprofile->asActivityObject(); + + $act->time = time(); + $act->title = _m("Join"); + $act->content = sprintf(_m("%s has joined group %s."), + $member->getBestName(), + $oprofile->getBestName()); + + if ($oprofile->notifyActivity($act)) { + return true; + } else { + throw new ServerException(_m("Failed joining remote group.")); + } + } + } + + /** + * When one of our local users leaves a remote group, notify the remote + * server. + * + * @fixme Might be good to schedule a resend of the leave notification + * if it failed due to a transitory error. We've canceled the local + * membership already anyway, but if the remote server comes back up + * it'll be left with a stray membership record. + * + * @param User_group $group + * @param User $user + * + * @return mixed hook return value + */ + + function onEndLeaveGroup($group, $user) + { + $oprofile = Ostatus_profile::staticGet('group_id', $group->id); + if ($oprofile) { + // Drop the PuSH subscription if there are no other subscribers. + + $members = $group->getMembers(0, 1); + if ($members->N == 0) { + common_log(LOG_INFO, "Unsubscribing from now-unused group feed $oprofile->feeduri"); + $oprofile->unsubscribe(); + } + + + $member = Profile::staticGet($user->id); + + $act = new Activity(); + $act->id = TagURI::mint('leave:%d:%d:%s', + $member->id, + $group->id, + common_date_iso8601(time())); + + $act->actor = ActivityObject::fromProfile($member); + $act->verb = ActivityVerb::LEAVE; + $act->object = $oprofile->asActivityObject(); + + $act->time = time(); + $act->title = _m("Leave"); + $act->content = sprintf(_m("%s has left group %s."), + $member->getBestName(), + $oprofile->getBestName()); + + $oprofile->notifyActivity($act); + } + } + /** * Notify remote users when their notices get favorited. * diff --git a/plugins/OStatus/actions/groupsalmon.php b/plugins/OStatus/actions/groupsalmon.php index 64ae9f3cc..2e4fe9443 100644 --- a/plugins/OStatus/actions/groupsalmon.php +++ b/plugins/OStatus/actions/groupsalmon.php @@ -88,21 +88,96 @@ class GroupsalmonAction extends SalmonAction * Save a subscription relationship for them. */ + /** + * Postel's law: consider a "follow" notification as a "join". + */ function handleFollow() { - $this->handleJoin(); // ??? + $this->handleJoin(); } + /** + * Postel's law: consider an "unfollow" notification as a "leave". + */ function handleUnfollow() { + $this->handleLeave(); } /** * A remote user joined our group. + * @fixme move permission checks and event call into common code, + * currently we're doing the main logic in joingroup action + * and so have to repeat it here. */ function handleJoin() { + $oprofile = $this->ensureProfile(); + if (!$oprofile) { + $this->clientError(_m("Can't read profile to set up group membership.")); + } + if ($oprofile->isGroup()) { + $this->clientError(_m("Groups can't join groups.")); + } + + common_log(LOG_INFO, "Remote profile {$oprofile->uri} joining local group {$this->group->nickname}"); + $profile = $oprofile->localProfile(); + + if ($profile->isMember($this->group)) { + // Already a member; we'll take it silently to aid in resolving + // inconsistencies on the other side. + return true; + } + + if (Group_block::isBlocked($this->group, $profile)) { + $this->clientError(_('You have been blocked from that group by the admin.'), 403); + return false; + } + + try { + // @fixme that event currently passes a user from main UI + // Event should probably move into Group_member::join + // and take a Profile object. + // + //if (Event::handle('StartJoinGroup', array($this->group, $profile))) { + Group_member::join($this->group->id, $profile->id); + //Event::handle('EndJoinGroup', array($this->group, $profile)); + //} + } catch (Exception $e) { + $this->serverError(sprintf(_m('Could not join remote user %1$s to group %2$s.'), + $oprofile->uri, $this->group->nickname)); + } + } + + /** + * A remote user left our group. + */ + + function handleLeave() + { + $oprofile = $this->ensureProfile(); + if (!$oprofile) { + $this->clientError(_m("Can't read profile to cancel group membership.")); + } + if ($oprofile->isGroup()) { + $this->clientError(_m("Groups can't join groups.")); + } + + common_log(LOG_INFO, "Remote profile {$oprofile->uri} leaving local group {$this->group->nickname}"); + $profile = $oprofile->localProfile(); + + try { + // @fixme event needs to be refactored as above + //if (Event::handle('StartLeaveGroup', array($this->group, $profile))) { + Group_member::leave($this->group->id, $profile->id); + //Event::handle('EndLeaveGroup', array($this->group, $profile)); + //} + } catch (Exception $e) { + $this->serverError(sprintf(_m('Could not remove remote user %1$s from group %2$s.'), + $oprofile->uri, $this->group->nickname)); + return; + } } } diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php index 95dec19af..592ae387e 100644 --- a/plugins/OStatus/actions/ostatussub.php +++ b/plugins/OStatus/actions/ostatussub.php @@ -248,7 +248,7 @@ class OStatusSubAction extends Action $group = $this->oprofile->localGroup(); if ($user->isMember($group)) { $this->showForm(_m('Already a member!')); - } elseif (Group_member::join($this->profile->group_id, $user->id)) { + } elseif (Group_member::join($this->oprofile->group_id, $user->id)) { $this->showForm(_m('Joined remote group!')); } else { $this->showForm(_m('Remote group join failed!')); diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 0e12f8fc6..c0e39add8 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -137,12 +137,49 @@ class Ostatus_profile extends Memcached_DataObject return null; } + /** + * Returns an ActivityObject describing this remote user or group profile. + * Can then be used to generate Atom chunks. + * + * @return ActivityObject + */ + function asActivityObject() + { + if ($this->isGroup()) { + $object = new ActivityObject(); + $object->type = 'http://activitystrea.ms/schema/1.0/group'; + $object->id = $this->uri; + $self = $this->localGroup(); + + // @fixme put a standard getAvatar() interface on groups too + if ($self->homepage_logo) { + $object->avatar = $self->homepage_logo; + $map = array('png' => 'image/png', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif'); + $extension = pathinfo(parse_url($avatarHref, PHP_URL_PATH), PATHINFO_EXTENSION); + if (isset($map[$extension])) { + // @fixme this ain't used/saved yet + $object->avatarType = $map[$extension]; + } + } + + $object->link = $this->uri; // @fixme accurate? + return $object; + } else { + return ActivityObject::fromProfile($this->localProfile()); + } + } + /** * Returns an XML string fragment with profile information as an * Activity Streams noun object with the given element type. * * Assumes that 'activity' namespace has been previously defined. * + * @fixme replace with wrappers on asActivityObject when it's got everything. + * * @param string $element one of 'actor', 'subject', 'object', 'target' * @return string */ @@ -202,11 +239,19 @@ class Ostatus_profile extends Memcached_DataObject } /** - * Damn dirty hack! + * @return boolean true if this is a remote group */ function isGroup() { - return (strpos($this->feeduri, '/groups/') !== false); + if ($this->profile_id && !$this->group_id) { + return false; + } else if ($this->group_id && !$this->profile_id) { + return true; + } else if ($this->group_id && $this->profile_id) { + throw new ServerException("Invalid ostatus_profile state: both group and profile IDs set for $this->uri"); + } else { + throw new ServerException("Invalid ostatus_profile state: both group and profile IDs empty for $this->uri"); + } } /** @@ -353,22 +398,24 @@ class Ostatus_profile extends Memcached_DataObject common_log(LOG_INFO, "Posting to Salmon endpoint $this->salmonuri: $xml"); $salmon = new Salmon(); // ? - $salmon->post($this->salmonuri, $xml); + return $salmon->post($this->salmonuri, $xml); } + return false; } public function notifyActivity($activity) { if ($this->salmonuri) { - $xml = $activity->asString(true); + $xml = '' . + $activity->asString(true); $salmon = new Salmon(); // ? - $salmon->post($this->salmonuri, $xml); + return $salmon->post($this->salmonuri, $xml); } - return; + return false; } function getBestName() @@ -597,10 +644,23 @@ class Ostatus_profile extends Memcached_DataObject */ protected function updateAvatar($url) { + if ($this->isGroup()) { + $self = $this->localGroup(); + } else { + $self = $this->localProfile(); + } + if (!$self) { + throw new ServerException(sprintf( + _m("Tried to update avatar for unsaved remote profile %s"), + $this->uri)); + } + // @fixme this should be better encapsulated // ripped from oauthstore.php (for old OMB client) $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); - copy($url, $temp_filename); + if (!copy($url, $temp_filename)) { + throw new ServerException(sprintf(_m("Unable to fetch avatar from %s"), $url)); + } if ($this->isGroup()) { $id = $this->group_id; @@ -614,13 +674,7 @@ class Ostatus_profile extends Memcached_DataObject null, common_timestamp()); rename($temp_filename, Avatar::path($filename)); - if ($this->isGroup()) { - $group = $this->localGroup(); - $group->setOriginal($filename); - } else { - $profile = $this->localProfile(); - $profile->setOriginal($filename); - } + $self->setOriginal($filename); } protected static function getActivityObjectAvatar($object) @@ -747,6 +801,18 @@ class Ostatus_profile extends Memcached_DataObject self::createActivityObjectProfile($actor, $feeduri, $salmonuri); } + /** + * Create local ostatus_profile and profile/user_group entries for + * the provided remote user or group. + * + * @param ActivityObject $object + * @param string $feeduri + * @param string $salmonuri + * @param array $hints + * + * @fixme fold $feeduri/$salmonuri into $hints + * @return Ostatus_profile + */ protected static function createActivityObjectProfile($object, $feeduri=null, $salmonuri=null, $hints=array()) { $homeuri = $object->id; @@ -784,46 +850,65 @@ class Ostatus_profile extends Memcached_DataObject } } - $profile = new Profile(); - $profile->nickname = $nickname; - $profile->fullname = $object->title; - if (!empty($object->link)) { - $profile->profileurl = $object->link; - } else if (array_key_exists('profileurl', $hints)) { - $profile->profileurl = $hints['profileurl']; - } - $profile->created = common_sql_now(); - - // @fixme bio - // @fixme tags/categories - // @fixme location? - // @todo tags from categories - // @todo lat/lon/location? - - $profile_id = $profile->insert(); - - if (!$profile_id) { - throw new ServerException("Can't save local profile"); - } - - // @fixme either need to do feed discovery here - // or need to split out some of the feed stuff - // so we can leave it empty until later. - $oprofile = new Ostatus_profile(); $oprofile->uri = $homeuri; $oprofile->feeduri = $feeduri; $oprofile->salmonuri = $salmonuri; - $oprofile->profile_id = $profile_id; $oprofile->created = common_sql_now(); $oprofile->modified = common_sql_now(); + if ($object->type == ActivityObject::PERSON) { + $profile = new Profile(); + $profile->nickname = $nickname; + $profile->fullname = $object->title; + if (!empty($object->link)) { + $profile->profileurl = $object->link; + } else if (array_key_exists('profileurl', $hints)) { + $profile->profileurl = $hints['profileurl']; + } + $profile->created = common_sql_now(); + + // @fixme bio + // @fixme tags/categories + // @fixme location? + // @todo tags from categories + // @todo lat/lon/location? + + $oprofile->profile_id = $profile->insert(); + + if (!$oprofile->profile_id) { + throw new ServerException("Can't save local profile"); + } + } else { + $group = new User_group(); + $group->nickname = $nickname; + $group->fullname = $object->title; + // @fixme no canonical profileurl; using homepage instead for now + $group->homepage = $homeuri; + $group->created = common_sql_now(); + + // @fixme homepage + // @fixme bio + // @fixme tags/categories + // @fixme location? + // @todo tags from categories + // @todo lat/lon/location? + + $oprofile->group_id = $group->insert(); + + if (!$oprofile->group_id) { + throw new ServerException("Can't save local profile"); + } + } + $ok = $oprofile->insert(); if ($ok) { - $oprofile->updateAvatar($avatar); + if ($avatar) { + $oprofile->updateAvatar($avatar); + } return $oprofile; } else { throw new ServerException("Can't save OStatus profile"); diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php index a26248f19..6cb9881bf 100644 --- a/plugins/OStatus/lib/activity.php +++ b/plugins/OStatus/lib/activity.php @@ -367,6 +367,9 @@ class ActivityObject return $object; } + /** + * @fixme missing avatar, bio info, etc + */ static function fromProfile($profile) { $object = new ActivityObject(); @@ -379,6 +382,9 @@ class ActivityObject return $object; } + /** + * @fixme missing avatar, bio info, etc + */ function asString($tag='activity:object') { $xs = new XMLStringer(true); diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php index 53925dc3f..b5f178cc6 100644 --- a/plugins/OStatus/lib/salmon.php +++ b/plugins/OStatus/lib/salmon.php @@ -28,15 +28,26 @@ */ class Salmon { + /** + * Sign and post the given Atom entry as a Salmon message. + * + * @fixme pass through the actor for signing? + * + * @param string $endpoint_uri + * @param string $xml + * @return boolean success + */ public function post($endpoint_uri, $xml) { if (empty($endpoint_uri)) { - return FALSE; + return false; } - $xml = $this->createMagicEnv($xml); - - $headers = array('Content-type: application/atom+xml'); + if (!common_config('ostatus', 'skip_signatures')) { + $xml = $this->createMagicEnv($xml); + } + + $headers = array('Content-Type: application/atom+xml'); try { $client = new HTTPClient(); @@ -51,7 +62,7 @@ class Salmon $response->getStatus() . ': ' . $response->getBody()); return false; } - + return true; } public function createMagicEnv($text) diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 09a042975..83cf0b8f8 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -41,7 +41,7 @@ class SalmonAction extends Action $this->clientError(_('This method requires a POST.')); } - if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') { + if (empty($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != 'application/atom+xml') { $this->clientError(_('Salmon requires application/atom+xml')); } @@ -57,11 +57,13 @@ class SalmonAction extends Action // Check the signature $salmon = new Salmon; - if (!$salmon->verifyMagicEnv($dom)) { - common_log(LOG_DEBUG, "Salmon signature verification failed."); - $this->clientError(_m('Salmon signature verification failed.')); + if (!common_config('ostatus', 'skip_signatures')) { + if (!$salmon->verifyMagicEnv($dom)) { + common_log(LOG_DEBUG, "Salmon signature verification failed."); + $this->clientError(_m('Salmon signature verification failed.')); + } } - + $this->act = new Activity($dom->documentElement); return true; } @@ -101,6 +103,9 @@ class SalmonAction extends Action case ActivityVerb::JOIN: $this->handleJoin(); break; + case ActivityVerb::LEAVE: + $this->handleLeave(); + break; default: throw new ClientException(_("Unimplemented.")); } @@ -154,6 +159,14 @@ class SalmonAction extends Action throw new ClientException(_("Unimplemented!")); } + /** + * Hmmmm + */ + function handleLeave() + { + throw new ClientException(_("Unimplemented!")); + } + /** * @return Ostatus_profile */ -- cgit v1.2.3-54-g00ecf From 584b87cfe57bd2d683101194040e3563f0706536 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 24 Feb 2010 01:09:52 +0000 Subject: OStatus: consolidate the low-level notice save code between Salmon and PuSH input paths. Validation etc remains at higher levels. --- plugins/OStatus/OStatusPlugin.php | 2 +- plugins/OStatus/classes/Ostatus_profile.php | 214 ++++++++++++++++------------ plugins/OStatus/lib/salmonaction.php | 50 +------ 3 files changed, 125 insertions(+), 141 deletions(-) (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 724634924..35f952935 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -314,7 +314,7 @@ class OStatusPlugin extends Plugin { $oprofile = Ostatus_profile::staticGet('feeduri', $feedsub->uri); if ($oprofile) { - $oprofile->processFeed($feed); + $oprofile->processFeed($feed, 'push'); } else { common_log(LOG_DEBUG, "No ostatus profile for incoming feed $feedsub->uri"); } diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 4998809bc..6beaf0f5d 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -488,7 +488,7 @@ class Ostatus_profile extends Memcached_DataObject * * @param DOMDocument $feed */ - public function processFeed($feed) + public function processFeed($feed, $source) { $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); if ($entries->length == 0) { @@ -498,7 +498,7 @@ class Ostatus_profile extends Memcached_DataObject for ($i = 0; $i < $entries->length; $i++) { $entry = $entries->item($i); - $this->processEntry($entry, $feed); + $this->processEntry($entry, $feed, $source); } } @@ -508,15 +508,12 @@ class Ostatus_profile extends Memcached_DataObject * @param DOMElement $entry * @param DOMElement $feed for context */ - protected function processEntry($entry, $feed) + public function processEntry($entry, $feed, $source) { $activity = new Activity($entry, $feed); - $debug = var_export($activity, true); - common_log(LOG_DEBUG, $debug); - if ($activity->verb == ActivityVerb::POST) { - $this->processPost($activity); + $this->processPost($activity, $source); } else { common_log(LOG_INFO, "Ignoring activity with unrecognized verb $activity->verb"); } @@ -525,35 +522,47 @@ class Ostatus_profile extends Memcached_DataObject /** * Process an incoming post activity from this remote feed. * @param Activity $activity + * @param string $method 'push' or 'salmon' + * @return mixed saved Notice or false * @fixme break up this function, it's getting nasty long */ - protected function processPost($activity) + public function processPost($activity, $method) { if ($this->isGroup()) { + // A group feed will contain posts from multiple authors. // @fixme validate these profiles in some way! $oprofile = self::ensureActorProfile($activity); + if ($oprofile->isGroup()) { + // Groups can't post notices in StatusNet. + common_log(LOG_WARNING, "OStatus: skipping post with group listed as author: $oprofile->uri in feed from $this->uri"); + return false; + } } else { + // Individual user feeds may contain only posts from themselves. + // Authorship is validated against the profile URI on upper layers, + // through PuSH setup or Salmon signature checks. $actorUri = self::getActorProfileURI($activity); if ($actorUri == $this->uri) { // @fixme check if profile info has changed and update it } else { - // @fixme drop or reject the messages once we've got the canonical profile URI recorded sanely - common_log(LOG_INFO, "OStatus: Warning: non-group post with unexpected author: $actorUri expected $this->uri"); - //return; + common_log(LOG_WARNING, "OStatus: skipping post with bad author: got $actorUri expected $this->uri"); + return false; } $oprofile = $this; } - $sourceUri = $activity->object->id; + // The id URI will be used as a unique identifier for for the notice, + // protecting against duplicate saves. It isn't required to be a URL; + // tag: URIs for instance are found in Google Buzz feeds. + $sourceUri = $activity->object->id; $dupe = Notice::staticGet('uri', $sourceUri); - if ($dupe) { common_log(LOG_INFO, "OStatus: ignoring duplicate post: $sourceUri"); - return; + return false; } + // We'll also want to save a web link to the original notice, if provided. $sourceUrl = null; - if ($activity->object->link) { $sourceUrl = $activity->object->link; } else if ($activity->link) { @@ -563,103 +572,126 @@ class Ostatus_profile extends Memcached_DataObject } // Get (safe!) HTML and text versions of the content - - require_once(INSTALLDIR.'/extlib/HTMLPurifier/HTMLPurifier.auto.php'); - - $html = $activity->object->content; - - $purifier = new HTMLPurifier(); - - $rendered = $purifier->purify($html); - + $rendered = $this->purify($activity->object->content); $content = html_entity_decode(strip_tags($rendered)); - $params = array('is_local' => Notice::REMOTE_OMB, + $options = array('is_local' => Notice::REMOTE_OMB, 'url' => $sourceUrl, 'uri' => $sourceUri, - 'rendered' => $rendered); + 'rendered' => $rendered, + 'replies' => array(), + 'groups' => array()); - $location = $activity->context->location; + // Check for optional attributes... - if ($location) { - $params['lat'] = $location->lat; - $params['lon'] = $location->lon; - if ($location->location_id) { - $params['location_ns'] = $location->location_ns; - $params['location_id'] = $location->location_id; - } + if (!empty($activity->time)) { + $options['created'] = common_sql_date($activity->time); } - $profile = $oprofile->localProfile(); - $params['groups'] = array(); - $params['replies'] = array(); if ($activity->context) { - foreach ($activity->context->attention as $recipient) { - $roprofile = Ostatus_profile::staticGet('uri', $recipient); - if ($roprofile) { - if ($roprofile->isGroup()) { - // Deliver to local recipients of this remote group. - // @fixme sender verification? - $params['groups'][] = $roprofile->group_id; - continue; - } else { - // Delivery to remote users is the source service's job. - continue; - } - } - - $user = User::staticGet('uri', $recipient); - if ($user) { - // An @-reply directed to a local user. - // @fixme sender verification, spam etc? - $params['replies'][] = $recipient; - continue; + // Any individual or group attn: targets? + $replies = $activity->context->attention; + $options['groups'] = $this->filterReplies($oprofile, $replies); + $options['replies'] = $replies; + + // Maintain direct reply associations + // @fixme what about conversation ID? + if (!empty($activity->context->replyToID)) { + $orig = Notice::staticGet('uri', + $activity->context->replyToID); + if (!empty($orig)) { + $options['reply_to'] = $orig->id; } - - // @fixme we need a uri on user_group - // $group = User_group::staticGet('uri', $recipient); - $template = common_local_url('groupbyid', array('id' => '31337')); - $template = preg_quote($template, '/'); - $template = str_replace('31337', '(\d+)', $template); - common_log(LOG_DEBUG, $template); - if (preg_match("/$template/", $recipient, $matches)) { - $id = $matches[1]; - $group = User_group::staticGet('id', $id); - if ($group) { - // Deliver to all members of this local group. - // @fixme sender verification? - if ($profile->isMember($group)) { - common_log(LOG_DEBUG, "delivering to group $id $group->nickname"); - $params['groups'][] = $group->id; - } else { - common_log(LOG_DEBUG, "not delivering to group $id $group->nickname because sender $profile->nickname is not a member"); - } - continue; - } else { - common_log(LOG_DEBUG, "not delivering to missing group $id"); - } - } else { - common_log(LOG_DEBUG, "not delivering to groups for $recipient"); + } + + $location = $activity->context->location; + if ($location) { + $options['lat'] = $location->lat; + $options['lon'] = $location->lon; + if ($location->location_id) { + $options['location_ns'] = $location->location_ns; + $options['location_id'] = $location->location_id; } } } try { - $saved = Notice::saveNew($profile->id, + $saved = Notice::saveNew($oprofile->profile_id, $content, 'ostatus', - $params); + $options); + if ($saved) { + Ostatus_source::saveNew($saved, $this, $method); + } } catch (Exception $e) { - common_log(LOG_ERR, "Failed saving notice entry for $sourceUri: " . $e->getMessage()); - return; + common_log(LOG_ERR, "OStatus save of remote message $sourceUri failed: " . $e->getMessage()); + throw $e; } + common_log(LOG_INFO, "OStatus saved remote message $sourceUri as notice id $saved->id"); + return $saved; + } - // Record which feed this came through... - try { - Ostatus_source::saveNew($saved, $this, 'push'); - } catch (Exception $e) { - common_log(LOG_ERR, "Failed saving ostatus_source entry for $saved->notice_id: " . $e->getMessage()); + /** + * Clean up HTML + */ + protected function purify($html) + { + // @fixme disable caching or set a sane temp dir + require_once(INSTALLDIR.'/extlib/HTMLPurifier/HTMLPurifier.auto.php'); + $purifier = new HTMLPurifier(); + return $purifier->purify($html); + } + + /** + * Filters a list of recipient ID URIs to just those for local delivery. + * @param Ostatus_profile local profile of sender + * @param array in/out &$attention_uris set of URIs, will be pruned on output + * @return array of group IDs + */ + protected function filterReplies($sender, &$attention_uris) + { + $groups = array(); + $replies = array(); + foreach ($attention_uris as $recipient) { + // Is the recipient a local user? + $user = User::staticGet('uri', $recipient); + if ($user) { + // @fixme sender verification, spam etc? + $replies[] = $recipient; + continue; + } + + // Is the recipient a remote group? + $oprofile = Ostatus_profile::staticGet('uri', $recipient); + if ($oprofile) { + if ($oprofile->isGroup()) { + // Deliver to local members of this remote group. + // @fixme sender verification? + $groups[] = $oprofile->group_id; + } + continue; + } + + // Is the recipient a local group? + // @fixme we need a uri on user_group + // $group = User_group::staticGet('uri', $recipient); + $template = common_local_url('groupbyid', array('id' => '31337')); + $template = preg_quote($template, '/'); + $template = str_replace('31337', '(\d+)', $template); + if (preg_match("/$template/", $recipient, $matches)) { + $id = $matches[1]; + $group = User_group::staticGet('id', $id); + if ($group) { + // Deliver to all members of this local group if allowed. + if ($sender->localProfile()->isMember($group)) { + $groups[] = $group->id; + } + continue; + } + } } + $attention_uris = $replies; + return $groups; } /** diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 83cf0b8f8..9aac2ed52 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -185,54 +185,6 @@ class SalmonAction extends Action function saveNotice() { $oprofile = $this->ensureProfile(); - - // Get (safe!) HTML and text versions of the content - - require_once(INSTALLDIR.'/extlib/HTMLPurifier/HTMLPurifier.auto.php'); - - $html = $this->act->object->content; - - $purifier = new HTMLPurifier(); - - $rendered = $purifier->purify($html); - - $content = html_entity_decode(strip_tags($rendered)); - - $options = array('is_local' => Notice::REMOTE_OMB, - 'uri' => $this->act->object->id, - 'url' => $this->act->object->link, - 'rendered' => $rendered, - 'replies' => $this->act->context->attention); - - if (!empty($this->act->context->location)) { - $options['lat'] = $location->lat; - $options['lon'] = $location->lon; - if ($location->location_id) { - $options['location_ns'] = $location->location_ns; - $options['location_id'] = $location->location_id; - } - } - - if (!empty($this->act->context->replyToID)) { - $orig = Notice::staticGet('uri', - $this->act->context->replyToID); - if (!empty($orig)) { - $options['reply_to'] = $orig->id; - } - } - - if (!empty($this->act->time)) { - $options['created'] = common_sql_date($this->act->time); - } - - $saved = Notice::saveNew($oprofile->profile_id, - $content, - 'ostatus+salmon', - $options); - - // Record that this was saved through a validated Salmon source - // @fixme actually do the signature validation! - Ostatus_source::saveNew($saved, $oprofile, 'salmon'); - return $saved; + return $oprofile->processPost($this->act, 'salmon'); } } -- cgit v1.2.3-54-g00ecf From 93507a192755494417eb0433edd315880752c857 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 24 Feb 2010 23:28:01 +0000 Subject: OStatus: handle update-profile Salmon pings --- plugins/OStatus/classes/Ostatus_profile.php | 4 +-- plugins/OStatus/lib/salmonaction.php | 55 +++++++++++++++-------------- 2 files changed, 31 insertions(+), 28 deletions(-) (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index c755a094e..33b0ceb9e 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -971,7 +971,7 @@ class Ostatus_profile extends Memcached_DataObject * @param Activity $activity * @return mixed matching Ostatus_profile or false if none known */ - protected static function getActorProfile($activity) + public static function getActorProfile($activity) { return self::getActivityObjectProfile($activity->actor); } @@ -1109,7 +1109,7 @@ class Ostatus_profile extends Memcached_DataObject * @param ActivityObject $object * @param array $hints */ - protected function updateFromActivityObject($object, $hints=array()) + public function updateFromActivityObject($object, $hints=array()) { if ($this->isGroup()) { $group = $this->localGroup(); diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 9aac2ed52..a03169101 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -38,11 +38,11 @@ class SalmonAction extends Action parent::prepare($args); if ($_SERVER['REQUEST_METHOD'] != 'POST') { - $this->clientError(_('This method requires a POST.')); + $this->clientError(_m('This method requires a POST.')); } if (empty($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != 'application/atom+xml') { - $this->clientError(_('Salmon requires application/atom+xml')); + $this->clientError(_m('Salmon requires application/atom+xml')); } $xml = file_get_contents('php://input'); @@ -76,8 +76,7 @@ class SalmonAction extends Action { StatusNet::setApi(true); // Send smaller error pages - // TODO : Insert new $xml -> notice code - + common_log(LOG_DEBUG, "Got a " . $this->act->verb); if (Event::handle('StartHandleSalmon', array($this->activity))) { switch ($this->act->verb) { @@ -106,8 +105,11 @@ class SalmonAction extends Action case ActivityVerb::LEAVE: $this->handleLeave(); break; + case ActivityVerb::UPDATE_PROFILE: + $this->handleUpdateProfile(); + break; default: - throw new ClientException(_("Unimplemented.")); + throw new ClientException(_m("Unrecognized activity type.")); } Event::handle('EndHandleSalmon', array($this->activity)); } @@ -115,56 +117,57 @@ class SalmonAction extends Action function handlePost() { - throw new ClientException(_("Unimplemented!")); + throw new ClientException(_m("This target doesn't understand posts.")); } function handleFollow() { - throw new ClientException(_("Unimplemented!")); + throw new ClientException(_m("This target doesn't understand follows.")); } function handleUnfollow() { - throw new ClientException(_("Unimplemented!")); + throw new ClientException(_m("This target doesn't understand unfollows.")); } function handleFavorite() { - throw new ClientException(_("Unimplemented!")); + throw new ClientException(_m("This target doesn't understand favorites.")); } - /** - * Remote user doesn't like one of our posts after all! - * Confirm the post is ours, and delete a local favorite event. - */ - function handleUnfavorite() { - throw new ClientException(_("Unimplemented!")); + throw new ClientException(_m("This target doesn't understand unfavorites.")); } - /** - * Hmmmm - */ function handleShare() { - throw new ClientException(_("Unimplemented!")); + throw new ClientException(_m("This target doesn't understand share events.")); } - /** - * Hmmmm - */ function handleJoin() { - throw new ClientException(_("Unimplemented!")); + throw new ClientException(_m("This target doesn't understand joins.")); + } + + function handleLeave() + { + throw new ClientException(_m("This target doesn't understand leave events.")); } /** - * Hmmmm + * Remote user sent us an update to their profile. + * If we already know them, accept the updates. */ - function handleLeave() + function handleUpdateProfile() { - throw new ClientException(_("Unimplemented!")); + $oprofile = Ostatus_profile::getActorProfile($this->act); + if ($oprofile) { + common_log(LOG_INFO, "Got a profile-update ping from $oprofile->uri"); + $oprofile->updateFromActivityObject($this->act->actor); + } else { + common_log(LOG_INFO, "Ignoring profile-update ping from unknown " . $this->act->actor->id); + } } /** -- cgit v1.2.3-54-g00ecf From e4c462570f8010f751caf214f329617c08bf7105 Mon Sep 17 00:00:00 2001 From: James Walker Date: Fri, 26 Feb 2010 15:39:30 -0500 Subject: move salmon posting to send application/magic-envelope+xml per http://salmon-protocol.googlecode.com/svn/trunk/draft-panzer-salmon-00.html#RPF --- plugins/OStatus/lib/magicenvelope.php | 22 ++++++++++++++++++++++ plugins/OStatus/lib/salmon.php | 18 +++++++++++------- plugins/OStatus/lib/salmonaction.php | 26 ++++++++++++++------------ 3 files changed, 47 insertions(+), 19 deletions(-) (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/plugins/OStatus/lib/magicenvelope.php b/plugins/OStatus/lib/magicenvelope.php index f33119b8f..230d81ba1 100644 --- a/plugins/OStatus/lib/magicenvelope.php +++ b/plugins/OStatus/lib/magicenvelope.php @@ -83,6 +83,28 @@ class MagicEnvelope } + public function toXML($env) { + $dom = new DOMDocument(); + + $envelope = $dom->createElementNS(MagicEnvelope::NS, 'me:env'); + $envelope->setAttribute('xmlns:me', MagicEnvelope::NS); + $data = $dom->createElementNS(MagicEnvelope::NS, 'me:data', $env['data']); + $data->setAttribute('type', $env['data_type']); + $envelope->appendChild($data); + $enc = $dom->createElementNS(MagicEnvelope::NS, 'me:encoding', $env['encoding']); + $envelope->appendChild($enc); + $alg = $dom->createElementNS(MagicEnvelope::NS, 'me:alg', $env['alg']); + $envelope->appendChild($alg); + $sig = $dom->createElementNS(MagicEnvelope::NS, 'me:sig', $env['sig']); + $envelope->appendChild($sig); + + $dom->appendChild($envelope); + + + return $dom->saveXML(); + } + + public function unfold($env) { $dom = new DOMDocument(); diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php index 6e2459544..68883a410 100644 --- a/plugins/OStatus/lib/salmon.php +++ b/plugins/OStatus/lib/salmon.php @@ -48,12 +48,17 @@ class Salmon return false; } - if (!common_config('ostatus', 'skip_signatures')) { + try { $xml = $this->createMagicEnv($xml, $actor); + } catch (Exception $e) { + common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage()); + return false; } - $headers = array('Content-Type: application/atom+xml'); + $headers = array('Content-Type: application/magic-envelope+xml'); + common_log(LOG_DEBUG, "Salmon: going to post " . $xml); + try { $client = new HTTPClient(); $client->setBody($xml); @@ -72,7 +77,6 @@ class Salmon public function createMagicEnv($text, $actor) { - common_log(LOG_DEBUG, "Got actor as : ". print_r($actor, true)); $magic_env = new MagicEnvelope(); $user = User::staticGet('id', $actor->id); @@ -84,7 +88,6 @@ class Salmon $magickey = new Magicsig(); $magickey->generate($user->id); } - common_log(LOG_DEBUG, "Salmon: Loaded key for ". $user->id); } else { throw new Exception("Salmon invalid actor for signing"); } @@ -95,15 +98,16 @@ class Salmon common_log(LOG_ERR, "Salmon signing failed: ". $e->getMessage()); return $text; } - return $magic_env->unfold($env); + return $magic_env->toXML($env); } - public function verifyMagicEnv($dom) + public function verifyMagicEnv($text) { + common_log(LOG_DEBUG, "Going to verify ". $text); $magic_env = new MagicEnvelope(); - $env = $magic_env->fromDom($dom); + $env = $magic_env->parse($text); return $magic_env->verify($env); } diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index a03169101..9ca350e67 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -41,29 +41,31 @@ class SalmonAction extends Action $this->clientError(_m('This method requires a POST.')); } - if (empty($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != 'application/atom+xml') { - $this->clientError(_m('Salmon requires application/atom+xml')); + if (empty($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != 'application/magic-envelope+xml') { + $this->clientError(_m('Salmon requires application/magic-envelope+xml')); } $xml = file_get_contents('php://input'); - $dom = DOMDocument::loadXML($xml); + // Check the signature + $salmon = new Salmon; + if (!$salmon->verifyMagicEnv($xml)) { + common_log(LOG_DEBUG, "Salmon signature verification failed."); + $this->clientError(_m('Salmon signature verification failed.')); + } else { + $env = MagicEnvelope::parse($xml); + $xml = MagicEnvelope::unfold($env); + } + + + $dom = DOMDocument::loadXML($xml); if ($dom->documentElement->namespaceURI != Activity::ATOM || $dom->documentElement->localName != 'entry') { common_log(LOG_DEBUG, "Got invalid Salmon post: $xml"); $this->clientError(_m('Salmon post must be an Atom entry.')); } - // Check the signature - $salmon = new Salmon; - if (!common_config('ostatus', 'skip_signatures')) { - if (!$salmon->verifyMagicEnv($dom)) { - common_log(LOG_DEBUG, "Salmon signature verification failed."); - $this->clientError(_m('Salmon signature verification failed.')); - } - } - $this->act = new Activity($dom->documentElement); return true; } -- cgit v1.2.3-54-g00ecf From 1cf08c7ad7704cd92d59f319573e831d1115e996 Mon Sep 17 00:00:00 2001 From: James Walker Date: Fri, 26 Feb 2010 17:09:50 -0500 Subject: MagicEnvelope::parse shouldn't be called statically --- plugins/OStatus/lib/salmonaction.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'plugins/OStatus/lib/salmonaction.php') diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 9ca350e67..fa9dc3b1d 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -54,8 +54,9 @@ class SalmonAction extends Action common_log(LOG_DEBUG, "Salmon signature verification failed."); $this->clientError(_m('Salmon signature verification failed.')); } else { - $env = MagicEnvelope::parse($xml); - $xml = MagicEnvelope::unfold($env); + $magic_env = new MagicEnvelope(); + $env = $magic_env->parse($xml); + $xml = $magic_env->unfold($env); } -- cgit v1.2.3-54-g00ecf