Merge branch 'testing' of gitorious.org:statusnet/mainline into 0.9.x

This commit is contained in:
Brion Vibber 2010-02-12 11:18:35 -08:00
commit 122c8677b7
45 changed files with 3007 additions and 581 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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,

View File

@ -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);

View File

@ -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);

View File

@ -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()) {

View File

@ -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:

View File

@ -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);

View File

@ -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);

View File

@ -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,

View File

@ -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');
}
}

View File

@ -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');

View File

@ -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');
}
}

View File

@ -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);
}

View File

@ -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

View File

@ -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
View 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
View 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
View 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());
}
}

View File

@ -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
),

View File

@ -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;
}
}

View File

@ -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.");
}

View File

@ -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) {

View File

@ -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)) {

View File

@ -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;
}

View File

@ -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) {

View 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);
}
}

View 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');
}
}

View 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.');
}
}
}

View File

@ -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;
}
}

View 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:
}
}
}

View 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();
}
}

View File

@ -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");
}
}
}

View File

@ -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)';

View 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");
}
}
}

View 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()
{
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View 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)
{
}
}

View 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
View 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;
}
}

View File

@ -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;

View File

@ -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] {

View File

@ -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] {