From 71ecd689019a8086570c677af47ead4e02227fb3 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 16 Feb 2010 12:45:00 -0500 Subject: [PATCH 01/18] add a FIXME to Profile --- classes/Profile.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/classes/Profile.php b/classes/Profile.php index ab05bb8546..c79b1d893a 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -807,6 +807,8 @@ class Profile extends Memcached_DataObject null, 'http://activitystrea.ms/schema/1.0/person' ); + // FIXME: this presupposes a local user -- not necessarily the case + // instead use User::uri or Remote_profile::uri or Ostatus_profile::homeuri $xs->element( 'id', null, From 201110ffaa8ae0ce4e74fe46a7a730203be681cd Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 16 Feb 2010 12:49:54 -0500 Subject: [PATCH 02/18] restructure Activity classes to push more DOM stuff to ActivityUtils --- plugins/OStatus/lib/activity.php | 123 +++++++++++++++++++++---------- 1 file changed, 83 insertions(+), 40 deletions(-) diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php index 048efda2c9..3d02e35848 100644 --- a/plugins/OStatus/lib/activity.php +++ b/plugins/OStatus/lib/activity.php @@ -63,22 +63,78 @@ class ActivityUtils * @return string related link, if any */ - static function getLink($element) + static function getPermalink($element) + { + return self::getLink($element, 'alternate', 'text/html'); + } + + /** + * Get the permalink for an Activity object + * + * @param DOMElement $element A DOM element + * + * @return string related link, if any + */ + + static function getLink($element, $rel, $type=null) { $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK); foreach ($links as $link) { - $rel = $link->getAttribute(self::REL); - $type = $link->getAttribute(self::TYPE); + $linkRel = $link->getAttribute(self::REL); + $linkType = $link->getAttribute(self::TYPE); - if ($rel == 'alternate' && $type == 'text/html') { + if ($linkRel == $rel && + (is_null($type) || $linkType == $type)) { return $link->getAttribute(self::HREF); } } return null; } + + /** + * Gets the first child element with the given tag + * + * @param DOMElement $element element to pick at + * @param string $tag tag to look for + * @param string $namespace Namespace to look under + * + * @return DOMElement found element or null + */ + + static function child($element, $tag, $namespace=self::ATOM) + { + $els = $element->getElementsByTagnameNS($namespace, $tag); + + if (empty($els) || $els->length == 0) { + return null; + } else { + return $els->item(0); + } + } + + /** + * Grab the text content of a DOM element child of the current element + * + * @param DOMElement $element Element whose children we examine + * @param string $tag Tag to look up + * @param string $namespace Namespace to use, defaults to Atom + * + * @return string content of the child + */ + + static function childContent($element, $tag, $namespace=self::ATOM) + { + $el = self::child($element, $tag, $namespace); + + if (empty($el)) { + return null; + } else { + return $el->textContent; + } + } } /** @@ -130,6 +186,7 @@ class ActivityObject const URI = 'uri'; const EMAIL = 'email'; + public $element; public $type; public $id; public $title; @@ -150,7 +207,7 @@ class ActivityObject function __construct($element) { - $this->source = $element; + $this->element = $element; if ($element->tagName == 'author') { @@ -179,33 +236,35 @@ class ActivityObject $this->title = $this->_childContent($element, self::TITLE); $this->summary = $this->_childContent($element, self::SUMMARY); $this->content = $this->_childContent($element, self::CONTENT); - $this->source = $this->_childContent($element, self::SOURCE); - $this->link = ActivityUtils::getLink($element); + $this->source = $this->_getSource($element); + + $this->link = ActivityUtils::getPermalink($element); // XXX: grab PoCo stuff } } - /** - * Grab the text content of a DOM element child of the current element - * - * @param DOMElement $element Element whose children we examine - * @param string $tag Tag to look up - * @param string $namespace Namespace to use, defaults to Atom - * - * @return string content of the child - */ - - private function _childContent($element, $tag, $namespace=Activity::ATOM) + private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM) { - $els = $element->getElementsByTagnameNS($namespace, $tag); + return ActivityUtils::childContent($element, $tag, $namespace); + } - if (empty($els) || $els->length == 0) { + // Try to get a unique id for the source feed + + private function _getSource($element) + { + $sourceEl = ActivityUtils::child($element, 'source'); + + if (empty($sourceEl)) { return null; } else { - $el = $els->item(0); - return $el->textContent; + $href = ActivityUtils::getLink($sourceEl, 'self'); + if (!empty($href)) { + return $href; + } else { + return ActivityUtils::childContent($sourceEl, 'id'); + } } } } @@ -306,7 +365,7 @@ class Activity } } - $this->link = ActivityUtils::getLink($entry); + $this->link = ActivityUtils::getPermalink($entry); $verbEl = $this->_child($entry, self::VERB); @@ -370,24 +429,8 @@ class Activity return null; } - /** - * Gets the first child element with the given tag - * - * @param DOMElement $element element to pick at - * @param string $tag tag to look for - * @param string $namespace Namespace to look under - * - * @return DOMElement found element or null - */ - private function _child($element, $tag, $namespace=self::SPEC) { - $els = $element->getElementsByTagnameNS($namespace, $tag); - - if (empty($els) || $els->length == 0) { - return null; - } else { - return $els->item(0); - } + return ActivityUtils::child($element, $tag, $namespace); } } \ No newline at end of file From eea52c708b4688c9b39f24d3931edc9da2cf1b07 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 16 Feb 2010 11:32:10 -0800 Subject: [PATCH 03/18] Add rel="avatar" to img links in stanzas --- classes/Profile.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/classes/Profile.php b/classes/Profile.php index c79b1d893a..8f578c95a3 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -807,8 +807,6 @@ class Profile extends Memcached_DataObject null, 'http://activitystrea.ms/schema/1.0/person' ); - // FIXME: this presupposes a local user -- not necessarily the case - // instead use User::uri or Remote_profile::uri or Ostatus_profile::homeuri $xs->element( 'id', null, @@ -824,6 +822,7 @@ class Profile extends Memcached_DataObject $xs->element( 'link', array( 'type' => empty($avatar) ? 'image/png' : $avatar->mediatype, + 'rel' => 'avatar', 'href' => empty($avatar) ? Avatar::defaultImage(AVATAR_PROFILE_SIZE) : $avatar->displayUrl() From a116cde1a401b1959515b8e6b512c41603387031 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 16 Feb 2010 20:11:48 +0000 Subject: [PATCH 04/18] OStatus: fix for low-level remote subscribe --- plugins/OStatus/classes/Ostatus_profile.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index b750e18839..9b6ef2f163 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -225,7 +225,7 @@ class Ostatus_profile extends Memcached_DataObject try { $local = $munger->profile(); - if ($entity->isGroup()) { + if ($profile->isGroup()) { $group = new User_group(); $group->nickname = $local->nickname . '@remote'; // @fixme $group->fullname = $local->fullname; @@ -245,17 +245,17 @@ class Ostatus_profile extends Memcached_DataObject $profile->profile_id = $local->id; } - $profile->created = sql_common_date(); - $profile->lastupdate = sql_common_date(); + $profile->created = common_sql_now(); + $profile->lastupdate = common_sql_now(); $result = $profile->insert(); if (empty($result)) { throw new FeedDBException($profile); } - $entity->query('COMMIT'); + $profile->query('COMMIT'); } catch (FeedDBException $e) { common_log_db_error($e->obj, 'INSERT', __FILE__); - $entity->query('ROLLBACK'); + $profile->query('ROLLBACK'); return false; } @@ -269,7 +269,7 @@ class Ostatus_profile extends Memcached_DataObject } } - return $entity; + return $profile; } /** From 440ab9039178bfc58c55316eb9ba2e19551bd12b Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 16 Feb 2010 22:03:24 +0000 Subject: [PATCH 05/18] OStatus: fix up some recent regressions in subscription setup; fix state checks and verification token, and avatar save on setup. Needs updates for new atom code next... --- plugins/OStatus/actions/pushhub.php | 6 +- plugins/OStatus/classes/Ostatus_profile.php | 83 ++++++++++++++++----- plugins/OStatus/lib/feedmunger.php | 9 ++- 3 files changed, 73 insertions(+), 25 deletions(-) diff --git a/plugins/OStatus/actions/pushhub.php b/plugins/OStatus/actions/pushhub.php index 901c18f702..13ec09d528 100644 --- a/plugins/OStatus/actions/pushhub.php +++ b/plugins/OStatus/actions/pushhub.php @@ -44,7 +44,7 @@ class PushHubAction extends Action // PHP converts '.'s in incoming var names to '_'s. // It also merges multiple values, which'll break hub.verify and hub.topic for publishing // @fixme handle multiple args - $arg = str_replace('.', '_', $arg); + $arg = str_replace('hub.', 'hub_', $arg); return parent::arg($arg, $def); } @@ -96,7 +96,11 @@ class PushHubAction extends Action $sub = new HubSub(); $sub->topic = $feed; $sub->callback = $callback; + $sub->verify_token = $this->arg('hub.verify_token', null); $sub->secret = $this->arg('hub.secret', null); + if (strlen($sub->secret) > 200) { + throw new ClientException("hub.secret must be no longer than 200 chars", 400); + } $sub->setLease(intval($this->arg('hub.lease_seconds'))); // @fixme check for feeds we don't manage diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 9b6ef2f163..243211c31f 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -262,7 +262,7 @@ class Ostatus_profile extends Memcached_DataObject $avatar = $munger->getAvatar(); if ($avatar) { try { - $this->updateAvatar($avatar); + $profile->updateAvatar($avatar); } catch (Exception $e) { common_log(LOG_ERR, "Exception setting OStatus avatar: " . $e->getMessage()); @@ -283,8 +283,10 @@ class Ostatus_profile extends Memcached_DataObject // ripped from oauthstore.php (for old OMB client) $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); copy($url, $temp_filename); - $imagefile = new ImageFile($profile->id, $temp_filename); - $filename = Avatar::filename($profile->id, + + // @fixme should we be using different ids? + $imagefile = new ImageFile($this->id, $temp_filename); + $filename = Avatar::filename($this->id, image_type_to_extension($imagefile->type), null, common_timestamp()); @@ -376,17 +378,56 @@ class Ostatus_profile extends Memcached_DataObject * The hub will later send us a confirmation POST to /main/push/callback. * * @return bool true on success, false on failure + * @throws ServerException if feed state is not valid */ public function subscribe($mode='subscribe') { - if (common_config('feedsub', 'nohub')) { - // Fake it! We're just testing remote feeds w/o hubs. - return true; + if ($this->sub_state != '') { + throw new ServerException("Attempting to start PuSH subscription to feed in state $this->sub_state"); } - // @fixme use the verification token - #$token = md5(mt_rand() . ':' . $this->feeduri); - #$this->verify_token = $token; - #$this->update(); // @fixme + if (empty($this->huburi)) { + if (common_config('feedsub', 'nohub')) { + // Fake it! We're just testing remote feeds w/o hubs. + return true; + } else { + throw new ServerException("Attempting to start PuSH subscription for feed with no hub"); + } + } + + return $this->doSubscribe('subscribe'); + } + + /** + * Send a PuSH unsubscription request to the hub for this feed. + * The hub will later send us a confirmation POST to /main/push/callback. + * + * @return bool true on success, false on failure + * @throws ServerException if feed state is not valid + */ + public function unsubscribe() { + if ($this->sub_state != 'active') { + throw new ServerException("Attempting to end PuSH subscription to feed in state $this->sub_state"); + } + if (empty($this->huburi)) { + if (common_config('feedsub', 'nohub')) { + // Fake it! We're just testing remote feeds w/o hubs. + return true; + } else { + throw new ServerException("Attempting to end PuSH subscription for feed with no hub"); + } + } + + return $this->doSubscribe('unsubscribe'); + } + + protected function doSubscribe($mode) + { + $orig = clone($this); + $this->verify_token = md5(mt_rand() . ':' . $this->feeduri); + $this->sub_state = $mode; + $this->update($orig); + unset($orig); + try { $callback = common_local_url('pushcallback', array('feed' => $this->id)); $headers = array('Content-Type: application/x-www-form-urlencoded'); @@ -416,6 +457,13 @@ class Ostatus_profile extends Memcached_DataObject } catch (Exception $e) { // wtf! common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri"); + + $orig = clone($this); + $this->verify_token = null; + $this->sub_state = null; + $this->update($orig); + unset($orig); + return false; } } @@ -460,16 +508,6 @@ class Ostatus_profile extends Memcached_DataObject return $this->update($original); } - /** - * Send a PuSH unsubscription request to the hub for this feed. - * The hub will later send us a confirmation POST to /main/push/callback. - * - * @return bool true on success, false on failure - */ - public function unsubscribe() { - return $this->subscribe('unsubscribe'); - } - /** * Send an Activity Streams notification to the remote Salmon endpoint, * if so configured. @@ -568,6 +606,11 @@ class Ostatus_profile extends Memcached_DataObject { common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml"); + if ($this->sub_state != 'active') { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH for inactive feed $this->feeduri (in state '$this->sub_state')"); + return; + } + if ($this->secret) { if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) { $their_hmac = strtolower($matches[1]); diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php index c895b6ce24..e8c46de90e 100644 --- a/plugins/OStatus/lib/feedmunger.php +++ b/plugins/OStatus/lib/feedmunger.php @@ -258,11 +258,12 @@ class FeedMunger { // hack hack hack // should get profile for this entry's author... - $remote = Ostatus_profile::staticGet('feeduri', $this->getSelfLink()); - if ($feed) { - return $feed->profile_id; + $feeduri = $this->getSelfLink(); + $remote = Ostatus_profile::staticGet('feeduri', $feeduri); + if ($remote) { + return $remote->profile_id; } else { - throw new Exception("Can't find feed profile"); + throw new Exception("Can't find feed profile for $feeduri"); } } From 880acb05b0f51f873e72a7b4d322cefafe7e850c Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 16 Feb 2010 22:04:57 +0000 Subject: [PATCH 06/18] OStatus: temporary output mode hack for apitimelineuser until PuSH feed generation is updated to use the shared code instead of output buffering --- actions/apitimelineuser.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index 24752e45fd..9f7ec4c236 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -196,7 +196,8 @@ class ApiTimelineUserAction extends ApiBareAuthAction $atom->addEntryFromNotices($this->notices); - $this->raw($atom->getString()); + #$this->raw($atom->getString()); + print $atom->getString(); // temporary for output buffering break; case 'json': From 014a32e6b873291bcd289a1ed25759a7a29221d7 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 16 Feb 2010 23:04:39 +0000 Subject: [PATCH 07/18] OStatus: check only direct children in ActivityUtil::child; fixes pulling actor's info when we wanted post info --- plugins/OStatus/lib/activity.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php index 3d02e35848..5b1c4fa8fa 100644 --- a/plugins/OStatus/lib/activity.php +++ b/plugins/OStatus/lib/activity.php @@ -106,12 +106,16 @@ class ActivityUtils static function child($element, $tag, $namespace=self::ATOM) { - $els = $element->getElementsByTagnameNS($namespace, $tag); - + $els = $element->childNodes; if (empty($els) || $els->length == 0) { return null; } else { - return $els->item(0); + for ($i = 0; $i < $els->length; $i++) { + $el = $els->item($i); + if ($el->localName == $tag && $el->namespaceURI == $namespace) { + return $el; + } + } } } From 4a139d1cc861272b45812969878fa62f81ed9cfe Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 16 Feb 2010 23:31:11 +0000 Subject: [PATCH 08/18] OStatus: migrated notice parsing to use Activity helper classes; on the way to killing FeedMunger --- plugins/OStatus/actions/pushcallback.php | 3 + plugins/OStatus/classes/Ostatus_profile.php | 240 ++++++++++++++------ 2 files changed, 173 insertions(+), 70 deletions(-) diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php index 2601a377a0..388c8f9c3d 100644 --- a/plugins/OStatus/actions/pushcallback.php +++ b/plugins/OStatus/actions/pushcallback.php @@ -59,6 +59,9 @@ class PushCallbackAction extends Action } $post = file_get_contents('php://input'); + + // @fixme Queue this to a background process; we should return + // as quickly as possible from a distribution POST. $profile->postUpdates($post, $hmac); } diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 243211c31f..4376d64c66 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -599,89 +599,189 @@ class Ostatus_profile extends Memcached_DataObject * Currently assumes that all items in the feed are new, * coming from a PuSH hub. * - * @param string $xml source of Atom or RSS feed + * @param string $post source of Atom or RSS feed * @param string $hmac X-Hub-Signature header, if present */ - public function postUpdates($xml, $hmac) + public function postUpdates($post, $hmac) { - common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml"); + common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $post"); if ($this->sub_state != 'active') { common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH for inactive feed $this->feeduri (in state '$this->sub_state')"); return; } - if ($this->secret) { - if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) { - $their_hmac = strtolower($matches[1]); - $our_hmac = hash_hmac('sha1', $xml, $this->secret); - if ($their_hmac !== $our_hmac) { - common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac"); - return; - } - } else { - common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'"); - return; - } - } else if ($hmac) { - common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'"); + if ($post === '') { + common_log(LOG_ERR, __METHOD__ . ": ignoring empty post"); return; } - require_once "XML/Feed/Parser.php"; - $feed = new XML_Feed_Parser($xml, false, false, true); - $munger = new FeedMunger($feed); - - $hits = 0; - foreach ($feed as $index => $entry) { - // @fixme this might sort in wrong order if we get multiple updates - - $notice = $munger->notice($index); - - // Double-check for oldies - // @fixme this could explode horribly for multiple feeds on a blog. sigh - - $dupe = Notice::staticGet('uri', $notice->uri); - - if (!empty($dupe)) { - common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}"); - continue; - } - - // @fixme need to ensure that groups get handled correctly - $saved = Notice::saveNew($notice->profile_id, - $notice->content, - 'ostatus', - array('is_local' => Notice::REMOTE_OMB, - 'uri' => $notice->uri, - 'lat' => $notice->lat, - 'lon' => $notice->lon, - 'location_ns' => $notice->location_ns, - 'location_id' => $notice->location_id)); - - /* - common_log(LOG_DEBUG, "going to check group delivery..."); - if ($this->group_id) { - $group = User_group::staticGet($this->group_id); - if ($group) { - common_log(LOG_INFO, __METHOD__ . ": saving to local shadow group $group->id $group->nickname"); - $groups = array($group); - } else { - common_log(LOG_INFO, __METHOD__ . ": lost the local shadow group?"); - } - } else { - common_log(LOG_INFO, __METHOD__ . ": no local shadow groups"); - $groups = array(); - } - common_log(LOG_DEBUG, "going to add to inboxes..."); - $notice->addToInboxes($groups, array()); - common_log(LOG_DEBUG, "added to inboxes."); - */ - - $hits++; + if (!$this->validatePushSig($post, $hmac)) { + // Per spec we silently drop input with a bad sig, + // while reporting receipt to the server. + return; } - if ($hits == 0) { - common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml"); + + $feed = new DOMDocument(); + if (!$feed->loadXML($post)) { + // @fixme might help to include the err message + common_log(LOG_ERR, __METHOD__ . ": ignoring invalid XML"); + return; + } + + $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); + if ($entries->length == 0) { + common_log(LOG_ERR, __METHOD__ . ": no entries in feed update, ignoring"); + return; + } + + for ($i = 0; $i < $entries->length; $i++) { + $entry = $entries->item($i); + $this->processEntry($entry, $feed); } } + + /** + * Validate the given Atom chunk and HMAC signature against our + * shared secret that was set up at subscription time. + * + * If we don't have a shared secret, there should be no signature. + * If we we do, our the calculated HMAC should match theirs. + * + * @param string $post raw XML source as POSTed to us + * @param string $hmac X-Hub-Signature HTTP header value, or empty + * @return boolean true for a match + */ + protected function validatePushSig($post, $hmac) + { + if ($this->secret) { + if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) { + $their_hmac = strtolower($matches[1]); + $our_hmac = hash_hmac('sha1', $post, $this->secret); + if ($their_hmac === $our_hmac) { + return true; + } + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac"); + } else { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'"); + } + } else { + if (empty($hmac)) { + return true; + } else { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'"); + } + } + return false; + } + + /** + * Process a posted entry from this feed source. + * + * @param DOMElement $entry + * @param DOMElement $feed for context + */ + protected function processEntry($entry, $feed) + { + $activity = new Activity($entry, $feed); + + $debug = var_export($activity, true); + common_log(LOG_DEBUG, $debug); + + if ($activity->verb == ActivityVerb::POST) { + $this->processPost($activity); + } else { + common_log(LOG_INFO, "Ignoring activity with unrecognized verb $activity->verb"); + } + } + + /** + * Process an incoming post activity from this remote feed. + * @param Activity $activity + */ + protected function processPost($activity) + { + // @fixme pull profile reference from actor for group feeds + $actor = $this; + $localProfile = $actor->localProfile(); + if (empty($localProfile)) { + common_log(LOG_INFO, "OStatus: ignoring post with invalid author"); + return; + } + + if (empty($activity->object)) { + // This shouldn't happen! + common_log(LOG_INFO, "OStatus: ignoring post with missing post object."); + return; + } + + if ($activity->object->link) { + $sourceUri = $activity->object->link; + } else if (preg_match('!^https?://!', $activity->object->id)) { + $sourceUri = $activity->object->id; + } else { + common_log(LOG_INFO, "OStatus: ignoring post with no source link: id $activity->object->id"); + return; + } + + $dupe = Notice::staticGet('uri', $sourceUri); + if ($dupe) { + common_log(LOG_INFO, "OStatus: ignoring duplicate post: $noticeLink"); + return; + } + + // @fixme sanitize and save HTML content if available + $content = $activity->object->title; + + $params = array('is_local' => Notice::REMOTE_OMB, + 'uri' => $sourceUri); + + $location = $this->getEntryLocation($activity->entry); + 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; + } + } + + // @fixme save detailed ostatus source info + // @fixme ensure that groups get handled correctly + + $saved = Notice::saveNew($localProfile->id, + $content, + 'ostatus', + $params); + } + + /** + * Parse location given as a GeoRSS-simple point, if provided. + * http://www.georss.org/simple + * + * @param feed item $entry + * @return mixed Location or false + */ + function getLocation($dom) + { + $points = $dom->getElementsByTagNameNS('http://www.georss.org/georss', 'point'); + + for ($i = 0; $i < $points->length; $i++) { + $point = $points->item(0)->textContent; + $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace" + $point = preg_replace('/\s+/', ' ', $point); + $point = trim($point); + $coords = explode(' ', $point); + if (count($coords) == 2) { + list($lat, $lon) = $coords; + if (is_numeric($lat) && is_numeric($lon)) { + common_log(LOG_INFO, "Looking up location for $lat $lon from georss"); + return Location::fromLatLon($lat, $lon); + } + } + common_log(LOG_ERR, "Ignoring bogus georss:point value $point"); + } + + return false; + } } From c892726c80b4e466b2bbad0f7b396cf0c7a137d9 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 16 Feb 2010 16:22:58 -0800 Subject: [PATCH 09/18] Take remote profiles into account when looking up canonical profile URIs --- EVENTS.txt | 5 ++++- classes/Notice.php | 2 +- classes/Profile.php | 21 +++++++++++++++------ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/EVENTS.txt b/EVENTS.txt index 69fe2ddccb..f333c5442f 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -1,4 +1,4 @@ -\InitializePlugin: a chance to initialize a plugin in a complete environment +InitializePlugin: a chance to initialize a plugin in a complete environment CleanupPlugin: a chance to cleanup a plugin at the end of a program @@ -722,3 +722,6 @@ StartRobotsTxt: Before outputting the robots.txt page EndRobotsTxt: After the default robots.txt page (good place for customization) - &$action: RobotstxtAction being shown +GetProfileUri: When determining the canonical URI for a given profile +- &$profile: the current profile + diff --git a/classes/Notice.php b/classes/Notice.php index 73b22d58a0..f184b9c52c 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1036,7 +1036,7 @@ class Notice extends Memcached_DataObject $xs->element( 'link', array( 'rel' => 'ostatus:attention', - 'href' => $profile->getAcctUri() + 'href' => $profile->getUri() ) ); } diff --git a/classes/Profile.php b/classes/Profile.php index 8f578c95a3..5a86619fd2 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -810,10 +810,7 @@ class Profile extends Memcached_DataObject $xs->element( 'id', null, - common_local_url( - 'userbyid', - array('id' => $this->id) - ) + $this->getUri() ); $xs->element('title', null, $this->getBestName()); @@ -835,9 +832,21 @@ class Profile extends Memcached_DataObject return $xs->getString(); } - function getAcctUri() + function getUri() { - return $this->nickname . '@' . common_config('site', 'server'); + if (Event::handle('GetProfileUri', array($this))) { + + $remote = Remote_profile::staticGet('id', $this->id); + + if (!empty($remote)) { + return $remote->uri; + } else { + return common_local_url( + 'userbyid', + array('id' => $this->id) + ); + } + } } } From 454d0b5738a1b79510b998b6da6d0df129a182ab Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 17 Feb 2010 01:49:49 +0000 Subject: [PATCH 10/18] OStatus: moving parts of profile processing to Activity from feedmunger. Pausing before refactoring DB schema a bit to clean up feed vs person vs group info --- plugins/OStatus/classes/Ostatus_profile.php | 185 ++++++++++++++++++-- plugins/OStatus/lib/activity.php | 8 + 2 files changed, 175 insertions(+), 18 deletions(-) diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 4376d64c66..be01cdfe19 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -218,10 +218,6 @@ class Ostatus_profile extends Memcached_DataObject $profile->query('BEGIN'); - // Awful hack! Awful hack! - $profile->verify = common_good_rand(16); - $profile->secret = common_good_rand(32); - try { $local = $munger->profile(); @@ -423,7 +419,10 @@ class Ostatus_profile extends Memcached_DataObject protected function doSubscribe($mode) { $orig = clone($this); - $this->verify_token = md5(mt_rand() . ':' . $this->feeduri); + $this->verify_token = common_good_rand(16); + if ($mode == 'subscribe') { + $this->secret = common_good_rand(32); + } $this->sub_state = $mode; $this->update($orig); unset($orig); @@ -701,18 +700,19 @@ class Ostatus_profile extends Memcached_DataObject */ protected function processPost($activity) { - // @fixme pull profile reference from actor for group feeds - $actor = $this; - $localProfile = $actor->localProfile(); - if (empty($localProfile)) { - common_log(LOG_INFO, "OStatus: ignoring post with invalid author"); - return; - } - - if (empty($activity->object)) { - // This shouldn't happen! - common_log(LOG_INFO, "OStatus: ignoring post with missing post object."); - return; + if ($this->isGroup()) { + // @fixme validate these profiles in some way! + $oprofile = $this->ensureActorProfile($activity); + } else { + $actorUri = $this->getActorProfileURI($activity); + if ($actorUri == $this->homeuri) { + // @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->homeuri"); + //return; + } + $oprofile = $this; } if ($activity->object->link) { @@ -749,7 +749,7 @@ class Ostatus_profile extends Memcached_DataObject // @fixme save detailed ostatus source info // @fixme ensure that groups get handled correctly - $saved = Notice::saveNew($localProfile->id, + $saved = Notice::saveNew($oprofile->localProfile()->id, $content, 'ostatus', $params); @@ -784,4 +784,153 @@ class Ostatus_profile extends Memcached_DataObject return false; } + + /** + * Get an appropriate avatar image source URL, if available. + * + * @param ActivityObject $actor + * @param DOMElement $feed + * @return string + */ + function getAvatar($actor, $feed) + { + $url = ''; + $icon = ''; + if ($actor->avatar) { + $url = trim($actor->avatar); + } + if (!$url) { + // Check and on the feed + $els = $feed->childNodes(); + if ($els && $els->length) { + for ($i = 0; $i < $els->length; $i++) { + $el = $els->item($i); + if ($el->namespaceURI == Activity::ATOM) { + if (empty($url) && $el->localName == 'logo') { + $url = trim($el->textContent); + break; + } + if (empty($icon) && $el->localName == 'icon') { + // Use as a fallback + $icon = trim($el->textContent); + } + } + } + } + if ($icon && !$url) { + $url = $icon; + } + } + if ($url) { + $opts = array('allowed_schemes' => array('http', 'https')); + if (Validate::uri($url, $opts)) { + return $url; + } + } + return common_path('plugins/OStatus/images/96px-Feed-icon.svg.png'); + } + + /** + * @fixme move off of ostatus_profile or static? + */ + function ensureActorProfile($activity) + { + $profile = $this->getActorProfile($activity); + if (!$profile) { + $profile = $this->createActorProfile($activity); + } + return $profile; + } + + /** + * @param Activity $activity + * @return mixed matching Ostatus_profile or false if none known + */ + function getActorProfile($activity) + { + $homeuri = $this->getActorProfileURI($activity); + return Ostatus_profile::staticGet('homeuri', $homeuri); + } + + /** + * @param Activity $activity + * @return string + * @throws ServerException + */ + function getActorProfileURI($activity) + { + $opts = array('allowed_schemes' => array('http', 'https')); + $actor = $activity->actor; + if ($actor->id && Validate::uri($actor->id, $opts)) { + return $actor->id; + } + if ($actor->link && Validate::uri($actor->link, $opts)) { + return $actor->link; + } + throw new ServerException("No author ID URI found"); + } + + /** + * + */ + function createActorProfile($activity) + { + $actor = $activity->actor(); + $homeuri = $this->getActivityProfileURI($activity); + $nickname = $this->getAuthorNick($activity); + $avatar = $this->getAvatar($actor, $feed); + + $profile = new Profile(); + $profile->nickname = $nickname; + $profile->fullname = $actor->displayName; + $profile->homepage = $actor->link; // @fixme + $profile->profileurl = $homeuri; + // @fixme bio + // @fixme tags/categories + // @fixme location? + // @todo tags from categories + // @todo lat/lon/location? + + $ok = $profile->insert(); + if ($ok) { + $this->updateAvatar($profile, $avatar); + } else { + 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->homeuri = $homeuri; + $oprofile->profile_id = $profile->id; + + $ok = $oprofile->insert(); + if ($ok) { + return $oprofile; + } else { + throw new ServerException("Can't save OStatus profile"); + } + } + + /** + * @fixme move this into Activity? + * @param Activity $activity + * @return string + */ + function getAuthorNick($activity) + { + // @fixme not technically part of the actor? + foreach (array($activity->entry, $activity->feed) as $source) { + $author = ActivityUtil::child($source, 'author', Activity::ATOM); + if ($author) { + $name = ActivityUtil::child($author, 'name', Activity::ATOM); + if ($name) { + return trim($name->textContent); + } + } + } + return false; + } + } diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php index 5b1c4fa8fa..f137946ab4 100644 --- a/plugins/OStatus/lib/activity.php +++ b/plugins/OStatus/lib/activity.php @@ -247,6 +247,14 @@ class ActivityObject // XXX: grab PoCo stuff } + + // Some per-type attributes... + if ($this->type == self::PERSON || $this->type == self::GROUP) { + $this->displayName = $this->title; + + // @fixme we may have multiple avatars with different resolutions specified + $this->avatar = ActivityUtils::getLink($element, 'avatar'); + } } private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM) From e51e96d7248b281e7d0e59f5a9bdcd4e7e651e82 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 17 Feb 2010 02:16:03 +0000 Subject: [PATCH 11/18] OStatus: override source link with the source domain and link to original message --- lib/noticelist.php | 28 ++++++++++++++++------------ plugins/OStatus/OStatusPlugin.php | 13 +++++++++++++ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/lib/noticelist.php b/lib/noticelist.php index c05b990245..837cb90faa 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -492,30 +492,34 @@ class NoticeListItem extends Widget break; default: - $name = null; + $name = $source_name; $url = null; - $ns = Notice_source::staticGet($this->notice->source); + if (Event::handle('StartNoticeSourceLink', array($this->notice, &$name, &$url, &$title))) { + $ns = Notice_source::staticGet($this->notice->source); - if ($ns) { - $name = $ns->name; - $url = $ns->url; - } else { - $app = Oauth_application::staticGet('name', $this->notice->source); - if ($app) { - $name = $app->name; - $url = $app->source_url; + if ($ns) { + $name = $ns->name; + $url = $ns->url; + } else { + $app = Oauth_application::staticGet('name', $this->notice->source); + if ($app) { + $name = $app->name; + $url = $app->source_url; + } } } + Event::handle('EndNoticeSourceLink', array($this->notice, &$name, &$url, &$title)); if (!empty($name) && !empty($url)) { $this->out->elementStart('span', 'device'); $this->out->element('a', array('href' => $url, - 'rel' => 'external'), + 'rel' => 'external', + 'title' => $title), $name); $this->out->elementEnd('span'); } else { - $this->out->element('span', 'device', $source_name); + $this->out->element('span', 'device', $name); } break; } diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 3b1329d6c5..b6c9fa1d4c 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -289,4 +289,17 @@ class OStatusPlugin extends Plugin $action->script(common_path('plugins/OStatus/js/ostatus.js')); return true; } + + function onStartNoticeSourceLink($notice, &$name, &$url, &$title) + { + if ($notice->source == 'ostatus') { + $bits = parse_url($notice->uri); + $domain = $bits['host']; + + $name = $domain; + $url = $notice->uri; + $title = sprintf(_m("Sent from %s via OStatus"), $domain); + return false; + } + } } From 2cb243808c2c1540f2690bff5a2d9932fa428923 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 16 Feb 2010 20:13:39 -0800 Subject: [PATCH 12/18] More sensical profile::getUri() --- EVENTS.txt | 8 ++++++-- classes/Profile.php | 39 +++++++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/EVENTS.txt b/EVENTS.txt index f333c5442f..90242fa133 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -722,6 +722,10 @@ StartRobotsTxt: Before outputting the robots.txt page EndRobotsTxt: After the default robots.txt page (good place for customization) - &$action: RobotstxtAction being shown -GetProfileUri: When determining the canonical URI for a given profile -- &$profile: the current profile +StartGetProfileUri: When determining the canonical URI for a given profile +- $profile: the current profile +- &$uri: the URI +EndGetProfileUri: After determining the canonical URI for a given profile +- $profile: the current profile +- &$uri: the URI diff --git a/classes/Profile.php b/classes/Profile.php index 5a86619fd2..494c697e42 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -769,7 +769,7 @@ class Profile extends Memcached_DataObject $xs->elementStart('author'); $xs->element('name', null, $this->nickname); - $xs->element('uri', null, $this->profileurl); + $xs->element('uri', null, $this->getUri()); $xs->elementEnd('author'); return $xs->getString(); @@ -832,21 +832,40 @@ class Profile extends Memcached_DataObject return $xs->getString(); } + /** + * Returns the best URI for a profile. Plugins may override. + * + * @return string $uri + */ function getUri() { - if (Event::handle('GetProfileUri', array($this))) { + $uri = null; - $remote = Remote_profile::staticGet('id', $this->id); + // check for a local user first + $user = User::staticGet('id', $this->id); - if (!empty($remote)) { - return $remote->uri; - } else { - return common_local_url( - 'userbyid', - array('id' => $this->id) - ); + if (!empty($user)) { + $uri = common_local_url( + 'userbyid', + array('id' => $user->id) + ); + } else { + + // give plugins a chance to set the URI + if (Event::handle('StartGetProfileUri', array($this, &$uri))) { + + // return OMB profile if any + $remote = Remote_profile::staticGet('id', $this->id); + + if (!empty($remote)) { + $uri = $remote->uri; + } + + Event::handle('EndGetProfileUri', array($this, &$uri)); } } + + return $uri; } } From a2f8c5da171d23790811677affd7ca5301a995a5 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 16 Feb 2010 23:30:08 -0800 Subject: [PATCH 13/18] New Conversation DO to handle remote notices as conversation roots --- classes/Conversation.php | 49 ++++++++++++++++++++++++++++++++++++++++ classes/statusnet.ini | 9 ++++++++ db/statusnet.sql | 8 +++++++ 3 files changed, 66 insertions(+) create mode 100755 classes/Conversation.php diff --git a/classes/Conversation.php b/classes/Conversation.php new file mode 100755 index 0000000000..929b06c149 --- /dev/null +++ b/classes/Conversation.php @@ -0,0 +1,49 @@ +. + * + * @category Data + * @package StatusNet + * @author Zach Copley + * @copyright 2010 StatusNet Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +class Conversation extends Memcached_DataObject +{ + ###START_AUTOCODE + /* the code below is auto generated do not remove the above tag */ + + public $__table = 'conversation'; // table name + public $id; // int(4) primary_key not_null + public $uri; // varchar(225) unique_key not_null + public $created; // datetime not_null + public $modified; // timestamp not_null default_CURRENT_TIMESTAMP + + /* Static get */ + function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Session',$k,$v); } + + /* the code above is auto generated do not remove the tag below */ + ###END_AUTOCODE +} + diff --git a/classes/statusnet.ini b/classes/statusnet.ini index 5f8da7cf51..7a9ae07e70 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -47,6 +47,15 @@ modified = 384 [consumer__keys] consumer_key = K +[conversation] +id = 129 +uri = 130 +created = 142 +modified = 384 + +[conversation__keys] +id = N + [deleted_notice] id = 129 profile_id = 129 diff --git a/db/statusnet.sql b/db/statusnet.sql index 3434648016..74e5b69547 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -633,3 +633,11 @@ create table inbox ( constraint primary key (user_id) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + +create table conversation ( + id integer auto_increment primary key comment 'unique identifier', + uri varchar(225) not null unique comment 'URI of the conversation', + created datetime not null comment 'date this record was created', + modified timestamp comment 'date this record was modified' +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + From ed46a38ecfea0a87e01aacfcde181087d5e0f19f Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 17 Feb 2010 01:11:14 -0800 Subject: [PATCH 14/18] - conversation.uri needs to be nullable - factory method for creating new local conversations --- classes/Conversation.php | 33 +++++++++++++++++++++++++++++++-- classes/statusnet.ini | 3 ++- db/statusnet.sql | 2 +- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/classes/Conversation.php b/classes/Conversation.php index 929b06c149..ea8bd87b56 100755 --- a/classes/Conversation.php +++ b/classes/Conversation.php @@ -36,14 +36,43 @@ class Conversation extends Memcached_DataObject public $__table = 'conversation'; // table name public $id; // int(4) primary_key not_null - public $uri; // varchar(225) unique_key not_null + public $uri; // varchar(225) unique_key public $created; // datetime not_null public $modified; // timestamp not_null default_CURRENT_TIMESTAMP /* Static get */ - function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Session',$k,$v); } + function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('conversation',$k,$v); } /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE + + /** + * Factory method for creating a new conversation + * + * @return Conversation the new conversation DO + */ + static function create() + { + $conv = new Conversation(); + $conv->created = common_sql_now(); + $id = $conv->insert(); + + if (empty($id)) { + common_log_db_error($conv, 'INSERT', __FILE__); + return null; + } + + $orig = clone($conv); + $orig->uri = common_local_url('conversation', array('id' => $id)); + $result = $orig->update($conv); + + if (empty($result)) { + common_log_db_error($conv, 'UPDATE', __FILE__); + return null; + } + + return $conv; + } + } diff --git a/classes/statusnet.ini b/classes/statusnet.ini index 7a9ae07e70..81c1b68b23 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -49,12 +49,13 @@ consumer_key = K [conversation] id = 129 -uri = 130 +uri = 2 created = 142 modified = 384 [conversation__keys] id = N +uri = U [deleted_notice] id = 129 diff --git a/db/statusnet.sql b/db/statusnet.sql index 74e5b69547..97117c80aa 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -636,7 +636,7 @@ create table inbox ( create table conversation ( id integer auto_increment primary key comment 'unique identifier', - uri varchar(225) not null unique comment 'URI of the conversation', + uri varchar(225) unique comment 'URI of the conversation', created datetime not null comment 'date this record was created', modified timestamp comment 'date this record was modified' ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; From 198c046c896c2a1c4dc9037fa538c14179e827ce Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 17 Feb 2010 01:12:13 -0800 Subject: [PATCH 15/18] - Set the root of a new local conversation to a new conversation.id - Output conversation URIs from conversation.uri --- classes/Notice.php | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index f184b9c52c..b0edb6de60 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -309,7 +309,8 @@ class Notice extends Memcached_DataObject // the beginning of a new conversation. if (empty($notice->conversation)) { - $notice->conversation = $notice->id; + $conv = Conversation::create(); + $notice->conversation = $conv->id; $changed = true; } @@ -331,14 +332,15 @@ class Notice extends Memcached_DataObject return $notice; } - function blowOnInsert() + function blowOnInsert($conversation = false) { self::blow('profile:notice_ids:%d', $this->profile_id); self::blow('public'); - if ($this->conversation != $this->id) { - self::blow('notice:conversation_ids:%d', $this->conversation); - } + // XXX: Before we were blowing the casche only if the notice id + // was not the root of the conversation. What to do now? + + self::blow('notice:conversation_ids:%d', $this->conversation); if (!empty($this->repeat_of)) { self::blow('notice:repeats:%d', $this->repeat_of); @@ -1015,24 +1017,25 @@ class Notice extends Memcached_DataObject } } - if (!empty($this->conversation) - && $this->conversation != $this->id) { - $xs->element( - 'link', array( - 'rel' => 'ostatus:conversation', - 'href' => common_local_url( - 'conversation', - array('id' => $this->conversation) - ) + if (!empty($this->conversation)) { + + $conv = Conversation::staticGet('id', $this->conversation); + + if (!empty($conv)) { + $xs->element( + 'link', array( + 'rel' => 'ostatus:conversation', + 'href' => $conv->uri ) ); + } } $reply_ids = $this->getReplies(); foreach ($reply_ids as $id) { $profile = Profile::staticGet('id', $id); - if (!empty($profile)) { + if (!empty($profile)) { $xs->element( 'link', array( 'rel' => 'ostatus:attention', From 9f3246124dc9702ff3a7e422df4adff687e62f9c Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 17 Feb 2010 09:58:34 -0800 Subject: [PATCH 16/18] PostDebug plugin - saves POST data to debug log or directory to help debug form submission and server-to-server communications. Some sensitive items are sanitized but not all - don't just shove out the log results publicly! --- plugins/PostDebug/PostDebugPlugin.php | 150 ++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 plugins/PostDebug/PostDebugPlugin.php diff --git a/plugins/PostDebug/PostDebugPlugin.php b/plugins/PostDebug/PostDebugPlugin.php new file mode 100644 index 0000000000..48fe28eabd --- /dev/null +++ b/plugins/PostDebug/PostDebugPlugin.php @@ -0,0 +1,150 @@ +. + * + * @category Sample + * @package StatusNet + * @author Brion Vibber + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class PostDebugPlugin extends Plugin +{ + /** + * Set to a directory to dump individual items instead of + * sending to the debug log + */ + public $dir=false; + + public function onArgsInitialize(&$args) + { + if (isset($_SERVER['REQUEST_METHOD']) && + $_SERVER['REQUEST_METHOD'] == 'POST') { + $this->doDebug(); + } + } + + public function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'PostDebug', + 'version' => STATUSNET_VERSION, + 'author' => 'Brion Vibber', + 'homepage' => 'http://status.net/wiki/Plugin:PostDebug', + 'rawdescription' => + _m('Debugging tool to record request details on POST.')); + return true; + } + + protected function doDebug() + { + $data = array('timestamp' => gmdate('r'), + 'remote_addr' => @$_SERVER['REMOTE_ADDR'], + 'url' => @$_SERVER['REQUEST_URI'], + 'have_session' => common_have_session(), + 'logged_in' => common_logged_in(), + 'is_real_login' => common_is_real_login(), + 'user' => common_logged_in() ? common_current_user()->nickname : null, + 'headers' => $this->getHttpHeaders(), + 'post_data' => $this->sanitizePostData($_POST)); + $this->saveDebug($data); + } + + protected function saveDebug($data) + { + $output = var_export($data, true); + if ($this->dir) { + $file = $this->dir . DIRECTORY_SEPARATOR . $this->logFileName(); + file_put_contents($file, $output); + } else { + common_log(LOG_DEBUG, "PostDebug: $output"); + } + } + + protected function logFileName() + { + $base = common_request_id(); + $base = preg_replace('/^(.+?) .*$/', '$1', $base); + $base = str_replace(':', '-', $base); + $base = rawurlencode($base); + return $base; + } + + protected function getHttpHeaders() + { + if (function_exists('getallheaders')) { + $headers = getallheaders(); + } else { + $headers = array(); + $prefix = 'HTTP_'; + $prefixLen = strlen($prefix); + foreach ($_SERVER as $key => $val) { + if (substr($key, 0, $prefixLen) == $prefix) { + $header = $this->normalizeHeader(substr($key, $prefixLen)); + $headers[$header] = $val; + } + } + } + foreach ($headers as $header => $val) { + if (strtolower($header) == 'cookie') { + $headers[$header] = $this->sanitizeCookies($val); + } + } + return $headers; + } + + protected function normalizeHeader($key) + { + return implode('-', + array_map('ucfirst', + explode("_", + strtolower($key)))); + } + + function sanitizeCookies($val) + { + $blacklist = array(session_name(), 'rememberme'); + foreach ($blacklist as $name) { + $val = preg_replace("/(^|;\s*)({$name}=)(.*?)(;|$)/", + "$1$2########$4", + $val); + } + return $val; + } + + function sanitizePostData($data) + { + $blacklist = array('password', 'confirm', 'token'); + foreach ($data as $key => $val) { + if (in_array($key, $blacklist)) { + $data[$key] = '########'; + } + } + return $data; + } + +} + From 8e07926a9c0736b5a3f408465dded92676ac1862 Mon Sep 17 00:00:00 2001 From: Craig Andrews Date: Wed, 17 Feb 2010 12:02:59 -0500 Subject: [PATCH 17/18] parse_url returns an associative array - not an object --- lib/htmloutputter.php | 2 +- plugins/Minify/MinifyPlugin.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php index 317f5ea612..4a88337bc5 100644 --- a/lib/htmloutputter.php +++ b/lib/htmloutputter.php @@ -428,7 +428,7 @@ class HTMLOutputter extends XMLOutputter { if(Event::handle('StartCssLinkElement', array($this,&$src,&$theme,&$media))) { $url = parse_url($src); - if( empty($url->scheme) && empty($url->host) && empty($url->query) && empty($url->fragment)) + if( empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment'])) { if(file_exists(Theme::file($src,$theme))){ $src = Theme::path($src, $theme); diff --git a/plugins/Minify/MinifyPlugin.php b/plugins/Minify/MinifyPlugin.php index b49b6a4bad..fe1883ded4 100644 --- a/plugins/Minify/MinifyPlugin.php +++ b/plugins/Minify/MinifyPlugin.php @@ -96,7 +96,7 @@ class MinifyPlugin extends Plugin && is_null(common_config('theme', 'path')) && is_null(common_config('theme', 'server')); $url = parse_url($src); - if( empty($url->scheme) && empty($url->host) && empty($url->query) && empty($url->fragment)) + if( empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment'])) { if(!isset($theme)) { $theme = common_config('site', 'theme'); From e8275aa60abd381de9e95c29fd00819c81d3db79 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 17 Feb 2010 10:12:37 -0800 Subject: [PATCH 18/18] Fix exception on bad plugin load --- lib/statusnet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/statusnet.php b/lib/statusnet.php index 9c7ede5a5d..257bd861da 100644 --- a/lib/statusnet.php +++ b/lib/statusnet.php @@ -63,7 +63,7 @@ class StatusNet } } if (!class_exists($pluginclass)) { - throw new ServerException(500, "Plugin $name not found."); + throw new ServerException("Plugin $name not found.", 500); } }