Merge branch 'testing' of git@gitorious.org:statusnet/mainline into testing

This commit is contained in:
Evan Prodromou 2010-02-18 06:36:47 -05:00
commit b6e5d4ecc3
23 changed files with 869 additions and 240 deletions

View File

@ -1,4 +1,4 @@
\InitializePlugin: a chance to initialize a plugin in a complete environment
InitializePlugin: a chance to initialize a plugin in a complete environment
CleanupPlugin: a chance to cleanup a plugin at the end of a program
@ -722,3 +722,10 @@ StartRobotsTxt: Before outputting the robots.txt page
EndRobotsTxt: After the default robots.txt page (good place for customization)
- &$action: RobotstxtAction being shown
StartGetProfileUri: When determining the canonical URI for a given profile
- $profile: the current profile
- &$uri: the URI
EndGetProfileUri: After determining the canonical URI for a given profile
- $profile: the current profile
- &$uri: the URI

View File

@ -196,7 +196,8 @@ class ApiTimelineUserAction extends ApiBareAuthAction
$atom->addEntryFromNotices($this->notices);
$this->raw($atom->getString());
#$this->raw($atom->getString());
print $atom->getString(); // temporary for output buffering
break;
case 'json':

78
classes/Conversation.php Executable file
View File

@ -0,0 +1,78 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Data class for Conversations
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Data
* @package StatusNet
* @author Zach Copley <zach@status.net>
* @copyright 2010 StatusNet Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
require_once INSTALLDIR . '/classes/Memcached_DataObject.php';
class Conversation extends Memcached_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
public $__table = 'conversation'; // table name
public $id; // int(4) primary_key not_null
public $uri; // varchar(225) unique_key
public $created; // datetime not_null
public $modified; // timestamp not_null default_CURRENT_TIMESTAMP
/* Static get */
function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('conversation',$k,$v); }
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
/**
* Factory method for creating a new conversation
*
* @return Conversation the new conversation DO
*/
static function create()
{
$conv = new Conversation();
$conv->created = common_sql_now();
$id = $conv->insert();
if (empty($id)) {
common_log_db_error($conv, 'INSERT', __FILE__);
return null;
}
$orig = clone($conv);
$orig->uri = common_local_url('conversation', array('id' => $id));
$result = $orig->update($conv);
if (empty($result)) {
common_log_db_error($conv, 'UPDATE', __FILE__);
return null;
}
return $conv;
}
}

View File

