summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--actions/apitimelinefavorites.php69
-rw-r--r--actions/apitimelinefriends.php74
-rw-r--r--actions/apitimelinegroup.php2
-rw-r--r--actions/apitimelinehome.php60
-rw-r--r--actions/apitimelinementions.php34
-rw-r--r--actions/apitimelinepublic.php31
-rw-r--r--actions/apitimelineretweetsofme.php36
-rw-r--r--actions/apitimelinetag.php51
-rw-r--r--actions/apitimelineuser.php48
-rw-r--r--actions/showgroup.php4
-rw-r--r--classes/Nonce.php15
-rw-r--r--classes/Notice.php56
-rw-r--r--classes/Profile.php85
-rw-r--r--classes/User_group.php4
-rw-r--r--classes/statusnet.links.ini7
-rw-r--r--lib/api.php18
-rw-r--r--lib/atom10entry.php106
-rw-r--r--lib/atom10feed.php227
-rw-r--r--lib/atomnoticefeed.php103
-rw-r--r--lib/default.php1
-rw-r--r--lib/queuemanager.php36
-rw-r--r--lib/spawningdaemon.php2
-rw-r--r--lib/stompqueuemanager.php44
-rw-r--r--lib/util.php2
-rw-r--r--plugins/OStatus/OStatusPlugin.php141
-rw-r--r--plugins/OStatus/actions/feedsubsettings.php45
-rw-r--r--plugins/OStatus/actions/hostmeta.php42
-rw-r--r--plugins/OStatus/actions/ostatusinit.php128
-rw-r--r--plugins/OStatus/actions/ostatussub.php226
-rw-r--r--plugins/OStatus/actions/pushcallback.php40
-rw-r--r--plugins/OStatus/actions/salmon.php81
-rw-r--r--plugins/OStatus/actions/webfinger.php77
-rw-r--r--plugins/OStatus/classes/Feedinfo.php345
-rw-r--r--plugins/OStatus/classes/HubSub.php2
-rw-r--r--plugins/OStatus/classes/Ostatus_profile.php644
-rw-r--r--plugins/OStatus/lib/activity.php85
-rw-r--r--plugins/OStatus/lib/feedmunger.php51
-rw-r--r--plugins/OStatus/lib/hubdistribqueuehandler.php116
-rw-r--r--plugins/OStatus/lib/huboutqueuehandler.php2
-rw-r--r--plugins/OStatus/lib/salmon.php64
-rw-r--r--plugins/OStatus/lib/webfinger.php143
-rw-r--r--plugins/OStatus/lib/xrd.php183
-rw-r--r--theme/base/css/display.css23
-rw-r--r--theme/default/css/display.css14
-rw-r--r--theme/identica/css/display.css11
45 files changed, 3002 insertions, 576 deletions
diff --git a/actions/apitimelinefavorites.php b/actions/apitimelinefavorites.php
index 1027d97d4..f7f900ddf 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 4e3827bae..0af04fe4f 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');
+
+ $atom = new AtomNoticeFeed();
+
+ $atom->setId($id);
+ $atom->setTitle($title);
+ $atom->setSubtitle($subtitle);
+ $atom->setLogo($logo);
+ $atom->setUpdated('now');
- 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->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/apitimelinegroup.php b/actions/apitimelinegroup.php
index af414c680..fd2ed9ff9 100644
--- a/actions/apitimelinegroup.php
+++ b/actions/apitimelinegroup.php
@@ -130,7 +130,7 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction
case 'atom':
$selfuri = common_root_url() .
'api/statusnet/groups/timeline/' .
- $this->group->nickname . '.atom';
+ $this->group->id . '.atom';
$this->showAtomTimeline(
$this->notices,
$title,
diff --git a/actions/apitimelinehome.php b/actions/apitimelinehome.php
index 828eae6cf..ae4168070 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');
+
+ $atom = new AtomNoticeFeed();
- 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->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 9dc2162cc..d2e31d0bd 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 3f4a46c0f..c1fa72a3e 100644
--- a/actions/apitimelinepublic.php
+++ b/actions/apitimelinepublic.php
@@ -75,6 +75,10 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
$this->notices = $this->getNotices();
+ if ($this->since) {
+ throw new ServerException("since parameter is disabled for performance; use since_id", 403);
+ }
+
return true;
}
@@ -118,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);
@@ -145,7 +166,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()) {
diff --git a/actions/apitimelineretweetsofme.php b/actions/apitimelineretweetsofme.php
index e4b09e9bd..26706a75e 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 1427d23b6..5b6ded4c0 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,23 +113,52 @@ 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);
break;
diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php
index ed9104905..d20bb0d20 100644
--- a/actions/apitimelineuser.php
+++ b/actions/apitimelineuser.php
@@ -145,19 +145,47 @@ class ApiTimelineUserAction extends ApiBareAuthAction
);
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(
+ 'showstream',
+ array('nickname' => $this->user->nickname)
+ )
+ );
+
$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;
}
- $this->showAtomTimeline(
- $this->notices, $title, $id, $link,
- $subtitle, $suplink, $selfuri, $logo
+
+ $atom->addLink(
+ $this->getSelfUri('ApiTimelineUser', $aargs),
+ 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);
+
+ $this->raw($atom->getString());
+
break;
case 'json':
$this->showJsonTimeline($this->notices);
diff --git a/actions/showgroup.php b/actions/showgroup.php
index 8042a4951..eb1238902 100644
--- a/actions/showgroup.php
+++ b/actions/showgroup.php
@@ -330,13 +330,13 @@ class ShowgroupAction extends GroupDesignAction
new Feed(Feed::RSS2,
common_local_url('ApiTimelineGroup',
array('format' => 'rss',
- 'id' => $this->group->nickname)),
+ 'id' => $this->group->id)),
sprintf(_('Notice feed for %s group (RSS 2.0)'),
$this->group->nickname)),
new Feed(Feed::ATOM,
common_local_url('ApiTimelineGroup',
array('format' => 'atom',
- 'id' => $this->group->nickname)),
+ 'id' => $this->group->id)),
sprintf(_('Notice feed for %s group (Atom)'),
$this->group->nickname)),
new Feed(Feed::FOAF,
diff --git a/classes/Nonce.php b/classes/Nonce.php
index 486a65a3c..2f8ab00b5 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/Notice.php b/classes/Notice.php
index fca1c599c..924931e42 100644
--- a/classes/Notice.php
+++ b/classes/Notice.php
@@ -783,7 +783,7 @@ class Notice extends Memcached_DataObject
$result = $gi->insert();
- if (!result) {
+ if (!$result) {
common_log_db_error($gi, 'INSERT', __FILE__);
throw new ServerException(_('Problem saving group inbox.'));
}
@@ -917,7 +917,7 @@ class Notice extends Memcached_DataObject
/**
* Same calculation as saveGroups but without the saving
* @fixme merge the functions
- * @return array of Group objects
+ * @return array of Group_inbox objects
*/
function getGroups()
{
@@ -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());
+
$xs->element('link', array('rel' => 'alternate',
'href' => $this->bestUrl()));
@@ -1014,6 +1015,43 @@ class Notice extends Memcached_DataObject
}
}
+ if (!empty($this->conversation)
+ && $this->conversation != $this->notice->id) {
+ $xs->element(
+ 'link', array(
+ 'rel' => 'ostatus:conversation',
+ 'href' => common_local_url(
+ 'conversation',
+ array('id' => $this->conversation)
+ )
+ )
+ );
+ }
+
+ $reply_ids = $this->getReplies();
+
+ foreach ($reply_ids as $id) {
+ $profile = Profile::staticGet('id', $id);
+ if (!empty($profile)) {
+ $xs->element(
+ 'link', array(
+ 'rel' => 'ostatus:attention',
+ 'href' => $profile->getAcctUri()
+ )
+ );
+ }
+ }
+
+ 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 +1079,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 feabc2508..ab05bb854 100644
--- a/classes/Profile.php
+++ b/classes/Profile.php
@@ -754,4 +754,89 @@ class Profile extends Memcached_DataObject
return !empty($notice);
}
+
+ /**
+ * Returns an XML string fragment with limited profile information
+ * as an Atom <author> element.
+ *
+ * Assumes that Atom has been previously set up as the base namespace.
+ *
+ * @return string
+ */
+ 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();
+ }
+
+ /**
+ * Returns an XML string fragment with profile information as an
+ * Activity Streams <activity:actor> 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:' . $element);
+ $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:' . $element);
+
+ return $xs->getString();
+ }
+
+ function getAcctUri()
+ {
+ return $this->nickname . '@' . common_config('site', 'server');
+ }
+
}
diff --git a/classes/User_group.php b/classes/User_group.php
index c86eadf8f..1fbb50a6e 100644
--- a/classes/User_group.php
+++ b/classes/User_group.php
@@ -49,12 +49,12 @@ class User_group extends Memcached_DataObject
array('id' => $this->id));
}
- function getNotices($offset, $limit)
+ function getNotices($offset, $limit, $since_id=null, $max_id=null)
{
$ids = Notice::stream(array($this, '_streamDirect'),
array(),
'user_group:notice_ids:' . $this->id,
- $offset, $limit);
+ $offset, $limit, $since_id, $max_id);
return Notice::getStreamByIds($ids);
}
diff --git a/classes/statusnet.links.ini b/classes/statusnet.links.ini
index 7f233e676..b9dd5af0c 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
diff --git a/lib/api.php b/lib/api.php
index b987badc0..5758cc874 100644
--- a/lib/api.php
+++ b/lib/api.php
@@ -1322,4 +1322,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/atom10entry.php b/lib/atom10entry.php
new file mode 100644
index 000000000..5710c80fc
--- /dev/null
+++ b/lib/atom10entry.php
@@ -0,0 +1,106 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building / manipulating an Atom entry in memory
+ *
+ * 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 Feed
+ * @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/
+ */
+
+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 <zach@status.net>
+ * @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;
+ private $categories;
+ private $content;
+ private $contributors;
+ private $id;
+ private $links;
+ private $published;
+ private $rights;
+ private $source;
+ private $summary;
+ private $title;
+
+ function __construct($indent = true) {
+ parent::__construct($indent);
+ $this->namespaces = array();
+ }
+
+ function addNamespace($namespace, $uri)
+ {
+ $ns = array($namespace => $uri);
+ $this->namespaces = array_merge($this->namespaces, $ns);
+ }
+
+ function initEntry()
+ {
+
+ }
+
+ function endEntry()
+ {
+
+ }
+
+ /**
+ * Check that all required elements have been set, etc.
+ * Throws an Atom10EntryException if something's missing.
+ *
+ * @return void
+ */
+ 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 000000000..ccca76a09
--- /dev/null
+++ b/lib/atom10feed.php
@@ -0,0 +1,227 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building an Atom feed in memory
+ *
+ * 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 Feed
+ * @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/
+ */
+
+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 <zach@status.net>
+ * @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;
+ private $namespaces;
+ private $authors;
+ private $categories;
+ private $contributors;
+ private $generator;
+ private $icon;
+ private $links;
+ private $logo;
+ private $rights;
+ private $subtitle;
+ private $title;
+ private $published;
+ private $updated;
+ private $entries;
+
+ /**
+ * Constructor
+ *
+ * @param boolean $indent flag to turn indenting on or off
+ *
+ * @return void
+ */
+ function __construct($indent = true) {
+ parent::__construct($indent);
+ $this->namespaces = array();
+ $this->links = array();
+ $this->entries = array();
+ $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);
+ $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);
+
+ if (!empty($this->logo)) {
+ $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 needs 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 000000000..34ed44b2e
--- /dev/null
+++ b/lib/atomnoticefeed.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building and Atom feed from a collection of notices
+ *
+ * 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 Feed
+ * @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/
+ */
+
+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 <zach@status.net>
+ * @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) {
+ parent::__construct($indent);
+
+ // 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'
+ );
+ }
+
+ /**
+ * 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)) {
+ foreach ($notices as $notice) {
+ $this->addEntryFromNotice($notice);
+ }
+ } else {
+ while ($notices->fetch()) {
+ $this->addEntryFromNotice($notices);
+ }
+ }
+ }
+
+ /**
+ * Add a single Notice to the feed
+ *
+ * @param Notice $notice a Notice to add
+ */
+ function addEntryFromNotice($notice)
+ {
+ $this->addEntryRaw($notice->asAtomEntry());
+ }
+
+}
diff --git a/lib/default.php b/lib/default.php
index 16d1330f0..cc6863488 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/queuemanager.php b/lib/queuemanager.php
index 274e1c2f6..64bb52e10 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/spawningdaemon.php b/lib/spawningdaemon.php
index b1961d688..862cbb4fa 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.");
}
diff --git a/lib/stompqueuemanager.php b/lib/stompqueuemanager.php
index 6730cd213..cd62c25bd 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;
}
@@ -555,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/lib/util.php b/lib/util.php
index 879834a3d..e255c5fe0 100644
--- a/lib/util.php
+++ b/lib/util.php
@@ -690,7 +690,7 @@ function common_group_link($sender_id, $nickname)
{
$sender = Profile::staticGet($sender_id);
$group = User_group::getForNickname($nickname);
- if ($group && $sender->isMember($group)) {
+ if ($sender && $group && $sender->isMember($group)) {
$attrs = array('href' => $group->permalink(),
'class' => 'url');
if (!empty($group->fullname)) {
diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php
index 4e8b892c6..8444c3d73 100644
--- a/plugins/OStatus/OStatusPlugin.php
+++ b/plugins/OStatus/OStatusPlugin.php
@@ -53,6 +53,21 @@ class OStatusPlugin extends Plugin
*/
function onRouterInitialized($m)
{
+ // Discovery actions
+ $m->connect('.well-known/host-meta',
+ array('action' => 'hostmeta'));
+ $m->connect('main/webfinger',
+ array('action' => 'webfinger'));
+ $m->connect('main/ostatus',
+ array('action' => 'ostatusinit'));
+ $m->connect('main/ostatus?nickname=:nickname',
+ array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+'));
+ $m->connect('main/ostatussub',
+ array('action' => 'ostatussub'));
+ $m->connect('main/ostatussub',
+ array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+'));
+
+ // PuSH actions
$m->connect('main/push/hub', array('action' => 'pushhub'));
$m->connect('main/push/callback/:feed',
@@ -60,6 +75,14 @@ class OStatusPlugin extends Plugin
array('feed' => '[0-9]+'));
$m->connect('settings/feedsub',
array('action' => 'feedsubsettings'));
+
+ // Salmon endpoint
+ $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;
}
@@ -87,22 +110,37 @@ class OStatusPlugin extends Plugin
/**
* Set up a PuSH hub link to our internal link for canonical timeline
- * Atom feeds for users.
+ * Atom feeds for users and groups.
*/
function onStartApiAtom(Action $action)
{
if ($action instanceof ApiTimelineUserAction) {
- $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.
- $action->element('link', array('rel' => 'hub',
- 'href' => common_local_url('pushhub')));
+ $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;
}
-
+
/**
* Add the feed settings page to the Connect Settings menu
*
@@ -148,11 +186,90 @@ class OStatusPlugin extends Plugin
return true;
}
+ /**
+ * Add in an OStatus subscribe button
+ */
+ function onStartProfilePageActionsElements($output, $profile)
+ {
+ $cur = common_current_user();
+
+ if (empty($cur)) {
+ // Add an OStatus subscribe
+ $output->elementStart('li', 'entity_subscribe');
+ $url = common_local_url('ostatusinit',
+ array('nickname' => $profile->nickname));
+ $output->element('a', array('href' => $url,
+ 'class' => 'entity_remote_subscribe'),
+ _m('OStatus'));
+
+ $output->elementEnd('li');
+ }
+ }
+
+ /**
+ * 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)
+ {
+ $count = preg_match_all('/(\w+\.)*\w+@(\w+\.)*\w+(\w+\-\w+)*\.\w+/', $notice->content, $matches);
+ if ($count) {
+ foreach ($matches[0] as $webfinger) {
+ // Check to see if we've got an actual webfinger
+ $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 version="1.0" encoding="UTF-8" ?>';
+ $xml .= $notice->asAtomEntry();
+
+ $salmon = new Salmon();
+ $salmon->post($endpoint_uri, $xml);
+ }
+ }
+ }
+
+ /**
+ * Garbage collect unused feeds on unsubscribe
+ */
+ function onEndUnsubscribe($user, $other)
+ {
+ $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");
+ $profile->unsubscribe();
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Make sure necessary tables are filled out.
+ */
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('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 4d5b7b60f..6933c9bf2 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,33 +196,44 @@ class FeedSubSettingsAction extends ConnectSettingsAction
{
if ($this->validateFeed()) {
$this->preview = true;
- $this->feedinfo = Feedinfo::ensureProfile($this->munger);
+ $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->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.'));
return;
}
}
-
+
// And subscribe the current user to the local profile
$user = common_current_user();
- $profile = $this->feedinfo->getProfile();
- if (!$profile) {
- throw new ServerException("Feed profile was not saved properly.");
- }
- if ($user->isSubscribed($profile)) {
- $this->showForm(_m('Already subscribed!'));
- } elseif ($user->subscribeTo($profile)) {
- $this->showForm(_m('Feed subscribed!'));
+ if ($this->profile->isGroup()) {
+ $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!'));
+ } else {
+ $this->showForm(_m('Remote group join failed!'));
+ }
} else {
- $this->showForm(_m('Feed subscription failed!'));
+ $local = $this->profile->localProfile();
+ if ($user->isSubscribed($local)) {
+ $this->showForm(_m('Already subscribed!'));
+ } elseif ($user->subscribeTo($local)) {
+ $this->showForm(_m('Feed subscribed!'));
+ } else {
+ $this->showForm(_m('Feed subscription failed!'));
+ }
}
}
}
@@ -237,7 +248,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/hostmeta.php b/plugins/OStatus/actions/hostmeta.php
new file mode 100644
index 000000000..850b8a0fe
--- /dev/null
+++ b/plugins/OStatus/actions/hostmeta.php
@@ -0,0 +1,42 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class HostMetaAction extends Action
+{
+
+ function handle()
+ {
+ parent::handle();
+
+ $w = new Webfinger();
+
+
+ $domain = common_config('site', 'server');
+ $url = common_local_url('webfinger');
+ $url.= '?uri={uri}';
+ print $w->getHostMeta($domain, $url);
+ }
+}
diff --git a/plugins/OStatus/actions/ostatusinit.php b/plugins/OStatus/actions/ostatusinit.php
new file mode 100644
index 000000000..bac2c4d43
--- /dev/null
+++ b/plugins/OStatus/actions/ostatusinit.php
@@ -0,0 +1,128 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+
+class OStatusInitAction extends Action
+{
+
+ var $nickname;
+ var $acct;
+ var $err;
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ if (common_logged_in()) {
+ $this->clientError(_('You can use the local subscription!'));
+ return false;
+ }
+
+ $this->nickname = $this->trimmed('nickname');
+ $this->acct = $this->trimmed('acct');
+
+ return true;
+ }
+
+ function handle($args)
+ {
+ parent::handle($args);
+
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ /* Use a session token for CSRF protection. */
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_('There was a problem with your session token. '.
+ 'Try again, please.'));
+ return;
+ }
+ $this->ostatusConnect();
+ } else {
+ $this->showForm();
+ }
+ }
+
+ function showForm($err = null)
+ {
+ $this->err = $err;
+ $this->showPage();
+
+ }
+
+ function showContent()
+ {
+ $this->elementStart('form', array('id' => 'form_ostatus_connect',
+ 'method' => 'post',
+ 'class' => 'form_settings',
+ 'action' => common_local_url('ostatusinit')));
+ $this->elementStart('fieldset');
+ $this->element('legend', _('Subscribe to a remote user'));
+ $this->hidden('token', common_session_token());
+
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li');
+ $this->input('nickname', _('User nickname'), $this->nickname,
+ _('Nickname of the user you want to follow'));
+ $this->elementEnd('li');
+ $this->elementStart('li');
+ $this->input('acct', _('Profile Account'), $this->acct,
+ _('Your account id (i.e. user@identi.ca)'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+ $this->submit('submit', _('Subscribe'));
+ $this->elementEnd('fieldset');
+ $this->elementEnd('form');
+ }
+
+ function ostatusConnect()
+ {
+ $w = new Webfinger;
+
+ $result = $w->lookup($this->acct);
+ foreach ($result->links as $link) {
+ if ($link['rel'] == 'http://ostatus.org/schema/1.0/subscribe') {
+ // We found a URL - let's redirect!
+
+ $user = User::staticGet('nickname', $this->nickname);
+
+ $feed_url = common_local_url('ApiTimelineUser',
+ array('id' => $user->id,
+ 'format' => 'atom'));
+ $url = $w->applyTemplate($link['template'], $feed_url);
+
+ common_redirect($url, 303);
+ }
+
+ }
+
+ }
+
+ function title()
+ {
+ return _('OStatus Connect');
+ }
+
+} \ No newline at end of file
diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php
new file mode 100644
index 000000000..9774286fd
--- /dev/null
+++ b/plugins/OStatus/actions/ostatussub.php
@@ -0,0 +1,226 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class OStatusSubAction extends Action
+{
+
+ protected $feedurl;
+
+ function title()
+ {
+ return _m("OStatus Subscribe");
+ }
+
+ function handle($args)
+ {
+ if ($this->validateFeed()) {
+ $this->showForm();
+ }
+
+ return true;
+
+ }
+
+ function showForm($err = null)
+ {
+ $this->err = $err;
+ $this->showPage();
+ }
+
+
+ function showContent()
+ {
+ $user = common_current_user();
+
+ $profile = $user->getProfile();
+
+ $fuser = null;
+
+ $flink = Foreign_link::getByUserID($user->id, FEEDSUB_SERVICE);
+
+ if (!empty($flink)) {
+ $fuser = $flink->getForeignUser();
+ }
+
+ $this->elementStart('form', array('method' => 'post',
+ 'id' => 'form_settings_feedsub',
+ 'class' => 'form_settings',
+ 'action' =>
+ common_local_url('feedsubsettings')));
+
+ $this->hidden('token', common_session_token());
+
+ $this->elementStart('fieldset', array('id' => 'settings_feeds'));
+
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li', array('id' => 'settings_twitter_login_button'));
+ $this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+
+ $this->submit('subscribe', _m('Subscribe'));
+
+ $this->elementEnd('fieldset');
+
+ $this->elementEnd('form');
+
+ $this->previewFeed();
+ }
+
+ /**
+ * Handle posts to this form
+ *
+ * Based on the button that was pressed, muxes out to other functions
+ * to do the actual task requested.
+ *
+ * All sub-functions reload the form with a message -- success or failure.
+ *
+ * @return void
+ */
+
+ function handlePost()
+ {
+ // CSRF protection
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_('There was a problem with your session token. '.
+ 'Try again, please.'));
+ return;
+ }
+
+ if ($this->arg('subscribe')) {
+ $this->saveFeed();
+ } else {
+ $this->showForm(_('Unexpected form submission.'));
+ }
+ }
+
+
+ /**
+ * Set up and add a feed
+ *
+ * @return boolean true if feed successfully read
+ * Sends you back to input form if not.
+ */
+ function validateFeed()
+ {
+ $feedurl = $this->trimmed('feed');
+
+ if ($feedurl == '') {
+ $this->showForm(_m('Empty feed URL!'));
+ return;
+ }
+ $this->feedurl = $feedurl;
+
+ // Get the canonical feed URI and check it
+ try {
+ $discover = new FeedDiscovery();
+ $uri = $discover->discoverFromURL($feedurl);
+ } catch (FeedSubBadURLException $e) {
+ $this->showForm(_m('Invalid URL or could not reach server.'));
+ return false;
+ } catch (FeedSubBadResponseException $e) {
+ $this->showForm(_m('Cannot read feed; server returned error.'));
+ return false;
+ } catch (FeedSubEmptyException $e) {
+ $this->showForm(_m('Cannot read feed; server returned an empty page.'));
+ return false;
+ } catch (FeedSubBadHTMLException $e) {
+ $this->showForm(_m('Bad HTML, could not find feed link.'));
+ return false;
+ } catch (FeedSubNoFeedException $e) {
+ $this->showForm(_m('Could not find a feed linked from this URL.'));
+ return false;
+ } catch (FeedSubUnrecognizedTypeException $e) {
+ $this->showForm(_m('Not a recognized feed type.'));
+ return false;
+ } catch (FeedSubException $e) {
+ // Any new ones we forgot about
+ $this->showForm(_m('Bad feed URL.'));
+ return false;
+ }
+
+ $this->munger = $discover->feedMunger();
+ $this->profile = $this->munger->ostatusProfile();
+
+ if ($this->profile->huburi == '') {
+ $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
+ return false;
+ }
+
+ return true;
+ }
+
+ function saveFeed()
+ {
+ if ($this->validateFeed()) {
+ $this->preview = true;
+ $this->profile = Ostatus_profile::ensureProfile($this->munger);
+
+ // If not already in use, subscribe to updates via the hub
+ 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->profile->subscribe();
+ common_log(LOG_INFO, __METHOD__ . ": sub was $ok");
+ if (!$ok) {
+ $this->showForm(_m('Feed subscription failed! Bad response from hub.'));
+ return;
+ }
+ }
+
+ // And subscribe the current user to the local profile
+ $user = common_current_user();
+ $profile = $this->profile->getProfile();
+
+ if ($user->isSubscribed($profile)) {
+ $this->showForm(_m('Already subscribed!'));
+ } elseif ($user->subscribeTo($profile)) {
+ $this->showForm(_m('Feed subscribed!'));
+ } else {
+ $this->showForm(_m('Feed subscription failed!'));
+ }
+ }
+ }
+
+
+ function previewFeed()
+ {
+ $profile = $this->munger->ostatusProfile();
+ $notice = $this->munger->notice(0, true); // preview
+
+ if ($notice) {
+ $this->element('b', null, 'Preview of latest post from this feed:');
+
+ $item = new NoticeList($notice, $this);
+ $item->show();
+ } else {
+ $this->element('b', null, 'No posts in this feed yet.');
+ }
+ }
+
+
+} \ No newline at end of file
diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php
index a5e02e08f..2601a377a 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,28 +78,30 @@ 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);
}
- # 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!
- 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');
+ $profile->confirmSubscribe($lease_seconds);
} else {
- $feedinfo->sub_end = null;
+ common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic");
+ $profile->confirmUnsubscribe();
}
- $feedinfo->update();
-
print $challenge;
}
}
diff --git a/plugins/OStatus/actions/salmon.php b/plugins/OStatus/actions/salmon.php
new file mode 100644
index 000000000..b616027a9
--- /dev/null
+++ b/plugins/OStatus/actions/salmon.php
@@ -0,0 +1,81 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @author James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+class SalmonAction extends Action
+{
+ var $user = null;
+ var $xml = null;
+ var $activity = null;
+
+ function prepare($args)
+ {
+ if ($_SERVER['REQUEST_METHOD'] != 'POST') {
+ $this->clientError(_('This method requires a POST.'));
+ }
+
+ if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') {
+ $this->clientError(_('Salmon requires application/atom+xml'));
+ }
+
+ $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:
+ }
+ }
+}
diff --git a/plugins/OStatus/actions/webfinger.php b/plugins/OStatus/actions/webfinger.php
new file mode 100644
index 000000000..75ba16638
--- /dev/null
+++ b/plugins/OStatus/actions/webfinger.php
@@ -0,0 +1,77 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class WebfingerAction extends Action
+{
+
+ public $uri;
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ $this->uri = $this->trimmed('uri');
+
+ return true;
+ }
+
+ function handle()
+ {
+ $acct = Webfinger::normalize($this->uri);
+
+ $xrd = new XRD();
+
+ list($nick, $domain) = explode('@', urldecode($acct));
+ $nick = common_canonical_nickname($nick);
+
+ $this->user = User::staticGet('nickname', $nick);
+ if (!$this->user) {
+ $this->clientError(_('No such user.'), 404);
+ return false;
+ }
+
+ $xrd->subject = $this->uri;
+ $xrd->alias[] = common_profile_url($nick);
+ $xrd->links[] = array('rel' => 'http://webfinger.net/rel/profile-page',
+ 'type' => 'text/html',
+ 'href' => common_profile_url($nick));
+
+ $salmon_url = common_local_url('salmon',
+ array('id' => $this->user->id));
+
+ $xrd->links[] = array('rel' => 'salmon',
+ 'href' => $salmon_url);
+
+ // TODO - finalize where the redirect should go on the publisher
+ $url = common_local_url('ostatussub') . '?feed={uri}';
+ $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe',
+ 'template' => $url );
+
+ header('Content-type: text/xml');
+ print $xrd->toXML();
+ }
+
+}
diff --git a/plugins/OStatus/classes/Feedinfo.php b/plugins/OStatus/classes/Feedinfo.php
deleted file mode 100644
index 107faf012..000000000
--- a/plugins/OStatus/classes/Feedinfo.php
+++ /dev/null
@@ -1,345 +0,0 @@
-<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2009-2010, StatusNet, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * @package FeedSubPlugin
- * @maintainer Brion Vibber <brion@status.net>
- */
-
-/*
-PuSH subscription flow:
-
- $feedinfo->subscribe()
- generate random verification token
- save to verify_token
- sends a sub request to the hub...
-
- feedsub/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
- hub sends us updates via POST
-
-*/
-
-class FeedDBException extends FeedSubException
-{
- public $obj;
-
- function __construct($obj)
- {
- parent::__construct('Database insert failure');
- $this->obj = $obj;
- }
-}
-
-class Feedinfo extends Memcached_DataObject
-{
- public $__table = 'feedinfo';
-
- public $id;
- public $profile_id;
-
- public $feeduri;
- public $homeuri;
- public $huburi;
-
- // PuSH subscription data
- public $secret;
- public $verify_token;
- public $sub_start;
- public $sub_end;
-
- public $created;
- public $lastupdate;
-
-
- public /*static*/ function staticGet($k, $v=null)
- {
- return parent::staticGet(__CLASS__, $k, $v);
- }
-
- /**
- * return table definition for DB_DataObject
- *
- * DB_DataObject needs to know something about the table to manipulate
- * instances. This method provides all the DB_DataObject needs to know.
- *
- * @return array array of column definitions
- */
-
- function table()
- {
- return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
- 'profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
- 'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
- 'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
- 'huburi' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
- '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,
- '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);
- }
-
- static function schemaDef()
- {
- return array(new ColumnDef('id', 'integer',
- /*size*/ null,
- /*nullable*/ false,
- /*key*/ 'PRI',
- /*default*/ '0',
- /*extra*/ null,
- /*auto_increment*/ true),
- new ColumnDef('profile_id', 'integer',
- null, false),
- new ColumnDef('feeduri', 'varchar',
- 255, false, 'UNI'),
- new ColumnDef('homeuri', 'varchar',
- 255, false),
- new ColumnDef('huburi', 'varchar',
- 255, false),
- new ColumnDef('verify_token', 'varchar',
- 32, true),
- new ColumnDef('secret', 'varchar',
- 64, true),
- new ColumnDef('sub_start', 'datetime',
- null, true),
- new ColumnDef('sub_end', 'datetime',
- null, true),
- new ColumnDef('created', 'datetime',
- null, false),
- new ColumnDef('lastupdate', 'datetime',
- null, false));
- }
-
- /**
- * return key definitions for DB_DataObject
- *
- * DB_DataObject needs to know about keys that the table has; this function
- * defines them.
- *
- * @return array key definitions
- */
-
- function keys()
- {
- return array_keys($this->keyTypes());
- }
-
- /**
- * return key definitions for Memcached_DataObject
- *
- * Our caching system uses the same key definitions, but uses a different
- * method to get them.
- *
- * @return array key definitions
- */
-
- function keyTypes()
- {
- return array('id' => 'K'); // @fixme we'll need a profile_id key at least
- }
-
- function sequenceKey()
- {
- return array('id', true, false);
- }
-
- /**
- * Fetch the StatusNet-side profile for this feed
- * @return Profile
- */
- public function getProfile()
- {
- return Profile::staticGet('id', $this->profile_id);
- }
-
- /**
- * @param FeedMunger $munger
- * @return Feedinfo
- */
- public static function ensureProfile($munger)
- {
- $feedinfo = $munger->feedinfo();
-
- $current = self::staticGet('feeduri', $feedinfo->feeduri);
- if ($current) {
- // @fixme we should probably update info as necessary
- return $current;
- }
-
- $feedinfo->query('BEGIN');
-
- // Awful hack! Awful hack!
- $feedinfo->verify = common_good_rand(16);
- $feedinfo->secret = common_good_rand(32);
-
- try {
- $profile = $munger->profile();
- $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);
- }
-
- $feedinfo->profile_id = $profile->id;
- $result = $feedinfo->insert();
- if (empty($result)) {
- throw new FeedDBException($feedinfo);
- }
-
- $feedinfo->query('COMMIT');
- } catch (FeedDBException $e) {
- common_log_db_error($e->obj, 'INSERT', __FILE__);
- $feedinfo->query('ROLLBACK');
- return false;
- }
- return $feedinfo;
- }
-
- /**
- * Send a subscription request to the hub for this feed.
- * The hub will later send us a confirmation POST to /feedsub/callback.
- *
- * @return bool true on success, false on failure
- */
- public function subscribe()
- {
- if (common_config('feedsub', 'nohub')) {
- // Fake it! We're just testing remote feeds w/o hubs.
- return true;
- }
- // @fixme use the verification token
- #$token = md5(mt_rand() . ':' . $this->feeduri);
- #$this->verify_token = $token;
- #$this->update(); // @fixme
- try {
- $callback = common_local_url('pushcallback', array('feed' => $this->id));
- $headers = array('Content-Type: application/x-www-form-urlencoded');
- $post = array('hub.mode' => 'subscribe',
- 'hub.callback' => $callback,
- 'hub.verify' => 'async',
- 'hub.verify_token' => $this->verify_token,
- 'hub.secret' => $this->secret,
- //'hub.lease_seconds' => 0,
- 'hub.topic' => $this->feeduri);
- $client = new HTTPClient();
- $response = $client->post($this->huburi, $headers, $post);
- $status = $response->getStatus();
- if ($status == 202) {
- common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
- return true;
- } else if ($status == 204) {
- common_log(LOG_INFO, __METHOD__ . ': sub req ok and verified');
- return true;
- } else if ($status >= 200 && $status < 300) {
- common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody());
- return false;
- } else {
- common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody());
- return false;
- }
- } catch (Exception $e) {
- // wtf!
- common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri");
- return false;
- }
- }
-
- /**
- * Read and post notices for updates from the feed.
- * 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 $hmac X-Hub-Signature header, if present
- */
- public function postUpdates($xml, $hmac)
- {
- common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml");
-
- 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);
- 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'");
- 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);
- $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)) {
- 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));
- }
- $notice->addToInboxes();
-
- common_log(LOG_INFO, __METHOD__ . ": saved notice {$notice->id} for entry $index of update to \"{$this->feeduri}\"");
- $hits++;
- }
- if ($hits == 0) {
- common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml");
- }
- }
-}
diff --git a/plugins/OStatus/classes/HubSub.php b/plugins/OStatus/classes/HubSub.php
index 1769f6c94..7071ee5b4 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/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php
new file mode 100644
index 000000000..733d8843b
--- /dev/null
+++ b/plugins/OStatus/classes/Ostatus_profile.php
@@ -0,0 +1,644 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009-2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package FeedSubPlugin
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+/*
+PuSH subscription flow:
+
+ $profile->subscribe()
+ generate random verification token
+ save to verify_token
+ sends a sub request to the hub...
+
+ 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
+
+ main/push/callback
+ hub sends us updates via POST
+
+*/
+
+class FeedDBException extends FeedSubException
+{
+ public $obj;
+
+ function __construct($obj)
+ {
+ parent::__construct('Database insert failure');
+ $this->obj = $obj;
+ }
+}
+
+class Ostatus_profile extends Memcached_DataObject
+{
+ public $__table = 'ostatus_profile';
+
+ public $id;
+ public $profile_id;
+ public $group_id;
+
+ public $feeduri;
+ public $homeuri;
+
+ // 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;
+
+
+ public /*static*/ function staticGet($k, $v=null)
+ {
+ return parent::staticGet(__CLASS__, $k, $v);
+ }
+
+ /**
+ * return table definition for DB_DataObject
+ *
+ * DB_DataObject needs to know something about the table to manipulate
+ * instances. This method provides all the DB_DataObject needs to know.
+ *
+ * @return array array of column definitions
+ */
+
+ function table()
+ {
+ return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+ 'profile_id' => DB_DATAOBJECT_INT,
+ 'group_id' => DB_DATAOBJECT_INT,
+ 'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ '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,
+ '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);
+ }
+
+ static function schemaDef()
+ {
+ return array(new ColumnDef('id', 'integer',
+ /*size*/ null,
+ /*nullable*/ false,
+ /*key*/ 'PRI',
+ /*default*/ '0',
+ /*extra*/ null,
+ /*auto_increment*/ true),
+ new ColumnDef('profile_id', 'integer',
+ null, true, 'UNI'),
+ new ColumnDef('group_id', 'integer',
+ null, true, 'UNI'),
+ new ColumnDef('feeduri', 'varchar',
+ 255, false, 'UNI'),
+ new ColumnDef('homeuri', 'varchar',
+ 255, false),
+ new ColumnDef('huburi', 'text',
+ null, true),
+ new ColumnDef('verify_token', 'varchar',
+ 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',
+ null, true),
+ new ColumnDef('salmonuri', 'text',
+ null, true),
+ new ColumnDef('created', 'datetime',
+ null, false),
+ new ColumnDef('lastupdate', 'datetime',
+ null, false));
+ }
+
+ /**
+ * return key definitions for DB_DataObject
+ *
+ * DB_DataObject needs to know about keys that the table has; this function
+ * defines them.
+ *
+ * @return array key definitions
+ */
+
+ function keys()
+ {
+ return array_keys($this->keyTypes());
+ }
+
+ /**
+ * return key definitions for Memcached_DataObject
+ *
+ * Our caching system uses the same key definitions, but uses a different
+ * method to get them.
+ *
+ * @return array key definitions
+ */
+
+ function keyTypes()
+ {
+ return array('id' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U');
+ }
+
+ function sequenceKey()
+ {
+ return array('id', true, false);
+ }
+
+ /**
+ * Fetch the StatusNet-side profile for this feed
+ * @return Profile
+ */
+ public function localProfile()
+ {
+ 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;
+ }
+
+ /**
+ * @param FeedMunger $munger
+ * @param boolean $isGroup is this a group record?
+ * @return Ostatus_profile
+ */
+ public static function ensureProfile($munger)
+ {
+ $profile = $munger->ostatusProfile();
+
+ $current = self::staticGet('feeduri', $profile->feeduri);
+ if ($current) {
+ // @fixme we should probably update info as necessary
+ return $current;
+ }
+
+ $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()) {
+ $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);
+ }
+
+ $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);
+ }
+ }
+
+ /**
+ * 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!
+ */
+ function isGroup()
+ {
+ return (strpos($this->feeduri, '/groups/') !== false);
+ }
+
+ /**
+ * Send a subscription 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 subscribe($mode='subscribe')
+ {
+ if (common_config('feedsub', 'nohub')) {
+ // Fake it! We're just testing remote feeds w/o hubs.
+ return true;
+ }
+ // @fixme use the verification token
+ #$token = md5(mt_rand() . ':' . $this->feeduri);
+ #$this->verify_token = $token;
+ #$this->update(); // @fixme
+ try {
+ $callback = common_local_url('pushcallback', array('feed' => $this->id));
+ $headers = array('Content-Type: application/x-www-form-urlencoded');
+ $post = array('hub.mode' => $mode,
+ 'hub.callback' => $callback,
+ 'hub.verify' => 'async',
+ 'hub.verify_token' => $this->verify_token,
+ 'hub.secret' => $this->secret,
+ //'hub.lease_seconds' => 0,
+ 'hub.topic' => $this->feeduri);
+ $client = new HTTPClient();
+ $response = $client->post($this->huburi, $headers, $post);
+ $status = $response->getStatus();
+ if ($status == 202) {
+ common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
+ return true;
+ } else if ($status == 204) {
+ common_log(LOG_INFO, __METHOD__ . ': sub req ok and verified');
+ return true;
+ } else if ($status >= 200 && $status < 300) {
+ common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody());
+ return false;
+ } else {
+ common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody());
+ return false;
+ }
+ } catch (Exception $e) {
+ // wtf!
+ common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri");
+ return false;
+ }
+ }
+
+ /**
+ * 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 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.
+ *
+ * @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,
+ * coming from a PuSH hub.
+ *
+ * @param string $xml source of Atom or RSS feed
+ * @param string $hmac X-Hub-Signature header, if present
+ */
+ public function postUpdates($xml, $hmac)
+ {
+ common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml");
+
+ 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'");
+ 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 = new Notice();
+ $dupe->uri = $notice->uri;
+ if ($dupe->find(true)) {
+ 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 ($hits == 0) {
+ common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml");
+ }
+ }
+}
diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php
new file mode 100644
index 000000000..36e227913
--- /dev/null
+++ b/plugins/OStatus/lib/activity.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * 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 OStatus
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @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()
+ {
+ }
+}
diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php
index cbaec6775..c895b6ce2 100644
--- a/plugins/OStatus/lib/feedmunger.php
+++ b/plugins/OStatus/lib/feedmunger.php
@@ -83,13 +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();
- return $feedinfo;
+ $profile = new Ostatus_profile();
+ $profile->feeduri = $this->url;
+ $profile->homeuri = $this->feed->link;
+ $profile->huburi = $this->getHubLink();
+ $salmon = $this->getSalmonLink();
+ if ($salmon) {
+ $profile->salmonuri = $salmon;
+ }
+ return $profile;
}
function getAtomLink($item, $attribs=array())
@@ -155,6 +159,16 @@ 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'));
+ }
+
/**
* Get an appropriate avatar image source URL, if available.
* @return mixed string or false
@@ -203,12 +217,13 @@ class FeedMunger
if (!$entry) {
return null;
}
-
+
if ($preview) {
$notice = new FeedSubPreviewNotice($this->profile(true));
$notice->id = -1;
} else {
$notice = new Notice();
+ $notice->profile_id = $this->profileIdForEntry($index);
}
$link = $this->getAltLink($entry);
@@ -221,7 +236,7 @@ class FeedMunger
$notice->uri = $link;
$notice->url = $link;
$notice->content = $this->noticeFromEntry($entry);
- $notice->rendered = common_render_content($notice->content, $notice);
+ $notice->rendered = common_render_content($notice->content, $notice); // @fixme this is failing on group posts
$notice->created = common_sql_date($entry->updated); // @fixme
$notice->is_local = Notice::GATEWAY;
$notice->source = 'feed';
@@ -239,7 +254,22 @@ class FeedMunger
return $notice;
}
+ function profileIdForEntry($index=1)
+ {
+ // hack hack hack
+ // should get profile for this entry's author...
+ $remote = Ostatus_profile::staticGet('feeduri', $this->getSelfLink());
+ if ($feed) {
+ return $feed->profile_id;
+ } else {
+ throw new Exception("Can't find feed profile");
+ }
+ }
+
/**
+ * Parse location given as a GeoRSS-simple point, if provided.
+ * http://www.georss.org/simple
+ *
* @param feed item $entry
* @return mixed Location or false
*/
@@ -249,7 +279,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;
diff --git a/plugins/OStatus/lib/hubdistribqueuehandler.php b/plugins/OStatus/lib/hubdistribqueuehandler.php
index 126f1355f..245a57f72 100644
--- a/plugins/OStatus/lib/hubdistribqueuehandler.php
+++ b/plugins/OStatus/lib/hubdistribqueuehandler.php
@@ -34,27 +34,101 @@ class HubDistribQueueHandler extends QueueHandler
{
assert($notice instanceof Notice);
+ $this->pushUser($notice);
+ foreach ($notice->getGroups() as $group) {
+ $this->pushGroup($notice, $group->group_id);
+ }
+ return true;
+ }
+
+ function pushUser($notice)
+ {
// See if there's any PuSH subscriptions, including OStatus clients.
// @fixme handle group subscriptions as well
// http://identi.ca/api/statuses/user_timeline/1.atom
$feed = common_local_url('ApiTimelineUser',
array('id' => $notice->profile_id,
'format' => 'atom'));
+ $this->pushFeed($feed, array($this, 'userFeedForNotice'), $notice);
+ }
+
+ function pushGroup($notice, $group_id)
+ {
+ $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, "Preparing $sub->N PuSH distribution(s) for $feed");
- $qm = QueueManager::get();
- $atom = $this->userFeedForNotice($notice);
- while ($sub->fetch()) {
- common_log(LOG_INFO, "Prepping PuSH distribution to $sub->callback for $feed");
- $data = array('sub' => clone($sub),
- 'atom' => $atom);
- $qm->enqueue($data, 'hubout');
- }
+ $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;
+ }
+
+ /**
+ * 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();
+ while ($sub->fetch()) {
+ common_log(LOG_INFO, "Prepping PuSH distribution to $sub->callback for $sub->topic");
+ $data = array('sub' => clone($sub),
+ 'atom' => $atom);
+ $qm->enqueue($data, 'hubout');
+ }
}
/**
@@ -83,5 +157,29 @@ class HubDistribQueueHandler extends QueueHandler
common_log(LOG_DEBUG, $feed);
return $feed;
}
+
+ function groupFeedForNotice($group_id, $notice)
+ {
+ // @fixme this feels VERY hacky...
+ // should probably be a cleaner way to do it
+
+ ob_start();
+ $api = new ApiTimelineGroupAction();
+ $args = array('id' => $group_id,
+ 'format' => 'atom',
+ 'max_id' => $notice->id,
+ 'since_id' => $notice->id - 1);
+ $api->prepare($args);
+ $api->handle($args);
+ $feed = ob_get_clean();
+
+ // ...and override the content-type back to something normal... eww!
+ // hope there's no other headers that got set while we weren't looking.
+ header('Content-Type: text/html; charset=utf-8');
+
+ common_log(LOG_DEBUG, $feed);
+ return $feed;
+ }
+
}
diff --git a/plugins/OStatus/lib/huboutqueuehandler.php b/plugins/OStatus/lib/huboutqueuehandler.php
index cb44ad2c4..0791c7e5d 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;
diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php
new file mode 100644
index 000000000..8c77222a6
--- /dev/null
+++ b/plugins/OStatus/lib/salmon.php
@@ -0,0 +1,64 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A sample module to show best practices for StatusNet plugins
+ *
+ * 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/>.
+ *
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+class Salmon
+{
+ public function post($endpoint_uri, $xml)
+ {
+ if (empty($endpoint_uri)) {
+ return FALSE;
+ }
+
+ $headers = array('Content-type: application/atom+xml');
+
+ try {
+ $client = new HTTPClient();
+ $client->setBody($xml);
+ $response = $client->post($endpoint_uri, $headers);
+ } catch (HTTP_Request2_Exception $e) {
+ return false;
+ }
+ if ($response->getStatus() != 200) {
+ return false;
+ }
+
+ }
+
+ public function createMagicEnv($text, $userid)
+ {
+
+
+ }
+
+
+ public function verifyMagicEnv($env)
+ {
+
+
+ }
+}
diff --git a/plugins/OStatus/lib/webfinger.php b/plugins/OStatus/lib/webfinger.php
new file mode 100644
index 000000000..417d54904
--- /dev/null
+++ b/plugins/OStatus/lib/webfinger.php
@@ -0,0 +1,143 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A sample module to show best practices for StatusNet plugins
+ *
+ * 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/>.
+ *
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+define('WEBFINGER_SERVICE_REL_VALUE', 'lrdd');
+
+/**
+ * Implement the webfinger protocol.
+ */
+class Webfinger
+{
+ /**
+ * Perform a webfinger lookup given an account.
+ */
+ public function lookup($id)
+ {
+ $id = $this->normalize($id);
+ list($name, $domain) = explode('@', $id);
+
+ $links = $this->getServiceLinks($domain);
+ if (!$links) {
+ return false;
+ }
+
+ $services = array();
+ foreach ($links as $link) {
+ if ($link['template']) {
+ return $this->getServiceDescription($link['template'], $id);
+ }
+ if ($link['href']) {
+ return $this->getServiceDescription($link['href'], $id);
+ }
+ }
+ }
+
+ /**
+ * Normalize an account ID
+ */
+ function normalize($id)
+ {
+ if (substr($id, 0, 7) == 'acct://') {
+ return substr($id, 7);
+ } else if (substr($id, 0, 5) == 'acct:') {
+ return substr($id, 5);
+ }
+
+ return $id;
+ }
+
+ function getServiceLinks($domain)
+ {
+ $url = 'http://'. $domain .'/.well-known/host-meta';
+ $content = $this->fetchURL($url);
+ if (empty($content)) {
+ common_log(LOG_DEBUG, 'Error fetching host-meta');
+ return false;
+ }
+ $result = XRD::parse($content);
+
+ // Ensure that the host == domain (spec may include signing later)
+ if ($result->host != $domain) {
+ return false;
+ }
+
+ $links = array();
+ foreach ($result->links as $link) {
+ if ($link['rel'] == WEBFINGER_SERVICE_REL_VALUE) {
+ $links[] = $link;
+ }
+
+ }
+ return $links;
+ }
+
+ function getServiceDescription($template, $id)
+ {
+ $url = $this->applyTemplate($template, 'acct:' . $id);
+
+ $content = $this->fetchURL($url);
+
+ return XRD::parse($content);
+ }
+
+ function fetchURL($url)
+ {
+ try {
+ $client = new HTTPClient();
+ $response = $client->get($url);
+ } catch (HTTP_Request2_Exception $e) {
+ return false;
+ }
+
+ if ($response->getStatus() != 200) {
+ return false;
+ }
+
+ return $response->getBody();
+ }
+
+ function applyTemplate($template, $id)
+ {
+ $template = str_replace('{uri}', urlencode($id), $template);
+
+ return $template;
+ }
+
+ function getHostMeta($domain, $template) {
+ $xrd = new XRD();
+ $xrd->host = $domain;
+ $xrd->links[] = array('rel' => 'lrdd',
+ 'template' => $template,
+ 'title' => array('Resource Descriptor'));
+
+ return $xrd->toXML();
+ }
+}
+
+
diff --git a/plugins/OStatus/lib/xrd.php b/plugins/OStatus/lib/xrd.php
new file mode 100644
index 000000000..16d27f8eb
--- /dev/null
+++ b/plugins/OStatus/lib/xrd.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A sample module to show best practices for StatusNet plugins
+ *
+ * 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/>.
+ *
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+
+class XRD
+{
+ const XML_NS = 'http://www.w3.org/2000/xmlns/';
+
+ const XRD_NS = 'http://docs.oasis-open.org/ns/xri/xrd-1.0';
+
+ const HOST_META_NS = 'http://host-meta.net/xrd/1.0';
+
+ public $expires;
+
+ public $subject;
+
+ public $host;
+
+ public $alias = array();
+
+ public $types = array();
+
+ public $links = array();
+
+ public static function parse($xml)
+ {
+ $xrd = new XRD();
+
+ $dom = new DOMDocument();
+ $dom->loadXML($xml);
+ $xrd_element = $dom->getElementsByTagName('XRD')->item(0);
+
+ // Check for host-meta host
+ $host = $xrd_element->getElementsByTagName('Host')->item(0)->nodeValue;
+ if ($host) {
+ $xrd->host = $host;
+ }
+
+ // Loop through other elements
+ foreach ($xrd_element->childNodes as $node) {
+ switch ($node->tagName) {
+ case 'Expires':
+ $xrd->expires = $node->nodeValue;
+ break;
+ case 'Subject':
+ $xrd->subject = $node->nodeValue;
+ break;
+
+ case 'Alias':
+ $xrd->alias[] = $node->nodeValue;
+ break;
+
+ case 'Link':
+ $xrd->links[] = $xrd->parseLink($node);
+ break;
+
+ case 'Type':
+ $xrd->types[] = $xrd->parseType($node);
+ break;
+
+ }
+ }
+ return $xrd;
+ }
+
+ public function toXML()
+ {
+ $dom = new DOMDocument('1.0', 'UTF-8');
+ $dom->formatOutput = true;
+
+ $xrd_dom = $dom->createElementNS(XRD::XRD_NS, 'XRD');
+ $dom->appendChild($xrd_dom);
+
+ if ($this->host) {
+ $host_dom = $dom->createElement('hm:Host', $this->host);
+ $xrd_dom->setAttributeNS(XRD::XML_NS, 'xmlns:hm', XRD::HOST_META_NS);
+ $xrd_dom->appendChild($host_dom);
+ }
+
+ if ($this->expires) {
+ $expires_dom = $dom->createElement('Expires', $this->expires);
+ $xrd_dom->appendChild($expires_dom);
+ }
+
+ if ($this->subject) {
+ $subject_dom = $dom->createElement('Subject', $this->subject);
+ $xrd_dom->appendChild($subject_dom);
+ }
+
+ foreach ($this->alias as $alias) {
+ $alias_dom = $dom->createElement('Alias', $alias);
+ $xrd_dom->appendChild($alias_dom);
+ }
+
+ foreach ($this->types as $type) {
+ $type_dom = $dom->createElement('Type', $type);
+ $xrd_dom->appendChild($type_dom);
+ }
+
+ foreach ($this->links as $link) {
+ $link_dom = $this->saveLink($dom, $link);
+ $xrd_dom->appendChild($link_dom);
+ }
+
+ return $dom->saveXML();
+ }
+
+ function parseType($element)
+ {
+ return array();
+ }
+
+ function parseLink($element)
+ {
+ $link = array();
+ $link['rel'] = $element->getAttribute('rel');
+ $link['type'] = $element->getAttribute('type');
+ $link['href'] = $element->getAttribute('href');
+ $link['template'] = $element->getAttribute('template');
+ foreach ($element->childNodes as $node) {
+ switch($node->tagName) {
+ case 'Title':
+ $link['title'][] = $node->nodeValue;
+ }
+ }
+
+ return $link;
+ }
+
+ function saveLink($doc, $link)
+ {
+ $link_element = $doc->createElement('Link');
+ if ($link['rel']) {
+ $link_element->setAttribute('rel', $link['rel']);
+ }
+ if ($link['type']) {
+ $link_element->setAttribute('type', $link['type']);
+ }
+ if ($link['href']) {
+ $link_element->setAttribute('href', $link['href']);
+ }
+ if ($link['template']) {
+ $link_element->setAttribute('template', $link['template']);
+ }
+
+ if (is_array($link['title'])) {
+ foreach($link['title'] as $title) {
+ $title = $doc->createElement('Title', $title);
+ $link_element->appendChild($title);
+ }
+ }
+
+
+ return $link_element;
+ }
+}
+
diff --git a/theme/base/css/display.css b/theme/base/css/display.css
index 70ddc411f..3218276a6 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:-1px;
+right:-1px;
z-index:9;
-min-width:199px;
float:none;
padding:11px;
border-radius:7px;
@@ -1120,6 +1119,7 @@ border-width:1px;
.dialogbox legend {
display:block !important;
margin-right:18px;
+margin-bottom:18px;
}
.dialogbox button.close {
@@ -1128,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,
@@ -1142,6 +1153,12 @@ outline:none;
text-indent:-9999px;
}
+.form_repeat.dialogbox {
+top:-4px;
+right:29px;
+min-width:199px;
+}
+
.notice-options {
position:relative;
font-size:0.95em;
diff --git a/theme/default/css/display.css b/theme/default/css/display.css
index 02e1645f4..a2f101342 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 6dc7d21df..e21404745 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] {