Merge branch 'testing' of git@gitorious.org:statusnet/mainline into testing

This commit is contained in:
Zach Copley 2010-02-15 21:14:32 +00:00
commit 5db40c440d
62 changed files with 3024 additions and 718 deletions

View File

@ -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 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 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 StartProfilePageProfileSection: Starting to show the section of the
profile page with the actual profile data; profile page with the actual profile data;
hook to prevent showing the profile (e.g.) hook to prevent showing the profile (e.g.)

19
README
View File

@ -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 typically only make 2 connections to a single server at a
time <http://ur1.ca/6ih>, so this can parallelize the job. time <http://ur1.ca/6ih>, so this can parallelize the job.
Defaults to null. Defaults to null.
ssl: Whether to access avatars using HTTPS. Defaults to null, meaning
to guess based on site-wide SSL settings.
public 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 (using version numbers as the path) to make sure that all files are
reloaded by caching clients or proxies. Defaults to null, reloaded by caching clients or proxies. Defaults to null,
which means to use the site path + '/theme'. 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 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. a virtual server here can speed up Web performance.
path: URL path, relative to the server, to find files. Defaults to path: URL path, relative to the server, to find files. Defaults to
main path + '/file/'. 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 filecommand: command to use for determining the type of a file. May be
skipped if fileinfo extension is installed. Defaults to skipped if fileinfo extension is installed. Defaults to
'/usr/bin/file'. '/usr/bin/file'.
@ -1506,6 +1523,8 @@ dir: directory to write backgrounds too. Default is '/background/'
subdir of install dir. subdir of install dir.
path: path to backgrounds. Default is sub-path of install path; note 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. 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 ping
---- ----

View File

