From bc4e843f396dc450b04b612e7de14246084469d1 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Feb 2010 19:22:55 -0800 Subject: [PATCH 01/55] Disable deprecated 'since' parameter on public_timeline API; causes performance problems. (since_id will work cleanly) --- actions/apitimelinepublic.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/actions/apitimelinepublic.php b/actions/apitimelinepublic.php index 3f4a46c0fa..0fb0788e98 100644 --- a/actions/apitimelinepublic.php +++ b/actions/apitimelinepublic.php @@ -74,6 +74,10 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction parent::prepare($args); $this->notices = $this->getNotices(); + + if ($this->since) { + throw new ServerException("since parameter is disabled for performance; use since_id", 403); + } return true; } @@ -145,7 +149,7 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction $notice = Notice::publicStream( ($this->page - 1) * $this->count, $this->count, $this->since_id, - $this->max_id, $this->since + $this->max_id ); while ($notice->fetch()) { From 70d5f39ed66cb277e925ea71dcc24ca580110b3d Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 8 Feb 2010 21:52:05 -0800 Subject: [PATCH 02/55] Better checking for duplicate app names --- actions/editapplication.php | 2 +- actions/newapplication.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/editapplication.php b/actions/editapplication.php index ca5dba1e49..64cf0a5745 100644 --- a/actions/editapplication.php +++ b/actions/editapplication.php @@ -277,7 +277,7 @@ class EditApplicationAction extends OwnerDesignAction function nameExists($name) { $newapp = Oauth_application::staticGet('name', $name); - if (!$newapp) { + if (empty($newapp)) { return false; } else { return $newapp->id != $this->app->id; diff --git a/actions/newapplication.php b/actions/newapplication.php index c0c5207979..0f819b3499 100644 --- a/actions/newapplication.php +++ b/actions/newapplication.php @@ -290,7 +290,7 @@ class NewApplicationAction extends OwnerDesignAction function nameExists($name) { $app = Oauth_application::staticGet('name', $name); - return ($app !== false); + return !empty($app); } } From e856af34c3ac560a21286ca89019c2249994c080 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 9 Feb 2010 12:39:31 -0800 Subject: [PATCH 03/55] Configurable delay between queuedaemon.php spawns/respawns to help stagger out startups and subscriptions. Defaults to 1 second. $config['queue']['spawndelay'] = 1; --- lib/default.php | 1 + lib/spawningdaemon.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/lib/default.php b/lib/default.php index 485a08ba44..bf4b83718d 100644 --- a/lib/default.php +++ b/lib/default.php @@ -88,6 +88,7 @@ $default = 'stomp_manual_failover' => true, // if multiple servers are listed, treat them as separate (enqueue on one randomly, listen on all) 'monitor' => null, // URL to monitor ping endpoint (work in progress) 'softlimit' => '90%', // total size or % of memory_limit at which to restart queue threads gracefully + 'spawndelay' => 1, // Wait at least N seconds between (re)spawns of child processes to avoid slamming the queue server with subscription startup 'debug_memory' => false, // true to spit memory usage to log 'inboxes' => true, // true to do inbox distribution & output queueing from in background via 'distrib' queue ), diff --git a/lib/spawningdaemon.php b/lib/spawningdaemon.php index b1961d6880..862cbb4fa3 100644 --- a/lib/spawningdaemon.php +++ b/lib/spawningdaemon.php @@ -83,6 +83,7 @@ abstract class SpawningDaemon extends Daemon $this->log(LOG_INFO, "Spawned thread $i as pid $pid"); $children[$i] = $pid; } + sleep(common_config('queue', 'spawndelay')); } $this->log(LOG_INFO, "Waiting for children to complete."); @@ -111,6 +112,7 @@ abstract class SpawningDaemon extends Daemon $this->log(LOG_INFO, "Respawned thread $i as pid $pid"); $children[$i] = $pid; } + sleep(common_config('queue', 'spawndelay')); } else { $this->log(LOG_INFO, "Thread $i pid $pid exited with status $exitCode; closing out thread."); } From f37063cd63a30fdcc0948d4710c088ba5e5d0990 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 10 Feb 2010 10:18:47 -0800 Subject: [PATCH 04/55] Filename case fix --- plugins/OStatus/lib/{Salmon.php => salmon.php} | 0 plugins/OStatus/lib/{Webfinger.php => webfinger.php} | 0 plugins/OStatus/lib/{XRD.php => xrd.php} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename plugins/OStatus/lib/{Salmon.php => salmon.php} (100%) rename plugins/OStatus/lib/{Webfinger.php => webfinger.php} (100%) rename plugins/OStatus/lib/{XRD.php => xrd.php} (100%) diff --git a/plugins/OStatus/lib/Salmon.php b/plugins/OStatus/lib/salmon.php similarity index 100% rename from plugins/OStatus/lib/Salmon.php rename to plugins/OStatus/lib/salmon.php diff --git a/plugins/OStatus/lib/Webfinger.php b/plugins/OStatus/lib/webfinger.php similarity index 100% rename from plugins/OStatus/lib/Webfinger.php rename to plugins/OStatus/lib/webfinger.php diff --git a/plugins/OStatus/lib/XRD.php b/plugins/OStatus/lib/xrd.php similarity index 100% rename from plugins/OStatus/lib/XRD.php rename to plugins/OStatus/lib/xrd.php From d9c9b2a12fe6cbb800440eeb0174d375760e0103 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 10 Feb 2010 10:59:30 -0800 Subject: [PATCH 05/55] Queue daemon fixes: * skip unnecessary unsubscribes on graceful shutdown -- takes a long time for many queues, slows down our restarts when hitting graceful mem limit * fix control channel (was broken when we switched to support multiple queue servers) --- lib/stompqueuemanager.php | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/lib/stompqueuemanager.php b/lib/stompqueuemanager.php index 6730cd213d..cc4c817d8f 100644 --- a/lib/stompqueuemanager.php +++ b/lib/stompqueuemanager.php @@ -107,9 +107,10 @@ class StompQueueManager extends QueueManager $message .= ':' . $param; } $this->_connect(); - $result = $this->_send($this->control, - $message, - array ('created' => common_sql_now())); + $con = $this->cons[$this->defaultIdx]; + $result = $con->send($this->control, + $message, + array ('created' => common_sql_now())); if ($result) { $this->_log(LOG_INFO, "Sent control ping to queue daemons: $message"); return true; @@ -368,17 +369,10 @@ class StompQueueManager extends QueueManager foreach ($this->cons as $i => $con) { if ($con) { $this->rollback($i); - $con->unsubscribe($this->control); + $con->disconnect(); + $this->cons[$i] = null; } } - if ($this->sites) { - foreach ($this->sites as $server) { - StatusNet::init($server); - $this->doUnsubscribe(); - } - } else { - $this->doUnsubscribe(); - } return true; } From 045797331c82b86e03c61f00f4db68a085688520 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Feb 2010 16:43:37 -0800 Subject: [PATCH 06/55] fix up hub queueing to work w/ stomp queues --- lib/queuemanager.php | 36 +++++++++++++------ lib/stompqueuemanager.php | 26 ++++---------- .../OStatus/lib/hubdistribqueuehandler.php | 1 + plugins/OStatus/lib/huboutqueuehandler.php | 2 +- 4 files changed, 34 insertions(+), 31 deletions(-) diff --git a/lib/queuemanager.php b/lib/queuemanager.php index afe710e884..149617eb50 100644 --- a/lib/queuemanager.php +++ b/lib/queuemanager.php @@ -155,26 +155,26 @@ abstract class QueueManager extends IoManager } /** - * Encode an object for queued storage. - * Next gen may use serialization. + * Encode an object or variable for queued storage. + * Notice objects are currently stored as an id reference; + * other items are serialized. * - * @param mixed $object + * @param mixed $item * @return string */ - protected function encode($object) + protected function encode($item) { - if ($object instanceof Notice) { - return $object->id; - } else if (is_string($object)) { - return $object; + if ($item instanceof Notice) { + // Backwards compat + return $item->id; } else { - throw new ServerException("Can't queue this type", 500); + return serialize($item); } } /** * Decode an object from queued storage. - * Accepts back-compat notice reference entries and strings for now. + * Accepts notice reference entries and serialized items. * * @param string * @return mixed @@ -182,9 +182,23 @@ abstract class QueueManager extends IoManager protected function decode($frame) { if (is_numeric($frame)) { + // Back-compat for notices... return Notice::staticGet(intval($frame)); - } else { + } elseif (substr($frame, 0, 1) == '<') { + // Back-compat for XML source return $frame; + } else { + // Deserialize! + #$old = error_reporting(); + #error_reporting($old & ~E_NOTICE); + $out = unserialize($frame); + #error_reporting($old); + + if ($out === false && $frame !== 'b:0;') { + common_log(LOG_ERR, "Couldn't unserialize queued frame: $frame"); + return false; + } + return $out; } } diff --git a/lib/stompqueuemanager.php b/lib/stompqueuemanager.php index cc4c817d8f..cd62c25bd8 100644 --- a/lib/stompqueuemanager.php +++ b/lib/stompqueuemanager.php @@ -549,26 +549,14 @@ class StompQueueManager extends QueueManager } $host = $this->cons[$idx]->getServer(); - if (is_numeric($frame->body)) { - $id = intval($frame->body); - $info = "notice $id posted at {$frame->headers['created']} in queue $queue from $host"; - - $notice = Notice::staticGet('id', $id); - if (empty($notice)) { - $this->_log(LOG_WARNING, "Skipping missing $info"); - $this->ack($idx, $frame); - $this->commit($idx); - $this->begin($idx); - $this->stats('badnotice', $queue); - return false; - } - - $item = $notice; - } else { - // @fixme should we serialize, or json, or what here? - $info = "string posted at {$frame->headers['created']} in queue $queue from $host"; - $item = $frame->body; + $item = $this->decode($frame->body); + if (empty($item)) { + $this->_log(LOG_ERR, "Skipping empty or deleted item in queue $queue from $host"); + return true; } + $info = $this->logrep($item) . " posted at " . + $frame->headers['created'] . " in queue $queue from $host"; + $this->_log(LOG_DEBUG, "Dequeued $info"); $handler = $this->getHandler($queue); if (!$handler) { diff --git a/plugins/OStatus/lib/hubdistribqueuehandler.php b/plugins/OStatus/lib/hubdistribqueuehandler.php index 189ccbedf9..de3a813858 100644 --- a/plugins/OStatus/lib/hubdistribqueuehandler.php +++ b/plugins/OStatus/lib/hubdistribqueuehandler.php @@ -56,6 +56,7 @@ class HubDistribQueueHandler extends QueueHandler } else { common_log(LOG_INFO, "No PuSH subscribers for $feed"); } + return true; } function pushGroup($notice, $group_id) diff --git a/plugins/OStatus/lib/huboutqueuehandler.php b/plugins/OStatus/lib/huboutqueuehandler.php index cb44ad2c4e..0791c7e5db 100644 --- a/plugins/OStatus/lib/huboutqueuehandler.php +++ b/plugins/OStatus/lib/huboutqueuehandler.php @@ -43,7 +43,7 @@ class HubOutQueueHandler extends QueueHandler common_log(LOG_ERR, "Failed PuSH to $sub->callback for $sub->topic: " . $e->getMessage()); // @fixme Reschedule a later delivery? - // Currently we have no way to do this other than 'send NOW' + return true; } return true; From 7752612ef6c6d00ad3adee178998d997f956f4b5 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 10 Feb 2010 20:47:42 +0000 Subject: [PATCH 07/55] fix hubdistrib --- plugins/OStatus/lib/hubdistribqueuehandler.php | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/OStatus/lib/hubdistribqueuehandler.php b/plugins/OStatus/lib/hubdistribqueuehandler.php index de3a813858..a35b8874c5 100644 --- a/plugins/OStatus/lib/hubdistribqueuehandler.php +++ b/plugins/OStatus/lib/hubdistribqueuehandler.php @@ -38,6 +38,7 @@ class HubDistribQueueHandler extends QueueHandler foreach ($notice->getGroups() as $group) { $this->pushGroup($notice, $group->group_id); } + return true; } function pushUser($notice) From 162868afdb1181a3d6e973a3de9d0abbb5e1c168 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 10 Feb 2010 21:18:53 +0000 Subject: [PATCH 08/55] OStatus update: now using standard save/delivery for incoming ostatus messages -- they get reflected to realtime and everything! woooo Group delivery may still need some munging --- plugins/OStatus/classes/Feedinfo.php | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/plugins/OStatus/classes/Feedinfo.php b/plugins/OStatus/classes/Feedinfo.php index 792ea60345..b4e55c3643 100644 --- a/plugins/OStatus/classes/Feedinfo.php +++ b/plugins/OStatus/classes/Feedinfo.php @@ -344,26 +344,31 @@ class Feedinfo extends Memcached_DataObject $hits = 0; foreach ($feed as $index => $entry) { // @fixme this might sort in wrong order if we get multiple updates - + $notice = $munger->notice($index); $notice->profile_id = $this->profile_id; - + // Double-check for oldies // @fixme this could explode horribly for multiple feeds on a blog. sigh $dupe = new Notice(); $dupe->uri = $notice->uri; if ($dupe->find(true)) { - // @fixme we might have to do individual and group delivery separately! common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}"); continue; } - if (Event::handle('StartNoticeSave', array(&$notice))) { - $id = $notice->insert(); - Event::handle('EndNoticeSave', array($notice)); - } - common_log(LOG_INFO, __METHOD__ . ": saved notice {$notice->id} for entry $index of update to \"{$this->feeduri}\""); + // @fixme need to ensure that groups get handled correctly + $saved = Notice::saveNew($this->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); @@ -380,6 +385,7 @@ class Feedinfo extends Memcached_DataObject common_log(LOG_DEBUG, "going to add to inboxes..."); $notice->addToInboxes($groups, array()); common_log(LOG_DEBUG, "added to inboxes."); + */ $hits++; } From 4ae760cb62657e68b6b2313e64d2bb59fe264df4 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 10 Feb 2010 22:58:39 +0000 Subject: [PATCH 09/55] OStatus PuSH fixes: * HMAC now calculated correctly - confirmed interop with Google's public hub * Can optionally use an external PuSH hub, set URL in $config['ostatus']['hub'] (may have issues in replication environment, and will ping the hub for every update rather than just those with subscribers) Internal hub will still function when this is set, but won't be advertised. Warning: setting this, then turning it off later will break subscriptions as that hub will no longer receive pings. --- plugins/OStatus/OStatusPlugin.php | 11 ++- plugins/OStatus/classes/Feedinfo.php | 4 +- plugins/OStatus/classes/HubSub.php | 2 +- .../OStatus/lib/hubdistribqueuehandler.php | 70 +++++++++++++++---- 4 files changed, 67 insertions(+), 20 deletions(-) diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 62ecaf6310..4b9b4d2c32 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -114,10 +114,15 @@ class OStatusPlugin extends Plugin if ($action instanceof ApiTimelineUserAction || $action instanceof ApiTimelineGroupAction) { $id = $action->arg('id'); if (strval(intval($id)) === strval($id)) { - // Canonical form of id in URL? - // Updates will be handled for our internal PuSH hub. + // Canonical form of id in URL? These are used for OStatus syndication. + + $hub = common_config('ostatus', 'hub'); + if (empty($hub)) { + // Updates will be handled through our internal PuSH hub. + $hub = common_local_url('pushhub'); + } $action->element('link', array('rel' => 'hub', - 'href' => common_local_url('pushhub'))); + 'href' => $hub)); // Also, we'll add in the salmon link $action->element('link', array('rel' => 'salmon', diff --git a/plugins/OStatus/classes/Feedinfo.php b/plugins/OStatus/classes/Feedinfo.php index b4e55c3643..2344a4a0ee 100644 --- a/plugins/OStatus/classes/Feedinfo.php +++ b/plugins/OStatus/classes/Feedinfo.php @@ -160,7 +160,7 @@ class Feedinfo extends Memcached_DataObject function keyTypes() { - return array('id' => 'K'); // @fixme we'll need a profile_id key at least + return array('id' => 'K', 'feeduri' => 'U'); // @fixme we'll need a profile_id key at least } function sequenceKey() @@ -323,7 +323,7 @@ class Feedinfo extends Memcached_DataObject if ($this->secret) { if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) { $their_hmac = strtolower($matches[1]); - $our_hmac = sha1($xml . $this->secret); + $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; diff --git a/plugins/OStatus/classes/HubSub.php b/plugins/OStatus/classes/HubSub.php index 1769f6c941..7071ee5b4f 100644 --- a/plugins/OStatus/classes/HubSub.php +++ b/plugins/OStatus/classes/HubSub.php @@ -242,7 +242,7 @@ class HubSub extends Memcached_DataObject { $headers = array('Content-Type: application/atom+xml'); if ($this->secret) { - $hmac = sha1($atom . $this->secret); + $hmac = hash_hmac('sha1', $atom, $this->secret); $headers[] = "X-Hub-Signature: sha1=$hmac"; } else { $hmac = '(none)'; diff --git a/plugins/OStatus/lib/hubdistribqueuehandler.php b/plugins/OStatus/lib/hubdistribqueuehandler.php index a35b8874c5..245a57f720 100644 --- a/plugins/OStatus/lib/hubdistribqueuehandler.php +++ b/plugins/OStatus/lib/hubdistribqueuehandler.php @@ -49,15 +49,7 @@ class HubDistribQueueHandler extends QueueHandler $feed = common_local_url('ApiTimelineUser', array('id' => $notice->profile_id, 'format' => 'atom')); - $sub = new HubSub(); - $sub->topic = $feed; - if ($sub->find()) { - $atom = $this->userFeedForNotice($notice); - $this->pushFeeds($atom, $sub); - } else { - common_log(LOG_INFO, "No PuSH subscribers for $feed"); - } - return true; + $this->pushFeed($feed, array($this, 'userFeedForNotice'), $notice); } function pushGroup($notice, $group_id) @@ -65,19 +57,69 @@ class HubDistribQueueHandler extends QueueHandler $feed = common_local_url('ApiTimelineGroup', array('id' => $group_id, 'format' => 'atom')); + $this->pushFeed($feed, array($this, 'groupFeedForNotice'), $group_id, $notice); + } + + /** + * @param string $feed URI to the feed + * @param callable $callback function to generate Atom feed update if needed + * any additional params are passed to the callback. + */ + function pushFeed($feed, $callback) + { + $hub = common_config('ostatus', 'hub'); + if ($hub) { + $this->pushFeedExternal($feed, $hub); + } + $sub = new HubSub(); $sub->topic = $feed; if ($sub->find()) { - common_log(LOG_INFO, "Building PuSH feed for $feed"); - $atom = $this->groupFeedForNotice($group_id, $notice); - $this->pushFeeds($atom, $sub); + $args = array_slice(func_get_args(), 2); + $atom = call_user_func_array($callback, $args); + $this->pushFeedInternal($atom, $sub); } else { common_log(LOG_INFO, "No PuSH subscribers for $feed"); } + return true; } - - function pushFeeds($atom, $sub) + /** + * Ping external hub about this update. + * The hub will pull the feed and check for new items later. + * Not guaranteed safe in an environment with database replication. + * + * @param string $feed feed topic URI + * @param string $hub PuSH hub URI + * @fixme can consolidate pings for user & group posts + */ + function pushFeedExternal($feed, $hub) + { + $client = new HTTPClient(); + try { + $data = array('hub.mode' => 'publish', + 'hub.url' => $feed); + $response = $client->post($hub, array(), $data); + if ($response->getStatus() == 204) { + common_log(LOG_INFO, "PuSH ping to hub $hub for $feed ok"); + return true; + } else { + common_log(LOG_ERR, "PuSH ping to hub $hub for $feed failed with HTTP " . + $response->getStatus() . ': ' . + $response->getBody()); + } + } catch (Exception $e) { + common_log(LOG_ERR, "PuSH ping to hub $hub for $feed failed: " . $e->getMessage()); + return false; + } + } + + /** + * Queue up direct feed update pushes to subscribers on our internal hub. + * @param string $atom update feed, containing only new/changed items + * @param HubSub $sub open query of subscribers + */ + function pushFeedInternal($atom, $sub) { common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $sub->topic"); $qm = QueueManager::get(); From 71151b2583d81e28c5f5d42a690c649f4e84f3bf Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Thu, 11 Feb 2010 00:09:20 +0000 Subject: [PATCH 10/55] OStatus: garbage collect unused PuSH subscriptions when the last local subscriber unsubs --- plugins/OStatus/OStatusPlugin.php | 23 ++++++++++++++++++++--- plugins/OStatus/actions/pushcallback.php | 19 ++++++++++++------- plugins/OStatus/classes/Feedinfo.php | 22 ++++++++++++++++------ 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 4b9b4d2c32..ce02393e43 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -234,13 +234,30 @@ class OStatusPlugin extends Plugin } } + + /** + * Garbage collect unused feeds on unsubscribe + */ + function onEndUnsubscribe($user, $other) + { + $feed = Feedinfo::staticGet('profile_id', $other->id); + if ($feed) { + $sub = new Subscription(); + $sub->subscribed = $other->id; + $sub->limit(1); + if (!$sub->find(true)) { + common_log(LOG_INFO, "Unsubscribing from now-unused feed $feed->feeduri on hub $feed->huburi"); + $feed->unsubscribe(); + } + } + return true; + } + function onCheckSchema() { - // warning: the autoincrement doesn't seem to set. - // alter table feedinfo change column id id int(11) not null auto_increment; $schema = Schema::get(); $schema->ensureTable('feedinfo', Feedinfo::schemaDef()); $schema->ensureTable('hubsub', HubSub::schemaDef()); return true; - } + } } diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php index a5e02e08f1..471d079ab9 100644 --- a/plugins/OStatus/actions/pushcallback.php +++ b/plugins/OStatus/actions/pushcallback.php @@ -91,15 +91,20 @@ class PushCallbackAction extends Action #} // OK! - common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); - $feedinfo->sub_start = common_sql_date(time()); - if ($lease_seconds > 0) { - $feedinfo->sub_end = common_sql_date(time() + $lease_seconds); + if ($mode == 'subscribe') { + common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); + $feedinfo->sub_start = common_sql_date(time()); + if ($lease_seconds > 0) { + $feedinfo->sub_end = common_sql_date(time() + $lease_seconds); + } else { + $feedinfo->sub_end = null; + } + $feedinfo->update(); } else { - $feedinfo->sub_end = null; + common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic"); + $feedinfo->delete(); } - $feedinfo->update(); - + print $challenge; } } diff --git a/plugins/OStatus/classes/Feedinfo.php b/plugins/OStatus/classes/Feedinfo.php index 2344a4a0ee..d3cccd42f0 100644 --- a/plugins/OStatus/classes/Feedinfo.php +++ b/plugins/OStatus/classes/Feedinfo.php @@ -112,9 +112,9 @@ class Feedinfo extends Memcached_DataObject /*extra*/ null, /*auto_increment*/ true), new ColumnDef('profile_id', 'integer', - null, true), + null, true, 'UNI'), new ColumnDef('group_id', 'integer', - null, true), + null, true, 'UNI'), new ColumnDef('feeduri', 'varchar', 255, false, 'UNI'), new ColumnDef('homeuri', 'varchar', @@ -160,7 +160,7 @@ class Feedinfo extends Memcached_DataObject function keyTypes() { - return array('id' => 'K', 'feeduri' => 'U'); // @fixme we'll need a profile_id key at least + return array('id' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U'); } function sequenceKey() @@ -261,11 +261,11 @@ class Feedinfo extends Memcached_DataObject /** * Send a subscription request to the hub for this feed. - * The hub will later send us a confirmation POST to /feedsub/callback. + * The hub will later send us a confirmation POST to /main/push/callback. * * @return bool true on success, false on failure */ - public function subscribe() + public function subscribe($mode='subscribe') { if (common_config('feedsub', 'nohub')) { // Fake it! We're just testing remote feeds w/o hubs. @@ -278,7 +278,7 @@ class Feedinfo extends Memcached_DataObject try { $callback = common_local_url('pushcallback', array('feed' => $this->id)); $headers = array('Content-Type: application/x-www-form-urlencoded'); - $post = array('hub.mode' => 'subscribe', + $post = array('hub.mode' => $mode, 'hub.callback' => $callback, 'hub.verify' => 'async', 'hub.verify_token' => $this->verify_token, @@ -308,6 +308,16 @@ class Feedinfo extends Memcached_DataObject } } + /** + * Send an 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'); + } + /** * Read and post notices for updates from the feed. * Currently assumes that all items in the feed are new, From 20714d1f35305cc29c2b657310c2e0db290fbb48 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Thu, 11 Feb 2010 19:44:03 +0000 Subject: [PATCH 11/55] OStatus fix: include feed profile at notice text processing time, fixes replies --- plugins/OStatus/classes/Feedinfo.php | 3 +-- plugins/OStatus/lib/feedmunger.php | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/plugins/OStatus/classes/Feedinfo.php b/plugins/OStatus/classes/Feedinfo.php index d3cccd42f0..e71b0cfa00 100644 --- a/plugins/OStatus/classes/Feedinfo.php +++ b/plugins/OStatus/classes/Feedinfo.php @@ -356,7 +356,6 @@ class Feedinfo extends Memcached_DataObject // @fixme this might sort in wrong order if we get multiple updates $notice = $munger->notice($index); - $notice->profile_id = $this->profile_id; // Double-check for oldies // @fixme this could explode horribly for multiple feeds on a blog. sigh @@ -368,7 +367,7 @@ class Feedinfo extends Memcached_DataObject } // @fixme need to ensure that groups get handled correctly - $saved = Notice::saveNew($this->profile_id, + $saved = Notice::saveNew($notice->profile_id, $notice->content, 'ostatus', array('is_local' => Notice::REMOTE_OMB, diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php index 5dce95342e..7f223cb20d 100644 --- a/plugins/OStatus/lib/feedmunger.php +++ b/plugins/OStatus/lib/feedmunger.php @@ -154,6 +154,11 @@ class FeedMunger { return $this->getAtomLink($this->feed, array('rel' => 'hub')); } + + function getSelfLink() + { + return $this->getAtomLink($this->feed, array('rel' => 'self')); + } /** * Get an appropriate avatar image source URL, if available. @@ -209,6 +214,7 @@ class FeedMunger $notice->id = -1; } else { $notice = new Notice(); + $notice->profile_id = $this->profileIdForEntry($index); } $link = $this->getAltLink($entry); @@ -239,6 +245,20 @@ class FeedMunger return $notice; } + function profileIdForEntry($index=1) + { + // hack hack hack + // should get profile for this entry's author... + $feed = new Feedinfo(); + $feed->feeduri = $self; + $feed = Feedinfo::staticGet('feeduri', $this->getSelfLink()); + if ($feed) { + return $feed->profile_id; + } else { + throw new Exception("Can't find feed profile"); + } + } + /** * @param feed item $entry * @return mixed Location or false From 21bfbc43ad026afdab4040713805913000fef626 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Thu, 11 Feb 2010 20:02:17 +0000 Subject: [PATCH 12/55] OStatus: fix salmon link on Atom feeds; add a url spec for group feeds as well (endpoint needs impl) --- plugins/OStatus/OStatusPlugin.php | 57 +++++++++++++++++++------------ 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index ce02393e43..c0f9dadc4a 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -80,6 +80,9 @@ class OStatusPlugin extends Plugin $m->connect('main/salmon/user/:id', array('action' => 'salmon'), array('id' => '[0-9]+')); + $m->connect('main/salmon/group/:id', + array('action' => 'salmongroup'), + array('id' => '[0-9]+')); return true; } @@ -111,25 +114,31 @@ class OStatusPlugin extends Plugin */ function onStartApiAtom(Action $action) { - if ($action instanceof ApiTimelineUserAction || $action instanceof ApiTimelineGroupAction) { - $id = $action->arg('id'); - if (strval(intval($id)) === strval($id)) { - // Canonical form of id in URL? These are used for OStatus syndication. - - $hub = common_config('ostatus', 'hub'); - if (empty($hub)) { - // Updates will be handled through our internal PuSH hub. - $hub = common_local_url('pushhub'); - } - $action->element('link', array('rel' => 'hub', - 'href' => $hub)); - - // Also, we'll add in the salmon link - $action->element('link', array('rel' => 'salmon', - 'href' => common_local_url('salmon'))); - } + if ($action instanceof ApiTimelineUserAction) { + $salmonAction = 'salmon'; + } else if ($action instanceof ApiTimelineGroupAction) { + $salmonAction = 'salmongroup'; + } else { + return; + } + + $id = $action->arg('id'); + if (strval(intval($id)) === strval($id)) { + // Canonical form of id in URL? These are used for OStatus syndication. + + $hub = common_config('ostatus', 'hub'); + if (empty($hub)) { + // Updates will be handled through our internal PuSH hub. + $hub = common_local_url('pushhub'); + } + $action->element('link', array('rel' => 'hub', + 'href' => $hub)); + + // Also, we'll add in the salmon link + $salmon = common_local_url($salmonAction, array('id' => $id)); + $action->element('link', array('rel' => 'salmon', + 'href' => $salmon)); } - return true; } /** @@ -191,14 +200,17 @@ class OStatusPlugin extends Plugin array('nickname' => $profile->nickname)); $output->element('a', array('href' => $url, 'class' => 'entity_remote_subscribe'), - _('OStatus')); + _m('OStatus')); $output->elementEnd('li'); } } /** - * Check if we've got some Salmon stuff to send + * Check if we've got remote replies to send via Salmon. + * + * @fixme push webfinger lookup & sending to a background queue + * @fixme also detect short-form name for remote subscribees where not ambiguous */ function onEndNoticeSave($notice) { @@ -233,7 +245,6 @@ class OStatusPlugin extends Plugin } } } - /** * Garbage collect unused feeds on unsubscribe @@ -253,7 +264,9 @@ class OStatusPlugin extends Plugin return true; } - + /** + * Make sure necessary tables are filled out. + */ function onCheckSchema() { $schema = Schema::get(); $schema->ensureTable('feedinfo', Feedinfo::schemaDef()); From 1773d12a24d2720cdb6c1b517999cac1f708b355 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Thu, 11 Feb 2010 20:12:48 +0000 Subject: [PATCH 13/55] OStatus: save Salmon postback URI in feed subscription info, if provided. Will need it for sub/unsub postbacks and other notifications. --- plugins/OStatus/classes/Feedinfo.php | 9 ++++++--- plugins/OStatus/lib/feedmunger.php | 11 ++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/plugins/OStatus/classes/Feedinfo.php b/plugins/OStatus/classes/Feedinfo.php index e71b0cfa00..5b8a9039a6 100644 --- a/plugins/OStatus/classes/Feedinfo.php +++ b/plugins/OStatus/classes/Feedinfo.php @@ -93,11 +93,12 @@ class Feedinfo extends Memcached_DataObject 'group_id' => DB_DATAOBJECT_INT, 'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, 'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, - 'huburi' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'huburi' => DB_DATAOBJECT_STR, 'secret' => DB_DATAOBJECT_STR, 'verify_token' => DB_DATAOBJECT_STR, 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'salmonuri' => DB_DATAOBJECT_STR, 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, 'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); } @@ -119,8 +120,8 @@ class Feedinfo extends Memcached_DataObject 255, false, 'UNI'), new ColumnDef('homeuri', 'varchar', 255, false), - new ColumnDef('huburi', 'varchar', - 255, false), + new ColumnDef('huburi', 'text', + null, true), new ColumnDef('verify_token', 'varchar', 32, true), new ColumnDef('secret', 'varchar', @@ -129,6 +130,8 @@ class Feedinfo extends Memcached_DataObject null, true), new ColumnDef('sub_end', 'datetime', null, true), + new ColumnDef('salmonuri', 'text', + null, true), new ColumnDef('created', 'datetime', null, false), new ColumnDef('lastupdate', 'datetime', diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php index 7f223cb20d..25b0a09317 100644 --- a/plugins/OStatus/lib/feedmunger.php +++ b/plugins/OStatus/lib/feedmunger.php @@ -89,6 +89,10 @@ class FeedMunger $feedinfo->feeduri = $this->url; $feedinfo->homeuri = $this->feed->link; $feedinfo->huburi = $this->getHubLink(); + $salmon = $this->getSalmonLink(); + if ($salmon) { + $feedinfo->salmonuri = $salmon; + } return $feedinfo; } @@ -154,7 +158,12 @@ class FeedMunger { return $this->getAtomLink($this->feed, array('rel' => 'hub')); } - + + function getSalmonLink() + { + return $this->getAtomLink($this->feed, array('rel' => 'salmon')); + } + function getSelfLink() { return $this->getAtomLink($this->feed, array('rel' => 'self')); From a6ab9c4a3e820b9d293075b1fec8b5eb05df87e9 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 11 Feb 2010 16:42:58 -0500 Subject: [PATCH 14/55] Themes can be served from an SSL server --- lib/default.php | 3 ++- lib/theme.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/default.php b/lib/default.php index bf4b83718d..1a2cc4cf6d 100644 --- a/lib/default.php +++ b/lib/default.php @@ -123,7 +123,8 @@ $default = 'theme' => array('server' => null, 'dir' => null, - 'path'=> null), + 'path'=> null, + 'ssl' => false), 'javascript' => array('server' => null, 'path'=> null), diff --git a/lib/theme.php b/lib/theme.php index 020ce1ac40..bed631d9c0 100644 --- a/lib/theme.php +++ b/lib/theme.php @@ -110,9 +110,9 @@ class Theme $server = common_config('site', 'server'); } - // XXX: protocol + $protocol = common_config('theme', 'ssl') ? 'https' : 'http'; - $this->path = 'http://'.$server.$path.$name; + $this->path = $protocol . '://'.$server.$path.$name; } } From 316ed3f86b60150d66460b478bf7146811bb6bb1 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 11 Feb 2010 16:47:47 -0500 Subject: [PATCH 15/55] null theme ssl setting means 'guess' --- README | 2 ++ lib/theme.php | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README b/README index 9b4147645b..2b021b36ca 100644 --- a/README +++ b/README @@ -1221,6 +1221,8 @@ path: Path part of theme URLs, before the theme name. Relative to the (using version numbers as the path) to make sure that all files are reloaded by caching clients or proxies. Defaults to null, which means to use the site path + '/theme'. +ssl: Whether to use SSL for theme elements. Default is null, which means + guess based on site SSL settings. xmpp ---- diff --git a/lib/theme.php b/lib/theme.php index bed631d9c0..0be8c3b9df 100644 --- a/lib/theme.php +++ b/lib/theme.php @@ -110,7 +110,18 @@ class Theme $server = common_config('site', 'server'); } - $protocol = common_config('theme', 'ssl') ? 'https' : 'http'; + $ssl = common_config('theme', 'ssl'); + + if (is_null($ssl)) { // null -> guess + if (common_config('site', 'ssl') == 'always' && + !common_config('theme', 'server')) { + $ssl = true; + } else { + $ssl = false; + } + } + + $protocol = ($ssl) ? 'https' : 'http'; $this->path = $protocol . '://'.$server.$path.$name; } From 5175b5062ea7635016a392496e8495d03d71a4ae Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 11 Feb 2010 16:48:15 -0500 Subject: [PATCH 16/55] default theme ssl to null --- lib/default.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/default.php b/lib/default.php index 1a2cc4cf6d..fd6831fa9b 100644 --- a/lib/default.php +++ b/lib/default.php @@ -124,7 +124,7 @@ $default = array('server' => null, 'dir' => null, 'path'=> null, - 'ssl' => false), + 'ssl' => null), 'javascript' => array('server' => null, 'path'=> null), From d6869cde7ba7e577d54f0c6ecab3599dc85f0f67 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 11 Feb 2010 16:51:15 -0500 Subject: [PATCH 17/55] let avatars be served over SSL --- README | 2 ++ classes/Avatar.php | 15 +++++++++++++-- lib/default.php | 3 ++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README b/README index 2b021b36ca..9843ab89bc 100644 --- a/README +++ b/README @@ -1192,6 +1192,8 @@ server: If set, defines another server where avatars are stored in the typically only make 2 connections to a single server at a time , so this can parallelize the job. Defaults to null. +ssl: Whether to access avatars using HTTPS. Defaults to null, meaning + to guess based on site-wide SSL settings. public ------ diff --git a/classes/Avatar.php b/classes/Avatar.php index 91bde0f040..dbe2cd8138 100644 --- a/classes/Avatar.php +++ b/classes/Avatar.php @@ -82,9 +82,20 @@ class Avatar extends Memcached_DataObject $server = common_config('site', 'server'); } - // XXX: protocol + $ssl = common_config('avatar', 'ssl'); - return 'http://'.$server.$path.$filename; + if (is_null($ssl)) { // null -> guess + if (common_config('site', 'ssl') == 'always' && + !common_config('avatar', 'server')) { + $ssl = true; + } else { + $ssl = false; + } + } + + $protocol = ($ssl) ? 'https' : 'http'; + + return $protocol.'://'.$server.$path.$filename; } function displayUrl() diff --git a/lib/default.php b/lib/default.php index fd6831fa9b..d19e040367 100644 --- a/lib/default.php +++ b/lib/default.php @@ -111,7 +111,8 @@ $default = 'avatar' => array('server' => null, 'dir' => INSTALLDIR . '/avatar/', - 'path' => $_path . '/avatar/'), + 'path' => $_path . '/avatar/', + 'ssl' => null), 'background' => array('server' => null, 'dir' => INSTALLDIR . '/background/', From ce3c3be1bf971329f82bedbf3aae636e3c8ecbf9 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 10 Feb 2010 14:24:16 -0800 Subject: [PATCH 18/55] Utility classes for atom feeds --- actions/apitimelineuser.php | 39 +++++++- lib/atom10entry.php | 58 ++++++++++++ lib/atom10feed.php | 177 ++++++++++++++++++++++++++++++++++++ lib/atomnoticefeed.php | 34 +++++++ 4 files changed, 305 insertions(+), 3 deletions(-) create mode 100644 lib/atom10entry.php create mode 100644 lib/atom10feed.php create mode 100644 lib/atomnoticefeed.php diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index ed9104905d..bcc48f59c1 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -145,7 +145,26 @@ class ApiTimelineUserAction extends ApiBareAuthAction ); break; case 'atom': + + header('Content-Type: application/atom+xml; charset=utf-8'); + + $atom = new AtomNoticeFeed(); + + $atom->addLink( + common_local_url( + 'showstream', + array('nickname' => $this->user->nickname) + ) + ); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($logo); + $atom->setUpdated('now'); + $id = $this->arg('id'); + if ($id) { $selfuri = common_root_url() . 'api/statuses/user_timeline/' . @@ -154,10 +173,24 @@ class ApiTimelineUserAction extends ApiBareAuthAction $selfuri = common_root_url() . 'api/statuses/user_timeline.atom'; } - $this->showAtomTimeline( - $this->notices, $title, $id, $link, - $subtitle, $suplink, $selfuri, $logo + + $atom->addLink( + $selfuri, + array('rel' => 'self', 'type' => 'application/atom+xml') ); + + $atom->addLink( + $suplink, + array( + 'rel' => 'http://api.friendfeed.com/2008/03#sup', + 'type' => 'application/json' + ) + ); + + $atom->addEntryFromNotices($this->notices); + + print $atom->getString(); + break; case 'json': $this->showJsonTimeline($this->notices); diff --git a/lib/atom10entry.php b/lib/atom10entry.php new file mode 100644 index 0000000000..1b79ce7ad5 --- /dev/null +++ b/lib/atom10entry.php @@ -0,0 +1,58 @@ +namespaces = array(); + } + + function addNamespace($namespace, $uri) + { + $ns = array($namespace => $uri); + $this->namespaces = array_merge($this->namespaces, $ns); + } + + function initEntry() + { + + } + + function endEntry() + { + + } + + function validate + { + + } + + function getString() + { + $this->validate(); + + $this->initEntry(); + $this->renderEntries(); + $this->endEntry(); + + return $this->xw->outputMemory(); + } + +} \ No newline at end of file diff --git a/lib/atom10feed.php b/lib/atom10feed.php new file mode 100644 index 0000000000..9dd8ebc9bc --- /dev/null +++ b/lib/atom10feed.php @@ -0,0 +1,177 @@ +namespaces = array(); + $this->links = array(); + $this->entries = array(); + $this->addNamespace('xmlns', 'http://www.w3.org/2005/Atom'); + } + + function addNamespace($namespace, $uri) + { + $ns = array($namespace => $uri); + $this->namespaces = array_merge($this->namespaces, $ns); + } + + function getNamespaces() + { + return $this->namespaces; + } + + function initFeed() + { + $this->xw->startDocument('1.0', 'UTF-8'); + $commonAttrs = array('xml:lang' => 'en-US'); + $commonAttrs = array_merge($commonAttrs, $this->namespaces); + $this->elementStart('feed', $commonAttrs); + + $this->element('id', null, $this->id); + $this->element('title', null, $this->title); + $this->element('subtitle', null, $this->subtitle); + $this->element('logo', null, $this->logo); + $this->element('updated', null, $this->updated); + + $this->renderLinks(); + } + + /** + * Check that all required elements have been set, etc. + * Throws an Atom10FeedException if something's missing. + * + * @return void + */ + function validate() + { + } + + function renderLinks() + { + foreach ($this->links as $attrs) + { + $this->element('link', $attrs, null); + } + } + + function addEntryRaw($entry) + { + array_push($this->entries, $entry); + } + + function addEntry($entry) + { + array_push($this->entries, $entry->getString()); + } + + function renderEntries() + { + foreach ($this->entries as $entry) { + $this->raw($entry); + } + } + + function endFeed() + { + $this->elementEnd('feed'); + $this->xw->endDocument(); + } + + function getString() + { + $this->validate(); + + $this->initFeed(); + $this->renderEntries(); + $this->endFeed(); + + return $this->xw->outputMemory(); + } + + function setId($id) + { + $this->id = $id; + } + + function setTitle($title) + { + $this->title = $title; + } + + function setSubtitle($subtitle) + { + $this->subtitle = $subtitle; + } + + function setLogo($logo) + { + $this->logo = $logo; + } + + function setUpdated($dt) + { + $this->updated = common_date_iso8601($dt); + } + + function setPublished($dt) + { + $this->published = common_date_iso8601($dt); + } + + /** + * Adds a link element into the Atom document + * + * Assumes you want rel="alternate" and type="text/html" unless + * you send in $otherAttrs. + * + * @param string $uri the uri the href need to point to + * @param array $otherAttrs other attributes to stick in + * + * @return void + */ + function addLink($uri, $otherAttrs = null) { + $attrs = array('href' => $uri); + + if (is_null($otherAttrs)) { + $attrs['rel'] = 'alternate'; + $attrs['type'] = 'text/html'; + } else { + $attrs = array_merge($attrs, $otherAttrs); + } + + array_push($this->links, $attrs); + } + +} + + + diff --git a/lib/atomnoticefeed.php b/lib/atomnoticefeed.php new file mode 100644 index 0000000000..a28c9cda7b --- /dev/null +++ b/lib/atomnoticefeed.php @@ -0,0 +1,34 @@ +addNamespace( + 'xmlns:thr', + 'http://purl.org/syndication/thread/1.0' + ); + } + + function addEntryFromNotices($notices) + { + if (is_array($notices)) { + foreach ($notices as $notice) { + $this->addEntryFromNotice($notice); + } + } else { + while ($notices->fetch()) { + $this->addEntryFromNotice($notice); + } + } + } + + function addEntryFromNotice($notice) + { + $this->addEntryRaw($notice->asAtomEntry()); + } + +} \ No newline at end of file From e2c0f59414dd7e9a33ffbae7307b81a85c2c168b Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 10 Feb 2010 18:55:14 -0800 Subject: [PATCH 19/55] Some upgrades to Atom output for OStatus --- actions/apitimelineuser.php | 2 +- classes/Notice.php | 38 ++++++++++++++++++++++------ classes/Profile.php | 49 +++++++++++++++++++++++++++++++++++++ lib/atom10feed.php | 2 +- lib/atomnoticefeed.php | 18 +++++++++++++- 5 files changed, 98 insertions(+), 11 deletions(-) diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index bcc48f59c1..cb82136195 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -189,7 +189,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction $atom->addEntryFromNotices($this->notices); - print $atom->getString(); + $this->raw($atom->getString()); break; case 'json': diff --git a/classes/Notice.php b/classes/Notice.php index 247440f29c..091f2dc7b4 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -957,7 +957,10 @@ class Notice extends Memcached_DataObject if ($namespace) { $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom', - 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'); + 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0', + 'xmlns:georss' => 'http://www.georss.org/georss', + 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', + 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0'); } else { $attrs = array(); } @@ -983,11 +986,6 @@ class Notice extends Memcached_DataObject $xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE)); } - $xs->elementStart('author'); - $xs->element('name', null, $profile->nickname); - $xs->element('uri', null, $profile->profileurl); - $xs->elementEnd('author'); - if ($source) { $xs->elementEnd('source'); } @@ -995,6 +993,9 @@ class Notice extends Memcached_DataObject $xs->element('title', null, $this->content); $xs->element('summary', null, $this->content); + $xs->raw($profile->asAtomAuthor()); + $xs->raw($profile->asActivityActor($namespace)); + $xs->element('link', array('rel' => 'alternate', 'href' => $this->bestUrl())); @@ -1014,6 +1015,29 @@ class Notice extends Memcached_DataObject } } + if (!empty($this->conversation) + && $this->conversation != $this->notice->id) { + $xs->element( + 'link', array( + 'rel' => 'osatus:conversation', + 'href' => common_local_url( + 'conversation', + array('id' => $this->conversation) + ) + ) + ); + } + + if (!empty($this->repeat_of)) { + $repeat = Notice::staticGet('id', $this->repeat_of); + if (!empty($repeat)) { + $xs->element( + 'ostatus:forward', + array('ref' => $repeat->uri, 'href' => $repeat->bestUrl()) + ); + } + } + $xs->element('content', array('type' => 'html'), $this->rendered); $tag = new Notice_tag(); @@ -1041,9 +1065,7 @@ class Notice extends Memcached_DataObject } if (!empty($this->lat) && !empty($this->lon)) { - $xs->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss')); $xs->element('georss:point', null, $this->lat . ' ' . $this->lon); - $xs->elementEnd('geo'); } $xs->elementEnd('entry'); diff --git a/classes/Profile.php b/classes/Profile.php index feabc25087..664c45f640 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -754,4 +754,53 @@ class Profile extends Memcached_DataObject return !empty($notice); } + + function asAtomAuthor() + { + $xs = new XMLStringer(true); + + $xs->elementStart('author'); + $xs->element('name', null, $this->nickname); + $xs->element('uri', null, $this->profileurl); + $xs->elementEnd('author'); + + return $xs->getString(); + } + + function asActivityActor() + { + $xs = new XMLStringer(true); + + $xs->elementStart('activity:actor'); + $xs->element( + 'activity:object-type', + null, + 'http://activitystrea.ms/schema/1.0/person' + ); + $xs->element( + 'id', + null, + common_local_url( + 'userbyid', + array('id' => $this->id) + ) + ); + $xs->element('title', null, $this->getBestName()); + + $avatar = $this->getAvatar(AVATAR_PROFILE_SIZE); + + $xs->element( + 'link', array( + 'type' => empty($avatar) ? 'image/png' : $avatar->mediatype, + 'href' => empty($avatar) + ? Avatar::defaultImage(AVATAR_PROFILE_SIZE) + : $avatar->displayUrl() + ), + '' + ); + + $xs->elementEnd('activity:actor'); + + return $xs->getString(); + } } diff --git a/lib/atom10feed.php b/lib/atom10feed.php index 9dd8ebc9bc..01fc69072c 100644 --- a/lib/atom10feed.php +++ b/lib/atom10feed.php @@ -153,7 +153,7 @@ class Atom10Feed extends XMLStringer * Assumes you want rel="alternate" and type="text/html" unless * you send in $otherAttrs. * - * @param string $uri the uri the href need to point to + * @param string $uri the uri the href needs to point to * @param array $otherAttrs other attributes to stick in * * @return void diff --git a/lib/atomnoticefeed.php b/lib/atomnoticefeed.php index a28c9cda7b..ce87ec9e64 100644 --- a/lib/atomnoticefeed.php +++ b/lib/atomnoticefeed.php @@ -5,12 +5,28 @@ class AtomNoticeFeed extends Atom10Feed function __construct($indent = true) { parent::__construct($indent); - // Feeds containing notice info use the Atom Threading Extensions + // Feeds containing notice info use these namespaces $this->addNamespace( 'xmlns:thr', 'http://purl.org/syndication/thread/1.0' ); + + $this->addNamespace( + 'xmlns:georss', + 'http://www.georss.org/georss' + ); + + $this->addNamespace( + 'xmlns:activity', + 'http://activitystrea.ms/spec/1.0/' + ); + + // XXX: What should the uri be? + $this->addNamespace( + 'xmlns:ostatus', + 'http://ostatus.org/schema/1.0' + ); } function addEntryFromNotices($notices) From c8d5c8442fe6ce54f7f65d1d0eb4203b06c09583 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 10 Feb 2010 21:21:42 -0800 Subject: [PATCH 20/55] Added some boilerplate class comments, etc. --- lib/atom10entry.php | 48 ++++++++++++++++++++++++++++++++++ lib/atom10feed.php | 58 +++++++++++++++++++++++++++++++++++++----- lib/atomnoticefeed.php | 55 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 154 insertions(+), 7 deletions(-) diff --git a/lib/atom10entry.php b/lib/atom10entry.php index 1b79ce7ad5..5710c80fc5 100644 --- a/lib/atom10entry.php +++ b/lib/atom10entry.php @@ -1,9 +1,51 @@ . + * + * @category Feed + * @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/ + */ + +if (!defined('STATUSNET') +{ + exit(1); +} class Atom10EntryException extends Exception { } +/** + * Class for manipulating an Atom entry in memory. Get the entry as an XML + * string with Atom10Entry::getString(). + * + * @category Feed + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ class Atom10Entry extends XMLStringer { private $namespaces; @@ -39,6 +81,12 @@ class Atom10Entry extends XMLStringer } + /** + * Check that all required elements have been set, etc. + * Throws an Atom10EntryException if something's missing. + * + * @return void + */ function validate { diff --git a/lib/atom10feed.php b/lib/atom10feed.php index 01fc69072c..a37f6521ad 100644 --- a/lib/atom10feed.php +++ b/lib/atom10feed.php @@ -1,9 +1,51 @@ . + * + * @category Feed + * @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/ + */ + +if (!defined('STATUSNET') +{ + exit(1); +} class Atom10FeedException extends Exception { } +/** + * Class for building an Atom feed in memory. Get the finished doc + * as a string with Atom10Feed::getString(). + * + * @category Feed + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ class Atom10Feed extends XMLStringer { public $xw; @@ -23,12 +65,11 @@ class Atom10Feed extends XMLStringer private $entries; /** - * undocumented function + * Constructor * - * @param array $entries an array of FeedItems + * @param boolean $indent flag to turn indenting on or off * * @return void - * */ function __construct($indent = true) { parent::__construct($indent); @@ -38,6 +79,14 @@ class Atom10Feed extends XMLStringer $this->addNamespace('xmlns', 'http://www.w3.org/2005/Atom'); } + /** + * Add another namespace to the feed + * + * @param string $namespace the namespace + * @param string $uri namspace uri + * + * @return void + */ function addNamespace($namespace, $uri) { $ns = array($namespace => $uri); @@ -172,6 +221,3 @@ class Atom10Feed extends XMLStringer } } - - - diff --git a/lib/atomnoticefeed.php b/lib/atomnoticefeed.php index ce87ec9e64..a626ab549b 100644 --- a/lib/atomnoticefeed.php +++ b/lib/atomnoticefeed.php @@ -1,5 +1,47 @@ . + * + * @category Feed + * @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/ + */ +if (!defined('STATUSNET') +{ + exit(1); +} + +/** + * Class for creating a feed that represents a collection of notices. Builds the + * feed in memory. Get the feed as a string with AtomNoticeFeed::getString(). + * + * @category Feed + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ class AtomNoticeFeed extends Atom10Feed { function __construct($indent = true) { @@ -29,6 +71,12 @@ class AtomNoticeFeed extends Atom10Feed ); } + /** + * Add more than one Notice to the feed + * + * @param mixed $notices an array of Notice objects or handle + * + */ function addEntryFromNotices($notices) { if (is_array($notices)) { @@ -42,9 +90,14 @@ class AtomNoticeFeed extends Atom10Feed } } + /** + * Add a single Notice to the feed + * + * @param Notice $notice a Notice to add + */ function addEntryFromNotice($notice) { $this->addEntryRaw($notice->asAtomEntry()); } -} \ No newline at end of file +} From c465f675d9dbcf9f808bc31a1d01e753df4ddf58 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 11 Feb 2010 13:54:40 -0800 Subject: [PATCH 21/55] Make Atom timelines in the API use Atom10feed --- actions/apitimelinefavorites.php | 69 ++++++++++++++++++++------- actions/apitimelinefriends.php | 74 +++++++++++++++++++++-------- actions/apitimelinehome.php | 60 ++++++++++++++++------- actions/apitimelinementions.php | 34 +++++++++++-- actions/apitimelinepublic.php | 27 +++++++++-- actions/apitimelineretweetsofme.php | 36 ++++++++++++-- actions/apitimelinetag.php | 51 +++++++++++++++----- actions/apitimelineuser.php | 25 ++++------ lib/api.php | 18 +++++++ lib/atom10feed.php | 8 +++- lib/atomnoticefeed.php | 4 +- 11 files changed, 308 insertions(+), 98 deletions(-) diff --git a/actions/apitimelinefavorites.php b/actions/apitimelinefavorites.php index 1027d97d44..f7f900ddfb 100644 --- a/actions/apitimelinefavorites.php +++ b/actions/apitimelinefavorites.php @@ -100,11 +100,11 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction function showTimeline() { - $profile = $this->user->getProfile(); - $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); + $profile = $this->user->getProfile(); + $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); - $sitename = common_config('site', 'name'); - $title = sprintf( + $sitename = common_config('site', 'name'); + $title = sprintf( _('%1$s / Favorites from %2$s'), $sitename, $this->user->nickname @@ -112,32 +112,69 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction $taguribase = common_config('integration', 'taguri'); $id = "tag:$taguribase:Favorites:" . $this->user->id; - $link = common_local_url( - 'favorites', - array('nickname' => $this->user->nickname) - ); - $subtitle = sprintf( + + $subtitle = sprintf( _('%1$s updates favorited by %2$s / %2$s.'), $sitename, $profile->getBestName(), $this->user->nickname ); - $logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE); + $logo = !empty($avatar) + ? $avatar->displayUrl() + : Avatar::defaultImage(AVATAR_PROFILE_SIZE); switch($this->format) { case 'xml': $this->showXmlTimeline($this->notices); break; case 'rss': - $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo); + $link = common_local_url( + 'showfavorites', + array('nickname' => $this->user->nickname) + ); + $this->showRssTimeline( + $this->notices, + $title, + $link, + $subtitle, + null, + $logo + ); break; case 'atom': - $selfuri = common_root_url() . - ltrim($_SERVER['QUERY_STRING'], 'p='); - $this->showAtomTimeline( - $this->notices, $title, $id, $link, $subtitle, - null, $selfuri, $logo + + header('Content-Type: application/atom+xml; charset=utf-8'); + + $atom = new AtomNoticeFeed(); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($logo); + $atom->setUpdated('now'); + + $atom->addLink( + common_local_url( + 'showfavorites', + array('nickname' => $this->user->nickname) + ) ); + + $id = $this->arg('id'); + $aargs = array('format' => 'atom'); + if (!empty($id)) { + $aargs['id'] = $id; + } + + $atom->addLink( + $this->getSelfUri('ApiTimelineFavorites', $aargs), + array('rel' => 'self', 'type' => 'application/atom+xml') + ); + + $atom->addEntryFromNotices($this->notices); + + $this->raw($atom->getString()); + break; case 'json': $this->showJsonTimeline($this->notices); diff --git a/actions/apitimelinefriends.php b/actions/apitimelinefriends.php index 4e3827baea..0af04fe4fb 100644 --- a/actions/apitimelinefriends.php +++ b/actions/apitimelinefriends.php @@ -114,39 +114,71 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction $title = sprintf(_("%s and friends"), $this->user->nickname); $taguribase = common_config('integration', 'taguri'); $id = "tag:$taguribase:FriendsTimeline:" . $this->user->id; - $link = common_local_url( - 'all', array('nickname' => $this->user->nickname) - ); - $subtitle = sprintf( - _('Updates from %1$s and friends on %2$s!'), - $this->user->nickname, $sitename - ); - $logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE); + + $subtitle = sprintf( + _('Updates from %1$s and friends on %2$s!'), + $this->user->nickname, $sitename + ); + + $logo = (!empty($avatar)) + ? $avatar->displayUrl() + : Avatar::defaultImage(AVATAR_PROFILE_SIZE); switch($this->format) { case 'xml': $this->showXmlTimeline($this->notices); break; case 'rss': - $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo); + + $link = common_local_url( + 'all', array( + 'nickname' => $this->user->nickname + ) + ); + + $this->showRssTimeline( + $this->notices, + $title, + $link, + $subtitle, + null, + $logo + ); break; case 'atom': - $target_id = $this->arg('id'); + header('Content-Type: application/atom+xml; charset=utf-8'); - if (isset($target_id)) { - $selfuri = common_root_url() . - 'api/statuses/friends_timeline/' . - $target_id . '.atom'; - } else { - $selfuri = common_root_url() . - 'api/statuses/friends_timeline.atom'; + $atom = new AtomNoticeFeed(); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($logo); + $atom->setUpdated('now'); + + $atom->addLink( + common_local_url( + 'all', + array('nickname' => $this->user->nickname) + ) + ); + + $id = $this->arg('id'); + $aargs = array('format' => 'atom'); + if (!empty($id)) { + $aargs['id'] = $id; } - $this->showAtomTimeline( - $this->notices, $title, $id, $link, - $subtitle, null, $selfuri, $logo - ); + $atom->addLink( + $this->getSelfUri('ApiTimelineFriends', $aargs), + array('rel' => 'self', 'type' => 'application/atom+xml') + ); + + $atom->addEntryFromNotices($this->notices); + + $this->raw($atom->getString()); + break; case 'json': $this->showJsonTimeline($this->notices); diff --git a/actions/apitimelinehome.php b/actions/apitimelinehome.php index 828eae6cf6..ae41680702 100644 --- a/actions/apitimelinehome.php +++ b/actions/apitimelinehome.php @@ -115,39 +115,67 @@ class ApiTimelineHomeAction extends ApiBareAuthAction $title = sprintf(_("%s and friends"), $this->user->nickname); $taguribase = common_config('integration', 'taguri'); $id = "tag:$taguribase:HomeTimeline:" . $this->user->id; - $link = common_local_url( - 'all', array('nickname' => $this->user->nickname) - ); + $subtitle = sprintf( _('Updates from %1$s and friends on %2$s!'), $this->user->nickname, $sitename ); - $logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE); + + $logo = (!empty($avatar)) + ? $avatar->displayUrl() + : Avatar::defaultImage(AVATAR_PROFILE_SIZE); switch($this->format) { case 'xml': $this->showXmlTimeline($this->notices); break; case 'rss': - $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo); + $link = common_local_url( + 'all', + array('nickname' => $this->user->nickname) + ); + $this->showRssTimeline( + $this->notices, + $title, + $link, + $subtitle, + null, + $logo + ); break; case 'atom': - $target_id = $this->arg('id'); + header('Content-Type: application/atom+xml; charset=utf-8'); - if (isset($target_id)) { - $selfuri = common_root_url() . - 'api/statuses/home_timeline/' . - $target_id . '.atom'; - } else { - $selfuri = common_root_url() . - 'api/statuses/home_timeline.atom'; + $atom = new AtomNoticeFeed(); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($logo); + $atom->setUpdated('now'); + + $atom->addLink( + common_local_url( + 'all', + array('nickname' => $this->user->nickname) + ) + ); + + $id = $this->arg('id'); + $aargs = array('format' => 'atom'); + if (!empty($id)) { + $aargs['id'] = $id; } - $this->showAtomTimeline( - $this->notices, $title, $id, $link, - $subtitle, null, $selfuri, $logo + $atom->addLink( + $this->getSelfUri('ApiTimelineHome', $aargs), + array('rel' => 'self', 'type' => 'application/atom+xml') ); + + $atom->addEntryFromNotices($this->notices); + $this->raw($atom->getString()); + break; case 'json': $this->showJsonTimeline($this->notices); diff --git a/actions/apitimelinementions.php b/actions/apitimelinementions.php index 9dc2162cc4..d2e31d0bdd 100644 --- a/actions/apitimelinementions.php +++ b/actions/apitimelinementions.php @@ -137,12 +137,36 @@ class ApiTimelineMentionsAction extends ApiBareAuthAction $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo); break; case 'atom': - $selfuri = common_root_url() . - ltrim($_SERVER['QUERY_STRING'], 'p='); - $this->showAtomTimeline( - $this->notices, $title, $id, $link, $subtitle, - null, $selfuri, $logo + + $atom = new AtomNoticeFeed(); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($logo); + $atom->setUpdated('now'); + + $atom->addLink( + common_local_url( + 'replies', + array('nickname' => $this->user->nickname) + ) ); + + $id = $this->arg('id'); + $aargs = array('format' => 'atom'); + if (!empty($id)) { + $aargs['id'] = $id; + } + + $atom->addLink( + $this->getSelfUri('ApiTimelineMentions', $aargs), + array('rel' => 'self', 'type' => 'application/atom+xml') + ); + + $atom->addEntryFromNotices($this->notices); + $this->raw($atom->getString()); + break; case 'json': $this->showJsonTimeline($this->notices); diff --git a/actions/apitimelinepublic.php b/actions/apitimelinepublic.php index 0fb0788e98..c1fa72a3ee 100644 --- a/actions/apitimelinepublic.php +++ b/actions/apitimelinepublic.php @@ -74,7 +74,7 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction parent::prepare($args); $this->notices = $this->getNotices(); - + if ($this->since) { throw new ServerException("since parameter is disabled for performance; use since_id", 403); } @@ -122,11 +122,28 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo); break; case 'atom': - $selfuri = common_root_url() . 'api/statuses/public_timeline.atom'; - $this->showAtomTimeline( - $this->notices, $title, $id, $link, - $subtitle, null, $selfuri, $sitelogo + + $atom = new AtomNoticeFeed(); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($sitelogo); + $atom->setUpdated('now'); + + $atom->addLink(common_local_url('public')); + + $atom->addLink( + $this->getSelfUri( + 'ApiTimelinePublic', array('format' => 'atom') + ), + array('rel' => 'self', 'type' => 'application/atom+xml') ); + + $atom->addEntryFromNotices($this->notices); + + $this->raw($atom->getString()); + break; case 'json': $this->showJsonTimeline($this->notices); diff --git a/actions/apitimelineretweetsofme.php b/actions/apitimelineretweetsofme.php index e4b09e9bda..26706a75e7 100644 --- a/actions/apitimelineretweetsofme.php +++ b/actions/apitimelineretweetsofme.php @@ -99,6 +99,8 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction $strm = $this->auth_user->repeatsOfMe($offset, $limit, $this->since_id, $this->max_id); + common_debug(var_export($strm, true)); + switch ($this->format) { case 'xml': $this->showXmlTimeline($strm); @@ -112,10 +114,38 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction $title = sprintf(_("Repeats of %s"), $this->auth_user->nickname); $taguribase = common_config('integration', 'taguri'); $id = "tag:$taguribase:RepeatsOfMe:" . $this->auth_user->id; - $link = common_local_url('showstream', - array('nickname' => $this->auth_user->nickname)); - $this->showAtomTimeline($strm, $title, $id, $link); + header('Content-Type: application/atom+xml; charset=utf-8'); + + $atom = new AtomNoticeFeed(); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setUpdated('now'); + + $atom->addLink( + common_local_url( + 'showstream', + array('nickname' => $this->auth_user->nickname) + ) + ); + + $id = $this->arg('id'); + $aargs = array('format' => 'atom'); + if (!empty($id)) { + $aargs['id'] = $id; + } + + $atom->addLink( + $this->getSelfUri('ApiTimelineRetweetsOfMe', $aargs), + array('rel' => 'self', 'type' => 'application/atom+xml') + ); + + $atom->addEntryFromNotices($strm); + + $this->raw($atom->getString()); + break; default: diff --git a/actions/apitimelinetag.php b/actions/apitimelinetag.php index 1427d23b6a..5b6ded4c04 100644 --- a/actions/apitimelinetag.php +++ b/actions/apitimelinetag.php @@ -100,10 +100,6 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction $sitename = common_config('site', 'name'); $sitelogo = (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png'); $title = sprintf(_("Notices tagged with %s"), $this->tag); - $link = common_local_url( - 'tag', - array('tag' => $this->tag) - ); $subtitle = sprintf( _('Updates tagged with %1$s on %2$s!'), $this->tag, @@ -117,22 +113,51 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction $this->showXmlTimeline($this->notices); break; case 'rss': - $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo); - break; - case 'atom': - $selfuri = common_root_url() . - 'api/statusnet/tags/timeline/' . - $this->tag . '.atom'; - $this->showAtomTimeline( + $link = common_local_url( + 'tag', + array('tag' => $this->tag) + ); + $this->showRssTimeline( $this->notices, $title, - $id, $link, $subtitle, null, - $selfuri, $sitelogo ); + break; + case 'atom': + + header('Content-Type: application/atom+xml; charset=utf-8'); + + $atom = new AtomNoticeFeed(); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($logo); + $atom->setUpdated('now'); + + $atom->addLink( + common_local_url( + 'tag', + array('tag' => $this->tag) + ) + ); + + $aargs = array('format' => 'atom'); + if (!empty($this->tag)) { + $aargs['tag'] = $this->tag; + } + + $atom->addLink( + $this->getSelfUri('ApiTimelineTag', $aargs), + array('rel' => 'self', 'type' => 'application/atom+xml') + ); + + $atom->addEntryFromNotices($this->notices); + $this->raw($atom->getString()); + break; case 'json': $this->showJsonTimeline($this->notices); diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index cb82136195..d20bb0d202 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -150,6 +150,12 @@ class ApiTimelineUserAction extends ApiBareAuthAction $atom = new AtomNoticeFeed(); + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($logo); + $atom->setUpdated('now'); + $atom->addLink( common_local_url( 'showstream', @@ -157,25 +163,14 @@ class ApiTimelineUserAction extends ApiBareAuthAction ) ); - $atom->setId($id); - $atom->setTitle($title); - $atom->setSubtitle($subtitle); - $atom->setLogo($logo); - $atom->setUpdated('now'); - $id = $this->arg('id'); - - if ($id) { - $selfuri = common_root_url() . - 'api/statuses/user_timeline/' . - rawurlencode($id) . '.atom'; - } else { - $selfuri = common_root_url() . - 'api/statuses/user_timeline.atom'; + $aargs = array('format' => 'atom'); + if (!empty($id)) { + $aargs['id'] = $id; } $atom->addLink( - $selfuri, + $this->getSelfUri('ApiTimelineUser', $aargs), array('rel' => 'self', 'type' => 'application/atom+xml') ); diff --git a/lib/api.php b/lib/api.php index fd07bbbbe0..8f1fe1ef71 100644 --- a/lib/api.php +++ b/lib/api.php @@ -1321,4 +1321,22 @@ class ApiAction extends Action } } + function getSelfUri($action, $aargs) + { + parse_str($_SERVER['QUERY_STRING'], $params); + $pstring = ''; + if (!empty($params)) { + unset($params['p']); + $pstring = http_build_query($params); + } + + $uri = common_local_url($action, $aargs); + + if (!empty($pstring)) { + $uri .= '?' . $pstring; + } + + return $uri; + } + } diff --git a/lib/atom10feed.php b/lib/atom10feed.php index a37f6521ad..ccca76a09e 100644 --- a/lib/atom10feed.php +++ b/lib/atom10feed.php @@ -27,7 +27,7 @@ * @link http://status.net/ */ -if (!defined('STATUSNET') +if (!defined('STATUSNET')) { exit(1); } @@ -108,7 +108,11 @@ class Atom10Feed extends XMLStringer $this->element('id', null, $this->id); $this->element('title', null, $this->title); $this->element('subtitle', null, $this->subtitle); - $this->element('logo', null, $this->logo); + + if (!empty($this->logo)) { + $this->element('logo', null, $this->logo); + } + $this->element('updated', null, $this->updated); $this->renderLinks(); diff --git a/lib/atomnoticefeed.php b/lib/atomnoticefeed.php index a626ab549b..34ed44b2ed 100644 --- a/lib/atomnoticefeed.php +++ b/lib/atomnoticefeed.php @@ -27,7 +27,7 @@ * @link http://status.net/ */ -if (!defined('STATUSNET') +if (!defined('STATUSNET')) { exit(1); } @@ -85,7 +85,7 @@ class AtomNoticeFeed extends Atom10Feed } } else { while ($notices->fetch()) { - $this->addEntryFromNotice($notice); + $this->addEntryFromNotice($notices); } } } From b96af33d978bddfa66aa893ff1d59f2d83903afa Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 11 Feb 2010 16:59:39 -0500 Subject: [PATCH 22/55] put Javascript files under SSL --- README | 11 +++++++++++ lib/default.php | 3 ++- lib/htmloutputter.php | 15 +++++++++++++-- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/README b/README index 9843ab89bc..7531df9971 100644 --- a/README +++ b/README @@ -1226,6 +1226,17 @@ path: Path part of theme URLs, before the theme name. Relative to the ssl: Whether to use SSL for theme elements. Default is null, which means guess based on site SSL settings. +javascript +---------- + +server: You can speed up page loading by pointing the + theme file lookup to another server (virtual or real). + Defaults to NULL, meaning to use the site server. +path: Path part of Javascript URLs. Defaults to null, + which means to use the site path + '/js/'. +ssl: Whether to use SSL for JavaScript files. Default is null, which means + guess based on site SSL settings. + xmpp ---- diff --git a/lib/default.php b/lib/default.php index d19e040367..8a21271b8f 100644 --- a/lib/default.php +++ b/lib/default.php @@ -128,7 +128,8 @@ $default = 'ssl' => null), 'javascript' => array('server' => null, - 'path'=> null), + 'path'=> null, + 'ssl' => null), 'throttle' => array('enabled' => false, // whether to throttle edits; false by default 'count' => 20, // number of allowed messages in timespan diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php index 317f5ea612..47e56fc8f8 100644 --- a/lib/htmloutputter.php +++ b/lib/htmloutputter.php @@ -376,9 +376,20 @@ class HTMLOutputter extends XMLOutputter $server = common_config('site', 'server'); } - // XXX: protocol + $ssl = common_config('javascript', 'ssl'); - $src = 'http://'.$server.$path.$src . '?version=' . STATUSNET_VERSION; + if (is_null($ssl)) { // null -> guess + if (common_config('site', 'ssl') == 'always' && + !common_config('javascript', 'server')) { + $ssl = true; + } else { + $ssl = false; + } + } + + $protocol = ($ssl) ? 'https' : 'http'; + + $src = $protocol.'://'.$server.$path.$src . '?version=' . STATUSNET_VERSION; } $this->element('script', array('type' => $type, From 3018683718bd73bf00472622f9e81914703d50a7 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 11 Feb 2010 17:03:31 -0500 Subject: [PATCH 23/55] let backgrounds be put under SSL --- README | 2 ++ classes/Design.php | 15 +++++++++++++-- lib/default.php | 3 ++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README b/README index 7531df9971..3b2baaeebf 100644 --- a/README +++ b/README @@ -1521,6 +1521,8 @@ dir: directory to write backgrounds too. Default is '/background/' subdir of install dir. path: path to backgrounds. Default is sub-path of install path; note that you may need to change this if you change site-path too. +ssl: Whether or not to use HTTPS for background files. Defaults to + null, meaning to guess from site-wide SSL settings. ping ---- diff --git a/classes/Design.php b/classes/Design.php index 4e7d7dfb25..ff44e01096 100644 --- a/classes/Design.php +++ b/classes/Design.php @@ -155,9 +155,20 @@ class Design extends Memcached_DataObject $server = common_config('site', 'server'); } - // XXX: protocol + $ssl = common_config('background', 'ssl'); - return 'http://'.$server.$path.$filename; + if (is_null($ssl)) { // null -> guess + if (common_config('site', 'ssl') == 'always' && + !common_config('background', 'server')) { + $ssl = true; + } else { + $ssl = false; + } + } + + $protocol = ($ssl) ? 'https' : 'http'; + + return $protocol.'://'.$server.$path.$filename; } function setDisposition($on, $off, $tile) diff --git a/lib/default.php b/lib/default.php index 8a21271b8f..0822654f62 100644 --- a/lib/default.php +++ b/lib/default.php @@ -116,7 +116,8 @@ $default = 'background' => array('server' => null, 'dir' => INSTALLDIR . '/background/', - 'path' => $_path . '/background/'), + 'path' => $_path . '/background/', + 'ssl' => null), 'public' => array('localonly' => true, 'blacklist' => array(), From 31461e120f23416c8c4979805900e3018fb2a6fd Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 11 Feb 2010 17:06:57 -0500 Subject: [PATCH 24/55] let files go to SSL dir too --- README | 2 ++ classes/File.php | 15 +++++++++++++-- lib/default.php | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README b/README index 3b2baaeebf..75336eb83f 100644 --- a/README +++ b/README @@ -1462,6 +1462,8 @@ server: server name to use when creating URLs for uploaded files. a virtual server here can speed up Web performance. path: URL path, relative to the server, to find files. Defaults to main path + '/file/'. +ssl: whether to use HTTPS for file URLs. Defaults to null, meaning to + guess based on other SSL settings. filecommand: command to use for determining the type of a file. May be skipped if fileinfo extension is installed. Defaults to '/usr/bin/file'. diff --git a/classes/File.php b/classes/File.php index ee418a8024..91b12d2e28 100644 --- a/classes/File.php +++ b/classes/File.php @@ -228,9 +228,20 @@ class File extends Memcached_DataObject $server = common_config('site', 'server'); } - // XXX: protocol + $ssl = common_config('attachments', 'ssl'); - return 'http://'.$server.$path.$filename; + if (is_null($ssl)) { // null -> guess + if (common_config('site', 'ssl') == 'always' && + !common_config('attachments', 'server')) { + $ssl = true; + } else { + $ssl = false; + } + } + + $protocol = ($ssl) ? 'https' : 'http'; + + return $protocol.'://'.$server.$path.$filename; } } diff --git a/lib/default.php b/lib/default.php index 0822654f62..8b1fe27694 100644 --- a/lib/default.php +++ b/lib/default.php @@ -188,6 +188,7 @@ $default = array('server' => null, 'dir' => INSTALLDIR . '/file/', 'path' => $_path . '/file/', + 'ssl' => null, 'supported' => array('image/png', 'image/jpeg', 'image/gif', From e08657d56cc10d2cf45e9e5701856d8a0dc7351e Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Thu, 11 Feb 2010 22:42:36 +0000 Subject: [PATCH 25/55] OStatus: correct parsing of georss:point for max interop (commas allowed, whitespace not strictly defined) --- plugins/OStatus/lib/feedmunger.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php index 25b0a09317..927a2fe7a7 100644 --- a/plugins/OStatus/lib/feedmunger.php +++ b/plugins/OStatus/lib/feedmunger.php @@ -269,6 +269,9 @@ class FeedMunger } /** + * Parse location given as a GeoRSS-simple point, if provided. + * http://www.georss.org/simple + * * @param feed item $entry * @return mixed Location or false */ @@ -278,7 +281,10 @@ class FeedMunger $points = $dom->getElementsByTagNameNS('http://www.georss.org/georss', 'point'); for ($i = 0; $i < $points->length; $i++) { - $point = trim($points->item(0)->textContent); + $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; From 8e6b52e8994ce9a3180554f999bdc89b414fc892 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Feb 2010 00:22:16 +0000 Subject: [PATCH 26/55] OStatus: renamed feedinfo table to ostatus_profile -- will cover remote ostatus people and groups whether a subscription's active or not (maintains identity over unsub/resub, and between subscribers and subscribees) --- plugins/OStatus/OStatusPlugin.php | 6 +-- plugins/OStatus/actions/feedsubsettings.php | 20 ++++---- plugins/OStatus/actions/ostatussub.php | 16 +++--- plugins/OStatus/actions/pushcallback.php | 22 ++++----- .../{Feedinfo.php => Ostatus_profile.php} | 49 +++++++++++-------- plugins/OStatus/lib/feedmunger.php | 18 +++---- 6 files changed, 68 insertions(+), 63 deletions(-) rename plugins/OStatus/classes/{Feedinfo.php => Ostatus_profile.php} (92%) diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index c0f9dadc4a..8444c3d73d 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -251,14 +251,14 @@ class OStatusPlugin extends Plugin */ function onEndUnsubscribe($user, $other) { - $feed = Feedinfo::staticGet('profile_id', $other->id); + $profile = Ostatus_profile::staticGet('profile_id', $other->id); if ($feed) { $sub = new Subscription(); $sub->subscribed = $other->id; $sub->limit(1); if (!$sub->find(true)) { common_log(LOG_INFO, "Unsubscribing from now-unused feed $feed->feeduri on hub $feed->huburi"); - $feed->unsubscribe(); + $profile->unsubscribe(); } } return true; @@ -269,7 +269,7 @@ class OStatusPlugin extends Plugin */ function onCheckSchema() { $schema = Schema::get(); - $schema->ensureTable('feedinfo', Feedinfo::schemaDef()); + $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef()); $schema->ensureTable('hubsub', HubSub::schemaDef()); return true; } diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php index 6f592bf5b0..af8bf4d25e 100644 --- a/plugins/OStatus/actions/feedsubsettings.php +++ b/plugins/OStatus/actions/feedsubsettings.php @@ -182,9 +182,9 @@ class FeedSubSettingsAction extends ConnectSettingsAction } $this->munger = $discover->feedMunger(); - $this->feedinfo = $this->munger->feedInfo(); + $this->profile = $this->munger->ostatusProfile(); - if ($this->feedinfo->huburi == '' && !common_config('feedsub', 'nohub')) { + if ($this->profile->huburi == '' && !common_config('feedsub', 'nohub')) { $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.')); return false; } @@ -196,13 +196,13 @@ class FeedSubSettingsAction extends ConnectSettingsAction { if ($this->validateFeed()) { $this->preview = true; - $this->feedinfo = Feedinfo::ensureProfile($this->munger); + $this->profile = Ostatus_profile::ensureProfile($this->munger); // If not already in use, subscribe to updates via the hub - if ($this->feedinfo->sub_start) { - common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}"); + if ($this->profile->sub_start) { + common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}"); } else { - $ok = $this->feedinfo->subscribe(); + $ok = $this->profile->subscribe(); common_log(LOG_INFO, __METHOD__ . ": sub was $ok"); if (!$ok) { $this->showForm(_m('Feed subscription failed! Bad response from hub.')); @@ -212,15 +212,15 @@ class FeedSubSettingsAction extends ConnectSettingsAction // And subscribe the current user to the local profile $user = common_current_user(); - $profile = $this->feedinfo->getProfile(); + $profile = $this->profile->getLocalProfile(); if (!$profile) { throw new ServerException("Feed profile was not saved properly."); } - if ($this->feedinfo->isGroup()) { + if ($this->profile->isGroup()) { if ($user->isMember($profile)) { $this->showForm(_m('Already a member!')); - } elseif (Group_member::join($this->feedinfo->group_id, $user->id)) { + } elseif (Group_member::join($this->profile->group_id, $user->id)) { $this->showForm(_m('Joined remote group!')); } else { $this->showForm(_m('Remote group join failed!')); @@ -247,7 +247,7 @@ class FeedSubSettingsAction extends ConnectSettingsAction function previewFeed() { - $feedinfo = $this->munger->feedinfo(); + $profile = $this->munger->ostatusProfile(); $notice = $this->munger->notice(0, true); // preview if ($notice) { diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php index ffc4ae8dfe..9774286fdd 100644 --- a/plugins/OStatus/actions/ostatussub.php +++ b/plugins/OStatus/actions/ostatussub.php @@ -164,9 +164,9 @@ class OStatusSubAction extends Action } $this->munger = $discover->feedMunger(); - $this->feedinfo = $this->munger->feedInfo(); + $this->profile = $this->munger->ostatusProfile(); - if ($this->feedinfo->huburi == '') { + if ($this->profile->huburi == '') { $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.')); return false; } @@ -178,13 +178,13 @@ class OStatusSubAction extends Action { if ($this->validateFeed()) { $this->preview = true; - $this->feedinfo = Feedinfo::ensureProfile($this->munger); + $this->profile = Ostatus_profile::ensureProfile($this->munger); // If not already in use, subscribe to updates via the hub - if ($this->feedinfo->sub_start) { - common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}"); + if ($this->profile->sub_start) { + common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}"); } else { - $ok = $this->feedinfo->subscribe(); + $ok = $this->profile->subscribe(); common_log(LOG_INFO, __METHOD__ . ": sub was $ok"); if (!$ok) { $this->showForm(_m('Feed subscription failed! Bad response from hub.')); @@ -194,7 +194,7 @@ class OStatusSubAction extends Action // And subscribe the current user to the local profile $user = common_current_user(); - $profile = $this->feedinfo->getProfile(); + $profile = $this->profile->getProfile(); if ($user->isSubscribed($profile)) { $this->showForm(_m('Already subscribed!')); @@ -209,7 +209,7 @@ class OStatusSubAction extends Action function previewFeed() { - $feedinfo = $this->munger->feedinfo(); + $profile = $this->munger->ostatusProfile(); $notice = $this->munger->notice(0, true); // preview if ($notice) { diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php index 471d079ab9..a446593ff9 100644 --- a/plugins/OStatus/actions/pushcallback.php +++ b/plugins/OStatus/actions/pushcallback.php @@ -48,9 +48,9 @@ class PushCallbackAction extends Action throw new ServerException('Empty or invalid feed id', 400); } - $feedinfo = Feedinfo::staticGet('id', $feedid); - if (!$feedinfo) { - throw new ServerException('Unknown feed id ' . $feedid, 400); + $profile = Ostatus_profile::staticGet('id', $feedid); + if (!$profile) { + throw new ServerException('Unknown OStatus/PuSH feed id ' . $feedid, 400); } $hmac = ''; @@ -59,7 +59,7 @@ class PushCallbackAction extends Action } $post = file_get_contents('php://input'); - $feedinfo->postUpdates($post, $hmac); + $profile->postUpdates($post, $hmac); } /** @@ -78,8 +78,8 @@ class PushCallbackAction extends Action throw new ServerException("Bogus hub callback: bad mode", 404); } - $feedinfo = Feedinfo::staticGet('feeduri', $topic); - if (!$feedinfo) { + $profile = Ostatus_profile::staticGet('feeduri', $topic); + if (!$profile) { common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic"); throw new ServerException("Bogus hub callback: unknown feed", 404); } @@ -93,16 +93,16 @@ class PushCallbackAction extends Action // OK! if ($mode == 'subscribe') { common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); - $feedinfo->sub_start = common_sql_date(time()); + $profile->sub_start = common_sql_date(time()); if ($lease_seconds > 0) { - $feedinfo->sub_end = common_sql_date(time() + $lease_seconds); + $profile->sub_end = common_sql_date(time() + $lease_seconds); } else { - $feedinfo->sub_end = null; + $profile->sub_end = null; } - $feedinfo->update(); + $profile->update(); } else { common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic"); - $feedinfo->delete(); + $profile->delete(); } print $challenge; diff --git a/plugins/OStatus/classes/Feedinfo.php b/plugins/OStatus/classes/Ostatus_profile.php similarity index 92% rename from plugins/OStatus/classes/Feedinfo.php rename to plugins/OStatus/classes/Ostatus_profile.php index 5b8a9039a6..748ecce18f 100644 --- a/plugins/OStatus/classes/Feedinfo.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -25,17 +25,17 @@ /* PuSH subscription flow: - $feedinfo->subscribe() + $profile->subscribe() generate random verification token save to verify_token sends a sub request to the hub... - feedsub/callback + main/push/callback hub sends confirmation back to us via GET We verify the request, then echo back the challenge. On our end, we save the time we subscribed and the lease expiration - feedsub/callback + main/push/callback hub sends us updates via POST */ @@ -51,23 +51,27 @@ class FeedDBException extends FeedSubException } } -class Feedinfo extends Memcached_DataObject +class Ostatus_profile extends Memcached_DataObject { - public $__table = 'feedinfo'; + public $__table = 'ostatus_profile'; public $id; public $profile_id; + public $group_id; public $feeduri; public $homeuri; - public $huburi; // PuSH subscription data + public $huburi; public $secret; public $verify_token; + public $sub_state; // subscribe, active, unsubscribe public $sub_start; public $sub_end; + public $salmonuri; + public $created; public $lastupdate; @@ -96,6 +100,7 @@ class Feedinfo extends Memcached_DataObject 'huburi' => DB_DATAOBJECT_STR, 'secret' => DB_DATAOBJECT_STR, 'verify_token' => DB_DATAOBJECT_STR, + 'sub_state' => DB_DATAOBJECT_STR, 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, 'salmonuri' => DB_DATAOBJECT_STR, @@ -126,6 +131,8 @@ class Feedinfo extends Memcached_DataObject 32, true), new ColumnDef('secret', 'varchar', 64, true), + new ColumnDef('sub_state', "enum('subscribe','active','unsubscribe')", + null, true), new ColumnDef('sub_start', 'datetime', null, true), new ColumnDef('sub_end', 'datetime', @@ -175,7 +182,7 @@ class Feedinfo extends Memcached_DataObject * Fetch the StatusNet-side profile for this feed * @return Profile */ - public function getProfile() + public function getLocalProfile() { return Profile::staticGet('id', $this->profile_id); } @@ -183,23 +190,23 @@ class Feedinfo extends Memcached_DataObject /** * @param FeedMunger $munger * @param boolean $isGroup is this a group record? - * @return Feedinfo + * @return Ostatus_profile */ public static function ensureProfile($munger) { - $feedinfo = $munger->feedinfo(); + $entity = $munger->ostatusProfile(); - $current = self::staticGet('feeduri', $feedinfo->feeduri); + $current = self::staticGet('feeduri', $entity->feeduri); if ($current) { // @fixme we should probably update info as necessary return $current; } - $feedinfo->query('BEGIN'); + $entity->query('BEGIN'); // Awful hack! Awful hack! - $feedinfo->verify = common_good_rand(16); - $feedinfo->secret = common_good_rand(32); + $entity->verify = common_good_rand(16); + $entity->secret = common_good_rand(32); try { $profile = $munger->profile(); @@ -223,8 +230,8 @@ class Feedinfo extends Memcached_DataObject $profile->setOriginal($filename); } - $feedinfo->profile_id = $profile->id; - if ($feedinfo->isGroup()) { + $entity->profile_id = $profile->id; + if ($entity->isGroup()) { $group = new User_group(); $group->nickname = $profile->nickname . '@remote'; // @fixme $group->fullname = $profile->fullname; @@ -237,21 +244,21 @@ class Feedinfo extends Memcached_DataObject $group->setOriginal($filename); } - $feedinfo->group_id = $group->id; + $entity->group_id = $group->id; } - $result = $feedinfo->insert(); + $result = $entity->insert(); if (empty($result)) { - throw new FeedDBException($feedinfo); + throw new FeedDBException($entity); } - $feedinfo->query('COMMIT'); + $entity->query('COMMIT'); } catch (FeedDBException $e) { common_log_db_error($e->obj, 'INSERT', __FILE__); - $feedinfo->query('ROLLBACK'); + $entity->query('ROLLBACK'); return false; } - return $feedinfo; + return $entity; } /** diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php index 927a2fe7a7..c895b6ce24 100644 --- a/plugins/OStatus/lib/feedmunger.php +++ b/plugins/OStatus/lib/feedmunger.php @@ -83,17 +83,17 @@ class FeedMunger $this->url = $url; } - function feedinfo() + function ostatusProfile() { - $feedinfo = new Feedinfo(); - $feedinfo->feeduri = $this->url; - $feedinfo->homeuri = $this->feed->link; - $feedinfo->huburi = $this->getHubLink(); + $profile = new Ostatus_profile(); + $profile->feeduri = $this->url; + $profile->homeuri = $this->feed->link; + $profile->huburi = $this->getHubLink(); $salmon = $this->getSalmonLink(); if ($salmon) { - $feedinfo->salmonuri = $salmon; + $profile->salmonuri = $salmon; } - return $feedinfo; + return $profile; } function getAtomLink($item, $attribs=array()) @@ -258,9 +258,7 @@ class FeedMunger { // hack hack hack // should get profile for this entry's author... - $feed = new Feedinfo(); - $feed->feeduri = $self; - $feed = Feedinfo::staticGet('feeduri', $this->getSelfLink()); + $remote = Ostatus_profile::staticGet('feeduri', $this->getSelfLink()); if ($feed) { return $feed->profile_id; } else { From 3beddffc39e9a0bc5d32f50f4c8f93771060a032 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 11 Feb 2010 15:24:18 -0800 Subject: [PATCH 27/55] ostatus:attention links in Notice Atom output --- classes/Notice.php | 16 +++++++++++++++- classes/Profile.php | 6 ++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/classes/Notice.php b/classes/Notice.php index 091f2dc7b4..a39388cdb3 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -994,7 +994,7 @@ class Notice extends Memcached_DataObject $xs->element('summary', null, $this->content); $xs->raw($profile->asAtomAuthor()); - $xs->raw($profile->asActivityActor($namespace)); + $xs->raw($profile->asActivityActor()); $xs->element('link', array('rel' => 'alternate', 'href' => $this->bestUrl())); @@ -1028,6 +1028,20 @@ class Notice extends Memcached_DataObject ); } + $reply_ids = $this->getReplies(); + + foreach ($reply_ids as $id) { + $profile = Profile::staticGet('id', $id); + if (!empty($profile)) { + $xs->element( + 'link', array( + 'rel' => 'osatus:attention', + 'href' => $profile->getAcctUri() + ) + ); + } + } + if (!empty($this->repeat_of)) { $repeat = Notice::staticGet('id', $this->repeat_of); if (!empty($repeat)) { diff --git a/classes/Profile.php b/classes/Profile.php index 664c45f640..3e5150c182 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -803,4 +803,10 @@ class Profile extends Memcached_DataObject return $xs->getString(); } + + function getAcctUri() + { + return $this->nickname . '@' . common_config('site', 'server'); + } + } From 525358fa101784fa5bbbac8b214091de89ec0634 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 11 Feb 2010 17:08:50 -0800 Subject: [PATCH 28/55] Fix retarded spelling mistake --- classes/Notice.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index a39388cdb3..924931e42b 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1019,7 +1019,7 @@ class Notice extends Memcached_DataObject && $this->conversation != $this->notice->id) { $xs->element( 'link', array( - 'rel' => 'osatus:conversation', + 'rel' => 'ostatus:conversation', 'href' => common_local_url( 'conversation', array('id' => $this->conversation) @@ -1035,7 +1035,7 @@ class Notice extends Memcached_DataObject if (!empty($profile)) { $xs->element( 'link', array( - 'rel' => 'osatus:attention', + 'rel' => 'ostatus:attention', 'href' => $profile->getAcctUri() ) ); From bc46621af2bea8d2f9f132f275c70c5964f880b4 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Feb 2010 01:11:46 +0000 Subject: [PATCH 29/55] OStatus sub setup code cleanup and partial group fixes (needs more work after the Atom updates are done) --- plugins/OStatus/actions/feedsubsettings.php | 15 +- plugins/OStatus/actions/pushcallback.php | 27 ++-- plugins/OStatus/classes/Ostatus_profile.php | 168 ++++++++++++++------ 3 files changed, 143 insertions(+), 67 deletions(-) diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php index af8bf4d25e..6933c9bf21 100644 --- a/plugins/OStatus/actions/feedsubsettings.php +++ b/plugins/OStatus/actions/feedsubsettings.php @@ -197,6 +197,9 @@ class FeedSubSettingsAction extends ConnectSettingsAction if ($this->validateFeed()) { $this->preview = true; $this->profile = Ostatus_profile::ensureProfile($this->munger); + if (!$this->profile) { + throw new ServerException("Feed profile was not saved properly."); + } // If not already in use, subscribe to updates via the hub if ($this->profile->sub_start) { @@ -212,13 +215,10 @@ class FeedSubSettingsAction extends ConnectSettingsAction // And subscribe the current user to the local profile $user = common_current_user(); - $profile = $this->profile->getLocalProfile(); - if (!$profile) { - throw new ServerException("Feed profile was not saved properly."); - } if ($this->profile->isGroup()) { - if ($user->isMember($profile)) { + $group = $this->profile->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!')); @@ -226,9 +226,10 @@ class FeedSubSettingsAction extends ConnectSettingsAction $this->showForm(_m('Remote group join failed!')); } } else { - if ($user->isSubscribed($profile)) { + $local = $this->profile->localProfile(); + if ($user->isSubscribed($local)) { $this->showForm(_m('Already subscribed!')); - } elseif ($user->subscribeTo($profile)) { + } elseif ($user->subscribeTo($local)) { $this->showForm(_m('Feed subscribed!')); } else { $this->showForm(_m('Feed subscription failed!')); diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php index a446593ff9..2601a377a0 100644 --- a/plugins/OStatus/actions/pushcallback.php +++ b/plugins/OStatus/actions/pushcallback.php @@ -84,27 +84,24 @@ class PushCallbackAction extends Action throw new ServerException("Bogus hub callback: unknown feed", 404); } - # Can't currently set the token in our sub api - #if ($feedinfo->verify_token !== $verify_token) { - # common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic"); - # throw new ServerError("Bogus hub callback: bad token", 404); - #} - + if ($profile->verify_token !== $verify_token) { + common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic"); + throw new ServerError("Bogus hub callback: bad token", 404); + } + + if ($mode != $profile->sub_state) { + common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad mode \"$mode\" for feed $topic in state \"{$profile->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'); - $profile->sub_start = common_sql_date(time()); - if ($lease_seconds > 0) { - $profile->sub_end = common_sql_date(time() + $lease_seconds); - } else { - $profile->sub_end = null; - } - $profile->update(); + $profile->confirmSubscribe($lease_seconds); } else { common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic"); - $profile->delete(); + $profile->confirmUnsubscribe(); } - print $challenge; } } diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 748ecce18f..f7bbcd0286 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -182,9 +182,24 @@ class Ostatus_profile extends Memcached_DataObject * Fetch the StatusNet-side profile for this feed * @return Profile */ - public function getLocalProfile() + public function localProfile() { - return Profile::staticGet('id', $this->profile_id); + if ($this->profile_id) { + return Profile::staticGet('id', $this->profile_id); + } + return null; + } + + /** + * Fetch the StatusNet-side profile for this feed + * @return Profile + */ + public function localGroup() + { + if ($this->group_id) { + return User_group::staticGet('id', $this->group_id); + } + return null; } /** @@ -194,73 +209,96 @@ class Ostatus_profile extends Memcached_DataObject */ public static function ensureProfile($munger) { - $entity = $munger->ostatusProfile(); + $profile = $munger->ostatusProfile(); - $current = self::staticGet('feeduri', $entity->feeduri); + $current = self::staticGet('feeduri', $profile->feeduri); if ($current) { // @fixme we should probably update info as necessary return $current; } - $entity->query('BEGIN'); + $profile->query('BEGIN'); // Awful hack! Awful hack! - $entity->verify = common_good_rand(16); - $entity->secret = common_good_rand(32); + $profile->verify = common_good_rand(16); + $profile->secret = common_good_rand(32); try { - $profile = $munger->profile(); + $local = $munger->profile(); + + if ($entity->isGroup()) { + $group = new User_group(); + $group->nickname = $local->nickname . '@remote'; // @fixme + $group->fullname = $local->fullname; + $group->homepage = $local->homepage; + $group->location = $local->location; + $group->created = $local->created; + $group->insert(); + if (empty($result)) { + throw new FeedDBException($group); + } + $profile->group_id = $group->id; + } else { + $result = $local->insert(); + if (empty($result)) { + throw new FeedDBException($local); + } + $profile->profile_id = $local->id; + } + + $profile->created = sql_common_date(); + $profile->lastupdate = sql_common_date(); $result = $profile->insert(); if (empty($result)) { throw new FeedDBException($profile); } - $avatar = $munger->getAvatar(); - if ($avatar) { - // @fixme this should be better encapsulated - // ripped from oauthstore.php (for old OMB client) - $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); - copy($avatar, $temp_filename); - $imagefile = new ImageFile($profile->id, $temp_filename); - $filename = Avatar::filename($profile->id, - image_type_to_extension($imagefile->type), - null, - common_timestamp()); - rename($temp_filename, Avatar::path($filename)); - $profile->setOriginal($filename); - } - - $entity->profile_id = $profile->id; - if ($entity->isGroup()) { - $group = new User_group(); - $group->nickname = $profile->nickname . '@remote'; // @fixme - $group->fullname = $profile->fullname; - $group->homepage = $profile->homepage; - $group->location = $profile->location; - $group->created = $profile->created; - $group->insert(); - - if ($avatar) { - $group->setOriginal($filename); - } - - $entity->group_id = $group->id; - } - - $result = $entity->insert(); - if (empty($result)) { - throw new FeedDBException($entity); - } - $entity->query('COMMIT'); } catch (FeedDBException $e) { common_log_db_error($e->obj, 'INSERT', __FILE__); $entity->query('ROLLBACK'); return false; } + + $avatar = $munger->getAvatar(); + if ($avatar) { + try { + $this->updateAvatar($avatar); + } catch (Exception $e) { + common_log(LOG_ERR, "Exception setting OStatus avatar: " . + $e->getMessage()); + } + } + return $entity; } + /** + * Download and update given avatar image + * @param string $url + * @throws Exception in various failure cases + */ + public function updateAvatar($url) + { + // @fixme this should be better encapsulated + // ripped from oauthstore.php (for old OMB client) + $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); + copy($url, $temp_filename); + $imagefile = new ImageFile($profile->id, $temp_filename); + $filename = Avatar::filename($profile->id, + image_type_to_extension($imagefile->type), + null, + common_timestamp()); + rename($temp_filename, Avatar::path($filename)); + if ($this->isGroup()) { + $group = $this->localGroup(); + $group->setOriginal($filename); + } else { + $profile = $this->localProfile(); + $profile->setOriginal($filename); + } + } + /** * Damn dirty hack! */ @@ -318,6 +356,46 @@ class Ostatus_profile extends Memcached_DataObject } } + /** + * Save PuSH subscription confirmation. + * Sets approximate lease start and end times and finalizes state. + * + * @param int $lease_seconds provided hub.lease_seconds parameter, if given + */ + public function confirmSubscribe($lease_seconds=0) + { + $original = clone($this); + + $this->sub_state = 'active'; + $this->sub_start = common_sql_date(time()); + if ($lease_seconds > 0) { + $this->sub_end = common_sql_date(time() + $lease_seconds); + } else { + $this->sub_end = null; + } + $this->lastupdate = common_sql_date(); + + return $this->update($original); + } + + /** + * Save PuSH unsubscription confirmation. + * Wipes active PuSH sub info and resets state. + */ + public function confirmUnsubscribe() + { + $original = clone($this); + + $this->verify_token = null; + $this->secret = null; + $this->sub_state = null; + $this->sub_start = null; + $this->sub_end = null; + $this->lastupdate = common_sql_date(); + + return $this->update($original); + } + /** * Send an unsubscription request to the hub for this feed. * The hub will later send us a confirmation POST to /main/push/callback. From 5f94efc45463378f246f9db82e9f2e0e8a109f7d Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 12 Feb 2010 00:42:42 -0500 Subject: [PATCH 30/55] stub for activities --- plugins/OStatus/lib/activity.php | 85 ++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 plugins/OStatus/lib/activity.php diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php new file mode 100644 index 0000000000..36e2279134 --- /dev/null +++ b/plugins/OStatus/lib/activity.php @@ -0,0 +1,85 @@ +. + * + * @category OStatus + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class ActivityNoun +{ + const ARTICLE = 'http://activitystrea.ms/schema/1.0/article'; + const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry'; + const NOTE = 'http://activitystrea.ms/schema/1.0/note'; + const STATUS = 'http://activitystrea.ms/schema/1.0/status'; + const FILE = 'http://activitystrea.ms/schema/1.0/file'; + const PHOTO = 'http://activitystrea.ms/schema/1.0/photo'; + const ALBUM = 'http://activitystrea.ms/schema/1.0/photo-album'; + const PLAYLIST = 'http://activitystrea.ms/schema/1.0/playlist'; + const VIDEO = 'http://activitystrea.ms/schema/1.0/video'; + const AUDIO = 'http://activitystrea.ms/schema/1.0/audio'; + const BOOKMARK = 'http://activitystrea.ms/schema/1.0/bookmark'; + const PERSON = 'http://activitystrea.ms/schema/1.0/person'; + const GROUP = 'http://activitystrea.ms/schema/1.0/group'; + const PLACE = 'http://activitystrea.ms/schema/1.0/place'; + const COMMENT = 'http://activitystrea.ms/schema/1.0/comment'; // tea + + public $type; + public $id; + public $title; + public $summary; + public $content; +} + +class Activity +{ + const NAMESPACE = 'http://activitystrea.ms/schema/1.0/'; + + const POST = 'http://activitystrea.ms/schema/1.0/post'; + const SHARE = 'http://activitystrea.ms/schema/1.0/share'; + const SAVE = 'http://activitystrea.ms/schema/1.0/save'; + const FAVORITE = 'http://activitystrea.ms/schema/1.0/favorite'; + const PLAY = 'http://activitystrea.ms/schema/1.0/play'; + const FOLLOW = 'http://activitystrea.ms/schema/1.0/follow'; + const FRIEND = 'http://activitystrea.ms/schema/1.0/make-friend'; + const JOIN = 'http://activitystrea.ms/schema/1.0/join'; + const TAG = 'http://activitystrea.ms/schema/1.0/tag'; + + public $actor; // an ActivityNoun + public $verb; // a string (the URL) + public $object; // an ActivityNoun + public $target; // an ActivityNoun + + static function fromAtomEntry($domEntry) + { + } + + function toAtomEntry() + { + } +} From 320532560fe6fa4661d58317923c54b708c897c2 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 12 Feb 2010 00:43:16 -0500 Subject: [PATCH 31/55] flesh out salmon endpoint --- plugins/OStatus/actions/salmon.php | 52 ++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/plugins/OStatus/actions/salmon.php b/plugins/OStatus/actions/salmon.php index 012869cf73..b616027a93 100644 --- a/plugins/OStatus/actions/salmon.php +++ b/plugins/OStatus/actions/salmon.php @@ -22,28 +22,60 @@ * @author James Walker */ -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +if (!defined('STATUSNET')) { + exit(1); +} class SalmonAction extends Action { + var $user = null; + var $xml = null; + var $activity = null; - function handle() + function prepare($args) { - parent::handle(); - if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $this->handlePost(); + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + $this->clientError(_('This method requires a POST.')); } - } + if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') { + $this->clientError(_('Salmon requires application/atom+xml')); + } - function handlePost() - { - $user_id = $this->arg('id'); - common_log(LOG_DEBUG, 'Salmon: incoming post for user: '. $user_id); + $id = $this->trimmed('id'); + + if (!$id) { + $this->clientError(_('No ID.')); + } + + $this->user = User::staticGet($id); + + if (empty($this->user)) { + $this->clientError(_('No such user.')); + } $xml = file_get_contents('php://input'); + $dom = DOMDocument::loadXML($xml); + + // XXX: check that document element is Atom entry + // XXX: check the signature + + $this->act = Activity::fromAtomEntry($dom->documentElement); + } + + function handle($args) + { + common_log(LOG_DEBUG, 'Salmon: incoming post for user: '. $user_id); + // TODO : Insert new $xml -> notice code + switch ($this->act->verb) + { + case Activity::POST: + case Activity::SHARE: + case Activity::FAVORITE: + case Activity::FOLLOW: + } } } From fd527b8de120587670eb6fdd0ecce7ca7833ea72 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Fri, 12 Feb 2010 11:34:23 +0100 Subject: [PATCH 32/55] Moved colour properties out of base stylesheet --- theme/base/css/display.css | 3 --- theme/default/css/display.css | 9 ++++++--- theme/identica/css/display.css | 10 +++++++--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 8490fb5803..70ddc411f8 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -1109,15 +1109,12 @@ right:29px; z-index:9; min-width:199px; float:none; -background-color:#FFF; padding:11px; border-radius:7px; -moz-border-radius:7px; -webkit-border-radius:7px; border-style:solid; border-width:1px; -border-color:#DDDDDD; --moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); } .dialogbox legend { diff --git a/theme/default/css/display.css b/theme/default/css/display.css index 82eb135316..02e1645f47 100644 --- a/theme/default/css/display.css +++ b/theme/default/css/display.css @@ -46,7 +46,8 @@ box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3); .pagination .nav_prev a, .pagination .nav_next a, .form_settings fieldset fieldset, -.entity_moderation:hover ul { +.entity_moderation:hover ul, +.dialogbox { border-color:#DDDDDD; } @@ -221,7 +222,8 @@ border-color:transparent; #content, #site_nav_local_views .current a, .entity_send-a-message .form_notice, -.entity_moderation:hover ul { +.entity_moderation:hover ul, +.dialogbox { background-color:#FFFFFF; } @@ -308,7 +310,8 @@ background-position: 5px -718px; background-position: 5px -852px; } .entity_send-a-message .form_notice, -.entity_moderation:hover ul { +.entity_moderation:hover ul, +.dialogbox { box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); -moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); -webkit-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); diff --git a/theme/identica/css/display.css b/theme/identica/css/display.css index 44ae4953b7..6dc7d21df0 100644 --- a/theme/identica/css/display.css +++ b/theme/identica/css/display.css @@ -46,7 +46,8 @@ box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3); .pagination .nav_prev a, .pagination .nav_next a, .form_settings fieldset fieldset, -.entity_moderation:hover ul { +.entity_moderation:hover ul, +.dialogbox { border-color:#DDDDDD; } @@ -88,6 +89,7 @@ color:#FFFFFF; border-color:transparent; text-shadow:none; } + .dialogbox .submit_dialogbox, input.submit, .form_notice input.submit { @@ -221,7 +223,8 @@ border-color:transparent; #content, #site_nav_local_views .current a, .entity_send-a-message .form_notice, -.entity_moderation:hover ul { +.entity_moderation:hover ul, +.dialogbox { background-color:#FFFFFF; } @@ -307,7 +310,8 @@ background-position: 5px -718px; background-position: 5px -852px; } .entity_send-a-message .form_notice, -.entity_moderation:hover ul { +.entity_moderation:hover ul, +.dialogbox { box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); -moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); -webkit-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); From 42679a22dc712467db567a9bac41c58e4788dd58 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Fri, 12 Feb 2010 12:04:14 +0100 Subject: [PATCH 33/55] Extracted default values for dialogbox layout and uniqe for form_repeat --- theme/base/css/display.css | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 70ddc411f8..990280d847 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -1104,10 +1104,9 @@ left:0; .dialogbox { position:absolute; -top:-4px; -right:29px; +top:0; +right:0; z-index:9; -min-width:199px; float:none; padding:11px; border-radius:7px; @@ -1142,6 +1141,12 @@ outline:none; text-indent:-9999px; } +.form_repeat.dialogbox { +top:-4px; +right:29px; +min-width:199px; +} + .notice-options { position:relative; font-size:0.95em; From b57e3dfae2f48ef6097fef04df218201783abed1 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Fri, 12 Feb 2010 14:16:38 +0100 Subject: [PATCH 34/55] More style generalisation for dialogbox --- theme/base/css/display.css | 16 ++++++++++++++-- theme/default/css/display.css | 14 ++++++++------ theme/identica/css/display.css | 11 ++++++----- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 990280d847..3218276a68 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -1104,8 +1104,8 @@ left:0; .dialogbox { position:absolute; -top:0; -right:0; +top:-1px; +right:-1px; z-index:9; float:none; padding:11px; @@ -1119,6 +1119,7 @@ border-width:1px; .dialogbox legend { display:block !important; margin-right:18px; +margin-bottom:18px; } .dialogbox button.close { @@ -1127,11 +1128,22 @@ right:3px; top:3px; } +.dialogbox .form_guide { +font-weight:normal; +padding:0; +} + .dialogbox .submit_dialogbox { font-weight:bold; text-indent:0; min-width:46px; } +.dialogbox input { +padding-left:4px; +} +.dialogbox fieldset { +margin-bottom:0; +} #wrap form.processing input.submit, .entity_actions a.processing, diff --git a/theme/default/css/display.css b/theme/default/css/display.css index 02e1645f47..a2f1013428 100644 --- a/theme/default/css/display.css +++ b/theme/default/css/display.css @@ -30,7 +30,9 @@ border-radius:4px; input, textarea, select, option { font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif; } -input, textarea, select { +input, textarea, select, +.entity_actions .dialogbox input, +.mark-top { border-color:#AAAAAA; } @@ -79,7 +81,8 @@ background-color:transparent; input:focus, textarea:focus, select:focus, .form_notice.warning #notice_data-text, .form_notice.warning #notice_text-count, -.form_settings .form_note { +.form_settings .form_note, +.entity_actions .dialogbox .form_data input:focus { border-color:#9BB43E; } input.submit { @@ -134,9 +137,6 @@ color:#002FA7; #content tbody tr { border-top-color:#C8D1D5; } -.mark-top { -border-color:#AAAAAA; -} #aside_primary { background-color:#C8D1D5; @@ -145,7 +145,9 @@ background-color:#C8D1D5; #notice_text-count { color:#333333; } -.form_notice.warning #notice_text-count { +.form_notice.warning #notice_text-count, +.dialogbox, +.entity_actions .dialogbox input { color:#000000; } .form_notice label[for=notice_data-attach] { diff --git a/theme/identica/css/display.css b/theme/identica/css/display.css index 6dc7d21df0..e214047451 100644 --- a/theme/identica/css/display.css +++ b/theme/identica/css/display.css @@ -30,7 +30,9 @@ border-radius:4px; input, textarea, select, option { font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif; } -input, textarea, select { +input, textarea, select, +.entity_actions .dialogbox input, +.mark-top { border-color:#AAAAAA; } @@ -135,9 +137,6 @@ color:#002FA7; #content tbody tr { border-top-color:#CEE1E9; } -.mark-top { -border-color:#AAAAAA; -} #aside_primary { background-color:#CEE1E9; @@ -146,7 +145,9 @@ background-color:#CEE1E9; #notice_text-count { color:#333333; } -.form_notice.warning #notice_text-count { +.form_notice.warning #notice_text-count, +.dialogbox, +.entity_actions .dialogbox input { color:#000000; } .form_notice label[for=notice_data-attach] { From 094565b4aa1893e6a4422f3d05a0a43844e47a67 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Fri, 12 Feb 2010 18:20:13 +0100 Subject: [PATCH 35/55] Added 'pre' to pick up Palm Pre's UA string: Mozilla/5.0 (webOS/1.3.5.1; U; en-US) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/1.0 Safari/525.27.1 Pre/1.0 --- plugins/MobileProfile/MobileProfilePlugin.php | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/MobileProfile/MobileProfilePlugin.php b/plugins/MobileProfile/MobileProfilePlugin.php index cd2531fa72..e9b4a05f7d 100644 --- a/plugins/MobileProfile/MobileProfilePlugin.php +++ b/plugins/MobileProfile/MobileProfilePlugin.php @@ -121,6 +121,7 @@ class MobileProfilePlugin extends WAP20Plugin 'philips', 'pocketpc', 'portalmmm', + 'pre', 'rover', 'samsung', 'sanyo', From 47f6b0afc9a94b5e649102dd209950b94c6ac33a Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Fri, 12 Feb 2010 18:30:27 +0100 Subject: [PATCH 36/55] Revert "Added 'pre' to pick up Palm Pre's UA string:" This reverts commit 094565b4aa1893e6a4422f3d05a0a43844e47a67. On second thought, "pre" is probably the stupidest way of differentiating one agent from another. Need a different solution. --- plugins/MobileProfile/MobileProfilePlugin.php | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/MobileProfile/MobileProfilePlugin.php b/plugins/MobileProfile/MobileProfilePlugin.php index e9b4a05f7d..cd2531fa72 100644 --- a/plugins/MobileProfile/MobileProfilePlugin.php +++ b/plugins/MobileProfile/MobileProfilePlugin.php @@ -121,7 +121,6 @@ class MobileProfilePlugin extends WAP20Plugin 'philips', 'pocketpc', 'portalmmm', - 'pre', 'rover', 'samsung', 'sanyo', From b39047d95b447251de75d15b986017286aca05e0 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Feb 2010 18:54:48 +0000 Subject: [PATCH 37/55] OStatus: prep work for sending notifications on sub/unsub/join/leave/favorite/unfavorite via Salmon; needs to be completed and hooked up once feed gen is fixed. --- classes/Profile.php | 34 ++++- plugins/OStatus/classes/Ostatus_profile.php | 153 +++++++++++++++++++- 2 files changed, 184 insertions(+), 3 deletions(-) diff --git a/classes/Profile.php b/classes/Profile.php index 3e5150c182..ab05bb8546 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -755,6 +755,14 @@ class Profile extends Memcached_DataObject return !empty($notice); } + /** + * Returns an XML string fragment with limited profile information + * as an Atom element. + * + * Assumes that Atom has been previously set up as the base namespace. + * + * @return string + */ function asAtomAuthor() { $xs = new XMLStringer(true); @@ -767,11 +775,33 @@ class Profile extends Memcached_DataObject return $xs->getString(); } + /** + * Returns an XML string fragment with profile information as an + * Activity Streams element. + * + * Assumes that 'activity' namespace has been previously defined. + * + * @return string + */ function asActivityActor() + { + return $this->asActivityNoun('actor'); + } + + /** + * Returns an XML string fragment with profile information as an + * Activity Streams noun object with the given element type. + * + * Assumes that 'activity' namespace has been previously defined. + * + * @param string $element one of 'actor', 'subject', 'object', 'target' + * @return string + */ + function asActivityNoun($element) { $xs = new XMLStringer(true); - $xs->elementStart('activity:actor'); + $xs->elementStart('activity:' . $element); $xs->element( 'activity:object-type', null, @@ -799,7 +829,7 @@ class Profile extends Memcached_DataObject '' ); - $xs->elementEnd('activity:actor'); + $xs->elementEnd('activity:' . $element); return $xs->getString(); } diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index f7bbcd0286..733d8843b8 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -299,6 +299,71 @@ class Ostatus_profile extends Memcached_DataObject } } + /** + * Returns an XML string fragment with profile information as an + * Activity Streams noun object with the given element type. + * + * Assumes that 'activity' namespace has been previously defined. + * + * @param string $element one of 'actor', 'subject', 'object', 'target' + * @return string + */ + function asActivityNoun($element) + { + $xs = new XMLStringer(true); + + $avatarHref = Avatar::defaultImage(AVATAR_PROFILE_SIZE); + $avatarType = 'image/png'; + if ($this->isGroup()) { + $type = 'http://activitystrea.ms/schema/1.0/group'; + $self = $this->localGroup(); + + // @fixme put a standard getAvatar() interface on groups too + if ($self->homepage_logo) { + $avatarHref = $self->homepage_logo; + $map = array('png' => 'image/png', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif'); + $extension = pathinfo(parse_url($avatarHref, PHP_URL_PATH), PATHINFO_EXTENSION); + if (isset($map[$extension])) { + $avatarType = $map[$extension]; + } + } + } else { + $type = 'http://activitystrea.ms/schema/1.0/person'; + $self = $this->localProfile(); + $avatar = $self->getAvatar(AVATAR_PROFILE_SIZE); + if ($avatar) { + $avatarHref = $avatar-> + $avatarType = $avatar->mediatype; + } + } + $xs->elementStart('activity:' . $element); + $xs->element( + 'activity:object-type', + null, + $type + ); + $xs->element( + 'id', + null, + $this->homeuri); // ? + $xs->element('title', null, $self->getBestName()); + + $xs->element( + 'link', array( + 'type' => $avatarType, + 'href' => $avatarHref + ), + '' + ); + + $xs->elementEnd('activity:' . $element); + + return $xs->getString(); + } + /** * Damn dirty hack! */ @@ -397,7 +462,7 @@ class Ostatus_profile extends Memcached_DataObject } /** - * Send an unsubscription request to the hub for this feed. + * 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 @@ -406,6 +471,92 @@ class Ostatus_profile extends Memcached_DataObject return $this->subscribe('unsubscribe'); } + /** + * Send an Activity Streams notification to the remote Salmon endpoint, + * if so configured. + * + * @param Profile $actor + * @param $verb eg Activity::SUBSCRIBE or Activity::JOIN + * @param $object object of the action; if null, the remote entity itself is assumed + */ + public function notify(Profile $actor, $verb, $object=null) + { + if ($object == null) { + $object = $this; + } + if ($this->salmonuri) { + $text = 'update'; // @fixme + $id = 'tag:' . common_config('site', 'server') . + ':' . $verb . + ':' . $actor->id . + ':' . time(); // @fixme + + $entry = new Atom10Entry(); + $entry->elementStart('entry'); + $entry->element('id', null, $id); + $entry->element('title', null, $text); + $entry->element('summary', null, $text); + $entry->element('published', null, common_date_w3dtf()); + + $entry->element('activity:verb', null, $verb); + $entry->raw($profile->asAtomAuthor()); + $entry->raw($profile->asActivityActor()); + $entry->raw($object->asActivityNoun('object')); + $entry->elmentEnd('entry'); + + $feed = $this->atomFeed($actor); + $feed->initFeed(); + $feed->addEntry($entry); + $feed->renderEntries(); + $feed->endFeed(); + + $xml = $feed->getString(); + common_log(LOG_INFO, "Posting to Salmon endpoint $salmon: $xml"); + + $salmon = new Salmon(); // ? + $salmon->post($this->salmonuri, $xml); + } + } + + function getBestName() + { + if ($this->isGroup()) { + return $this->localGroup()->getBestName(); + } else { + return $this->localProfile()->getBestName(); + } + } + + function atomFeed($actor) + { + $feed = new Atom10Feed(); + // @fixme should these be set up somewhere else? + $feed->addNamespace('activity', 'http://activitystrea.ms/spec/1.0/'); + $feed->addNamesapce('thr', 'http://purl.org/syndication/thread/1.0'); + $feed->addNamespace('georss', 'http://www.georss.org/georss'); + $feed->addNamespace('ostatus', 'http://ostatus.org/schema/1.0'); + + $taguribase = common_config('integration', 'taguri'); + $feed->setId("tag:{$taguribase}:UserTimeline:{$actor->id}"); // ??? + + $feed->setTitle($actor->getBestName() . ' timeline'); // @fixme + $feed->setUpdated(time()); + $feed->setPublished(time()); + + $feed->addLink(common_url('ApiTimelineUser', + array('id' => $actor->id, + 'type' => 'atom')), + array('rel' => 'self', + 'type' => 'application/atom+xml')); + + $feed->addLink(common_url('userbyid', + array('id' => $actor->id)), + array('rel' => 'alternate', + 'type' => 'text/html')); + + return $feed; + } + /** * Read and post notices for updates from the feed. * Currently assumes that all items in the feed are new, From fd3c9334bfcfe627446feb86ac3054b24ed05449 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Feb 2010 11:15:12 -0800 Subject: [PATCH 38/55] PHP 5.3 compatibility hack for DB_DataObject statusnet.links.ini file could not be read anymore due to the entry for nonce containing a comma in its key value. PHP's parse_ini_file() function no longer allows commas in keys, and rejects the *ENTIRE FILE* if it's present, breaking various automatic joins. --- classes/Nonce.php | 15 +++++++++++++++ classes/statusnet.links.ini | 7 +++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/classes/Nonce.php b/classes/Nonce.php index 486a65a3c7..2f8ab00b5d 100644 --- a/classes/Nonce.php +++ b/classes/Nonce.php @@ -22,4 +22,19 @@ class Nonce extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE + + /** + * Compatibility hack for PHP 5.3 + * + * The statusnet.links.ini entry cannot be read because "," is no longer + * allowed in key names when read by parse_ini_file(). + * + * @return array + * @access public + */ + function links() + { + return array('consumer_key,token' => 'token:consumer_key,token'); + } + } diff --git a/classes/statusnet.links.ini b/classes/statusnet.links.ini index 7f233e6760..b9dd5af0c9 100644 --- a/classes/statusnet.links.ini +++ b/classes/statusnet.links.ini @@ -19,8 +19,11 @@ profile_id = profile:id [token] consumer_key = consumer:consumer_key -[nonce] -consumer_key,token = token:consumer_key,token +; Compatibility hack for PHP 5.3 +; This entry has been moved to the class definition, as commas are no longer +; considered valid in keys, causing parse_ini_file() to reject the whole file. +;[nonce] +;consumer_key,token = token:consumer_key,token [confirm_address] user_id = user:id From 506c2d7491f7f229a1469ef176fee6c21d61a6c6 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 12 Feb 2010 12:22:12 -0800 Subject: [PATCH 39/55] Initial upgraded Atom output for group timelines --- actions/apitimelinegroup.php | 62 ++++++++++++++++++++++++-------- classes/User_group.php | 33 +++++++++++++++++ lib/api.php | 2 +- lib/atom10feed.php | 70 ++++++++++++++++++++++++++++++++++-- 4 files changed, 149 insertions(+), 18 deletions(-) diff --git a/actions/apitimelinegroup.php b/actions/apitimelinegroup.php index fd2ed9ff93..45962fa76f 100644 --- a/actions/apitimelinegroup.php +++ b/actions/apitimelinegroup.php @@ -109,38 +109,70 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction $title = sprintf(_("%s timeline"), $this->group->nickname); $taguribase = common_config('integration', 'taguri'); $id = "tag:$taguribase:GroupTimeline:" . $this->group->id; - $link = common_local_url( - 'showgroup', - array('nickname' => $this->group->nickname) - ); + $subtitle = sprintf( _('Updates from %1$s on %2$s!'), $this->group->nickname, $sitename ); - $logo = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE); + + $logo = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE); switch($this->format) { case 'xml': $this->showXmlTimeline($this->notices); break; case 'rss': - $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo); - break; - case 'atom': - $selfuri = common_root_url() . - 'api/statusnet/groups/timeline/' . - $this->group->id . '.atom'; - $this->showAtomTimeline( + $this->showRssTimeline( $this->notices, $title, - $id, - $link, + $this->group->homeUrl(), $subtitle, null, - $selfuri, $logo ); + break; + case 'atom': + + header('Content-Type: application/atom+xml; charset=utf-8'); + + try { + + $atom = new AtomNoticeFeed(); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($logo); + $atom->setUpdated('now'); + + $atom->addAuthorRaw($this->group->asAtomAuthor()); + $atom->setActivitySubject($this->group->asActivitySubject()); + + $atom->addLink($this->group->homeUrl()); + + $id = $this->arg('id'); + $aargs = array('format' => 'atom'); + if (!empty($id)) { + $aargs['id'] = $id; + } + + $atom->addLink( + $this->getSelfUri('ApiTimelineGroup', $aargs), + array('rel' => 'self', 'type' => 'application/atom+xml') + ); + + $atom->addEntryFromNotices($this->notices); + + $this->raw($atom->getString()); + + } catch (Atom10FeedException $e) { + $this->serverError( + 'Could not generate feed for group - ' . $e->getMessage() + ); + return; + } + break; case 'json': $this->showJsonTimeline($this->notices); diff --git a/classes/User_group.php b/classes/User_group.php index 1fbb50a6eb..379e6b7219 100644 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -355,6 +355,39 @@ class User_group extends Memcached_DataObject return $xs->getString(); } + function asAtomAuthor() + { + $xs = new XMLStringer(true); + + $xs->elementStart('author'); + $xs->element('name', null, $this->nickname); + $xs->element('uri', null, $this->permalink()); + $xs->elementEnd('author'); + + return $xs->getString(); + } + + function asActivitySubject() + { + $xs = new XMLStringer(true); + + $xs->elementStart('activity:subject'); + $xs->element('activity:object', null, 'http://activitystrea.ms/schema/1.0/group'); + $xs->element('id', null, $this->permalink()); + $xs->element('title', null, $this->getBestName()); + $xs->element( + 'link', array( + 'rel' => 'avatar', + 'href' => empty($this->homepage_logo) + ? User_group::defaultLogo(AVATAR_PROFILE_SIZE) + : $this->homepage_logo + ) + ); + $xs->elementEnd('activity:subject'); + + return $xs->getString(); + } + static function register($fields) { // MAGICALLY put fields into current scope diff --git a/lib/api.php b/lib/api.php index 8f1fe1ef71..494b595d17 100644 --- a/lib/api.php +++ b/lib/api.php @@ -1103,7 +1103,7 @@ class ApiAction extends Action } } - function serverError($msg, $code = 500, $content_type = 'json') + function serverError($msg, $code = 500, $content_type = 'xml') { $action = $this->trimmed('action'); diff --git a/lib/atom10feed.php b/lib/atom10feed.php index ccca76a09e..806a9684b7 100644 --- a/lib/atom10feed.php +++ b/lib/atom10feed.php @@ -51,6 +51,7 @@ class Atom10Feed extends XMLStringer public $xw; private $namespaces; private $authors; + private $subject; private $categories; private $contributors; private $generator; @@ -74,6 +75,7 @@ class Atom10Feed extends XMLStringer function __construct($indent = true) { parent::__construct($indent); $this->namespaces = array(); + $this->authors = array(); $this->links = array(); $this->entries = array(); $this->addNamespace('xmlns', 'http://www.w3.org/2005/Atom'); @@ -93,6 +95,64 @@ class Atom10Feed extends XMLStringer $this->namespaces = array_merge($this->namespaces, $ns); } + function addAuthor($name, $uri = null, $email = null) + { + $xs = new XMLStringer(true); + + $xs->elementStart('author'); + + if (!empty($name)) { + $xs->element('name', null, $name); + } else { + throw new Atom10FeedException( + 'author element must contain a name element.' + ); + } + + if (!is_null($uri)) { + $xs->element('uri', null, $uri); + } + + if (!is_null(email)) { + $xs->element('email', null, $email); + } + + $xs->elementEnd('author'); + + array_push($this->authors, $xs->getString()); + } + + /** + * Add an Author to the feed via raw XML string + * + * @param string $xmlAuthor An XML string representation author + * + * @return void + */ + function addAuthorRaw($xmlAuthor) + { + array_push($this->authors, $xmlAuthor); + } + + function renderAuthors() + { + foreach ($this->authors as $author) { + $this->raw($author); + } + } + + /** + * Add a activity feed subject via raw XML string + * + * @param string $xmlSubject An XML string representation of the subject + * + * @return void + */ + function setActivitySubject($xmlSubject) + { + $this->subject = $xmlSubject; + } + function getNamespaces() { return $this->namespaces; @@ -136,9 +196,9 @@ class Atom10Feed extends XMLStringer } } - function addEntryRaw($entry) + function addEntryRaw($xmlEntry) { - array_push($this->entries, $entry); + array_push($this->entries, $xmlEntry); } function addEntry($entry) @@ -164,6 +224,12 @@ class Atom10Feed extends XMLStringer $this->validate(); $this->initFeed(); + $this->renderAuthors(); + + if (!empty($this->subject)) { + $this->raw($this->subject); + } + $this->renderEntries(); $this->endFeed(); From 38f42d56bc5f2871d69d1df066a1f1d76e7cbc68 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Feb 2010 15:24:15 -0800 Subject: [PATCH 40/55] Session fix for PHP 5.3 configurations where cookies are excluded from $_REQUEST via request_order in php.ini (Fedora 12, MacPorts known to be affected) --- lib/util.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/util.php b/lib/util.php index a07fe49e33..209dc2254e 100644 --- a/lib/util.php +++ b/lib/util.php @@ -367,7 +367,8 @@ function common_current_user() if ($_cur === false) { - if (isset($_REQUEST[session_name()]) || (isset($_SESSION['userid']) && $_SESSION['userid'])) { + if (isset($_COOKIE[session_name()]) || isset($_GET[session_name()]) + || (isset($_SESSION['userid']) && $_SESSION['userid'])) { common_ensure_session(); $id = isset($_SESSION['userid']) ? $_SESSION['userid'] : false; if ($id) { From d6f1df8b76259acfc0d0566e8bf3610172b27884 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Feb 2010 15:30:23 -0800 Subject: [PATCH 41/55] fix for Atom notice output: correct check against conversation & current id --- classes/Notice.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/Notice.php b/classes/Notice.php index 924931e42b..73b22d58a0 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1016,7 +1016,7 @@ class Notice extends Memcached_DataObject } if (!empty($this->conversation) - && $this->conversation != $this->notice->id) { + && $this->conversation != $this->id) { $xs->element( 'link', array( 'rel' => 'ostatus:conversation', From f3a82e787c70e8cf749c79f22fe37ce6c9c9d4d3 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 12 Feb 2010 19:00:35 -0800 Subject: [PATCH 42/55] Add OStatus PuSH hub and Salmon links back into user and group feeds --- actions/apitimelinegroup.php | 14 ++++++- actions/apitimelineuser.php | 14 ++++++- lib/api.php | 1 - lib/atom10feed.php | 21 ++++++---- lib/atomgroupnoticefeed.php | 67 +++++++++++++++++++++++++++++++ lib/atomnoticefeed.php | 4 +- lib/atomusernoticefeed.php | 66 ++++++++++++++++++++++++++++++ plugins/OStatus/OStatusPlugin.php | 37 +++++++++-------- 8 files changed, 193 insertions(+), 31 deletions(-) create mode 100644 lib/atomgroupnoticefeed.php create mode 100644 lib/atomusernoticefeed.php diff --git a/actions/apitimelinegroup.php b/actions/apitimelinegroup.php index 45962fa76f..3c74e36b56 100644 --- a/actions/apitimelinegroup.php +++ b/actions/apitimelinegroup.php @@ -138,7 +138,19 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction try { - $atom = new AtomNoticeFeed(); + // If this was called using an integer ID, i.e.: using the canonical + // URL for this group's feed, then pass the Group object into the feed, + // so the OStatus plugin, and possibly other plugins, can access it. + // Feels sorta hacky. -- Z + + $atom = null; + $id = $this->arg('id'); + + if (strval(intval($id)) === strval($id)) { + $atom = new AtomGroupNoticeFeed($this->group); + } else { + $atom = new AtomGroupNoticeFeed(); + } $atom->setId($id); $atom->setTitle($title); diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index d20bb0d202..24752e45fd 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -148,7 +148,19 @@ class ApiTimelineUserAction extends ApiBareAuthAction header('Content-Type: application/atom+xml; charset=utf-8'); - $atom = new AtomNoticeFeed(); + // If this was called using an integer ID, i.e.: using the canonical + // URL for this user's feed, then pass the User object into the feed, + // so the OStatus plugin, and possibly other plugins, can access it. + // Feels sorta hacky. -- Z + + $atom = null; + $id = $this->arg('id'); + + if (strval(intval($id)) === strval($id)) { + $atom = new AtomUserNoticeFeed($this->user); + } else { + $atom = new AtomUserNoticeFeed(); + } $atom->setId($id); $atom->setTitle($title); diff --git a/lib/api.php b/lib/api.php index 494b595d17..22eef7436d 100644 --- a/lib/api.php +++ b/lib/api.php @@ -1154,7 +1154,6 @@ class ApiAction extends Action $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom', 'xml:lang' => 'en-US', 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0')); - Event::handle('StartApiAtom', array($this)); } function endTwitterAtom() diff --git a/lib/atom10feed.php b/lib/atom10feed.php index 806a9684b7..14a3beb83e 100644 --- a/lib/atom10feed.php +++ b/lib/atom10feed.php @@ -175,6 +175,8 @@ class Atom10Feed extends XMLStringer $this->element('updated', null, $this->updated); + $this->renderAuthors(); + $this->renderLinks(); } @@ -221,18 +223,21 @@ class Atom10Feed extends XMLStringer function getString() { - $this->validate(); + if (Event::handle('StartApiAtom', array($this))) { - $this->initFeed(); - $this->renderAuthors(); + $this->validate(); + $this->initFeed(); - if (!empty($this->subject)) { - $this->raw($this->subject); + if (!empty($this->subject)) { + $this->raw($this->subject); + } + + $this->renderEntries(); + $this->endFeed(); + + Event::handle('EndApiAtom', array($this)); } - $this->renderEntries(); - $this->endFeed(); - return $this->xw->outputMemory(); } diff --git a/lib/atomgroupnoticefeed.php b/lib/atomgroupnoticefeed.php new file mode 100644 index 0000000000..52ee4c7d6e --- /dev/null +++ b/lib/atomgroupnoticefeed.php @@ -0,0 +1,67 @@ +. + * + * @category Feed + * @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/ + */ + +if (!defined('STATUSNET')) +{ + exit(1); +} + +/** + * Class for group notice feeds. May contains a reference to the group. + * + * @category Feed + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class AtomGroupNoticeFeed extends AtomNoticeFeed +{ + private $group; + + /** + * Constructor + * + * @param Group $group the group for the feed (optional) + * @param boolean $indent flag to turn indenting on or off + * + * @return void + */ + function __construct($group = null, $indent = true) { + parent::__construct($indent); + $this->group = $group; + } + + function getGroup() + { + return $this->group; + } + +} diff --git a/lib/atomnoticefeed.php b/lib/atomnoticefeed.php index 34ed44b2ed..b7a60bde6e 100644 --- a/lib/atomnoticefeed.php +++ b/lib/atomnoticefeed.php @@ -2,7 +2,7 @@ /** * StatusNet, the distributed open-source microblogging tool * - * Class for building and Atom feed from a collection of notices + * Class for building an Atom feed from a collection of notices * * PHP version 5 * @@ -101,3 +101,5 @@ class AtomNoticeFeed extends Atom10Feed } } + + diff --git a/lib/atomusernoticefeed.php b/lib/atomusernoticefeed.php new file mode 100644 index 0000000000..9f224325c6 --- /dev/null +++ b/lib/atomusernoticefeed.php @@ -0,0 +1,66 @@ +. + * + * @category Feed + * @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/ + */ + +if (!defined('STATUSNET')) +{ + exit(1); +} + +/** + * Class for user notice feeds. May contain a reference to the user. + * + * @category Feed + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class AtomUserNoticeFeed extends AtomNoticeFeed +{ + private $user; + + /** + * Constructor + * + * @param User $user the user for the feed (optional) + * @param boolean $indent flag to turn indenting on or off + * + * @return void + */ + function __construct($user = null, $indent = true) { + parent::__construct($indent); + $this->user = $user; + } + + function getUser() + { + return $this->user; + } +} diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 8444c3d73d..bf7dde2967 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -63,9 +63,9 @@ class OStatusPlugin extends Plugin $m->connect('main/ostatus?nickname=:nickname', array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+')); $m->connect('main/ostatussub', - array('action' => 'ostatussub')); + array('action' => 'ostatussub')); $m->connect('main/ostatussub', - array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+')); + array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+')); // PuSH actions $m->connect('main/push/hub', array('action' => 'pushhub')); @@ -112,35 +112,34 @@ class OStatusPlugin extends Plugin * Set up a PuSH hub link to our internal link for canonical timeline * Atom feeds for users and groups. */ - function onStartApiAtom(Action $action) + function onStartApiAtom(AtomNoticeFeed $feed) { - if ($action instanceof ApiTimelineUserAction) { + $id = null; + + if ($feed instanceof AtomUserNoticeFeed) { $salmonAction = 'salmon'; - } else if ($action instanceof ApiTimelineGroupAction) { + $id = $feed->getUser()->id; + } else if ($feed instanceof AtomGroupNoticeFeed) { $salmonAction = 'salmongroup'; + $id = $feed->getGroup()->id; } else { return; } - $id = $action->arg('id'); - if (strval(intval($id)) === strval($id)) { - // Canonical form of id in URL? These are used for OStatus syndication. - + if (!empty($id)) { $hub = common_config('ostatus', 'hub'); if (empty($hub)) { // Updates will be handled through our internal PuSH hub. $hub = common_local_url('pushhub'); } - $action->element('link', array('rel' => 'hub', - 'href' => $hub)); + $feed->addLink($hub, array('rel' => 'hub')); // Also, we'll add in the salmon link $salmon = common_local_url($salmonAction, array('id' => $id)); - $action->element('link', array('rel' => 'salmon', - 'href' => $salmon)); + $feed->addLink($salmon, array('rel' => 'salmon')); } } - + /** * Add the feed settings page to the Connect Settings menu * @@ -201,7 +200,7 @@ class OStatusPlugin extends Plugin $output->element('a', array('href' => $url, 'class' => 'entity_remote_subscribe'), _m('OStatus')); - + $output->elementEnd('li'); } } @@ -221,25 +220,25 @@ class OStatusPlugin extends Plugin $w = new Webfinger; $endpoint_uri = ''; - + $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; } $xml = ''; $xml .= $notice->asAtomEntry(); - + $salmon = new Salmon(); $salmon->post($endpoint_uri, $xml); } From fc19179bc54de1837bbc64f052a93628be9c6a3d Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Sat, 13 Feb 2010 18:40:36 +0100 Subject: [PATCH 43/55] Added event hook to remote subscription --- EVENTS.txt | 10 +++++++++- lib/userprofile.php | 9 ++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/EVENTS.txt b/EVENTS.txt index 6bf12bf13f..69fe2ddccb 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 @@ -355,6 +355,14 @@ EndShowHeadElements: Right before the tag; put