forked from GNUsocial/gnu-social
Merge branch 'testing' of gitorious.org:statusnet/mainline into 0.9.x
This commit is contained in:
commit
122c8677b7
@ -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);
|
||||
|
@ -114,39 +114,71 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction
|
||||
$title = sprintf(_("%s and friends"), $this->user->nickname);
|
||||
$taguribase = common_config('integration', 'taguri');
|
||||
$id = "tag:$taguribase:FriendsTimeline:" . $this->user->id;
|
||||
$link = common_local_url(
|
||||
'all', array('nickname' => $this->user->nickname)
|
||||
);
|
||||
$subtitle = sprintf(
|
||||
_('Updates from %1$s and friends on %2$s!'),
|
||||
$this->user->nickname, $sitename
|
||||
);
|
||||
$logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
|
||||
|
||||
$subtitle = sprintf(
|
||||
_('Updates from %1$s and friends on %2$s!'),
|
||||
$this->user->nickname, $sitename
|
||||
);
|
||||
|
||||
$logo = (!empty($avatar))
|
||||
? $avatar->displayUrl()
|
||||
: Avatar::defaultImage(AVATAR_PROFILE_SIZE);
|
||||
|
||||
switch($this->format) {
|
||||
case 'xml':
|
||||
$this->showXmlTimeline($this->notices);
|
||||
break;
|
||||
case 'rss':
|
||||
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
|
||||
|
||||
$link = common_local_url(
|
||||
'all', array(
|
||||
'nickname' => $this->user->nickname
|
||||
)
|
||||
);
|
||||
|
||||
$this->showRssTimeline(
|
||||
$this->notices,
|
||||
$title,
|
||||
$link,
|
||||
$subtitle,
|
||||
null,
|
||||
$logo
|
||||
);
|
||||
break;
|
||||
case 'atom':
|
||||
|
||||
$target_id = $this->arg('id');
|
||||
header('Content-Type: application/atom+xml; charset=utf-8');
|
||||
|
||||
if (isset($target_id)) {
|
||||
$selfuri = common_root_url() .
|
||||
'api/statuses/friends_timeline/' .
|
||||
$target_id . '.atom';
|
||||
} else {
|
||||
$selfuri = common_root_url() .
|
||||
'api/statuses/friends_timeline.atom';
|
||||
$atom = new AtomNoticeFeed();
|
||||
|
||||
$atom->setId($id);
|
||||
$atom->setTitle($title);
|
||||
$atom->setSubtitle($subtitle);
|
||||
$atom->setLogo($logo);
|
||||
$atom->setUpdated('now');
|
||||
|
||||
$atom->addLink(
|
||||
common_local_url(
|
||||
'all',
|
||||
array('nickname' => $this->user->nickname)
|
||||
)
|
||||
);
|
||||
|
||||
$id = $this->arg('id');
|
||||
$aargs = array('format' => 'atom');
|
||||
if (!empty($id)) {
|
||||
$aargs['id'] = $id;
|
||||
}
|
||||
|
||||
$this->showAtomTimeline(
|
||||
$this->notices, $title, $id, $link,
|
||||
$subtitle, null, $selfuri, $logo
|
||||
);
|
||||
$atom->addLink(
|
||||
$this->getSelfUri('ApiTimelineFriends', $aargs),
|
||||
array('rel' => 'self', 'type' => 'application/atom+xml')
|
||||
);
|
||||
|
||||
$atom->addEntryFromNotices($this->notices);
|
||||
|
||||
$this->raw($atom->getString());
|
||||
|
||||
break;
|
||||
case 'json':
|
||||
$this->showJsonTimeline($this->notices);
|
||||
|
@ -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,
|
||||
|
@ -115,39 +115,67 @@ class ApiTimelineHomeAction extends ApiBareAuthAction
|
||||
$title = sprintf(_("%s and friends"), $this->user->nickname);
|
||||
$taguribase = common_config('integration', 'taguri');
|
||||
$id = "tag:$taguribase:HomeTimeline:" . $this->user->id;
|
||||
$link = common_local_url(
|
||||
'all', array('nickname' => $this->user->nickname)
|
||||
);
|
||||
|
||||
$subtitle = sprintf(
|
||||
_('Updates from %1$s and friends on %2$s!'),
|
||||
$this->user->nickname, $sitename
|
||||
);
|
||||
$logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
|
||||
|
||||
$logo = (!empty($avatar))
|
||||
? $avatar->displayUrl()
|
||||
: Avatar::defaultImage(AVATAR_PROFILE_SIZE);
|
||||
|
||||
switch($this->format) {
|
||||
case 'xml':
|
||||
$this->showXmlTimeline($this->notices);
|
||||
break;
|
||||
case 'rss':
|
||||
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
|
||||
$link = common_local_url(
|
||||
'all',
|
||||
array('nickname' => $this->user->nickname)
|
||||
);
|
||||
$this->showRssTimeline(
|
||||
$this->notices,
|
||||
$title,
|
||||
$link,
|
||||
$subtitle,
|
||||
null,
|
||||
$logo
|
||||
);
|
||||
break;
|
||||
case 'atom':
|
||||
|
||||
$target_id = $this->arg('id');
|
||||
header('Content-Type: application/atom+xml; charset=utf-8');
|
||||
|
||||
if (isset($target_id)) {
|
||||
$selfuri = common_root_url() .
|
||||
'api/statuses/home_timeline/' .
|
||||
$target_id . '.atom';
|
||||
} else {
|
||||
$selfuri = common_root_url() .
|
||||
'api/statuses/home_timeline.atom';
|
||||
$atom = new AtomNoticeFeed();
|
||||
|
||||
$atom->setId($id);
|
||||
$atom->setTitle($title);
|
||||
$atom->setSubtitle($subtitle);
|
||||
$atom->setLogo($logo);
|
||||
$atom->setUpdated('now');
|
||||
|
||||
$atom->addLink(
|
||||
common_local_url(
|
||||
'all',
|
||||
array('nickname' => $this->user->nickname)
|
||||
)
|
||||
);
|
||||
|
||||
$id = $this->arg('id');
|
||||
$aargs = array('format' => 'atom');
|
||||
if (!empty($id)) {
|
||||
$aargs['id'] = $id;
|
||||
}
|
||||
|
||||
$this->showAtomTimeline(
|
||||
$this->notices, $title, $id, $link,
|
||||
$subtitle, null, $selfuri, $logo
|
||||
$atom->addLink(
|
||||
$this->getSelfUri('ApiTimelineHome', $aargs),
|
||||
array('rel' => 'self', 'type' => 'application/atom+xml')
|
||||
);
|
||||
|
||||
$atom->addEntryFromNotices($this->notices);
|
||||
$this->raw($atom->getString());
|
||||
|
||||
break;
|
||||
case 'json':
|
||||
$this->showJsonTimeline($this->notices);
|
||||
|
@ -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);
|
||||
|
@ -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()) {
|
||||
|
@ -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:
|
||||
|
@ -100,10 +100,6 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction
|
||||
$sitename = common_config('site', 'name');
|
||||
$sitelogo = (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png');
|
||||
$title = sprintf(_("Notices tagged with %s"), $this->tag);
|
||||
$link = common_local_url(
|
||||
'tag',
|
||||
array('tag' => $this->tag)
|
||||
);
|
||||
$subtitle = sprintf(
|
||||
_('Updates tagged with %1$s on %2$s!'),
|
||||
$this->tag,
|
||||
@ -117,22 +113,51 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction
|
||||
$this->showXmlTimeline($this->notices);
|
||||
break;
|
||||
case 'rss':
|
||||
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo);
|
||||
break;
|
||||
case 'atom':
|
||||
$selfuri = common_root_url() .
|
||||
'api/statusnet/tags/timeline/' .
|
||||
$this->tag . '.atom';
|
||||
$this->showAtomTimeline(
|
||||
$link = common_local_url(
|
||||
'tag',
|
||||
array('tag' => $this->tag)
|
||||
);
|
||||
$this->showRssTimeline(
|
||||
$this->notices,
|
||||
$title,
|
||||
$id,
|
||||
$link,
|
||||
$subtitle,
|
||||
null,
|
||||
$selfuri,
|
||||
$sitelogo
|
||||
);
|
||||
break;
|
||||
case 'atom':
|
||||
|
||||
header('Content-Type: application/atom+xml; charset=utf-8');
|
||||
|
||||
$atom = new AtomNoticeFeed();
|
||||
|
||||
$atom->setId($id);
|
||||
$atom->setTitle($title);
|
||||
$atom->setSubtitle($subtitle);
|
||||
$atom->setLogo($logo);
|
||||
$atom->setUpdated('now');
|
||||
|
||||
$atom->addLink(
|
||||
common_local_url(
|
||||
'tag',
|
||||
array('tag' => $this->tag)
|
||||
)
|
||||
);
|
||||
|
||||
$aargs = array('format' => 'atom');
|
||||
if (!empty($this->tag)) {
|
||||
$aargs['tag'] = $this->tag;
|
||||
}
|
||||
|
||||
$atom->addLink(
|
||||
$this->getSelfUri('ApiTimelineTag', $aargs),
|
||||
array('rel' => 'self', 'type' => 'application/atom+xml')
|
||||
);
|
||||
|
||||
$atom->addEntryFromNotices($this->notices);
|
||||
$this->raw($atom->getString());
|
||||
|
||||
break;
|
||||
case 'json':
|
||||
$this->showJsonTimeline($this->notices);
|
||||
|
@ -145,19 +145,47 @@ class ApiTimelineUserAction extends ApiBareAuthAction
|
||||
);
|
||||
break;
|
||||
case 'atom':
|
||||
$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';
|
||||
}
|
||||
$this->showAtomTimeline(
|
||||
$this->notices, $title, $id, $link,
|
||||
$subtitle, $suplink, $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(
|
||||
'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);
|
||||
|
@ -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,
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
18
lib/api.php
18
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
106
lib/atom10entry.php
Normal file
106
lib/atom10entry.php
Normal file
@ -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();
|
||||
}
|
||||
|
||||
}
|
227
lib/atom10feed.php
Normal file
227
lib/atom10feed.php
Normal file
@ -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);
|
||||
}
|
||||
|
||||
}
|
103
lib/atomnoticefeed.php
Normal file
103
lib/atomnoticefeed.php
Normal file
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.");
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)) {
|
||||
|
@ -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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
$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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
42
plugins/OStatus/actions/hostmeta.php
Normal file
42
plugins/OStatus/actions/hostmeta.php
Normal file
@ -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);
|
||||
}
|
||||
}
|
128
plugins/OStatus/actions/ostatusinit.php
Normal file
128
plugins/OStatus/actions/ostatusinit.php
Normal file
@ -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');
|
||||
}
|
||||
|
||||
}
|
226
plugins/OStatus/actions/ostatussub.php
Normal file
226
plugins/OStatus/actions/ostatussub.php
Normal file
@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
#}
|
||||
|
||||
// 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);
|
||||
} else {
|
||||
$feedinfo->sub_end = null;
|
||||
if ($profile->verify_token !== $verify_token) {
|
||||
common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic");
|
||||
throw new ServerError("Bogus hub callback: bad token", 404);
|
||||
}
|
||||
|
||||
if ($mode != $profile->sub_state) {
|
||||
common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad mode \"$mode\" for feed $topic in state \"{$profile->sub_state}\"");
|
||||
throw new ServerException("Bogus hub callback: mode doesn't match subscription state.", 404);
|
||||
}
|
||||
|
||||
// OK!
|
||||
if ($mode == 'subscribe') {
|
||||
common_log(LOG_INFO, __METHOD__ . ': sub confirmed');
|
||||
$profile->confirmSubscribe($lease_seconds);
|
||||
} else {
|
||||
common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic");
|
||||
$profile->confirmUnsubscribe();
|
||||
}
|
||||
$feedinfo->update();
|
||||
|
||||
print $challenge;
|
||||
}
|
||||
}
|
||||
|
81
plugins/OStatus/actions/salmon.php
Normal file
81
plugins/OStatus/actions/salmon.php
Normal file
@ -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:
|
||||
}
|
||||
}
|
||||
}
|
77
plugins/OStatus/actions/webfinger.php
Normal file
77
plugins/OStatus/actions/webfinger.php
Normal file
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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)';
|
||||
|
644
plugins/OStatus/classes/Ostatus_profile.php
Normal file
644
plugins/OStatus/classes/Ostatus_profile.php
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
85
plugins/OStatus/lib/activity.php
Normal file
85
plugins/OStatus/lib/activity.php
Normal file
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
64
plugins/OStatus/lib/salmon.php
Normal file
64
plugins/OStatus/lib/salmon.php
Normal file
@ -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)
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
}
|
143
plugins/OStatus/lib/webfinger.php
Normal file
143
plugins/OStatus/lib/webfinger.php
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
183
plugins/OStatus/lib/xrd.php
Normal file
183
plugins/OStatus/lib/xrd.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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] {
|
||||
|
@ -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] {
|
||||
|
Loading…
Reference in New Issue
Block a user