@ -112,32 +112,69 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction
$taguribase = common_config('integration', 'taguri'); $taguribase = common_config('integration', 'taguri');
$id = "tag:$taguribase:Favorites:" . $this->user->id; $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.'), _('%1$s updates favorited by %2$s / %2$s.'),
$sitename, $sitename,
$profile->getBestName(), $profile->getBestName(),
$this->user->nickname $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) { switch($this->format) {
case 'xml': case 'xml':
$this->showXmlTimeline($this->notices); $this->showXmlTimeline($this->notices);
break; break;
case 'rss': 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; break;
case 'atom': case 'atom':
$selfuri = common_root_url() .
ltrim($_SERVER['QUERY_STRING'], 'p='); header('Content-Type: application/atom+xml; charset=utf-8');
$this->showAtomTimeline(
$this->notices, $title, $id, $link, $subtitle, $atom = new AtomNoticeFeed();
null, $selfuri, $logo
$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; break;
case 'json': case 'json':
$this->showJsonTimeline($this->notices); $this->showJsonTimeline($this->notices);

View File

@ -114,39 +114,71 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction
$title = sprintf(_("%s and friends"), $this->user->nickname); $title = sprintf(_("%s and friends"), $this->user->nickname);
$taguribase = common_config('integration', 'taguri'); $taguribase = common_config('integration', 'taguri');
$id = "tag:$taguribase:FriendsTimeline:" . $this->user->id; $id = "tag:$taguribase:FriendsTimeline:" . $this->user->id;
$link = common_local_url(
'all', array('nickname' => $this->user->nickname)
);
$subtitle = sprintf( $subtitle = sprintf(
_('Updates from %1$s and friends on %2$s!'), _('Updates from %1$s and friends on %2$s!'),
$this->user->nickname, $sitename $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) { switch($this->format) {
case 'xml': case 'xml':
$this->showXmlTimeline($this->notices); $this->showXmlTimeline($this->notices);
break; break;
case 'rss': 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; break;
case 'atom': case 'atom':
$target_id = $this->arg('id'); header('Content-Type: application/atom+xml; charset=utf-8');
if (isset($target_id)) { $atom = new AtomNoticeFeed();
$selfuri = common_root_url() .
'api/statuses/friends_timeline/' . $atom->setId($id);
$target_id . '.atom'; $atom->setTitle($title);
} else { $atom->setSubtitle($subtitle);
$selfuri = common_root_url() . $atom->setLogo($logo);
'api/statuses/friends_timeline.atom'; $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( $atom->addLink(
$this->notices, $title, $id, $link, $this->getSelfUri('ApiTimelineFriends', $aargs),
$subtitle, null, $selfuri, $logo array('rel' => 'self', 'type' => 'application/atom+xml')
); );
$atom->addEntryFromNotices($this->notices);
$this->raw($atom->getString());
break; break;
case 'json': case 'json':
$this->showJsonTimeline($this->notices); $this->showJsonTimeline($this->notices);

View File

@ -109,15 +109,13 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction
$title = sprintf(_("%s timeline"), $this->group->nickname); $title = sprintf(_("%s timeline"), $this->group->nickname);
$taguribase = common_config('integration', 'taguri'); $taguribase = common_config('integration', 'taguri');
$id = "tag:$taguribase:GroupTimeline:" . $this->group->id; $id = "tag:$taguribase:GroupTimeline:" . $this->group->id;
$link = common_local_url(
'showgroup',
array('nickname' => $this->group->nickname)
);
$subtitle = sprintf( $subtitle = sprintf(
_('Updates from %1$s on %2$s!'), _('Updates from %1$s on %2$s!'),
$this->group->nickname, $this->group->nickname,
$sitename $sitename
); );
$logo = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE); $logo = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE);
switch($this->format) { switch($this->format) {
@ -125,22 +123,68 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction
$this->showXmlTimeline($this->notices); $this->showXmlTimeline($this->notices);
break; break;
case 'rss': case 'rss':
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo); $this->showRssTimeline(
break;
case 'atom':
$selfuri = common_root_url() .
'api/statusnet/groups/timeline/' .
$this->group->id . '.atom';
$this->showAtomTimeline(
$this->notices, $this->notices,
$title, $title,
$id, $this->group->homeUrl(),
$link,
$subtitle, $subtitle,
null, null,
$selfuri,
$logo $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; break;
case 'json': case 'json':
$this->showJsonTimeline($this->notices); $this->showJsonTimeline($this->notices);

View File

@ -115,39 +115,67 @@ class ApiTimelineHomeAction extends ApiBareAuthAction
$title = sprintf(_("%s and friends"), $this->user->nickname); $title = sprintf(_("%s and friends"), $this->user->nickname);
$taguribase = common_config('integration', 'taguri'); $taguribase = common_config('integration', 'taguri');
$id = "tag:$taguribase:HomeTimeline:" . $this->user->id; $id = "tag:$taguribase:HomeTimeline:" . $this->user->id;
$link = common_local_url(
'all', array('nickname' => $this->user->nickname)
);
$subtitle = sprintf( $subtitle = sprintf(
_('Updates from %1$s and friends on %2$s!'), _('Updates from %1$s and friends on %2$s!'),
$this->user->nickname, $sitename $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) { switch($this->format) {
case 'xml': case 'xml':
$this->showXmlTimeline($this->notices); $this->showXmlTimeline($this->notices);
break; break;
case 'rss': 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; break;
case 'atom': case 'atom':
$target_id = $this->arg('id'); header('Content-Type: application/atom+xml; charset=utf-8');
if (isset($target_id)) { $atom = new AtomNoticeFeed();
$selfuri = common_root_url() .
'api/statuses/home_timeline/' . $atom->setId($id);
$target_id . '.atom'; $atom->setTitle($title);
} else { $atom->setSubtitle($subtitle);
$selfuri = common_root_url() . $atom->setLogo($logo);
'api/statuses/home_timeline.atom'; $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( $atom->addLink(
$this->notices, $title, $id, $link, $this->getSelfUri('ApiTimelineHome', $aargs),
$subtitle, null, $selfuri, $logo array('rel' => 'self', 'type' => 'application/atom+xml')
); );
$atom->addEntryFromNotices($this->notices);
$this->raw($atom->getString());
break; break;
case 'json': case 'json':
$this->showJsonTimeline($this->notices); $this->showJsonTimeline($this->notices);

View File

@ -137,12 +137,36 @@ class ApiTimelineMentionsAction extends ApiBareAuthAction
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo); $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
break; break;
case 'atom': case 'atom':
$selfuri = common_root_url() .
ltrim($_SERVER['QUERY_STRING'], 'p='); $atom = new AtomNoticeFeed();
$this->showAtomTimeline(
$this->notices, $title, $id, $link, $subtitle, $atom->setId($id);
null, $selfuri, $logo $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; break;
case 'json': case 'json':
$this->showJsonTimeline($this->notices); $this->showJsonTimeline($this->notices);

View File

@ -75,6 +75,10 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
$this->notices = $this->getNotices(); $this->notices = $this->getNotices();
if ($this->since) {
throw new ServerException("since parameter is disabled for performance; use since_id", 403);
}
return true; return true;
} }
@ -118,11 +122,28 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo); $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo);
break; break;
case 'atom': case 'atom':
$selfuri = common_root_url() . 'api/statuses/public_timeline.atom';
$this->showAtomTimeline( $atom = new AtomNoticeFeed();
$this->notices, $title, $id, $link,
$subtitle, null, $selfuri, $sitelogo $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; break;
case 'json': case 'json':
$this->showJsonTimeline($this->notices); $this->showJsonTimeline($this->notices);
@ -145,7 +166,7 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
$notice = Notice::publicStream( $notice = Notice::publicStream(
($this->page - 1) * $this->count, $this->count, $this->since_id, ($this->page - 1) * $this->count, $this->count, $this->since_id,
$this->max_id, $this->since $this->max_id
); );
while ($notice->fetch()) { while ($notice->fetch()) {

View File

@ -99,6 +99,8 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction
$strm = $this->auth_user->repeatsOfMe($offset, $limit, $this->since_id, $this->max_id); $strm = $this->auth_user->repeatsOfMe($offset, $limit, $this->since_id, $this->max_id);
common_debug(var_export($strm, true));
switch ($this->format) { switch ($this->format) {
case 'xml': case 'xml':
$this->showXmlTimeline($strm); $this->showXmlTimeline($strm);
@ -112,10 +114,38 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction
$title = sprintf(_("Repeats of %s"), $this->auth_user->nickname); $title = sprintf(_("Repeats of %s"), $this->auth_user->nickname);
$taguribase = common_config('integration', 'taguri'); $taguribase = common_config('integration', 'taguri');
$id = "tag:$taguribase:RepeatsOfMe:" . $this->auth_user->id; $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; break;
default: default:

View File

@ -100,10 +100,6 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction
$sitename = common_config('site', 'name'); $sitename = common_config('site', 'name');
$sitelogo = (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png'); $sitelogo = (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png');
$title = sprintf(_("Notices tagged with %s"), $this->tag); $title = sprintf(_("Notices tagged with %s"), $this->tag);
$link = common_local_url(
'tag',
array('tag' => $this->tag)
);
$subtitle = sprintf( $subtitle = sprintf(
_('Updates tagged with %1$s on %2$s!'), _('Updates tagged with %1$s on %2$s!'),
$this->tag, $this->tag,
@ -117,22 +113,51 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction
$this->showXmlTimeline($this->notices); $this->showXmlTimeline($this->notices);
break; break;
case 'rss': case 'rss':
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo); $link = common_local_url(
break; 'tag',
case 'atom': array('tag' => $this->tag)
$selfuri = common_root_url() . );
'api/statusnet/tags/timeline/' . $this->showRssTimeline(
$this->tag . '.atom';
$this->showAtomTimeline(
$this->notices, $this->notices,
$title, $title,
$id,
$link, $link,
$subtitle, $subtitle,
null, null,
$selfuri,
$sitelogo $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; break;
case 'json': case 'json':
$this->showJsonTimeline($this->notices); $this->showJsonTimeline($this->notices);

View File

@ -145,19 +145,59 @@ class ApiTimelineUserAction extends ApiBareAuthAction
); );
break; break;
case 'atom': 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'); $id = $this->arg('id');
if ($id) {
$selfuri = common_root_url() . if (strval(intval($id)) === strval($id)) {
'api/statuses/user_timeline/' . $atom = new AtomUserNoticeFeed($this->user);
rawurlencode($id) . '.atom';
} else { } else {
$selfuri = common_root_url() . $atom = new AtomUserNoticeFeed();
'api/statuses/user_timeline.atom';
} }
$this->showAtomTimeline(
$this->notices, $title, $id, $link, $atom->setId($id);
$subtitle, $suplink, $selfuri, $logo $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; break;
case 'json': case 'json':
$this->showJsonTimeline($this->notices); $this->showJsonTimeline($this->notices);

View File

@ -82,9 +82,20 @@ class Avatar extends Memcached_DataObject
$server = common_config('site', 'server'); $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() function displayUrl()

View File

@ -155,9 +155,20 @@ class Design extends Memcached_DataObject
$server = common_config('site', 'server'); $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) function setDisposition($on, $off, $tile)

View File

@ -228,9 +228,20 @@ class File extends Memcached_DataObject
$server = common_config('site', 'server'); $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;
} }
} }

View File

@ -22,4 +22,19 @@ class Nonce extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */ /* the code above is auto generated do not remove the tag below */
###END_AUTOCODE ###END_AUTOCODE
/**
* Compatibility hack for PHP 5.3
*
* The statusnet.links.ini entry cannot be read because "," is no longer
* allowed in key names when read by parse_ini_file().
*
* @return array
* @access public
*/
function links()
{
return array('consumer_key,token' => 'token:consumer_key,token');
}
} }

View File

@ -957,7 +957,10 @@ class Notice extends Memcached_DataObject
if ($namespace) { if ($namespace) {
$attrs = array('xmlns' => 'http://www.w3.org/2005/Atom', $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 { } else {
$attrs = array(); $attrs = array();
} }
@ -983,11 +986,6 @@ class Notice extends Memcached_DataObject
$xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE)); $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) { if ($source) {
$xs->elementEnd('source'); $xs->elementEnd('source');
} }
@ -995,6 +993,9 @@ class Notice extends Memcached_DataObject
$xs->element('title', null, $this->content); $xs->element('title', null, $this->content);
$xs->element('summary', null, $this->content); $xs->element('summary', null, $this->content);
$xs->raw($profile->asAtomAuthor());
$xs->raw($profile->asActivityActor());
$xs->element('link', array('rel' => 'alternate', $xs->element('link', array('rel' => 'alternate',
'href' => $this->bestUrl())); '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); $xs->element('content', array('type' => 'html'), $this->rendered);
$tag = new Notice_tag(); $tag = new Notice_tag();
@ -1041,9 +1079,7 @@ class Notice extends Memcached_DataObject
} }
if (!empty($this->lat) && !empty($this->lon)) { 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->element('georss:point', null, $this->lat . ' ' . $this->lon);
$xs->elementEnd('geo');
} }
$xs->elementEnd('entry'); $xs->elementEnd('entry');

View File

@ -754,4 +754,89 @@ class Profile extends Memcached_DataObject
return !empty($notice); return !empty($notice);
} }
/**
* Returns an XML string fragment with limited profile information
* as an Atom <author> element.
*
* Assumes that Atom has been previously set up as the base namespace.
*
* @return string
*/
function asAtomAuthor()
{
$xs = new XMLStringer(true);
$xs->elementStart('author');
$xs->element('name', null, $this->nickname);
$xs->element('uri', null, $this->profileurl);
$xs->elementEnd('author');
return $xs->getString();
}
/**
* Returns an XML string fragment with profile information as an
* Activity Streams <activity:actor> element.
*
* Assumes that 'activity' namespace has been previously defined.
*
* @return string
*/
function asActivityActor()
{
return $this->asActivityNoun('actor');
}
/**
* Returns an XML string fragment with profile information as an
* Activity Streams noun object with the given element type.
*
* Assumes that 'activity' namespace has been previously defined.
*
* @param string $element one of 'actor', 'subject', 'object', 'target'
* @return string
*/
function asActivityNoun($element)
{
$xs = new XMLStringer(true);
$xs->elementStart('activity:' . $element);
$xs->element(
'activity:object-type',
null,
'http://activitystrea.ms/schema/1.0/person'
);
$xs->element(
'id',
null,
common_local_url(
'userbyid',
array('id' => $this->id)
)
);
$xs->element('title', null, $this->getBestName());
$avatar = $this->getAvatar(AVATAR_PROFILE_SIZE);
$xs->element(
'link', array(
'type' => empty($avatar) ? 'image/png' : $avatar->mediatype,
'href' => empty($avatar)
? Avatar::defaultImage(AVATAR_PROFILE_SIZE)
: $avatar->displayUrl()
),
''
);
$xs->elementEnd('activity:' . $element);
return $xs->getString();
}
function getAcctUri()
{
return $this->nickname . '@' . common_config('site', 'server');
}
} }

View File

@ -355,6 +355,39 @@ class User_group extends Memcached_DataObject
return $xs->getString(); 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) { static function register($fields) {
// MAGICALLY put fields into current scope // MAGICALLY put fields into current scope

View File

@ -19,8 +19,11 @@ profile_id = profile:id
[token] [token]
consumer_key = consumer:consumer_key consumer_key = consumer:consumer_key
[nonce] ; Compatibility hack for PHP 5.3
consumer_key,token = token:consumer_key,token ; 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] [confirm_address]
user_id = user:id user_id = user:id

View File

@ -405,6 +405,7 @@ class Action extends HTMLOutputter // lawsuit
'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png'), 'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png'),
'alt' => common_config('site', 'name'))); 'alt' => common_config('site', 'name')));
} }
$this->text(' ');
$this->element('span', array('class' => 'fn org'), common_config('site', 'name')); $this->element('span', array('class' => 'fn org'), common_config('site', 'name'));
$this->elementEnd('a'); $this->elementEnd('a');
Event::handle('EndAddressData', array($this)); Event::handle('EndAddressData', array($this));
@ -822,12 +823,14 @@ class Action extends HTMLOutputter // lawsuit
'alt' => common_config('license', 'title'), 'alt' => common_config('license', 'title'),
'width' => '80', 'width' => '80',
'height' => '15')); 'height' => '15'));
$this->text(' ');
//TODO: This is dirty: i18n //TODO: This is dirty: i18n
$this->text(_('All '.common_config('site', 'name').' content and data are available under the ')); $this->text(_('All '.common_config('site', 'name').' content and data are available under the '));
$this->element('a', array('class' => 'license', $this->element('a', array('class' => 'license',
'rel' => 'external license', 'rel' => 'external license',
'href' => common_config('license', 'url')), 'href' => common_config('license', 'url')),
common_config('license', 'title')); common_config('license', 'title'));
$this->text(' ');
$this->text(_('license.')); $this->text(_('license.'));
$this->elementEnd('p'); $this->elementEnd('p');
break; break;

View File

@ -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'); $action = $this->trimmed('action');
@ -1154,7 +1154,6 @@ class ApiAction extends Action
$this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom', $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
'xml:lang' => 'en-US', 'xml:lang' => 'en-US',
'xmlns:thr' => 'http://purl.org/syndication/thread/1.0')); 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
Event::handle('StartApiAtom', array($this));
} }
function endTwitterAtom() 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
View File

@ -0,0 +1,106 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Class for building / manipulating an Atom entry in memory
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Feed
* @package StatusNet
* @author Zach Copley <zach@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET')
{
exit(1);
}
class Atom10EntryException extends Exception
{
}
/**
* Class for manipulating an Atom entry in memory. Get the entry as an XML
* string with Atom10Entry::getString().
*
* @category Feed
* @package StatusNet
* @author Zach Copley <zach@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class Atom10Entry extends XMLStringer
{
private $namespaces;
private $categories;
private $content;
private $contributors;
private $id;
private $links;
private $published;
private $rights;
private $source;
private $summary;
private $title;
function __construct($indent = true) {
parent::__construct($indent);
$this->namespaces = array();
}
function addNamespace($namespace, $uri)
{
$ns = array($namespace => $uri);
$this->namespaces = array_merge($this->namespaces, $ns);
}
function initEntry()
{
}
function endEntry()
{
}
/**
* Check that all required elements have been set, etc.
* Throws an Atom10EntryException if something's missing.
*
* @return void
*/
function validate
{
}
function getString()
{
$this->validate();
$this->initEntry();
$this->renderEntries();
$this->endEntry();
return $this->xw->outputMemory();
}
}