@ -309,7 +309,8 @@ class Notice extends Memcached_DataObject
// the beginning of a new conversation.
if (empty($notice->conversation)) {
$notice->conversation = $notice->id;
$conv = Conversation::create();
$notice->conversation = $conv->id;
$changed = true;
}
@ -331,14 +332,15 @@ class Notice extends Memcached_DataObject
return $notice;
}
function blowOnInsert()
function blowOnInsert($conversation = false)
{
self::blow('profile:notice_ids:%d', $this->profile_id);
self::blow('public');
if ($this->conversation != $this->id) {
self::blow('notice:conversation_ids:%d', $this->conversation);
}
// XXX: Before we were blowing the casche only if the notice id
// was not the root of the conversation. What to do now?
self::blow('notice:conversation_ids:%d', $this->conversation);
if (!empty($this->repeat_of)) {
self::blow('notice:repeats:%d', $this->repeat_of);
@ -1015,28 +1017,29 @@ class Notice extends Memcached_DataObject
}
}
if (!empty($this->conversation)
&& $this->conversation != $this->id) {
$xs->element(
'link', array(
'rel' => 'ostatus:conversation',
'href' => common_local_url(
'conversation',
array('id' => $this->conversation)
)
if (!empty($this->conversation)) {
$conv = Conversation::staticGet('id', $this->conversation);
if (!empty($conv)) {
$xs->element(
'link', array(
'rel' => 'ostatus:conversation',
'href' => $conv->uri
)
);
}
}
$reply_ids = $this->getReplies();
foreach ($reply_ids as $id) {
$profile = Profile::staticGet('id', $id);
if (!empty($profile)) {
if (!empty($profile)) {
$xs->element(
'link', array(
'rel' => 'ostatus:attention',
'href' => $profile->getAcctUri()
'href' => $profile->getUri()
)
);
}

View File

@ -769,7 +769,7 @@ class Profile extends Memcached_DataObject
$xs->elementStart('author');
$xs->element('name', null, $this->nickname);
$xs->element('uri', null, $this->profileurl);
$xs->element('uri', null, $this->getUri());
$xs->elementEnd('author');
return $xs->getString();
@ -810,10 +810,7 @@ class Profile extends Memcached_DataObject
$xs->element(
'id',
null,
common_local_url(
'userbyid',
array('id' => $this->id)
)
$this->getUri()
);
$xs->element('title', null, $this->getBestName());
@ -835,9 +832,40 @@ class Profile extends Memcached_DataObject
return $xs->getString();
}
function getAcctUri()
/**
* Returns the best URI for a profile. Plugins may override.
*
* @return string $uri
*/
function getUri()
{
return $this->nickname . '@' . common_config('site', 'server');
$uri = null;
// check for a local user first
$user = User::staticGet('id', $this->id);
if (!empty($user)) {
$uri = common_local_url(
'userbyid',
array('id' => $user->id)
);
} else {
// give plugins a chance to set the URI
if (Event::handle('StartGetProfileUri', array($this, &$uri))) {
// return OMB profile if any
$remote = Remote_profile::staticGet('id', $this->id);
if (!empty($remote)) {
$uri = $remote->uri;
}
Event::handle('EndGetProfileUri', array($this, &$uri));
}
}
return $uri;
}
}

View File

@ -47,6 +47,16 @@ modified = 384
[consumer__keys]
consumer_key = K
[conversation]
id = 129
uri = 2
created = 142
modified = 384
[conversation__keys]
id = N
uri = U
[deleted_notice]
id = 129
profile_id = 129

View File

@ -633,3 +633,11 @@ create table inbox (
constraint primary key (user_id)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
create table conversation (
id integer auto_increment primary key comment 'unique identifier',
uri varchar(225) unique comment 'URI of the conversation',
created datetime not null comment 'date this record was created',
modified timestamp comment 'date this record was modified'
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;

View File

@ -91,10 +91,13 @@ $default =
'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
'breakout' => array('*' => 'shared'), // set global or per-handler queue breakout
// 'shared': use a shared queue for all sites
// 'handler': share each/this handler over multiple sites
// 'site': break out for each/this handler on this site
'breakout' => array(), // List queue specifiers to break out when using Stomp queue.
// Default will share all queues for all sites within each group.
// Specify as <group>/<queue> or <group>/<queue>/<site>,
// using nickname identifier as site.
//
// 'main/distrib' separate "distrib" queue covering all sites
// 'xmpp/xmppout/mysite' separate "xmppout" queue covering just 'mysite'
'max_retries' => 10, // drop messages after N failed attempts to process (Stomp)
'dead_letter_dir' => false, // set to directory to save dropped messages into (Stomp)
),

View File

@ -439,7 +439,7 @@ class HTMLOutputter extends XMLOutputter
{
if(Event::handle('StartCssLinkElement', array($this,&$src,&$theme,&$media))) {
$url = parse_url($src);
if( empty($url->scheme) && empty($url->host) && empty($url->query) && empty($url->fragment))
if( empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment']))
{
if(file_exists(Theme::file($src,$theme))){
$src = Theme::path($src, $theme);

View File

@ -55,27 +55,18 @@ abstract class IoMaster
if ($multiSite !== null) {
$this->multiSite = $multiSite;
}
if ($this->multiSite) {
$this->sites = StatusNet::findAllSites();
} else {
$this->sites = array(StatusNet::currentSite());
}
if (empty($this->sites)) {
throw new Exception("Empty status_network table, cannot init");
}
foreach ($this->sites as $site) {
StatusNet::switchSite($site);
$this->initManagers();
}
$this->initManagers();
}
/**
* Initialize IoManagers for the currently configured site
* which are appropriate to this instance.
* Initialize IoManagers which are appropriate to this instance;
* pass class names or instances into $this->instantiate().
*
* Pass class names into $this->instantiate()
* If setup and configuration may vary between sites in multi-site
* mode, it's the subclass's responsibility to set them up here.
*
* Switching site configurations is an acceptable side effect.
*/
abstract function initManagers();

View File

@ -492,30 +492,34 @@ class NoticeListItem extends Widget
break;
default:
$name = null;
$name = $source_name;
$url = null;
$ns = Notice_source::staticGet($this->notice->source);
if (Event::handle('StartNoticeSourceLink', array($this->notice, &$name, &$url, &$title))) {
$ns = Notice_source::staticGet($this->notice->source);
if ($ns) {
$name = $ns->name;
$url = $ns->url;
} else {
$app = Oauth_application::staticGet('name', $this->notice->source);
if ($app) {
$name = $app->name;
$url = $app->source_url;
if ($ns) {
$name = $ns->name;
$url = $ns->url;
} else {
$app = Oauth_application::staticGet('name', $this->notice->source);
if ($app) {
$name = $app->name;
$url = $app->source_url;
}
}
}
Event::handle('EndNoticeSourceLink', array($this->notice, &$name, &$url, &$title));
if (!empty($name) && !empty($url)) {
$this->out->elementStart('span', 'device');
$this->out->element('a', array('href' => $url,
'rel' => 'external'),
'rel' => 'external',
'title' => $title),
$name);
$this->out->elementEnd('span');
} else {
$this->out->element('span', 'device', $source_name);
$this->out->element('span', 'device', $name);
}
break;
}

View File

@ -63,7 +63,7 @@ class StompQueueManager extends QueueManager
$this->password = common_config('queue', 'stomp_password');
$this->base = common_config('queue', 'queue_basename');
$this->control = common_config('queue', 'control_channel');
$this->subscriptions = array($this->control => $this->control);
$this->breakout = common_config('queue', 'breakout');
}
/**
@ -75,28 +75,6 @@ class StompQueueManager extends QueueManager
return IoManager::INSTANCE_PER_PROCESS;
}
/**
* Record queue subscriptions we'll need to handle the current site.
*/
public function addSite()
{
$this->sites[] = StatusNet::currentSite();
// Set up handlers active for this site...
$this->initialize();
foreach ($this->activeGroups as $group) {
if (isset($this->groups[$group])) {
// Actual queues may be broken out or consolidated...
// Subscribe to all the target queues we'll need.
foreach ($this->groups[$group] as $transport => $class) {
$target = $this->queueName($transport);
$this->subscriptions[$target] = $target;
}
}
}
}
/**
* Optional; ping any running queue handler daemons with a notification
* such as announcing a new site to handle or requesting clean shutdown.
@ -166,14 +144,15 @@ class StompQueueManager extends QueueManager
$con = $this->cons[$idx];
$host = $con->getServer();
$result = $con->send($this->queueName($queue), $msg, $props);
$target = $this->queueName($queue);
$result = $con->send($target, $msg, $props);
if (!$result) {
$this->_log(LOG_ERR, "Error sending $rep to $queue queue on $host");
$this->_log(LOG_ERR, "Error sending $rep to $queue queue on $host $target");
return false;
}
$this->_log(LOG_DEBUG, "complete remote queueing $rep for $queue on $host");
$this->_log(LOG_DEBUG, "complete remote queueing $rep for $queue on $host $target");
$this->stats('enqueued', $queue);
return true;
}
@ -432,11 +411,42 @@ class StompQueueManager extends QueueManager
protected function doSubscribe(LiberalStomp $con)
{
$host = $con->getServer();
foreach ($this->subscriptions as $queue) {
$this->_log(LOG_INFO, "Subscribing to $queue on $host");
$con->subscribe($queue);
foreach ($this->subscriptions() as $sub) {
$this->_log(LOG_INFO, "Subscribing to $sub on $host");
$con->subscribe($sub);
}
}
/**
* Grab a full list of stomp-side queue subscriptions.
* Will include:
* - control broadcast channel
* - shared group queues for active groups
* - per-handler and per-site breakouts from $config['queue']['breakout']
* that are rooted in the active groups.
*
* @return array of strings
*/
protected function subscriptions()
{
$subs = array();
$subs[] = $this->control;
foreach ($this->activeGroups as $group) {
$subs[] = $this->base . $group;
}
foreach ($this->breakout as $spec) {
$parts = explode('/', $spec);
if (count($parts) < 2 || count($parts) > 3) {
common_log(LOG_ERR, "Bad queue breakout specifier $spec");
}
if (in_array($parts[0], $this->activeGroups)) {
$subs[] = $this->base . $spec;
}
}
return array_unique($subs);
}
/**
* Handle and acknowledge an event that's come in through a queue.
@ -612,32 +622,26 @@ class StompQueueManager extends QueueManager
}
/**
* Set us up with queue subscriptions for a new site added at runtime,
* (Re)load runtime configuration for a given site by nickname,
* triggered by a broadcast to the 'statusnet-control' topic.
*
* Configuration changes in database should update, but config
* files might not.
*
* @param array $frame Stomp frame
* @return bool true to continue; false to stop further processing.
*/
protected function updateSiteConfig($nickname)
{
if (empty($this->sites)) {
if ($nickname == common_config('site', 'nickname')) {
StatusNet::init(common_config('site', 'server'));
} else {
$this->_log(LOG_INFO, "Ignoring update ping for other site $nickname");
$sn = Status_network::staticGet($nickname);
if ($sn) {
$this->switchSite($nickname);
if (!in_array($nickname, $this->sites)) {
$this->addSite();
}
$this->stats('siteupdate');
} else {
$sn = Status_network::staticGet($nickname);
if ($sn) {
$this->switchSite($nickname);
if (!in_array($nickname, $this->sites)) {
$this->addSite();
}
// @fixme update subscriptions, if applicable
$this->stats('siteupdate');
} else {
$this->_log(LOG_ERR, "Ignoring ping for unrecognized new site $nickname");
}
$this->_log(LOG_ERR, "Ignoring ping for unrecognized new site $nickname");
}
}
@ -646,24 +650,25 @@ class StompQueueManager extends QueueManager
* group name for this queue to give eg:
*
* /queue/statusnet/main
* /queue/statusnet/main/distrib
* /queue/statusnet/xmpp/xmppout/site01
*
* @param string $queue
* @return string
*/
protected function queueName($queue)
{
$base = common_config('queue', 'queue_basename');
$group = $this->queueGroup($queue);
$breakout = $this->breakoutMode($queue);
if ($breakout == 'shared') {
return $base . "$group";
} else if ($breakout == 'handler') {
return $base . "$group/$queue";
} else if ($breakout == 'site') {
$site = StatusNet::currentSite();
return $base . "$group/$queue/$site";
$site = StatusNet::currentSite();
$specs = array("$group/$queue/$site",
"$group/$queue");
foreach ($specs as $spec) {
if (in_array($spec, $this->breakout)) {
return $this->base . $spec;
}
}
throw Exception("Unrecognized queue breakout mode '$breakout' for '$queue'");
return $this->base . $group;
}
/**

View File

@ -96,7 +96,7 @@ class MinifyPlugin extends Plugin
&& is_null(common_config('theme', 'path'))
&& is_null(common_config('theme', 'server'));
$url = parse_url($src);
if( empty($url->scheme) && empty($url->host) && empty($url->query) && empty($url->fragment))
if( empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment']))
{
if(!isset($theme)) {
$theme = common_config('site', 'theme');

View File

@ -289,4 +289,17 @@ class OStatusPlugin extends Plugin
$action->script(common_path('plugins/OStatus/js/ostatus.js'));
return true;
}
function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
{
if ($notice->source == 'ostatus') {
$bits = parse_url($notice->uri);
$domain = $bits['host'];
$name = $domain;
$url = $notice->uri;
$title = sprintf(_m("Sent from %s via OStatus"), $domain);
return false;
}
}
}

View File

@ -59,6 +59,9 @@ class PushCallbackAction extends Action
}
$post = file_get_contents('php://input');
// @fixme Queue this to a background process; we should return
// as quickly as possible from a distribution POST.
$profile->postUpdates($post, $hmac);
}

View File

@ -44,7 +44,7 @@ class PushHubAction extends Action
// PHP converts '.'s in incoming var names to '_'s.
// It also merges multiple values, which'll break hub.verify and hub.topic for publishing
// @fixme handle multiple args
$arg = str_replace('.', '_', $arg);
$arg = str_replace('hub.', 'hub_', $arg);
return parent::arg($arg, $def);
}
@ -96,7 +96,11 @@ class PushHubAction extends Action
$sub = new HubSub();
$sub->topic = $feed;
$sub->callback = $callback;
$sub->verify_token = $this->arg('hub.verify_token', null);
$sub->secret = $this->arg('hub.secret', null);
if (strlen($sub->secret) > 200) {
throw new ClientException("hub.secret must be no longer than 200 chars", 400);
}
$sub->setLease(intval($this->arg('hub.lease_seconds')));
// @fixme check for feeds we don't manage

View File

@ -218,14 +218,10 @@ class Ostatus_profile extends Memcached_DataObject
$profile->query('BEGIN');
// Awful hack! Awful hack!
$profile->verify = common_good_rand(16);
$profile->secret = common_good_rand(32);
try {
$local = $munger->profile();
if ($entity->isGroup()) {
if ($profile->isGroup()) {
$group = new User_group();
$group->nickname = $local->nickname . '@remote'; // @fixme
$group->fullname = $local->fullname;
@ -245,31 +241,31 @@ class Ostatus_profile extends Memcached_DataObject
$profile->profile_id = $local->id;
}
$profile->created = sql_common_date();
$profile->lastupdate = sql_common_date();
$profile->created = common_sql_now();
$profile->lastupdate = common_sql_now();
$result = $profile->insert();
if (empty($result)) {
throw new FeedDBException($profile);
}
$entity->query('COMMIT');
$profile->query('COMMIT');
} catch (FeedDBException $e) {
common_log_db_error($e->obj, 'INSERT', __FILE__);
$entity->query('ROLLBACK');
$profile->query('ROLLBACK');
return false;
}
$avatar = $munger->getAvatar();
if ($avatar) {
try {
$this->updateAvatar($avatar);
$profile->updateAvatar($avatar);
} catch (Exception $e) {
common_log(LOG_ERR, "Exception setting OStatus avatar: " .
$e->getMessage());
}
}
return $entity;
return $profile;
}
/**
@ -283,8 +279,10 @@ class Ostatus_profile extends Memcached_DataObject
// ripped from oauthstore.php (for old OMB client)
$temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
copy($url, $temp_filename);
$imagefile = new ImageFile($profile->id, $temp_filename);
$filename = Avatar::filename($profile->id,
// @fixme should we be using different ids?
$imagefile = new ImageFile($this->id, $temp_filename);
$filename = Avatar::filename($this->id,
image_type_to_extension($imagefile->type),
null,
common_timestamp());
@ -376,17 +374,59 @@ class Ostatus_profile extends Memcached_DataObject
* The hub will later send us a confirmation POST to /main/push/callback.
*
* @return bool true on success, false on failure
* @throws ServerException if feed state is not valid
*/
public function subscribe($mode='subscribe')
{
if (common_config('feedsub', 'nohub')) {
// Fake it! We're just testing remote feeds w/o hubs.
return true;
if ($this->sub_state != '') {
throw new ServerException("Attempting to start PuSH subscription to feed in state $this->sub_state");
}
// @fixme use the verification token
#$token = md5(mt_rand() . ':' . $this->feeduri);
#$this->verify_token = $token;
#$this->update(); // @fixme
if (empty($this->huburi)) {
if (common_config('feedsub', 'nohub')) {
// Fake it! We're just testing remote feeds w/o hubs.
return true;
} else {
throw new ServerException("Attempting to start PuSH subscription for feed with no hub");
}
}
return $this->doSubscribe('subscribe');
}
/**
* Send a PuSH unsubscription request to the hub for this feed.
* The hub will later send us a confirmation POST to /main/push/callback.
*
* @return bool true on success, false on failure
* @throws ServerException if feed state is not valid
*/
public function unsubscribe() {
if ($this->sub_state != 'active') {
throw new ServerException("Attempting to end PuSH subscription to feed in state $this->sub_state");
}
if (empty($this->huburi)) {
if (common_config('feedsub', 'nohub')) {
// Fake it! We're just testing remote feeds w/o hubs.
return true;
} else {
throw new ServerException("Attempting to end PuSH subscription for feed with no hub");
}
}
return $this->doSubscribe('unsubscribe');
}
protected function doSubscribe($mode)
{
$orig = clone($this);
$this->verify_token = common_good_rand(16);
if ($mode == 'subscribe') {
$this->secret = common_good_rand(32);
}
$this->sub_state = $mode;
$this->update($orig);
unset($orig);
try {
$callback = common_local_url('pushcallback', array('feed' => $this->id));
$headers = array('Content-Type: application/x-www-form-urlencoded');
@ -416,6 +456,13 @@ class Ostatus_profile extends Memcached_DataObject
} catch (Exception $e) {
// wtf!
common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri");
$orig = clone($this);
$this->verify_token = null;
$this->sub_state = null;
$this->update($orig);
unset($orig);
return false;
}
}
@ -460,16 +507,6 @@ class Ostatus_profile extends Memcached_DataObject
return $this->update($original);
}
/**
* Send a PuSH unsubscription request to the hub for this feed.
* The hub will later send us a confirmation POST to /main/push/callback.
*
* @return bool true on success, false on failure
*/
public function unsubscribe() {
return $this->subscribe('unsubscribe');
}
/**
* Send an Activity Streams notification to the remote Salmon endpoint,
* if so configured.
@ -561,84 +598,339 @@ class Ostatus_profile extends Memcached_DataObject
* Currently assumes that all items in the feed are new,
* coming from a PuSH hub.
*
* @param string $xml source of Atom or RSS feed
* @param string $post source of Atom or RSS feed
* @param string $hmac X-Hub-Signature header, if present
*/
public function postUpdates($xml, $hmac)
public function postUpdates($post, $hmac)
{
common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml");
common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $post");
if ($this->secret) {
if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) {
$their_hmac = strtolower($matches[1]);
$our_hmac = hash_hmac('sha1', $xml, $this->secret);
if ($their_hmac !== $our_hmac) {
common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac");
return;
}
} else {
common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'");
return;
}
} else if ($hmac) {
common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'");
if ($this->sub_state != 'active') {
common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH for inactive feed $this->feeduri (in state '$this->sub_state')");
return;
}
require_once "XML/Feed/Parser.php";
$feed = new XML_Feed_Parser($xml, false, false, true);
$munger = new FeedMunger($feed);
$hits = 0;
foreach ($feed as $index => $entry) {
// @fixme this might sort in wrong order if we get multiple updates
$notice = $munger->notice($index);
// Double-check for oldies
// @fixme this could explode horribly for multiple feeds on a blog. sigh
$dupe = Notice::staticGet('uri', $notice->uri);
if (!empty($dupe)) {
common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}");
continue;
}
// @fixme need to ensure that groups get handled correctly
$saved = Notice::saveNew($notice->profile_id,
$notice->content,
'ostatus',
array('is_local' => Notice::REMOTE_OMB,
'uri' => $notice->uri,
'lat' => $notice->lat,
'lon' => $notice->lon,
'location_ns' => $notice->location_ns,
'location_id' => $notice->location_id));
/*
common_log(LOG_DEBUG, "going to check group delivery...");
if ($this->group_id) {
$group = User_group::staticGet($this->group_id);
if ($group) {
common_log(LOG_INFO, __METHOD__ . ": saving to local shadow group $group->id $group->nickname");
$groups = array($group);
} else {
common_log(LOG_INFO, __METHOD__ . ": lost the local shadow group?");
}
} else {
common_log(LOG_INFO, __METHOD__ . ": no local shadow groups");
$groups = array();
}
common_log(LOG_DEBUG, "going to add to inboxes...");
$notice->addToInboxes($groups, array());
common_log(LOG_DEBUG, "added to inboxes.");
*/
$hits++;
if ($post === '') {
common_log(LOG_ERR, __METHOD__ . ": ignoring empty post");
return;
}
if ($hits == 0) {
common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml");
if (!$this->validatePushSig($post, $hmac)) {
// Per spec we silently drop input with a bad sig,
// while reporting receipt to the server.
return;
}
$feed = new DOMDocument();
if (!$feed->loadXML($post)) {
// @fixme might help to include the err message
common_log(LOG_ERR, __METHOD__ . ": ignoring invalid XML");
return;
}
$entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
if ($entries->length == 0) {
common_log(LOG_ERR, __METHOD__ . ": no entries in feed update, ignoring");
return;
}
for ($i = 0; $i < $entries->length; $i++) {
$entry = $entries->item($i);
$this->processEntry($entry, $feed);
}
}
/**
* Validate the given Atom chunk and HMAC signature against our
* shared secret that was set up at subscription time.
*
* If we don't have a shared secret, there should be no signature.
* If we we do, our the calculated HMAC should match theirs.
*
* @param string $post raw XML source as POSTed to us
* @param string $hmac X-Hub-Signature HTTP header value, or empty
* @return boolean true for a match
*/
protected function validatePushSig($post, $hmac)
{
if ($this->secret) {
if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) {
$their_hmac = strtolower($matches[1]);
$our_hmac = hash_hmac('sha1', $post, $this->secret);
if ($their_hmac === $our_hmac) {
return true;
}
common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac");
} else {
common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'");
}
} else {
if (empty($hmac)) {
return true;
} else {
common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'");
}
}
return false;
}
/**
* Process a posted entry from this feed source.
*
* @param DOMElement $entry
* @param DOMElement $feed for context
*/
protected function processEntry($entry, $feed)
{
$activity = new Activity($entry, $feed);
$debug = var_export($activity, true);
common_log(LOG_DEBUG, $debug);
if ($activity->verb == ActivityVerb::POST) {
$this->processPost($activity);
} else {
common_log(LOG_INFO, "Ignoring activity with unrecognized verb $activity->verb");
}
}
/**
* Process an incoming post activity from this remote feed.
* @param Activity $activity
*/
protected function processPost($activity)
{
if ($this->isGroup()) {
// @fixme validate these profiles in some way!
$oprofile = $this->ensureActorProfile($activity);
} else {
$actorUri = $this->getActorProfileURI($activity);
if ($actorUri == $this->homeuri) {
// @fixme check if profile info has changed and update it
} else {
// @fixme drop or reject the messages once we've got the canonical profile URI recorded sanely
common_log(LOG_INFO, "OStatus: Warning: non-group post with unexpected author: $actorUri expected $this->homeuri");
//return;
}
$oprofile = $this;
}
if ($activity->object->link) {
$sourceUri = $activity->object->link;
} else if (preg_match('!^https?://!', $activity->object->id)) {
$sourceUri = $activity->object->id;
} else {
common_log(LOG_INFO, "OStatus: ignoring post with no source link: id $activity->object->id");
return;
}
$dupe = Notice::staticGet('uri', $sourceUri);
if ($dupe) {
common_log(LOG_INFO, "OStatus: ignoring duplicate post: $noticeLink");
return;
}
// @fixme sanitize and save HTML content if available
$content = $activity->object->title;
$params = array('is_local' => Notice::REMOTE_OMB,
'uri' => $sourceUri);
$location = $this->getEntryLocation($activity->entry);
if ($location) {
$params['lat'] = $location->lat;
$params['lon'] = $location->lon;
if ($location->location_id) {
$params['location_ns'] = $location->location_ns;
$params['location_id'] = $location->location_id;
}
}
// @fixme save detailed ostatus source info
// @fixme ensure that groups get handled correctly
$saved = Notice::saveNew($oprofile->localProfile()->id,
$content,
'ostatus',
$params);
}
/**
* Parse location given as a GeoRSS-simple point, if provided.
* http://www.georss.org/simple
*
* @param feed item $entry
* @return mixed Location or false
*/
function getLocation($dom)
{
$points = $dom->getElementsByTagNameNS('http://www.georss.org/georss', 'point');
for ($i = 0; $i < $points->length; $i++) {
$point = $points->item(0)->textContent;
$point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace"
$point = preg_replace('/\s+/', ' ', $point);
$point = trim($point);
$coords = explode(' ', $point);
if (count($coords) == 2) {
list($lat, $lon) = $coords;
if (is_numeric($lat) && is_numeric($lon)) {
common_log(LOG_INFO, "Looking up location for $lat $lon from georss");
return Location::fromLatLon($lat, $lon);
}
}
common_log(LOG_ERR, "Ignoring bogus georss:point value $point");
}
return false;
}
/**
* Get an appropriate avatar image source URL, if available.
*
* @param ActivityObject $actor
* @param DOMElement $feed
* @return string
*/
function getAvatar($actor, $feed)
{
$url = '';
$icon = '';
if ($actor->avatar) {
$url = trim($actor->avatar);
}
if (!$url) {
// Check <atom:logo> and <atom:icon> on the feed
$els = $feed->childNodes();
if ($els && $els->length) {
for ($i = 0; $i < $els->length; $i++) {
$el = $els->item($i);
if ($el->namespaceURI == Activity::ATOM) {
if (empty($url) && $el->localName == 'logo') {
$url = trim($el->textContent);
break;
}
if (empty($icon) && $el->localName == 'icon') {
// Use as a fallback
$icon = trim($el->textContent);
}
}
}
}
if ($icon && !$url) {
$url = $icon;
}
}
if ($url) {
$opts = array('allowed_schemes' => array('http', 'https'));
if (Validate::uri($url, $opts)) {
return $url;
}
}
return common_path('plugins/OStatus/images/96px-Feed-icon.svg.png');
}
/**
* @fixme move off of ostatus_profile or static?
*/
function ensureActorProfile($activity)
{
$profile = $this->getActorProfile($activity);
if (!$profile) {
$profile = $this->createActorProfile($activity);
}
return $profile;
}
/**
* @param Activity $activity
* @return mixed matching Ostatus_profile or false if none known
*/
function getActorProfile($activity)
{
$homeuri = $this->getActorProfileURI($activity);
return Ostatus_profile::staticGet('homeuri', $homeuri);
}
/**
* @param Activity $activity
* @return string
* @throws ServerException
*/
function getActorProfileURI($activity)
{
$opts = array('allowed_schemes' => array('http', 'https'));
$actor = $activity->actor;
if ($actor->id && Validate::uri($actor->id, $opts)) {
return $actor->id;
}
if ($actor->link && Validate::uri($actor->link, $opts)) {
return $actor->link;
}
throw new ServerException("No author ID URI found");
}
/**
*
*/
function createActorProfile($activity)
{
$actor = $activity->actor();
$homeuri = $this->getActivityProfileURI($activity);
$nickname = $this->getAuthorNick($activity);
$avatar = $this->getAvatar($actor, $feed);
$profile = new Profile();
$profile->nickname = $nickname;
$profile->fullname = $actor->displayName;
$profile->homepage = $actor->link; // @fixme
$profile->profileurl = $homeuri;
// @fixme bio
// @fixme tags/categories
// @fixme location?
// @todo tags from categories
// @todo lat/lon/location?
$ok = $profile->insert();
if ($ok) {
$this->updateAvatar($profile, $avatar);
} else {
throw new ServerException("Can't save local profile");
}
// @fixme either need to do feed discovery here
// or need to split out some of the feed stuff
// so we can leave it empty until later.
$oprofile = new Ostatus_profile();
$oprofile->homeuri = $homeuri;
$oprofile->profile_id = $profile->id;
$ok = $oprofile->insert();
if ($ok) {
return $oprofile;
} else {
throw new ServerException("Can't save OStatus profile");
}
}
/**
* @fixme move this into Activity?
* @param Activity $activity
* @return string
*/
function getAuthorNick($activity)
{
// @fixme not technically part of the actor?
foreach (array($activity->entry, $activity->feed) as $source) {
$author = ActivityUtil::child($source, 'author', Activity::ATOM);
if ($author) {
$name = ActivityUtil::child($author, 'name', Activity::ATOM);
if ($name) {
return trim($name->textContent);
}
}
}
return false;
}
}

View File

@ -106,12 +106,16 @@ class ActivityUtils
static function child($element, $tag, $namespace=self::ATOM)
{
$els = $element->getElementsByTagnameNS($namespace, $tag);
$els = $element->childNodes;
if (empty($els) || $els->length == 0) {
return null;
} else {
return $els->item(0);
for ($i = 0; $i < $els->length; $i++) {
$el = $els->item($i);
if ($el->localName == $tag && $el->namespaceURI == $namespace) {
return $el;
}
}
}
}
@ -243,6 +247,14 @@ class ActivityObject
// XXX: grab PoCo stuff
}
// Some per-type attributes...
if ($this->type == self::PERSON || $this->type == self::GROUP) {
$this->displayName = $this->title;
// @fixme we may have multiple avatars with different resolutions specified
$this->avatar = ActivityUtils::getLink($element, 'avatar');
}
}
private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM)

View File

@ -258,11 +258,12 @@ class FeedMunger
{
// hack hack hack
// should get profile for this entry's author...
$remote = Ostatus_profile::staticGet('feeduri', $this->getSelfLink());
if ($feed) {
return $feed->profile_id;
$feeduri = $this->getSelfLink();
$remote = Ostatus_profile::staticGet('feeduri', $feeduri);
if ($remote) {
return $remote->profile_id;
} else {
throw new Exception("Can't find feed profile");
throw new Exception("Can't find feed profile for $feeduri");
}
}

View File

@ -0,0 +1,150 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2010, StatusNet, Inc.
*
* Debugging helper plugin -- records detailed data on POSTs to log
*
* PHP version 5
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Sample
* @package StatusNet
* @author Brion Vibber <brionv@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
class PostDebugPlugin extends Plugin
{
/**
* Set to a directory to dump individual items instead of
* sending to the debug log
*/
public $dir=false;
public function onArgsInitialize(&$args)
{
if (isset($_SERVER['REQUEST_METHOD']) &&
$_SERVER['REQUEST_METHOD'] == 'POST') {
$this->doDebug();
}
}
public function onPluginVersion(&$versions)
{
$versions[] = array('name' => 'PostDebug',
'version' => STATUSNET_VERSION,
'author' => 'Brion Vibber',
'homepage' => 'http://status.net/wiki/Plugin:PostDebug',
'rawdescription' =>
_m('Debugging tool to record request details on POST.'));
return true;
}
protected function doDebug()
{
$data = array('timestamp' => gmdate('r'),
'remote_addr' => @$_SERVER['REMOTE_ADDR'],
'url' => @$_SERVER['REQUEST_URI'],
'have_session' => common_have_session(),
'logged_in' => common_logged_in(),
'is_real_login' => common_is_real_login(),
'user' => common_logged_in() ? common_current_user()->nickname : null,
'headers' => $this->getHttpHeaders(),
'post_data' => $this->sanitizePostData($_POST));
$this->saveDebug($data);
}
protected function saveDebug($data)
{
$output = var_export($data, true);
if ($this->dir) {
$file = $this->dir . DIRECTORY_SEPARATOR . $this->logFileName();
file_put_contents($file, $output);
} else {
common_log(LOG_DEBUG, "PostDebug: $output");
}
}
protected function logFileName()
{
$base = common_request_id();
$base = preg_replace('/^(.+?) .*$/', '$1', $base);
$base = str_replace(':', '-', $base);
$base = rawurlencode($base);
return $base;
}
protected function getHttpHeaders()
{
if (function_exists('getallheaders')) {
$headers = getallheaders();
} else {
$headers = array();
$prefix = 'HTTP_';
$prefixLen = strlen($prefix);
foreach ($_SERVER as $key => $val) {
if (substr($key, 0, $prefixLen) == $prefix) {
$header = $this->normalizeHeader(substr($key, $prefixLen));
$headers[$header] = $val;
}
}
}
foreach ($headers as $header => $val) {
if (strtolower($header) == 'cookie') {
$headers[$header] = $this->sanitizeCookies($val);
}
}
return $headers;
}
protected function normalizeHeader($key)
{
return implode('-',
array_map('ucfirst',
explode("_",
strtolower($key))));
}
function sanitizeCookies($val)
{
$blacklist = array(session_name(), 'rememberme');
foreach ($blacklist as $name) {
$val = preg_replace("/(^|;\s*)({$name}=)(.*?)(;|$)/",
"$1$2########$4",
$val);
}
return $val;
}
function sanitizePostData($data)
{
$blacklist = array('password', 'confirm', 'token');
foreach ($data as $key => $val) {
if (in_array($key, $blacklist)) {
$data[$key] = '########';
}
}
return $data;
}
}

View File

@ -1,7 +1,7 @@
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2008, 2009, StatusNet, Inc.
* Copyright (C) 2008-2010 StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -33,11 +33,15 @@ function add_twitter_user($twitter_id, $screen_name)
// repoed, and things like that.
$luser = Foreign_user::getForeignUser($twitter_id, TWITTER_SERVICE);
$result = $luser->delete();
if ($result != false) {
common_log(LOG_INFO,
"Twitter bridge - removed old Twitter user: $screen_name ($twitter_id).");
if (!empty($luser)) {
$result = $luser->delete();
if ($result != false) {
common_log(
LOG_INFO,
"Twitter bridge - removed old Twitter user: $screen_name ($twitter_id)."
);
}
}
$fuser = new Foreign_user();

View File

@ -131,8 +131,7 @@ class TwitterauthorizationAction extends Action
} else if ($this->arg('connect')) {
$this->connectNewUser();
} else {
common_debug('Twitter Connect Plugin - ' .
print_r($this->args, true));
common_debug('Twitter bridge - ' . print_r($this->args, true));
$this->showForm(_('Something weird happened.'),
$this->trimmed('newname'));
}
@ -172,9 +171,15 @@ class TwitterauthorizationAction extends Action
$auth_link = $client->getAuthorizeLink($req_tok, $this->signin);
} catch (OAuthClientException $e) {
$msg = sprintf('OAuth client error - code: %1s, msg: %2s',
$e->getCode(), $e->getMessage());
$this->serverError(_m('Couldn\'t link your Twitter account.'));
$msg = sprintf(
'OAuth client error - code: %1s, msg: %2s',
$e->getCode(),
$e->getMessage()
);
common_log(LOG_INFO, 'Twitter bridge - ' . $msg);
$this->serverError(
_m('Couldn\'t link your Twitter account.')
);
}
common_redirect($auth_link);
@ -192,7 +197,9 @@ class TwitterauthorizationAction extends Action
// token we sent them
if ($_SESSION['twitter_request_token'] != $this->oauth_token) {
$this->serverError(_m('Couldn\'t link your Twitter account.'));
$this->serverError(
_m('Couldn\'t link your Twitter account: oauth_token mismatch.')
);
}
$twitter_user = null;
@ -212,9 +219,15 @@ class TwitterauthorizationAction extends Action
$twitter_user = $client->verifyCredentials();
} catch (OAuthClientException $e) {
$msg = sprintf('OAuth client error - code: %1$s, msg: %2$s',
$e->getCode(), $e->getMessage());
$this->serverError(_m('Couldn\'t link your Twitter account.'));
$msg = sprintf(
'OAuth client error - code: %1$s, msg: %2$s',
$e->getCode(),
$e->getMessage()
);
common_log(LOG_INFO, 'Twitter bridge - ' . $msg);
$this->serverError(
_m('Couldn\'t link your Twitter account.')
);
}
if (common_logged_in()) {
@ -279,7 +292,7 @@ class TwitterauthorizationAction extends Action
if (empty($flink_id)) {
common_log_db_error($flink, 'INSERT', __FILE__);
$this->serverError(_('Couldn\'t link your Twitter account.'));
$this->serverError(_('Couldn\'t link your Twitter account.'));
}
return $flink_id;

View File

@ -126,8 +126,7 @@ class QueueDaemon extends SpawningDaemon
class QueueMaster extends IoMaster
{
/**
* Initialize IoManagers for the currently configured site
* which are appropriate to this instance.
* Initialize IoManagers which are appropriate to this instance.
*/
function initManagers()
{