diff options
62 files changed, 3022 insertions, 716 deletions
diff --git a/EVENTS.txt b/EVENTS.txt index 6bf12bf13..69fe2ddcc 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -1,4 +1,4 @@ -InitializePlugin: a chance to initialize a plugin in a complete environment +\InitializePlugin: a chance to initialize a plugin in a complete environment CleanupPlugin: a chance to cleanup a plugin at the end of a program @@ -355,6 +355,14 @@ EndShowHeadElements: Right before the </head> tag; put <script>s here if you nee CheckSchema: chance to check the schema +StartProfileRemoteSubscribe: Before showing the link to remote subscription +- $userprofile: UserProfile widget +- &$profile: the profile being shown + +EndProfileRemoteSubscribe: After showing the link to remote subscription +- $userprofile: UserProfile widget +- &$profile: the profile being shown + StartProfilePageProfileSection: Starting to show the section of the profile page with the actual profile data; hook to prevent showing the profile (e.g.) @@ -1192,6 +1192,8 @@ server: If set, defines another server where avatars are stored in the typically only make 2 connections to a single server at a time <http://ur1.ca/6ih>, so this can parallelize the job. Defaults to null. +ssl: Whether to access avatars using HTTPS. Defaults to null, meaning + to guess based on site-wide SSL settings. public ------ @@ -1221,6 +1223,19 @@ path: Path part of theme URLs, before the theme name. Relative to the (using version numbers as the path) to make sure that all files are reloaded by caching clients or proxies. Defaults to null, which means to use the site path + '/theme'. +ssl: Whether to use SSL for theme elements. Default is null, which means + guess based on site SSL settings. + +javascript +---------- + +server: You can speed up page loading by pointing the + theme file lookup to another server (virtual or real). + Defaults to NULL, meaning to use the site server. +path: Path part of Javascript URLs. Defaults to null, + which means to use the site path + '/js/'. +ssl: Whether to use SSL for JavaScript files. Default is null, which means + guess based on site SSL settings. xmpp ---- @@ -1447,6 +1462,8 @@ server: server name to use when creating URLs for uploaded files. a virtual server here can speed up Web performance. path: URL path, relative to the server, to find files. Defaults to main path + '/file/'. +ssl: whether to use HTTPS for file URLs. Defaults to null, meaning to + guess based on other SSL settings. filecommand: command to use for determining the type of a file. May be skipped if fileinfo extension is installed. Defaults to '/usr/bin/file'. @@ -1506,6 +1523,8 @@ dir: directory to write backgrounds too. Default is '/background/' subdir of install dir. path: path to backgrounds. Default is sub-path of install path; note that you may need to change this if you change site-path too. +ssl: Whether or not to use HTTPS for background files. Defaults to + null, meaning to guess from site-wide SSL settings. ping ---- diff --git a/actions/apitimelinefavorites.php b/actions/apitimelinefavorites.php index 1027d97d4..f7f900ddf 100644 --- a/actions/apitimelinefavorites.php +++ b/actions/apitimelinefavorites.php @@ -100,11 +100,11 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction function showTimeline() { - $profile = $this->user->getProfile(); - $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); + $profile = $this->user->getProfile(); + $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); - $sitename = common_config('site', 'name'); - $title = sprintf( + $sitename = common_config('site', 'name'); + $title = sprintf( _('%1$s / Favorites from %2$s'), $sitename, $this->user->nickname @@ -112,32 +112,69 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction $taguribase = common_config('integration', 'taguri'); $id = "tag:$taguribase:Favorites:" . $this->user->id; - $link = common_local_url( - 'favorites', - array('nickname' => $this->user->nickname) - ); - $subtitle = sprintf( + + $subtitle = sprintf( _('%1$s updates favorited by %2$s / %2$s.'), $sitename, $profile->getBestName(), $this->user->nickname ); - $logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE); + $logo = !empty($avatar) + ? $avatar->displayUrl() + : Avatar::defaultImage(AVATAR_PROFILE_SIZE); switch($this->format) { case 'xml': $this->showXmlTimeline($this->notices); break; case 'rss': - $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo); + $link = common_local_url( + 'showfavorites', + array('nickname' => $this->user->nickname) + ); + $this->showRssTimeline( + $this->notices, + $title, + $link, + $subtitle, + null, + $logo + ); break; case 'atom': - $selfuri = common_root_url() . - ltrim($_SERVER['QUERY_STRING'], 'p='); - $this->showAtomTimeline( - $this->notices, $title, $id, $link, $subtitle, - null, $selfuri, $logo + + header('Content-Type: application/atom+xml; charset=utf-8'); + + $atom = new AtomNoticeFeed(); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($logo); + $atom->setUpdated('now'); + + $atom->addLink( + common_local_url( + 'showfavorites', + array('nickname' => $this->user->nickname) + ) + ); + + $id = $this->arg('id'); + $aargs = array('format' => 'atom'); + if (!empty($id)) { + $aargs['id'] = $id; + } + + $atom->addLink( + $this->getSelfUri('ApiTimelineFavorites', $aargs), + array('rel' => 'self', 'type' => 'application/atom+xml') ); + + $atom->addEntryFromNotices($this->notices); + + $this->raw($atom->getString()); + break; case 'json': $this->showJsonTimeline($this->notices); diff --git a/actions/apitimelinefriends.php b/actions/apitimelinefriends.php index 4e3827bae..0af04fe4f 100644 --- a/actions/apitimelinefriends.php +++ b/actions/apitimelinefriends.php @@ -114,39 +114,71 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction $title = sprintf(_("%s and friends"), $this->user->nickname); $taguribase = common_config('integration', 'taguri'); $id = "tag:$taguribase:FriendsTimeline:" . $this->user->id; - $link = common_local_url( - 'all', array('nickname' => $this->user->nickname) - ); - $subtitle = sprintf( - _('Updates from %1$s and friends on %2$s!'), - $this->user->nickname, $sitename - ); - $logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE); + + $subtitle = sprintf( + _('Updates from %1$s and friends on %2$s!'), + $this->user->nickname, $sitename + ); + + $logo = (!empty($avatar)) + ? $avatar->displayUrl() + : Avatar::defaultImage(AVATAR_PROFILE_SIZE); switch($this->format) { case 'xml': $this->showXmlTimeline($this->notices); break; case 'rss': - $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo); + + $link = common_local_url( + 'all', array( + 'nickname' => $this->user->nickname + ) + ); + + $this->showRssTimeline( + $this->notices, + $title, + $link, + $subtitle, + null, + $logo + ); break; case 'atom': - $target_id = $this->arg('id'); + header('Content-Type: application/atom+xml; charset=utf-8'); + + $atom = new AtomNoticeFeed(); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($logo); + $atom->setUpdated('now'); - if (isset($target_id)) { - $selfuri = common_root_url() . - 'api/statuses/friends_timeline/' . - $target_id . '.atom'; - } else { - $selfuri = common_root_url() . - 'api/statuses/friends_timeline.atom'; + $atom->addLink( + common_local_url( + 'all', + array('nickname' => $this->user->nickname) + ) + ); + + $id = $this->arg('id'); + $aargs = array('format' => 'atom'); + if (!empty($id)) { + $aargs['id'] = $id; } - $this->showAtomTimeline( - $this->notices, $title, $id, $link, - $subtitle, null, $selfuri, $logo - ); + $atom->addLink( + $this->getSelfUri('ApiTimelineFriends', $aargs), + array('rel' => 'self', 'type' => 'application/atom+xml') + ); + + $atom->addEntryFromNotices($this->notices); + + $this->raw($atom->getString()); + break; case 'json': $this->showJsonTimeline($this->notices); diff --git a/actions/apitimelinegroup.php b/actions/apitimelinegroup.php index fd2ed9ff9..3c74e36b5 100644 --- a/actions/apitimelinegroup.php +++ b/actions/apitimelinegroup.php @@ -109,39 +109,83 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction $title = sprintf(_("%s timeline"), $this->group->nickname); $taguribase = common_config('integration', 'taguri'); $id = "tag:$taguribase:GroupTimeline:" . $this->group->id; - $link = common_local_url( - 'showgroup', - array('nickname' => $this->group->nickname) - ); + $subtitle = sprintf( _('Updates from %1$s on %2$s!'), $this->group->nickname, $sitename ); - $logo = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE); + + $logo = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE); switch($this->format) { case 'xml': $this->showXmlTimeline($this->notices); break; case 'rss': - $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo); - break; - case 'atom': - $selfuri = common_root_url() . - 'api/statusnet/groups/timeline/' . - $this->group->id . '.atom'; - $this->showAtomTimeline( + $this->showRssTimeline( $this->notices, $title, - $id, - $link, + $this->group->homeUrl(), $subtitle, null, - $selfuri, $logo ); break; + case 'atom': + + header('Content-Type: application/atom+xml; charset=utf-8'); + + try { + + // If this was called using an integer ID, i.e.: using the canonical + // URL for this group's feed, then pass the Group object into the feed, + // so the OStatus plugin, and possibly other plugins, can access it. + // Feels sorta hacky. -- Z + + $atom = null; + $id = $this->arg('id'); + + if (strval(intval($id)) === strval($id)) { + $atom = new AtomGroupNoticeFeed($this->group); + } else { + $atom = new AtomGroupNoticeFeed(); + } + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($logo); + $atom->setUpdated('now'); + + $atom->addAuthorRaw($this->group->asAtomAuthor()); + $atom->setActivitySubject($this->group->asActivitySubject()); + + $atom->addLink($this->group->homeUrl()); + + $id = $this->arg('id'); + $aargs = array('format' => 'atom'); + if (!empty($id)) { + $aargs['id'] = $id; + } + + $atom->addLink( + $this->getSelfUri('ApiTimelineGroup', $aargs), + array('rel' => 'self', 'type' => 'application/atom+xml') + ); + + $atom->addEntryFromNotices($this->notices); + + $this->raw($atom->getString()); + + } catch (Atom10FeedException $e) { + $this->serverError( + 'Could not generate feed for group - ' . $e->getMessage() + ); + return; + } + + break; case 'json': $this->showJsonTimeline($this->notices); break; diff --git a/actions/apitimelinehome.php b/actions/apitimelinehome.php index 828eae6cf..ae4168070 100644 --- a/actions/apitimelinehome.php +++ b/actions/apitimelinehome.php @@ -115,39 +115,67 @@ class ApiTimelineHomeAction extends ApiBareAuthAction $title = sprintf(_("%s and friends"), $this->user->nickname); $taguribase = common_config('integration', 'taguri'); $id = "tag:$taguribase:HomeTimeline:" . $this->user->id; - $link = common_local_url( - 'all', array('nickname' => $this->user->nickname) - ); + $subtitle = sprintf( _('Updates from %1$s and friends on %2$s!'), $this->user->nickname, $sitename ); - $logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE); + + $logo = (!empty($avatar)) + ? $avatar->displayUrl() + : Avatar::defaultImage(AVATAR_PROFILE_SIZE); switch($this->format) { case 'xml': $this->showXmlTimeline($this->notices); break; case 'rss': - $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo); + $link = common_local_url( + 'all', + array('nickname' => $this->user->nickname) + ); + $this->showRssTimeline( + $this->notices, + $title, + $link, + $subtitle, + null, + $logo + ); break; case 'atom': - $target_id = $this->arg('id'); + header('Content-Type: application/atom+xml; charset=utf-8'); + + $atom = new AtomNoticeFeed(); - if (isset($target_id)) { - $selfuri = common_root_url() . - 'api/statuses/home_timeline/' . - $target_id . '.atom'; - } else { - $selfuri = common_root_url() . - 'api/statuses/home_timeline.atom'; + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($logo); + $atom->setUpdated('now'); + + $atom->addLink( + common_local_url( + 'all', + array('nickname' => $this->user->nickname) + ) + ); + + $id = $this->arg('id'); + $aargs = array('format' => 'atom'); + if (!empty($id)) { + $aargs['id'] = $id; } - $this->showAtomTimeline( - $this->notices, $title, $id, $link, - $subtitle, null, $selfuri, $logo + $atom->addLink( + $this->getSelfUri('ApiTimelineHome', $aargs), + array('rel' => 'self', 'type' => 'application/atom+xml') ); + + $atom->addEntryFromNotices($this->notices); + $this->raw($atom->getString()); + break; case 'json': $this->showJsonTimeline($this->notices); diff --git a/actions/apitimelinementions.php b/actions/apitimelinementions.php index 9dc2162cc..d2e31d0bd 100644 --- a/actions/apitimelinementions.php +++ b/actions/apitimelinementions.php @@ -137,12 +137,36 @@ class ApiTimelineMentionsAction extends ApiBareAuthAction $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo); break; case 'atom': - $selfuri = common_root_url() . - ltrim($_SERVER['QUERY_STRING'], 'p='); - $this->showAtomTimeline( - $this->notices, $title, $id, $link, $subtitle, - null, $selfuri, $logo + + $atom = new AtomNoticeFeed(); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($logo); + $atom->setUpdated('now'); + + $atom->addLink( + common_local_url( + 'replies', + array('nickname' => $this->user->nickname) + ) + ); + + $id = $this->arg('id'); + $aargs = array('format' => 'atom'); + if (!empty($id)) { + $aargs['id'] = $id; + } + + $atom->addLink( + $this->getSelfUri('ApiTimelineMentions', $aargs), + array('rel' => 'self', 'type' => 'application/atom+xml') ); + + $atom->addEntryFromNotices($this->notices); + $this->raw($atom->getString()); + break; case 'json': $this->showJsonTimeline($this->notices); diff --git a/actions/apitimelinepublic.php b/actions/apitimelinepublic.php index 3f4a46c0f..c1fa72a3e 100644 --- a/actions/apitimelinepublic.php +++ b/actions/apitimelinepublic.php @@ -75,6 +75,10 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction $this->notices = $this->getNotices(); + if ($this->since) { + throw new ServerException("since parameter is disabled for performance; use since_id", 403); + } + return true; } @@ -118,11 +122,28 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo); break; case 'atom': - $selfuri = common_root_url() . 'api/statuses/public_timeline.atom'; - $this->showAtomTimeline( - $this->notices, $title, $id, $link, - $subtitle, null, $selfuri, $sitelogo + + $atom = new AtomNoticeFeed(); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($sitelogo); + $atom->setUpdated('now'); + + $atom->addLink(common_local_url('public')); + + $atom->addLink( + $this->getSelfUri( + 'ApiTimelinePublic', array('format' => 'atom') + ), + array('rel' => 'self', 'type' => 'application/atom+xml') ); + + $atom->addEntryFromNotices($this->notices); + + $this->raw($atom->getString()); + break; case 'json': $this->showJsonTimeline($this->notices); @@ -145,7 +166,7 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction $notice = Notice::publicStream( ($this->page - 1) * $this->count, $this->count, $this->since_id, - $this->max_id, $this->since + $this->max_id ); while ($notice->fetch()) { diff --git a/actions/apitimelineretweetsofme.php b/actions/apitimelineretweetsofme.php index e4b09e9bd..26706a75e 100644 --- a/actions/apitimelineretweetsofme.php +++ b/actions/apitimelineretweetsofme.php @@ -99,6 +99,8 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction $strm = $this->auth_user->repeatsOfMe($offset, $limit, $this->since_id, $this->max_id); + common_debug(var_export($strm, true)); + switch ($this->format) { case 'xml': $this->showXmlTimeline($strm); @@ -112,10 +114,38 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction $title = sprintf(_("Repeats of %s"), $this->auth_user->nickname); $taguribase = common_config('integration', 'taguri'); $id = "tag:$taguribase:RepeatsOfMe:" . $this->auth_user->id; - $link = common_local_url('showstream', - array('nickname' => $this->auth_user->nickname)); - $this->showAtomTimeline($strm, $title, $id, $link); + header('Content-Type: application/atom+xml; charset=utf-8'); + + $atom = new AtomNoticeFeed(); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setUpdated('now'); + + $atom->addLink( + common_local_url( + 'showstream', + array('nickname' => $this->auth_user->nickname) + ) + ); + + $id = $this->arg('id'); + $aargs = array('format' => 'atom'); + if (!empty($id)) { + $aargs['id'] = $id; + } + + $atom->addLink( + $this->getSelfUri('ApiTimelineRetweetsOfMe', $aargs), + array('rel' => 'self', 'type' => 'application/atom+xml') + ); + + $atom->addEntryFromNotices($strm); + + $this->raw($atom->getString()); + break; default: diff --git a/actions/apitimelinetag.php b/actions/apitimelinetag.php index 1427d23b6..5b6ded4c0 100644 --- a/actions/apitimelinetag.php +++ b/actions/apitimelinetag.php @@ -100,10 +100,6 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction $sitename = common_config('site', 'name'); $sitelogo = (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png'); $title = sprintf(_("Notices tagged with %s"), $this->tag); - $link = common_local_url( - 'tag', - array('tag' => $this->tag) - ); $subtitle = sprintf( _('Updates tagged with %1$s on %2$s!'), $this->tag, @@ -117,23 +113,52 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction $this->showXmlTimeline($this->notices); break; case 'rss': - $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo); - break; - case 'atom': - $selfuri = common_root_url() . - 'api/statusnet/tags/timeline/' . - $this->tag . '.atom'; - $this->showAtomTimeline( + $link = common_local_url( + 'tag', + array('tag' => $this->tag) + ); + $this->showRssTimeline( $this->notices, $title, - $id, $link, $subtitle, null, - $selfuri, $sitelogo ); break; + case 'atom': + + header('Content-Type: application/atom+xml; charset=utf-8'); + + $atom = new AtomNoticeFeed(); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($logo); + $atom->setUpdated('now'); + + $atom->addLink( + common_local_url( + 'tag', + array('tag' => $this->tag) + ) + ); + + $aargs = array('format' => 'atom'); + if (!empty($this->tag)) { + $aargs['tag'] = $this->tag; + } + + $atom->addLink( + $this->getSelfUri('ApiTimelineTag', $aargs), + array('rel' => 'self', 'type' => 'application/atom+xml') + ); + + $atom->addEntryFromNotices($this->notices); + $this->raw($atom->getString()); + + break; case 'json': $this->showJsonTimeline($this->notices); break; diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index ed9104905..24752e45f 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -145,19 +145,59 @@ class ApiTimelineUserAction extends ApiBareAuthAction ); break; case 'atom': + + header('Content-Type: application/atom+xml; charset=utf-8'); + + // If this was called using an integer ID, i.e.: using the canonical + // URL for this user's feed, then pass the User object into the feed, + // so the OStatus plugin, and possibly other plugins, can access it. + // Feels sorta hacky. -- Z + + $atom = null; $id = $this->arg('id'); - if ($id) { - $selfuri = common_root_url() . - 'api/statuses/user_timeline/' . - rawurlencode($id) . '.atom'; + + if (strval(intval($id)) === strval($id)) { + $atom = new AtomUserNoticeFeed($this->user); } else { - $selfuri = common_root_url() . - 'api/statuses/user_timeline.atom'; + $atom = new AtomUserNoticeFeed(); } - $this->showAtomTimeline( - $this->notices, $title, $id, $link, - $subtitle, $suplink, $selfuri, $logo + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($logo); + $atom->setUpdated('now'); + + $atom->addLink( + common_local_url( + 'showstream', + array('nickname' => $this->user->nickname) + ) ); + + $id = $this->arg('id'); + $aargs = array('format' => 'atom'); + if (!empty($id)) { + $aargs['id'] = $id; + } + + $atom->addLink( + $this->getSelfUri('ApiTimelineUser', $aargs), + array('rel' => 'self', 'type' => 'application/atom+xml') + ); + + $atom->addLink( + $suplink, + array( + 'rel' => 'http://api.friendfeed.com/2008/03#sup', + 'type' => 'application/json' + ) + ); + + $atom->addEntryFromNotices($this->notices); + + $this->raw($atom->getString()); + break; case 'json': $this->showJsonTimeline($this->notices); diff --git a/classes/Avatar.php b/classes/Avatar.php index 91bde0f04..dbe2cd813 100644 --- a/classes/Avatar.php +++ b/classes/Avatar.php @@ -82,9 +82,20 @@ class Avatar extends Memcached_DataObject $server = common_config('site', 'server'); } - // XXX: protocol + $ssl = common_config('avatar', 'ssl'); + + 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 'http://'.$server.$path.$filename; + return $protocol.'://'.$server.$path.$filename; } function displayUrl() diff --git a/classes/Design.php b/classes/Design.php index 4e7d7dfb2..ff44e0109 100644 --- a/classes/Design.php +++ b/classes/Design.php @@ -155,9 +155,20 @@ class Design extends Memcached_DataObject $server = common_config('site', 'server'); } - // XXX: protocol + $ssl = common_config('background', 'ssl'); + + 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 'http://'.$server.$path.$filename; + return $protocol.'://'.$server.$path.$filename; } function setDisposition($on, $off, $tile) diff --git a/classes/File.php b/classes/File.php index ee418a802..91b12d2e2 100644 --- a/classes/File.php +++ b/classes/File.php @@ -228,9 +228,20 @@ class File extends Memcached_DataObject $server = common_config('site', 'server'); } - // XXX: protocol + $ssl = common_config('attachments', 'ssl'); - return 'http://'.$server.$path.$filename; + if (is_null($ssl)) { // null -> guess + if (common_config('site', 'ssl') == 'always' && + !common_config('attachments', 'server')) { + $ssl = true; + } else { + $ssl = false; + } + } + + $protocol = ($ssl) ? 'https' : 'http'; + + return $protocol.'://'.$server.$path.$filename; } } diff --git a/classes/Nonce.php b/classes/Nonce.php index 486a65a3c..2f8ab00b5 100644 --- a/classes/Nonce.php +++ b/classes/Nonce.php @@ -22,4 +22,19 @@ class Nonce extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE + + /** + * Compatibility hack for PHP 5.3 + * + * The statusnet.links.ini entry cannot be read because "," is no longer + * allowed in key names when read by parse_ini_file(). + * + * @return array + * @access public + */ + function links() + { + return array('consumer_key,token' => 'token:consumer_key,token'); + } + } diff --git a/classes/Notice.php b/classes/Notice.php index 247440f29..73b22d58a 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -957,7 +957,10 @@ class Notice extends Memcached_DataObject if ($namespace) { $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom', - 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'); + 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0', + 'xmlns:georss' => 'http://www.georss.org/georss', + 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', + 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0'); } else { $attrs = array(); } @@ -983,11 +986,6 @@ class Notice extends Memcached_DataObject $xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE)); } - $xs->elementStart('author'); - $xs->element('name', null, $profile->nickname); - $xs->element('uri', null, $profile->profileurl); - $xs->elementEnd('author'); - if ($source) { $xs->elementEnd('source'); } @@ -995,6 +993,9 @@ class Notice extends Memcached_DataObject $xs->element('title', null, $this->content); $xs->element('summary', null, $this->content); + $xs->raw($profile->asAtomAuthor()); + $xs->raw($profile->asActivityActor()); + $xs->element('link', array('rel' => 'alternate', 'href' => $this->bestUrl())); @@ -1014,6 +1015,43 @@ class Notice extends Memcached_DataObject } } + if (!empty($this->conversation) + && $this->conversation != $this->id) { + $xs->element( + 'link', array( + 'rel' => 'ostatus:conversation', + 'href' => common_local_url( + 'conversation', + array('id' => $this->conversation) + ) + ) + ); + } + + $reply_ids = $this->getReplies(); + + foreach ($reply_ids as $id) { + $profile = Profile::staticGet('id', $id); + if (!empty($profile)) { + $xs->element( + 'link', array( + 'rel' => 'ostatus:attention', + 'href' => $profile->getAcctUri() + ) + ); + } + } + + if (!empty($this->repeat_of)) { + $repeat = Notice::staticGet('id', $this->repeat_of); + if (!empty($repeat)) { + $xs->element( + 'ostatus:forward', + array('ref' => $repeat->uri, 'href' => $repeat->bestUrl()) + ); + } + } + $xs->element('content', array('type' => 'html'), $this->rendered); $tag = new Notice_tag(); @@ -1041,9 +1079,7 @@ class Notice extends Memcached_DataObject } if (!empty($this->lat) && !empty($this->lon)) { - $xs->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss')); $xs->element('georss:point', null, $this->lat . ' ' . $this->lon); - $xs->elementEnd('geo'); } $xs->elementEnd('entry'); diff --git a/classes/Profile.php b/classes/Profile.php index feabc2508..ab05bb854 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -754,4 +754,89 @@ class Profile extends Memcached_DataObject return !empty($notice); } + + /** + * Returns an XML string fragment with limited profile information + * as an Atom <author> element. + * + * Assumes that Atom has been previously set up as the base namespace. + * + * @return string + */ + function asAtomAuthor() + { + $xs = new XMLStringer(true); + + $xs->elementStart('author'); + $xs->element('name', null, $this->nickname); + $xs->element('uri', null, $this->profileurl); + $xs->elementEnd('author'); + + return $xs->getString(); + } + + /** + * Returns an XML string fragment with profile information as an + * Activity Streams <activity:actor> element. + * + * Assumes that 'activity' namespace has been previously defined. + * + * @return string + */ + function asActivityActor() + { + return $this->asActivityNoun('actor'); + } + + /** + * Returns an XML string fragment with profile information as an + * Activity Streams noun object with the given element type. + * + * Assumes that 'activity' namespace has been previously defined. + * + * @param string $element one of 'actor', 'subject', 'object', 'target' + * @return string + */ + function asActivityNoun($element) + { + $xs = new XMLStringer(true); + + $xs->elementStart('activity:' . $element); + $xs->element( + 'activity:object-type', + null, + 'http://activitystrea.ms/schema/1.0/person' + ); + $xs->element( + 'id', + null, + common_local_url( + 'userbyid', + array('id' => $this->id) + ) + ); + $xs->element('title', null, $this->getBestName()); + + $avatar = $this->getAvatar(AVATAR_PROFILE_SIZE); + + $xs->element( + 'link', array( + 'type' => empty($avatar) ? 'image/png' : $avatar->mediatype, + 'href' => empty($avatar) + ? Avatar::defaultImage(AVATAR_PROFILE_SIZE) + : $avatar->displayUrl() + ), + '' + ); + + $xs->elementEnd('activity:' . $element); + + return $xs->getString(); + } + + function getAcctUri() + { + return $this->nickname . '@' . common_config('site', 'server'); + } + } diff --git a/classes/User_group.php b/classes/User_group.php index 1fbb50a6e..379e6b721 100644 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -355,6 +355,39 @@ class User_group extends Memcached_DataObject return $xs->getString(); } + function asAtomAuthor() + { + $xs = new XMLStringer(true); + + $xs->elementStart('author'); + $xs->element('name', null, $this->nickname); + $xs->element('uri', null, $this->permalink()); + $xs->elementEnd('author'); + + return $xs->getString(); + } + + function asActivitySubject() + { + $xs = new XMLStringer(true); + + $xs->elementStart('activity:subject'); + $xs->element('activity:object', null, 'http://activitystrea.ms/schema/1.0/group'); + $xs->element('id', null, $this->permalink()); + $xs->element('title', null, $this->getBestName()); + $xs->element( + 'link', array( + 'rel' => 'avatar', + 'href' => empty($this->homepage_logo) + ? User_group::defaultLogo(AVATAR_PROFILE_SIZE) + : $this->homepage_logo + ) + ); + $xs->elementEnd('activity:subject'); + + return $xs->getString(); + } + static function register($fields) { // MAGICALLY put fields into current scope diff --git a/classes/statusnet.links.ini b/classes/statusnet.links.ini index 7f233e676..b9dd5af0c 100644 --- a/classes/statusnet.links.ini +++ b/classes/statusnet.links.ini @@ -19,8 +19,11 @@ profile_id = profile:id [token] consumer_key = consumer:consumer_key -[nonce] -consumer_key,token = token:consumer_key,token +; Compatibility hack for PHP 5.3 +; This entry has been moved to the class definition, as commas are no longer +; considered valid in keys, causing parse_ini_file() to reject the whole file. +;[nonce] +;consumer_key,token = token:consumer_key,token [confirm_address] user_id = user:id diff --git a/lib/action.php b/lib/action.php index cc4f4aad0..b85f353a3 100644 --- a/lib/action.php +++ b/lib/action.php @@ -405,6 +405,7 @@ class Action extends HTMLOutputter // lawsuit 'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png'), 'alt' => common_config('site', 'name'))); } + $this->text(' '); $this->element('span', array('class' => 'fn org'), common_config('site', 'name')); $this->elementEnd('a'); Event::handle('EndAddressData', array($this)); @@ -822,12 +823,14 @@ class Action extends HTMLOutputter // lawsuit 'alt' => common_config('license', 'title'), 'width' => '80', 'height' => '15')); + $this->text(' '); //TODO: This is dirty: i18n $this->text(_('All '.common_config('site', 'name').' content and data are available under the ')); $this->element('a', array('class' => 'license', 'rel' => 'external license', 'href' => common_config('license', 'url')), common_config('license', 'title')); + $this->text(' '); $this->text(_('license.')); $this->elementEnd('p'); break; diff --git a/lib/api.php b/lib/api.php index fd07bbbbe..22eef7436 100644 --- a/lib/api.php +++ b/lib/api.php @@ -1103,7 +1103,7 @@ class ApiAction extends Action } } - function serverError($msg, $code = 500, $content_type = 'json') + function serverError($msg, $code = 500, $content_type = 'xml') { $action = $this->trimmed('action'); @@ -1154,7 +1154,6 @@ class ApiAction extends Action $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom', 'xml:lang' => 'en-US', 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0')); - Event::handle('StartApiAtom', array($this)); } function endTwitterAtom() @@ -1321,4 +1320,22 @@ class ApiAction extends Action } } + function getSelfUri($action, $aargs) + { + parse_str($_SERVER['QUERY_STRING'], $params); + $pstring = ''; + if (!empty($params)) { + unset($params['p']); + $pstring = http_build_query($params); + } + + $uri = common_local_url($action, $aargs); + + if (!empty($pstring)) { + $uri .= '?' . $pstring; + } + + return $uri; + } + } diff --git a/lib/atom10entry.php b/lib/atom10entry.php new file mode 100644 index 000000000..5710c80fc --- /dev/null +++ b/lib/atom10entry.php @@ -0,0 +1,106 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Class for building / manipulating an Atom entry in memory + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Feed + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') +{ + exit(1); +} + +class Atom10EntryException extends Exception +{ +} + +/** + * Class for manipulating an Atom entry in memory. Get the entry as an XML + * string with Atom10Entry::getString(). + * + * @category Feed + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class Atom10Entry extends XMLStringer +{ + private $namespaces; + private $categories; + private $content; + private $contributors; + private $id; + private $links; + private $published; + private $rights; + private $source; + private $summary; + private $title; + + function __construct($indent = true) { + parent::__construct($indent); + $this->namespaces = array(); + } + + function addNamespace($namespace, $uri) + { + $ns = array($namespace => $uri); + $this->namespaces = array_merge($this->namespaces, $ns); + } + + function initEntry() + { + + } + + function endEntry() + { + + } + + /** + * Check that all required elements have been set, etc. + * Throws an Atom10EntryException if something's missing. + * + * @return void + */ + function validate + { + + } + + function getString() + { + $this->validate(); + + $this->initEntry(); + $this->renderEntries(); + $this->endEntry(); + + return $this->xw->outputMemory(); + } + +}
\ No newline at end of file diff --git a/lib/atom10feed.php b/lib/atom10feed.php new file mode 100644 index 000000000..14a3beb83 --- /dev/null +++ b/lib/atom10feed.php @@ -0,0 +1,298 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Class for building an Atom feed in memory + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Feed + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) +{ + exit(1); +} + +class Atom10FeedException extends Exception +{ +} + +/** + * Class for building an Atom feed in memory. Get the finished doc + * as a string with Atom10Feed::getString(). + * + * @category Feed + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class Atom10Feed extends XMLStringer +{ + public $xw; + private $namespaces; + private $authors; + private $subject; + private $categories; + private $contributors; + private $generator; + private $icon; + private $links; + private $logo; + private $rights; + private $subtitle; + private $title; + private $published; + private $updated; + private $entries; + + /** + * Constructor + * + * @param boolean $indent flag to turn indenting on or off + * + * @return void + */ + function __construct($indent = true) { + parent::__construct($indent); + $this->namespaces = array(); + $this->authors = array(); + $this->links = array(); + $this->entries = array(); + $this->addNamespace('xmlns', 'http://www.w3.org/2005/Atom'); + } + + /** + * Add another namespace to the feed + * + * @param string $namespace the namespace + * @param string $uri namspace uri + * + * @return void + */ + function addNamespace($namespace, $uri) + { + $ns = array($namespace => $uri); + $this->namespaces = array_merge($this->namespaces, $ns); + } + + function addAuthor($name, $uri = null, $email = null) + { + $xs = new XMLStringer(true); + + $xs->elementStart('author'); + + if (!empty($name)) { + $xs->element('name', null, $name); + } else { + throw new Atom10FeedException( + 'author element must contain a name element.' + ); + } + + if (!is_null($uri)) { + $xs->element('uri', null, $uri); + } + + if (!is_null(email)) { + $xs->element('email', null, $email); + } + + $xs->elementEnd('author'); + + array_push($this->authors, $xs->getString()); + } + + /** + * Add an Author to the feed via raw XML string + * + * @param string $xmlAuthor An XML string representation author + * + * @return void + */ + function addAuthorRaw($xmlAuthor) + { + array_push($this->authors, $xmlAuthor); + } + + function renderAuthors() + { + foreach ($this->authors as $author) { + $this->raw($author); + } + } + + /** + * Add a activity feed subject via raw XML string + * + * @param string $xmlSubject An XML string representation of the subject + * + * @return void + */ + function setActivitySubject($xmlSubject) + { + $this->subject = $xmlSubject; + } + + function getNamespaces() + { + return $this->namespaces; + } + + function initFeed() + { + $this->xw->startDocument('1.0', 'UTF-8'); + $commonAttrs = array('xml:lang' => 'en-US'); + $commonAttrs = array_merge($commonAttrs, $this->namespaces); + $this->elementStart('feed', $commonAttrs); + + $this->element('id', null, $this->id); + $this->element('title', null, $this->title); + $this->element('subtitle', null, $this->subtitle); + + if (!empty($this->logo)) { + $this->element('logo', null, $this->logo); + } + + $this->element('updated', null, $this->updated); + + $this->renderAuthors(); + + $this->renderLinks(); + } + + /** + * Check that all required elements have been set, etc. + * Throws an Atom10FeedException if something's missing. + * + * @return void + */ + function validate() + { + } + + function renderLinks() + { + foreach ($this->links as $attrs) + { + $this->element('link', $attrs, null); + } + } + + function addEntryRaw($xmlEntry) + { + array_push($this->entries, $xmlEntry); + } + + function addEntry($entry) + { + array_push($this->entries, $entry->getString()); + } + + function renderEntries() + { + foreach ($this->entries as $entry) { + $this->raw($entry); + } + } + + function endFeed() + { + $this->elementEnd('feed'); + $this->xw->endDocument(); + } + + function getString() + { + if (Event::handle('StartApiAtom', array($this))) { + + $this->validate(); + $this->initFeed(); + + if (!empty($this->subject)) { + $this->raw($this->subject); + } + + $this->renderEntries(); + $this->endFeed(); + + Event::handle('EndApiAtom', array($this)); + } + + return $this->xw->outputMemory(); + } + + function setId($id) + { + $this->id = $id; + } + + function setTitle($title) + { + $this->title = $title; + } + + function setSubtitle($subtitle) + { + $this->subtitle = $subtitle; + } + + function setLogo($logo) + { + $this->logo = $logo; + } + + function setUpdated($dt) + { + $this->updated = common_date_iso8601($dt); + } + + function setPublished($dt) + { + $this->published = common_date_iso8601($dt); + } + + /** + * Adds a link element into the Atom document + * + * Assumes you want rel="alternate" and type="text/html" unless + * you send in $otherAttrs. + * + * @param string $uri the uri the href needs to point to + * @param array $otherAttrs other attributes to stick in + * + * @return void + */ + function addLink($uri, $otherAttrs = null) { + $attrs = array('href' => $uri); + + if (is_null($otherAttrs)) { + $attrs['rel'] = 'alternate'; + $attrs['type'] = 'text/html'; + } else { + $attrs = array_merge($attrs, $otherAttrs); + } + + array_push($this->links, $attrs); + } + +} diff --git a/lib/atomgroupnoticefeed.php b/lib/atomgroupnoticefeed.php new file mode 100644 index 000000000..52ee4c7d6 --- /dev/null +++ b/lib/atomgroupnoticefeed.php @@ -0,0 +1,67 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Class for building an in-memory Atom feed for a particular group's + * timeline. + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Feed + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) +{ + exit(1); +} + +/** + * Class for group notice feeds. May contains a reference to the group. + * + * @category Feed + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class AtomGroupNoticeFeed extends AtomNoticeFeed +{ + private $group; + + /** + * Constructor + * + * @param Group $group the group for the feed (optional) + * @param boolean $indent flag to turn indenting on or off + * + * @return void + */ + function __construct($group = null, $indent = true) { + parent::__construct($indent); + $this->group = $group; + } + + function getGroup() + { + return $this->group; + } + +} diff --git a/lib/atomnoticefeed.php b/lib/atomnoticefeed.php new file mode 100644 index 000000000..b7a60bde6 --- /dev/null +++ b/lib/atomnoticefeed.php @@ -0,0 +1,105 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Class for building an Atom feed from a collection of notices + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Feed + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) +{ + exit(1); +} + +/** + * Class for creating a feed that represents a collection of notices. Builds the + * feed in memory. Get the feed as a string with AtomNoticeFeed::getString(). + * + * @category Feed + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class AtomNoticeFeed extends Atom10Feed +{ + function __construct($indent = true) { + parent::__construct($indent); + + // Feeds containing notice info use these namespaces + + $this->addNamespace( + 'xmlns:thr', + 'http://purl.org/syndication/thread/1.0' + ); + + $this->addNamespace( + 'xmlns:georss', + 'http://www.georss.org/georss' + ); + + $this->addNamespace( + 'xmlns:activity', + 'http://activitystrea.ms/spec/1.0/' + ); + + // XXX: What should the uri be? + $this->addNamespace( + 'xmlns:ostatus', + 'http://ostatus.org/schema/1.0' + ); + } + + /** + * Add more than one Notice to the feed + * + * @param mixed $notices an array of Notice objects or handle + * + */ + function addEntryFromNotices($notices) + { + if (is_array($notices)) { + foreach ($notices as $notice) { + $this->addEntryFromNotice($notice); + } + } else { + while ($notices->fetch()) { + $this->addEntryFromNotice($notices); + } + } + } + + /** + * Add a single Notice to the feed + * + * @param Notice $notice a Notice to add + */ + function addEntryFromNotice($notice) + { + $this->addEntryRaw($notice->asAtomEntry()); + } + +} + + diff --git a/lib/atomusernoticefeed.php b/lib/atomusernoticefeed.php new file mode 100644 index 000000000..9f224325c --- /dev/null +++ b/lib/atomusernoticefeed.php @@ -0,0 +1,66 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Class for building an in-memory Atom feed for a particular user's + * timeline. + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Feed + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) +{ + exit(1); +} + +/** + * Class for user notice feeds. May contain a reference to the user. + * + * @category Feed + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class AtomUserNoticeFeed extends AtomNoticeFeed +{ + private $user; + + /** + * Constructor + * + * @param User $user the user for the feed (optional) + * @param boolean $indent flag to turn indenting on or off + * + * @return void + */ + function __construct($user = null, $indent = true) { + parent::__construct($indent); + $this->user = $user; + } + + function getUser() + { + return $this->user; + } +} diff --git a/lib/default.php b/lib/default.php index bf4b83718..8b1fe2769 100644 --- a/lib/default.php +++ b/lib/default.php @@ -111,11 +111,13 @@ $default = 'avatar' => array('server' => null, 'dir' => INSTALLDIR . '/avatar/', - 'path' => $_path . '/avatar/'), + 'path' => $_path . '/avatar/', + 'ssl' => null), 'background' => array('server' => null, 'dir' => INSTALLDIR . '/background/', - 'path' => $_path . '/background/'), + 'path' => $_path . '/background/', + 'ssl' => null), 'public' => array('localonly' => true, 'blacklist' => array(), @@ -123,10 +125,12 @@ $default = 'theme' => array('server' => null, 'dir' => null, - 'path'=> null), + 'path'=> null, + 'ssl' => null), 'javascript' => array('server' => null, - 'path'=> null), + 'path'=> null, + 'ssl' => null), 'throttle' => array('enabled' => false, // whether to throttle edits; false by default 'count' => 20, // number of allowed messages in timespan @@ -184,6 +188,7 @@ $default = array('server' => null, 'dir' => INSTALLDIR . '/file/', 'path' => $_path . '/file/', + 'ssl' => null, 'supported' => array('image/png', 'image/jpeg', 'image/gif', diff --git a/lib/grouplist.php b/lib/grouplist.php index 99bff9cdc..854bc34e2 100644 --- a/lib/grouplist.php +++ b/lib/grouplist.php @@ -105,6 +105,7 @@ class GroupList extends Widget 'alt' => ($this->group->fullname) ? $this->group->fullname : $this->group->nickname)); + $this->out->text(' '); $hasFN = ($this->group->fullname) ? 'nickname' : 'fn org nickname'; $this->out->elementStart('span', $hasFN); $this->out->raw($this->highlight($this->group->nickname)); @@ -112,16 +113,19 @@ class GroupList extends Widget $this->out->elementEnd('a'); if ($this->group->fullname) { + $this->out->text(' '); $this->out->elementStart('span', 'fn org'); $this->out->raw($this->highlight($this->group->fullname)); $this->out->elementEnd('span'); } if ($this->group->location) { + $this->out->text(' '); $this->out->elementStart('span', 'label'); $this->out->raw($this->highlight($this->group->location)); $this->out->elementEnd('span'); } if ($this->group->homepage) { + $this->out->text(' '); $this->out->elementStart('a', array('href' => $this->group->homepage, 'class' => 'url')); $this->out->raw($this->highlight($this->group->homepage)); diff --git a/lib/groupsection.php b/lib/groupsection.php index 7327f9e1a..3b0b3029d 100644 --- a/lib/groupsection.php +++ b/lib/groupsection.php @@ -85,9 +85,9 @@ class GroupSection extends Section 'href' => $group->homeUrl(), 'rel' => 'contact group', 'class' => 'url')); + $this->out->text(' '); $logo = ($group->stream_logo) ? $group->stream_logo : User_group::defaultLogo(AVATAR_STREAM_SIZE); - $this->out->element('img', array('src' => $logo, 'width' => AVATAR_MINI_SIZE, 'height' => AVATAR_MINI_SIZE, @@ -95,6 +95,7 @@ class GroupSection extends Section 'alt' => ($group->fullname) ? $group->fullname : $group->nickname)); + $this->out->text(' '); $this->out->element('span', 'fn org nickname', $group->nickname); $this->out->elementEnd('a'); $this->out->elementEnd('span'); diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php index 317f5ea61..47e56fc8f 100644 --- a/lib/htmloutputter.php +++ b/lib/htmloutputter.php @@ -376,9 +376,20 @@ class HTMLOutputter extends XMLOutputter $server = common_config('site', 'server'); } - // XXX: protocol + $ssl = common_config('javascript', 'ssl'); + + 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 = 'http://'.$server.$path.$src . '?version=' . STATUSNET_VERSION; + $src = $protocol.'://'.$server.$path.$src . '?version=' . STATUSNET_VERSION; } $this->element('script', array('type' => $type, diff --git a/lib/noticelist.php b/lib/noticelist.php index a4a0f2651..c05b99024 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -294,6 +294,7 @@ class NoticeListItem extends Widget } $this->out->elementStart('a', $attrs); $this->showAvatar(); + $this->out->text(' '); $this->showNickname(); $this->out->elementEnd('a'); $this->out->elementEnd('span'); @@ -432,8 +433,10 @@ class NoticeListItem extends Widget $url = $location->getUrl(); + $this->out->text(' '); $this->out->elementStart('span', array('class' => 'location')); $this->out->text(_('at')); + $this->out->text(' '); if (empty($url)) { $this->out->element('span', array('class' => 'geo', 'title' => $latlon), @@ -473,9 +476,11 @@ class NoticeListItem extends Widget function showNoticeSource() { if ($this->notice->source) { + $this->out->text(' '); $this->out->elementStart('span', 'source'); $this->out->text(_('from')); $source_name = _($this->notice->source); + $this->out->text(' '); switch ($this->notice->source) { case 'web': case 'xmpp': @@ -540,6 +545,7 @@ class NoticeListItem extends Widget } } if ($hasConversation){ + $this->out->text(' '); $convurl = common_local_url('conversation', array('id' => $this->notice->conversation)); $this->out->element('a', array('href' => $convurl.'#notice-'.$this->notice->id, @@ -591,12 +597,14 @@ class NoticeListItem extends Widget function showReplyLink() { if (common_logged_in()) { + $this->out->text(' '); $reply_url = common_local_url('newnotice', array('replyto' => $this->profile->nickname, 'inreplyto' => $this->notice->id)); $this->out->elementStart('a', array('href' => $reply_url, 'class' => 'notice_reply', 'title' => _('Reply to this notice'))); $this->out->text(_('Reply')); + $this->out->text(' '); $this->out->element('span', 'notice_id', $this->notice->id); $this->out->elementEnd('a'); } @@ -616,7 +624,7 @@ class NoticeListItem extends Widget if (!empty($user) && ($todel->profile_id == $user->id || $user->hasRight(Right::DELETEOTHERSNOTICE))) { - + $this->out->text(' '); $deleteurl = common_local_url('deletenotice', array('notice' => $todel->id)); $this->out->element('a', array('href' => $deleteurl, @@ -635,6 +643,7 @@ class NoticeListItem extends Widget { $user = common_current_user(); if ($user && $user->id != $this->notice->profile_id) { + $this->out->text(' '); $profile = $user->getProfile(); if ($profile->hasRepeated($this->notice->id)) { $this->out->element('span', array('class' => 'repeated', diff --git a/lib/noticesection.php b/lib/noticesection.php index 24465f8ba..7157feafc 100644 --- a/lib/noticesection.php +++ b/lib/noticesection.php @@ -90,6 +90,7 @@ class NoticeSection extends Section 'alt' => ($profile->fullname) ? $profile->fullname : $profile->nickname)); + $this->out->text(' '); $this->out->element('span', 'fn nickname', $profile->nickname); $this->out->elementEnd('a'); $this->out->elementEnd('span'); diff --git a/lib/profilelist.php b/lib/profilelist.php index 3412d41d1..693cd6449 100644 --- a/lib/profilelist.php +++ b/lib/profilelist.php @@ -191,6 +191,7 @@ class ProfileListItem extends Widget 'alt' => ($this->profile->fullname) ? $this->profile->fullname : $this->profile->nickname)); + $this->out->text(' '); $hasFN = (!empty($this->profile->fullname)) ? 'nickname' : 'fn nickname'; $this->out->elementStart('span', $hasFN); $this->out->raw($this->highlight($this->profile->nickname)); @@ -201,6 +202,7 @@ class ProfileListItem extends Widget function showFullName() { if (!empty($this->profile->fullname)) { + $this->out->text(' '); $this->out->elementStart('span', 'fn'); $this->out->raw($this->highlight($this->profile->fullname)); $this->out->elementEnd('span'); @@ -210,6 +212,7 @@ class ProfileListItem extends Widget function showLocation() { if (!empty($this->profile->location)) { + $this->out->text(' '); $this->out->elementStart('span', 'location'); $this->out->raw($this->highlight($this->profile->location)); $this->out->elementEnd('span'); @@ -219,6 +222,7 @@ class ProfileListItem extends Widget function showHomepage() { if (!empty($this->profile->homepage)) { + $this->out->text(' '); $this->out->elementStart('a', array('href' => $this->profile->homepage, 'class' => 'url')); $this->out->raw($this->highlight($this->profile->homepage)); diff --git a/lib/profilesection.php b/lib/profilesection.php index 504b1b7f7..a9482cd63 100644 --- a/lib/profilesection.php +++ b/lib/profilesection.php @@ -85,6 +85,7 @@ class ProfileSection extends Section 'href' => $profile->profileurl, 'rel' => 'contact member', 'class' => 'url')); + $this->out->text(' '); $avatar = $profile->getAvatar(AVATAR_MINI_SIZE); $this->out->element('img', array('src' => (($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_MINI_SIZE)), 'width' => AVATAR_MINI_SIZE, @@ -93,6 +94,7 @@ class ProfileSection extends Section 'alt' => ($profile->fullname) ? $profile->fullname : $profile->nickname)); + $this->out->text(' '); $this->out->element('span', 'fn nickname', $profile->nickname); $this->out->elementEnd('a'); $this->out->elementEnd('span'); diff --git a/lib/queuemanager.php b/lib/queuemanager.php index afe710e88..149617eb5 100644 --- a/lib/queuemanager.php +++ b/lib/queuemanager.php @@ -155,26 +155,26 @@ abstract class QueueManager extends IoManager } /** - * Encode an object for queued storage. - * Next gen may use serialization. + * Encode an object or variable for queued storage. + * Notice objects are currently stored as an id reference; + * other items are serialized. * - * @param mixed $object + * @param mixed $item * @return string */ - protected function encode($object) + protected function encode($item) { - if ($object instanceof Notice) { - return $object->id; - } else if (is_string($object)) { - return $object; + if ($item instanceof Notice) { + // Backwards compat + return $item->id; } else { - throw new ServerException("Can't queue this type", 500); + return serialize($item); } } /** * Decode an object from queued storage. - * Accepts back-compat notice reference entries and strings for now. + * Accepts notice reference entries and serialized items. * * @param string * @return mixed @@ -182,9 +182,23 @@ abstract class QueueManager extends IoManager protected function decode($frame) { if (is_numeric($frame)) { + // Back-compat for notices... return Notice::staticGet(intval($frame)); - } else { + } elseif (substr($frame, 0, 1) == '<') { + // Back-compat for XML source return $frame; + } else { + // Deserialize! + #$old = error_reporting(); + #error_reporting($old & ~E_NOTICE); + $out = unserialize($frame); + #error_reporting($old); + + if ($out === false && $frame !== 'b:0;') { + common_log(LOG_ERR, "Couldn't unserialize queued frame: $frame"); + return false; + } + return $out; } } diff --git a/lib/stompqueuemanager.php b/lib/stompqueuemanager.php index 6730cd213..cd62c25bd 100644 --- a/lib/stompqueuemanager.php +++ b/lib/stompqueuemanager.php @@ -107,9 +107,10 @@ class StompQueueManager extends QueueManager $message .= ':' . $param; } $this->_connect(); - $result = $this->_send($this->control, - $message, - array ('created' => common_sql_now())); + $con = $this->cons[$this->defaultIdx]; + $result = $con->send($this->control, + $message, + array ('created' => common_sql_now())); if ($result) { $this->_log(LOG_INFO, "Sent control ping to queue daemons: $message"); return true; @@ -368,17 +369,10 @@ class StompQueueManager extends QueueManager foreach ($this->cons as $i => $con) { if ($con) { $this->rollback($i); - $con->unsubscribe($this->control); + $con->disconnect(); + $this->cons[$i] = null; } } - if ($this->sites) { - foreach ($this->sites as $server) { - StatusNet::init($server); - $this->doUnsubscribe(); - } - } else { - $this->doUnsubscribe(); - } return true; } @@ -555,26 +549,14 @@ class StompQueueManager extends QueueManager } $host = $this->cons[$idx]->getServer(); - if (is_numeric($frame->body)) { - $id = intval($frame->body); - $info = "notice $id posted at {$frame->headers['created']} in queue $queue from $host"; - - $notice = Notice::staticGet('id', $id); - if (empty($notice)) { - $this->_log(LOG_WARNING, "Skipping missing $info"); - $this->ack($idx, $frame); - $this->commit($idx); - $this->begin($idx); - $this->stats('badnotice', $queue); - return false; - } - - $item = $notice; - } else { - // @fixme should we serialize, or json, or what here? - $info = "string posted at {$frame->headers['created']} in queue $queue from $host"; - $item = $frame->body; + $item = $this->decode($frame->body); + if (empty($item)) { + $this->_log(LOG_ERR, "Skipping empty or deleted item in queue $queue from $host"); + return true; } + $info = $this->logrep($item) . " posted at " . + $frame->headers['created'] . " in queue $queue from $host"; + $this->_log(LOG_DEBUG, "Dequeued $info"); $handler = $this->getHandler($queue); if (!$handler) { diff --git a/lib/theme.php b/lib/theme.php index 020ce1ac4..0be8c3b9d 100644 --- a/lib/theme.php +++ b/lib/theme.php @@ -110,9 +110,20 @@ class Theme $server = common_config('site', 'server'); } - // XXX: protocol + $ssl = common_config('theme', 'ssl'); + + 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 = 'http://'.$server.$path.$name; + $this->path = $protocol . '://'.$server.$path.$name; } } diff --git a/lib/userprofile.php b/lib/userprofile.php index 07e575085..43dfd05be 100644 --- a/lib/userprofile.php +++ b/lib/userprofile.php @@ -238,9 +238,12 @@ class UserProfile extends Widget if (Event::handle('StartProfilePageActionsElements', array(&$this->out, $this->profile))) { if (empty($cur)) { // not logged in - $this->out->elementStart('li', 'entity_subscribe'); - $this->showRemoteSubscribeLink(); - $this->out->elementEnd('li'); + if (Event::handle('StartProfileRemoteSubscribe', array(&$this->out, $this->profile))) { + $this->out->elementStart('li', 'entity_subscribe'); + $this->showRemoteSubscribeLink(); + $this->out->elementEnd('li'); + Event::handle('EndProfileRemoteSubscribe', array(&$this->out, $this->profile)); + } } else { if ($cur->id == $this->profile->id) { // your own page $this->out->elementStart('li', 'entity_edit'); diff --git a/lib/util.php b/lib/util.php index a07fe49e3..209dc2254 100644 --- a/lib/util.php +++ b/lib/util.php @@ -367,7 +367,8 @@ function common_current_user() if ($_cur === false) { - if (isset($_REQUEST[session_name()]) || (isset($_SESSION['userid']) && $_SESSION['userid'])) { + if (isset($_COOKIE[session_name()]) || isset($_GET[session_name()]) + || (isset($_SESSION['userid']) && $_SESSION['userid'])) { common_ensure_session(); $id = isset($_SESSION['userid']) ? $_SESSION['userid'] : false; if ($id) { diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 62ecaf631..276ca1b3d 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -63,9 +63,9 @@ class OStatusPlugin extends Plugin $m->connect('main/ostatus?nickname=:nickname', array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+')); $m->connect('main/ostatussub', - array('action' => 'ostatussub')); + array('action' => 'ostatussub')); $m->connect('main/ostatussub', - array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+')); + array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+')); // PuSH actions $m->connect('main/push/hub', array('action' => 'pushhub')); @@ -80,6 +80,9 @@ class OStatusPlugin extends Plugin $m->connect('main/salmon/user/:id', array('action' => 'salmon'), array('id' => '[0-9]+')); + $m->connect('main/salmon/group/:id', + array('action' => 'salmongroup'), + array('id' => '[0-9]+')); return true; } @@ -109,24 +112,34 @@ class OStatusPlugin extends Plugin * Set up a PuSH hub link to our internal link for canonical timeline * Atom feeds for users and groups. */ - function onStartApiAtom(Action $action) + function onStartApiAtom(AtomNoticeFeed $feed) { - if ($action instanceof ApiTimelineUserAction || $action instanceof ApiTimelineGroupAction) { - $id = $action->arg('id'); - if (strval(intval($id)) === strval($id)) { - // Canonical form of id in URL? - // Updates will be handled for our internal PuSH hub. - $action->element('link', array('rel' => 'hub', - 'href' => common_local_url('pushhub'))); - - // Also, we'll add in the salmon link - $action->element('link', array('rel' => 'salmon', - 'href' => common_local_url('salmon'))); + $id = null; + + if ($feed instanceof AtomUserNoticeFeed) { + $salmonAction = 'salmon'; + $id = $feed->getUser()->id; + } else if ($feed instanceof AtomGroupNoticeFeed) { + $salmonAction = 'salmongroup'; + $id = $feed->getGroup()->id; + } else { + return; + } + + if (!empty($id)) { + $hub = common_config('ostatus', 'hub'); + if (empty($hub)) { + // Updates will be handled through our internal PuSH hub. + $hub = common_local_url('pushhub'); } + $feed->addLink($hub, array('rel' => 'hub')); + + // Also, we'll add in the salmon link + $salmon = common_local_url($salmonAction, array('id' => $id)); + $feed->addLink($salmon, array('rel' => 'salmon')); } - return true; } - + /** * Add the feed settings page to the Connect Settings menu * @@ -175,7 +188,7 @@ class OStatusPlugin extends Plugin /** * Add in an OStatus subscribe button */ - function onStartProfilePageActionsElements($output, $profile) + function onStartProfileRemoteSubscribe($output, $profile) { $cur = common_current_user(); @@ -186,14 +199,19 @@ class OStatusPlugin extends Plugin array('nickname' => $profile->nickname)); $output->element('a', array('href' => $url, 'class' => 'entity_remote_subscribe'), - _('OStatus')); - + _m('Subscribe')); + $output->elementEnd('li'); } + + return false; } /** - * Check if we've got some Salmon stuff to send + * Check if we've got remote replies to send via Salmon. + * + * @fixme push webfinger lookup & sending to a background queue + * @fixme also detect short-form name for remote subscribees where not ambiguous */ function onEndNoticeSave($notice) { @@ -204,38 +222,66 @@ class OStatusPlugin extends Plugin $w = new Webfinger; $endpoint_uri = ''; - + $result = $w->lookup($webfinger); if (empty($result)) { continue; } - + foreach ($result->links as $link) { if ($link['rel'] == 'salmon') { $endpoint_uri = $link['href']; } } - + if (empty($endpoint_uri)) { continue; } $xml = '<?xml version="1.0" encoding="UTF-8" ?>'; $xml .= $notice->asAtomEntry(); - + $salmon = new Salmon(); $salmon->post($endpoint_uri, $xml); } } } - - + + /** + * Garbage collect unused feeds on unsubscribe + */ + function onEndUnsubscribe($user, $other) + { + $profile = Ostatus_profile::staticGet('profile_id', $other->id); + if ($feed) { + $sub = new Subscription(); + $sub->subscribed = $other->id; + $sub->limit(1); + if (!$sub->find(true)) { + common_log(LOG_INFO, "Unsubscribing from now-unused feed $feed->feeduri on hub $feed->huburi"); + $profile->unsubscribe(); + } + } + return true; + } + + /** + * Make sure necessary tables are filled out. + */ function onCheckSchema() { - // warning: the autoincrement doesn't seem to set. - // alter table feedinfo change column id id int(11) not null auto_increment; $schema = Schema::get(); - $schema->ensureTable('feedinfo', Feedinfo::schemaDef()); + $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef()); $schema->ensureTable('hubsub', HubSub::schemaDef()); return true; - } + } + + function onEndShowStatusNetStyles($action) { + $action->cssLink(common_path('plugins/OStatus/theme/base/css/ostatus.css')); + return true; + } + + function onEndShowStatusNetScripts($action) { + $action->script(common_path('plugins/OStatus/js/ostatus.js')); + return true; + } } diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php index 6f592bf5b..6933c9bf2 100644 --- a/plugins/OStatus/actions/feedsubsettings.php +++ b/plugins/OStatus/actions/feedsubsettings.php @@ -182,9 +182,9 @@ class FeedSubSettingsAction extends ConnectSettingsAction } $this->munger = $discover->feedMunger(); - $this->feedinfo = $this->munger->feedInfo(); + $this->profile = $this->munger->ostatusProfile(); - if ($this->feedinfo->huburi == '' && !common_config('feedsub', 'nohub')) { + if ($this->profile->huburi == '' && !common_config('feedsub', 'nohub')) { $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.')); return false; } @@ -196,13 +196,16 @@ class FeedSubSettingsAction extends ConnectSettingsAction { if ($this->validateFeed()) { $this->preview = true; - $this->feedinfo = Feedinfo::ensureProfile($this->munger); + $this->profile = Ostatus_profile::ensureProfile($this->munger); + if (!$this->profile) { + throw new ServerException("Feed profile was not saved properly."); + } // If not already in use, subscribe to updates via the hub - if ($this->feedinfo->sub_start) { - common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}"); + if ($this->profile->sub_start) { + common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}"); } else { - $ok = $this->feedinfo->subscribe(); + $ok = $this->profile->subscribe(); common_log(LOG_INFO, __METHOD__ . ": sub was $ok"); if (!$ok) { $this->showForm(_m('Feed subscription failed! Bad response from hub.')); @@ -212,23 +215,21 @@ class FeedSubSettingsAction extends ConnectSettingsAction // And subscribe the current user to the local profile $user = common_current_user(); - $profile = $this->feedinfo->getProfile(); - if (!$profile) { - throw new ServerException("Feed profile was not saved properly."); - } - if ($this->feedinfo->isGroup()) { - if ($user->isMember($profile)) { + if ($this->profile->isGroup()) { + $group = $this->profile->localGroup(); + if ($user->isMember($group)) { $this->showForm(_m('Already a member!')); - } elseif (Group_member::join($this->feedinfo->group_id, $user->id)) { + } elseif (Group_member::join($this->profile->group_id, $user->id)) { $this->showForm(_m('Joined remote group!')); } else { $this->showForm(_m('Remote group join failed!')); } } else { - if ($user->isSubscribed($profile)) { + $local = $this->profile->localProfile(); + if ($user->isSubscribed($local)) { $this->showForm(_m('Already subscribed!')); - } elseif ($user->subscribeTo($profile)) { + } elseif ($user->subscribeTo($local)) { $this->showForm(_m('Feed subscribed!')); } else { $this->showForm(_m('Feed subscription failed!')); @@ -247,7 +248,7 @@ class FeedSubSettingsAction extends ConnectSettingsAction function previewFeed() { - $feedinfo = $this->munger->feedinfo(); + $profile = $this->munger->ostatusProfile(); $notice = $this->munger->notice(0, true); // preview if ($notice) { diff --git a/plugins/OStatus/actions/ostatusinit.php b/plugins/OStatus/actions/ostatusinit.php index bac2c4d43..d21774420 100644 --- a/plugins/OStatus/actions/ostatusinit.php +++ b/plugins/OStatus/actions/ostatusinit.php @@ -67,9 +67,21 @@ class OStatusInitAction extends Action function showForm($err = null) { - $this->err = $err; - $this->showPage(); - + $this->err = $err; + if ($this->boolean('ajax')) { + header('Content-Type: text/xml;charset=utf-8'); + $this->xw->startDocument('1.0', 'UTF-8'); + $this->elementStart('html'); + $this->elementStart('head'); + $this->element('title', null, _('Subscribe to user')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->showContent(); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + $this->showPage(); + } } function showContent() @@ -79,15 +91,15 @@ class OStatusInitAction extends Action 'class' => 'form_settings', 'action' => common_local_url('ostatusinit'))); $this->elementStart('fieldset'); - $this->element('legend', _('Subscribe to a remote user')); + $this->element('legend', null, sprintf(_('Subscribe to %s'), $this->nickname)); $this->hidden('token', common_session_token()); $this->elementStart('ul', 'form_data'); - $this->elementStart('li'); + $this->elementStart('li', array('id' => 'ostatus_nickname')); $this->input('nickname', _('User nickname'), $this->nickname, _('Nickname of the user you want to follow')); $this->elementEnd('li'); - $this->elementStart('li'); + $this->elementStart('li', array('id' => 'ostatus_profile')); $this->input('acct', _('Profile Account'), $this->acct, _('Your account id (i.e. user@identi.ca)')); $this->elementEnd('li'); @@ -95,7 +107,7 @@ class OStatusInitAction extends Action $this->submit('submit', _('Subscribe')); $this->elementEnd('fieldset'); $this->elementEnd('form'); - } + } function ostatusConnect() { @@ -125,4 +137,4 @@ class OStatusInitAction extends Action return _('OStatus Connect'); } -}
\ No newline at end of file +} diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php index ffc4ae8df..239122501 100644 --- a/plugins/OStatus/actions/ostatussub.php +++ b/plugins/OStatus/actions/ostatussub.php @@ -76,7 +76,7 @@ class OStatusSubAction extends Action $this->elementStart('fieldset', array('id' => 'settings_feeds')); $this->elementStart('ul', 'form_data'); - $this->elementStart('li', array('id' => 'settings_twitter_login_button')); + $this->elementStart('li'); $this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed')); $this->elementEnd('li'); $this->elementEnd('ul'); @@ -164,9 +164,9 @@ class OStatusSubAction extends Action } $this->munger = $discover->feedMunger(); - $this->feedinfo = $this->munger->feedInfo(); + $this->profile = $this->munger->ostatusProfile(); - if ($this->feedinfo->huburi == '') { + if ($this->profile->huburi == '') { $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.')); return false; } @@ -178,13 +178,13 @@ class OStatusSubAction extends Action { if ($this->validateFeed()) { $this->preview = true; - $this->feedinfo = Feedinfo::ensureProfile($this->munger); + $this->profile = Ostatus_profile::ensureProfile($this->munger); // If not already in use, subscribe to updates via the hub - if ($this->feedinfo->sub_start) { - common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}"); + if ($this->profile->sub_start) { + common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}"); } else { - $ok = $this->feedinfo->subscribe(); + $ok = $this->profile->subscribe(); common_log(LOG_INFO, __METHOD__ . ": sub was $ok"); if (!$ok) { $this->showForm(_m('Feed subscription failed! Bad response from hub.')); @@ -194,7 +194,7 @@ class OStatusSubAction extends Action // And subscribe the current user to the local profile $user = common_current_user(); - $profile = $this->feedinfo->getProfile(); + $profile = $this->profile->getProfile(); if ($user->isSubscribed($profile)) { $this->showForm(_m('Already subscribed!')); @@ -209,7 +209,7 @@ class OStatusSubAction extends Action function previewFeed() { - $feedinfo = $this->munger->feedinfo(); + $profile = $this->munger->ostatusProfile(); $notice = $this->munger->notice(0, true); // preview if ($notice) { @@ -223,4 +223,4 @@ class OStatusSubAction extends Action } -}
\ No newline at end of file +} diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php index a5e02e08f..2601a377a 100644 --- a/plugins/OStatus/actions/pushcallback.php +++ b/plugins/OStatus/actions/pushcallback.php @@ -48,9 +48,9 @@ class PushCallbackAction extends Action throw new ServerException('Empty or invalid feed id', 400); } - $feedinfo = Feedinfo::staticGet('id', $feedid); - if (!$feedinfo) { - throw new ServerException('Unknown feed id ' . $feedid, 400); + $profile = Ostatus_profile::staticGet('id', $feedid); + if (!$profile) { + throw new ServerException('Unknown OStatus/PuSH feed id ' . $feedid, 400); } $hmac = ''; @@ -59,7 +59,7 @@ class PushCallbackAction extends Action } $post = file_get_contents('php://input'); - $feedinfo->postUpdates($post, $hmac); + $profile->postUpdates($post, $hmac); } /** @@ -78,28 +78,30 @@ class PushCallbackAction extends Action throw new ServerException("Bogus hub callback: bad mode", 404); } - $feedinfo = Feedinfo::staticGet('feeduri', $topic); - if (!$feedinfo) { + $profile = Ostatus_profile::staticGet('feeduri', $topic); + if (!$profile) { common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic"); throw new ServerException("Bogus hub callback: unknown feed", 404); } - # Can't currently set the token in our sub api - #if ($feedinfo->verify_token !== $verify_token) { - # common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic"); - # throw new ServerError("Bogus hub callback: bad token", 404); - #} - + if ($profile->verify_token !== $verify_token) { + common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic"); + throw new ServerError("Bogus hub callback: bad token", 404); + } + + if ($mode != $profile->sub_state) { + common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad mode \"$mode\" for feed $topic in state \"{$profile->sub_state}\""); + throw new ServerException("Bogus hub callback: mode doesn't match subscription state.", 404); + } + // OK! - common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); - $feedinfo->sub_start = common_sql_date(time()); - if ($lease_seconds > 0) { - $feedinfo->sub_end = common_sql_date(time() + $lease_seconds); + if ($mode == 'subscribe') { + common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); + $profile->confirmSubscribe($lease_seconds); } else { - $feedinfo->sub_end = null; + common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic"); + $profile->confirmUnsubscribe(); } - $feedinfo->update(); - print $challenge; } } diff --git a/plugins/OStatus/actions/salmon.php b/plugins/OStatus/actions/salmon.php index 012869cf7..c79d09c95 100644 --- a/plugins/OStatus/actions/salmon.php +++ b/plugins/OStatus/actions/salmon.php @@ -22,28 +22,60 @@ * @author James Walker <james@status.net> */ -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +if (!defined('STATUSNET')) { + exit(1); +} class SalmonAction extends Action { + var $user = null; + var $xml = null; + var $activity = null; - function handle() + function prepare($args) { - parent::handle(); - if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $this->handlePost(); + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + $this->clientError(_('This method requires a POST.')); } - } + if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') { + $this->clientError(_('Salmon requires application/atom+xml')); + } - function handlePost() - { - $user_id = $this->arg('id'); - common_log(LOG_DEBUG, 'Salmon: incoming post for user: '. $user_id); + $id = $this->trimmed('id'); + + if (!$id) { + $this->clientError(_('No ID.')); + } + + $this->user = User::staticGet($id); + + if (empty($this->user)) { + $this->clientError(_('No such user.')); + } $xml = file_get_contents('php://input'); + $dom = DOMDocument::loadXML($xml); + + // XXX: check that document element is Atom entry + // XXX: check the signature + + $this->act = new Activity($dom->documentElement); + } + + function handle($args) + { + common_log(LOG_DEBUG, 'Salmon: incoming post for user: '. $user_id); + // TODO : Insert new $xml -> notice code + switch ($this->act->verb) + { + case Activity::POST: + case Activity::SHARE: + case Activity::FAVORITE: + case Activity::FOLLOW: + } } } diff --git a/plugins/OStatus/classes/Feedinfo.php b/plugins/OStatus/classes/Feedinfo.php deleted file mode 100644 index 792ea6034..000000000 --- a/plugins/OStatus/classes/Feedinfo.php +++ /dev/null @@ -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"); - } - } -} diff --git a/plugins/OStatus/classes/HubSub.php b/plugins/OStatus/classes/HubSub.php index 1769f6c94..7071ee5b4 100644 --- a/plugins/OStatus/classes/HubSub.php +++ b/plugins/OStatus/classes/HubSub.php @@ -242,7 +242,7 @@ class HubSub extends Memcached_DataObject { $headers = array('Content-Type: application/atom+xml'); if ($this->secret) { - $hmac = sha1($atom . $this->secret); + $hmac = hash_hmac('sha1', $atom, $this->secret); $headers[] = "X-Hub-Signature: sha1=$hmac"; } else { $hmac = '(none)'; diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php new file mode 100644 index 000000000..733d8843b --- /dev/null +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -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"); + } + } +} diff --git a/plugins/OStatus/js/ostatus.js b/plugins/OStatus/js/ostatus.js new file mode 100644 index 000000000..671795558 --- /dev/null +++ b/plugins/OStatus/js/ostatus.js @@ -0,0 +1,60 @@ +SN.U.DialogBox = { + Subscribe: function(a) { + var f = a.parent().find('#form_ostatus_connect'); + if (f.length > 0) { + f.show(); + } + else { + $.ajax({ + type: 'GET', + dataType: 'xml', + url: a[0].href+'&ajax=1', + beforeSend: function(formData) { + a.addClass('processing'); + }, + error: function (xhr, textStatus, errorThrown) { + alert(errorThrown || textStatus); + }, + success: function(data, textStatus, xhr) { + if (typeof($('form', data)[0]) != 'undefined') { + a.after(document._importNode($('form', data)[0], true)); + + var form = a.parent().find('#form_ostatus_connect'); + + form + .addClass('dialogbox') + .append('<button class="close">×</button>'); + + form + .find('.submit') + .addClass('submit_dialogbox') + .removeClass('submit') + .bind('click', function() { + form.addClass('processing'); + }); + + form.find('button.close').click(function(){ + form.hide(); + + return false; + }); + + form.find('#acct').focus(); + } + + a.removeClass('processing'); + } + }); + } + } +}; + +SN.Init.Subscribe = function() { + $('.entity_subscribe a').live('click', function() { SN.U.DialogBox.Subscribe($(this)); return false; }); +}; + +$(document).ready(function() { + if ($('.entity_subscribe .entity_remote_subscribe').length > 0) { + SN.Init.Subscribe(); + } +}); diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php new file mode 100644 index 000000000..048efda2c --- /dev/null +++ b/plugins/OStatus/lib/activity.php @@ -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); + } + } +}
\ No newline at end of file diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php index 5dce95342..c895b6ce2 100644 --- a/plugins/OStatus/lib/feedmunger.php +++ b/plugins/OStatus/lib/feedmunger.php @@ -83,13 +83,17 @@ class FeedMunger $this->url = $url; } - function feedinfo() + function ostatusProfile() { - $feedinfo = new Feedinfo(); - $feedinfo->feeduri = $this->url; - $feedinfo->homeuri = $this->feed->link; - $feedinfo->huburi = $this->getHubLink(); - return $feedinfo; + $profile = new Ostatus_profile(); + $profile->feeduri = $this->url; + $profile->homeuri = $this->feed->link; + $profile->huburi = $this->getHubLink(); + $salmon = $this->getSalmonLink(); + if ($salmon) { + $profile->salmonuri = $salmon; + } + return $profile; } function getAtomLink($item, $attribs=array()) @@ -155,6 +159,16 @@ class FeedMunger return $this->getAtomLink($this->feed, array('rel' => 'hub')); } + function getSalmonLink() + { + return $this->getAtomLink($this->feed, array('rel' => 'salmon')); + } + + function getSelfLink() + { + return $this->getAtomLink($this->feed, array('rel' => 'self')); + } + /** * Get an appropriate avatar image source URL, if available. * @return mixed string or false @@ -209,6 +223,7 @@ class FeedMunger $notice->id = -1; } else { $notice = new Notice(); + $notice->profile_id = $this->profileIdForEntry($index); } $link = $this->getAltLink($entry); @@ -239,7 +254,22 @@ class FeedMunger return $notice; } + function profileIdForEntry($index=1) + { + // hack hack hack + // should get profile for this entry's author... + $remote = Ostatus_profile::staticGet('feeduri', $this->getSelfLink()); + if ($feed) { + return $feed->profile_id; + } else { + throw new Exception("Can't find feed profile"); + } + } + /** + * Parse location given as a GeoRSS-simple point, if provided. + * http://www.georss.org/simple + * * @param feed item $entry * @return mixed Location or false */ @@ -249,7 +279,10 @@ class FeedMunger $points = $dom->getElementsByTagNameNS('http://www.georss.org/georss', 'point'); for ($i = 0; $i < $points->length; $i++) { - $point = trim($points->item(0)->textContent); + $point = $points->item(0)->textContent; + $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace" + $point = preg_replace('/\s+/', ' ', $point); + $point = trim($point); $coords = explode(' ', $point); if (count($coords) == 2) { list($lat, $lon) = $coords; diff --git a/plugins/OStatus/lib/hubdistribqueuehandler.php b/plugins/OStatus/lib/hubdistribqueuehandler.php index 189ccbedf..245a57f72 100644 --- a/plugins/OStatus/lib/hubdistribqueuehandler.php +++ b/plugins/OStatus/lib/hubdistribqueuehandler.php @@ -38,6 +38,7 @@ class HubDistribQueueHandler extends QueueHandler foreach ($notice->getGroups() as $group) { $this->pushGroup($notice, $group->group_id); } + return true; } function pushUser($notice) @@ -48,14 +49,7 @@ class HubDistribQueueHandler extends QueueHandler $feed = common_local_url('ApiTimelineUser', array('id' => $notice->profile_id, 'format' => 'atom')); - $sub = new HubSub(); - $sub->topic = $feed; - if ($sub->find()) { - $atom = $this->userFeedForNotice($notice); - $this->pushFeeds($atom, $sub); - } else { - common_log(LOG_INFO, "No PuSH subscribers for $feed"); - } + $this->pushFeed($feed, array($this, 'userFeedForNotice'), $notice); } function pushGroup($notice, $group_id) @@ -63,19 +57,69 @@ class HubDistribQueueHandler extends QueueHandler $feed = common_local_url('ApiTimelineGroup', array('id' => $group_id, 'format' => 'atom')); + $this->pushFeed($feed, array($this, 'groupFeedForNotice'), $group_id, $notice); + } + + /** + * @param string $feed URI to the feed + * @param callable $callback function to generate Atom feed update if needed + * any additional params are passed to the callback. + */ + function pushFeed($feed, $callback) + { + $hub = common_config('ostatus', 'hub'); + if ($hub) { + $this->pushFeedExternal($feed, $hub); + } + $sub = new HubSub(); $sub->topic = $feed; if ($sub->find()) { - common_log(LOG_INFO, "Building PuSH feed for $feed"); - $atom = $this->groupFeedForNotice($group_id, $notice); - $this->pushFeeds($atom, $sub); + $args = array_slice(func_get_args(), 2); + $atom = call_user_func_array($callback, $args); + $this->pushFeedInternal($atom, $sub); } else { common_log(LOG_INFO, "No PuSH subscribers for $feed"); } + return true; } - - function pushFeeds($atom, $sub) + /** + * Ping external hub about this update. + * The hub will pull the feed and check for new items later. + * Not guaranteed safe in an environment with database replication. + * + * @param string $feed feed topic URI + * @param string $hub PuSH hub URI + * @fixme can consolidate pings for user & group posts + */ + function pushFeedExternal($feed, $hub) + { + $client = new HTTPClient(); + try { + $data = array('hub.mode' => 'publish', + 'hub.url' => $feed); + $response = $client->post($hub, array(), $data); + if ($response->getStatus() == 204) { + common_log(LOG_INFO, "PuSH ping to hub $hub for $feed ok"); + return true; + } else { + common_log(LOG_ERR, "PuSH ping to hub $hub for $feed failed with HTTP " . + $response->getStatus() . ': ' . + $response->getBody()); + } + } catch (Exception $e) { + common_log(LOG_ERR, "PuSH ping to hub $hub for $feed failed: " . $e->getMessage()); + return false; + } + } + + /** + * Queue up direct feed update pushes to subscribers on our internal hub. + * @param string $atom update feed, containing only new/changed items + * @param HubSub $sub open query of subscribers + */ + function pushFeedInternal($atom, $sub) { common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $sub->topic"); $qm = QueueManager::get(); diff --git a/plugins/OStatus/lib/huboutqueuehandler.php b/plugins/OStatus/lib/huboutqueuehandler.php index cb44ad2c4..0791c7e5d 100644 --- a/plugins/OStatus/lib/huboutqueuehandler.php +++ b/plugins/OStatus/lib/huboutqueuehandler.php @@ -43,7 +43,7 @@ class HubOutQueueHandler extends QueueHandler common_log(LOG_ERR, "Failed PuSH to $sub->callback for $sub->topic: " . $e->getMessage()); // @fixme Reschedule a later delivery? - // Currently we have no way to do this other than 'send NOW' + return true; } return true; diff --git a/plugins/OStatus/lib/Salmon.php b/plugins/OStatus/lib/salmon.php index 8c77222a6..8c77222a6 100644 --- a/plugins/OStatus/lib/Salmon.php +++ b/plugins/OStatus/lib/salmon.php diff --git a/plugins/OStatus/lib/Webfinger.php b/plugins/OStatus/lib/webfinger.php index 417d54904..417d54904 100644 --- a/plugins/OStatus/lib/Webfinger.php +++ b/plugins/OStatus/lib/webfinger.php diff --git a/plugins/OStatus/lib/XRD.php b/plugins/OStatus/lib/xrd.php index 16d27f8eb..16d27f8eb 100644 --- a/plugins/OStatus/lib/XRD.php +++ b/plugins/OStatus/lib/xrd.php diff --git a/plugins/OStatus/tests/ActivityParseTests.php b/plugins/OStatus/tests/ActivityParseTests.php new file mode 100644 index 000000000..fa8bcdda2 --- /dev/null +++ b/plugins/OStatus/tests/ActivityParseTests.php @@ -0,0 +1,147 @@ +<?php + +if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) { + print "This script must be run from the command line\n"; + exit(); +} + +// XXX: we should probably have some common source for this stuff + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..')); +define('STATUSNET', true); + +require_once INSTALLDIR . '/lib/common.php'; +require_once INSTALLDIR . '/plugins/OStatus/lib/activity.php'; + +class ActivityParseTests extends PHPUnit_Framework_TestCase +{ + public function testExample1() + { + global $_example1; + $dom = DOMDocument::loadXML($_example1); + $act = new Activity($dom->documentElement); + + $this->assertFalse(empty($act)); + $this->assertEquals($act->time, 1243860840); + $this->assertEquals($act->verb, ActivityVerb::POST); + } + + public function testExample3() + { + global $_example3; + $dom = DOMDocument::loadXML($_example3); + + $feed = $dom->documentElement; + + $entries = $feed->getElementsByTagName('entry'); + + $entry = $entries->item(0); + + $act = new Activity($entry, $feed); + + $this->assertFalse(empty($act)); + $this->assertEquals($act->time, 1071340202); + $this->assertEquals($act->link, 'http://example.org/2003/12/13/atom03.html'); + + $this->assertEquals($act->verb, ActivityVerb::POST); + + $this->assertFalse(empty($act->actor)); + $this->assertEquals($act->actor->type, ActivityObject::PERSON); + $this->assertEquals($act->actor->title, 'John Doe'); + $this->assertEquals($act->actor->id, 'mailto:johndoe@example.com'); + + $this->assertFalse(empty($act->object)); + $this->assertEquals($act->object->type, ActivityObject::NOTE); + $this->assertEquals($act->object->id, 'urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a'); + $this->assertEquals($act->object->title, 'Atom-Powered Robots Run Amok'); + $this->assertEquals($act->object->summary, 'Some text.'); + $this->assertEquals($act->object->link, 'http://example.org/2003/12/13/atom03.html'); + + $this->assertTrue(empty($act->context)); + $this->assertTrue(empty($act->target)); + + $this->assertEquals($act->entry, $entry); + $this->assertEquals($act->feed, $feed); + } +} + +$_example1 = <<<EXAMPLE1 +<?xml version='1.0' encoding='UTF-8'?> +<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'> + <id>tag:versioncentral.example.org,2009:/commit/1643245</id> + <published>2009-06-01T12:54:00Z</published> + <title>Geraldine committed a change to yate</title> + <content type="xhtml">Geraldine just committed a change to yate on VersionCentral</content> + <link rel="alternate" type="text/html" + href="http://versioncentral.example.org/geraldine/yate/commit/1643245" /> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <activity:verb>http://versioncentral.example.org/activity/commit</activity:verb> + <activity:object> + <activity:object-type>http://versioncentral.example.org/activity/changeset</activity:object-type> + <id>tag:versioncentral.example.org,2009:/change/1643245</id> + <title>Punctuation Changeset</title> + <summary>Fixing punctuation because it makes it more readable.</summary> + <link rel="alternate" type="text/html" href="..." /> + </activity:object> +</entry> +EXAMPLE1; + +$_example2 = <<<EXAMPLE2 +<?xml version='1.0' encoding='UTF-8'?> +<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'> + <id>tag:photopanic.example.com,2008:activity01</id> + <title>Geraldine posted a Photo on PhotoPanic</title> + <published>2008-11-02T15:29:00Z</published> + <link rel="alternate" type="text/html" href="/geraldine/activities/1" /> + <activity:verb> + http://activitystrea.ms/schema/1.0/post + </activity:verb> + <activity:object> + <id>tag:photopanic.example.com,2008:photo01</id> + <title>My Cat</title> + <published>2008-11-02T15:29:00Z</published> + <link rel="alternate" type="text/html" href="/geraldine/photos/1" /> + <activity:object-type> + tag:atomactivity.example.com,2008:photo + </activity:object-type> + <source> + <title>Geraldine's Photos</title> + <link rel="self" type="application/atom+xml" href="/geraldine/photofeed.xml" /> + <link rel="alternate" type="text/html" href="/geraldine/" /> + </source> + </activity:object> + <content type="html"> + <p>Geraldine posted a Photo on PhotoPanic</p> + <img src="/geraldine/photo1.jpg"> + </content> +</entry> +EXAMPLE2; + +$_example3 = <<<EXAMPLE3 +<?xml version="1.0" encoding="utf-8"?> + +<feed xmlns="http://www.w3.org/2005/Atom"> + + <title>Example Feed</title> + <subtitle>A subtitle.</subtitle> + <link href="http://example.org/feed/" rel="self" /> + <link href="http://example.org/" /> + <id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id> + <updated>2003-12-13T18:30:02Z</updated> + <author> + <name>John Doe</name> + <email>johndoe@example.com</email> + </author> + + <entry> + <title>Atom-Powered Robots Run Amok</title> + <link href="http://example.org/2003/12/13/atom03" /> + <link rel="alternate" type="text/html" href="http://example.org/2003/12/13/atom03.html"/> + <link rel="edit" href="http://example.org/2003/12/13/atom03/edit"/> + <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id> + <updated>2003-12-13T18:30:02Z</updated> + <summary>Some text.</summary> + </entry> + +</feed> +EXAMPLE3; diff --git a/plugins/OStatus/theme/base/css/ostatus.css b/plugins/OStatus/theme/base/css/ostatus.css new file mode 100644 index 000000000..9bc90a731 --- /dev/null +++ b/plugins/OStatus/theme/base/css/ostatus.css @@ -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; +} diff --git a/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php b/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php index 14d1608d3..fb4eff738 100644 --- a/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php +++ b/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php @@ -45,6 +45,7 @@ class PoweredByStatusNetPlugin extends Plugin { function onEndAddressData($action) { + $action->text(' '); $action->elementStart('span', 'poweredby'); $action->raw(sprintf(_m('powered by %s'), sprintf('<a href="http://status.net/">%s</a>', diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 8490fb580..89fe810c6 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -288,7 +288,7 @@ margin-left:18px; } #site_nav_global_primary li { display:inline; -margin-left:11px; +margin-left:18px; } .system_notice dt { @@ -370,7 +370,7 @@ margin-bottom:11px; #site_nav_global_secondary ul li { display:inline; -margin-right:11px; +margin-right:18px; } #export_data li a { padding-left:20px; @@ -383,15 +383,13 @@ padding-left:28px; } #export_data ul { -display:inline; +width:100%; +float:left; } #export_data li { list-style-type:none; -display:inline; -margin-left:11px; -} -#export_data li:first-child { -margin-left:0; +float:left; +margin-right:11px; } #licenses { @@ -801,8 +799,8 @@ list-style-type:none; display:inline; } .entity_tags li { -display:inline; -margin-right:4px; +float:left; +margin-right:11px; } .aside .section { @@ -820,6 +818,7 @@ font-size:1em; #entity_statistics dt, #entity_statistics dd { display:inline; +margin-right:11px; } #entity_statistics dt:after { content: ":"; @@ -1104,25 +1103,22 @@ left:0; .dialogbox { position:absolute; -top:-4px; -right:29px; +top:-1px; +right:-1px; z-index:9; -min-width:199px; float:none; -background-color:#FFF; padding:11px; border-radius:7px; -moz-border-radius:7px; -webkit-border-radius:7px; border-style:solid; border-width:1px; -border-color:#DDDDDD; --moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); } .dialogbox legend { display:block !important; margin-right:18px; +margin-bottom:18px; } .dialogbox button.close { @@ -1131,11 +1127,22 @@ right:3px; top:3px; } +.dialogbox .form_guide { +font-weight:normal; +padding:0; +} + .dialogbox .submit_dialogbox { font-weight:bold; text-indent:0; min-width:46px; } +.dialogbox input { +padding-left:4px; +} +.dialogbox fieldset { +margin-bottom:0; +} #wrap form.processing input.submit, .entity_actions a.processing, @@ -1145,6 +1152,12 @@ outline:none; text-indent:-9999px; } +.form_repeat.dialogbox { +top:-4px; +right:29px; +min-width:199px; +} + .notice-options { position:relative; font-size:0.95em; @@ -1482,6 +1495,11 @@ display:inline; margin-right:7px; line-height:1.25; } + +.tag-cloud li:before { +content:'\0009'; +} + .aside .tag-cloud li { line-height:1.5; } diff --git a/theme/default/css/display.css b/theme/default/css/display.css index 82eb13531..a2f101342 100644 --- a/theme/default/css/display.css +++ b/theme/default/css/display.css @@ -30,7 +30,9 @@ border-radius:4px; input, textarea, select, option { font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif; } -input, textarea, select { +input, textarea, select, +.entity_actions .dialogbox input, +.mark-top { border-color:#AAAAAA; } @@ -46,7 +48,8 @@ box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3); .pagination .nav_prev a, .pagination .nav_next a, .form_settings fieldset fieldset, -.entity_moderation:hover ul { +.entity_moderation:hover ul, +.dialogbox { border-color:#DDDDDD; } @@ -78,7 +81,8 @@ background-color:transparent; input:focus, textarea:focus, select:focus, .form_notice.warning #notice_data-text, .form_notice.warning #notice_text-count, -.form_settings .form_note { +.form_settings .form_note, +.entity_actions .dialogbox .form_data input:focus { border-color:#9BB43E; } input.submit { @@ -133,9 +137,6 @@ color:#002FA7; #content tbody tr { border-top-color:#C8D1D5; } -.mark-top { -border-color:#AAAAAA; -} #aside_primary { background-color:#C8D1D5; @@ -144,7 +145,9 @@ background-color:#C8D1D5; #notice_text-count { color:#333333; } -.form_notice.warning #notice_text-count { +.form_notice.warning #notice_text-count, +.dialogbox, +.entity_actions .dialogbox input { color:#000000; } .form_notice label[for=notice_data-attach] { @@ -221,7 +224,8 @@ border-color:transparent; #content, #site_nav_local_views .current a, .entity_send-a-message .form_notice, -.entity_moderation:hover ul { +.entity_moderation:hover ul, +.dialogbox { background-color:#FFFFFF; } @@ -308,7 +312,8 @@ background-position: 5px -718px; background-position: 5px -852px; } .entity_send-a-message .form_notice, -.entity_moderation:hover ul { +.entity_moderation:hover ul, +.dialogbox { box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); -moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); -webkit-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); diff --git a/theme/identica/css/display.css b/theme/identica/css/display.css index 44ae4953b..e21404745 100644 --- a/theme/identica/css/display.css +++ b/theme/identica/css/display.css @@ -30,7 +30,9 @@ border-radius:4px; input, textarea, select, option { font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif; } -input, textarea, select { +input, textarea, select, +.entity_actions .dialogbox input, +.mark-top { border-color:#AAAAAA; } @@ -46,7 +48,8 @@ box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3); .pagination .nav_prev a, .pagination .nav_next a, .form_settings fieldset fieldset, -.entity_moderation:hover ul { +.entity_moderation:hover ul, +.dialogbox { border-color:#DDDDDD; } @@ -88,6 +91,7 @@ color:#FFFFFF; border-color:transparent; text-shadow:none; } + .dialogbox .submit_dialogbox, input.submit, .form_notice input.submit { @@ -133,9 +137,6 @@ color:#002FA7; #content tbody tr { border-top-color:#CEE1E9; } -.mark-top { -border-color:#AAAAAA; -} #aside_primary { background-color:#CEE1E9; @@ -144,7 +145,9 @@ background-color:#CEE1E9; #notice_text-count { color:#333333; } -.form_notice.warning #notice_text-count { +.form_notice.warning #notice_text-count, +.dialogbox, +.entity_actions .dialogbox input { color:#000000; } .form_notice label[for=notice_data-attach] { @@ -221,7 +224,8 @@ border-color:transparent; #content, #site_nav_local_views .current a, .entity_send-a-message .form_notice, -.entity_moderation:hover ul { +.entity_moderation:hover ul, +.dialogbox { background-color:#FFFFFF; } @@ -307,7 +311,8 @@ background-position: 5px -718px; background-position: 5px -852px; } .entity_send-a-message .form_notice, -.entity_moderation:hover ul { +.entity_moderation:hover ul, +.dialogbox { box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); -moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); -webkit-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); |