Merge branch 'testing' of git@gitorious.org:statusnet/mainline into testing
This commit is contained in:
commit
5db40c440d
10
EVENTS.txt
10
EVENTS.txt
@ -1,4 +1,4 @@
|
||||
InitializePlugin: a chance to initialize a plugin in a complete environment
|
||||
\InitializePlugin: a chance to initialize a plugin in a complete environment
|
||||
|
||||
CleanupPlugin: a chance to cleanup a plugin at the end of a program
|
||||
|
||||
@ -355,6 +355,14 @@ EndShowHeadElements: Right before the </head> tag; put <script>s here if you nee
|
||||
|
||||
CheckSchema: chance to check the schema
|
||||
|
||||
StartProfileRemoteSubscribe: Before showing the link to remote subscription
|
||||
- $userprofile: UserProfile widget
|
||||
- &$profile: the profile being shown
|
||||
|
||||
EndProfileRemoteSubscribe: After showing the link to remote subscription
|
||||
- $userprofile: UserProfile widget
|
||||
- &$profile: the profile being shown
|
||||
|
||||
StartProfilePageProfileSection: Starting to show the section of the
|
||||
profile page with the actual profile data;
|
||||
hook to prevent showing the profile (e.g.)
|
||||
|
19
README
19
README
@ -1192,6 +1192,8 @@ server: If set, defines another server where avatars are stored in the
|
||||
typically only make 2 connections to a single server at a
|
||||
time <http://ur1.ca/6ih>, so this can parallelize the job.
|
||||
Defaults to null.
|
||||
ssl: Whether to access avatars using HTTPS. Defaults to null, meaning
|
||||
to guess based on site-wide SSL settings.
|
||||
|
||||
public
|
||||
------
|
||||
@ -1221,6 +1223,19 @@ path: Path part of theme URLs, before the theme name. Relative to the
|
||||
(using version numbers as the path) to make sure that all files are
|
||||
reloaded by caching clients or proxies. Defaults to null,
|
||||
which means to use the site path + '/theme'.
|
||||
ssl: Whether to use SSL for theme elements. Default is null, which means
|
||||
guess based on site SSL settings.
|
||||
|
||||
javascript
|
||||
----------
|
||||
|
||||
server: You can speed up page loading by pointing the
|
||||
theme file lookup to another server (virtual or real).
|
||||
Defaults to NULL, meaning to use the site server.
|
||||
path: Path part of Javascript URLs. Defaults to null,
|
||||
which means to use the site path + '/js/'.
|
||||
ssl: Whether to use SSL for JavaScript files. Default is null, which means
|
||||
guess based on site SSL settings.
|
||||
|
||||
xmpp
|
||||
----
|
||||
@ -1447,6 +1462,8 @@ server: server name to use when creating URLs for uploaded files.
|
||||
a virtual server here can speed up Web performance.
|
||||
path: URL path, relative to the server, to find files. Defaults to
|
||||
main path + '/file/'.
|
||||
ssl: whether to use HTTPS for file URLs. Defaults to null, meaning to
|
||||
guess based on other SSL settings.
|
||||
filecommand: command to use for determining the type of a file. May be
|
||||
skipped if fileinfo extension is installed. Defaults to
|
||||
'/usr/bin/file'.
|
||||
@ -1506,6 +1523,8 @@ dir: directory to write backgrounds too. Default is '/background/'
|
||||
subdir of install dir.
|
||||
path: path to backgrounds. Default is sub-path of install path; note
|
||||
that you may need to change this if you change site-path too.
|
||||
ssl: Whether or not to use HTTPS for background files. Defaults to
|
||||
null, meaning to guess from site-wide SSL settings.
|
||||
|
||||
ping
|
||||
----
|
||||
|
@ -100,11 +100,11 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction
|
||||
|
||||
function showTimeline()
|
||||
{
|
||||
$profile = $this->user->getProfile();
|
||||
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
|
||||
$profile = $this->user->getProfile();
|
||||
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
|
||||
|
||||
$sitename = common_config('site', 'name');
|
||||
$title = sprintf(
|
||||
$sitename = common_config('site', 'name');
|
||||
$title = sprintf(
|
||||
_('%1$s / Favorites from %2$s'),
|
||||
$sitename,
|
||||
$this->user->nickname
|
||||
@ -112,32 +112,69 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction
|
||||
|
||||
$taguribase = common_config('integration', 'taguri');
|
||||
$id = "tag:$taguribase:Favorites:" . $this->user->id;
|
||||
$link = common_local_url(
|
||||
'favorites',
|
||||
array('nickname' => $this->user->nickname)
|
||||
);
|
||||
$subtitle = sprintf(
|
||||
|
||||
$subtitle = sprintf(
|
||||
_('%1$s updates favorited by %2$s / %2$s.'),
|
||||
$sitename,
|
||||
$profile->getBestName(),
|
||||
$this->user->nickname
|
||||
);
|
||||
$logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
|
||||
$logo = !empty($avatar)
|
||||
? $avatar->displayUrl()
|
||||
: Avatar::defaultImage(AVATAR_PROFILE_SIZE);
|
||||
|
||||
switch($this->format) {
|
||||
case 'xml':
|
||||
$this->showXmlTimeline($this->notices);
|
||||
break;
|
||||
case 'rss':
|
||||
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
|
||||
$link = common_local_url(
|
||||
'showfavorites',
|
||||
array('nickname' => $this->user->nickname)
|
||||
);
|
||||
$this->showRssTimeline(
|
||||
$this->notices,
|
||||
$title,
|
||||
$link,
|
||||
$subtitle,
|
||||
null,
|
||||
$logo
|
||||
);
|
||||
break;
|
||||
case 'atom':
|
||||
$selfuri = common_root_url() .
|
||||
ltrim($_SERVER['QUERY_STRING'], 'p=');
|
||||
$this->showAtomTimeline(
|
||||
$this->notices, $title, $id, $link, $subtitle,
|
||||
null, $selfuri, $logo
|
||||
|
||||
header('Content-Type: application/atom+xml; charset=utf-8');
|
||||
|
||||
$atom = new AtomNoticeFeed();
|
||||
|
||||
$atom->setId($id);
|
||||
$atom->setTitle($title);
|
||||
$atom->setSubtitle($subtitle);
|
||||
$atom->setLogo($logo);
|
||||
$atom->setUpdated('now');
|
||||
|
||||
$atom->addLink(
|
||||
common_local_url(
|
||||
'showfavorites',
|
||||
array('nickname' => $this->user->nickname)
|
||||
)
|
||||
);
|
||||
|
||||
$id = $this->arg('id');
|
||||
$aargs = array('format' => 'atom');
|
||||
if (!empty($id)) {
|
||||
$aargs['id'] = $id;
|
||||
}
|
||||
|
||||
$atom->addLink(
|
||||
$this->getSelfUri('ApiTimelineFavorites', $aargs),
|
||||
array('rel' => 'self', 'type' => 'application/atom+xml')
|
||||
);
|
||||
|
||||
$atom->addEntryFromNotices($this->notices);
|
||||
|
||||
$this->raw($atom->getString());
|
||||
|
||||
break;
|
||||
case 'json':
|
||||
$this->showJsonTimeline($this->notices);
|
||||
|
@ -114,39 +114,71 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction
|
||||
$title = sprintf(_("%s and friends"), $this->user->nickname);
|
||||
$taguribase = common_config('integration', 'taguri');
|
||||
$id = "tag:$taguribase:FriendsTimeline:" . $this->user->id;
|
||||
$link = common_local_url(
|
||||
'all', array('nickname' => $this->user->nickname)
|
||||
);
|
||||
$subtitle = sprintf(
|
||||
_('Updates from %1$s and friends on %2$s!'),
|
||||
$this->user->nickname, $sitename
|
||||
);
|
||||
$logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
|
||||
|
||||
$subtitle = sprintf(
|
||||
_('Updates from %1$s and friends on %2$s!'),
|
||||
$this->user->nickname, $sitename
|
||||
);
|
||||
|
||||
$logo = (!empty($avatar))
|
||||
? $avatar->displayUrl()
|
||||
: Avatar::defaultImage(AVATAR_PROFILE_SIZE);
|
||||
|
||||
switch($this->format) {
|
||||
case 'xml':
|
||||
$this->showXmlTimeline($this->notices);
|
||||
break;
|
||||
case 'rss':
|
||||
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
|
||||
|
||||
$link = common_local_url(
|
||||
'all', array(
|
||||
'nickname' => $this->user->nickname
|
||||
)
|
||||
);
|
||||
|
||||
$this->showRssTimeline(
|
||||
$this->notices,
|
||||
$title,
|
||||
$link,
|
||||
$subtitle,
|
||||
null,
|
||||
$logo
|
||||
);
|
||||
break;
|
||||
case 'atom':
|
||||
|
||||
$target_id = $this->arg('id');
|
||||
header('Content-Type: application/atom+xml; charset=utf-8');
|
||||
|
||||
if (isset($target_id)) {
|
||||
$selfuri = common_root_url() .
|
||||
'api/statuses/friends_timeline/' .
|
||||
$target_id . '.atom';
|
||||
} else {
|
||||
$selfuri = common_root_url() .
|
||||
'api/statuses/friends_timeline.atom';
|
||||
$atom = new AtomNoticeFeed();
|
||||
|
||||
$atom->setId($id);
|
||||
$atom->setTitle($title);
|
||||
$atom->setSubtitle($subtitle);
|
||||
$atom->setLogo($logo);
|
||||
$atom->setUpdated('now');
|
||||
|
||||
$atom->addLink(
|
||||
common_local_url(
|
||||
'all',
|
||||
array('nickname' => $this->user->nickname)
|
||||
)
|
||||
);
|
||||
|
||||
$id = $this->arg('id');
|
||||
$aargs = array('format' => 'atom');
|
||||
if (!empty($id)) {
|
||||
$aargs['id'] = $id;
|
||||
}
|
||||
|
||||
$this->showAtomTimeline(
|
||||
$this->notices, $title, $id, $link,
|
||||
$subtitle, null, $selfuri, $logo
|
||||
);
|
||||
$atom->addLink(
|
||||
$this->getSelfUri('ApiTimelineFriends', $aargs),
|
||||
array('rel' => 'self', 'type' => 'application/atom+xml')
|
||||
);
|
||||
|
||||
$atom->addEntryFromNotices($this->notices);
|
||||
|
||||
$this->raw($atom->getString());
|
||||
|
||||
break;
|
||||
case 'json':
|
||||
$this->showJsonTimeline($this->notices);
|
||||
|
@ -109,38 +109,82 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction
|
||||
$title = sprintf(_("%s timeline"), $this->group->nickname);
|
||||
$taguribase = common_config('integration', 'taguri');
|
||||
$id = "tag:$taguribase:GroupTimeline:" . $this->group->id;
|
||||
$link = common_local_url(
|
||||
'showgroup',
|
||||
array('nickname' => $this->group->nickname)
|
||||
);
|
||||
|
||||
$subtitle = sprintf(
|
||||
_('Updates from %1$s on %2$s!'),
|
||||
$this->group->nickname,
|
||||
$sitename
|
||||
);
|
||||
$logo = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE);
|
||||
|
||||
$logo = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE);
|
||||
|
||||
switch($this->format) {
|
||||
case 'xml':
|
||||
$this->showXmlTimeline($this->notices);
|
||||
break;
|
||||
case 'rss':
|
||||
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
|
||||
break;
|
||||
case 'atom':
|
||||
$selfuri = common_root_url() .
|
||||
'api/statusnet/groups/timeline/' .
|
||||
$this->group->id . '.atom';
|
||||
$this->showAtomTimeline(
|
||||
$this->showRssTimeline(
|
||||
$this->notices,
|
||||
$title,
|
||||
$id,
|
||||
$link,
|
||||
$this->group->homeUrl(),
|
||||
$subtitle,
|
||||
null,
|
||||
$selfuri,
|
||||
$logo
|
||||
);
|
||||
break;
|
||||
case 'atom':
|
||||
|
||||
header('Content-Type: application/atom+xml; charset=utf-8');
|
||||
|
||||
try {
|
||||
|
||||
// If this was called using an integer ID, i.e.: using the canonical
|
||||
// URL for this group's feed, then pass the Group object into the feed,
|
||||
// so the OStatus plugin, and possibly other plugins, can access it.
|
||||
// Feels sorta hacky. -- Z
|
||||
|
||||
$atom = null;
|
||||
$id = $this->arg('id');
|
||||
|
||||
if (strval(intval($id)) === strval($id)) {
|
||||
$atom = new AtomGroupNoticeFeed($this->group);
|
||||
} else {
|
||||
$atom = new AtomGroupNoticeFeed();
|
||||
}
|
||||
|
||||
$atom->setId($id);
|
||||
$atom->setTitle($title);
|
||||
$atom->setSubtitle($subtitle);
|
||||
$atom->setLogo($logo);
|
||||
$atom->setUpdated('now');
|
||||
|
||||
$atom->addAuthorRaw($this->group->asAtomAuthor());
|
||||
$atom->setActivitySubject($this->group->asActivitySubject());
|
||||
|
||||
$atom->addLink($this->group->homeUrl());
|
||||
|
||||
$id = $this->arg('id');
|
||||
$aargs = array('format' => 'atom');
|
||||
if (!empty($id)) {
|
||||
$aargs['id'] = $id;
|
||||
}
|
||||
|
||||
$atom->addLink(
|
||||
$this->getSelfUri('ApiTimelineGroup', $aargs),
|
||||
array('rel' => 'self', 'type' => 'application/atom+xml')
|
||||
);
|
||||
|
||||
$atom->addEntryFromNotices($this->notices);
|
||||
|
||||
$this->raw($atom->getString());
|
||||
|
||||
} catch (Atom10FeedException $e) {
|
||||
$this->serverError(
|
||||
'Could not generate feed for group - ' . $e->getMessage()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'json':
|
||||
$this->showJsonTimeline($this->notices);
|
||||
|
@ -115,39 +115,67 @@ class ApiTimelineHomeAction extends ApiBareAuthAction
|
||||
$title = sprintf(_("%s and friends"), $this->user->nickname);
|
||||
$taguribase = common_config('integration', 'taguri');
|
||||
$id = "tag:$taguribase:HomeTimeline:" . $this->user->id;
|
||||
$link = common_local_url(
|
||||
'all', array('nickname' => $this->user->nickname)
|
||||
);
|
||||
|
||||
$subtitle = sprintf(
|
||||
_('Updates from %1$s and friends on %2$s!'),
|
||||
$this->user->nickname, $sitename
|
||||
);
|
||||
$logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
|
||||
|
||||
$logo = (!empty($avatar))
|
||||
? $avatar->displayUrl()
|
||||
: Avatar::defaultImage(AVATAR_PROFILE_SIZE);
|
||||
|
||||
switch($this->format) {
|
||||
case 'xml':
|
||||
$this->showXmlTimeline($this->notices);
|
||||
break;
|
||||
case 'rss':
|
||||
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
|
||||
$link = common_local_url(
|
||||
'all',
|
||||
array('nickname' => $this->user->nickname)
|
||||
);
|
||||
$this->showRssTimeline(
|
||||
$this->notices,
|
||||
$title,
|
||||
$link,
|
||||
$subtitle,
|
||||
null,
|
||||
$logo
|
||||
);
|
||||
break;
|
||||
case 'atom':
|
||||
|
||||
$target_id = $this->arg('id');
|
||||
header('Content-Type: application/atom+xml; charset=utf-8');
|
||||
|
||||
if (isset($target_id)) {
|
||||
$selfuri = common_root_url() .
|
||||
'api/statuses/home_timeline/' .
|
||||
$target_id . '.atom';
|
||||
} else {
|
||||
$selfuri = common_root_url() .
|
||||
'api/statuses/home_timeline.atom';
|
||||
$atom = new AtomNoticeFeed();
|
||||
|
||||
$atom->setId($id);
|
||||
$atom->setTitle($title);
|
||||
$atom->setSubtitle($subtitle);
|
||||
$atom->setLogo($logo);
|
||||
$atom->setUpdated('now');
|
||||
|
||||
$atom->addLink(
|
||||
common_local_url(
|
||||
'all',
|
||||
array('nickname' => $this->user->nickname)
|
||||
)
|
||||
);
|
||||
|
||||
$id = $this->arg('id');
|
||||
$aargs = array('format' => 'atom');
|
||||
if (!empty($id)) {
|
||||
$aargs['id'] = $id;
|
||||
}
|
||||
|
||||
$this->showAtomTimeline(
|
||||
$this->notices, $title, $id, $link,
|
||||
$subtitle, null, $selfuri, $logo
|
||||
$atom->addLink(
|
||||
$this->getSelfUri('ApiTimelineHome', $aargs),
|
||||
array('rel' => 'self', 'type' => 'application/atom+xml')
|
||||
);
|
||||
|
||||
$atom->addEntryFromNotices($this->notices);
|
||||
$this->raw($atom->getString());
|
||||
|
||||
break;
|
||||
case 'json':
|
||||
$this->showJsonTimeline($this->notices);
|
||||
|
@ -137,12 +137,36 @@ class ApiTimelineMentionsAction extends ApiBareAuthAction
|
||||
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
|
||||
break;
|
||||
case 'atom':
|
||||
$selfuri = common_root_url() .
|
||||
ltrim($_SERVER['QUERY_STRING'], 'p=');
|
||||
$this->showAtomTimeline(
|
||||
$this->notices, $title, $id, $link, $subtitle,
|
||||
null, $selfuri, $logo
|
||||
|
||||
$atom = new AtomNoticeFeed();
|
||||
|
||||
$atom->setId($id);
|
||||
$atom->setTitle($title);
|
||||
$atom->setSubtitle($subtitle);
|
||||
$atom->setLogo($logo);
|
||||
$atom->setUpdated('now');
|
||||
|
||||
$atom->addLink(
|
||||
common_local_url(
|
||||
'replies',
|
||||
array('nickname' => $this->user->nickname)
|
||||
)
|
||||
);
|
||||
|
||||
$id = $this->arg('id');
|
||||
$aargs = array('format' => 'atom');
|
||||
if (!empty($id)) {
|
||||
$aargs['id'] = $id;
|
||||
}
|
||||
|
||||
$atom->addLink(
|
||||
$this->getSelfUri('ApiTimelineMentions', $aargs),
|
||||
array('rel' => 'self', 'type' => 'application/atom+xml')
|
||||
);
|
||||
|
||||
$atom->addEntryFromNotices($this->notices);
|
||||
$this->raw($atom->getString());
|
||||
|
||||
break;
|
||||
case 'json':
|
||||
$this->showJsonTimeline($this->notices);
|
||||
|
@ -75,6 +75,10 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
|
||||
|
||||
$this->notices = $this->getNotices();
|
||||
|
||||
if ($this->since) {
|
||||
throw new ServerException("since parameter is disabled for performance; use since_id", 403);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -118,11 +122,28 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
|
||||
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo);
|
||||
break;
|
||||
case 'atom':
|
||||
$selfuri = common_root_url() . 'api/statuses/public_timeline.atom';
|
||||
$this->showAtomTimeline(
|
||||
$this->notices, $title, $id, $link,
|
||||
$subtitle, null, $selfuri, $sitelogo
|
||||
|
||||
$atom = new AtomNoticeFeed();
|
||||
|
||||
$atom->setId($id);
|
||||
$atom->setTitle($title);
|
||||
$atom->setSubtitle($subtitle);
|
||||
$atom->setLogo($sitelogo);
|
||||
$atom->setUpdated('now');
|
||||
|
||||
$atom->addLink(common_local_url('public'));
|
||||
|
||||
$atom->addLink(
|
||||
$this->getSelfUri(
|
||||
'ApiTimelinePublic', array('format' => 'atom')
|
||||
),
|
||||
array('rel' => 'self', 'type' => 'application/atom+xml')
|
||||
);
|
||||
|
||||
$atom->addEntryFromNotices($this->notices);
|
||||
|
||||
$this->raw($atom->getString());
|
||||
|
||||
break;
|
||||
case 'json':
|
||||
$this->showJsonTimeline($this->notices);
|
||||
@ -145,7 +166,7 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
|
||||
|
||||
$notice = Notice::publicStream(
|
||||
($this->page - 1) * $this->count, $this->count, $this->since_id,
|
||||
$this->max_id, $this->since
|
||||
$this->max_id
|
||||
);
|
||||
|
||||
while ($notice->fetch()) {
|
||||
|
@ -99,6 +99,8 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction
|
||||
|
||||
$strm = $this->auth_user->repeatsOfMe($offset, $limit, $this->since_id, $this->max_id);
|
||||
|
||||
common_debug(var_export($strm, true));
|
||||
|
||||
switch ($this->format) {
|
||||
case 'xml':
|
||||
$this->showXmlTimeline($strm);
|
||||
@ -112,10 +114,38 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction
|
||||
$title = sprintf(_("Repeats of %s"), $this->auth_user->nickname);
|
||||
$taguribase = common_config('integration', 'taguri');
|
||||
$id = "tag:$taguribase:RepeatsOfMe:" . $this->auth_user->id;
|
||||
$link = common_local_url('showstream',
|
||||
array('nickname' => $this->auth_user->nickname));
|
||||
|
||||
$this->showAtomTimeline($strm, $title, $id, $link);
|
||||
header('Content-Type: application/atom+xml; charset=utf-8');
|
||||
|
||||
$atom = new AtomNoticeFeed();
|
||||
|
||||
$atom->setId($id);
|
||||
$atom->setTitle($title);
|
||||
$atom->setSubtitle($subtitle);
|
||||
$atom->setUpdated('now');
|
||||
|
||||
$atom->addLink(
|
||||
common_local_url(
|
||||
'showstream',
|
||||
array('nickname' => $this->auth_user->nickname)
|
||||
)
|
||||
);
|
||||
|
||||
$id = $this->arg('id');
|
||||
$aargs = array('format' => 'atom');
|
||||
if (!empty($id)) {
|
||||
$aargs['id'] = $id;
|
||||
}
|
||||
|
||||
$atom->addLink(
|
||||
$this->getSelfUri('ApiTimelineRetweetsOfMe', $aargs),
|
||||
array('rel' => 'self', 'type' => 'application/atom+xml')
|
||||
);
|
||||
|
||||
$atom->addEntryFromNotices($strm);
|
||||
|
||||
$this->raw($atom->getString());
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -100,10 +100,6 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction
|
||||
$sitename = common_config('site', 'name');
|
||||
$sitelogo = (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png');
|
||||
$title = sprintf(_("Notices tagged with %s"), $this->tag);
|
||||
$link = common_local_url(
|
||||
'tag',
|
||||
array('tag' => $this->tag)
|
||||
);
|
||||
$subtitle = sprintf(
|
||||
_('Updates tagged with %1$s on %2$s!'),
|
||||
$this->tag,
|
||||
@ -117,22 +113,51 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction
|
||||
$this->showXmlTimeline($this->notices);
|
||||
break;
|
||||
case 'rss':
|
||||
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo);
|
||||
break;
|
||||
case 'atom':
|
||||
$selfuri = common_root_url() .
|
||||
'api/statusnet/tags/timeline/' .
|
||||
$this->tag . '.atom';
|
||||
$this->showAtomTimeline(
|
||||
$link = common_local_url(
|
||||
'tag',
|
||||
array('tag' => $this->tag)
|
||||
);
|
||||
$this->showRssTimeline(
|
||||
$this->notices,
|
||||
$title,
|
||||
$id,
|
||||
$link,
|
||||
$subtitle,
|
||||
null,
|
||||
$selfuri,
|
||||
$sitelogo
|
||||
);
|
||||
break;
|
||||
case 'atom':
|
||||
|
||||
header('Content-Type: application/atom+xml; charset=utf-8');
|
||||
|
||||
$atom = new AtomNoticeFeed();
|
||||
|
||||
$atom->setId($id);
|
||||
$atom->setTitle($title);
|
||||
$atom->setSubtitle($subtitle);
|
||||
$atom->setLogo($logo);
|
||||
$atom->setUpdated('now');
|
||||
|
||||
$atom->addLink(
|
||||
common_local_url(
|
||||
'tag',
|
||||
array('tag' => $this->tag)
|
||||
)
|
||||
);
|
||||
|
||||
$aargs = array('format' => 'atom');
|
||||
if (!empty($this->tag)) {
|
||||
$aargs['tag'] = $this->tag;
|
||||
}
|
||||
|
||||
$atom->addLink(
|
||||
$this->getSelfUri('ApiTimelineTag', $aargs),
|
||||
array('rel' => 'self', 'type' => 'application/atom+xml')
|
||||
);
|
||||
|
||||
$atom->addEntryFromNotices($this->notices);
|
||||
$this->raw($atom->getString());
|
||||
|
||||
break;
|
||||
case 'json':
|
||||
$this->showJsonTimeline($this->notices);
|
||||
|
@ -145,19 +145,59 @@ class ApiTimelineUserAction extends ApiBareAuthAction
|
||||
);
|
||||
break;
|
||||
case 'atom':
|
||||
|
||||
header('Content-Type: application/atom+xml; charset=utf-8');
|
||||
|
||||
// If this was called using an integer ID, i.e.: using the canonical
|
||||
// URL for this user's feed, then pass the User object into the feed,
|
||||
// so the OStatus plugin, and possibly other plugins, can access it.
|
||||
// Feels sorta hacky. -- Z
|
||||
|
||||
$atom = null;
|
||||
$id = $this->arg('id');
|
||||
if ($id) {
|
||||
$selfuri = common_root_url() .
|
||||
'api/statuses/user_timeline/' .
|
||||
rawurlencode($id) . '.atom';
|
||||
|
||||
if (strval(intval($id)) === strval($id)) {
|
||||
$atom = new AtomUserNoticeFeed($this->user);
|
||||
} else {
|
||||
$selfuri = common_root_url() .
|
||||
'api/statuses/user_timeline.atom';
|
||||
$atom = new AtomUserNoticeFeed();
|
||||
}
|
||||
$this->showAtomTimeline(
|
||||
$this->notices, $title, $id, $link,
|
||||
$subtitle, $suplink, $selfuri, $logo
|
||||
|
||||
$atom->setId($id);
|
||||
$atom->setTitle($title);
|
||||
$atom->setSubtitle($subtitle);
|
||||
$atom->setLogo($logo);
|
||||
$atom->setUpdated('now');
|
||||
|
||||
$atom->addLink(
|
||||
common_local_url(
|
||||
'showstream',
|
||||
array('nickname' => $this->user->nickname)
|
||||
)
|
||||
);
|
||||
|
||||
$id = $this->arg('id');
|
||||
$aargs = array('format' => 'atom');
|
||||
if (!empty($id)) {
|
||||
$aargs['id'] = $id;
|
||||
}
|
||||
|
||||
$atom->addLink(
|
||||
$this->getSelfUri('ApiTimelineUser', $aargs),
|
||||
array('rel' => 'self', 'type' => 'application/atom+xml')
|
||||
);
|
||||
|
||||
$atom->addLink(
|
||||
$suplink,
|
||||
array(
|
||||
'rel' => 'http://api.friendfeed.com/2008/03#sup',
|
||||
'type' => 'application/json'
|
||||
)
|
||||
);
|
||||
|
||||
$atom->addEntryFromNotices($this->notices);
|
||||
|
||||
$this->raw($atom->getString());
|
||||
|
||||
break;
|
||||
case 'json':
|
||||
$this->showJsonTimeline($this->notices);
|
||||
|
@ -82,9 +82,20 @@ class Avatar extends Memcached_DataObject
|
||||
$server = common_config('site', 'server');
|
||||
}
|
||||
|
||||
// XXX: protocol
|
||||
$ssl = common_config('avatar', 'ssl');
|
||||
|
||||
return 'http://'.$server.$path.$filename;
|
||||
if (is_null($ssl)) { // null -> guess
|
||||
if (common_config('site', 'ssl') == 'always' &&
|
||||
!common_config('avatar', 'server')) {
|
||||
$ssl = true;
|
||||
} else {
|
||||
$ssl = false;
|
||||
}
|
||||
}
|
||||
|
||||
$protocol = ($ssl) ? 'https' : 'http';
|
||||
|
||||
return $protocol.'://'.$server.$path.$filename;
|
||||
}
|
||||
|
||||
function displayUrl()
|
||||
|
@ -155,9 +155,20 @@ class Design extends Memcached_DataObject
|
||||
$server = common_config('site', 'server');
|
||||
}
|
||||
|
||||
// XXX: protocol
|
||||
$ssl = common_config('background', 'ssl');
|
||||
|
||||
return 'http://'.$server.$path.$filename;
|
||||
if (is_null($ssl)) { // null -> guess
|
||||
if (common_config('site', 'ssl') == 'always' &&
|
||||
!common_config('background', 'server')) {
|
||||
$ssl = true;
|
||||
} else {
|
||||
$ssl = false;
|
||||
}
|
||||
}
|
||||
|
||||
$protocol = ($ssl) ? 'https' : 'http';
|
||||
|
||||
return $protocol.'://'.$server.$path.$filename;
|
||||
}
|
||||
|
||||
function setDisposition($on, $off, $tile)
|
||||
|
@ -228,9 +228,20 @@ class File extends Memcached_DataObject
|
||||
$server = common_config('site', 'server');
|
||||
}
|
||||
|
||||
// XXX: protocol
|
||||
$ssl = common_config('attachments', 'ssl');
|
||||
|
||||
return 'http://'.$server.$path.$filename;
|
||||
if (is_null($ssl)) { // null -> guess
|
||||
if (common_config('site', 'ssl') == 'always' &&
|
||||
!common_config('attachments', 'server')) {
|
||||
$ssl = true;
|
||||
} else {
|
||||
$ssl = false;
|
||||
}
|
||||
}
|
||||
|
||||
$protocol = ($ssl) ? 'https' : 'http';
|
||||
|
||||
return $protocol.'://'.$server.$path.$filename;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -957,7 +957,10 @@ class Notice extends Memcached_DataObject
|
||||
|
||||
if ($namespace) {
|
||||
$attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
|
||||
'xmlns:thr' => 'http://purl.org/syndication/thread/1.0');
|
||||
'xmlns:thr' => 'http://purl.org/syndication/thread/1.0',
|
||||
'xmlns:georss' => 'http://www.georss.org/georss',
|
||||
'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/',
|
||||
'xmlns:ostatus' => 'http://ostatus.org/schema/1.0');
|
||||
} else {
|
||||
$attrs = array();
|
||||
}
|
||||
@ -983,11 +986,6 @@ class Notice extends Memcached_DataObject
|
||||
$xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE));
|
||||
}
|
||||
|
||||
$xs->elementStart('author');
|
||||
$xs->element('name', null, $profile->nickname);
|
||||
$xs->element('uri', null, $profile->profileurl);
|
||||
$xs->elementEnd('author');
|
||||
|
||||
if ($source) {
|
||||
$xs->elementEnd('source');
|
||||
}
|
||||
@ -995,6 +993,9 @@ class Notice extends Memcached_DataObject
|
||||
$xs->element('title', null, $this->content);
|
||||
$xs->element('summary', null, $this->content);
|
||||
|
||||
$xs->raw($profile->asAtomAuthor());
|
||||
$xs->raw($profile->asActivityActor());
|
||||
|
||||
$xs->element('link', array('rel' => 'alternate',
|
||||
'href' => $this->bestUrl()));
|
||||
|
||||
@ -1014,6 +1015,43 @@ class Notice extends Memcached_DataObject
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->conversation)
|
||||
&& $this->conversation != $this->id) {
|
||||
$xs->element(
|
||||
'link', array(
|
||||
'rel' => 'ostatus:conversation',
|
||||
'href' => common_local_url(
|
||||
'conversation',
|
||||
array('id' => $this->conversation)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$reply_ids = $this->getReplies();
|
||||
|
||||
foreach ($reply_ids as $id) {
|
||||
$profile = Profile::staticGet('id', $id);
|
||||
if (!empty($profile)) {
|
||||
$xs->element(
|
||||
'link', array(
|
||||
'rel' => 'ostatus:attention',
|
||||
'href' => $profile->getAcctUri()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->repeat_of)) {
|
||||
$repeat = Notice::staticGet('id', $this->repeat_of);
|
||||
if (!empty($repeat)) {
|
||||
$xs->element(
|
||||
'ostatus:forward',
|
||||
array('ref' => $repeat->uri, 'href' => $repeat->bestUrl())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$xs->element('content', array('type' => 'html'), $this->rendered);
|
||||
|
||||
$tag = new Notice_tag();
|
||||
@ -1041,9 +1079,7 @@ class Notice extends Memcached_DataObject
|
||||
}
|
||||
|
||||
if (!empty($this->lat) && !empty($this->lon)) {
|
||||
$xs->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
|
||||
$xs->element('georss:point', null, $this->lat . ' ' . $this->lon);
|
||||
$xs->elementEnd('geo');
|
||||
}
|
||||
|
||||
$xs->elementEnd('entry');
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -355,6 +355,39 @@ class User_group extends Memcached_DataObject
|
||||
return $xs->getString();
|
||||
}
|
||||
|
||||
function asAtomAuthor()
|
||||
{
|
||||
$xs = new XMLStringer(true);
|
||||
|
||||
$xs->elementStart('author');
|
||||
$xs->element('name', null, $this->nickname);
|
||||
$xs->element('uri', null, $this->permalink());
|
||||
$xs->elementEnd('author');
|
||||
|
||||
return $xs->getString();
|
||||
}
|
||||
|
||||
function asActivitySubject()
|
||||
{
|
||||
$xs = new XMLStringer(true);
|
||||
|
||||
$xs->elementStart('activity:subject');
|
||||
$xs->element('activity:object', null, 'http://activitystrea.ms/schema/1.0/group');
|
||||
$xs->element('id', null, $this->permalink());
|
||||
$xs->element('title', null, $this->getBestName());
|
||||
$xs->element(
|
||||
'link', array(
|
||||
'rel' => 'avatar',
|
||||
'href' => empty($this->homepage_logo)
|
||||
? User_group::defaultLogo(AVATAR_PROFILE_SIZE)
|
||||
: $this->homepage_logo
|
||||
)
|
||||
);
|
||||
$xs->elementEnd('activity:subject');
|
||||
|
||||
return $xs->getString();
|
||||
}
|
||||
|
||||
static function register($fields) {
|
||||
|
||||
// MAGICALLY put fields into current scope
|
||||
|
@ -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
|
||||
|
@ -405,6 +405,7 @@ class Action extends HTMLOutputter // lawsuit
|
||||
'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png'),
|
||||
'alt' => common_config('site', 'name')));
|
||||
}
|
||||
$this->text(' ');
|
||||
$this->element('span', array('class' => 'fn org'), common_config('site', 'name'));
|
||||
$this->elementEnd('a');
|
||||
Event::handle('EndAddressData', array($this));
|
||||
@ -822,12 +823,14 @@ class Action extends HTMLOutputter // lawsuit
|
||||
'alt' => common_config('license', 'title'),
|
||||
'width' => '80',
|
||||
'height' => '15'));
|
||||
$this->text(' ');
|
||||
//TODO: This is dirty: i18n
|
||||
$this->text(_('All '.common_config('site', 'name').' content and data are available under the '));
|
||||
$this->element('a', array('class' => 'license',
|
||||
'rel' => 'external license',
|
||||
'href' => common_config('license', 'url')),
|
||||
common_config('license', 'title'));
|
||||
$this->text(' ');
|
||||
$this->text(_('license.'));
|
||||
$this->elementEnd('p');
|
||||
break;
|
||||
|
21
lib/api.php
21
lib/api.php
@ -1103,7 +1103,7 @@ class ApiAction extends Action
|
||||
}
|
||||
}
|
||||
|
||||
function serverError($msg, $code = 500, $content_type = 'json')
|
||||
function serverError($msg, $code = 500, $content_type = 'xml')
|
||||
{
|
||||
$action = $this->trimmed('action');
|
||||
|
||||
@ -1154,7 +1154,6 @@ class ApiAction extends Action
|
||||
$this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
|
||||
'xml:lang' => 'en-US',
|
||||
'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
|
||||
Event::handle('StartApiAtom', array($this));
|
||||
}
|
||||
|
||||
function endTwitterAtom()
|
||||
@ -1321,4 +1320,22 @@ class ApiAction extends Action
|
||||
}
|
||||
}
|
||||
|
||||
function getSelfUri($action, $aargs)
|
||||
{
|
||||
parse_str($_SERVER['QUERY_STRING'], $params);
|
||||
$pstring = '';
|
||||
if (!empty($params)) {
|
||||
unset($params['p']);
|
||||
$pstring = http_build_query($params);
|
||||
}
|
||||
|
||||
$uri = common_local_url($action, $aargs);
|
||||
|
||||
if (!empty($pstring)) {
|
||||
$uri .= '?' . $pstring;
|
||||
}
|
||||
|
||||
return $uri;
|
||||
}
|
||||
|
||||
}
|
||||
|
106
lib/atom10entry.php
Normal file
106
lib/atom10entry.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet, the distributed open-source microblogging tool
|
||||
*
|
||||
* Class for building / manipulating an Atom entry in memory
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* LICENCE: This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category Feed
|
||||
* @package StatusNet
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @copyright 2010 StatusNet, Inc.
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link http://status.net/
|
||||
*/
|
||||
|
||||
if (!defined('STATUSNET')
|
||||
{
|
||||
exit(1);
|
||||
}
|
||||
|
||||
class Atom10EntryException extends Exception
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for manipulating an Atom entry in memory. Get the entry as an XML
|
||||
* string with Atom10Entry::getString().
|
||||
*
|
||||
* @category Feed
|
||||
* @package StatusNet
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link http://status.net/
|
||||
*/
|
||||
class Atom10Entry extends XMLStringer
|
||||
{
|
||||
private $namespaces;
|
||||
private $categories;
|
||||
private $content;
|
||||
private $contributors;
|
||||
private $id;
|
||||
private $links;
|
||||
private $published;
|
||||
private $rights;
|
||||
private $source;
|
||||
private $summary;
|
||||
private $title;
|
||||
|
||||
function __construct($indent = true) {
|
||||
parent::__construct($indent);
|
||||
$this->namespaces = array();
|
||||
}
|
||||
|
||||
function addNamespace($namespace, $uri)
|
||||
{
|
||||
$ns = array($namespace => $uri);
|
||||
$this->namespaces = array_merge($this->namespaces, $ns);
|
||||
}
|
||||
|
||||
function initEntry()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
function endEntry()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that all required elements have been set, etc.
|
||||
* Throws an Atom10EntryException if something's missing.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function validate
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
function getString()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$this->initEntry();
|
||||
$this->renderEntries();
|
||||
$this->endEntry();
|
||||
|
||||
return $this->xw->outputMemory();
|
||||
}
|
||||
|
||||
}
|
298
lib/atom10feed.php
Normal file
298
lib/atom10feed.php
Normal file
@ -0,0 +1,298 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet, the distributed open-source microblogging tool
|
||||
*
|
||||
* Class for building an Atom feed in memory
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* LICENCE: This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category Feed
|
||||
* @package StatusNet
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @copyright 2010 StatusNet, Inc.
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link http://status.net/
|
||||
*/
|
||||
|
||||
if (!defined('STATUSNET'))
|
||||
{
|
||||
exit(1);
|
||||
}
|
||||
|
||||
class Atom10FeedException extends Exception
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for building an Atom feed in memory. Get the finished doc
|
||||
* as a string with Atom10Feed::getString().
|
||||
*
|
||||
* @category Feed
|
||||
* @package StatusNet
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link http://status.net/
|
||||
*/
|
||||
class Atom10Feed extends XMLStringer
|
||||
{
|
||||
public $xw;
|
||||
private $namespaces;
|
||||
private $authors;
|
||||
private $subject;
|
||||
private $categories;
|
||||
private $contributors;
|
||||
private $generator;
|
||||
private $icon;
|
||||
private $links;
|
||||
private $logo;
|
||||
private $rights;
|
||||
private $subtitle;
|
||||
private $title;
|
||||
private $published;
|
||||
private $updated;
|
||||
private $entries;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param boolean $indent flag to turn indenting on or off
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function __construct($indent = true) {
|
||||
parent::__construct($indent);
|
||||
$this->namespaces = array();
|
||||
$this->authors = array();
|
||||
$this->links = array();
|
||||
$this->entries = array();
|
||||
$this->addNamespace('xmlns', 'http://www.w3.org/2005/Atom');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add another namespace to the feed
|
||||
*
|
||||
* @param string $namespace the namespace
|
||||
* @param string $uri namspace uri
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function addNamespace($namespace, $uri)
|
||||
{
|
||||
$ns = array($namespace => $uri);
|
||||
$this->namespaces = array_merge($this->namespaces, $ns);
|
||||
}
|
||||
|
||||
function addAuthor($name, $uri = null, $email = null)
|
||||
{
|
||||
$xs = new XMLStringer(true);
|
||||
|
||||
$xs->elementStart('author');
|
||||
|
||||
if (!empty($name)) {
|
||||
$xs->element('name', null, $name);
|
||||
} else {
|
||||
throw new Atom10FeedException(
|
||||
'author element must contain a name element.'
|
||||
);
|
||||
}
|
||||
|
||||
if (!is_null($uri)) {
|
||||
$xs->element('uri', null, $uri);
|
||||
}
|
||||
|
||||
if (!is_null(email)) {
|
||||
$xs->element('email', null, $email);
|
||||
}
|
||||
|
||||
$xs->elementEnd('author');
|
||||
|
||||
array_push($this->authors, $xs->getString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an Author to the feed via raw XML string
|
||||
*
|
||||
* @param string $xmlAuthor An XML string representation author
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function addAuthorRaw($xmlAuthor)
|
||||
{
|
||||
array_push($this->authors, $xmlAuthor);
|
||||
}
|
||||
|
||||
function renderAuthors()
|
||||
{
|
||||
foreach ($this->authors as $author) {
|
||||
$this->raw($author);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a activity feed subject via raw XML string
|
||||
*
|
||||
* @param string $xmlSubject An XML string representation of the subject
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function setActivitySubject($xmlSubject)
|
||||
{
|
||||
$this->subject = $xmlSubject;
|
||||
}
|
||||
|
||||
function getNamespaces()
|
||||
{
|
||||
return $this->namespaces;
|
||||
}
|
||||
|
||||
function initFeed()
|
||||
{
|
||||
$this->xw->startDocument('1.0', 'UTF-8');
|
||||
$commonAttrs = array('xml:lang' => 'en-US');
|
||||
$commonAttrs = array_merge($commonAttrs, $this->namespaces);
|
||||
$this->elementStart('feed', $commonAttrs);
|
||||
|
||||
$this->element('id', null, $this->id);
|
||||
$this->element('title', null, $this->title);
|
||||
$this->element('subtitle', null, $this->subtitle);
|
||||
|
||||
if (!empty($this->logo)) {
|
||||
$this->element('logo', null, $this->logo);
|
||||
}
|
||||
|
||||
$this->element('updated', null, $this->updated);
|
||||
|
||||
$this->renderAuthors();
|
||||
|
||||
$this->renderLinks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that all required elements have been set, etc.
|
||||
* Throws an Atom10FeedException if something's missing.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function validate()
|
||||
{
|
||||
}
|
||||
|
||||
function renderLinks()
|
||||
{
|
||||
foreach ($this->links as $attrs)
|
||||
{
|
||||
$this->element('link', $attrs, null);
|
||||
}
|
||||
}
|
||||
|
||||
function addEntryRaw($xmlEntry)
|
||||
{
|
||||
array_push($this->entries, $xmlEntry);
|
||||
}
|
||||
|
||||
function addEntry($entry)
|
||||
{
|
||||
array_push($this->entries, $entry->getString());
|
||||
}
|
||||
|
||||
function renderEntries()
|
||||
{
|
||||
foreach ($this->entries as $entry) {
|
||||
$this->raw($entry);
|
||||
}
|
||||
}
|
||||
|
||||
function endFeed()
|
||||
{
|
||||
$this->elementEnd('feed');
|
||||
$this->xw->endDocument();
|
||||
}
|
||||
|
||||
function getString()
|
||||
{
|
||||
if (Event::handle('StartApiAtom', array($this))) {
|
||||
|
||||
$this->validate();
|
||||
$this->initFeed();
|
||||
|
||||
if (!empty($this->subject)) {
|
||||
$this->raw($this->subject);
|
||||
}
|
||||
|
||||
$this->renderEntries();
|
||||
$this->endFeed();
|
||||
|
||||
Event::handle('EndApiAtom', array($this));
|
||||
}
|
||||
|
||||
return $this->xw->outputMemory();
|
||||
}
|
||||
|
||||
function setId($id)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
function setTitle($title)
|
||||
{
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
function setSubtitle($subtitle)
|
||||
{
|
||||
$this->subtitle = $subtitle;
|
||||
}
|
||||
|
||||
function setLogo($logo)
|
||||
{
|
||||
$this->logo = $logo;
|
||||
}
|
||||
|
||||
function setUpdated($dt)
|
||||
{
|
||||
$this->updated = common_date_iso8601($dt);
|
||||
}
|
||||
|
||||
function setPublished($dt)
|
||||
{
|
||||
$this->published = common_date_iso8601($dt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a link element into the Atom document
|
||||
*
|
||||
* Assumes you want rel="alternate" and type="text/html" unless
|
||||
* you send in $otherAttrs.
|
||||
*
|
||||
* @param string $uri the uri the href needs to point to
|
||||
* @param array $otherAttrs other attributes to stick in
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function addLink($uri, $otherAttrs = null) {
|
||||
$attrs = array('href' => $uri);
|
||||
|
||||
if (is_null($otherAttrs)) {
|
||||
$attrs['rel'] = 'alternate';
|
||||
$attrs['type'] = 'text/html';
|
||||
} else {
|
||||
$attrs = array_merge($attrs, $otherAttrs);
|
||||
}
|
||||
|
||||
array_push($this->links, $attrs);
|
||||
}
|
||||
|
||||
}
|
67
lib/atomgroupnoticefeed.php
Normal file
67
lib/atomgroupnoticefeed.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet, the distributed open-source microblogging tool
|
||||
*
|
||||
* Class for building an in-memory Atom feed for a particular group's
|
||||
* timeline.
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* LICENCE: This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category Feed
|
||||
* @package StatusNet
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @copyright 2010 StatusNet, Inc.
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link http://status.net/
|
||||
*/
|
||||
|
||||
if (!defined('STATUSNET'))
|
||||
{
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for group notice feeds. May contains a reference to the group.
|
||||
*
|
||||
* @category Feed
|
||||
* @package StatusNet
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link http://status.net/
|
||||
*/
|
||||
class AtomGroupNoticeFeed extends AtomNoticeFeed
|
||||
{
|
||||
private $group;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Group $group the group for the feed (optional)
|
||||
* @param boolean $indent flag to turn indenting on or off
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function __construct($group = null, $indent = true) {
|
||||
parent::__construct($indent);
|
||||
$this->group = $group;
|
||||
}
|
||||
|
||||
function getGroup()
|
||||
{
|
||||
return $this->group;
|
||||
}
|
||||
|
||||
}
|
105
lib/atomnoticefeed.php
Normal file
105
lib/atomnoticefeed.php
Normal file
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet, the distributed open-source microblogging tool
|
||||
*
|
||||
* Class for building an Atom feed from a collection of notices
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* LICENCE: This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category Feed
|
||||
* @package StatusNet
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @copyright 2010 StatusNet, Inc.
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link http://status.net/
|
||||
*/
|
||||
|
||||
if (!defined('STATUSNET'))
|
||||
{
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for creating a feed that represents a collection of notices. Builds the
|
||||
* feed in memory. Get the feed as a string with AtomNoticeFeed::getString().
|
||||
*
|
||||
* @category Feed
|
||||
* @package StatusNet
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link http://status.net/
|
||||
*/
|
||||
class AtomNoticeFeed extends Atom10Feed
|
||||
{
|
||||
function __construct($indent = true) {
|
||||
parent::__construct($indent);
|
||||
|
||||
// Feeds containing notice info use these namespaces
|
||||
|
||||
$this->addNamespace(
|
||||
'xmlns:thr',
|
||||
'http://purl.org/syndication/thread/1.0'
|
||||
);
|
||||
|
||||
$this->addNamespace(
|
||||
'xmlns:georss',
|
||||
'http://www.georss.org/georss'
|
||||
);
|
||||
|
||||
$this->addNamespace(
|
||||
'xmlns:activity',
|
||||
'http://activitystrea.ms/spec/1.0/'
|
||||
);
|
||||
|
||||
// XXX: What should the uri be?
|
||||
$this->addNamespace(
|
||||
'xmlns:ostatus',
|
||||
'http://ostatus.org/schema/1.0'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add more than one Notice to the feed
|
||||
*
|
||||
* @param mixed $notices an array of Notice objects or handle
|
||||
*
|
||||
*/
|
||||
function addEntryFromNotices($notices)
|
||||
{
|
||||
if (is_array($notices)) {
|
||||
foreach ($notices as $notice) {
|
||||
$this->addEntryFromNotice($notice);
|
||||
}
|
||||
} else {
|
||||
while ($notices->fetch()) {
|
||||
$this->addEntryFromNotice($notices);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single Notice to the feed
|
||||
*
|
||||
* @param Notice $notice a Notice to add
|
||||
*/
|
||||
function addEntryFromNotice($notice)
|
||||
{
|
||||
$this->addEntryRaw($notice->asAtomEntry());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
66
lib/atomusernoticefeed.php
Normal file
66
lib/atomusernoticefeed.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet, the distributed open-source microblogging tool
|
||||
*
|
||||
* Class for building an in-memory Atom feed for a particular user's
|
||||
* timeline.
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* LICENCE: This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category Feed
|
||||
* @package StatusNet
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @copyright 2010 StatusNet, Inc.
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link http://status.net/
|
||||
*/
|
||||
|
||||
if (!defined('STATUSNET'))
|
||||
{
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for user notice feeds. May contain a reference to the user.
|
||||
*
|
||||
* @category Feed
|
||||
* @package StatusNet
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link http://status.net/
|
||||
*/
|
||||
class AtomUserNoticeFeed extends AtomNoticeFeed
|
||||
{
|
||||
private $user;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param User $user the user for the feed (optional)
|
||||
* @param boolean $indent flag to turn indenting on or off
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function __construct($user = null, $indent = true) {
|
||||
parent::__construct($indent);
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
function getUser()
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
}
|
@ -111,11 +111,13 @@ $default =
|
||||
'avatar' =>
|
||||
array('server' => null,
|
||||
'dir' => INSTALLDIR . '/avatar/',
|
||||
'path' => $_path . '/avatar/'),
|
||||
'path' => $_path . '/avatar/',
|
||||
'ssl' => null),
|
||||
'background' =>
|
||||
array('server' => null,
|
||||
'dir' => INSTALLDIR . '/background/',
|
||||
'path' => $_path . '/background/'),
|
||||
'path' => $_path . '/background/',
|
||||
'ssl' => null),
|
||||
'public' =>
|
||||
array('localonly' => true,
|
||||
'blacklist' => array(),
|
||||
@ -123,10 +125,12 @@ $default =
|
||||
'theme' =>
|
||||
array('server' => null,
|
||||
'dir' => null,
|
||||
'path'=> null),
|
||||
'path'=> null,
|
||||
'ssl' => null),
|
||||
'javascript' =>
|
||||
array('server' => null,
|
||||
'path'=> null),
|
||||
'path'=> null,
|
||||
'ssl' => null),
|
||||
'throttle' =>
|
||||
array('enabled' => false, // whether to throttle edits; false by default
|
||||
'count' => 20, // number of allowed messages in timespan
|
||||
@ -184,6 +188,7 @@ $default =
|
||||
array('server' => null,
|
||||
'dir' => INSTALLDIR . '/file/',
|
||||
'path' => $_path . '/file/',
|
||||
'ssl' => null,
|
||||
'supported' => array('image/png',
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
|
@ -105,6 +105,7 @@ class GroupList extends Widget
|
||||
'alt' =>
|
||||
($this->group->fullname) ? $this->group->fullname :
|
||||
$this->group->nickname));
|
||||
$this->out->text(' ');
|
||||
$hasFN = ($this->group->fullname) ? 'nickname' : 'fn org nickname';
|
||||
$this->out->elementStart('span', $hasFN);
|
||||
$this->out->raw($this->highlight($this->group->nickname));
|
||||
@ -112,16 +113,19 @@ class GroupList extends Widget
|
||||
$this->out->elementEnd('a');
|
||||
|
||||
if ($this->group->fullname) {
|
||||
$this->out->text(' ');
|
||||
$this->out->elementStart('span', 'fn org');
|
||||
$this->out->raw($this->highlight($this->group->fullname));
|
||||
$this->out->elementEnd('span');
|
||||
}
|
||||
if ($this->group->location) {
|
||||
$this->out->text(' ');
|
||||
$this->out->elementStart('span', 'label');
|
||||
$this->out->raw($this->highlight($this->group->location));
|
||||
$this->out->elementEnd('span');
|
||||
}
|
||||
if ($this->group->homepage) {
|
||||
$this->out->text(' ');
|
||||
$this->out->elementStart('a', array('href' => $this->group->homepage,
|
||||
'class' => 'url'));
|
||||
$this->out->raw($this->highlight($this->group->homepage));
|
||||
|
@ -85,9 +85,9 @@ class GroupSection extends Section
|
||||
'href' => $group->homeUrl(),
|
||||
'rel' => 'contact group',
|
||||
'class' => 'url'));
|
||||
$this->out->text(' ');
|
||||
$logo = ($group->stream_logo) ?
|
||||
$group->stream_logo : User_group::defaultLogo(AVATAR_STREAM_SIZE);
|
||||
|
||||
$this->out->element('img', array('src' => $logo,
|
||||
'width' => AVATAR_MINI_SIZE,
|
||||
'height' => AVATAR_MINI_SIZE,
|
||||
@ -95,6 +95,7 @@ class GroupSection extends Section
|
||||
'alt' => ($group->fullname) ?
|
||||
$group->fullname :
|
||||
$group->nickname));
|
||||
$this->out->text(' ');
|
||||
$this->out->element('span', 'fn org nickname', $group->nickname);
|
||||
$this->out->elementEnd('a');
|
||||
$this->out->elementEnd('span');
|
||||
|
@ -376,9 +376,20 @@ class HTMLOutputter extends XMLOutputter
|
||||
$server = common_config('site', 'server');
|
||||
}
|
||||
|
||||
// XXX: protocol
|
||||
$ssl = common_config('javascript', 'ssl');
|
||||
|
||||
$src = 'http://'.$server.$path.$src . '?version=' . STATUSNET_VERSION;
|
||||
if (is_null($ssl)) { // null -> guess
|
||||
if (common_config('site', 'ssl') == 'always' &&
|
||||
!common_config('javascript', 'server')) {
|
||||
$ssl = true;
|
||||
} else {
|
||||
$ssl = false;
|
||||
}
|
||||
}
|
||||
|
||||
$protocol = ($ssl) ? 'https' : 'http';
|
||||
|
||||
$src = $protocol.'://'.$server.$path.$src . '?version=' . STATUSNET_VERSION;
|
||||
}
|
||||
|
||||
$this->element('script', array('type' => $type,
|
||||
|
@ -294,6 +294,7 @@ class NoticeListItem extends Widget
|
||||
}
|
||||
$this->out->elementStart('a', $attrs);
|
||||
$this->showAvatar();
|
||||
$this->out->text(' ');
|
||||
$this->showNickname();
|
||||
$this->out->elementEnd('a');
|
||||
$this->out->elementEnd('span');
|
||||
@ -432,8 +433,10 @@ class NoticeListItem extends Widget
|
||||
|
||||
$url = $location->getUrl();
|
||||
|
||||
$this->out->text(' ');
|
||||
$this->out->elementStart('span', array('class' => 'location'));
|
||||
$this->out->text(_('at'));
|
||||
$this->out->text(' ');
|
||||
if (empty($url)) {
|
||||
$this->out->element('span', array('class' => 'geo',
|
||||
'title' => $latlon),
|
||||
@ -473,9 +476,11 @@ class NoticeListItem extends Widget
|
||||
function showNoticeSource()
|
||||
{
|
||||
if ($this->notice->source) {
|
||||
$this->out->text(' ');
|
||||
$this->out->elementStart('span', 'source');
|
||||
$this->out->text(_('from'));
|
||||
$source_name = _($this->notice->source);
|
||||
$this->out->text(' ');
|
||||
switch ($this->notice->source) {
|
||||
case 'web':
|
||||
case 'xmpp':
|
||||
@ -540,6 +545,7 @@ class NoticeListItem extends Widget
|
||||
}
|
||||
}
|
||||
if ($hasConversation){
|
||||
$this->out->text(' ');
|
||||
$convurl = common_local_url('conversation',
|
||||
array('id' => $this->notice->conversation));
|
||||
$this->out->element('a', array('href' => $convurl.'#notice-'.$this->notice->id,
|
||||
@ -591,12 +597,14 @@ class NoticeListItem extends Widget
|
||||
function showReplyLink()
|
||||
{
|
||||
if (common_logged_in()) {
|
||||
$this->out->text(' ');
|
||||
$reply_url = common_local_url('newnotice',
|
||||
array('replyto' => $this->profile->nickname, 'inreplyto' => $this->notice->id));
|
||||
$this->out->elementStart('a', array('href' => $reply_url,
|
||||
'class' => 'notice_reply',
|
||||
'title' => _('Reply to this notice')));
|
||||
$this->out->text(_('Reply'));
|
||||
$this->out->text(' ');
|
||||
$this->out->element('span', 'notice_id', $this->notice->id);
|
||||
$this->out->elementEnd('a');
|
||||
}
|
||||
@ -616,7 +624,7 @@ class NoticeListItem extends Widget
|
||||
|
||||
if (!empty($user) &&
|
||||
($todel->profile_id == $user->id || $user->hasRight(Right::DELETEOTHERSNOTICE))) {
|
||||
|
||||
$this->out->text(' ');
|
||||
$deleteurl = common_local_url('deletenotice',
|
||||
array('notice' => $todel->id));
|
||||
$this->out->element('a', array('href' => $deleteurl,
|
||||
@ -635,6 +643,7 @@ class NoticeListItem extends Widget
|
||||
{
|
||||
$user = common_current_user();
|
||||
if ($user && $user->id != $this->notice->profile_id) {
|
||||
$this->out->text(' ');
|
||||
$profile = $user->getProfile();
|
||||
if ($profile->hasRepeated($this->notice->id)) {
|
||||
$this->out->element('span', array('class' => 'repeated',
|
||||
|
@ -90,6 +90,7 @@ class NoticeSection extends Section
|
||||
'alt' => ($profile->fullname) ?
|
||||
$profile->fullname :
|
||||
$profile->nickname));
|
||||
$this->out->text(' ');
|
||||
$this->out->element('span', 'fn nickname', $profile->nickname);
|
||||
$this->out->elementEnd('a');
|
||||
$this->out->elementEnd('span');
|
||||
|
@ -191,6 +191,7 @@ class ProfileListItem extends Widget
|
||||
'alt' =>
|
||||
($this->profile->fullname) ? $this->profile->fullname :
|
||||
$this->profile->nickname));
|
||||
$this->out->text(' ');
|
||||
$hasFN = (!empty($this->profile->fullname)) ? 'nickname' : 'fn nickname';
|
||||
$this->out->elementStart('span', $hasFN);
|
||||
$this->out->raw($this->highlight($this->profile->nickname));
|
||||
@ -201,6 +202,7 @@ class ProfileListItem extends Widget
|
||||
function showFullName()
|
||||
{
|
||||
if (!empty($this->profile->fullname)) {
|
||||
$this->out->text(' ');
|
||||
$this->out->elementStart('span', 'fn');
|
||||
$this->out->raw($this->highlight($this->profile->fullname));
|
||||
$this->out->elementEnd('span');
|
||||
@ -210,6 +212,7 @@ class ProfileListItem extends Widget
|
||||
function showLocation()
|
||||
{
|
||||
if (!empty($this->profile->location)) {
|
||||
$this->out->text(' ');
|
||||
$this->out->elementStart('span', 'location');
|
||||
$this->out->raw($this->highlight($this->profile->location));
|
||||
$this->out->elementEnd('span');
|
||||
@ -219,6 +222,7 @@ class ProfileListItem extends Widget
|
||||
function showHomepage()
|
||||
{
|
||||
if (!empty($this->profile->homepage)) {
|
||||
$this->out->text(' ');
|
||||
$this->out->elementStart('a', array('href' => $this->profile->homepage,
|
||||
'class' => 'url'));
|
||||
$this->out->raw($this->highlight($this->profile->homepage));
|
||||
|
@ -85,6 +85,7 @@ class ProfileSection extends Section
|
||||
'href' => $profile->profileurl,
|
||||
'rel' => 'contact member',
|
||||
'class' => 'url'));
|
||||
$this->out->text(' ');
|
||||
$avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
|
||||
$this->out->element('img', array('src' => (($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_MINI_SIZE)),
|
||||
'width' => AVATAR_MINI_SIZE,
|
||||
@ -93,6 +94,7 @@ class ProfileSection extends Section
|
||||
'alt' => ($profile->fullname) ?
|
||||
$profile->fullname :
|
||||
$profile->nickname));
|
||||
$this->out->text(' ');
|
||||
$this->out->element('span', 'fn nickname', $profile->nickname);
|
||||
$this->out->elementEnd('a');
|
||||
$this->out->elementEnd('span');
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -110,9 +110,20 @@ class Theme
|
||||
$server = common_config('site', 'server');
|
||||
}
|
||||
|
||||
// XXX: protocol
|
||||
$ssl = common_config('theme', 'ssl');
|
||||
|
||||
$this->path = 'http://'.$server.$path.$name;
|
||||
if (is_null($ssl)) { // null -> guess
|
||||
if (common_config('site', 'ssl') == 'always' &&
|
||||
!common_config('theme', 'server')) {
|
||||
$ssl = true;
|
||||
} else {
|
||||
$ssl = false;
|
||||
}
|
||||
}
|
||||
|
||||
$protocol = ($ssl) ? 'https' : 'http';
|
||||
|
||||
$this->path = $protocol . '://'.$server.$path.$name;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -238,9 +238,12 @@ class UserProfile extends Widget
|
||||
|
||||
if (Event::handle('StartProfilePageActionsElements', array(&$this->out, $this->profile))) {
|
||||
if (empty($cur)) { // not logged in
|
||||
$this->out->elementStart('li', 'entity_subscribe');
|
||||
$this->showRemoteSubscribeLink();
|
||||
$this->out->elementEnd('li');
|
||||
if (Event::handle('StartProfileRemoteSubscribe', array(&$this->out, $this->profile))) {
|
||||
$this->out->elementStart('li', 'entity_subscribe');
|
||||
$this->showRemoteSubscribeLink();
|
||||
$this->out->elementEnd('li');
|
||||
Event::handle('EndProfileRemoteSubscribe', array(&$this->out, $this->profile));
|
||||
}
|
||||
} else {
|
||||
if ($cur->id == $this->profile->id) { // your own page
|
||||
$this->out->elementStart('li', 'entity_edit');
|
||||
|
@ -367,7 +367,8 @@ function common_current_user()
|
||||
|
||||
if ($_cur === false) {
|
||||
|
||||
if (isset($_REQUEST[session_name()]) || (isset($_SESSION['userid']) && $_SESSION['userid'])) {
|
||||
if (isset($_COOKIE[session_name()]) || isset($_GET[session_name()])
|
||||
|| (isset($_SESSION['userid']) && $_SESSION['userid'])) {
|
||||
common_ensure_session();
|
||||
$id = isset($_SESSION['userid']) ? $_SESSION['userid'] : false;
|
||||
if ($id) {
|
||||
|
@ -63,9 +63,9 @@ class OStatusPlugin extends Plugin
|
||||
$m->connect('main/ostatus?nickname=:nickname',
|
||||
array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+'));
|
||||
$m->connect('main/ostatussub',
|
||||
array('action' => 'ostatussub'));
|
||||
array('action' => 'ostatussub'));
|
||||
$m->connect('main/ostatussub',
|
||||
array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+'));
|
||||
array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+'));
|
||||
|
||||
// PuSH actions
|
||||
$m->connect('main/push/hub', array('action' => 'pushhub'));
|
||||
@ -80,6 +80,9 @@ class OStatusPlugin extends Plugin
|
||||
$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;
|
||||
}
|
||||
|
||||
@ -109,24 +112,34 @@ class OStatusPlugin extends Plugin
|
||||
* Set up a PuSH hub link to our internal link for canonical timeline
|
||||
* Atom feeds for users and groups.
|
||||
*/
|
||||
function onStartApiAtom(Action $action)
|
||||
function onStartApiAtom(AtomNoticeFeed $feed)
|
||||
{
|
||||
if ($action instanceof ApiTimelineUserAction || $action instanceof ApiTimelineGroupAction) {
|
||||
$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')));
|
||||
$id = null;
|
||||
|
||||
// Also, we'll add in the salmon link
|
||||
$action->element('link', array('rel' => 'salmon',
|
||||
'href' => common_local_url('salmon')));
|
||||
}
|
||||
if ($feed instanceof AtomUserNoticeFeed) {
|
||||
$salmonAction = 'salmon';
|
||||
$id = $feed->getUser()->id;
|
||||
} else if ($feed instanceof AtomGroupNoticeFeed) {
|
||||
$salmonAction = 'salmongroup';
|
||||
$id = $feed->getGroup()->id;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($id)) {
|
||||
$hub = common_config('ostatus', 'hub');
|
||||
if (empty($hub)) {
|
||||
// Updates will be handled through our internal PuSH hub.
|
||||
$hub = common_local_url('pushhub');
|
||||
}
|
||||
$feed->addLink($hub, array('rel' => 'hub'));
|
||||
|
||||
// Also, we'll add in the salmon link
|
||||
$salmon = common_local_url($salmonAction, array('id' => $id));
|
||||
$feed->addLink($salmon, array('rel' => 'salmon'));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add the feed settings page to the Connect Settings menu
|
||||
*
|
||||
@ -175,7 +188,7 @@ class OStatusPlugin extends Plugin
|
||||
/**
|
||||
* Add in an OStatus subscribe button
|
||||
*/
|
||||
function onStartProfilePageActionsElements($output, $profile)
|
||||
function onStartProfileRemoteSubscribe($output, $profile)
|
||||
{
|
||||
$cur = common_current_user();
|
||||
|
||||
@ -186,14 +199,19 @@ class OStatusPlugin extends Plugin
|
||||
array('nickname' => $profile->nickname));
|
||||
$output->element('a', array('href' => $url,
|
||||
'class' => 'entity_remote_subscribe'),
|
||||
_('OStatus'));
|
||||
|
||||
_m('Subscribe'));
|
||||
|
||||
$output->elementEnd('li');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we've got some Salmon stuff to send
|
||||
* 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)
|
||||
{
|
||||
@ -204,38 +222,66 @@ class OStatusPlugin extends Plugin
|
||||
$w = new Webfinger;
|
||||
|
||||
$endpoint_uri = '';
|
||||
|
||||
|
||||
$result = $w->lookup($webfinger);
|
||||
if (empty($result)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
foreach ($result->links as $link) {
|
||||
if ($link['rel'] == 'salmon') {
|
||||
$endpoint_uri = $link['href'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (empty($endpoint_uri)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8" ?>';
|
||||
$xml .= $notice->asAtomEntry();
|
||||
|
||||
|
||||
$salmon = new Salmon();
|
||||
$salmon->post($endpoint_uri, $xml);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
function onEndShowStatusNetStyles($action) {
|
||||
$action->cssLink(common_path('plugins/OStatus/theme/base/css/ostatus.css'));
|
||||
return true;
|
||||
}
|
||||
|
||||
function onEndShowStatusNetScripts($action) {
|
||||
$action->script(common_path('plugins/OStatus/js/ostatus.js'));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -182,9 +182,9 @@ class FeedSubSettingsAction extends ConnectSettingsAction
|
||||
}
|
||||
|
||||
$this->munger = $discover->feedMunger();
|
||||
$this->feedinfo = $this->munger->feedInfo();
|
||||
$this->profile = $this->munger->ostatusProfile();
|
||||
|
||||
if ($this->feedinfo->huburi == '' && !common_config('feedsub', 'nohub')) {
|
||||
if ($this->profile->huburi == '' && !common_config('feedsub', 'nohub')) {
|
||||
$this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
|
||||
return false;
|
||||
}
|
||||
@ -196,13 +196,16 @@ class FeedSubSettingsAction extends ConnectSettingsAction
|
||||
{
|
||||
if ($this->validateFeed()) {
|
||||
$this->preview = true;
|
||||
$this->feedinfo = Feedinfo::ensureProfile($this->munger);
|
||||
$this->profile = Ostatus_profile::ensureProfile($this->munger);
|
||||
if (!$this->profile) {
|
||||
throw new ServerException("Feed profile was not saved properly.");
|
||||
}
|
||||
|
||||
// If not already in use, subscribe to updates via the hub
|
||||
if ($this->feedinfo->sub_start) {
|
||||
common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}");
|
||||
if ($this->profile->sub_start) {
|
||||
common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}");
|
||||
} else {
|
||||
$ok = $this->feedinfo->subscribe();
|
||||
$ok = $this->profile->subscribe();
|
||||
common_log(LOG_INFO, __METHOD__ . ": sub was $ok");
|
||||
if (!$ok) {
|
||||
$this->showForm(_m('Feed subscription failed! Bad response from hub.'));
|
||||
@ -212,23 +215,21 @@ class FeedSubSettingsAction extends ConnectSettingsAction
|
||||
|
||||
// And subscribe the current user to the local profile
|
||||
$user = common_current_user();
|
||||
$profile = $this->feedinfo->getProfile();
|
||||
if (!$profile) {
|
||||
throw new ServerException("Feed profile was not saved properly.");
|
||||
}
|
||||
|
||||
if ($this->feedinfo->isGroup()) {
|
||||
if ($user->isMember($profile)) {
|
||||
if ($this->profile->isGroup()) {
|
||||
$group = $this->profile->localGroup();
|
||||
if ($user->isMember($group)) {
|
||||
$this->showForm(_m('Already a member!'));
|
||||
} elseif (Group_member::join($this->feedinfo->group_id, $user->id)) {
|
||||
} elseif (Group_member::join($this->profile->group_id, $user->id)) {
|
||||
$this->showForm(_m('Joined remote group!'));
|
||||
} else {
|
||||
$this->showForm(_m('Remote group join failed!'));
|
||||
}
|
||||
} else {
|
||||
if ($user->isSubscribed($profile)) {
|
||||
$local = $this->profile->localProfile();
|
||||
if ($user->isSubscribed($local)) {
|
||||
$this->showForm(_m('Already subscribed!'));
|
||||
} elseif ($user->subscribeTo($profile)) {
|
||||
} elseif ($user->subscribeTo($local)) {
|
||||
$this->showForm(_m('Feed subscribed!'));
|
||||
} else {
|
||||
$this->showForm(_m('Feed subscription failed!'));
|
||||
@ -247,7 +248,7 @@ class FeedSubSettingsAction extends ConnectSettingsAction
|
||||
|
||||
function previewFeed()
|
||||
{
|
||||
$feedinfo = $this->munger->feedinfo();
|
||||
$profile = $this->munger->ostatusProfile();
|
||||
$notice = $this->munger->notice(0, true); // preview
|
||||
|
||||
if ($notice) {
|
||||
|
@ -67,9 +67,21 @@ class OStatusInitAction extends Action
|
||||
|
||||
function showForm($err = null)
|
||||
{
|
||||
$this->err = $err;
|
||||
$this->showPage();
|
||||
|
||||
$this->err = $err;
|
||||
if ($this->boolean('ajax')) {
|
||||
header('Content-Type: text/xml;charset=utf-8');
|
||||
$this->xw->startDocument('1.0', 'UTF-8');
|
||||
$this->elementStart('html');
|
||||
$this->elementStart('head');
|
||||
$this->element('title', null, _('Subscribe to user'));
|
||||
$this->elementEnd('head');
|
||||
$this->elementStart('body');
|
||||
$this->showContent();
|
||||
$this->elementEnd('body');
|
||||
$this->elementEnd('html');
|
||||
} else {
|
||||
$this->showPage();
|
||||
}
|
||||
}
|
||||
|
||||
function showContent()
|
||||
@ -79,15 +91,15 @@ class OStatusInitAction extends Action
|
||||
'class' => 'form_settings',
|
||||
'action' => common_local_url('ostatusinit')));
|
||||
$this->elementStart('fieldset');
|
||||
$this->element('legend', _('Subscribe to a remote user'));
|
||||
$this->element('legend', null, sprintf(_('Subscribe to %s'), $this->nickname));
|
||||
$this->hidden('token', common_session_token());
|
||||
|
||||
$this->elementStart('ul', 'form_data');
|
||||
$this->elementStart('li');
|
||||
$this->elementStart('li', array('id' => 'ostatus_nickname'));
|
||||
$this->input('nickname', _('User nickname'), $this->nickname,
|
||||
_('Nickname of the user you want to follow'));
|
||||
$this->elementEnd('li');
|
||||
$this->elementStart('li');
|
||||
$this->elementStart('li', array('id' => 'ostatus_profile'));
|
||||
$this->input('acct', _('Profile Account'), $this->acct,
|
||||
_('Your account id (i.e. user@identi.ca)'));
|
||||
$this->elementEnd('li');
|
||||
@ -95,7 +107,7 @@ class OStatusInitAction extends Action
|
||||
$this->submit('submit', _('Subscribe'));
|
||||
$this->elementEnd('fieldset');
|
||||
$this->elementEnd('form');
|
||||
}
|
||||
}
|
||||
|
||||
function ostatusConnect()
|
||||
{
|
||||
@ -125,4 +137,4 @@ class OStatusInitAction extends Action
|
||||
return _('OStatus Connect');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ class OStatusSubAction extends Action
|
||||
$this->elementStart('fieldset', array('id' => 'settings_feeds'));
|
||||
|
||||
$this->elementStart('ul', 'form_data');
|
||||
$this->elementStart('li', array('id' => 'settings_twitter_login_button'));
|
||||
$this->elementStart('li');
|
||||
$this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed'));
|
||||
$this->elementEnd('li');
|
||||
$this->elementEnd('ul');
|
||||
@ -164,9 +164,9 @@ class OStatusSubAction extends Action
|
||||
}
|
||||
|
||||
$this->munger = $discover->feedMunger();
|
||||
$this->feedinfo = $this->munger->feedInfo();
|
||||
$this->profile = $this->munger->ostatusProfile();
|
||||
|
||||
if ($this->feedinfo->huburi == '') {
|
||||
if ($this->profile->huburi == '') {
|
||||
$this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
|
||||
return false;
|
||||
}
|
||||
@ -178,13 +178,13 @@ class OStatusSubAction extends Action
|
||||
{
|
||||
if ($this->validateFeed()) {
|
||||
$this->preview = true;
|
||||
$this->feedinfo = Feedinfo::ensureProfile($this->munger);
|
||||
$this->profile = Ostatus_profile::ensureProfile($this->munger);
|
||||
|
||||
// If not already in use, subscribe to updates via the hub
|
||||
if ($this->feedinfo->sub_start) {
|
||||
common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}");
|
||||
if ($this->profile->sub_start) {
|
||||
common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}");
|
||||
} else {
|
||||
$ok = $this->feedinfo->subscribe();
|
||||
$ok = $this->profile->subscribe();
|
||||
common_log(LOG_INFO, __METHOD__ . ": sub was $ok");
|
||||
if (!$ok) {
|
||||
$this->showForm(_m('Feed subscription failed! Bad response from hub.'));
|
||||
@ -194,7 +194,7 @@ class OStatusSubAction extends Action
|
||||
|
||||
// And subscribe the current user to the local profile
|
||||
$user = common_current_user();
|
||||
$profile = $this->feedinfo->getProfile();
|
||||
$profile = $this->profile->getProfile();
|
||||
|
||||
if ($user->isSubscribed($profile)) {
|
||||
$this->showForm(_m('Already subscribed!'));
|
||||
@ -209,7 +209,7 @@ class OStatusSubAction extends Action
|
||||
|
||||
function previewFeed()
|
||||
{
|
||||
$feedinfo = $this->munger->feedinfo();
|
||||
$profile = $this->munger->ostatusProfile();
|
||||
$notice = $this->munger->notice(0, true); // preview
|
||||
|
||||
if ($notice) {
|
||||
@ -223,4 +223,4 @@ class OStatusSubAction extends Action
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -22,28 +22,60 @@
|
||||
* @author James Walker <james@status.net>
|
||||
*/
|
||||
|
||||
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
|
||||
if (!defined('STATUSNET')) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
class SalmonAction extends Action
|
||||
{
|
||||
var $user = null;
|
||||
var $xml = null;
|
||||
var $activity = null;
|
||||
|
||||
function handle()
|
||||
function prepare($args)
|
||||
{
|
||||
parent::handle();
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$this->handlePost();
|
||||
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
|
||||
$this->clientError(_('This method requires a POST.'));
|
||||
}
|
||||
}
|
||||
|
||||
if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') {
|
||||
$this->clientError(_('Salmon requires application/atom+xml'));
|
||||
}
|
||||
|
||||
function handlePost()
|
||||
{
|
||||
$user_id = $this->arg('id');
|
||||
common_log(LOG_DEBUG, 'Salmon: incoming post for user: '. $user_id);
|
||||
$id = $this->trimmed('id');
|
||||
|
||||
if (!$id) {
|
||||
$this->clientError(_('No ID.'));
|
||||
}
|
||||
|
||||
$this->user = User::staticGet($id);
|
||||
|
||||
if (empty($this->user)) {
|
||||
$this->clientError(_('No such user.'));
|
||||
}
|
||||
|
||||
$xml = file_get_contents('php://input');
|
||||
|
||||
$dom = DOMDocument::loadXML($xml);
|
||||
|
||||
// XXX: check that document element is Atom entry
|
||||
// XXX: check the signature
|
||||
|
||||
$this->act = new Activity($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:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,390 +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,
|
||||
'group_id' => DB_DATAOBJECT_INT,
|
||||
'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, true),
|
||||
new ColumnDef('group_id', 'integer',
|
||||
null, true),
|
||||
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
|
||||
* @param boolean $isGroup is this a group record?
|
||||
* @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;
|
||||
if ($feedinfo->isGroup()) {
|
||||
$group = new User_group();
|
||||
$group->nickname = $profile->nickname . '@remote'; // @fixme
|
||||
$group->fullname = $profile->fullname;
|
||||
$group->homepage = $profile->homepage;
|
||||
$group->location = $profile->location;
|
||||
$group->created = $profile->created;
|
||||
$group->insert();
|
||||
|
||||
if ($avatar) {
|
||||
$group->setOriginal($filename);
|
||||
}
|
||||
|
||||
$feedinfo->group_id = $group->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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 /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)) {
|
||||
// @fixme we might have to do individual and group delivery separately!
|
||||
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));
|
||||
}
|
||||
common_log(LOG_INFO, __METHOD__ . ": saved notice {$notice->id} for entry $index of update to \"{$this->feeduri}\"");
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
@ -242,7 +242,7 @@ class HubSub extends Memcached_DataObject
|
||||
{
|
||||
$headers = array('Content-Type: application/atom+xml');
|
||||
if ($this->secret) {
|
||||
$hmac = sha1($atom . $this->secret);
|
||||
$hmac = hash_hmac('sha1', $atom, $this->secret);
|
||||
$headers[] = "X-Hub-Signature: sha1=$hmac";
|
||||
} else {
|
||||
$hmac = '(none)';
|
||||
|
644
plugins/OStatus/classes/Ostatus_profile.php
Normal file
644
plugins/OStatus/classes/Ostatus_profile.php
Normal file
@ -0,0 +1,644 @@
|
||||
<?php
|
||||
/*
|
||||
* StatusNet - the distributed open-source microblogging tool
|
||||
* Copyright (C) 2009-2010, StatusNet, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package FeedSubPlugin
|
||||
* @maintainer Brion Vibber <brion@status.net>
|
||||
*/
|
||||
|
||||
/*
|
||||
PuSH subscription flow:
|
||||
|
||||
$profile->subscribe()
|
||||
generate random verification token
|
||||
save to verify_token
|
||||
sends a sub request to the hub...
|
||||
|
||||
main/push/callback
|
||||
hub sends confirmation back to us via GET
|
||||
We verify the request, then echo back the challenge.
|
||||
On our end, we save the time we subscribed and the lease expiration
|
||||
|
||||
main/push/callback
|
||||
hub sends us updates via POST
|
||||
|
||||
*/
|
||||
|
||||
class FeedDBException extends FeedSubException
|
||||
{
|
||||
public $obj;
|
||||
|
||||
function __construct($obj)
|
||||
{
|
||||
parent::__construct('Database insert failure');
|
||||
$this->obj = $obj;
|
||||
}
|
||||
}
|
||||
|
||||
class Ostatus_profile extends Memcached_DataObject
|
||||
{
|
||||
public $__table = 'ostatus_profile';
|
||||
|
||||
public $id;
|
||||
public $profile_id;
|
||||
public $group_id;
|
||||
|
||||
public $feeduri;
|
||||
public $homeuri;
|
||||
|
||||
// PuSH subscription data
|
||||
public $huburi;
|
||||
public $secret;
|
||||
public $verify_token;
|
||||
public $sub_state; // subscribe, active, unsubscribe
|
||||
public $sub_start;
|
||||
public $sub_end;
|
||||
|
||||
public $salmonuri;
|
||||
|
||||
public $created;
|
||||
public $lastupdate;
|
||||
|
||||
|
||||
public /*static*/ function staticGet($k, $v=null)
|
||||
{
|
||||
return parent::staticGet(__CLASS__, $k, $v);
|
||||
}
|
||||
|
||||
/**
|
||||
* return table definition for DB_DataObject
|
||||
*
|
||||
* DB_DataObject needs to know something about the table to manipulate
|
||||
* instances. This method provides all the DB_DataObject needs to know.
|
||||
*
|
||||
* @return array array of column definitions
|
||||
*/
|
||||
|
||||
function table()
|
||||
{
|
||||
return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
|
||||
'profile_id' => DB_DATAOBJECT_INT,
|
||||
'group_id' => DB_DATAOBJECT_INT,
|
||||
'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
|
||||
'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
|
||||
'huburi' => DB_DATAOBJECT_STR,
|
||||
'secret' => DB_DATAOBJECT_STR,
|
||||
'verify_token' => DB_DATAOBJECT_STR,
|
||||
'sub_state' => DB_DATAOBJECT_STR,
|
||||
'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
|
||||
'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
|
||||
'salmonuri' => DB_DATAOBJECT_STR,
|
||||
'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
|
||||
'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
|
||||
}
|
||||
|
||||
static function schemaDef()
|
||||
{
|
||||
return array(new ColumnDef('id', 'integer',
|
||||
/*size*/ null,
|
||||
/*nullable*/ false,
|
||||
/*key*/ 'PRI',
|
||||
/*default*/ '0',
|
||||
/*extra*/ null,
|
||||
/*auto_increment*/ true),
|
||||
new ColumnDef('profile_id', 'integer',
|
||||
null, true, 'UNI'),
|
||||
new ColumnDef('group_id', 'integer',
|
||||
null, true, 'UNI'),
|
||||
new ColumnDef('feeduri', 'varchar',
|
||||
255, false, 'UNI'),
|
||||
new ColumnDef('homeuri', 'varchar',
|
||||
255, false),
|
||||
new ColumnDef('huburi', 'text',
|
||||
null, true),
|
||||
new ColumnDef('verify_token', 'varchar',
|
||||
32, true),
|
||||
new ColumnDef('secret', 'varchar',
|
||||
64, true),
|
||||
new ColumnDef('sub_state', "enum('subscribe','active','unsubscribe')",
|
||||
null, true),
|
||||
new ColumnDef('sub_start', 'datetime',
|
||||
null, true),
|
||||
new ColumnDef('sub_end', 'datetime',
|
||||
null, true),
|
||||
new ColumnDef('salmonuri', 'text',
|
||||
null, true),
|
||||
new ColumnDef('created', 'datetime',
|
||||
null, false),
|
||||
new ColumnDef('lastupdate', 'datetime',
|
||||
null, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* return key definitions for DB_DataObject
|
||||
*
|
||||
* DB_DataObject needs to know about keys that the table has; this function
|
||||
* defines them.
|
||||
*
|
||||
* @return array key definitions
|
||||
*/
|
||||
|
||||
function keys()
|
||||
{
|
||||
return array_keys($this->keyTypes());
|
||||
}
|
||||
|
||||
/**
|
||||
* return key definitions for Memcached_DataObject
|
||||
*
|
||||
* Our caching system uses the same key definitions, but uses a different
|
||||
* method to get them.
|
||||
*
|
||||
* @return array key definitions
|
||||
*/
|
||||
|
||||
function keyTypes()
|
||||
{
|
||||
return array('id' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U');
|
||||
}
|
||||
|
||||
function sequenceKey()
|
||||
{
|
||||
return array('id', true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the StatusNet-side profile for this feed
|
||||
* @return Profile
|
||||
*/
|
||||
public function localProfile()
|
||||
{
|
||||
if ($this->profile_id) {
|
||||
return Profile::staticGet('id', $this->profile_id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the StatusNet-side profile for this feed
|
||||
* @return Profile
|
||||
*/
|
||||
public function localGroup()
|
||||
{
|
||||
if ($this->group_id) {
|
||||
return User_group::staticGet('id', $this->group_id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FeedMunger $munger
|
||||
* @param boolean $isGroup is this a group record?
|
||||
* @return Ostatus_profile
|
||||
*/
|
||||
public static function ensureProfile($munger)
|
||||
{
|
||||
$profile = $munger->ostatusProfile();
|
||||
|
||||
$current = self::staticGet('feeduri', $profile->feeduri);
|
||||
if ($current) {
|
||||
// @fixme we should probably update info as necessary
|
||||
return $current;
|
||||
}
|
||||
|
||||
$profile->query('BEGIN');
|
||||
|
||||
// Awful hack! Awful hack!
|
||||
$profile->verify = common_good_rand(16);
|
||||
$profile->secret = common_good_rand(32);
|
||||
|
||||
try {
|
||||
$local = $munger->profile();
|
||||
|
||||
if ($entity->isGroup()) {
|
||||
$group = new User_group();
|
||||
$group->nickname = $local->nickname . '@remote'; // @fixme
|
||||
$group->fullname = $local->fullname;
|
||||
$group->homepage = $local->homepage;
|
||||
$group->location = $local->location;
|
||||
$group->created = $local->created;
|
||||
$group->insert();
|
||||
if (empty($result)) {
|
||||
throw new FeedDBException($group);
|
||||
}
|
||||
$profile->group_id = $group->id;
|
||||
} else {
|
||||
$result = $local->insert();
|
||||
if (empty($result)) {
|
||||
throw new FeedDBException($local);
|
||||
}
|
||||
$profile->profile_id = $local->id;
|
||||
}
|
||||
|
||||
$profile->created = sql_common_date();
|
||||
$profile->lastupdate = sql_common_date();
|
||||
$result = $profile->insert();
|
||||
if (empty($result)) {
|
||||
throw new FeedDBException($profile);
|
||||
}
|
||||
|
||||
$entity->query('COMMIT');
|
||||
} catch (FeedDBException $e) {
|
||||
common_log_db_error($e->obj, 'INSERT', __FILE__);
|
||||
$entity->query('ROLLBACK');
|
||||
return false;
|
||||
}
|
||||
|
||||
$avatar = $munger->getAvatar();
|
||||
if ($avatar) {
|
||||
try {
|
||||
$this->updateAvatar($avatar);
|
||||
} catch (Exception $e) {
|
||||
common_log(LOG_ERR, "Exception setting OStatus avatar: " .
|
||||
$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and update given avatar image
|
||||
* @param string $url
|
||||
* @throws Exception in various failure cases
|
||||
*/
|
||||
public function updateAvatar($url)
|
||||
{
|
||||
// @fixme this should be better encapsulated
|
||||
// ripped from oauthstore.php (for old OMB client)
|
||||
$temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
|
||||
copy($url, $temp_filename);
|
||||
$imagefile = new ImageFile($profile->id, $temp_filename);
|
||||
$filename = Avatar::filename($profile->id,
|
||||
image_type_to_extension($imagefile->type),
|
||||
null,
|
||||
common_timestamp());
|
||||
rename($temp_filename, Avatar::path($filename));
|
||||
if ($this->isGroup()) {
|
||||
$group = $this->localGroup();
|
||||
$group->setOriginal($filename);
|
||||
} else {
|
||||
$profile = $this->localProfile();
|
||||
$profile->setOriginal($filename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an XML string fragment with profile information as an
|
||||
* Activity Streams noun object with the given element type.
|
||||
*
|
||||
* Assumes that 'activity' namespace has been previously defined.
|
||||
*
|
||||
* @param string $element one of 'actor', 'subject', 'object', 'target'
|
||||
* @return string
|
||||
*/
|
||||
function asActivityNoun($element)
|
||||
{
|
||||
$xs = new XMLStringer(true);
|
||||
|
||||
$avatarHref = Avatar::defaultImage(AVATAR_PROFILE_SIZE);
|
||||
$avatarType = 'image/png';
|
||||
if ($this->isGroup()) {
|
||||
$type = 'http://activitystrea.ms/schema/1.0/group';
|
||||
$self = $this->localGroup();
|
||||
|
||||
// @fixme put a standard getAvatar() interface on groups too
|
||||
if ($self->homepage_logo) {
|
||||
$avatarHref = $self->homepage_logo;
|
||||
$map = array('png' => 'image/png',
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'gif' => 'image/gif');
|
||||
$extension = pathinfo(parse_url($avatarHref, PHP_URL_PATH), PATHINFO_EXTENSION);
|
||||
if (isset($map[$extension])) {
|
||||
$avatarType = $map[$extension];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$type = 'http://activitystrea.ms/schema/1.0/person';
|
||||
$self = $this->localProfile();
|
||||
$avatar = $self->getAvatar(AVATAR_PROFILE_SIZE);
|
||||
if ($avatar) {
|
||||
$avatarHref = $avatar->
|
||||
$avatarType = $avatar->mediatype;
|
||||
}
|
||||
}
|
||||
$xs->elementStart('activity:' . $element);
|
||||
$xs->element(
|
||||
'activity:object-type',
|
||||
null,
|
||||
$type
|
||||
);
|
||||
$xs->element(
|
||||
'id',
|
||||
null,
|
||||
$this->homeuri); // ?
|
||||
$xs->element('title', null, $self->getBestName());
|
||||
|
||||
$xs->element(
|
||||
'link', array(
|
||||
'type' => $avatarType,
|
||||
'href' => $avatarHref
|
||||
),
|
||||
''
|
||||
);
|
||||
|
||||
$xs->elementEnd('activity:' . $element);
|
||||
|
||||
return $xs->getString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Damn dirty hack!
|
||||
*/
|
||||
function isGroup()
|
||||
{
|
||||
return (strpos($this->feeduri, '/groups/') !== false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a subscription request to the hub for this feed.
|
||||
* The hub will later send us a confirmation POST to /main/push/callback.
|
||||
*
|
||||
* @return bool true on success, false on failure
|
||||
*/
|
||||
public function subscribe($mode='subscribe')
|
||||
{
|
||||
if (common_config('feedsub', 'nohub')) {
|
||||
// Fake it! We're just testing remote feeds w/o hubs.
|
||||
return true;
|
||||
}
|
||||
// @fixme use the verification token
|
||||
#$token = md5(mt_rand() . ':' . $this->feeduri);
|
||||
#$this->verify_token = $token;
|
||||
#$this->update(); // @fixme
|
||||
try {
|
||||
$callback = common_local_url('pushcallback', array('feed' => $this->id));
|
||||
$headers = array('Content-Type: application/x-www-form-urlencoded');
|
||||
$post = array('hub.mode' => $mode,
|
||||
'hub.callback' => $callback,
|
||||
'hub.verify' => 'async',
|
||||
'hub.verify_token' => $this->verify_token,
|
||||
'hub.secret' => $this->secret,
|
||||
//'hub.lease_seconds' => 0,
|
||||
'hub.topic' => $this->feeduri);
|
||||
$client = new HTTPClient();
|
||||
$response = $client->post($this->huburi, $headers, $post);
|
||||
$status = $response->getStatus();
|
||||
if ($status == 202) {
|
||||
common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
|
||||
return true;
|
||||
} else if ($status == 204) {
|
||||
common_log(LOG_INFO, __METHOD__ . ': sub req ok and verified');
|
||||
return true;
|
||||
} else if ($status >= 200 && $status < 300) {
|
||||
common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody());
|
||||
return false;
|
||||
} else {
|
||||
common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody());
|
||||
return false;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// wtf!
|
||||
common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save PuSH subscription confirmation.
|
||||
* Sets approximate lease start and end times and finalizes state.
|
||||
*
|
||||
* @param int $lease_seconds provided hub.lease_seconds parameter, if given
|
||||
*/
|
||||
public function confirmSubscribe($lease_seconds=0)
|
||||
{
|
||||
$original = clone($this);
|
||||
|
||||
$this->sub_state = 'active';
|
||||
$this->sub_start = common_sql_date(time());
|
||||
if ($lease_seconds > 0) {
|
||||
$this->sub_end = common_sql_date(time() + $lease_seconds);
|
||||
} else {
|
||||
$this->sub_end = null;
|
||||
}
|
||||
$this->lastupdate = common_sql_date();
|
||||
|
||||
return $this->update($original);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save PuSH unsubscription confirmation.
|
||||
* Wipes active PuSH sub info and resets state.
|
||||
*/
|
||||
public function confirmUnsubscribe()
|
||||
{
|
||||
$original = clone($this);
|
||||
|
||||
$this->verify_token = null;
|
||||
$this->secret = null;
|
||||
$this->sub_state = null;
|
||||
$this->sub_start = null;
|
||||
$this->sub_end = null;
|
||||
$this->lastupdate = common_sql_date();
|
||||
|
||||
return $this->update($original);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PuSH unsubscription request to the hub for this feed.
|
||||
* The hub will later send us a confirmation POST to /main/push/callback.
|
||||
*
|
||||
* @return bool true on success, false on failure
|
||||
*/
|
||||
public function unsubscribe() {
|
||||
return $this->subscribe('unsubscribe');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an Activity Streams notification to the remote Salmon endpoint,
|
||||
* if so configured.
|
||||
*
|
||||
* @param Profile $actor
|
||||
* @param $verb eg Activity::SUBSCRIBE or Activity::JOIN
|
||||
* @param $object object of the action; if null, the remote entity itself is assumed
|
||||
*/
|
||||
public function notify(Profile $actor, $verb, $object=null)
|
||||
{
|
||||
if ($object == null) {
|
||||
$object = $this;
|
||||
}
|
||||
if ($this->salmonuri) {
|
||||
$text = 'update'; // @fixme
|
||||
$id = 'tag:' . common_config('site', 'server') .
|
||||
':' . $verb .
|
||||
':' . $actor->id .
|
||||
':' . time(); // @fixme
|
||||
|
||||
$entry = new Atom10Entry();
|
||||
$entry->elementStart('entry');
|
||||
$entry->element('id', null, $id);
|
||||
$entry->element('title', null, $text);
|
||||
$entry->element('summary', null, $text);
|
||||
$entry->element('published', null, common_date_w3dtf());
|
||||
|
||||
$entry->element('activity:verb', null, $verb);
|
||||
$entry->raw($profile->asAtomAuthor());
|
||||
$entry->raw($profile->asActivityActor());
|
||||
$entry->raw($object->asActivityNoun('object'));
|
||||
$entry->elmentEnd('entry');
|
||||
|
||||
$feed = $this->atomFeed($actor);
|
||||
$feed->initFeed();
|
||||
$feed->addEntry($entry);
|
||||
$feed->renderEntries();
|
||||
$feed->endFeed();
|
||||
|
||||
$xml = $feed->getString();
|
||||
common_log(LOG_INFO, "Posting to Salmon endpoint $salmon: $xml");
|
||||
|
||||
$salmon = new Salmon(); // ?
|
||||
$salmon->post($this->salmonuri, $xml);
|
||||
}
|
||||
}
|
||||
|
||||
function getBestName()
|
||||
{
|
||||
if ($this->isGroup()) {
|
||||
return $this->localGroup()->getBestName();
|
||||
} else {
|
||||
return $this->localProfile()->getBestName();
|
||||
}
|
||||
}
|
||||
|
||||
function atomFeed($actor)
|
||||
{
|
||||
$feed = new Atom10Feed();
|
||||
// @fixme should these be set up somewhere else?
|
||||
$feed->addNamespace('activity', 'http://activitystrea.ms/spec/1.0/');
|
||||
$feed->addNamesapce('thr', 'http://purl.org/syndication/thread/1.0');
|
||||
$feed->addNamespace('georss', 'http://www.georss.org/georss');
|
||||
$feed->addNamespace('ostatus', 'http://ostatus.org/schema/1.0');
|
||||
|
||||
$taguribase = common_config('integration', 'taguri');
|
||||
$feed->setId("tag:{$taguribase}:UserTimeline:{$actor->id}"); // ???
|
||||
|
||||
$feed->setTitle($actor->getBestName() . ' timeline'); // @fixme
|
||||
$feed->setUpdated(time());
|
||||
$feed->setPublished(time());
|
||||
|
||||
$feed->addLink(common_url('ApiTimelineUser',
|
||||
array('id' => $actor->id,
|
||||
'type' => 'atom')),
|
||||
array('rel' => 'self',
|
||||
'type' => 'application/atom+xml'));
|
||||
|
||||
$feed->addLink(common_url('userbyid',
|
||||
array('id' => $actor->id)),
|
||||
array('rel' => 'alternate',
|
||||
'type' => 'text/html'));
|
||||
|
||||
return $feed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and post notices for updates from the feed.
|
||||
* Currently assumes that all items in the feed are new,
|
||||
* coming from a PuSH hub.
|
||||
*
|
||||
* @param string $xml source of Atom or RSS feed
|
||||
* @param string $hmac X-Hub-Signature header, if present
|
||||
*/
|
||||
public function postUpdates($xml, $hmac)
|
||||
{
|
||||
common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml");
|
||||
|
||||
if ($this->secret) {
|
||||
if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) {
|
||||
$their_hmac = strtolower($matches[1]);
|
||||
$our_hmac = hash_hmac('sha1', $xml, $this->secret);
|
||||
if ($their_hmac !== $our_hmac) {
|
||||
common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'");
|
||||
return;
|
||||
}
|
||||
} else if ($hmac) {
|
||||
common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'");
|
||||
return;
|
||||
}
|
||||
|
||||
require_once "XML/Feed/Parser.php";
|
||||
$feed = new XML_Feed_Parser($xml, false, false, true);
|
||||
$munger = new FeedMunger($feed);
|
||||
|
||||
$hits = 0;
|
||||
foreach ($feed as $index => $entry) {
|
||||
// @fixme this might sort in wrong order if we get multiple updates
|
||||
|
||||
$notice = $munger->notice($index);
|
||||
|
||||
// Double-check for oldies
|
||||
// @fixme this could explode horribly for multiple feeds on a blog. sigh
|
||||
$dupe = new Notice();
|
||||
$dupe->uri = $notice->uri;
|
||||
if ($dupe->find(true)) {
|
||||
common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// @fixme need to ensure that groups get handled correctly
|
||||
$saved = Notice::saveNew($notice->profile_id,
|
||||
$notice->content,
|
||||
'ostatus',
|
||||
array('is_local' => Notice::REMOTE_OMB,
|
||||
'uri' => $notice->uri,
|
||||
'lat' => $notice->lat,
|
||||
'lon' => $notice->lon,
|
||||
'location_ns' => $notice->location_ns,
|
||||
'location_id' => $notice->location_id));
|
||||
|
||||
/*
|
||||
common_log(LOG_DEBUG, "going to check group delivery...");
|
||||
if ($this->group_id) {
|
||||
$group = User_group::staticGet($this->group_id);
|
||||
if ($group) {
|
||||
common_log(LOG_INFO, __METHOD__ . ": saving to local shadow group $group->id $group->nickname");
|
||||
$groups = array($group);
|
||||
} else {
|
||||
common_log(LOG_INFO, __METHOD__ . ": lost the local shadow group?");
|
||||
}
|
||||
} else {
|
||||
common_log(LOG_INFO, __METHOD__ . ": no local shadow groups");
|
||||
$groups = array();
|
||||
}
|
||||
common_log(LOG_DEBUG, "going to add to inboxes...");
|
||||
$notice->addToInboxes($groups, array());
|
||||
common_log(LOG_DEBUG, "added to inboxes.");
|
||||
*/
|
||||
|
||||
$hits++;
|
||||
}
|
||||
if ($hits == 0) {
|
||||
common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml");
|
||||
}
|
||||
}
|
||||
}
|
60
plugins/OStatus/js/ostatus.js
Normal file
60
plugins/OStatus/js/ostatus.js
Normal file
@ -0,0 +1,60 @@
|
||||
SN.U.DialogBox = {
|
||||
Subscribe: function(a) {
|
||||
var f = a.parent().find('#form_ostatus_connect');
|
||||
if (f.length > 0) {
|
||||
f.show();
|
||||
}
|
||||
else {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
dataType: 'xml',
|
||||
url: a[0].href+'&ajax=1',
|
||||
beforeSend: function(formData) {
|
||||
a.addClass('processing');
|
||||
},
|
||||
error: function (xhr, textStatus, errorThrown) {
|
||||
alert(errorThrown || textStatus);
|
||||
},
|
||||
success: function(data, textStatus, xhr) {
|
||||
if (typeof($('form', data)[0]) != 'undefined') {
|
||||
a.after(document._importNode($('form', data)[0], true));
|
||||
|
||||
var form = a.parent().find('#form_ostatus_connect');
|
||||
|
||||
form
|
||||
.addClass('dialogbox')
|
||||
.append('<button class="close">×</button>');
|
||||
|
||||
form
|
||||
.find('.submit')
|
||||
.addClass('submit_dialogbox')
|
||||
.removeClass('submit')
|
||||
.bind('click', function() {
|
||||
form.addClass('processing');
|
||||
});
|
||||
|
||||
form.find('button.close').click(function(){
|
||||
form.hide();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
form.find('#acct').focus();
|
||||
}
|
||||
|
||||
a.removeClass('processing');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
SN.Init.Subscribe = function() {
|
||||
$('.entity_subscribe a').live('click', function() { SN.U.DialogBox.Subscribe($(this)); return false; });
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
if ($('.entity_subscribe .entity_remote_subscribe').length > 0) {
|
||||
SN.Init.Subscribe();
|
||||
}
|
||||
});
|
393
plugins/OStatus/lib/activity.php
Normal file
393
plugins/OStatus/lib/activity.php
Normal file
@ -0,0 +1,393 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilities for turning DOMish things into Activityish things
|
||||
*
|
||||
* Some common functions that I didn't have the bandwidth to try to factor
|
||||
* into some kind of reasonable superclass, so just dumped here. Might
|
||||
* be useful to have an ActivityObject parent class or something.
|
||||
*
|
||||
* @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/
|
||||
*/
|
||||
|
||||
class ActivityUtils
|
||||
{
|
||||
const ATOM = 'http://www.w3.org/2005/Atom';
|
||||
|
||||
const LINK = 'link';
|
||||
const REL = 'rel';
|
||||
const TYPE = 'type';
|
||||
const HREF = 'href';
|
||||
|
||||
/**
|
||||
* Get the permalink for an Activity object
|
||||
*
|
||||
* @param DOMElement $element A DOM element
|
||||
*
|
||||
* @return string related link, if any
|
||||
*/
|
||||
|
||||
static function getLink($element)
|
||||
{
|
||||
$links = $element->getElementsByTagnameNS(self::ATOM, self::LINK);
|
||||
|
||||
foreach ($links as $link) {
|
||||
|
||||
$rel = $link->getAttribute(self::REL);
|
||||
$type = $link->getAttribute(self::TYPE);
|
||||
|
||||
if ($rel == 'alternate' && $type == 'text/html') {
|
||||
return $link->getAttribute(self::HREF);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A noun-ish thing in the activity universe
|
||||
*
|
||||
* The activity streams spec talks about activity objects, while also having
|
||||
* a tag activity:object, which is in fact an activity object. Aaaaaah!
|
||||
*
|
||||
* This is just a thing in the activity universe. Can be the subject, object,
|
||||
* or indirect object (target!) of an activity verb. Rotten name, and I'm
|
||||
* propagating it. *sigh*
|
||||
*
|
||||
* @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/
|
||||
*/
|
||||
|
||||
class ActivityObject
|
||||
{
|
||||
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!
|
||||
|
||||
// Atom elements we snarf
|
||||
|
||||
const TITLE = 'title';
|
||||
const SUMMARY = 'summary';
|
||||
const CONTENT = 'content';
|
||||
const ID = 'id';
|
||||
const SOURCE = 'source';
|
||||
|
||||
const NAME = 'name';
|
||||
const URI = 'uri';
|
||||
const EMAIL = 'email';
|
||||
|
||||
public $type;
|
||||
public $id;
|
||||
public $title;
|
||||
public $summary;
|
||||
public $content;
|
||||
public $link;
|
||||
public $source;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* This probably needs to be refactored
|
||||
* to generate a local class (ActivityPerson, ActivityFile, ...)
|
||||
* based on the object type.
|
||||
*
|
||||
* @param DOMElement $element DOM thing to turn into an Activity thing
|
||||
*/
|
||||
|
||||
function __construct($element)
|
||||
{
|
||||
$this->source = $element;
|
||||
|
||||
if ($element->tagName == 'author') {
|
||||
|
||||
$this->type = self::PERSON; // XXX: is this fair?
|
||||
$this->title = $this->_childContent($element, self::NAME);
|
||||
$this->id = $this->_childContent($element, self::URI);
|
||||
|
||||
if (empty($this->id)) {
|
||||
$email = $this->_childContent($element, self::EMAIL);
|
||||
if (!empty($email)) {
|
||||
// XXX: acct: ?
|
||||
$this->id = 'mailto:'.$email;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
$this->type = $this->_childContent($element, Activity::OBJECTTYPE,
|
||||
Activity::SPEC);
|
||||
|
||||
if (empty($this->type)) {
|
||||
$this->type = ActivityObject::NOTE;
|
||||
}
|
||||
|
||||
$this->id = $this->_childContent($element, self::ID);
|
||||
$this->title = $this->_childContent($element, self::TITLE);
|
||||
$this->summary = $this->_childContent($element, self::SUMMARY);
|
||||
$this->content = $this->_childContent($element, self::CONTENT);
|
||||
$this->source = $this->_childContent($element, self::SOURCE);
|
||||
|
||||
$this->link = ActivityUtils::getLink($element);
|
||||
|
||||
// XXX: grab PoCo stuff
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab the text content of a DOM element child of the current element
|
||||
*
|
||||
* @param DOMElement $element Element whose children we examine
|
||||
* @param string $tag Tag to look up
|
||||
* @param string $namespace Namespace to use, defaults to Atom
|
||||
*
|
||||
* @return string content of the child
|
||||
*/
|
||||
|
||||
private function _childContent($element, $tag, $namespace=Activity::ATOM)
|
||||
{
|
||||
$els = $element->getElementsByTagnameNS($namespace, $tag);
|
||||
|
||||
if (empty($els) || $els->length == 0) {
|
||||
return null;
|
||||
} else {
|
||||
$el = $els->item(0);
|
||||
return $el->textContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class to hold a bunch of constant defining default verb types
|
||||
*
|
||||
* @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/
|
||||
*/
|
||||
|
||||
class ActivityVerb
|
||||
{
|
||||
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';
|
||||
}
|
||||
|
||||
/**
|
||||
* An activity in the ActivityStrea.ms world
|
||||
*
|
||||
* An activity is kind of like a sentence: someone did something
|
||||
* to something else.
|
||||
*
|
||||
* 'someone' is the 'actor'; 'did something' is the verb;
|
||||
* 'something else' is the object.
|
||||
*
|
||||
* @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/
|
||||
*/
|
||||
|
||||
class Activity
|
||||
{
|
||||
const SPEC = 'http://activitystrea.ms/spec/1.0/';
|
||||
const SCHEMA = 'http://activitystrea.ms/schema/1.0/';
|
||||
|
||||
const VERB = 'verb';
|
||||
const OBJECT = 'object';
|
||||
const ACTOR = 'actor';
|
||||
const SUBJECT = 'subject';
|
||||
const OBJECTTYPE = 'object-type';
|
||||
const CONTEXT = 'context';
|
||||
const TARGET = 'target';
|
||||
|
||||
const ATOM = 'http://www.w3.org/2005/Atom';
|
||||
|
||||
const AUTHOR = 'author';
|
||||
const PUBLISHED = 'published';
|
||||
const UPDATED = 'updated';
|
||||
|
||||
public $actor; // an ActivityObject
|
||||
public $verb; // a string (the URL)
|
||||
public $object; // an ActivityObject
|
||||
public $target; // an ActivityObject
|
||||
public $context; // an ActivityObject
|
||||
public $time; // Time of the activity
|
||||
public $link; // an ActivityObject
|
||||
public $entry; // the source entry
|
||||
public $feed; // the source feed
|
||||
|
||||
/**
|
||||
* Turns a regular old Atom <entry> into a magical activity
|
||||
*
|
||||
* @param DOMElement $entry Atom entry to poke at
|
||||
* @param DOMElement $feed Atom feed, for context
|
||||
*/
|
||||
|
||||
function __construct($entry, $feed = null)
|
||||
{
|
||||
$this->entry = $entry;
|
||||
$this->feed = $feed;
|
||||
|
||||
$pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM);
|
||||
|
||||
if (!empty($pubEl)) {
|
||||
$this->time = strtotime($pubEl->textContent);
|
||||
} else {
|
||||
// XXX technically an error; being liberal. Good idea...?
|
||||
$updateEl = $this->_child($entry, self::UPDATED, self::ATOM);
|
||||
if (!empty($updateEl)) {
|
||||
$this->time = strtotime($updateEl->textContent);
|
||||
} else {
|
||||
$this->time = null;
|
||||
}
|
||||
}
|
||||
|
||||
$this->link = ActivityUtils::getLink($entry);
|
||||
|
||||
$verbEl = $this->_child($entry, self::VERB);
|
||||
|
||||
if (!empty($verbEl)) {
|
||||
$this->verb = trim($verbEl->textContent);
|
||||
} else {
|
||||
$this->verb = ActivityVerb::POST;
|
||||
// XXX: do other implied stuff here
|
||||
}
|
||||
|
||||
$objectEl = $this->_child($entry, self::OBJECT);
|
||||
|
||||
if (!empty($objectEl)) {
|
||||
$this->object = new ActivityObject($objectEl);
|
||||
} else {
|
||||
$this->object = new ActivityObject($entry);
|
||||
}
|
||||
|
||||
$actorEl = $this->_child($entry, self::ACTOR);
|
||||
|
||||
if (!empty($actorEl)) {
|
||||
|
||||
$this->actor = new ActivityObject($actorEl);
|
||||
|
||||
} else if (!empty($feed) &&
|
||||
$subjectEl = $this->_child($feed, self::SUBJECT)) {
|
||||
|
||||
$this->actor = new ActivityObject($subjectEl);
|
||||
|
||||
} else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) {
|
||||
|
||||
$this->actor = new ActivityObject($authorEl);
|
||||
|
||||
} else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR,
|
||||
self::ATOM)) {
|
||||
|
||||
$this->actor = new ActivityObject($authorEl);
|
||||
}
|
||||
|
||||
$contextEl = $this->_child($entry, self::CONTEXT);
|
||||
|
||||
if (!empty($contextEl)) {
|
||||
$this->context = new ActivityObject($contextEl);
|
||||
}
|
||||
|
||||
$targetEl = $this->_child($entry, self::TARGET);
|
||||
|
||||
if (!empty($targetEl)) {
|
||||
$this->target = new ActivityObject($targetEl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an Atom <entry> based on this activity
|
||||
*
|
||||
* @return DOMElement Atom entry
|
||||
*/
|
||||
|
||||
function toAtomEntry()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first child element with the given tag
|
||||
*
|
||||
* @param DOMElement $element element to pick at
|
||||
* @param string $tag tag to look for
|
||||
* @param string $namespace Namespace to look under
|
||||
*
|
||||
* @return DOMElement found element or null
|
||||
*/
|
||||
|
||||
private function _child($element, $tag, $namespace=self::SPEC)
|
||||
{
|
||||
$els = $element->getElementsByTagnameNS($namespace, $tag);
|
||||
|
||||
if (empty($els) || $els->length == 0) {
|
||||
return null;
|
||||
} else {
|
||||
return $els->item(0);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
@ -209,6 +223,7 @@ class FeedMunger
|
||||
$notice->id = -1;
|
||||
} else {
|
||||
$notice = new Notice();
|
||||
$notice->profile_id = $this->profileIdForEntry($index);
|
||||
}
|
||||
|
||||
$link = $this->getAltLink($entry);
|
||||
@ -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;
|
||||
|
@ -38,6 +38,7 @@ class HubDistribQueueHandler extends QueueHandler
|
||||
foreach ($notice->getGroups() as $group) {
|
||||
$this->pushGroup($notice, $group->group_id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function pushUser($notice)
|
||||
@ -48,14 +49,7 @@ class HubDistribQueueHandler extends QueueHandler
|
||||
$feed = common_local_url('ApiTimelineUser',
|
||||
array('id' => $notice->profile_id,
|
||||
'format' => 'atom'));
|
||||
$sub = new HubSub();
|
||||
$sub->topic = $feed;
|
||||
if ($sub->find()) {
|
||||
$atom = $this->userFeedForNotice($notice);
|
||||
$this->pushFeeds($atom, $sub);
|
||||
} else {
|
||||
common_log(LOG_INFO, "No PuSH subscribers for $feed");
|
||||
}
|
||||
$this->pushFeed($feed, array($this, 'userFeedForNotice'), $notice);
|
||||
}
|
||||
|
||||
function pushGroup($notice, $group_id)
|
||||
@ -63,19 +57,69 @@ class HubDistribQueueHandler extends QueueHandler
|
||||
$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, "Building PuSH feed for $feed");
|
||||
$atom = $this->groupFeedForNotice($group_id, $notice);
|
||||
$this->pushFeeds($atom, $sub);
|
||||
$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;
|
||||
}
|
||||
|
||||
|
||||
function pushFeeds($atom, $sub)
|
||||
/**
|
||||
* 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();
|
||||
|
@ -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;
|
||||
|
147
plugins/OStatus/tests/ActivityParseTests.php
Normal file
147
plugins/OStatus/tests/ActivityParseTests.php
Normal file
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
|
||||
print "This script must be run from the command line\n";
|
||||
exit();
|
||||
}
|
||||
|
||||
// XXX: we should probably have some common source for this stuff
|
||||
|
||||
define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
|
||||
define('STATUSNET', true);
|
||||
|
||||
require_once INSTALLDIR . '/lib/common.php';
|
||||
require_once INSTALLDIR . '/plugins/OStatus/lib/activity.php';
|
||||
|
||||
class ActivityParseTests extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testExample1()
|
||||
{
|
||||
global $_example1;
|
||||
$dom = DOMDocument::loadXML($_example1);
|
||||
$act = new Activity($dom->documentElement);
|
||||
|
||||
$this->assertFalse(empty($act));
|
||||
$this->assertEquals($act->time, 1243860840);
|
||||
$this->assertEquals($act->verb, ActivityVerb::POST);
|
||||
}
|
||||
|
||||
public function testExample3()
|
||||
{
|
||||
global $_example3;
|
||||
$dom = DOMDocument::loadXML($_example3);
|
||||
|
||||
$feed = $dom->documentElement;
|
||||
|
||||
$entries = $feed->getElementsByTagName('entry');
|
||||
|
||||
$entry = $entries->item(0);
|
||||
|
||||
$act = new Activity($entry, $feed);
|
||||
|
||||
$this->assertFalse(empty($act));
|
||||
$this->assertEquals($act->time, 1071340202);
|
||||
$this->assertEquals($act->link, 'http://example.org/2003/12/13/atom03.html');
|
||||
|
||||
$this->assertEquals($act->verb, ActivityVerb::POST);
|
||||
|
||||
$this->assertFalse(empty($act->actor));
|
||||
$this->assertEquals($act->actor->type, ActivityObject::PERSON);
|
||||
$this->assertEquals($act->actor->title, 'John Doe');
|
||||
$this->assertEquals($act->actor->id, 'mailto:johndoe@example.com');
|
||||
|
||||
$this->assertFalse(empty($act->object));
|
||||
$this->assertEquals($act->object->type, ActivityObject::NOTE);
|
||||
$this->assertEquals($act->object->id, 'urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a');
|
||||
$this->assertEquals($act->object->title, 'Atom-Powered Robots Run Amok');
|
||||
$this->assertEquals($act->object->summary, 'Some text.');
|
||||
$this->assertEquals($act->object->link, 'http://example.org/2003/12/13/atom03.html');
|
||||
|
||||
$this->assertTrue(empty($act->context));
|
||||
$this->assertTrue(empty($act->target));
|
||||
|
||||
$this->assertEquals($act->entry, $entry);
|
||||
$this->assertEquals($act->feed, $feed);
|
||||
}
|
||||
}
|
||||
|
||||
$_example1 = <<<EXAMPLE1
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'>
|
||||
<id>tag:versioncentral.example.org,2009:/commit/1643245</id>
|
||||
<published>2009-06-01T12:54:00Z</published>
|
||||
<title>Geraldine committed a change to yate</title>
|
||||
<content type="xhtml">Geraldine just committed a change to yate on VersionCentral</content>
|
||||
<link rel="alternate" type="text/html"
|
||||
href="http://versioncentral.example.org/geraldine/yate/commit/1643245" />
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
|
||||
<activity:verb>http://versioncentral.example.org/activity/commit</activity:verb>
|
||||
<activity:object>
|
||||
<activity:object-type>http://versioncentral.example.org/activity/changeset</activity:object-type>
|
||||
<id>tag:versioncentral.example.org,2009:/change/1643245</id>
|
||||
<title>Punctuation Changeset</title>
|
||||
<summary>Fixing punctuation because it makes it more readable.</summary>
|
||||
<link rel="alternate" type="text/html" href="..." />
|
||||
</activity:object>
|
||||
</entry>
|
||||
EXAMPLE1;
|
||||
|
||||
$_example2 = <<<EXAMPLE2
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'>
|
||||
<id>tag:photopanic.example.com,2008:activity01</id>
|
||||
<title>Geraldine posted a Photo on PhotoPanic</title>
|
||||
<published>2008-11-02T15:29:00Z</published>
|
||||
<link rel="alternate" type="text/html" href="/geraldine/activities/1" />
|
||||
<activity:verb>
|
||||
http://activitystrea.ms/schema/1.0/post
|
||||
</activity:verb>
|
||||
<activity:object>
|
||||
<id>tag:photopanic.example.com,2008:photo01</id>
|
||||
<title>My Cat</title>
|
||||
<published>2008-11-02T15:29:00Z</published>
|
||||
<link rel="alternate" type="text/html" href="/geraldine/photos/1" />
|
||||
<activity:object-type>
|
||||
tag:atomactivity.example.com,2008:photo
|
||||
</activity:object-type>
|
||||
<source>
|
||||
<title>Geraldine's Photos</title>
|
||||
<link rel="self" type="application/atom+xml" href="/geraldine/photofeed.xml" />
|
||||
<link rel="alternate" type="text/html" href="/geraldine/" />
|
||||
</source>
|
||||
</activity:object>
|
||||
<content type="html">
|
||||
<p>Geraldine posted a Photo on PhotoPanic</p>
|
||||
<img src="/geraldine/photo1.jpg">
|
||||
</content>
|
||||
</entry>
|
||||
EXAMPLE2;
|
||||
|
||||
$_example3 = <<<EXAMPLE3
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
|
||||
<title>Example Feed</title>
|
||||
<subtitle>A subtitle.</subtitle>
|
||||
<link href="http://example.org/feed/" rel="self" />
|
||||
<link href="http://example.org/" />
|
||||
<id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id>
|
||||
<updated>2003-12-13T18:30:02Z</updated>
|
||||
<author>
|
||||
<name>John Doe</name>
|
||||
<email>johndoe@example.com</email>
|
||||
</author>
|
||||
|
||||
<entry>
|
||||
<title>Atom-Powered Robots Run Amok</title>
|
||||
<link href="http://example.org/2003/12/13/atom03" />
|
||||
<link rel="alternate" type="text/html" href="http://example.org/2003/12/13/atom03.html"/>
|
||||
<link rel="edit" href="http://example.org/2003/12/13/atom03/edit"/>
|
||||
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
|
||||
<updated>2003-12-13T18:30:02Z</updated>
|
||||
<summary>Some text.</summary>
|
||||
</entry>
|
||||
|
||||
</feed>
|
||||
EXAMPLE3;
|
30
plugins/OStatus/theme/base/css/ostatus.css
Normal file
30
plugins/OStatus/theme/base/css/ostatus.css
Normal file
@ -0,0 +1,30 @@
|
||||
/** theme: base for OStatus
|
||||
*
|
||||
* @package StatusNet
|
||||
* @author Sarven Capadisli <csarven@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/
|
||||
*/
|
||||
|
||||
#form_ostatus_connect.dialogbox {
|
||||
width:70%;
|
||||
background-image:none;
|
||||
}
|
||||
#form_ostatus_connect.dialogbox .form_data label {
|
||||
width:34%;
|
||||
}
|
||||
#form_ostatus_connect.dialogbox .form_data input {
|
||||
width:57%;
|
||||
}
|
||||
#form_ostatus_connect.dialogbox .form_data .form_guide {
|
||||
margin-left:36%;
|
||||
}
|
||||
|
||||
#form_ostatus_connect.dialogbox #ostatus_nickname {
|
||||
display:none;
|
||||
}
|
||||
|
||||
#form_ostatus_connect.dialogbox .submit_dialogbox {
|
||||
min-width:96px;
|
||||
}
|
@ -45,6 +45,7 @@ class PoweredByStatusNetPlugin extends Plugin
|
||||
{
|
||||
function onEndAddressData($action)
|
||||
{
|
||||
$action->text(' ');
|
||||
$action->elementStart('span', 'poweredby');
|
||||
$action->raw(sprintf(_m('powered by %s'),
|
||||
sprintf('<a href="http://status.net/">%s</a>',
|
||||
|
@ -288,7 +288,7 @@ margin-left:18px;
|
||||
}
|
||||
#site_nav_global_primary li {
|
||||
display:inline;
|
||||
margin-left:11px;
|
||||
margin-left:18px;
|
||||
}
|
||||
|
||||
.system_notice dt {
|
||||
@ -370,7 +370,7 @@ margin-bottom:11px;
|
||||
|
||||
#site_nav_global_secondary ul li {
|
||||
display:inline;
|
||||
margin-right:11px;
|
||||
margin-right:18px;
|
||||
}
|
||||
#export_data li a {
|
||||
padding-left:20px;
|
||||
@ -383,15 +383,13 @@ padding-left:28px;
|
||||
}
|
||||
|
||||
#export_data ul {
|
||||
display:inline;
|
||||
width:100%;
|
||||
float:left;
|
||||
}
|
||||
#export_data li {
|
||||
list-style-type:none;
|
||||
display:inline;
|
||||
margin-left:11px;
|
||||
}
|
||||
#export_data li:first-child {
|
||||
margin-left:0;
|
||||
float:left;
|
||||
margin-right:11px;
|
||||
}
|
||||
|
||||
#licenses {
|
||||
@ -801,8 +799,8 @@ list-style-type:none;
|
||||
display:inline;
|
||||
}
|
||||
.entity_tags li {
|
||||
display:inline;
|
||||
margin-right:4px;
|
||||
float:left;
|
||||
margin-right:11px;
|
||||
}
|
||||
|
||||
.aside .section {
|
||||
@ -820,6 +818,7 @@ font-size:1em;
|
||||
#entity_statistics dt,
|
||||
#entity_statistics dd {
|
||||
display:inline;
|
||||
margin-right:11px;
|
||||
}
|
||||
#entity_statistics dt:after {
|
||||
content: ":";
|
||||
@ -1104,25 +1103,22 @@ left:0;
|
||||
|
||||
.dialogbox {
|
||||
position:absolute;
|
||||
top:-4px;
|
||||
right:29px;
|
||||
top:-1px;
|
||||
right:-1px;
|
||||
z-index:9;
|
||||
min-width:199px;
|
||||
float:none;
|
||||
background-color:#FFF;
|
||||
padding:11px;
|
||||
border-radius:7px;
|
||||
-moz-border-radius:7px;
|
||||
-webkit-border-radius:7px;
|
||||
border-style:solid;
|
||||
border-width:1px;
|
||||
border-color:#DDDDDD;
|
||||
-moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
|
||||
}
|
||||
|
||||
.dialogbox legend {
|
||||
display:block !important;
|
||||
margin-right:18px;
|
||||
margin-bottom:18px;
|
||||
}
|
||||
|
||||
.dialogbox button.close {
|
||||
@ -1131,11 +1127,22 @@ right:3px;
|
||||
top:3px;
|
||||
}
|
||||
|
||||
.dialogbox .form_guide {
|
||||
font-weight:normal;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
.dialogbox .submit_dialogbox {
|
||||
font-weight:bold;
|
||||
text-indent:0;
|
||||
min-width:46px;
|
||||
}
|
||||
.dialogbox input {
|
||||
padding-left:4px;
|
||||
}
|
||||
.dialogbox fieldset {
|
||||
margin-bottom:0;
|
||||
}
|
||||
|
||||
#wrap form.processing input.submit,
|
||||
.entity_actions a.processing,
|
||||
@ -1145,6 +1152,12 @@ outline:none;
|
||||
text-indent:-9999px;
|
||||
}
|
||||
|
||||
.form_repeat.dialogbox {
|
||||
top:-4px;
|
||||
right:29px;
|
||||
min-width:199px;
|
||||
}
|
||||
|
||||
.notice-options {
|
||||
position:relative;
|
||||
font-size:0.95em;
|
||||
@ -1482,6 +1495,11 @@ display:inline;
|
||||
margin-right:7px;
|
||||
line-height:1.25;
|
||||
}
|
||||
|
||||
.tag-cloud li:before {
|
||||
content:'\0009';
|
||||
}
|
||||
|
||||
.aside .tag-cloud li {
|
||||
line-height:1.5;
|
||||
}
|
||||
|
@ -30,7 +30,9 @@ border-radius:4px;
|
||||
input, textarea, select, option {
|
||||
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
|
||||
}
|
||||
input, textarea, select {
|
||||
input, textarea, select,
|
||||
.entity_actions .dialogbox input,
|
||||
.mark-top {
|
||||
border-color:#AAAAAA;
|
||||
}
|
||||
|
||||
@ -46,7 +48,8 @@ box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3);
|
||||
.pagination .nav_prev a,
|
||||
.pagination .nav_next a,
|
||||
.form_settings fieldset fieldset,
|
||||
.entity_moderation:hover ul {
|
||||
.entity_moderation:hover ul,
|
||||
.dialogbox {
|
||||
border-color:#DDDDDD;
|
||||
}
|
||||
|
||||
@ -78,7 +81,8 @@ background-color:transparent;
|
||||
input:focus, textarea:focus, select:focus,
|
||||
.form_notice.warning #notice_data-text,
|
||||
.form_notice.warning #notice_text-count,
|
||||
.form_settings .form_note {
|
||||
.form_settings .form_note,
|
||||
.entity_actions .dialogbox .form_data input:focus {
|
||||
border-color:#9BB43E;
|
||||
}
|
||||
input.submit {
|
||||
@ -133,9 +137,6 @@ color:#002FA7;
|
||||
#content tbody tr {
|
||||
border-top-color:#C8D1D5;
|
||||
}
|
||||
.mark-top {
|
||||
border-color:#AAAAAA;
|
||||
}
|
||||
|
||||
#aside_primary {
|
||||
background-color:#C8D1D5;
|
||||
@ -144,7 +145,9 @@ background-color:#C8D1D5;
|
||||
#notice_text-count {
|
||||
color:#333333;
|
||||
}
|
||||
.form_notice.warning #notice_text-count {
|
||||
.form_notice.warning #notice_text-count,
|
||||
.dialogbox,
|
||||
.entity_actions .dialogbox input {
|
||||
color:#000000;
|
||||
}
|
||||
.form_notice label[for=notice_data-attach] {
|
||||
@ -221,7 +224,8 @@ border-color:transparent;
|
||||
#content,
|
||||
#site_nav_local_views .current a,
|
||||
.entity_send-a-message .form_notice,
|
||||
.entity_moderation:hover ul {
|
||||
.entity_moderation:hover ul,
|
||||
.dialogbox {
|
||||
background-color:#FFFFFF;
|
||||
}
|
||||
|
||||
@ -308,7 +312,8 @@ background-position: 5px -718px;
|
||||
background-position: 5px -852px;
|
||||
}
|
||||
.entity_send-a-message .form_notice,
|
||||
.entity_moderation:hover ul {
|
||||
.entity_moderation:hover ul,
|
||||
.dialogbox {
|
||||
box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
|
||||
-moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
|
||||
-webkit-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
|
||||
|
@ -30,7 +30,9 @@ border-radius:4px;
|
||||
input, textarea, select, option {
|
||||
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
|
||||
}
|
||||
input, textarea, select {
|
||||
input, textarea, select,
|
||||
.entity_actions .dialogbox input,
|
||||
.mark-top {
|
||||
border-color:#AAAAAA;
|
||||
}
|
||||
|
||||
@ -46,7 +48,8 @@ box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3);
|
||||
.pagination .nav_prev a,
|
||||
.pagination .nav_next a,
|
||||
.form_settings fieldset fieldset,
|
||||
.entity_moderation:hover ul {
|
||||
.entity_moderation:hover ul,
|
||||
.dialogbox {
|
||||
border-color:#DDDDDD;
|
||||
}
|
||||
|
||||
@ -88,6 +91,7 @@ color:#FFFFFF;
|
||||
border-color:transparent;
|
||||
text-shadow:none;
|
||||
}
|
||||
|
||||
.dialogbox .submit_dialogbox,
|
||||
input.submit,
|
||||
.form_notice input.submit {
|
||||
@ -133,9 +137,6 @@ color:#002FA7;
|
||||
#content tbody tr {
|
||||
border-top-color:#CEE1E9;
|
||||
}
|
||||
.mark-top {
|
||||
border-color:#AAAAAA;
|
||||
}
|
||||
|
||||
#aside_primary {
|
||||
background-color:#CEE1E9;
|
||||
@ -144,7 +145,9 @@ background-color:#CEE1E9;
|
||||
#notice_text-count {
|
||||
color:#333333;
|
||||
}
|
||||
.form_notice.warning #notice_text-count {
|
||||
.form_notice.warning #notice_text-count,
|
||||
.dialogbox,
|
||||
.entity_actions .dialogbox input {
|
||||
color:#000000;
|
||||
}
|
||||
.form_notice label[for=notice_data-attach] {
|
||||
@ -221,7 +224,8 @@ border-color:transparent;
|
||||
#content,
|
||||
#site_nav_local_views .current a,
|
||||
.entity_send-a-message .form_notice,
|
||||
.entity_moderation:hover ul {
|
||||
.entity_moderation:hover ul,
|
||||
.dialogbox {
|
||||
background-color:#FFFFFF;
|
||||
}
|
||||
|
||||
@ -307,7 +311,8 @@ background-position: 5px -718px;
|
||||
background-position: 5px -852px;
|
||||
}
|
||||
.entity_send-a-message .form_notice,
|
||||
.entity_moderation:hover ul {
|
||||
.entity_moderation:hover ul,
|
||||
.dialogbox {
|
||||
box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
|
||||
-moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
|
||||
-webkit-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
|
||||
|
Loading…
Reference in New Issue
Block a user