298
lib/atom10feed.php Normal file
View 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);
}
}

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

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

View File

@ -111,11 +111,13 @@ $default =
'avatar' => 'avatar' =>
array('server' => null, array('server' => null,
'dir' => INSTALLDIR . '/avatar/', 'dir' => INSTALLDIR . '/avatar/',
'path' => $_path . '/avatar/'), 'path' => $_path . '/avatar/',
'ssl' => null),
'background' => 'background' =>
array('server' => null, array('server' => null,
'dir' => INSTALLDIR . '/background/', 'dir' => INSTALLDIR . '/background/',
'path' => $_path . '/background/'), 'path' => $_path . '/background/',
'ssl' => null),
'public' => 'public' =>
array('localonly' => true, array('localonly' => true,
'blacklist' => array(), 'blacklist' => array(),
@ -123,10 +125,12 @@ $default =
'theme' => 'theme' =>
array('server' => null, array('server' => null,
'dir' => null, 'dir' => null,
'path'=> null), 'path'=> null,
'ssl' => null),
'javascript' => 'javascript' =>
array('server' => null, array('server' => null,
'path'=> null), 'path'=> null,
'ssl' => null),
'throttle' => 'throttle' =>
array('enabled' => false, // whether to throttle edits; false by default array('enabled' => false, // whether to throttle edits; false by default
'count' => 20, // number of allowed messages in timespan 'count' => 20, // number of allowed messages in timespan
@ -184,6 +188,7 @@ $default =
array('server' => null, array('server' => null,
'dir' => INSTALLDIR . '/file/', 'dir' => INSTALLDIR . '/file/',
'path' => $_path . '/file/', 'path' => $_path . '/file/',
'ssl' => null,
'supported' => array('image/png', 'supported' => array('image/png',
'image/jpeg', 'image/jpeg',
'image/gif', 'image/gif',

View File

@ -105,6 +105,7 @@ class GroupList extends Widget
'alt' => 'alt' =>
($this->group->fullname) ? $this->group->fullname : ($this->group->fullname) ? $this->group->fullname :
$this->group->nickname)); $this->group->nickname));
$this->out->text(' ');
$hasFN = ($this->group->fullname) ? 'nickname' : 'fn org nickname'; $hasFN = ($this->group->fullname) ? 'nickname' : 'fn org nickname';
$this->out->elementStart('span', $hasFN); $this->out->elementStart('span', $hasFN);
$this->out->raw($this->highlight($this->group->nickname)); $this->out->raw($this->highlight($this->group->nickname));
@ -112,16 +113,19 @@ class GroupList extends Widget
$this->out->elementEnd('a'); $this->out->elementEnd('a');
if ($this->group->fullname) { if ($this->group->fullname) {
$this->out->text(' ');
$this->out->elementStart('span', 'fn org'); $this->out->elementStart('span', 'fn org');
$this->out->raw($this->highlight($this->group->fullname)); $this->out->raw($this->highlight($this->group->fullname));
$this->out->elementEnd('span'); $this->out->elementEnd('span');
} }
if ($this->group->location) { if ($this->group->location) {
$this->out->text(' ');
$this->out->elementStart('span', 'label'); $this->out->elementStart('span', 'label');
$this->out->raw($this->highlight($this->group->location)); $this->out->raw($this->highlight($this->group->location));
$this->out->elementEnd('span'); $this->out->elementEnd('span');
} }
if ($this->group->homepage) { if ($this->group->homepage) {
$this->out->text(' ');
$this->out->elementStart('a', array('href' => $this->group->homepage, $this->out->elementStart('a', array('href' => $this->group->homepage,
'class' => 'url')); 'class' => 'url'));
$this->out->raw($this->highlight($this->group->homepage)); $this->out->raw($this->highlight($this->group->homepage));

View File

@ -85,9 +85,9 @@ class GroupSection extends Section
'href' => $group->homeUrl(), 'href' => $group->homeUrl(),
'rel' => 'contact group', 'rel' => 'contact group',
'class' => 'url')); 'class' => 'url'));
$this->out->text(' ');
$logo = ($group->stream_logo) ? $logo = ($group->stream_logo) ?
$group->stream_logo : User_group::defaultLogo(AVATAR_STREAM_SIZE); $group->stream_logo : User_group::defaultLogo(AVATAR_STREAM_SIZE);
$this->out->element('img', array('src' => $logo, $this->out->element('img', array('src' => $logo,
'width' => AVATAR_MINI_SIZE, 'width' => AVATAR_MINI_SIZE,
'height' => AVATAR_MINI_SIZE, 'height' => AVATAR_MINI_SIZE,
@ -95,6 +95,7 @@ class GroupSection extends Section
'alt' => ($group->fullname) ? 'alt' => ($group->fullname) ?
$group->fullname : $group->fullname :
$group->nickname)); $group->nickname));
$this->out->text(' ');
$this->out->element('span', 'fn org nickname', $group->nickname); $this->out->element('span', 'fn org nickname', $group->nickname);
$this->out->elementEnd('a'); $this->out->elementEnd('a');
$this->out->elementEnd('span'); $this->out->elementEnd('span');

View File

@ -376,9 +376,20 @@ class HTMLOutputter extends XMLOutputter
$server = common_config('site', 'server'); $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, $this->element('script', array('type' => $type,

View File

@ -294,6 +294,7 @@ class NoticeListItem extends Widget
} }
$this->out->elementStart('a', $attrs); $this->out->elementStart('a', $attrs);
$this->showAvatar(); $this->showAvatar();
$this->out->text(' ');
$this->showNickname(); $this->showNickname();
$this->out->elementEnd('a'); $this->out->elementEnd('a');
$this->out->elementEnd('span'); $this->out->elementEnd('span');
@ -432,8 +433,10 @@ class NoticeListItem extends Widget
$url = $location->getUrl(); $url = $location->getUrl();
$this->out->text(' ');
$this->out->elementStart('span', array('class' => 'location')); $this->out->elementStart('span', array('class' => 'location'));
$this->out->text(_('at')); $this->out->text(_('at'));
$this->out->text(' ');
if (empty($url)) { if (empty($url)) {
$this->out->element('span', array('class' => 'geo', $this->out->element('span', array('class' => 'geo',
'title' => $latlon), 'title' => $latlon),
@ -473,9 +476,11 @@ class NoticeListItem extends Widget
function showNoticeSource() function showNoticeSource()
{ {
if ($this->notice->source) { if ($this->notice->source) {
$this->out->text(' ');
$this->out->elementStart('span', 'source'); $this->out->elementStart('span', 'source');
$this->out->text(_('from')); $this->out->text(_('from'));
$source_name = _($this->notice->source); $source_name = _($this->notice->source);
$this->out->text(' ');
switch ($this->notice->source) { switch ($this->notice->source) {
case 'web': case 'web':
case 'xmpp': case 'xmpp':
@ -540,6 +545,7 @@ class NoticeListItem extends Widget
} }
} }
if ($hasConversation){ if ($hasConversation){
$this->out->text(' ');
$convurl = common_local_url('conversation', $convurl = common_local_url('conversation',
array('id' => $this->notice->conversation)); array('id' => $this->notice->conversation));
$this->out->element('a', array('href' => $convurl.'#notice-'.$this->notice->id, $this->out->element('a', array('href' => $convurl.'#notice-'.$this->notice->id,
@ -591,12 +597,14 @@ class NoticeListItem extends Widget
function showReplyLink() function showReplyLink()
{ {
if (common_logged_in()) { if (common_logged_in()) {
$this->out->text(' ');
$reply_url = common_local_url('newnotice', $reply_url = common_local_url('newnotice',
array('replyto' => $this->profile->nickname, 'inreplyto' => $this->notice->id)); array('replyto' => $this->profile->nickname, 'inreplyto' => $this->notice->id));
$this->out->elementStart('a', array('href' => $reply_url, $this->out->elementStart('a', array('href' => $reply_url,
'class' => 'notice_reply', 'class' => 'notice_reply',
'title' => _('Reply to this notice'))); 'title' => _('Reply to this notice')));
$this->out->text(_('Reply')); $this->out->text(_('Reply'));
$this->out->text(' ');
$this->out->element('span', 'notice_id', $this->notice->id); $this->out->element('span', 'notice_id', $this->notice->id);
$this->out->elementEnd('a'); $this->out->elementEnd('a');
} }
@ -616,7 +624,7 @@ class NoticeListItem extends Widget
if (!empty($user) && if (!empty($user) &&
($todel->profile_id == $user->id || $user->hasRight(Right::DELETEOTHERSNOTICE))) { ($todel->profile_id == $user->id || $user->hasRight(Right::DELETEOTHERSNOTICE))) {
$this->out->text(' ');
$deleteurl = common_local_url('deletenotice', $deleteurl = common_local_url('deletenotice',
array('notice' => $todel->id)); array('notice' => $todel->id));
$this->out->element('a', array('href' => $deleteurl, $this->out->element('a', array('href' => $deleteurl,
@ -635,6 +643,7 @@ class NoticeListItem extends Widget
{ {
$user = common_current_user(); $user = common_current_user();
if ($user && $user->id != $this->notice->profile_id) { if ($user && $user->id != $this->notice->profile_id) {
$this->out->text(' ');
$profile = $user->getProfile(); $profile = $user->getProfile();
if ($profile->hasRepeated($this->notice->id)) { if ($profile->hasRepeated($this->notice->id)) {
$this->out->element('span', array('class' => 'repeated', $this->out->element('span', array('class' => 'repeated',

View File

@ -90,6 +90,7 @@ class NoticeSection extends Section
'alt' => ($profile->fullname) ? 'alt' => ($profile->fullname) ?
$profile->fullname : $profile->fullname :
$profile->nickname)); $profile->nickname));
$this->out->text(' ');
$this->out->element('span', 'fn nickname', $profile->nickname); $this->out->element('span', 'fn nickname', $profile->nickname);
$this->out->elementEnd('a'); $this->out->elementEnd('a');
$this->out->elementEnd('span'); $this->out->elementEnd('span');

View File

@ -191,6 +191,7 @@ class ProfileListItem extends Widget
'alt' => 'alt' =>
($this->profile->fullname) ? $this->profile->fullname : ($this->profile->fullname) ? $this->profile->fullname :
$this->profile->nickname)); $this->profile->nickname));
$this->out->text(' ');
$hasFN = (!empty($this->profile->fullname)) ? 'nickname' : 'fn nickname'; $hasFN = (!empty($this->profile->fullname)) ? 'nickname' : 'fn nickname';
$this->out->elementStart('span', $hasFN); $this->out->elementStart('span', $hasFN);
$this->out->raw($this->highlight($this->profile->nickname)); $this->out->raw($this->highlight($this->profile->nickname));
@ -201,6 +202,7 @@ class ProfileListItem extends Widget
function showFullName() function showFullName()
{ {
if (!empty($this->profile->fullname)) { if (!empty($this->profile->fullname)) {
$this->out->text(' ');
$this->out->elementStart('span', 'fn'); $this->out->elementStart('span', 'fn');
$this->out->raw($this->highlight($this->profile->fullname)); $this->out->raw($this->highlight($this->profile->fullname));
$this->out->elementEnd('span'); $this->out->elementEnd('span');
@ -210,6 +212,7 @@ class ProfileListItem extends Widget
function showLocation() function showLocation()
{ {
if (!empty($this->profile->location)) { if (!empty($this->profile->location)) {
$this->out->text(' ');
$this->out->elementStart('span', 'location'); $this->out->elementStart('span', 'location');
$this->out->raw($this->highlight($this->profile->location)); $this->out->raw($this->highlight($this->profile->location));
$this->out->elementEnd('span'); $this->out->elementEnd('span');
@ -219,6 +222,7 @@ class ProfileListItem extends Widget
function showHomepage() function showHomepage()
{ {
if (!empty($this->profile->homepage)) { if (!empty($this->profile->homepage)) {
$this->out->text(' ');
$this->out->elementStart('a', array('href' => $this->profile->homepage, $this->out->elementStart('a', array('href' => $this->profile->homepage,
'class' => 'url')); 'class' => 'url'));
$this->out->raw($this->highlight($this->profile->homepage)); $this->out->raw($this->highlight($this->profile->homepage));

View File

@ -85,6 +85,7 @@ class ProfileSection extends Section
'href' => $profile->profileurl, 'href' => $profile->profileurl,
'rel' => 'contact member', 'rel' => 'contact member',
'class' => 'url')); 'class' => 'url'));
$this->out->text(' ');
$avatar = $profile->getAvatar(AVATAR_MINI_SIZE); $avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
$this->out->element('img', array('src' => (($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_MINI_SIZE)), $this->out->element('img', array('src' => (($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_MINI_SIZE)),
'width' => AVATAR_MINI_SIZE, 'width' => AVATAR_MINI_SIZE,
@ -93,6 +94,7 @@ class ProfileSection extends Section
'alt' => ($profile->fullname) ? 'alt' => ($profile->fullname) ?
$profile->fullname : $profile->fullname :
$profile->nickname)); $profile->nickname));
$this->out->text(' ');
$this->out->element('span', 'fn nickname', $profile->nickname); $this->out->element('span', 'fn nickname', $profile->nickname);
$this->out->elementEnd('a'); $this->out->elementEnd('a');
$this->out->elementEnd('span'); $this->out->elementEnd('span');

View File

@ -155,26 +155,26 @@ abstract class QueueManager extends IoManager
} }
/** /**
* Encode an object for queued storage. * Encode an object or variable for queued storage.
* Next gen may use serialization. * Notice objects are currently stored as an id reference;
* other items are serialized.
* *
* @param mixed $object * @param mixed $item
* @return string * @return string
*/ */
protected function encode($object) protected function encode($item)
{ {
if ($object instanceof Notice) { if ($item instanceof Notice) {
return $object->id; // Backwards compat
} else if (is_string($object)) { return $item->id;
return $object;
} else { } else {
throw new ServerException("Can't queue this type", 500); return serialize($item);
} }
} }
/** /**
* Decode an object from queued storage. * 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 * @param string
* @return mixed * @return mixed
@ -182,9 +182,23 @@ abstract class QueueManager extends IoManager
protected function decode($frame) protected function decode($frame)
{ {
if (is_numeric($frame)) { if (is_numeric($frame)) {
// Back-compat for notices...
return Notice::staticGet(intval($frame)); return Notice::staticGet(intval($frame));
} else { } elseif (substr($frame, 0, 1) == '<') {
// Back-compat for XML source
return $frame; return $frame;
} else {
// Deserialize!
#$old = error_reporting();
#error_reporting($old & ~E_NOTICE);
$out = unserialize($frame);
#error_reporting($old);
if ($out === false && $frame !== 'b:0;') {
common_log(LOG_ERR, "Couldn't unserialize queued frame: $frame");
return false;
}
return $out;
} }
} }

View File

@ -107,7 +107,8 @@ class StompQueueManager extends QueueManager
$message .= ':' . $param; $message .= ':' . $param;
} }
$this->_connect(); $this->_connect();
$result = $this->_send($this->control, $con = $this->cons[$this->defaultIdx];
$result = $con->send($this->control,
$message, $message,
array ('created' => common_sql_now())); array ('created' => common_sql_now()));
if ($result) { if ($result) {
@ -368,17 +369,10 @@ class StompQueueManager extends QueueManager
foreach ($this->cons as $i => $con) { foreach ($this->cons as $i => $con) {
if ($con) { if ($con) {
$this->rollback($i); $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; return true;
} }
@ -555,26 +549,14 @@ class StompQueueManager extends QueueManager
} }
$host = $this->cons[$idx]->getServer(); $host = $this->cons[$idx]->getServer();
if (is_numeric($frame->body)) { $item = $this->decode($frame->body);
$id = intval($frame->body); if (empty($item)) {
$info = "notice $id posted at {$frame->headers['created']} in queue $queue from $host"; $this->_log(LOG_ERR, "Skipping empty or deleted item in queue $queue from $host");
return true;
$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;
} }
$info = $this->logrep($item) . " posted at " .
$frame->headers['created'] . " in queue $queue from $host";
$this->_log(LOG_DEBUG, "Dequeued $info");
$handler = $this->getHandler($queue); $handler = $this->getHandler($queue);
if (!$handler) { if (!$handler) {

View File

@ -110,9 +110,20 @@ class Theme
$server = common_config('site', 'server'); $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;
} }
} }

View File

@ -238,9 +238,12 @@ class UserProfile extends Widget
if (Event::handle('StartProfilePageActionsElements', array(&$this->out, $this->profile))) { if (Event::handle('StartProfilePageActionsElements', array(&$this->out, $this->profile))) {
if (empty($cur)) { // not logged in if (empty($cur)) { // not logged in
if (Event::handle('StartProfileRemoteSubscribe', array(&$this->out, $this->profile))) {
$this->out->elementStart('li', 'entity_subscribe'); $this->out->elementStart('li', 'entity_subscribe');
$this->showRemoteSubscribeLink(); $this->showRemoteSubscribeLink();
$this->out->elementEnd('li'); $this->out->elementEnd('li');
Event::handle('EndProfileRemoteSubscribe', array(&$this->out, $this->profile));
}
} else { } else {
if ($cur->id == $this->profile->id) { // your own page if ($cur->id == $this->profile->id) { // your own page
$this->out->elementStart('li', 'entity_edit'); $this->out->elementStart('li', 'entity_edit');

View File

@ -367,7 +367,8 @@ function common_current_user()
if ($_cur === false) { 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(); common_ensure_session();
$id = isset($_SESSION['userid']) ? $_SESSION['userid'] : false; $id = isset($_SESSION['userid']) ? $_SESSION['userid'] : false;
if ($id) { if ($id) {

View File

@ -80,6 +80,9 @@ class OStatusPlugin extends Plugin
$m->connect('main/salmon/user/:id', $m->connect('main/salmon/user/:id',
array('action' => 'salmon'), array('action' => 'salmon'),
array('id' => '[0-9]+')); array('id' => '[0-9]+'));
$m->connect('main/salmon/group/:id',
array('action' => 'salmongroup'),
array('id' => '[0-9]+'));
return true; return true;
} }
@ -109,23 +112,33 @@ class OStatusPlugin extends Plugin
* Set up a PuSH hub link to our internal link for canonical timeline * Set up a PuSH hub link to our internal link for canonical timeline
* Atom feeds for users and groups. * Atom feeds for users and groups.
*/ */
function onStartApiAtom(Action $action) function onStartApiAtom(AtomNoticeFeed $feed)
{ {
if ($action instanceof ApiTimelineUserAction || $action instanceof ApiTimelineGroupAction) { $id = null;
$id = $action->arg('id');
if (strval(intval($id)) === strval($id)) { if ($feed instanceof AtomUserNoticeFeed) {
// Canonical form of id in URL? $salmonAction = 'salmon';
// Updates will be handled for our internal PuSH hub. $id = $feed->getUser()->id;
$action->element('link', array('rel' => 'hub', } else if ($feed instanceof AtomGroupNoticeFeed) {
'href' => common_local_url('pushhub'))); $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 // Also, we'll add in the salmon link
$action->element('link', array('rel' => 'salmon', $salmon = common_local_url($salmonAction, array('id' => $id));
'href' => common_local_url('salmon'))); $feed->addLink($salmon, array('rel' => 'salmon'));
} }
} }
return true;
}
/** /**
* Add the feed settings page to the Connect Settings menu * Add the feed settings page to the Connect Settings menu
@ -175,7 +188,7 @@ class OStatusPlugin extends Plugin
/** /**
* Add in an OStatus subscribe button * Add in an OStatus subscribe button
*/ */
function onStartProfilePageActionsElements($output, $profile) function onStartProfileRemoteSubscribe($output, $profile)
{ {
$cur = common_current_user(); $cur = common_current_user();
@ -186,14 +199,19 @@ class OStatusPlugin extends Plugin
array('nickname' => $profile->nickname)); array('nickname' => $profile->nickname));
$output->element('a', array('href' => $url, $output->element('a', array('href' => $url,
'class' => 'entity_remote_subscribe'), 'class' => 'entity_remote_subscribe'),
_('OStatus')); _m('Subscribe'));
$output->elementEnd('li'); $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) function onEndNoticeSave($notice)
{ {
@ -229,13 +247,41 @@ class OStatusPlugin extends Plugin
} }
} }
/**
* 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() { 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 = Schema::get();
$schema->ensureTable('feedinfo', Feedinfo::schemaDef()); $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef());
$schema->ensureTable('hubsub', HubSub::schemaDef()); $schema->ensureTable('hubsub', HubSub::schemaDef());
return true; 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;
}
} }

View File

@ -182,9 +182,9 @@ class FeedSubSettingsAction extends ConnectSettingsAction
} }
$this->munger = $discover->feedMunger(); $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.')); $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
return false; return false;
} }
@ -196,13 +196,16 @@ class FeedSubSettingsAction extends ConnectSettingsAction
{ {
if ($this->validateFeed()) { if ($this->validateFeed()) {
$this->preview = true; $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 not already in use, subscribe to updates via the hub
if ($this->feedinfo->sub_start) { if ($this->profile->sub_start) {
common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}"); common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}");
} else { } else {
$ok = $this->feedinfo->subscribe(); $ok = $this->profile->subscribe();
common_log(LOG_INFO, __METHOD__ . ": sub was $ok"); common_log(LOG_INFO, __METHOD__ . ": sub was $ok");
if (!$ok) { if (!$ok) {
$this->showForm(_m('Feed subscription failed! Bad response from hub.')); $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 // And subscribe the current user to the local profile
$user = common_current_user(); $user = common_current_user();
$profile = $this->feedinfo->getProfile();
if (!$profile) {
throw new ServerException("Feed profile was not saved properly.");
}
if ($this->feedinfo->isGroup()) { if ($this->profile->isGroup()) {
if ($user->isMember($profile)) { $group = $this->profile->localGroup();
if ($user->isMember($group)) {
$this->showForm(_m('Already a member!')); $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!')); $this->showForm(_m('Joined remote group!'));
} else { } else {
$this->showForm(_m('Remote group join failed!')); $this->showForm(_m('Remote group join failed!'));
} }
} else { } else {
if ($user->isSubscribed($profile)) { $local = $this->profile->localProfile();
if ($user->isSubscribed($local)) {
$this->showForm(_m('Already subscribed!')); $this->showForm(_m('Already subscribed!'));
} elseif ($user->subscribeTo($profile)) { } elseif ($user->subscribeTo($local)) {
$this->showForm(_m('Feed subscribed!')); $this->showForm(_m('Feed subscribed!'));
} else { } else {
$this->showForm(_m('Feed subscription failed!')); $this->showForm(_m('Feed subscription failed!'));
@ -247,7 +248,7 @@ class FeedSubSettingsAction extends ConnectSettingsAction
function previewFeed() function previewFeed()
{ {
$feedinfo = $this->munger->feedinfo(); $profile = $this->munger->ostatusProfile();
$notice = $this->munger->notice(0, true); // preview $notice = $this->munger->notice(0, true); // preview
if ($notice) { if ($notice) {

View File

@ -68,8 +68,20 @@ class OStatusInitAction extends Action
function showForm($err = null) function showForm($err = null)
{ {
$this->err = $err; $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(); $this->showPage();
}
} }
function showContent() function showContent()
@ -79,15 +91,15 @@ class OStatusInitAction extends Action
'class' => 'form_settings', 'class' => 'form_settings',
'action' => common_local_url('ostatusinit'))); 'action' => common_local_url('ostatusinit')));
$this->elementStart('fieldset'); $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->hidden('token', common_session_token());
$this->elementStart('ul', 'form_data'); $this->elementStart('ul', 'form_data');
$this->elementStart('li'); $this->elementStart('li', array('id' => 'ostatus_nickname'));
$this->input('nickname', _('User nickname'), $this->nickname, $this->input('nickname', _('User nickname'), $this->nickname,
_('Nickname of the user you want to follow')); _('Nickname of the user you want to follow'));
$this->elementEnd('li'); $this->elementEnd('li');
$this->elementStart('li'); $this->elementStart('li', array('id' => 'ostatus_profile'));
$this->input('acct', _('Profile Account'), $this->acct, $this->input('acct', _('Profile Account'), $this->acct,
_('Your account id (i.e. user@identi.ca)')); _('Your account id (i.e. user@identi.ca)'));
$this->elementEnd('li'); $this->elementEnd('li');

View File

@ -76,7 +76,7 @@ class OStatusSubAction extends Action
$this->elementStart('fieldset', array('id' => 'settings_feeds')); $this->elementStart('fieldset', array('id' => 'settings_feeds'));
$this->elementStart('ul', 'form_data'); $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->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed'));
$this->elementEnd('li'); $this->elementEnd('li');
$this->elementEnd('ul'); $this->elementEnd('ul');
@ -164,9 +164,9 @@ class OStatusSubAction extends Action
} }
$this->munger = $discover->feedMunger(); $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.')); $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
return false; return false;
} }
@ -178,13 +178,13 @@ class OStatusSubAction extends Action
{ {
if ($this->validateFeed()) { if ($this->validateFeed()) {
$this->preview = true; $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 not already in use, subscribe to updates via the hub
if ($this->feedinfo->sub_start) { if ($this->profile->sub_start) {
common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}"); common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}");
} else { } else {
$ok = $this->feedinfo->subscribe(); $ok = $this->profile->subscribe();
common_log(LOG_INFO, __METHOD__ . ": sub was $ok"); common_log(LOG_INFO, __METHOD__ . ": sub was $ok");
if (!$ok) { if (!$ok) {
$this->showForm(_m('Feed subscription failed! Bad response from hub.')); $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 // And subscribe the current user to the local profile
$user = common_current_user(); $user = common_current_user();
$profile = $this->feedinfo->getProfile(); $profile = $this->profile->getProfile();
if ($user->isSubscribed($profile)) { if ($user->isSubscribed($profile)) {
$this->showForm(_m('Already subscribed!')); $this->showForm(_m('Already subscribed!'));
@ -209,7 +209,7 @@ class OStatusSubAction extends Action
function previewFeed() function previewFeed()
{ {
$feedinfo = $this->munger->feedinfo(); $profile = $this->munger->ostatusProfile();
$notice = $this->munger->notice(0, true); // preview $notice = $this->munger->notice(0, true); // preview
if ($notice) { if ($notice) {

View File

@ -48,9 +48,9 @@ class PushCallbackAction extends Action
throw new ServerException('Empty or invalid feed id', 400); throw new ServerException('Empty or invalid feed id', 400);
} }
$feedinfo = Feedinfo::staticGet('id', $feedid); $profile = Ostatus_profile::staticGet('id', $feedid);
if (!$feedinfo) { if (!$profile) {
throw new ServerException('Unknown feed id ' . $feedid, 400); throw new ServerException('Unknown OStatus/PuSH feed id ' . $feedid, 400);
} }
$hmac = ''; $hmac = '';
@ -59,7 +59,7 @@ class PushCallbackAction extends Action
} }
$post = file_get_contents('php://input'); $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); throw new ServerException("Bogus hub callback: bad mode", 404);
} }
$feedinfo = Feedinfo::staticGet('feeduri', $topic); $profile = Ostatus_profile::staticGet('feeduri', $topic);
if (!$feedinfo) { if (!$profile) {
common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic"); common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic");
throw new ServerException("Bogus hub callback: unknown feed", 404); throw new ServerException("Bogus hub callback: unknown feed", 404);
} }
# Can't currently set the token in our sub api if ($profile->verify_token !== $verify_token) {
#if ($feedinfo->verify_token !== $verify_token) { common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic");
# 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);
# 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! // OK!
if ($mode == 'subscribe') {
common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); common_log(LOG_INFO, __METHOD__ . ': sub confirmed');
$feedinfo->sub_start = common_sql_date(time()); $profile->confirmSubscribe($lease_seconds);
if ($lease_seconds > 0) {
$feedinfo->sub_end = common_sql_date(time() + $lease_seconds);
} else { } else {
$feedinfo->sub_end = null; common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic");
$profile->confirmUnsubscribe();
} }
$feedinfo->update();
print $challenge; print $challenge;
} }
} }

View File

@ -22,28 +22,60 @@
* @author James Walker <james@status.net> * @author James Walker <james@status.net>
*/ */
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } if (!defined('STATUSNET')) {
exit(1);
}
class SalmonAction extends Action 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') {
if ($_SERVER['REQUEST_METHOD'] == 'POST') { $this->clientError(_('This method requires a POST.'));
$this->handlePost();
}
} }
if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') {
$this->clientError(_('Salmon requires application/atom+xml'));
}
function handlePost() $id = $this->trimmed('id');
{
$user_id = $this->arg('id'); if (!$id) {
common_log(LOG_DEBUG, 'Salmon: incoming post for user: '. $user_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'); $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 // TODO : Insert new $xml -> notice code
switch ($this->act->verb)
{
case Activity::POST:
case Activity::SHARE:
case Activity::FAVORITE:
case Activity::FOLLOW:
}
} }
} }

View File

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

View File

@ -242,7 +242,7 @@ class HubSub extends Memcached_DataObject
{ {
$headers = array('Content-Type: application/atom+xml'); $headers = array('Content-Type: application/atom+xml');
if ($this->secret) { if ($this->secret) {
$hmac = sha1($atom . $this->secret); $hmac = hash_hmac('sha1', $atom, $this->secret);
$headers[] = "X-Hub-Signature: sha1=$hmac"; $headers[] = "X-Hub-Signature: sha1=$hmac";
} else { } else {
$hmac = '(none)'; $hmac = '(none)';

View File

@ -0,0 +1,644 @@
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2009-2010, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* @package FeedSubPlugin
* @maintainer Brion Vibber <brion@status.net>
*/
/*
PuSH subscription flow:
$profile->subscribe()
generate random verification token
save to verify_token
sends a sub request to the hub...
main/push/callback
hub sends confirmation back to us via GET
We verify the request, then echo back the challenge.
On our end, we save the time we subscribed and the lease expiration
main/push/callback
hub sends us updates via POST
*/
class FeedDBException extends FeedSubException
{
public $obj;
function __construct($obj)
{
parent::__construct('Database insert failure');
$this->obj = $obj;
}
}
class Ostatus_profile extends Memcached_DataObject
{
public $__table = 'ostatus_profile';
public $id;
public $profile_id;
public $group_id;
public $feeduri;
public $homeuri;
// PuSH subscription data
public $huburi;
public $secret;
public $verify_token;
public $sub_state; // subscribe, active, unsubscribe
public $sub_start;
public $sub_end;
public $salmonuri;
public $created;
public $lastupdate;
public /*static*/ function staticGet($k, $v=null)
{
return parent::staticGet(__CLASS__, $k, $v);
}
/**
* return table definition for DB_DataObject
*
* DB_DataObject needs to know something about the table to manipulate
* instances. This method provides all the DB_DataObject needs to know.
*
* @return array array of column definitions
*/
function table()
{
return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
'profile_id' => DB_DATAOBJECT_INT,
'group_id' => DB_DATAOBJECT_INT,
'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
'huburi' => DB_DATAOBJECT_STR,
'secret' => DB_DATAOBJECT_STR,
'verify_token' => DB_DATAOBJECT_STR,
'sub_state' => DB_DATAOBJECT_STR,
'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
'salmonuri' => DB_DATAOBJECT_STR,
'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
}
static function schemaDef()
{
return array(new ColumnDef('id', 'integer',
/*size*/ null,
/*nullable*/ false,
/*key*/ 'PRI',
/*default*/ '0',
/*extra*/ null,
/*auto_increment*/ true),
new ColumnDef('profile_id', 'integer',
null, true, 'UNI'),
new ColumnDef('group_id', 'integer',
null, true, 'UNI'),
new ColumnDef('feeduri', 'varchar',
255, false, 'UNI'),
new ColumnDef('homeuri', 'varchar',
255, false),
new ColumnDef('huburi', 'text',
null, true),
new ColumnDef('verify_token', 'varchar',
32, true),
new ColumnDef('secret', 'varchar',
64, true),
new ColumnDef('sub_state', "enum('subscribe','active','unsubscribe')",
null, true),
new ColumnDef('sub_start', 'datetime',
null, true),
new ColumnDef('sub_end', 'datetime',
null, true),
new ColumnDef('salmonuri', 'text',
null, true),
new ColumnDef('created', 'datetime',
null, false),
new ColumnDef('lastupdate', 'datetime',
null, false));
}
/**
* return key definitions for DB_DataObject
*
* DB_DataObject needs to know about keys that the table has; this function
* defines them.
*
* @return array key definitions
*/
function keys()
{
return array_keys($this->keyTypes());
}
/**
* return key definitions for Memcached_DataObject
*
* Our caching system uses the same key definitions, but uses a different
* method to get them.
*
* @return array key definitions
*/
function keyTypes()
{
return array('id' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U');
}
function sequenceKey()
{
return array('id', true, false);
}
/**
* Fetch the StatusNet-side profile for this feed
* @return Profile
*/
public function localProfile()
{
if ($this->profile_id) {
return Profile::staticGet('id', $this->profile_id);
}
return null;
}
/**
* Fetch the StatusNet-side profile for this feed
* @return Profile
*/
public function localGroup()
{
if ($this->group_id) {
return User_group::staticGet('id', $this->group_id);
}
return null;
}
/**
* @param FeedMunger $munger
* @param boolean $isGroup is this a group record?
* @return Ostatus_profile
*/
public static function ensureProfile($munger)
{
$profile = $munger->ostatusProfile();
$current = self::staticGet('feeduri', $profile->feeduri);
if ($current) {
// @fixme we should probably update info as necessary
return $current;
}
$profile->query('BEGIN');
// Awful hack! Awful hack!
$profile->verify = common_good_rand(16);
$profile->secret = common_good_rand(32);
try {
$local = $munger->profile();
if ($entity->isGroup()) {
$group = new User_group();
$group->nickname = $local->nickname . '@remote'; // @fixme
$group->fullname = $local->fullname;
$group->homepage = $local->homepage;
$group->location = $local->location;
$group->created = $local->created;
$group->insert();
if (empty($result)) {
throw new FeedDBException($group);
}
$profile->group_id = $group->id;
} else {
$result = $local->insert();
if (empty($result)) {
throw new FeedDBException($local);
}
$profile->profile_id = $local->id;
}
$profile->created = sql_common_date();
$profile->lastupdate = sql_common_date();
$result = $profile->insert();
if (empty($result)) {
throw new FeedDBException($profile);
}
$entity->query('COMMIT');
} catch (FeedDBException $e) {
common_log_db_error($e->obj, 'INSERT', __FILE__);
$entity->query('ROLLBACK');
return false;
}
$avatar = $munger->getAvatar();
if ($avatar) {
try {
$this->updateAvatar($avatar);
} catch (Exception $e) {
common_log(LOG_ERR, "Exception setting OStatus avatar: " .
$e->getMessage());
}
}
return $entity;
}
/**
* Download and update given avatar image
* @param string $url
* @throws Exception in various failure cases
*/
public function updateAvatar($url)
{
// @fixme this should be better encapsulated
// ripped from oauthstore.php (for old OMB client)
$temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
copy($url, $temp_filename);
$imagefile = new ImageFile($profile->id, $temp_filename);
$filename = Avatar::filename($profile->id,
image_type_to_extension($imagefile->type),
null,
common_timestamp());
rename($temp_filename, Avatar::path($filename));
if ($this->isGroup()) {
$group = $this->localGroup();
$group->setOriginal($filename);
} else {
$profile = $this->localProfile();
$profile->setOriginal($filename);
}
}
/**
* Returns an XML string fragment with profile information as an
* Activity Streams noun object with the given element type.
*
* Assumes that 'activity' namespace has been previously defined.
*
* @param string $element one of 'actor', 'subject', 'object', 'target'
* @return string
*/
function asActivityNoun($element)
{
$xs = new XMLStringer(true);
$avatarHref = Avatar::defaultImage(AVATAR_PROFILE_SIZE);
$avatarType = 'image/png';
if ($this->isGroup()) {
$type = 'http://activitystrea.ms/schema/1.0/group';
$self = $this->localGroup();
// @fixme put a standard getAvatar() interface on groups too
if ($self->homepage_logo) {
$avatarHref = $self->homepage_logo;
$map = array('png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif');
$extension = pathinfo(parse_url($avatarHref, PHP_URL_PATH), PATHINFO_EXTENSION);
if (isset($map[$extension])) {
$avatarType = $map[$extension];
}
}
} else {
$type = 'http://activitystrea.ms/schema/1.0/person';
$self = $this->localProfile();
$avatar = $self->getAvatar(AVATAR_PROFILE_SIZE);
if ($avatar) {
$avatarHref = $avatar->
$avatarType = $avatar->mediatype;
}
}
$xs->elementStart('activity:' . $element);
$xs->element(
'activity:object-type',
null,
$type
);
$xs->element(
'id',
null,
$this->homeuri); // ?
$xs->element('title', null, $self->getBestName());
$xs->element(
'link', array(
'type' => $avatarType,
'href' => $avatarHref
),
''
);
$xs->elementEnd('activity:' . $element);
return $xs->getString();
}
/**
* Damn dirty hack!
*/
function isGroup()
{
return (strpos($this->feeduri, '/groups/') !== false);
}
/**
* Send a subscription request to the hub for this feed.
* The hub will later send us a confirmation POST to /main/push/callback.
*
* @return bool true on success, false on failure
*/
public function subscribe($mode='subscribe')
{
if (common_config('feedsub', 'nohub')) {
// Fake it! We're just testing remote feeds w/o hubs.
return true;
}
// @fixme use the verification token
#$token = md5(mt_rand() . ':' . $this->feeduri);
#$this->verify_token = $token;
#$this->update(); // @fixme
try {
$callback = common_local_url('pushcallback', array('feed' => $this->id));
$headers = array('Content-Type: application/x-www-form-urlencoded');
$post = array('hub.mode' => $mode,
'hub.callback' => $callback,
'hub.verify' => 'async',
'hub.verify_token' => $this->verify_token,
'hub.secret' => $this->secret,
//'hub.lease_seconds' => 0,
'hub.topic' => $this->feeduri);
$client = new HTTPClient();
$response = $client->post($this->huburi, $headers, $post);
$status = $response->getStatus();
if ($status == 202) {
common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
return true;
} else if ($status == 204) {
common_log(LOG_INFO, __METHOD__ . ': sub req ok and verified');
return true;
} else if ($status >= 200 && $status < 300) {
common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody());
return false;
} else {
common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody());
return false;
}
} catch (Exception $e) {
// wtf!
common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri");
return false;
}
}
/**
* Save PuSH subscription confirmation.
* Sets approximate lease start and end times and finalizes state.
*
* @param int $lease_seconds provided hub.lease_seconds parameter, if given
*/
public function confirmSubscribe($lease_seconds=0)
{
$original = clone($this);
$this->sub_state = 'active';
$this->sub_start = common_sql_date(time());
if ($lease_seconds > 0) {
$this->sub_end = common_sql_date(time() + $lease_seconds);
} else {
$this->sub_end = null;
}
$this->lastupdate = common_sql_date();
return $this->update($original);
}
/**
* Save PuSH unsubscription confirmation.
* Wipes active PuSH sub info and resets state.
*/
public function confirmUnsubscribe()
{
$original = clone($this);
$this->verify_token = null;
$this->secret = null;
$this->sub_state = null;
$this->sub_start = null;
$this->sub_end = null;
$this->lastupdate = common_sql_date();
return $this->update($original);
}
/**
* Send a PuSH unsubscription request to the hub for this feed.
* The hub will later send us a confirmation POST to /main/push/callback.
*
* @return bool true on success, false on failure
*/
public function unsubscribe() {
return $this->subscribe('unsubscribe');
}
/**
* Send an Activity Streams notification to the remote Salmon endpoint,
* if so configured.
*
* @param Profile $actor
* @param $verb eg Activity::SUBSCRIBE or Activity::JOIN
* @param $object object of the action; if null, the remote entity itself is assumed
*/
public function notify(Profile $actor, $verb, $object=null)
{
if ($object == null) {
$object = $this;
}
if ($this->salmonuri) {
$text = 'update'; // @fixme
$id = 'tag:' . common_config('site', 'server') .
':' . $verb .
':' . $actor->id .
':' . time(); // @fixme
$entry = new Atom10Entry();
$entry->elementStart('entry');
$entry->element('id', null, $id);
$entry->element('title', null, $text);
$entry->element('summary', null, $text);
$entry->element('published', null, common_date_w3dtf());
$entry->element('activity:verb', null, $verb);
$entry->raw($profile->asAtomAuthor());
$entry->raw($profile->asActivityActor());
$entry->raw($object->asActivityNoun('object'));
$entry->elmentEnd('entry');
$feed = $this->atomFeed($actor);
$feed->initFeed();
$feed->addEntry($entry);
$feed->renderEntries();
$feed->endFeed();
$xml = $feed->getString();
common_log(LOG_INFO, "Posting to Salmon endpoint $salmon: $xml");
$salmon = new Salmon(); // ?
$salmon->post($this->salmonuri, $xml);
}
}
function getBestName()
{
if ($this->isGroup()) {
return $this->localGroup()->getBestName();
} else {
return $this->localProfile()->getBestName();
}
}
function atomFeed($actor)
{
$feed = new Atom10Feed();
// @fixme should these be set up somewhere else?
$feed->addNamespace('activity', 'http://activitystrea.ms/spec/1.0/');
$feed->addNamesapce('thr', 'http://purl.org/syndication/thread/1.0');
$feed->addNamespace('georss', 'http://www.georss.org/georss');
$feed->addNamespace('ostatus', 'http://ostatus.org/schema/1.0');
$taguribase = common_config('integration', 'taguri');
$feed->setId("tag:{$taguribase}:UserTimeline:{$actor->id}"); // ???
$feed->setTitle($actor->getBestName() . ' timeline'); // @fixme
$feed->setUpdated(time());
$feed->setPublished(time());
$feed->addLink(common_url('ApiTimelineUser',
array('id' => $actor->id,
'type' => 'atom')),
array('rel' => 'self',
'type' => 'application/atom+xml'));
$feed->addLink(common_url('userbyid',
array('id' => $actor->id)),
array('rel' => 'alternate',
'type' => 'text/html'));
return $feed;
}
/**
* Read and post notices for updates from the feed.
* Currently assumes that all items in the feed are new,
* coming from a PuSH hub.
*
* @param string $xml source of Atom or RSS feed
* @param string $hmac X-Hub-Signature header, if present
*/
public function postUpdates($xml, $hmac)
{
common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml");
if ($this->secret) {
if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) {
$their_hmac = strtolower($matches[1]);
$our_hmac = hash_hmac('sha1', $xml, $this->secret);
if ($their_hmac !== $our_hmac) {
common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac");
return;
}
} else {
common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'");
return;
}
} else if ($hmac) {
common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'");
return;
}
require_once "XML/Feed/Parser.php";
$feed = new XML_Feed_Parser($xml, false, false, true);
$munger = new FeedMunger($feed);
$hits = 0;
foreach ($feed as $index => $entry) {
// @fixme this might sort in wrong order if we get multiple updates
$notice = $munger->notice($index);
// Double-check for oldies
// @fixme this could explode horribly for multiple feeds on a blog. sigh
$dupe = new Notice();
$dupe->uri = $notice->uri;
if ($dupe->find(true)) {
common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}");
continue;
}
// @fixme need to ensure that groups get handled correctly
$saved = Notice::saveNew($notice->profile_id,
$notice->content,
'ostatus',
array('is_local' => Notice::REMOTE_OMB,
'uri' => $notice->uri,
'lat' => $notice->lat,
'lon' => $notice->lon,
'location_ns' => $notice->location_ns,
'location_id' => $notice->location_id));
/*
common_log(LOG_DEBUG, "going to check group delivery...");
if ($this->group_id) {
$group = User_group::staticGet($this->group_id);
if ($group) {
common_log(LOG_INFO, __METHOD__ . ": saving to local shadow group $group->id $group->nickname");
$groups = array($group);
} else {
common_log(LOG_INFO, __METHOD__ . ": lost the local shadow group?");
}
} else {
common_log(LOG_INFO, __METHOD__ . ": no local shadow groups");
$groups = array();
}
common_log(LOG_DEBUG, "going to add to inboxes...");
$notice->addToInboxes($groups, array());
common_log(LOG_DEBUG, "added to inboxes.");
*/
$hits++;
}
if ($hits == 0) {
common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml");
}
}
}

View File

@ -0,0 +1,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">&#215;</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();
}
});

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

View File

@ -83,13 +83,17 @@ class FeedMunger
$this->url = $url; $this->url = $url;
} }
function feedinfo() function ostatusProfile()
{ {
$feedinfo = new Feedinfo(); $profile = new Ostatus_profile();
$feedinfo->feeduri = $this->url; $profile->feeduri = $this->url;
$feedinfo->homeuri = $this->feed->link; $profile->homeuri = $this->feed->link;
$feedinfo->huburi = $this->getHubLink(); $profile->huburi = $this->getHubLink();
return $feedinfo; $salmon = $this->getSalmonLink();
if ($salmon) {
$profile->salmonuri = $salmon;
}
return $profile;
} }
function getAtomLink($item, $attribs=array()) function getAtomLink($item, $attribs=array())
@ -155,6 +159,16 @@ class FeedMunger
return $this->getAtomLink($this->feed, array('rel' => 'hub')); 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. * Get an appropriate avatar image source URL, if available.
* @return mixed string or false * @return mixed string or false
@ -209,6 +223,7 @@ class FeedMunger
$notice->id = -1; $notice->id = -1;
} else { } else {
$notice = new Notice(); $notice = new Notice();
$notice->profile_id = $this->profileIdForEntry($index);
} }
$link = $this->getAltLink($entry); $link = $this->getAltLink($entry);
@ -239,7 +254,22 @@ class FeedMunger
return $notice; 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 * @param feed item $entry
* @return mixed Location or false * @return mixed Location or false
*/ */
@ -249,7 +279,10 @@ class FeedMunger
$points = $dom->getElementsByTagNameNS('http://www.georss.org/georss', 'point'); $points = $dom->getElementsByTagNameNS('http://www.georss.org/georss', 'point');
for ($i = 0; $i < $points->length; $i++) { 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); $coords = explode(' ', $point);
if (count($coords) == 2) { if (count($coords) == 2) {
list($lat, $lon) = $coords; list($lat, $lon) = $coords;

View File

@ -38,6 +38,7 @@ class HubDistribQueueHandler extends QueueHandler
foreach ($notice->getGroups() as $group) { foreach ($notice->getGroups() as $group) {
$this->pushGroup($notice, $group->group_id); $this->pushGroup($notice, $group->group_id);
} }
return true;
} }
function pushUser($notice) function pushUser($notice)
@ -48,14 +49,7 @@ class HubDistribQueueHandler extends QueueHandler
$feed = common_local_url('ApiTimelineUser', $feed = common_local_url('ApiTimelineUser',
array('id' => $notice->profile_id, array('id' => $notice->profile_id,
'format' => 'atom')); 'format' => 'atom'));
$sub = new HubSub(); $this->pushFeed($feed, array($this, 'userFeedForNotice'), $notice);
$sub->topic = $feed;
if ($sub->find()) {
$atom = $this->userFeedForNotice($notice);
$this->pushFeeds($atom, $sub);
} else {
common_log(LOG_INFO, "No PuSH subscribers for $feed");
}
} }
function pushGroup($notice, $group_id) function pushGroup($notice, $group_id)
@ -63,19 +57,69 @@ class HubDistribQueueHandler extends QueueHandler
$feed = common_local_url('ApiTimelineGroup', $feed = common_local_url('ApiTimelineGroup',
array('id' => $group_id, array('id' => $group_id,
'format' => 'atom')); '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 = new HubSub();
$sub->topic = $feed; $sub->topic = $feed;
if ($sub->find()) { if ($sub->find()) {
common_log(LOG_INFO, "Building PuSH feed for $feed"); $args = array_slice(func_get_args(), 2);
$atom = $this->groupFeedForNotice($group_id, $notice); $atom = call_user_func_array($callback, $args);
$this->pushFeeds($atom, $sub); $this->pushFeedInternal($atom, $sub);
} else { } else {
common_log(LOG_INFO, "No PuSH subscribers for $feed"); common_log(LOG_INFO, "No PuSH subscribers for $feed");
} }
return true;
} }
/**
* Ping external hub about this update.
* The hub will pull the feed and check for new items later.
* Not guaranteed safe in an environment with database replication.
*
* @param string $feed feed topic URI
* @param string $hub PuSH hub URI
* @fixme can consolidate pings for user & group posts
*/
function pushFeedExternal($feed, $hub)
{
$client = new HTTPClient();
try {
$data = array('hub.mode' => 'publish',
'hub.url' => $feed);
$response = $client->post($hub, array(), $data);
if ($response->getStatus() == 204) {
common_log(LOG_INFO, "PuSH ping to hub $hub for $feed ok");
return true;
} else {
common_log(LOG_ERR, "PuSH ping to hub $hub for $feed failed with HTTP " .
$response->getStatus() . ': ' .
$response->getBody());
}
} catch (Exception $e) {
common_log(LOG_ERR, "PuSH ping to hub $hub for $feed failed: " . $e->getMessage());
return false;
}
}
function pushFeeds($atom, $sub) /**
* 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"); common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $sub->topic");
$qm = QueueManager::get(); $qm = QueueManager::get();

View File

@ -43,7 +43,7 @@ class HubOutQueueHandler extends QueueHandler
common_log(LOG_ERR, "Failed PuSH to $sub->callback for $sub->topic: " . common_log(LOG_ERR, "Failed PuSH to $sub->callback for $sub->topic: " .
$e->getMessage()); $e->getMessage());
// @fixme Reschedule a later delivery? // @fixme Reschedule a later delivery?
// Currently we have no way to do this other than 'send NOW' return true;
} }
return true; return true;

View 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">
&lt;p&gt;Geraldine posted a Photo on PhotoPanic&lt;/p&gt;
&lt;img src="/geraldine/photo1.jpg"&gt;
</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;

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

View File

@ -45,6 +45,7 @@ class PoweredByStatusNetPlugin extends Plugin
{ {
function onEndAddressData($action) function onEndAddressData($action)
{ {
$action->text(' ');
$action->elementStart('span', 'poweredby'); $action->elementStart('span', 'poweredby');
$action->raw(sprintf(_m('powered by %s'), $action->raw(sprintf(_m('powered by %s'),
sprintf('<a href="http://status.net/">%s</a>', sprintf('<a href="http://status.net/">%s</a>',

View File

@ -288,7 +288,7 @@ margin-left:18px;
} }
#site_nav_global_primary li { #site_nav_global_primary li {
display:inline; display:inline;
margin-left:11px; margin-left:18px;
} }
.system_notice dt { .system_notice dt {
@ -370,7 +370,7 @@ margin-bottom:11px;
#site_nav_global_secondary ul li { #site_nav_global_secondary ul li {
display:inline; display:inline;
margin-right:11px; margin-right:18px;
} }
#export_data li a { #export_data li a {
padding-left:20px; padding-left:20px;
@ -383,15 +383,13 @@ padding-left:28px;
} }
#export_data ul { #export_data ul {
display:inline; width:100%;
float:left;
} }
#export_data li { #export_data li {
list-style-type:none; list-style-type:none;
display:inline; float:left;
margin-left:11px; margin-right:11px;
}
#export_data li:first-child {
margin-left:0;
} }
#licenses { #licenses {
@ -801,8 +799,8 @@ list-style-type:none;
display:inline; display:inline;
} }
.entity_tags li { .entity_tags li {
display:inline; float:left;
margin-right:4px; margin-right:11px;
} }
.aside .section { .aside .section {
@ -820,6 +818,7 @@ font-size:1em;
#entity_statistics dt, #entity_statistics dt,
#entity_statistics dd { #entity_statistics dd {
display:inline; display:inline;
margin-right:11px;
} }
#entity_statistics dt:after { #entity_statistics dt:after {
content: ":"; content: ":";
@ -1104,25 +1103,22 @@ left:0;
.dialogbox { .dialogbox {
position:absolute; position:absolute;
top:-4px; top:-1px;
right:29px; right:-1px;
z-index:9; z-index:9;
min-width:199px;
float:none; float:none;
background-color:#FFF;
padding:11px; padding:11px;
border-radius:7px; border-radius:7px;
-moz-border-radius:7px; -moz-border-radius:7px;
-webkit-border-radius:7px; -webkit-border-radius:7px;
border-style:solid; border-style:solid;
border-width:1px; border-width:1px;
border-color:#DDDDDD;
-moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
} }
.dialogbox legend { .dialogbox legend {
display:block !important; display:block !important;
margin-right:18px; margin-right:18px;
margin-bottom:18px;
} }
.dialogbox button.close { .dialogbox button.close {
@ -1131,11 +1127,22 @@ right:3px;
top:3px; top:3px;
} }
.dialogbox .form_guide {
font-weight:normal;
padding:0;
}
.dialogbox .submit_dialogbox { .dialogbox .submit_dialogbox {
font-weight:bold; font-weight:bold;
text-indent:0; text-indent:0;
min-width:46px; min-width:46px;
} }
.dialogbox input {
padding-left:4px;
}
.dialogbox fieldset {
margin-bottom:0;
}
#wrap form.processing input.submit, #wrap form.processing input.submit,
.entity_actions a.processing, .entity_actions a.processing,
@ -1145,6 +1152,12 @@ outline:none;
text-indent:-9999px; text-indent:-9999px;
} }
.form_repeat.dialogbox {
top:-4px;
right:29px;
min-width:199px;
}
.notice-options { .notice-options {
position:relative; position:relative;
font-size:0.95em; font-size:0.95em;
@ -1482,6 +1495,11 @@ display:inline;
margin-right:7px; margin-right:7px;
line-height:1.25; line-height:1.25;
} }
.tag-cloud li:before {
content:'\0009';
}
.aside .tag-cloud li { .aside .tag-cloud li {
line-height:1.5; line-height:1.5;
} }

View File

@ -30,7 +30,9 @@ border-radius:4px;
input, textarea, select, option { input, textarea, select, option {
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif; font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
} }
input, textarea, select { input, textarea, select,
.entity_actions .dialogbox input,
.mark-top {
border-color:#AAAAAA; border-color:#AAAAAA;
} }
@ -46,7 +48,8 @@ box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3);
.pagination .nav_prev a, .pagination .nav_prev a,
.pagination .nav_next a, .pagination .nav_next a,
.form_settings fieldset fieldset, .form_settings fieldset fieldset,
.entity_moderation:hover ul { .entity_moderation:hover ul,
.dialogbox {
border-color:#DDDDDD; border-color:#DDDDDD;
} }
@ -78,7 +81,8 @@ background-color:transparent;
input:focus, textarea:focus, select:focus, input:focus, textarea:focus, select:focus,
.form_notice.warning #notice_data-text, .form_notice.warning #notice_data-text,
.form_notice.warning #notice_text-count, .form_notice.warning #notice_text-count,
.form_settings .form_note { .form_settings .form_note,
.entity_actions .dialogbox .form_data input:focus {
border-color:#9BB43E; border-color:#9BB43E;
} }
input.submit { input.submit {
@ -133,9 +137,6 @@ color:#002FA7;
#content tbody tr { #content tbody tr {
border-top-color:#C8D1D5; border-top-color:#C8D1D5;
} }
.mark-top {
border-color:#AAAAAA;
}
#aside_primary { #aside_primary {
background-color:#C8D1D5; background-color:#C8D1D5;
@ -144,7 +145,9 @@ background-color:#C8D1D5;
#notice_text-count { #notice_text-count {
color:#333333; color:#333333;
} }
.form_notice.warning #notice_text-count { .form_notice.warning #notice_text-count,
.dialogbox,
.entity_actions .dialogbox input {
color:#000000; color:#000000;
} }
.form_notice label[for=notice_data-attach] { .form_notice label[for=notice_data-attach] {
@ -221,7 +224,8 @@ border-color:transparent;
#content, #content,
#site_nav_local_views .current a, #site_nav_local_views .current a,
.entity_send-a-message .form_notice, .entity_send-a-message .form_notice,
.entity_moderation:hover ul { .entity_moderation:hover ul,
.dialogbox {
background-color:#FFFFFF; background-color:#FFFFFF;
} }
@ -308,7 +312,8 @@ background-position: 5px -718px;
background-position: 5px -852px; background-position: 5px -852px;
} }
.entity_send-a-message .form_notice, .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); box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
-moz-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); -webkit-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);

View File

@ -30,7 +30,9 @@ border-radius:4px;
input, textarea, select, option { input, textarea, select, option {
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif; font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
} }
input, textarea, select { input, textarea, select,
.entity_actions .dialogbox input,
.mark-top {
border-color:#AAAAAA; border-color:#AAAAAA;
} }
@ -46,7 +48,8 @@ box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3);
.pagination .nav_prev a, .pagination .nav_prev a,
.pagination .nav_next a, .pagination .nav_next a,
.form_settings fieldset fieldset, .form_settings fieldset fieldset,
.entity_moderation:hover ul { .entity_moderation:hover ul,
.dialogbox {
border-color:#DDDDDD; border-color:#DDDDDD;
} }
@ -88,6 +91,7 @@ color:#FFFFFF;
border-color:transparent; border-color:transparent;
text-shadow:none; text-shadow:none;
} }
.dialogbox .submit_dialogbox, .dialogbox .submit_dialogbox,
input.submit, input.submit,
.form_notice input.submit { .form_notice input.submit {
@ -133,9 +137,6 @@ color:#002FA7;
#content tbody tr { #content tbody tr {
border-top-color:#CEE1E9; border-top-color:#CEE1E9;
} }
.mark-top {
border-color:#AAAAAA;
}
#aside_primary { #aside_primary {
background-color:#CEE1E9; background-color:#CEE1E9;
@ -144,7 +145,9 @@ background-color:#CEE1E9;
#notice_text-count { #notice_text-count {
color:#333333; color:#333333;
} }
.form_notice.warning #notice_text-count { .form_notice.warning #notice_text-count,
.dialogbox,
.entity_actions .dialogbox input {
color:#000000; color:#000000;
} }
.form_notice label[for=notice_data-attach] { .form_notice label[for=notice_data-attach] {
@ -221,7 +224,8 @@ border-color:transparent;
#content, #content,
#site_nav_local_views .current a, #site_nav_local_views .current a,
.entity_send-a-message .form_notice, .entity_send-a-message .form_notice,
.entity_moderation:hover ul { .entity_moderation:hover ul,
.dialogbox {
background-color:#FFFFFF; background-color:#FFFFFF;
} }
@ -307,7 +311,8 @@ background-position: 5px -718px;
background-position: 5px -852px; background-position: 5px -852px;
} }
.entity_send-a-message .form_notice, .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); box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
-moz-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); -webkit-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);