summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--actions/apitimelinefavorites.php69
-rw-r--r--actions/apitimelinefriends.php74
-rw-r--r--actions/apitimelinegroup.php74
-rw-r--r--actions/apitimelinehome.php60
-rw-r--r--actions/apitimelinementions.php34
-rw-r--r--actions/apitimelinepublic.php27
-rw-r--r--actions/apitimelineretweetsofme.php36
-rw-r--r--actions/apitimelinetag.php51
-rw-r--r--actions/apitimelineuser.php58
-rw-r--r--classes/Nonce.php15
-rw-r--r--classes/Notice.php52
-rw-r--r--classes/Profile.php85
-rw-r--r--classes/User_group.php33
-rw-r--r--classes/statusnet.links.ini7
-rw-r--r--lib/api.php21
-rw-r--r--lib/atom10entry.php106
-rw-r--r--lib/atom10feed.php298
-rw-r--r--lib/atomgroupnoticefeed.php67
-rw-r--r--lib/atomnoticefeed.php105
-rw-r--r--lib/atomusernoticefeed.php66
-rw-r--r--lib/util.php3
-rw-r--r--plugins/OStatus/OStatusPlugin.php43
-rw-r--r--plugins/OStatus/actions/feedsubsettings.php33
-rw-r--r--plugins/OStatus/actions/ostatussub.php16
-rw-r--r--plugins/OStatus/actions/pushcallback.php39
-rw-r--r--plugins/OStatus/actions/salmon.php52
-rw-r--r--plugins/OStatus/classes/Ostatus_profile.php (renamed from plugins/OStatus/classes/Feedinfo.php)340
-rw-r--r--plugins/OStatus/lib/activity.php85
-rw-r--r--plugins/OStatus/lib/feedmunger.php26
-rw-r--r--theme/base/css/display.css26
-rw-r--r--theme/default/css/display.css23
-rw-r--r--theme/identica/css/display.css21
32 files changed, 1766 insertions, 279 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 fd2ed9ff9..3c74e36b5 100644
--- a/actions/apitimelinegroup.php
+++ b/actions/apitimelinegroup.php
@@ -109,39 +109,83 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction
$title = sprintf(_("%s timeline"), $this->group->nickname);
$taguribase = common_config('integration', 'taguri');
$id = "tag:$taguribase:GroupTimeline:" . $this->group->id;
- $link = common_local_url(
- 'showgroup',
- array('nickname' => $this->group->nickname)
- );
+
$subtitle = sprintf(
_('Updates from %1$s on %2$s!'),
$this->group->nickname,
$sitename
);
- $logo = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE);
+
+ $logo = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE);
switch($this->format) {
case 'xml':
$this->showXmlTimeline($this->notices);
break;
case 'rss':
- $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
- break;
- case 'atom':
- $selfuri = common_root_url() .
- 'api/statusnet/groups/timeline/' .
- $this->group->id . '.atom';
- $this->showAtomTimeline(
+ $this->showRssTimeline(
$this->notices,
$title,
- $id,
- $link,
+ $this->group->homeUrl(),
$subtitle,
null,
- $selfuri,
$logo
);
break;
+ case 'atom':
+
+ header('Content-Type: application/atom+xml; charset=utf-8');
+
+ try {
+
+ // If this was called using an integer ID, i.e.: using the canonical
+ // URL for this group's feed, then pass the Group object into the feed,
+ // so the OStatus plugin, and possibly other plugins, can access it.
+ // Feels sorta hacky. -- Z
+
+ $atom = null;
+ $id = $this->arg('id');
+
+ if (strval(intval($id)) === strval($id)) {
+ $atom = new AtomGroupNoticeFeed($this->group);
+ } else {
+ $atom = new AtomGroupNoticeFeed();
+ }
+
+ $atom->setId($id);
+ $atom->setTitle($title);
+ $atom->setSubtitle($subtitle);
+ $atom->setLogo($logo);
+ $atom->setUpdated('now');
+
+ $atom->addAuthorRaw($this->group->asAtomAuthor());
+ $atom->setActivitySubject($this->group->asActivitySubject());
+
+ $atom->addLink($this->group->homeUrl());
+
+ $id = $this->arg('id');
+ $aargs = array('format' => 'atom');
+ if (!empty($id)) {
+ $aargs['id'] = $id;
+ }
+
+ $atom->addLink(
+ $this->getSelfUri('ApiTimelineGroup', $aargs),
+ array('rel' => 'self', 'type' => 'application/atom+xml')
+ );
+
+ $atom->addEntryFromNotices($this->notices);
+
+ $this->raw($atom->getString());
+
+ } catch (Atom10FeedException $e) {
+ $this->serverError(
+ 'Could not generate feed for group - ' . $e->getMessage()
+ );
+ return;
+ }
+
+ break;
case 'json':
$this->showJsonTimeline($this->notices);
break;
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 0fb0788e9..c1fa72a3e 100644
--- a/actions/apitimelinepublic.php
+++ b/actions/apitimelinepublic.php
@@ -74,7 +74,7 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
parent::prepare($args);
$this->notices = $this->getNotices();
-
+
if ($this->since) {
throw new ServerException("since parameter is disabled for performance; use since_id", 403);
}
@@ -122,11 +122,28 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo);
break;
case 'atom':
- $selfuri = common_root_url() . 'api/statuses/public_timeline.atom';
- $this->showAtomTimeline(
- $this->notices, $title, $id, $link,
- $subtitle, null, $selfuri, $sitelogo
+
+ $atom = new AtomNoticeFeed();
+
+ $atom->setId($id);
+ $atom->setTitle($title);
+ $atom->setSubtitle($subtitle);
+ $atom->setLogo($sitelogo);
+ $atom->setUpdated('now');
+
+ $atom->addLink(common_local_url('public'));
+
+ $atom->addLink(
+ $this->getSelfUri(
+ 'ApiTimelinePublic', array('format' => 'atom')
+ ),
+ array('rel' => 'self', 'type' => 'application/atom+xml')
);
+
+ $atom->addEntryFromNotices($this->notices);
+
+ $this->raw($atom->getString());
+
break;
case 'json':
$this->showJsonTimeline($this->notices);
diff --git a/actions/apitimelineretweetsofme.php b/actions/apitimelineretweetsofme.php
index 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..24752e45f 100644
--- a/actions/apitimelineuser.php
+++ b/actions/apitimelineuser.php
@@ -145,19 +145,59 @@ class ApiTimelineUserAction extends ApiBareAuthAction
);
break;
case 'atom':
+
+ header('Content-Type: application/atom+xml; charset=utf-8');
+
+ // If this was called using an integer ID, i.e.: using the canonical
+ // URL for this user's feed, then pass the User object into the feed,
+ // so the OStatus plugin, and possibly other plugins, can access it.
+ // Feels sorta hacky. -- Z
+
+ $atom = null;
$id = $this->arg('id');
- if ($id) {
- $selfuri = common_root_url() .
- 'api/statuses/user_timeline/' .
- rawurlencode($id) . '.atom';
+
+ if (strval(intval($id)) === strval($id)) {
+ $atom = new AtomUserNoticeFeed($this->user);
} else {
- $selfuri = common_root_url() .
- 'api/statuses/user_timeline.atom';
+ $atom = new AtomUserNoticeFeed();
}
- $this->showAtomTimeline(
- $this->notices, $title, $id, $link,
- $subtitle, $suplink, $selfuri, $logo
+
+ $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');
+ $aargs = array('format' => 'atom');
+ if (!empty($id)) {
+ $aargs['id'] = $id;
+ }
+
+ $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/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 247440f29..73b22d58a 100644
--- a/classes/Notice.php
+++ b/classes/Notice.php
@@ -957,7 +957,10 @@ class Notice extends Memcached_DataObject
if ($namespace) {
$attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
- 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0');
+ 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0',
+ 'xmlns:georss' => 'http://www.georss.org/georss',
+ 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/',
+ 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0');
} else {
$attrs = array();
}
@@ -983,11 +986,6 @@ class Notice extends Memcached_DataObject
$xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE));
}
- $xs->elementStart('author');
- $xs->element('name', null, $profile->nickname);
- $xs->element('uri', null, $profile->profileurl);
- $xs->elementEnd('author');
-
if ($source) {
$xs->elementEnd('source');
}
@@ -995,6 +993,9 @@ class Notice extends Memcached_DataObject
$xs->element('title', null, $this->content);
$xs->element('summary', null, $this->content);
+ $xs->raw($profile->asAtomAuthor());
+ $xs->raw($profile->asActivityActor());
+
$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->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 1fbb50a6e..379e6b721 100644
--- a/classes/User_group.php
+++ b/classes/User_group.php
@@ -355,6 +355,39 @@ class User_group extends Memcached_DataObject
return $xs->getString();
}
+ function asAtomAuthor()
+ {
+ $xs = new XMLStringer(true);
+
+ $xs->elementStart('author');
+ $xs->element('name', null, $this->nickname);
+ $xs->element('uri', null, $this->permalink());
+ $xs->elementEnd('author');
+
+ return $xs->getString();
+ }
+
+ function asActivitySubject()
+ {
+ $xs = new XMLStringer(true);
+
+ $xs->elementStart('activity:subject');
+ $xs->element('activity:object', null, 'http://activitystrea.ms/schema/1.0/group');
+ $xs->element('id', null, $this->permalink());
+ $xs->element('title', null, $this->getBestName());
+ $xs->element(
+ 'link', array(
+ 'rel' => 'avatar',
+ 'href' => empty($this->homepage_logo)
+ ? User_group::defaultLogo(AVATAR_PROFILE_SIZE)
+ : $this->homepage_logo
+ )
+ );
+ $xs->elementEnd('activity:subject');
+
+ return $xs->getString();
+ }
+
static function register($fields) {
// MAGICALLY put fields into current scope
diff --git a/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 fd07bbbbe..22eef7436 100644
--- a/lib/api.php
+++ b/lib/api.php
@@ -1103,7 +1103,7 @@ class ApiAction extends Action
}
}
- function serverError($msg, $code = 500, $content_type = 'json')
+ function serverError($msg, $code = 500, $content_type = 'xml')
{
$action = $this->trimmed('action');
@@ -1154,7 +1154,6 @@ class ApiAction extends Action
$this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
'xml:lang' => 'en-US',
'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
- Event::handle('StartApiAtom', array($this));
}
function endTwitterAtom()
@@ -1321,4 +1320,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..14a3beb83
--- /dev/null
+++ b/lib/atom10feed.php
@@ -0,0 +1,298 @@
+<?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 $subject;
+ 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->authors = 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 addAuthor($name, $uri = null, $email = null)
+ {
+ $xs = new XMLStringer(true);
+
+ $xs->elementStart('author');
+
+ if (!empty($name)) {
+ $xs->element('name', null, $name);
+ } else {
+ throw new Atom10FeedException(
+ 'author element must contain a name element.'
+ );
+ }
+
+ if (!is_null($uri)) {
+ $xs->element('uri', null, $uri);
+ }
+
+ if (!is_null(email)) {
+ $xs->element('email', null, $email);
+ }
+
+ $xs->elementEnd('author');
+
+ array_push($this->authors, $xs->getString());
+ }
+
+ /**
+ * Add an Author to the feed via raw XML string
+ *
+ * @param string $xmlAuthor An XML string representation author
+ *
+ * @return void
+ */
+ function addAuthorRaw($xmlAuthor)
+ {
+ array_push($this->authors, $xmlAuthor);
+ }
+
+ function renderAuthors()
+ {
+ foreach ($this->authors as $author) {
+ $this->raw($author);
+ }
+ }
+
+ /**
+ * Add a activity feed subject via raw XML string
+ *
+ * @param string $xmlSubject An XML string representation of the subject
+ *
+ * @return void
+ */
+ function setActivitySubject($xmlSubject)
+ {
+ $this->subject = $xmlSubject;
+ }
+
+ function getNamespaces()
+ {
+ return $this->namespaces;
+ }
+
+ 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->renderAuthors();
+
+ $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($xmlEntry)
+ {
+ array_push($this->entries, $xmlEntry);
+ }
+
+ 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()
+ {
+ if (Event::handle('StartApiAtom', array($this))) {
+
+ $this->validate();
+ $this->initFeed();
+
+ if (!empty($this->subject)) {
+ $this->raw($this->subject);
+ }
+
+ $this->renderEntries();
+ $this->endFeed();
+
+ Event::handle('EndApiAtom', array($this));
+ }
+
+ 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/atomgroupnoticefeed.php b/lib/atomgroupnoticefeed.php
new file mode 100644
index 000000000..52ee4c7d6
--- /dev/null
+++ b/lib/atomgroupnoticefeed.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building an in-memory Atom feed for a particular group's
+ * timeline.
+ *
+ * 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 group notice feeds. May contains a reference to the group.
+ *
+ * @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 AtomGroupNoticeFeed extends AtomNoticeFeed
+{
+ private $group;
+
+ /**
+ * Constructor
+ *
+ * @param Group $group the group for the feed (optional)
+ * @param boolean $indent flag to turn indenting on or off
+ *
+ * @return void
+ */
+ function __construct($group = null, $indent = true) {
+ parent::__construct($indent);
+ $this->group = $group;
+ }
+
+ function getGroup()
+ {
+ return $this->group;
+ }
+
+}
diff --git a/lib/atomnoticefeed.php b/lib/atomnoticefeed.php
new file mode 100644
index 000000000..b7a60bde6
--- /dev/null
+++ b/lib/atomnoticefeed.php
@@ -0,0 +1,105 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building an 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/atomusernoticefeed.php b/lib/atomusernoticefeed.php
new file mode 100644
index 000000000..9f224325c
--- /dev/null
+++ b/lib/atomusernoticefeed.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building an in-memory Atom feed for a particular user's
+ * timeline.
+ *
+ * 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 user notice feeds. May contain a reference to the user.
+ *
+ * @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 AtomUserNoticeFeed extends AtomNoticeFeed
+{
+ private $user;
+
+ /**
+ * Constructor
+ *
+ * @param User $user the user for the feed (optional)
+ * @param boolean $indent flag to turn indenting on or off
+ *
+ * @return void
+ */
+ function __construct($user = null, $indent = true) {
+ parent::__construct($indent);
+ $this->user = $user;
+ }
+
+ function getUser()
+ {
+ return $this->user;
+ }
+}
diff --git a/lib/util.php b/lib/util.php
index a07fe49e3..209dc2254 100644
--- a/lib/util.php
+++ b/lib/util.php
@@ -367,7 +367,8 @@ function common_current_user()
if ($_cur === false) {
- if (isset($_REQUEST[session_name()]) || (isset($_SESSION['userid']) && $_SESSION['userid'])) {
+ if (isset($_COOKIE[session_name()]) || isset($_GET[session_name()])
+ || (isset($_SESSION['userid']) && $_SESSION['userid'])) {
common_ensure_session();
$id = isset($_SESSION['userid']) ? $_SESSION['userid'] : false;
if ($id) {
diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php
index c0f9dadc4..bf7dde296 100644
--- a/plugins/OStatus/OStatusPlugin.php
+++ b/plugins/OStatus/OStatusPlugin.php
@@ -63,9 +63,9 @@ class OStatusPlugin extends Plugin
$m->connect('main/ostatus?nickname=:nickname',
array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+'));
$m->connect('main/ostatussub',
- array('action' => 'ostatussub'));
+ array('action' => 'ostatussub'));
$m->connect('main/ostatussub',
- array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+'));
+ array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+'));
// PuSH actions
$m->connect('main/push/hub', array('action' => 'pushhub'));
@@ -112,35 +112,34 @@ class OStatusPlugin extends Plugin
* Set up a PuSH hub link to our internal link for canonical timeline
* Atom feeds for users and groups.
*/
- function onStartApiAtom(Action $action)
+ function onStartApiAtom(AtomNoticeFeed $feed)
{
- if ($action instanceof ApiTimelineUserAction) {
+ $id = null;
+
+ if ($feed instanceof AtomUserNoticeFeed) {
$salmonAction = 'salmon';
- } else if ($action instanceof ApiTimelineGroupAction) {
+ $id = $feed->getUser()->id;
+ } else if ($feed instanceof AtomGroupNoticeFeed) {
$salmonAction = 'salmongroup';
+ $id = $feed->getGroup()->id;
} else {
return;
}
- $id = $action->arg('id');
- if (strval(intval($id)) === strval($id)) {
- // Canonical form of id in URL? These are used for OStatus syndication.
-
+ if (!empty($id)) {
$hub = common_config('ostatus', 'hub');
if (empty($hub)) {
// Updates will be handled through our internal PuSH hub.
$hub = common_local_url('pushhub');
}
- $action->element('link', array('rel' => 'hub',
- 'href' => $hub));
+ $feed->addLink($hub, array('rel' => 'hub'));
// Also, we'll add in the salmon link
$salmon = common_local_url($salmonAction, array('id' => $id));
- $action->element('link', array('rel' => 'salmon',
- 'href' => $salmon));
+ $feed->addLink($salmon, array('rel' => 'salmon'));
}
}
-
+
/**
* Add the feed settings page to the Connect Settings menu
*
@@ -201,7 +200,7 @@ class OStatusPlugin extends Plugin
$output->element('a', array('href' => $url,
'class' => 'entity_remote_subscribe'),
_m('OStatus'));
-
+
$output->elementEnd('li');
}
}
@@ -221,25 +220,25 @@ class OStatusPlugin extends Plugin
$w = new Webfinger;
$endpoint_uri = '';
-
+
$result = $w->lookup($webfinger);
if (empty($result)) {
continue;
}
-
+
foreach ($result->links as $link) {
if ($link['rel'] == 'salmon') {
$endpoint_uri = $link['href'];
}
}
-
+
if (empty($endpoint_uri)) {
continue;
}
$xml = '<?xml version="1.0" encoding="UTF-8" ?>';
$xml .= $notice->asAtomEntry();
-
+
$salmon = new Salmon();
$salmon->post($endpoint_uri, $xml);
}
@@ -251,14 +250,14 @@ class OStatusPlugin extends Plugin
*/
function onEndUnsubscribe($user, $other)
{
- $feed = Feedinfo::staticGet('profile_id', $other->id);
+ $profile = Ostatus_profile::staticGet('profile_id', $other->id);
if ($feed) {
$sub = new Subscription();
$sub->subscribed = $other->id;
$sub->limit(1);
if (!$sub->find(true)) {
common_log(LOG_INFO, "Unsubscribing from now-unused feed $feed->feeduri on hub $feed->huburi");
- $feed->unsubscribe();
+ $profile->unsubscribe();
}
}
return true;
@@ -269,7 +268,7 @@ class OStatusPlugin extends Plugin
*/
function onCheckSchema() {
$schema = Schema::get();
- $schema->ensureTable('feedinfo', Feedinfo::schemaDef());
+ $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef());
$schema->ensureTable('hubsub', HubSub::schemaDef());
return true;
}
diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php
index 6f592bf5b..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,13 +196,16 @@ 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.'));
@@ -212,23 +215,21 @@ class FeedSubSettingsAction extends ConnectSettingsAction
// 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 ($this->feedinfo->isGroup()) {
- if ($user->isMember($profile)) {
+ if ($this->profile->isGroup()) {
+ $group = $this->profile->localGroup();
+ if ($user->isMember($group)) {
$this->showForm(_m('Already a member!'));
- } elseif (Group_member::join($this->feedinfo->group_id, $user->id)) {
+ } elseif (Group_member::join($this->profile->group_id, $user->id)) {
$this->showForm(_m('Joined remote group!'));
} else {
$this->showForm(_m('Remote group join failed!'));
}
} else {
- if ($user->isSubscribed($profile)) {
+ $local = $this->profile->localProfile();
+ if ($user->isSubscribed($local)) {
$this->showForm(_m('Already subscribed!'));
- } elseif ($user->subscribeTo($profile)) {
+ } elseif ($user->subscribeTo($local)) {
$this->showForm(_m('Feed subscribed!'));
} else {
$this->showForm(_m('Feed subscription failed!'));
@@ -247,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/ostatussub.php b/plugins/OStatus/actions/ostatussub.php
index ffc4ae8df..9774286fd 100644
--- a/plugins/OStatus/actions/ostatussub.php
+++ b/plugins/OStatus/actions/ostatussub.php
@@ -164,9 +164,9 @@ class OStatusSubAction extends Action
}
$this->munger = $discover->feedMunger();
- $this->feedinfo = $this->munger->feedInfo();
+ $this->profile = $this->munger->ostatusProfile();
- if ($this->feedinfo->huburi == '') {
+ if ($this->profile->huburi == '') {
$this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
return false;
}
@@ -178,13 +178,13 @@ class OStatusSubAction extends Action
{
if ($this->validateFeed()) {
$this->preview = true;
- $this->feedinfo = Feedinfo::ensureProfile($this->munger);
+ $this->profile = Ostatus_profile::ensureProfile($this->munger);
// If not already in use, subscribe to updates via the hub
- if ($this->feedinfo->sub_start) {
- common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}");
+ if ($this->profile->sub_start) {
+ common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}");
} else {
- $ok = $this->feedinfo->subscribe();
+ $ok = $this->profile->subscribe();
common_log(LOG_INFO, __METHOD__ . ": sub was $ok");
if (!$ok) {
$this->showForm(_m('Feed subscription failed! Bad response from hub.'));
@@ -194,7 +194,7 @@ class OStatusSubAction extends Action
// And subscribe the current user to the local profile
$user = common_current_user();
- $profile = $this->feedinfo->getProfile();
+ $profile = $this->profile->getProfile();
if ($user->isSubscribed($profile)) {
$this->showForm(_m('Already subscribed!'));
@@ -209,7 +209,7 @@ class OStatusSubAction extends Action
function previewFeed()
{
- $feedinfo = $this->munger->feedinfo();
+ $profile = $this->munger->ostatusProfile();
$notice = $this->munger->notice(0, true); // preview
if ($notice) {
diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php
index 471d079ab..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,33 +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!
if ($mode == 'subscribe') {
common_log(LOG_INFO, __METHOD__ . ': sub confirmed');
- $feedinfo->sub_start = common_sql_date(time());
- if ($lease_seconds > 0) {
- $feedinfo->sub_end = common_sql_date(time() + $lease_seconds);
- } else {
- $feedinfo->sub_end = null;
- }
- $feedinfo->update();
+ $profile->confirmSubscribe($lease_seconds);
} else {
common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic");
- $feedinfo->delete();
+ $profile->confirmUnsubscribe();
}
-
print $challenge;
}
}
diff --git a/plugins/OStatus/actions/salmon.php b/plugins/OStatus/actions/salmon.php
index 012869cf7..b616027a9 100644
--- a/plugins/OStatus/actions/salmon.php
+++ b/plugins/OStatus/actions/salmon.php
@@ -22,28 +22,60 @@
* @author James Walker <james@status.net>
*/
-if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+if (!defined('STATUSNET')) {
+ exit(1);
+}
class SalmonAction extends Action
{
+ var $user = null;
+ var $xml = null;
+ var $activity = null;
- function handle()
+ function prepare($args)
{
- parent::handle();
- if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- $this->handlePost();
+ if ($_SERVER['REQUEST_METHOD'] != 'POST') {
+ $this->clientError(_('This method requires a POST.'));
}
- }
+ if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') {
+ $this->clientError(_('Salmon requires application/atom+xml'));
+ }
- function handlePost()
- {
- $user_id = $this->arg('id');
- common_log(LOG_DEBUG, 'Salmon: incoming post for user: '. $user_id);
+ $id = $this->trimmed('id');
+
+ if (!$id) {
+ $this->clientError(_('No ID.'));
+ }
+
+ $this->user = User::staticGet($id);
+
+ if (empty($this->user)) {
+ $this->clientError(_('No such user.'));
+ }
$xml = file_get_contents('php://input');
+ $dom = DOMDocument::loadXML($xml);
+
+ // XXX: check that document element is Atom entry
+ // XXX: check the signature
+
+ $this->act = Activity::fromAtomEntry($dom->documentElement);
+ }
+
+ function handle($args)
+ {
+ common_log(LOG_DEBUG, 'Salmon: incoming post for user: '. $user_id);
+
// TODO : Insert new $xml -> notice code
+ switch ($this->act->verb)
+ {
+ case Activity::POST:
+ case Activity::SHARE:
+ case Activity::FAVORITE:
+ case Activity::FOLLOW:
+ }
}
}
diff --git a/plugins/OStatus/classes/Feedinfo.php b/plugins/OStatus/classes/Ostatus_profile.php
index 5b8a9039a..733d8843b 100644
--- a/plugins/OStatus/classes/Feedinfo.php
+++ b/plugins/OStatus/classes/Ostatus_profile.php
@@ -25,17 +25,17 @@
/*
PuSH subscription flow:
- $feedinfo->subscribe()
+ $profile->subscribe()
generate random verification token
save to verify_token
sends a sub request to the hub...
- feedsub/callback
+ main/push/callback
hub sends confirmation back to us via GET
We verify the request, then echo back the challenge.
On our end, we save the time we subscribed and the lease expiration
- feedsub/callback
+ main/push/callback
hub sends us updates via POST
*/
@@ -51,23 +51,27 @@ class FeedDBException extends FeedSubException
}
}
-class Feedinfo extends Memcached_DataObject
+class Ostatus_profile extends Memcached_DataObject
{
- public $__table = 'feedinfo';
+ public $__table = 'ostatus_profile';
public $id;
public $profile_id;
+ public $group_id;
public $feeduri;
public $homeuri;
- public $huburi;
// PuSH subscription data
+ public $huburi;
public $secret;
public $verify_token;
+ public $sub_state; // subscribe, active, unsubscribe
public $sub_start;
public $sub_end;
+ public $salmonuri;
+
public $created;
public $lastupdate;
@@ -96,6 +100,7 @@ class Feedinfo extends Memcached_DataObject
'huburi' => DB_DATAOBJECT_STR,
'secret' => DB_DATAOBJECT_STR,
'verify_token' => DB_DATAOBJECT_STR,
+ 'sub_state' => DB_DATAOBJECT_STR,
'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
'salmonuri' => DB_DATAOBJECT_STR,
@@ -126,6 +131,8 @@ class Feedinfo extends Memcached_DataObject
32, true),
new ColumnDef('secret', 'varchar',
64, true),
+ new ColumnDef('sub_state', "enum('subscribe','active','unsubscribe')",
+ null, true),
new ColumnDef('sub_start', 'datetime',
null, true),
new ColumnDef('sub_end', 'datetime',
@@ -175,83 +182,186 @@ class Feedinfo extends Memcached_DataObject
* Fetch the StatusNet-side profile for this feed
* @return Profile
*/
- public function getProfile()
+ 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()
{
- return Profile::staticGet('id', $this->profile_id);
+ 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 Feedinfo
+ * @return Ostatus_profile
*/
public static function ensureProfile($munger)
{
- $feedinfo = $munger->feedinfo();
+ $profile = $munger->ostatusProfile();
- $current = self::staticGet('feeduri', $feedinfo->feeduri);
+ $current = self::staticGet('feeduri', $profile->feeduri);
if ($current) {
// @fixme we should probably update info as necessary
return $current;
}
- $feedinfo->query('BEGIN');
+ $profile->query('BEGIN');
// Awful hack! Awful hack!
- $feedinfo->verify = common_good_rand(16);
- $feedinfo->secret = common_good_rand(32);
+ $profile->verify = common_good_rand(16);
+ $profile->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);
- }
+ $local = $munger->profile();
- $feedinfo->profile_id = $profile->id;
- if ($feedinfo->isGroup()) {
+ if ($entity->isGroup()) {
$group = new User_group();
- $group->nickname = $profile->nickname . '@remote'; // @fixme
- $group->fullname = $profile->fullname;
- $group->homepage = $profile->homepage;
- $group->location = $profile->location;
- $group->created = $profile->created;
+ $group->nickname = $local->nickname . '@remote'; // @fixme
+ $group->fullname = $local->fullname;
+ $group->homepage = $local->homepage;
+ $group->location = $local->location;
+ $group->created = $local->created;
$group->insert();
-
- if ($avatar) {
- $group->setOriginal($filename);
+ if (empty($result)) {
+ throw new FeedDBException($group);
}
-
- $feedinfo->group_id = $group->id;
+ $profile->group_id = $group->id;
+ } else {
+ $result = $local->insert();
+ if (empty($result)) {
+ throw new FeedDBException($local);
+ }
+ $profile->profile_id = $local->id;
}
- $result = $feedinfo->insert();
+ $profile->created = sql_common_date();
+ $profile->lastupdate = sql_common_date();
+ $result = $profile->insert();
if (empty($result)) {
- throw new FeedDBException($feedinfo);
+ throw new FeedDBException($profile);
}
- $feedinfo->query('COMMIT');
+ $entity->query('COMMIT');
} catch (FeedDBException $e) {
common_log_db_error($e->obj, 'INSERT', __FILE__);
- $feedinfo->query('ROLLBACK');
+ $entity->query('ROLLBACK');
return false;
}
- return $feedinfo;
+
+ $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();
}
/**
@@ -312,7 +422,47 @@ class Feedinfo extends Memcached_DataObject
}
/**
- * Send an unsubscription request to the hub for this feed.
+ * 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
@@ -322,6 +472,92 @@ class Feedinfo extends Memcached_DataObject
}
/**
+ * 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.
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 25b0a0931..c895b6ce2 100644
--- a/plugins/OStatus/lib/feedmunger.php
+++ b/plugins/OStatus/lib/feedmunger.php
@@ -83,17 +83,17 @@ class FeedMunger
$this->url = $url;
}
- function feedinfo()
+ function ostatusProfile()
{
- $feedinfo = new Feedinfo();
- $feedinfo->feeduri = $this->url;
- $feedinfo->homeuri = $this->feed->link;
- $feedinfo->huburi = $this->getHubLink();
+ $profile = new Ostatus_profile();
+ $profile->feeduri = $this->url;
+ $profile->homeuri = $this->feed->link;
+ $profile->huburi = $this->getHubLink();
$salmon = $this->getSalmonLink();
if ($salmon) {
- $feedinfo->salmonuri = $salmon;
+ $profile->salmonuri = $salmon;
}
- return $feedinfo;
+ return $profile;
}
function getAtomLink($item, $attribs=array())
@@ -258,9 +258,7 @@ class FeedMunger
{
// hack hack hack
// should get profile for this entry's author...
- $feed = new Feedinfo();
- $feed->feeduri = $self;
- $feed = Feedinfo::staticGet('feeduri', $this->getSelfLink());
+ $remote = Ostatus_profile::staticGet('feeduri', $this->getSelfLink());
if ($feed) {
return $feed->profile_id;
} else {
@@ -269,6 +267,9 @@ class FeedMunger
}
/**
+ * Parse location given as a GeoRSS-simple point, if provided.
+ * http://www.georss.org/simple
+ *
* @param feed item $entry
* @return mixed Location or false
*/
@@ -278,7 +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/theme/base/css/display.css b/theme/base/css/display.css
index 8490fb580..3218276a6 100644
--- a/theme/base/css/display.css
+++ b/theme/base/css/display.css
@@ -1104,25 +1104,22 @@ left:0;
.dialogbox {
position:absolute;
-top:-4px;
-right:29px;
+top:-1px;
+right:-1px;
z-index:9;
-min-width:199px;
float:none;
-background-color:#FFF;
padding:11px;
border-radius:7px;
-moz-border-radius:7px;
-webkit-border-radius:7px;
border-style:solid;
border-width:1px;
-border-color:#DDDDDD;
--moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
}
.dialogbox legend {
display:block !important;
margin-right:18px;
+margin-bottom:18px;
}
.dialogbox button.close {
@@ -1131,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,
@@ -1145,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 82eb13531..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;
}
@@ -46,7 +48,8 @@ box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3);
.pagination .nav_prev a,
.pagination .nav_next a,
.form_settings fieldset fieldset,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
border-color:#DDDDDD;
}
@@ -78,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 {
@@ -133,9 +137,6 @@ color:#002FA7;
#content tbody tr {
border-top-color:#C8D1D5;
}
-.mark-top {
-border-color:#AAAAAA;
-}
#aside_primary {
background-color:#C8D1D5;
@@ -144,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] {
@@ -221,7 +224,8 @@ border-color:transparent;
#content,
#site_nav_local_views .current a,
.entity_send-a-message .form_notice,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
background-color:#FFFFFF;
}
@@ -308,7 +312,8 @@ background-position: 5px -718px;
background-position: 5px -852px;
}
.entity_send-a-message .form_notice,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
-moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
-webkit-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
diff --git a/theme/identica/css/display.css b/theme/identica/css/display.css
index 44ae4953b..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;
}
@@ -46,7 +48,8 @@ box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3);
.pagination .nav_prev a,
.pagination .nav_next a,
.form_settings fieldset fieldset,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
border-color:#DDDDDD;
}
@@ -88,6 +91,7 @@ color:#FFFFFF;
border-color:transparent;
text-shadow:none;
}
+
.dialogbox .submit_dialogbox,
input.submit,
.form_notice input.submit {
@@ -133,9 +137,6 @@ color:#002FA7;
#content tbody tr {
border-top-color:#CEE1E9;
}
-.mark-top {
-border-color:#AAAAAA;
-}
#aside_primary {
background-color:#CEE1E9;
@@ -144,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] {
@@ -221,7 +224,8 @@ border-color:transparent;
#content,
#site_nav_local_views .current a,
.entity_send-a-message .form_notice,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
background-color:#FFFFFF;
}
@@ -307,7 +311,8 @@ background-position: 5px -718px;
background-position: 5px -852px;
}
.entity_send-a-message .form_notice,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
-moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
-webkit-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);