Merge branch 'testing' of git@gitorious.org:statusnet/mainline into testing
This commit is contained in:
		| @@ -66,10 +66,12 @@ class SupAction extends Action | ||||
|         $divider = common_sql_date(time() - $seconds); | ||||
|  | ||||
|         $notice->query('SELECT profile_id, max(id) AS max_id ' . | ||||
|                        'FROM notice ' . | ||||
|                        'FROM ( ' . | ||||
|                        'SELECT profile_id, id FROM notice ' . | ||||
|                         ((common_config('db','type') == 'pgsql') ? | ||||
|                        'WHERE extract(epoch from created) > (extract(epoch from now()) - ' . $seconds . ') ' : | ||||
|                        'WHERE created > "'.$divider.'" ' ) . | ||||
|                        ') AS latest ' . | ||||
|                        'GROUP BY profile_id'); | ||||
|  | ||||
|         $updates = array(); | ||||
|   | ||||
| @@ -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 | ||||
|      */ | ||||
|   | ||||
| @@ -882,28 +882,22 @@ class Profile extends Memcached_DataObject | ||||
|     { | ||||
|         $uri = null; | ||||
|  | ||||
|         // check for a local user first | ||||
|         $user = User::staticGet('id', $this->id); | ||||
|         // give plugins a chance to set the URI | ||||
|         if (Event::handle('StartGetProfileUri', array($this, &$uri))) { | ||||
|  | ||||
|         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))) { | ||||
|             // check for a local user first | ||||
|             $user = User::staticGet('id', $this->id); | ||||
|  | ||||
|             if (!empty($user)) { | ||||
|                 $uri = $user->uri; | ||||
|             } else { | ||||
|                 // return OMB profile if any | ||||
|                 $remote = Remote_profile::staticGet('id', $this->id); | ||||
|  | ||||
|                 if (!empty($remote)) { | ||||
|                     $uri = $remote->uri; | ||||
|                 } | ||||
|  | ||||
|                 Event::handle('EndGetProfileUri', array($this, &$uri)); | ||||
|             } | ||||
|             Event::handle('EndGetProfileUri', array($this, &$uri)); | ||||
|         } | ||||
|  | ||||
|         return $uri; | ||||
|   | ||||
| @@ -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: " . | ||||
|   | ||||
| @@ -29,11 +29,9 @@ require_once 'Auth/Yadis/Yadis.php'; | ||||
|  | ||||
| function omb_oauth_consumer() | ||||
| { | ||||
|     static $con = null; | ||||
|     if (is_null($con)) { | ||||
|         $con = new OAuthConsumer(common_root_url(), ''); | ||||
|     } | ||||
|     return $con; | ||||
|     // Don't try to make this static. Leads to issues in | ||||
|     // multi-site setups - Z | ||||
|     return new OAuthConsumer(common_root_url(), ''); | ||||
| } | ||||
|  | ||||
| function omb_oauth_server() | ||||
|   | ||||
| @@ -58,8 +58,6 @@ class OStatusPlugin extends Plugin | ||||
|         $m->connect('main/push/callback/:feed', | ||||
|                     array('action' => 'pushcallback'), | ||||
|                     array('feed' => '[0-9]+')); | ||||
|         $m->connect('settings/feedsub', | ||||
|                     array('action' => 'feedsubsettings')); | ||||
|  | ||||
|         // Salmon endpoint | ||||
|         $m->connect('main/salmon/user/:id', | ||||
| @@ -136,25 +134,6 @@ class OStatusPlugin extends Plugin | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Add the feed settings page to the Connect Settings menu | ||||
|      * | ||||
|      * @param Action &$action The calling page | ||||
|      * | ||||
|      * @return boolean hook return | ||||
|      */ | ||||
|     function onEndConnectSettingsNav(&$action) | ||||
|     { | ||||
|         $action_name = $action->trimmed('action'); | ||||
|  | ||||
|         $action->menuItem(common_local_url('feedsubsettings'), | ||||
|                           _m('Feeds'), | ||||
|                           _m('Feed subscription options'), | ||||
|                           $action_name === 'feedsubsettings'); | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Automatically load the actions and libraries used by the plugin | ||||
|      * | ||||
| @@ -215,45 +194,61 @@ class OStatusPlugin extends Plugin | ||||
|      * @fixme push webfinger lookup & sending to a background queue | ||||
|      * @fixme also detect short-form name for remote subscribees where not ambiguous | ||||
|      */ | ||||
|  | ||||
|     function onEndNoticeSave($notice) | ||||
|     { | ||||
|         $count = preg_match_all('/(\w+\.)*\w+@(\w+\.)*\w+(\w+\-\w+)*\.\w+/', $notice->content, $matches); | ||||
|         if ($count) { | ||||
|             foreach ($matches[0] as $webfinger) { | ||||
|         $mentioned = $notice->getReplies(); | ||||
|  | ||||
|                 // FIXME: look up locally first | ||||
|         foreach ($mentioned as $profile_id) { | ||||
|  | ||||
|                 // Check to see if we've got an actual webfinger | ||||
|                 $w = new Webfinger; | ||||
|             $oprofile = Ostatus_profile::staticGet('profile_id', $profile_id); | ||||
|  | ||||
|                 $endpoint_uri = ''; | ||||
|             if (!empty($oprofile) && !empty($oprofile->salmonuri)) { | ||||
|  | ||||
|                 $result = $w->lookup($webfinger); | ||||
|                 if (empty($result)) { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 foreach ($result->links as $link) { | ||||
|                     if ($link['rel'] == 'salmon') { | ||||
|                         $endpoint_uri = $link['href']; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (empty($endpoint_uri)) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 common_log(LOG_INFO, "Sending notice '{$notice->uri}' to remote profile '{$oprofile->uri}'."); | ||||
|  | ||||
|                 // FIXME: this needs to go out in a queue handler | ||||
|  | ||||
|                 $xml = '<?xml version="1.0" encoding="UTF-8" ?>'; | ||||
|                 $xml .= $notice->asAtomEntry(); | ||||
|                 $xml .= $notice->asAtomEntry(true, true); | ||||
|  | ||||
|                 $salmon = new Salmon(); | ||||
|                 $salmon->post($endpoint_uri, $xml); | ||||
|                 $salmon->post($oprofile->salmonuri, $xml); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      */ | ||||
|  | ||||
|     function onEndFindMentions($sender, $text, &$mentions) | ||||
|     { | ||||
|         preg_match_all('/(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)/', | ||||
|                        $text, | ||||
|                        $wmatches, | ||||
|                        PREG_OFFSET_CAPTURE); | ||||
|  | ||||
|         foreach ($wmatches[1] as $wmatch) { | ||||
|  | ||||
|             $webfinger = $wmatch[0]; | ||||
|  | ||||
|             $oprofile = Ostatus_profile::ensureWebfinger($webfinger); | ||||
|  | ||||
|             if (!empty($oprofile)) { | ||||
|  | ||||
|                 $profile = $oprofile->localProfile(); | ||||
|  | ||||
|                 $mentions[] = array('mentioned' => array($profile), | ||||
|                                     'text' => $wmatch[0], | ||||
|                                     'position' => $wmatch[1], | ||||
|                                     'url' => $profile->profileurl); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Notify remote server and garbage collect unused feeds on unsubscribe. | ||||
|      * @fixme send these operations to background queues | ||||
| @@ -312,6 +307,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; | ||||
| @@ -491,4 +487,14 @@ class OStatusPlugin extends Plugin | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     function onStartGetProfileUri($profile, &$uri) | ||||
|     { | ||||
|         $oprofile = Ostatus_profile::staticGet('profile_id', $profile->id); | ||||
|         if (!empty($oprofile)) { | ||||
|             $uri = $oprofile->uri; | ||||
|             return false; | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,230 +0,0 @@ | ||||
| <?php | ||||
| /* | ||||
|  * StatusNet - the distributed open-source microblogging tool | ||||
|  * Copyright (C) 2009, StatusNet, Inc. | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * @package FeedSubPlugin | ||||
|  * @maintainer Brion Vibber <brion@status.net> | ||||
|  */ | ||||
|  | ||||
| if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } | ||||
|  | ||||
| class FeedSubSettingsAction extends ConnectSettingsAction | ||||
| { | ||||
|     protected $profile_uri; | ||||
|     protected $preview; | ||||
|     protected $munger; | ||||
|  | ||||
|     /** | ||||
|      * Title of the page | ||||
|      * | ||||
|      * @return string Title of the page | ||||
|      */ | ||||
|  | ||||
|     function title() | ||||
|     { | ||||
|         return _m('Feed subscriptions'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Instructions for use | ||||
|      * | ||||
|      * @return instructions for use | ||||
|      */ | ||||
|  | ||||
|     function getInstructions() | ||||
|     { | ||||
|         return _m('You can subscribe to feeds from other sites; ' . | ||||
|                   'updates will appear in your personal timeline.'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Content area of the page | ||||
|      * | ||||
|      * Shows a form for associating a Twitter account with this | ||||
|      * StatusNet account. Also lets the user set preferences. | ||||
|      * | ||||
|      * @return void | ||||
|      */ | ||||
|  | ||||
|     function showContent() | ||||
|     { | ||||
|         $user = common_current_user(); | ||||
|  | ||||
|         $profile = $user->getProfile(); | ||||
|  | ||||
|         $this->elementStart('form', array('method' => 'post', | ||||
|                                           'id' => 'form_settings_feedsub', | ||||
|                                           'class' => 'form_settings', | ||||
|                                           'action' => | ||||
|                                           common_local_url('feedsubsettings'))); | ||||
|  | ||||
|         $this->hidden('token', common_session_token()); | ||||
|  | ||||
|         $this->elementStart('fieldset', array('id' => 'settings_feeds')); | ||||
|  | ||||
|         $this->elementStart('ul', 'form_data'); | ||||
|         $this->elementStart('li', array('id' => 'settings_twitter_login_button')); | ||||
|         $this->input('profile_uri', | ||||
|                      _m('Feed URL'), | ||||
|                      $this->profile_uri, | ||||
|                      _m('Enter the profile URL of a PubSubHubbub-enabled feed')); | ||||
|         $this->elementEnd('li'); | ||||
|         $this->elementEnd('ul'); | ||||
|  | ||||
|         if ($this->preview) { | ||||
|             $this->submit('subscribe', _m('Subscribe')); | ||||
|         } else { | ||||
|             $this->submit('validate', _m('Continue')); | ||||
|         } | ||||
|  | ||||
|         $this->elementEnd('fieldset'); | ||||
|  | ||||
|         $this->elementEnd('form'); | ||||
|  | ||||
|         if ($this->preview) { | ||||
|             $this->previewFeed(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Handle posts to this form | ||||
|      * | ||||
|      * Based on the button that was pressed, muxes out to other functions | ||||
|      * to do the actual task requested. | ||||
|      * | ||||
|      * All sub-functions reload the form with a message -- success or failure. | ||||
|      * | ||||
|      * @return void | ||||
|      */ | ||||
|  | ||||
|     function handlePost() | ||||
|     { | ||||
|         // CSRF protection | ||||
|         $token = $this->trimmed('token'); | ||||
|         if (!$token || $token != common_session_token()) { | ||||
|             $this->showForm(_('There was a problem with your session token. '. | ||||
|                               'Try again, please.')); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if ($this->arg('validate')) { | ||||
|             $this->validateAndPreview(); | ||||
|         } else if ($this->arg('subscribe')) { | ||||
|             $this->saveFeed(); | ||||
|         } else { | ||||
|             $this->showForm(_('Unexpected form submission.')); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set up and add a feed | ||||
|      * | ||||
|      * @return boolean true if feed successfully read | ||||
|      * Sends you back to input form if not. | ||||
|      */ | ||||
|     function validateFeed() | ||||
|     { | ||||
|         $profile_uri = trim($this->arg('profile_uri')); | ||||
|          | ||||
|         if ($profile_uri == '') { | ||||
|             $this->showForm(_m('Empty remote profile URL!')); | ||||
|             return; | ||||
|         } | ||||
|         $this->profile_uri = $profile_uri; | ||||
|          | ||||
|         // @fixme validate, normalize bla bla | ||||
|         try { | ||||
|             $oprofile = Ostatus_profile::ensureProfile($this->profile_uri); | ||||
|             $this->oprofile = $oprofile; | ||||
|             return true; | ||||
|         } catch (FeedSubBadURLException $e) { | ||||
|             $err = _m('Invalid URL or could not reach server.'); | ||||
|         } catch (FeedSubBadResponseException $e) { | ||||
|             $err = _m('Cannot read feed; server returned error.'); | ||||
|         } catch (FeedSubEmptyException $e) { | ||||
|             $err = _m('Cannot read feed; server returned an empty page.'); | ||||
|         } catch (FeedSubBadHTMLException $e) { | ||||
|             $err = _m('Bad HTML, could not find feed link.'); | ||||
|         } catch (FeedSubNoFeedException $e) { | ||||
|             $err = _m('Could not find a feed linked from this URL.'); | ||||
|         } catch (FeedSubUnrecognizedTypeException $e) { | ||||
|             $err = _m('Not a recognized feed type.'); | ||||
|         } catch (FeedSubException $e) { | ||||
|             // Any new ones we forgot about | ||||
|             $err = sprintf(_m('Bad feed URL: %s %s'), get_class($e), $e->getMessage()); | ||||
|         } | ||||
|  | ||||
|         $this->showForm($err); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     function saveFeed() | ||||
|     { | ||||
|         if ($this->validateFeed()) { | ||||
|             $this->preview = true; | ||||
|  | ||||
|             // And subscribe the current user to the local profile | ||||
|             $user = common_current_user(); | ||||
|  | ||||
|             if (!$this->oprofile->subscribe()) { | ||||
|                 $this->showForm(_m("Failed to set up server-to-server subscription.")); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if ($this->oprofile->isGroup()) { | ||||
|                 $group = $this->oprofile->localGroup(); | ||||
|                 if ($user->isMember($group)) { | ||||
|                     $this->showForm(_m('Already a member!')); | ||||
|                 } elseif (Group_member::join($this->profile->group_id, $user->id)) { | ||||
|                     $this->showForm(_m('Joined remote group!')); | ||||
|                 } else { | ||||
|                     $this->showForm(_m('Remote group join failed!')); | ||||
|                 } | ||||
|             } else { | ||||
|                 $local = $this->oprofile->localProfile(); | ||||
|                 if ($user->isSubscribed($local)) { | ||||
|                     $this->showForm(_m('Already subscribed!')); | ||||
|                 } elseif ($this->oprofile->subscribeLocalToRemote($user)) { | ||||
|                     $this->showForm(_m('Remote user subscribed!')); | ||||
|                 } else { | ||||
|                     $this->showForm(_m('Remote subscription failed!')); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function validateAndPreview() | ||||
|     { | ||||
|         if ($this->validateFeed()) { | ||||
|             $this->preview = true; | ||||
|             $this->showForm(_m('Previewing feed:')); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function previewFeed() | ||||
|     { | ||||
|         $this->text('Profile preview should go here'); | ||||
|     } | ||||
|  | ||||
|     function showScripts() | ||||
|     { | ||||
|         parent::showScripts(); | ||||
|         $this->autofocus('feedurl'); | ||||
|     } | ||||
| } | ||||
| @@ -119,7 +119,7 @@ class OStatusInitAction extends Action | ||||
|             } else { | ||||
|                 $this->connectProfile($this->acct); | ||||
|             } | ||||
|         } elseif (strpos('@', $this->acct) !== false) { | ||||
|         } elseif (strpos($this->acct, '@') !== false) { | ||||
|             $this->connectWebfinger($this->acct); | ||||
|         } | ||||
|     } | ||||
| @@ -139,7 +139,7 @@ class OStatusInitAction extends Action | ||||
|                 $user = User::staticGet('nickname', $this->nickname); | ||||
|                 $target_profile = common_local_url('userbyid', array('id' => $user->id)); | ||||
|  | ||||
|                 $url = $w->applyTemplate($link['template'], $feed_url); | ||||
|                 $url = $w->applyTemplate($link['template'], $target_profile); | ||||
|  | ||||
|                 common_redirect($url, 303); | ||||
|             } | ||||
|   | ||||
| @@ -72,7 +72,7 @@ class PushCallbackAction extends Action | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * Handler for GET verification requests from the hub | ||||
|      * Handler for GET verification requests from the hub. | ||||
|      */ | ||||
|     function handleGet() | ||||
|     { | ||||
| @@ -81,31 +81,37 @@ class PushCallbackAction extends Action | ||||
|         $challenge = $this->arg('hub_challenge'); | ||||
|         $lease_seconds = $this->arg('hub_lease_seconds'); | ||||
|         $verify_token = $this->arg('hub_verify_token'); | ||||
|          | ||||
|  | ||||
|         if ($mode != 'subscribe' && $mode != 'unsubscribe') { | ||||
|             common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with mode \"$mode\""); | ||||
|             throw new ServerException("Bogus hub callback: bad mode", 404); | ||||
|             throw new ClientException("Bad hub.mode $mode", 404); | ||||
|         } | ||||
|          | ||||
|  | ||||
|         $feedsub = FeedSub::staticGet('uri', $topic); | ||||
|         if (!$feedsub) { | ||||
|             common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic"); | ||||
|             throw new ServerException("Bogus hub callback: unknown feed", 404); | ||||
|             throw new ClientException("Bad hub.topic feed $topic", 404); | ||||
|         } | ||||
|  | ||||
|         if ($feedsub->verify_token !== $verify_token) { | ||||
|             common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic"); | ||||
|             throw new ServerException("Bogus hub callback: bad token", 404); | ||||
|             throw new ClientException("Bad hub.verify_token $token for $topic", 404); | ||||
|         } | ||||
|  | ||||
|         if ($mode != $feedsub->sub_state) { | ||||
|             common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad mode \"$mode\" for feed $topic in state \"{$feedsub->sub_state}\""); | ||||
|             throw new ServerException("Bogus hub callback: mode doesn't match subscription state.", 404); | ||||
|         } | ||||
|  | ||||
|         // OK! | ||||
|         if ($mode == 'subscribe') { | ||||
|             common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); | ||||
|             // We may get re-sub requests legitimately. | ||||
|             if ($feedsub->sub_state != 'subscribe' && $feedsub->sub_state != 'active') { | ||||
|                 throw new ClientException("Unexpected subscribe request for $topic.", 404); | ||||
|             } | ||||
|         } else { | ||||
|             if ($feedsub->sub_state != 'unsubscribe') { | ||||
|                 throw new ClientException("Unexpected unsubscribe request for $topic.", 404); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if ($mode == 'subscribe') { | ||||
|             if ($feedsub->sub_state == 'active') { | ||||
|                 common_log(LOG_INFO, __METHOD__ . ': sub update confirmed'); | ||||
|             } else { | ||||
|                 common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); | ||||
|             } | ||||
|             $feedsub->confirmSubscribe($lease_seconds); | ||||
|         } else { | ||||
|             common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic"); | ||||
|   | ||||
| @@ -59,102 +59,121 @@ class PushHubAction extends Action | ||||
|         $mode = $this->trimmed('hub.mode'); | ||||
|         switch ($mode) { | ||||
|         case "subscribe": | ||||
|             $this->subscribe(); | ||||
|             break; | ||||
|         case "unsubscribe": | ||||
|             $this->unsubscribe(); | ||||
|             $this->subunsub($mode); | ||||
|             break; | ||||
|         case "publish": | ||||
|             throw new ServerException("Publishing outside feeds not supported.", 400); | ||||
|             throw new ClientException("Publishing outside feeds not supported.", 400); | ||||
|         default: | ||||
|             throw new ServerException("Unrecognized mode '$mode'.", 400); | ||||
|             throw new ClientException("Unrecognized mode '$mode'.", 400); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Process a PuSH feed subscription request. | ||||
|      * Process a request for a new or modified PuSH feed subscription. | ||||
|      * If asynchronous verification is requested, updates won't be saved immediately. | ||||
|      * | ||||
|      * HTTP return codes: | ||||
|      *   202 Accepted - request saved and awaiting verification | ||||
|      *   204 No Content - already subscribed | ||||
|      *   403 Forbidden - rejecting this (not specifically spec'd) | ||||
|      *   400 Bad Request - rejecting this (not specifically spec'd) | ||||
|      */ | ||||
|     function subscribe() | ||||
|     function subunsub($mode) | ||||
|     { | ||||
|         $feed = $this->argUrl('hub.topic'); | ||||
|         $callback = $this->argUrl('hub.callback'); | ||||
|  | ||||
|         $topic = $this->argUrl('hub.topic'); | ||||
|         if (!$this->recognizedFeed($topic)) { | ||||
|             throw new ClientException("Unsupported hub.topic $topic; this hub only serves local user and group Atom feeds."); | ||||
|         } | ||||
|  | ||||
|         $verify = $this->arg('hub.verify'); // @fixme may be multiple | ||||
|         if ($verify != 'sync' && $verify != 'async') { | ||||
|             throw new ClientException("Invalid hub.verify $verify; must be sync or async."); | ||||
|         } | ||||
|  | ||||
|         $lease = $this->arg('hub.lease_seconds', null); | ||||
|         if ($mode == 'subscribe' && $lease != '' && !preg_match('/^\d+$/', $lease)) { | ||||
|             throw new ClientException("Invalid hub.lease $lease; must be empty or positive integer."); | ||||
|         } | ||||
|  | ||||
|         $token = $this->arg('hub.verify_token', null); | ||||
|  | ||||
|         common_log(LOG_DEBUG, __METHOD__ . ": checking sub'd to $feed $callback"); | ||||
|         if ($this->getSub($feed, $callback)) { | ||||
|             // Already subscribed; return 204 per spec. | ||||
|         $secret = $this->arg('hub.secret', null); | ||||
|         if ($secret != '' && strlen($secret) >= 200) { | ||||
|             throw new ClientException("Invalid hub.secret $secret; must be under 200 bytes."); | ||||
|         } | ||||
|  | ||||
|         $sub = HubSub::staticGet($sub->topic, $sub->callback); | ||||
|         if (!$sub) { | ||||
|             // Creating a new one! | ||||
|             $sub = new HubSub(); | ||||
|             $sub->topic = $topic; | ||||
|             $sub->callback = $callback; | ||||
|         } | ||||
|         if ($mode == 'subscribe') { | ||||
|             if ($secret) { | ||||
|                 $sub->secret = $secret; | ||||
|             } | ||||
|             if ($lease) { | ||||
|                 $sub->setLease(intval($lease)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (!common_config('queue', 'enabled')) { | ||||
|             // Won't be able to background it. | ||||
|             $verify = 'sync'; | ||||
|         } | ||||
|         if ($verify == 'async') { | ||||
|             $sub->scheduleVerify($mode, $token); | ||||
|             header('HTTP/1.1 202 Accepted'); | ||||
|         } else { | ||||
|             $sub->verify($mode, $token); | ||||
|             header('HTTP/1.1 204 No Content'); | ||||
|             common_log(LOG_DEBUG, __METHOD__ . ': already subscribed'); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         common_log(LOG_DEBUG, __METHOD__ . ': setting up'); | ||||
|         $sub = new HubSub(); | ||||
|         $sub->topic = $feed; | ||||
|         $sub->callback = $callback; | ||||
|         $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 | ||||
|         // @fixme check the verification mode, might want a return immediately? | ||||
|  | ||||
|         common_log(LOG_DEBUG, __METHOD__ . ': inserting'); | ||||
|         $ok = $sub->insert(); | ||||
|          | ||||
|         if (!$ok) { | ||||
|             throw new ServerException("Failed to save subscription record", 500); | ||||
|         } | ||||
|  | ||||
|         // @fixme check errors ;) | ||||
|  | ||||
|         $data = array('sub' => $sub, 'mode' => 'subscribe', 'token' => $token); | ||||
|         $qm = QueueManager::get(); | ||||
|         $qm->enqueue($data, 'hubverify'); | ||||
|          | ||||
|         header('HTTP/1.1 202 Accepted'); | ||||
|         common_log(LOG_DEBUG, __METHOD__ . ': done'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Process a PuSH feed unsubscription request. | ||||
|      * Check whether the given URL represents one of our canonical | ||||
|      * user or group Atom feeds. | ||||
|      * | ||||
|      * HTTP return codes: | ||||
|      *   202 Accepted - request saved and awaiting verification | ||||
|      *   204 No Content - already subscribed | ||||
|      *   400 Bad Request - invalid params or rejected feed | ||||
|      * | ||||
|      * @fixme background this | ||||
|      * @param string $feed URL | ||||
|      * @return boolean true if it matches | ||||
|      */ | ||||
|     function unsubscribe() | ||||
|     function recognizedFeed($feed) | ||||
|     { | ||||
|         $feed = $this->argUrl('hub.topic'); | ||||
|         $callback = $this->argUrl('hub.callback'); | ||||
|         $sub = $this->getSub($feed, $callback); | ||||
|          | ||||
|         if ($sub) { | ||||
|             $token = $this->arg('hub.verify_token', null); | ||||
|             if ($sub->verify('unsubscribe', $token)) { | ||||
|                 $sub->delete(); | ||||
|                 common_log(LOG_INFO, "PuSH unsubscribed $feed for $callback"); | ||||
|             } else { | ||||
|                 throw new ServerException("Failed PuSH unsubscription: verification failed! $feed for $callback"); | ||||
|         $matches = array(); | ||||
|         if (preg_match('!/(\d+)\.atom$!', $feed, $matches)) { | ||||
|             $id = $matches[1]; | ||||
|             $params = array('id' => $id, 'format' => 'atom'); | ||||
|             $userFeed = common_local_url('ApiTimelineUser', $params); | ||||
|             $groupFeed = common_local_url('ApiTimelineGroup', $params); | ||||
|  | ||||
|             if ($feed == $userFeed) { | ||||
|                 $user = User::staticGet('id', $id); | ||||
|                 if (!$user) { | ||||
|                     throw new ClientException("Invalid hub.topic $feed; user doesn't exist."); | ||||
|                 } else { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             throw new ServerException("Failed PuSH unsubscription: not subscribed! $feed for $callback"); | ||||
|             if ($feed == $groupFeed) { | ||||
|                 $user = User_group::staticGet('id', $id); | ||||
|                 if (!$user) { | ||||
|                     throw new ClientException("Invalid hub.topic $feed; group doesn't exist."); | ||||
|                 } else { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|             common_log(LOG_DEBUG, "Not a user or group feed? $feed $userFeed $groupFeed"); | ||||
|         } | ||||
|         common_log(LOG_DEBUG, "LOST $feed"); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Grab and validate a URL from POST parameters. | ||||
|      * @throws ServerException for malformed or non-http/https URLs | ||||
|      * @throws ClientException for malformed or non-http/https URLs | ||||
|      */ | ||||
|     protected function argUrl($arg) | ||||
|     { | ||||
| @@ -164,7 +183,7 @@ class PushHubAction extends Action | ||||
|         if (Validate::uri($url, $params)) { | ||||
|             return $url; | ||||
|         } else { | ||||
|             throw new ServerException("Invalid URL passed for $arg: '$url'", 400); | ||||
|             throw new ClientException("Invalid URL passed for $arg: '$url'"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -55,6 +55,8 @@ class UsersalmonAction extends SalmonAction | ||||
|      */ | ||||
|     function handlePost() | ||||
|     { | ||||
|         common_log(LOG_INFO, "Received post of '{$this->act->object->id}' from '{$this->act->actor->id}'"); | ||||
|  | ||||
|         switch ($this->act->object->type) { | ||||
|         case ActivityObject::ARTICLE: | ||||
|         case ActivityObject::BLOGENTRY: | ||||
| @@ -80,13 +82,21 @@ class UsersalmonAction extends SalmonAction | ||||
|                 throw new ClientException("In reply to a notice not by this user"); | ||||
|             } | ||||
|         } else if (!empty($context->attention)) { | ||||
|             if (!in_array($context->attention, $this->user->uri)) { | ||||
|             if (!in_array($this->user->uri, $context->attention)) { | ||||
|                 common_log(LOG_ERR, "{$this->user->uri} not in attention list (".implode(',', $context->attention).")"); | ||||
|                 throw new ClientException("To the attention of user(s) not including this one!"); | ||||
|             } | ||||
|         } else { | ||||
|             throw new ClientException("Not to anyone in reply to anything!"); | ||||
|         } | ||||
|  | ||||
|         $existing = Notice::staticGet('uri', $this->act->object->id); | ||||
|  | ||||
|         if (!empty($existing)) { | ||||
|             common_log(LOG_ERR, "Not saving notice '{$existing->uri}'; already exists."); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $this->saveNotice(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -37,7 +37,7 @@ class WebfingerAction extends Action | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|          | ||||
|  | ||||
|     function handle() | ||||
|     { | ||||
|         $acct = Webfinger::normalize($this->uri); | ||||
| @@ -55,16 +55,22 @@ class WebfingerAction extends Action | ||||
|  | ||||
|         $xrd->subject = $this->uri; | ||||
|         $xrd->alias[] = common_profile_url($nick); | ||||
|         $xrd->links[] = array('rel' => 'http://webfinger.net/rel/profile-page', | ||||
|         $xrd->links[] = array('rel' => Webfinger::PROFILEPAGE, | ||||
|                               'type' => 'text/html', | ||||
|                               'href' => common_profile_url($nick)); | ||||
|  | ||||
|         $xrd->links[] = array('rel' => Webfinger::UPDATESFROM, | ||||
|                               'href' => common_local_url('ApiTimelineUser', | ||||
|                                                          array('id' => $this->user->id, | ||||
|                                                                'format' => 'atom')), | ||||
|                               'type' => 'application/atom+xml'); | ||||
|  | ||||
|         $salmon_url = common_local_url('salmon', | ||||
|                                        array('id' => $this->user->id)); | ||||
|  | ||||
|         $xrd->links[] = array('rel' => 'salmon', | ||||
|                               'href' => $salmon_url); | ||||
|          | ||||
|  | ||||
|         // TODO - finalize where the redirect should go on the publisher | ||||
|         $url = common_local_url('ostatussub') . '?profile={uri}'; | ||||
|         $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe', | ||||
|   | ||||
| @@ -291,10 +291,9 @@ class FeedSub extends Memcached_DataObject | ||||
|             $headers = array('Content-Type: application/x-www-form-urlencoded'); | ||||
|             $post = array('hub.mode' => $mode, | ||||
|                           'hub.callback' => $callback, | ||||
|                           'hub.verify' => 'async', | ||||
|                           'hub.verify' => 'sync', | ||||
|                           'hub.verify_token' => $this->verify_token, | ||||
|                           'hub.secret' => $this->secret, | ||||
|                           //'hub.lease_seconds' => 0, | ||||
|                           'hub.topic' => $this->uri); | ||||
|             $client = new HTTPClient(); | ||||
|             $response = $client->post($this->huburi, $headers, $post); | ||||
| @@ -317,8 +316,8 @@ class FeedSub extends Memcached_DataObject | ||||
|             common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->uri"); | ||||
|  | ||||
|             $orig = clone($this); | ||||
|             $this->verify_token = null; | ||||
|             $this->sub_state = null; | ||||
|             $this->verify_token = ''; | ||||
|             $this->sub_state = 'inactive'; | ||||
|             $this->update($orig); | ||||
|             unset($orig); | ||||
|  | ||||
| @@ -343,7 +342,7 @@ class FeedSub extends Memcached_DataObject | ||||
|         } else { | ||||
|             $this->sub_end = null; | ||||
|         } | ||||
|         $this->lastupdate = common_sql_now(); | ||||
|         $this->modified = common_sql_now(); | ||||
|  | ||||
|         return $this->update($original); | ||||
|     } | ||||
| @@ -362,7 +361,7 @@ class FeedSub extends Memcached_DataObject | ||||
|         $this->sub_state = ''; | ||||
|         $this->sub_start = ''; | ||||
|         $this->sub_end = ''; | ||||
|         $this->lastupdate = common_sql_now(); | ||||
|         $this->modified = common_sql_now(); | ||||
|  | ||||
|         return $this->update($original); | ||||
|     } | ||||
|   | ||||
| @@ -30,11 +30,11 @@ class HubSub extends Memcached_DataObject | ||||
|     public $topic; | ||||
|     public $callback; | ||||
|     public $secret; | ||||
|     public $challenge; | ||||
|     public $lease; | ||||
|     public $sub_start; | ||||
|     public $sub_end; | ||||
|     public $created; | ||||
|     public $modified; | ||||
|  | ||||
|     public /*static*/ function staticGet($topic, $callback) | ||||
|     { | ||||
| @@ -61,11 +61,11 @@ class HubSub extends Memcached_DataObject | ||||
|                      'topic' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, | ||||
|                      'callback' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, | ||||
|                      'secret' => DB_DATAOBJECT_STR, | ||||
|                      'challenge' => DB_DATAOBJECT_STR, | ||||
|                      'lease' =>  DB_DATAOBJECT_INT, | ||||
|                      'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, | ||||
|                      'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, | ||||
|                      'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); | ||||
|                      'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, | ||||
|                      'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); | ||||
|     } | ||||
|  | ||||
|     static function schemaDef() | ||||
| @@ -82,8 +82,6 @@ class HubSub extends Memcached_DataObject | ||||
|                                    255, false), | ||||
|                      new ColumnDef('secret', 'text', | ||||
|                                    null, true), | ||||
|                      new ColumnDef('challenge', 'varchar', | ||||
|                                    32, true), | ||||
|                      new ColumnDef('lease', 'int', | ||||
|                                    null, true), | ||||
|                      new ColumnDef('sub_start', 'datetime', | ||||
| @@ -91,6 +89,8 @@ class HubSub extends Memcached_DataObject | ||||
|                      new ColumnDef('sub_end', 'datetime', | ||||
|                                    null, true), | ||||
|                      new ColumnDef('created', 'datetime', | ||||
|                                    null, false), | ||||
|                      new ColumnDef('modified', 'datetime', | ||||
|                                    null, false)); | ||||
|     } | ||||
|  | ||||
| @@ -148,84 +148,105 @@ class HubSub extends Memcached_DataObject | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Send a verification ping to subscriber | ||||
|      * Schedule a future verification ping to the subscriber. | ||||
|      * If queues are disabled, will be immediate. | ||||
|      * | ||||
|      * @param string $mode 'subscribe' or 'unsubscribe' | ||||
|      * @param string $token hub.verify_token value, if provided by client | ||||
|      */ | ||||
|     function scheduleVerify($mode, $token=null, $retries=null) | ||||
|     { | ||||
|         if ($retries === null) { | ||||
|             $retries = intval(common_config('ostatus', 'hub_retries')); | ||||
|         } | ||||
|         $data = array('sub' => clone($this), | ||||
|                       'mode' => $mode, | ||||
|                       'token' => $token, | ||||
|                       'retries' => $retries); | ||||
|         $qm = QueueManager::get(); | ||||
|         $qm->enqueue($data, 'hubverify'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Send a verification ping to subscriber, and if confirmed apply the changes. | ||||
|      * This may create, update, or delete the database record. | ||||
|      * | ||||
|      * @param string $mode 'subscribe' or 'unsubscribe' | ||||
|      * @param string $token hub.verify_token value, if provided by client | ||||
|      * @throws ClientException on failure | ||||
|      */ | ||||
|     function verify($mode, $token=null) | ||||
|     { | ||||
|         assert($mode == 'subscribe' || $mode == 'unsubscribe'); | ||||
|  | ||||
|         // Is this needed? data object fun... | ||||
|         $clone = clone($this); | ||||
|         $clone->challenge = common_good_rand(16); | ||||
|         $clone->update($this); | ||||
|         $this->challenge = $clone->challenge; | ||||
|         unset($clone); | ||||
|  | ||||
|         $challenge = common_good_rand(32); | ||||
|         $params = array('hub.mode' => $mode, | ||||
|                         'hub.topic' => $this->topic, | ||||
|                         'hub.challenge' => $this->challenge); | ||||
|                         'hub.challenge' => $challenge); | ||||
|         if ($mode == 'subscribe') { | ||||
|             $params['hub.lease_seconds'] = $this->lease; | ||||
|         } | ||||
|         if ($token !== null) { | ||||
|             $params['hub.verify_token'] = $token; | ||||
|         } | ||||
|         $url = $this->callback . '?' . http_build_query($params, '', '&'); // @fixme ugly urls | ||||
|  | ||||
|         try { | ||||
|             $request = new HTTPClient(); | ||||
|             $response = $request->get($url); | ||||
|             $status = $response->getStatus(); | ||||
|  | ||||
|             if ($status >= 200 && $status < 300) { | ||||
|                 $fail = false; | ||||
|             } else { | ||||
|                 // @fixme how can we schedule a second attempt? | ||||
|                 // Or should we? | ||||
|                 $fail = "Returned HTTP $status"; | ||||
|             } | ||||
|         } catch (Exception $e) { | ||||
|             $fail = $e->getMessage(); | ||||
|         } | ||||
|         if ($fail) { | ||||
|             // @fixme how can we schedule a second attempt? | ||||
|             // or save a fail count? | ||||
|             // Or should we? | ||||
|             common_log(LOG_ERR, "Failed to verify $mode for $this->topic at $this->callback: $fail"); | ||||
|             return false; | ||||
|         // Any existing query string parameters must be preserved | ||||
|         $url = $this->callback; | ||||
|         if (strpos('?', $url) !== false) { | ||||
|             $url .= '&'; | ||||
|         } else { | ||||
|             if ($mode == 'subscribe') { | ||||
|                 // Establish or renew the subscription! | ||||
|                 // This seems unnecessary... dataobject fun! | ||||
|                 $clone = clone($this); | ||||
|                 $clone->challenge = null; | ||||
|                 $clone->setLease($this->lease); | ||||
|                 $clone->update($this); | ||||
|                 unset($clone); | ||||
|             $url .= '?'; | ||||
|         } | ||||
|         $url .= http_build_query($params, '', '&'); | ||||
|  | ||||
|                 $this->challenge = null; | ||||
|                 $this->setLease($this->lease); | ||||
|                 common_log(LOG_ERR, "Verified $mode of $this->callback:$this->topic for $this->lease seconds"); | ||||
|             } else if ($mode == 'unsubscribe') { | ||||
|                 common_log(LOG_ERR, "Verified $mode of $this->callback:$this->topic"); | ||||
|                 $this->delete(); | ||||
|         $request = new HTTPClient(); | ||||
|         $response = $request->get($url); | ||||
|         $status = $response->getStatus(); | ||||
|  | ||||
|         if ($status >= 200 && $status < 300) { | ||||
|             common_log(LOG_INFO, "Verified $mode of $this->callback:$this->topic"); | ||||
|         } else { | ||||
|             throw new ClientException("Hub subscriber verification returned HTTP $status"); | ||||
|         } | ||||
|  | ||||
|         $old = HubSub::staticGet($this->topic, $this->callback); | ||||
|         if ($mode == 'subscribe') { | ||||
|             if ($old) { | ||||
|                 $this->update($old); | ||||
|             } else { | ||||
|                 $ok = $this->insert(); | ||||
|             } | ||||
|         } else if ($mode == 'unsubscribe') { | ||||
|             if ($old) { | ||||
|                 $old->delete(); | ||||
|             } else { | ||||
|                 // That's ok, we're already unsubscribed. | ||||
|             } | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Insert wrapper; transparently set the hash key from topic and callback columns. | ||||
|      * @return boolean success | ||||
|      * @return mixed success | ||||
|      */ | ||||
|     function insert() | ||||
|     { | ||||
|         $this->hashkey = self::hashkey($this->topic, $this->callback); | ||||
|         $this->created = common_sql_now(); | ||||
|         $this->modified = common_sql_now(); | ||||
|         return parent::insert(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update wrapper; transparently update modified column. | ||||
|      * @return boolean success | ||||
|      */ | ||||
|     function update($old=null) | ||||
|     { | ||||
|         $this->modified = common_sql_now(); | ||||
|         return parent::update($old); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Schedule delivery of a 'fat ping' to the subscriber's callback | ||||
|      * endpoint. If queues are disabled, this will run immediately. | ||||
|   | ||||
| @@ -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'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -522,7 +524,7 @@ class Ostatus_profile extends Memcached_DataObject | ||||
|      * @return Ostatus_profile | ||||
|      * @throws FeedSubException | ||||
|      */ | ||||
|     public static function ensureProfile($profile_uri) | ||||
|     public static function ensureProfile($profile_uri, $hints=array()) | ||||
|     { | ||||
|         // Get the canonical feed URI and check it | ||||
|         $discover = new FeedDiscovery(); | ||||
| @@ -545,7 +547,7 @@ class Ostatus_profile extends Memcached_DataObject | ||||
|  | ||||
|         if (!empty($subject)) { | ||||
|             $subjObject = new ActivityObject($subject); | ||||
|             return self::ensureActivityObjectProfile($subjObject, $feeduri, $salmonuri); | ||||
|             return self::ensureActivityObjectProfile($subjObject, $feeduri, $salmonuri, $hints); | ||||
|         } | ||||
|  | ||||
|         // Otherwise, try the feed author | ||||
| @@ -554,7 +556,7 @@ class Ostatus_profile extends Memcached_DataObject | ||||
|  | ||||
|         if (!empty($author)) { | ||||
|             $authorObject = new ActivityObject($author); | ||||
|             return self::ensureActivityObjectProfile($authorObject, $feeduri, $salmonuri); | ||||
|             return self::ensureActivityObjectProfile($authorObject, $feeduri, $salmonuri, $hints); | ||||
|         } | ||||
|  | ||||
|         // Sheesh. Not a very nice feed! Let's try fingerpoken in the | ||||
| @@ -570,7 +572,7 @@ class Ostatus_profile extends Memcached_DataObject | ||||
|  | ||||
|             if (!empty($actor)) { | ||||
|                 $actorObject = new ActivityObject($actor); | ||||
|                 return self::ensureActivityObjectProfile($actorObject, $feeduri, $salmonuri); | ||||
|                 return self::ensureActivityObjectProfile($actorObject, $feeduri, $salmonuri, $hints); | ||||
|  | ||||
|             } | ||||
|  | ||||
| @@ -578,7 +580,7 @@ class Ostatus_profile extends Memcached_DataObject | ||||
|  | ||||
|             if (!empty($author)) { | ||||
|                 $authorObject = new ActivityObject($author); | ||||
|                 return self::ensureActivityObjectProfile($authorObject, $feeduri, $salmonuri); | ||||
|                 return self::ensureActivityObjectProfile($authorObject, $feeduri, $salmonuri, $hints); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -688,11 +690,11 @@ class Ostatus_profile extends Memcached_DataObject | ||||
|         return self::ensureActivityObjectProfile($activity->actor, $feeduri, $salmonuri); | ||||
|     } | ||||
|  | ||||
|     public static function ensureActivityObjectProfile($object, $feeduri=null, $salmonuri=null) | ||||
|     public static function ensureActivityObjectProfile($object, $feeduri=null, $salmonuri=null, $hints=array()) | ||||
|     { | ||||
|         $profile = self::getActivityObjectProfile($object); | ||||
|         if (!$profile) { | ||||
|             $profile = self::createActivityObjectProfile($object, $feeduri, $salmonuri); | ||||
|             $profile = self::createActivityObjectProfile($object, $feeduri, $salmonuri, $hints); | ||||
|         } | ||||
|         return $profile; | ||||
|     } | ||||
| @@ -745,10 +747,10 @@ class Ostatus_profile extends Memcached_DataObject | ||||
|         self::createActivityObjectProfile($actor, $feeduri, $salmonuri); | ||||
|     } | ||||
|  | ||||
|     protected static function createActivityObjectProfile($object, $feeduri=null, $salmonuri=null) | ||||
|     protected static function createActivityObjectProfile($object, $feeduri=null, $salmonuri=null, $hints=array()) | ||||
|     { | ||||
|         $homeuri  = $object->id; | ||||
|         $nickname = self::getActivityObjectNickname($object); | ||||
|         $nickname = self::getActivityObjectNickname($object, $hints); | ||||
|         $avatar   = self::getActivityObjectAvatar($object); | ||||
|  | ||||
|         if (!$homeuri) { | ||||
| @@ -756,6 +758,18 @@ class Ostatus_profile extends Memcached_DataObject | ||||
|             throw new ServerException("No profile URI"); | ||||
|         } | ||||
|  | ||||
|         if (empty($feeduri)) { | ||||
|             if (array_key_exists('feedurl', $hints)) { | ||||
|                 $feeduri = $hints['feedurl']; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (empty($salmonuri)) { | ||||
|             if (array_key_exists('salmon', $hints)) { | ||||
|                 $salmonuri = $hints['salmon']; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (!$feeduri || !$salmonuri) { | ||||
|             // Get the canonical feed URI and check it | ||||
|             $discover = new FeedDiscovery(); | ||||
| @@ -773,7 +787,11 @@ class Ostatus_profile extends Memcached_DataObject | ||||
|         $profile = new Profile(); | ||||
|         $profile->nickname   = $nickname; | ||||
|         $profile->fullname   = $object->title; | ||||
|         $profile->profileurl = $object->link; | ||||
|         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 | ||||
| @@ -812,12 +830,24 @@ class Ostatus_profile extends Memcached_DataObject | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected static function getActivityObjectNickname($object) | ||||
|     protected static function getActivityObjectNickname($object, $hints=array()) | ||||
|     { | ||||
|         // XXX: check whatever PoCo calls a nickname first | ||||
|  | ||||
|         // Try the definitive ID | ||||
|  | ||||
|         $nickname = self::nicknameFromURI($object->id); | ||||
|  | ||||
|         // Try a Webfinger if one was passed (way) down | ||||
|  | ||||
|         if (empty($nickname)) { | ||||
|             if (array_key_exists('webfinger', $hints)) { | ||||
|                 $nickname = self::nicknameFromURI($hints['webfinger']); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Try the name | ||||
|  | ||||
|         if (empty($nickname)) { | ||||
|             $nickname = common_nicknamize($object->title); | ||||
|         } | ||||
| @@ -845,4 +875,120 @@ class Ostatus_profile extends Memcached_DataObject | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static function ensureWebfinger($addr) | ||||
|     { | ||||
|         // First, look it up | ||||
|  | ||||
|         $oprofile = Ostatus_profile::staticGet('uri', 'acct:'.$addr); | ||||
|  | ||||
|         if (!empty($oprofile)) { | ||||
|             return $oprofile; | ||||
|         } | ||||
|  | ||||
|         // Now, try some discovery | ||||
|  | ||||
|         $wf = new Webfinger(); | ||||
|  | ||||
|         $result = $wf->lookup($addr); | ||||
|  | ||||
|         if (!$result) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         foreach ($result->links as $link) { | ||||
|             switch ($link['rel']) { | ||||
|             case Webfinger::PROFILEPAGE: | ||||
|                 $profileUrl = $link['href']; | ||||
|                 break; | ||||
|             case 'salmon': | ||||
|                 $salmonEndpoint = $link['href']; | ||||
|                 break; | ||||
|             case Webfinger::UPDATESFROM: | ||||
|                 $feedUrl = $link['href']; | ||||
|                 break; | ||||
|             default: | ||||
|                 common_log(LOG_NOTICE, "Don't know what to do with rel = '{$link['rel']}'"); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         $hints = array('webfinger' => $addr, | ||||
|                        'profileurl' => $profileUrl, | ||||
|                        'feedurl' => $feedUrl, | ||||
|                        'salmon' => $salmonEndpoint); | ||||
|  | ||||
|         // If we got a feed URL, try that | ||||
|  | ||||
|         if (isset($feedUrl)) { | ||||
|             try { | ||||
|                 $oprofile = self::ensureProfile($feedUrl, $hints); | ||||
|                 return $oprofile; | ||||
|             } catch (Exception $e) { | ||||
|                 common_log(LOG_WARNING, "Failed creating profile from feed URL '$feedUrl': " . $e->getMessage()); | ||||
|                 // keep looking | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // If we got a profile page, try that! | ||||
|  | ||||
|         if (isset($profileUrl)) { | ||||
|             try { | ||||
|                 $oprofile = self::ensureProfile($profileUrl, $hints); | ||||
|                 return $oprofile; | ||||
|             } catch (Exception $e) { | ||||
|                 common_log(LOG_WARNING, "Failed creating profile from profile URL '$profileUrl': " . $e->getMessage()); | ||||
|                 // keep looking | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // XXX: try hcard | ||||
|         // XXX: try FOAF | ||||
|  | ||||
|         if (isset($salmonEndpoint)) { | ||||
|  | ||||
|             // An account URL, a salmon endpoint, and a dream? Not much to go | ||||
|             // on, but let's give it a try | ||||
|  | ||||
|             $uri = 'acct:'.$addr; | ||||
|  | ||||
|             $profile = new Profile(); | ||||
|  | ||||
|             $profile->nickname = self::nicknameFromUri($uri); | ||||
|             $profile->created  = common_sql_now(); | ||||
|  | ||||
|             if (isset($profileUrl)) { | ||||
|                 $profile->profileurl = $profileUrl; | ||||
|             } | ||||
|  | ||||
|             $profile_id = $profile->insert(); | ||||
|  | ||||
|             if (!$profile_id) { | ||||
|                 common_log_db_error($profile, 'INSERT', __FILE__); | ||||
|                 throw new Exception("Couldn't save profile for '$addr'"); | ||||
|             } | ||||
|  | ||||
|             $oprofile = new Ostatus_profile(); | ||||
|  | ||||
|             $oprofile->uri        = $uri; | ||||
|             $oprofile->salmonuri  = $salmonEndpoint; | ||||
|             $oprofile->profile_id = $profile_id; | ||||
|             $oprofile->created    = common_sql_now(); | ||||
|  | ||||
|             if (isset($feedUrl)) { | ||||
|                 $profile->feeduri = $feedUrl; | ||||
|             } | ||||
|  | ||||
|             $result = $oprofile->insert(); | ||||
|  | ||||
|             if (!$result) { | ||||
|                 common_log_db_error($oprofile, 'INSERT', __FILE__); | ||||
|                 throw new Exception("Couldn't save ostatus_profile for '$addr'"); | ||||
|             } | ||||
|  | ||||
|             return $oprofile; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										114
									
								
								plugins/OStatus/classes/Ostatus_source.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								plugins/OStatus/classes/Ostatus_source.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| <?php | ||||
| /* | ||||
|  * StatusNet - the distributed open-source microblogging tool | ||||
|  * Copyright (C) 2010, StatusNet, Inc. | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * @package OStatusPlugin | ||||
|  * @maintainer Brion Vibber <brion@status.net> | ||||
|  */ | ||||
|  | ||||
| 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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -173,13 +173,17 @@ class SalmonAction extends Action | ||||
|  | ||||
|         $html = $this->act->object->content; | ||||
|  | ||||
|         $rendered = HTMLPurifier::purify($html); | ||||
|         $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); | ||||
|                          'rendered' => $rendered, | ||||
|                          'replies' => $this->act->context->attention); | ||||
|  | ||||
|         if (!empty($this->act->context->location)) { | ||||
|             $options['lat'] = $location->lat; | ||||
| @@ -199,12 +203,17 @@ 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); | ||||
|         } | ||||
|  | ||||
|         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; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -32,11 +32,16 @@ define('WEBFINGER_SERVICE_REL_VALUE', 'lrdd'); | ||||
| /** | ||||
|  * Implement the webfinger protocol. | ||||
|  */ | ||||
|  | ||||
| class Webfinger | ||||
| { | ||||
|     const PROFILEPAGE = 'http://webfinger.net/rel/profile-page'; | ||||
|     const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from'; | ||||
|  | ||||
|     /** | ||||
|      * Perform a webfinger lookup given an account. | ||||
|      */  | ||||
|      */ | ||||
|  | ||||
|     public function lookup($id) | ||||
|     { | ||||
|         $id = $this->normalize($id); | ||||
| @@ -46,7 +51,7 @@ class Webfinger | ||||
|         if (!$links) { | ||||
|             return false; | ||||
|         } | ||||
|          | ||||
|  | ||||
|         $services = array(); | ||||
|         foreach ($links as $link) { | ||||
|             if ($link['template']) { | ||||
| @@ -64,7 +69,7 @@ class Webfinger | ||||
|     function normalize($id) | ||||
|     { | ||||
|         if (substr($id, 0, 7) == 'acct://') { | ||||
|             return substr($id, 7);  | ||||
|             return substr($id, 7); | ||||
|         } else if (substr($id, 0, 5) == 'acct:') { | ||||
|             return substr($id, 5); | ||||
|         } | ||||
| @@ -86,7 +91,7 @@ class Webfinger | ||||
|         if ($result->host != $domain) { | ||||
|             return false; | ||||
|         } | ||||
|          | ||||
|  | ||||
|         $links = array(); | ||||
|         foreach ($result->links as $link) { | ||||
|             if ($link['rel'] == WEBFINGER_SERVICE_REL_VALUE) { | ||||
| @@ -140,4 +145,3 @@ class Webfinger | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user