diff options
author | Brion Vibber <brion@pobox.com> | 2010-03-02 15:16:48 -0800 |
---|---|---|
committer | Brion Vibber <brion@pobox.com> | 2010-03-02 15:16:48 -0800 |
commit | 8e06b932af93b8fbd66610c948bdde4fafd4f300 (patch) | |
tree | 07b772b280e6f23761c0111d953b3c2f5b8d2120 /lib | |
parent | 453ad6a9964fe572f0f35821e3f78c6a77c57c49 (diff) | |
parent | ddf3614c843bcd8d9ecfd0850ac9a8cefae6dbba (diff) |
Merge branch '0.9.x' of git@gitorious.org:statusnet/mainline into 0.9.x
Diffstat (limited to 'lib')
32 files changed, 2115 insertions, 342 deletions
diff --git a/lib/action.php b/lib/action.php index b85f353a3..a804b8e82 100644 --- a/lib/action.php +++ b/lib/action.php @@ -249,7 +249,7 @@ class Action extends HTMLOutputter // lawsuit $this->script('jquery.min.js'); $this->script('jquery.form.js'); $this->script('jquery.cookie.js'); - $this->script('json2.js'); + $this->inlineScript('if (typeof window.JSON !== "object") { $.getScript("'.common_path('js/json2.js').'"); }'); $this->script('jquery.joverlay.min.js'); Event::handle('EndShowJQueryScripts', array($this)); } @@ -259,8 +259,7 @@ class Action extends HTMLOutputter // lawsuit $this->script('util.js'); $this->script('geometa.js'); // Frame-busting code to avoid clickjacking attacks. - $this->element('script', array('type' => 'text/javascript'), - 'if (window.top !== window.self) { window.top.location.href = window.self.location.href; }'); + $this->inlineScript('if (window.top !== window.self) { window.top.location.href = window.self.location.href; }'); Event::handle('EndShowStatusNetScripts', array($this)); Event::handle('EndShowLaconicaScripts', array($this)); } @@ -426,8 +425,6 @@ class Action extends HTMLOutputter // lawsuit $connect = 'imsettings'; } else if (common_config('sms', 'enabled')) { $connect = 'smssettings'; - } else if (common_config('twitter', 'enabled')) { - $connect = 'twittersettings'; } $this->elementStart('dl', array('id' => 'site_nav_global_primary')); @@ -436,41 +433,71 @@ class Action extends HTMLOutputter // lawsuit $this->elementStart('ul', array('class' => 'nav')); if (Event::handle('StartPrimaryNav', array($this))) { if ($user) { + // TRANS: Tooltip for main menu option "Personal" + $tooltip = _m('TOOLTIP', 'Personal profile and friends timeline'); + // TRANS: Main menu option when logged in for access to personal profile and friends timeline $this->menuItem(common_local_url('all', array('nickname' => $user->nickname)), - _('Home'), _('Personal profile and friends timeline'), false, 'nav_home'); + _m('MENU', 'Personal'), $tooltip, false, 'nav_home'); + // TRANS: Tooltip for main menu option "Account" + $tooltip = _m('TOOLTIP', 'Change your email, avatar, password, profile'); + // TRANS: Main menu option when logged in for access to user settings $this->menuItem(common_local_url('profilesettings'), - _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account'); + _m('MENU', 'Account'), $tooltip, false, 'nav_account'); if ($connect) { + // TRANS: Tooltip for main menu option "Services" + $tooltip = _m('TOOLTIP', 'Connect to services'); + // TRANS: Main menu option when logged in and connection are possible for access to options to connect to other services $this->menuItem(common_local_url($connect), - _('Connect'), _('Connect to services'), false, 'nav_connect'); + _m('MENU', 'Connect'), $tooltip, false, 'nav_connect'); } if ($user->hasRight(Right::CONFIGURESITE)) { + // TRANS: Tooltip for menu option "Admin" + $tooltip = _m('TOOLTIP', 'Change site configuration'); + // TRANS: Main menu option when logged in and site admin for access to site configuration $this->menuItem(common_local_url('siteadminpanel'), - _('Admin'), _('Change site configuration'), false, 'nav_admin'); + _m('MENU', 'Admin'), $tooltip, false, 'nav_admin'); } if (common_config('invite', 'enabled')) { + // TRANS: Tooltip for main menu option "Invite" + $tooltip = _m('TOOLTIP', 'Invite friends and colleagues to join you on %s'); + // TRANS: Main menu option when logged in and invitations are allowed for inviting new users $this->menuItem(common_local_url('invite'), - _('Invite'), - sprintf(_('Invite friends and colleagues to join you on %s'), + _m('MENU', 'Invite'), + sprintf($tooltip, common_config('site', 'name')), false, 'nav_invitecontact'); } + // TRANS: Tooltip for main menu option "Logout" + $tooltip = _m('TOOLTIP', 'Logout from the site'); + // TRANS: Main menu option when logged in to log out the current user $this->menuItem(common_local_url('logout'), - _('Logout'), _('Logout from the site'), false, 'nav_logout'); + _m('MENU', 'Logout'), $tooltip, false, 'nav_logout'); } else { if (!common_config('site', 'closed')) { + // TRANS: Tooltip for main menu option "Register" + $tooltip = _m('TOOLTIP', 'Create an account'); + // TRANS: Main menu option when not logged in to register a new account $this->menuItem(common_local_url('register'), - _('Register'), _('Create an account'), false, 'nav_register'); + _m('MENU', 'Register'), $tooltip, false, 'nav_register'); } + // TRANS: Tooltip for main menu option "Login" + $tooltip = _m('TOOLTIP', 'Login to the site'); + // TRANS: Main menu option when not logged in to log in $this->menuItem(common_local_url('login'), - _('Login'), _('Login to the site'), false, 'nav_login'); + _m('MENU', 'Login'), $tooltip, false, 'nav_login'); } + // TRANS: Tooltip for main menu option "Help" + $tooltip = _m('TOOLTIP', 'Help me!'); + // TRANS: Main menu option for help on the StatusNet site $this->menuItem(common_local_url('doc', array('title' => 'help')), - _('Help'), _('Help me!'), false, 'nav_help'); + _m('MENU', 'Help'), $tooltip, false, 'nav_help'); if ($user || !common_config('site', 'private')) { + // TRANS: Tooltip for main menu option "Search" + $tooltip = _m('TOOLTIP', 'Search for people or text'); + // TRANS: Main menu option when logged in or when the StatusNet instance is not private $this->menuItem(common_local_url('peoplesearch'), - _('Search'), _('Search for people or text'), false, 'nav_search'); + _m('MENU', 'Search'), $tooltip, false, 'nav_search'); } Event::handle('EndPrimaryNav', array($this)); } @@ -491,6 +518,7 @@ class Action extends HTMLOutputter // lawsuit if ($text) { $this->elementStart('dl', array('id' => 'site_notice', 'class' => 'system_notice')); + // TRANS: DT element for site notice. String is hidden in default CSS. $this->element('dt', null, _('Site notice')); $this->elementStart('dd', null); $this->raw($text); @@ -977,7 +1005,7 @@ class Action extends HTMLOutputter // lawsuit if (is_null($arg)) { return $def; - } else if (in_array($arg, array('true', 'yes', '1'))) { + } else if (in_array($arg, array('true', 'yes', '1', 'on'))) { return true; } else if (in_array($arg, array('false', 'no', '0'))) { return false; diff --git a/lib/activity.php b/lib/activity.php new file mode 100644 index 000000000..b20153213 --- /dev/null +++ b/lib/activity.php @@ -0,0 +1,1267 @@ +<?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 Feed + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @author Zach Copley <zach@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class PoCoURL +{ + const URLS = 'urls'; + const TYPE = 'type'; + const VALUE = 'value'; + const PRIMARY = 'primary'; + + public $type; + public $value; + public $primary; + + function __construct($type, $value, $primary = false) + { + $this->type = $type; + $this->value = $value; + $this->primary = $primary; + } + + function asString() + { + $xs = new XMLStringer(true); + $xs->elementStart('poco:urls'); + $xs->element('poco:type', null, $this->type); + $xs->element('poco:value', null, $this->value); + if (!empty($this->primary)) { + $xs->element('poco:primary', null, 'true'); + } + $xs->elementEnd('poco:urls'); + return $xs->getString(); + } +} + +class PoCoAddress +{ + const ADDRESS = 'address'; + const FORMATTED = 'formatted'; + + public $formatted; + + // @todo Other address fields + + function asString() + { + if (!empty($this->formatted)) { + $xs = new XMLStringer(true); + $xs->elementStart('poco:address'); + $xs->element('poco:formatted', null, $this->formatted); + $xs->elementEnd('poco:address'); + return $xs->getString(); + } + + return null; + } +} + +class PoCo +{ + const NS = 'http://portablecontacts.net/spec/1.0'; + + const USERNAME = 'preferredUsername'; + const DISPLAYNAME = 'displayName'; + const NOTE = 'note'; + + public $preferredUsername; + public $displayName; + public $note; + public $address; + public $urls = array(); + + function __construct($element = null) + { + if (empty($element)) { + return; + } + + $this->preferredUsername = ActivityUtils::childContent( + $element, + self::USERNAME, + self::NS + ); + + $this->displayName = ActivityUtils::childContent( + $element, + self::DISPLAYNAME, + self::NS + ); + + $this->note = ActivityUtils::childContent( + $element, + self::NOTE, + self::NS + ); + + $this->address = $this->_getAddress($element); + $this->urls = $this->_getURLs($element); + } + + private function _getURLs($element) + { + $urlEls = $element->getElementsByTagnameNS(self::NS, PoCoURL::URLS); + $urls = array(); + + foreach ($urlEls as $urlEl) { + + $type = ActivityUtils::childContent( + $urlEl, + PoCoURL::TYPE, + PoCo::NS + ); + + $value = ActivityUtils::childContent( + $urlEl, + PoCoURL::VALUE, + PoCo::NS + ); + + $primary = ActivityUtils::childContent( + $urlEl, + PoCoURL::PRIMARY, + PoCo::NS + ); + + $isPrimary = false; + + if (isset($primary) && $primary == 'true') { + $isPrimary = true; + } + + // @todo check to make sure a primary hasn't already been added + + array_push($urls, new PoCoURL($type, $value, $isPrimary)); + } + return $urls; + } + + private function _getAddress($element) + { + $addressEl = ActivityUtils::child( + $element, + PoCoAddress::ADDRESS, + PoCo::NS + ); + + if (!empty($addressEl)) { + $formatted = ActivityUtils::childContent( + $addressEl, + PoCoAddress::FORMATTED, + self::NS + ); + + if (!empty($formatted)) { + $address = new PoCoAddress(); + $address->formatted = $formatted; + return $address; + } + } + + return null; + } + + function fromProfile($profile) + { + if (empty($profile)) { + return null; + } + + $poco = new PoCo(); + + $poco->preferredUsername = $profile->nickname; + $poco->displayName = $profile->getBestName(); + + $poco->note = $profile->bio; + + $paddy = new PoCoAddress(); + $paddy->formatted = $profile->location; + $poco->address = $paddy; + + if (!empty($profile->homepage)) { + array_push( + $poco->urls, + new PoCoURL( + 'homepage', + $profile->homepage, + true + ) + ); + } + + return $poco; + } + + function fromGroup($group) + { + if (empty($group)) { + return null; + } + + $poco = new PoCo(); + + $poco->preferredUsername = $group->nickname; + $poco->displayName = $group->getBestName(); + + $poco->note = $group->description; + + $paddy = new PoCoAddress(); + $paddy->formatted = $group->location; + $poco->address = $paddy; + + if (!empty($group->homepage)) { + array_push( + $poco->urls, + new PoCoURL( + 'homepage', + $group->homepage, + true + ) + ); + } + + return $poco; + } + + function getPrimaryURL() + { + foreach ($this->urls as $url) { + if ($url->primary) { + return $url; + } + } + } + + function asString() + { + $xs = new XMLStringer(true); + $xs->element( + 'poco:preferredUsername', + null, + $this->preferredUsername + ); + + $xs->element( + 'poco:displayName', + null, + $this->displayName + ); + + if (!empty($this->note)) { + $xs->element('poco:note', null, $this->note); + } + + if (!empty($this->address)) { + $xs->raw($this->address->asString()); + } + + foreach ($this->urls as $url) { + $xs->raw($url->asString()); + } + + return $xs->getString(); + } +} + +/** + * 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'; + + const CONTENT = 'content'; + const SRC = 'src'; + + /** + * Get the permalink for an Activity object + * + * @param DOMElement $element A DOM element + * + * @return string related link, if any + */ + + static function getPermalink($element) + { + return self::getLink($element, 'alternate', 'text/html'); + } + + /** + * Get the permalink for an Activity object + * + * @param DOMElement $element A DOM element + * + * @return string related link, if any + */ + + static function getLink(DOMNode $element, $rel, $type=null) + { + $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK); + + foreach ($links as $link) { + + $linkRel = $link->getAttribute(self::REL); + $linkType = $link->getAttribute(self::TYPE); + + if ($linkRel == $rel && + (is_null($type) || $linkType == $type)) { + return $link->getAttribute(self::HREF); + } + } + + return null; + } + + static function getLinks(DOMNode $element, $rel, $type=null) + { + $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK); + $out = array(); + + foreach ($links as $link) { + + $linkRel = $link->getAttribute(self::REL); + $linkType = $link->getAttribute(self::TYPE); + + if ($linkRel == $rel && + (is_null($type) || $linkType == $type)) { + $out[] = $link; + } + } + + return $out; + } + + /** + * 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 + */ + + static function child(DOMNode $element, $tag, $namespace=self::ATOM) + { + $els = $element->childNodes; + if (empty($els) || $els->length == 0) { + return null; + } else { + for ($i = 0; $i < $els->length; $i++) { + $el = $els->item($i); + if ($el->localName == $tag && $el->namespaceURI == $namespace) { + return $el; + } + } + } + } + + /** + * 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 + */ + + static function childContent(DOMNode $element, $tag, $namespace=self::ATOM) + { + $el = self::child($element, $tag, $namespace); + + if (empty($el)) { + return null; + } else { + return $el->textContent; + } + } + + /** + * Get the content of an atom:entry-like object + * + * @param DOMElement $element The element to examine. + * + * @return string unencoded HTML content of the element, like "This -< is <b>HTML</b>." + * + * @todo handle remote content + * @todo handle embedded XML mime types + * @todo handle base64-encoded non-XML and non-text mime types + */ + + static function getContent($element) + { + $contentEl = ActivityUtils::child($element, self::CONTENT); + + if (!empty($contentEl)) { + + $src = $contentEl->getAttribute(self::SRC); + + if (!empty($src)) { + throw new ClientException(_("Can't handle remote content yet.")); + } + + $type = $contentEl->getAttribute(self::TYPE); + + // slavishly following http://atompub.org/rfc4287.html#rfc.section.4.1.3.3 + + if ($type == 'text') { + return $contentEl->textContent; + } else if ($type == 'html') { + $text = $contentEl->textContent; + return htmlspecialchars_decode($text, ENT_QUOTES); + } else if ($type == 'xhtml') { + $divEl = ActivityUtils::child($contentEl, 'div'); + if (empty($divEl)) { + return null; + } + $doc = $divEl->ownerDocument; + $text = ''; + $children = $divEl->childNodes; + + for ($i = 0; $i < $children->length; $i++) { + $child = $children->item($i); + $text .= $doc->saveXML($child); + } + return trim($text); + } else if (in_array(array('text/xml', 'application/xml'), $type) || + preg_match('#(+|/)xml$#', $type)) { + throw new ClientException(_("Can't handle embedded XML content yet.")); + } else if (strncasecmp($type, 'text/', 5)) { + return $contentEl->textContent; + } else { + throw new ClientException(_("Can't handle embedded Base64 content yet.")); + } + } + } +} + +// XXX: Arg! This wouldn't be necessary if we used Avatars conistently +class AvatarLink +{ + public $url; + public $type; + public $size; + public $width; + public $height; + + function __construct($element=null) + { + if ($element) { + // @fixme use correct namespaces + $this->url = $element->getAttribute('href'); + $this->type = $element->getAttribute('type'); + $width = $element->getAttribute('media:width'); + if ($width != null) { + $this->width = intval($width); + } + $height = $element->getAttribute('media:height'); + if ($height != null) { + $this->height = intval($height); + } + } + } + + static function fromAvatar($avatar) + { + if (empty($avatar)) { + return null; + } + $alink = new AvatarLink(); + $alink->type = $avatar->mediatype; + $alink->height = $avatar->height; + $alink->width = $avatar->width; + $alink->url = $avatar->displayUrl(); + return $alink; + } + + static function fromFilename($filename, $size) + { + $alink = new AvatarLink(); + $alink->url = $filename; + $alink->height = $size; + if (!empty($filename)) { + $alink->width = $size; + $alink->type = self::mediatype($filename); + } else { + $alink->url = User_group::defaultLogo($size); + $alink->type = 'image/png'; + } + return $alink; + } + + // yuck! + static function mediatype($filename) { + $ext = strtolower(end(explode('.', $filename))); + if ($ext == 'jpeg') { + $ext = 'jpg'; + } + // hope we don't support any others + $types = array('png', 'gif', 'jpg', 'jpeg'); + if (in_array($ext, $types)) { + return 'image/' . $ext; + } + 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 ID = 'id'; + const SOURCE = 'source'; + + const NAME = 'name'; + const URI = 'uri'; + const EMAIL = 'email'; + + public $element; + public $type; + public $id; + public $title; + public $summary; + public $content; + public $link; + public $source; + public $avatarLinks = array(); + public $geopoint; + public $poco; + public $displayName; + + /** + * 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 = null) + { + if (empty($element)) { + return; + } + + $this->element = $element; + + $this->geopoint = $this->_childContent( + $element, + ActivityContext::POINT, + ActivityContext::GEORSS + ); + + 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->source = $this->_getSource($element); + + $this->content = ActivityUtils::getContent($element); + + $this->link = ActivityUtils::getPermalink($element); + + } + + // Some per-type attributes... + if ($this->type == self::PERSON || $this->type == self::GROUP) { + $this->displayName = $this->title; + + $avatars = ActivityUtils::getLinks($element, 'avatar'); + foreach ($avatars as $link) { + $this->avatarLinks[] = new AvatarLink($link); + } + + $this->poco = new PoCo($element); + } + } + + private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM) + { + return ActivityUtils::childContent($element, $tag, $namespace); + } + + // Try to get a unique id for the source feed + + private function _getSource($element) + { + $sourceEl = ActivityUtils::child($element, 'source'); + + if (empty($sourceEl)) { + return null; + } else { + $href = ActivityUtils::getLink($sourceEl, 'self'); + if (!empty($href)) { + return $href; + } else { + return ActivityUtils::childContent($sourceEl, 'id'); + } + } + } + + static function fromNotice($notice) + { + $object = new ActivityObject(); + + $object->type = ActivityObject::NOTE; + + $object->id = $notice->uri; + $object->title = $notice->content; + $object->content = $notice->rendered; + $object->link = $notice->bestUrl(); + + return $object; + } + + static function fromProfile($profile) + { + $object = new ActivityObject(); + + $object->type = ActivityObject::PERSON; + $object->id = $profile->getUri(); + $object->title = $profile->getBestName(); + $object->link = $profile->profileurl; + + $orig = $profile->getOriginalAvatar(); + + if (!empty($orig)) { + $object->avatarLinks[] = AvatarLink::fromAvatar($orig); + } + + $sizes = array( + AVATAR_PROFILE_SIZE, + AVATAR_STREAM_SIZE, + AVATAR_MINI_SIZE + ); + + foreach ($sizes as $size) { + + $alink = null; + $avatar = $profile->getAvatar($size); + + if (!empty($avatar)) { + $alink = AvatarLink::fromAvatar($avatar); + } else { + $alink = new AvatarLink(); + $alink->type = 'image/png'; + $alink->height = $size; + $alink->width = $size; + $alink->url = Avatar::defaultImage($size); + } + + $object->avatarLinks[] = $alink; + } + + if (isset($profile->lat) && isset($profile->lon)) { + $object->geopoint = (float)$profile->lat + . ' ' . (float)$profile->lon; + } + + $object->poco = PoCo::fromProfile($profile); + + return $object; + } + + static function fromGroup($group) + { + $object = new ActivityObject(); + + $object->type = ActivityObject::GROUP; + $object->id = $group->getUri(); + $object->title = $group->getBestName(); + $object->link = $group->getUri(); + + $object->avatarLinks[] = AvatarLink::fromFilename( + $group->homepage_logo, + AVATAR_PROFILE_SIZE + ); + + $object->avatarLinks[] = AvatarLink::fromFilename( + $group->stream_logo, + AVATAR_STREAM_SIZE + ); + + $object->avatarLinks[] = AvatarLink::fromFilename( + $group->mini_logo, + AVATAR_MINI_SIZE + ); + + $object->poco = PoCo::fromGroup($group); + + return $object; + } + + + function asString($tag='activity:object') + { + $xs = new XMLStringer(true); + + $xs->elementStart($tag); + + $xs->element('activity:object-type', null, $this->type); + + $xs->element(self::ID, null, $this->id); + + if (!empty($this->title)) { + $xs->element(self::TITLE, null, $this->title); + } + + if (!empty($this->summary)) { + $xs->element(self::SUMMARY, null, $this->summary); + } + + if (!empty($this->content)) { + // XXX: assuming HTML content here + $xs->element(ActivityUtils::CONTENT, array('type' => 'html'), $this->content); + } + + if (!empty($this->link)) { + $xs->element( + 'link', + array( + 'rel' => 'alternate', + 'type' => 'text/html', + 'href' => $this->link + ), + null + ); + } + + if ($this->type == ActivityObject::PERSON + || $this->type == ActivityObject::GROUP) { + + foreach ($this->avatarLinks as $avatar) { + $xs->element( + 'link', array( + 'rel' => 'avatar', + 'type' => $avatar->type, + 'media:width' => $avatar->width, + 'media:height' => $avatar->height, + 'href' => $avatar->url + ), + null + ); + } + } + + if (!empty($this->geopoint)) { + $xs->element( + 'georss:point', + null, + $this->geopoint + ); + } + + if (!empty($this->poco)) { + $xs->raw($this->poco->asString()); + } + + $xs->elementEnd($tag); + + return $xs->getString(); + } +} + +/** + * 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'; + + // Custom OStatus verbs for the flipside until they're standardized + const DELETE = 'http://ostatus.org/schema/1.0/unfollow'; + const UNFAVORITE = 'http://ostatus.org/schema/1.0/unfavorite'; + const UNFOLLOW = 'http://ostatus.org/schema/1.0/unfollow'; + const LEAVE = 'http://ostatus.org/schema/1.0/leave'; + + // For simple profile-update pings; no content to share. + const UPDATE_PROFILE = 'http://ostatus.org/schema/1.0/update-profile'; +} + +class ActivityContext +{ + public $replyToID; + public $replyToUrl; + public $location; + public $attention = array(); + public $conversation; + + const THR = 'http://purl.org/syndication/thread/1.0'; + const GEORSS = 'http://www.georss.org/georss'; + const OSTATUS = 'http://ostatus.org/schema/1.0'; + + const INREPLYTO = 'in-reply-to'; + const REF = 'ref'; + const HREF = 'href'; + + const POINT = 'point'; + + const ATTENTION = 'ostatus:attention'; + const CONVERSATION = 'ostatus:conversation'; + + function __construct($element) + { + $replyToEl = ActivityUtils::child($element, self::INREPLYTO, self::THR); + + if (!empty($replyToEl)) { + $this->replyToID = $replyToEl->getAttribute(self::REF); + $this->replyToUrl = $replyToEl->getAttribute(self::HREF); + } + + $this->location = $this->getLocation($element); + + $this->conversation = ActivityUtils::getLink($element, self::CONVERSATION); + + // Multiple attention links allowed + + $links = $element->getElementsByTagNameNS(ActivityUtils::ATOM, ActivityUtils::LINK); + + for ($i = 0; $i < $links->length; $i++) { + + $link = $links->item($i); + + $linkRel = $link->getAttribute(ActivityUtils::REL); + + if ($linkRel == self::ATTENTION) { + $this->attention[] = $link->getAttribute(self::HREF); + } + } + } + + /** + * Parse location given as a GeoRSS-simple point, if provided. + * http://www.georss.org/simple + * + * @param feed item $entry + * @return mixed Location or false + */ + function getLocation($dom) + { + $points = $dom->getElementsByTagNameNS(self::GEORSS, self::POINT); + + for ($i = 0; $i < $points->length; $i++) { + $point = $points->item($i)->textContent; + return self::locationFromPoint($point); + } + + return null; + } + + // XXX: Move to ActivityUtils or Location? + static function locationFromPoint($point) + { + $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; + if (is_numeric($lat) && is_numeric($lon)) { + common_log(LOG_INFO, "Looking up location for $lat $lon from georss point"); + return Location::fromLatLon($lat, $lon); + } + } + common_log(LOG_ERR, "Ignoring bogus georss:point value $point"); + return null; + } +} + +/** + * 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 + + public $summary; // summary of activity + public $content; // HTML content of activity + public $id; // ID of the activity + public $title; // title of the activity + public $categories = array(); // list of AtomCategory objects + + /** + * 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 = null, $feed = null) + { + if (is_null($entry)) { + return; + } + + $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::getPermalink($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 ActivityContext($contextEl); + } else { + $this->context = new ActivityContext($entry); + } + + $targetEl = $this->_child($entry, self::TARGET); + + if (!empty($targetEl)) { + $this->target = new ActivityObject($targetEl); + } + + $this->summary = ActivityUtils::childContent($entry, 'summary'); + $this->id = ActivityUtils::childContent($entry, 'id'); + $this->content = ActivityUtils::getContent($entry); + + $catEls = $entry->getElementsByTagNameNS(self::ATOM, 'category'); + if ($catEls) { + for ($i = 0; $i < $catEls->length; $i++) { + $catEl = $catEls->item($i); + $this->categories[] = new AtomCategory($catEl); + } + } + } + + /** + * Returns an Atom <entry> based on this activity + * + * @return DOMElement Atom entry + */ + + function toAtomEntry() + { + return null; + } + + function asString($namespace=false) + { + $xs = new XMLStringer(true); + + if ($namespace) { + $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom', + 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', + 'xmlns:georss' => 'http://www.georss.org/georss', + 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0', + 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0', + 'xmlns:media' => 'http://purl.org/syndication/atommedia'); + } else { + $attrs = array(); + } + + $xs->elementStart('entry', $attrs); + + $xs->element('id', null, $this->id); + $xs->element('title', null, $this->title); + $xs->element('published', null, common_date_iso8601($this->time)); + $xs->element('content', array('type' => 'html'), $this->content); + + if (!empty($this->summary)) { + $xs->element('summary', null, $this->summary); + } + + if (!empty($this->link)) { + $xs->element('link', array('rel' => 'alternate', + 'type' => 'text/html'), + $this->link); + } + + // XXX: add context + + $xs->elementStart('author'); + $xs->element('uri', array(), $this->actor->id); + if ($this->actor->title) { + $xs->element('name', array(), $this->actor->title); + } + $xs->elementEnd('author'); + $xs->raw($this->actor->asString('activity:actor')); + + $xs->element('activity:verb', null, $this->verb); + + if ($this->object) { + $xs->raw($this->object->asString()); + } + + if ($this->target) { + $xs->raw($this->target->asString('activity:target')); + } + + foreach ($this->categories as $cat) { + $xs->raw($cat->asString()); + } + + $xs->elementEnd('entry'); + + return $xs->getString(); + } + + private function _child($element, $tag, $namespace=self::SPEC) + { + return ActivityUtils::child($element, $tag, $namespace); + } +} + +class AtomCategory +{ + public $term; + public $scheme; + public $label; + + function __construct($element=null) + { + if ($element && $element->attributes) { + $this->term = $this->extract($element, 'term'); + $this->scheme = $this->extract($element, 'scheme'); + $this->label = $this->extract($element, 'label'); + } + } + + protected function extract($element, $attrib) + { + $node = $element->attributes->getNamedItemNS(Activity::ATOM, $attrib); + if ($node) { + return trim($node->textContent); + } + $node = $element->attributes->getNamedItem($attrib); + if ($node) { + return trim($node->textContent); + } + return null; + } + + function asString() + { + $attribs = array(); + if ($this->term !== null) { + $attribs['term'] = $this->term; + } + if ($this->scheme !== null) { + $attribs['scheme'] = $this->scheme; + } + if ($this->label !== null) { + $attribs['label'] = $this->label; + } + $xs = new XMLStringer(); + $xs->element('category', $attribs); + return $xs->asString(); + } +} diff --git a/lib/adminpanelaction.php b/lib/adminpanelaction.php index f05627b31..a0cdab8a4 100644 --- a/lib/adminpanelaction.php +++ b/lib/adminpanelaction.php @@ -69,6 +69,7 @@ class AdminPanelAction extends Action // User must be logged in. if (!common_logged_in()) { + // TRANS: Client error message $this->clientError(_('Not logged in.')); return false; } @@ -93,6 +94,7 @@ class AdminPanelAction extends Action // User must have the right to change admin settings if (!$user->hasRight(Right::CONFIGURESITE)) { + // TRANS: Client error message $this->clientError(_('You cannot make changes to this site.')); return false; } @@ -103,7 +105,8 @@ class AdminPanelAction extends Action $name = mb_substr($name, 0, -10); - if (!in_array($name, common_config('admin', 'panels'))) { + if (!self::canAdmin($name)) { + // TRANS: Client error message $this->clientError(_('Changes to that panel are not allowed.'), 403); return false; } @@ -134,6 +137,7 @@ class AdminPanelAction extends Action Config::loadSettings(); $this->success = true; + // TRANS: Message after successful saving of administrative settings. $this->msg = _('Settings saved.'); } catch (Exception $e) { $this->success = false; @@ -203,6 +207,7 @@ class AdminPanelAction extends Action function showForm() { + // TRANS: Client error message $this->clientError(_('showForm() not implemented.')); return; } @@ -232,6 +237,7 @@ class AdminPanelAction extends Action function saveSettings() { + // TRANS: Client error message $this->clientError(_('saveSettings() not implemented.')); return; } @@ -255,6 +261,7 @@ class AdminPanelAction extends Action $result = $config->delete(); if (!$result) { common_log_db_error($config, 'DELETE', __FILE__); + // TRANS: Client error message $this->clientError(_("Unable to delete design setting.")); return null; } @@ -262,6 +269,17 @@ class AdminPanelAction extends Action return $result; } + + function canAdmin($name) + { + $isOK = false; + + if (Event::handle('AdminPanelCheck', array($name, &$isOK))) { + $isOK = in_array($name, common_config('admin', 'panels')); + } + + return $isOK; + } } /** @@ -307,34 +325,52 @@ class AdminPanelNav extends Widget if (Event::handle('StartAdminPanelNav', array($this))) { - if ($this->canAdmin('site')) { - $this->out->menuItem(common_local_url('siteadminpanel'), _('Site'), - _('Basic site configuration'), $action_name == 'siteadminpanel', 'nav_site_admin_panel'); + if (AdminPanelAction::canAdmin('site')) { + // TRANS: Menu item title/tooltip + $menu_title = _('Basic site configuration'); + // TRANS: Menu item for site administration + $this->out->menuItem(common_local_url('siteadminpanel'), _m('MENU', 'Site'), + $menu_title, $action_name == 'siteadminpanel', 'nav_site_admin_panel'); } - if ($this->canAdmin('design')) { - $this->out->menuItem(common_local_url('designadminpanel'), _('Design'), - _('Design configuration'), $action_name == 'designadminpanel', 'nav_design_admin_panel'); + if (AdminPanelAction::canAdmin('design')) { + // TRANS: Menu item title/tooltip + $menu_title = _('Design configuration'); + // TRANS: Menu item for site administration + $this->out->menuItem(common_local_url('designadminpanel'), _m('MENU', 'Design'), + $menu_title, $action_name == 'designadminpanel', 'nav_design_admin_panel'); } - if ($this->canAdmin('user')) { - $this->out->menuItem(common_local_url('useradminpanel'), _('User'), - _('User configuration'), $action_name == 'useradminpanel', 'nav_design_admin_panel'); + if (AdminPanelAction::canAdmin('user')) { + // TRANS: Menu item title/tooltip + $menu_title = _('User configuration'); + // TRANS: Menu item for site administration + $this->out->menuItem(common_local_url('useradminpanel'), _m('MENU', 'User'), + $menu_title, $action_name == 'useradminpanel', 'nav_design_admin_panel'); } - if ($this->canAdmin('access')) { - $this->out->menuItem(common_local_url('accessadminpanel'), _('Access'), - _('Access configuration'), $action_name == 'accessadminpanel', 'nav_design_admin_panel'); + if (AdminPanelAction::canAdmin('access')) { + // TRANS: Menu item title/tooltip + $menu_title = _('Access configuration'); + // TRANS: Menu item for site administration + $this->out->menuItem(common_local_url('accessadminpanel'), _m('MENU', 'Access'), + $menu_title, $action_name == 'accessadminpanel', 'nav_design_admin_panel'); } - if ($this->canAdmin('paths')) { - $this->out->menuItem(common_local_url('pathsadminpanel'), _('Paths'), - _('Paths configuration'), $action_name == 'pathsadminpanel', 'nav_design_admin_panel'); + if (AdminPanelAction::canAdmin('paths')) { + // TRANS: Menu item title/tooltip + $menu_title = _('Paths configuration'); + // TRANS: Menu item for site administration + $this->out->menuItem(common_local_url('pathsadminpanel'), _m('MENU', 'Paths'), + $menu_title, $action_name == 'pathsadminpanel', 'nav_design_admin_panel'); } - if ($this->canAdmin('sessions')) { - $this->out->menuItem(common_local_url('sessionsadminpanel'), _('Sessions'), - _('Sessions configuration'), $action_name == 'sessionsadminpanel', 'nav_design_admin_panel'); + if (AdminPanelAction::canAdmin('sessions')) { + // TRANS: Menu item title/tooltip + $menu_title = _('Sessions configuration'); + // TRANS: Menu item for site administration + $this->out->menuItem(common_local_url('sessionsadminpanel'), _m('MENU', 'Sessions'), + $menu_title, $action_name == 'sessionsadminpanel', 'nav_design_admin_panel'); } Event::handle('EndAdminPanelNav', array($this)); @@ -342,8 +378,4 @@ class AdminPanelNav extends Widget $this->action->elementEnd('ul'); } - function canAdmin($name) - { - return in_array($name, common_config('admin', 'panels')); - } } diff --git a/lib/api.php b/lib/apiaction.php index 9000fb4ba..eef0ba637 100644 --- a/lib/api.php +++ b/lib/apiaction.php @@ -63,7 +63,6 @@ class ApiAction extends Action var $count = null; var $max_id = null; var $since_id = null; - var $since = null; var $access = self::READ_ONLY; // read (default) or read-write @@ -85,7 +84,10 @@ class ApiAction extends Action $this->count = (int)$this->arg('count', 20); $this->max_id = (int)$this->arg('max_id', 0); $this->since_id = (int)$this->arg('since_id', 0); - $this->since = $this->arg('since'); + + if ($this->arg('since')) { + $this->clientError(_("since parameter is disabled for performance; use since_id"), 403); + } return true; } @@ -359,7 +361,7 @@ class ApiAction extends Action $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id)); $entry['published'] = common_date_iso8601($notice->created); - $taguribase = common_config('integration', 'taguri'); + $taguribase = TagURI::base(); $entry['id'] = "tag:$taguribase:$entry[link]"; $entry['updated'] = $entry['published']; @@ -803,7 +805,7 @@ class ApiAction extends Action $entry['link'] = common_local_url('showmessage', array('message' => $message->id)); $entry['published'] = common_date_iso8601($message->created); - $taguribase = common_config('integration', 'taguri'); + $taguribase = TagURI::base(); $entry['id'] = "tag:$taguribase:$entry[link]"; $entry['updated'] = $entry['published']; @@ -1219,7 +1221,12 @@ class ApiAction extends Action return User_group::staticGet($this->arg('id')); } else if ($this->arg('id')) { $nickname = common_canonical_nickname($this->arg('id')); - return User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); + if (empty($local)) { + return null; + } else { + return User_group::staticGet('id', $local->id); + } } else if ($this->arg('group_id')) { // This is to ensure that a non-numeric user_id still // overrides screen_name even if it doesn't get used @@ -1228,14 +1235,24 @@ class ApiAction extends Action } } else if ($this->arg('group_name')) { $nickname = common_canonical_nickname($this->arg('group_name')); - return User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); + if (empty($local)) { + return null; + } else { + return User_group::staticGet('id', $local->id); + } } } else if (is_numeric($id)) { return User_group::staticGet($id); } else { $nickname = common_canonical_nickname($id); - return User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); + if (empty($local)) { + return null; + } else { + return User_group::staticGet('id', $local->group_id); + } } } @@ -1311,8 +1328,6 @@ class ApiAction extends Action case 'max_id': $max_id = (int)$this->args['max_id']; return ($max_id < 1) ? 0 : $max_id; - case 'since': - return strtotime($this->args['since']); default: return parent::arg($key, $def); } diff --git a/lib/apiauth.php b/lib/apiauth.php index 25e2196cf..5090871cf 100644 --- a/lib/apiauth.php +++ b/lib/apiauth.php @@ -38,7 +38,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/api.php'; require_once INSTALLDIR . '/lib/apioauth.php'; /** diff --git a/lib/atom10entry.php b/lib/atom10entry.php index 5710c80fc..f8f16d594 100644 --- a/lib/atom10entry.php +++ b/lib/atom10entry.php @@ -27,8 +27,7 @@ * @link http://status.net/ */ -if (!defined('STATUSNET') -{ +if (!defined('STATUSNET')) { exit(1); } @@ -87,7 +86,7 @@ class Atom10Entry extends XMLStringer * * @return void */ - function validate + function validate() { } diff --git a/lib/atom10feed.php b/lib/atom10feed.php index 14a3beb83..8842840d5 100644 --- a/lib/atom10feed.php +++ b/lib/atom10feed.php @@ -78,7 +78,7 @@ class Atom10Feed extends XMLStringer $this->authors = array(); $this->links = array(); $this->entries = array(); - $this->addNamespace('xmlns', 'http://www.w3.org/2005/Atom'); + $this->addNamespace('', 'http://www.w3.org/2005/Atom'); } /** @@ -109,11 +109,11 @@ class Atom10Feed extends XMLStringer ); } - if (!is_null($uri)) { + if (isset($uri)) { $xs->element('uri', null, $uri); } - if (!is_null(email)) { + if (isset($email)) { $xs->element('email', null, $email); } @@ -162,7 +162,14 @@ class Atom10Feed extends XMLStringer { $this->xw->startDocument('1.0', 'UTF-8'); $commonAttrs = array('xml:lang' => 'en-US'); - $commonAttrs = array_merge($commonAttrs, $this->namespaces); + foreach ($this->namespaces as $prefix => $uri) { + if ($prefix == '') { + $attr = 'xmlns'; + } else { + $attr = 'xmlns:' . $prefix; + } + $commonAttrs[$attr] = $uri; + } $this->elementStart('feed', $commonAttrs); $this->element('id', null, $this->id); diff --git a/lib/atomnoticefeed.php b/lib/atomnoticefeed.php index b7a60bde6..3c3556cb9 100644 --- a/lib/atomnoticefeed.php +++ b/lib/atomnoticefeed.php @@ -50,23 +50,33 @@ class AtomNoticeFeed extends Atom10Feed // Feeds containing notice info use these namespaces $this->addNamespace( - 'xmlns:thr', + 'thr', 'http://purl.org/syndication/thread/1.0' ); $this->addNamespace( - 'xmlns:georss', + 'georss', 'http://www.georss.org/georss' ); $this->addNamespace( - 'xmlns:activity', + 'activity', 'http://activitystrea.ms/spec/1.0/' ); + $this->addNamespace( + 'media', + 'http://purl.org/syndication/atommedia' + ); + + $this->addNamespace( + 'poco', + 'http://portablecontacts.net/spec/1.0' + ); + // XXX: What should the uri be? $this->addNamespace( - 'xmlns:ostatus', + 'ostatus', 'http://ostatus.org/schema/1.0' ); } diff --git a/lib/atomusernoticefeed.php b/lib/atomusernoticefeed.php index 9f224325c..2ad8de455 100644 --- a/lib/atomusernoticefeed.php +++ b/lib/atomusernoticefeed.php @@ -54,9 +54,14 @@ class AtomUserNoticeFeed extends AtomNoticeFeed * * @return void */ + function __construct($user = null, $indent = true) { parent::__construct($indent); $this->user = $user; + if (!empty($user)) { + $profile = $user->getProfile(); + $this->addAuthor($profile->nickname, $user->uri); + } } function getUser() diff --git a/lib/authenticationplugin.php b/lib/authenticationplugin.php index 5be3ea5b9..0a3763e2e 100644 --- a/lib/authenticationplugin.php +++ b/lib/authenticationplugin.php @@ -79,7 +79,7 @@ abstract class AuthenticationPlugin extends Plugin $nickname = $username; } $registration_data = array(); - $registration_data['nickname'] = $nickname ; + $registration_data['nickname'] = $nickname; return User::register($registration_data); } @@ -101,12 +101,14 @@ abstract class AuthenticationPlugin extends Plugin * Used during autoregistration * Useful if your usernames are ugly, and you want to suggest * nice looking nicknames when users initially sign on + * All nicknames returned by this function should be valid + * implementations may want to use common_nicknamize() to ensure validity * @param username * @return string nickname */ function suggestNicknameForUsername($username) { - return $username; + return common_nicknamize($username); } //------------Below are the methods that connect StatusNet to the implementing Auth plugin------------\\ @@ -129,7 +131,7 @@ abstract class AuthenticationPlugin extends Plugin $test_user = User::staticGet('nickname', $suggested_nickname); if($test_user) { //someone already exists with the suggested nickname, so used the passed nickname - $suggested_nickname = $nickname; + $suggested_nickname = common_nicknamize($nickname); } $test_user = User::staticGet('nickname', $suggested_nickname); if($test_user) { diff --git a/lib/authorizationplugin.php b/lib/authorizationplugin.php index 733b0c065..07da9b2d1 100644 --- a/lib/authorizationplugin.php +++ b/lib/authorizationplugin.php @@ -85,7 +85,7 @@ abstract class AuthorizationPlugin extends Plugin } function onStartSetApiUser(&$user) { - return $this->onStartSetUser(&$user); + return $this->onStartSetUser($user); } function onStartHasRole($profile, $name, &$has_role) { diff --git a/lib/command.php b/lib/command.php index 2a51fd687..db8e80030 100644 --- a/lib/command.php +++ b/lib/command.php @@ -548,12 +548,19 @@ class SubCommand extends Command return; } - $result = subs_subscribe_user($this->user, $this->other); + $otherUser = User::staticGet('nickname', $this->other); - if ($result == 'true') { + if (empty($otherUser)) { + $channel->error($this->user, _('No such user')); + return; + } + + try { + Subscription::start($this->user->getProfile(), + $otherUser->getProfile()); $channel->output($this->user, sprintf(_('Subscribed to %s'), $this->other)); - } else { - $channel->error($this->user, $result); + } catch (Exception $e) { + $channel->error($this->user, $e->getMessage()); } } } @@ -576,12 +583,18 @@ class UnsubCommand extends Command return; } - $result=subs_unsubscribe_user($this->user, $this->other); + $otherUser = User::staticGet('nickname', $this->other); - if ($result) { + if (empty($otherUser)) { + $channel->error($this->user, _('No such user')); + } + + try { + Subscription::cancel($this->user->getProfile(), + $otherUser->getProfile()); $channel->output($this->user, sprintf(_('Unsubscribed from %s'), $this->other)); - } else { - $channel->error($this->user, $result); + } catch (Exception $e) { + $channel->error($this->user, $e->getMessage()); } } } @@ -655,6 +668,34 @@ class LoginCommand extends Command } } +class LoseCommand extends Command +{ + + var $other = null; + + function __construct($user, $other) + { + parent::__construct($user); + $this->other = $other; + } + + function execute($channel) + { + if(!$this->other) { + $channel->error($this->user, _('Specify the name of the user to unsubscribe from')); + return; + } + + $result=subs_unsubscribe_from($this->user, $this->other); + + if ($result) { + $channel->output($this->user, sprintf(_('Unsubscribed %s'), $this->other)); + } else { + $channel->error($this->user, $result); + } + } +} + class SubscriptionsCommand extends Command { function execute($channel) @@ -737,6 +778,7 @@ class HelpCommand extends Command "d <nickname> <text> - direct message to user\n". "get <nickname> - get last notice from user\n". "whois <nickname> - get profile info on user\n". + "lose <nickname> - force user to stop following you\n". "fav <nickname> - add user's last notice as a 'fave'\n". "fav #<notice_id> - add notice with the given id as a 'fave'\n". "repeat #<notice_id> - repeat a notice with a given id\n". diff --git a/lib/commandinterpreter.php b/lib/commandinterpreter.php index c2add7299..fbc6174bb 100644 --- a/lib/commandinterpreter.php +++ b/lib/commandinterpreter.php @@ -47,6 +47,17 @@ class CommandInterpreter } else { return new LoginCommand($user); } + case 'lose': + if ($arg) { + list($other, $extra) = $this->split_arg($arg); + if ($extra) { + return null; + } else { + return new LoseCommand($user, $other); + } + } else { + return null; + } case 'subscribers': if ($arg) { return null; diff --git a/lib/common.php b/lib/common.php index b95cd1175..6c01c7db4 100644 --- a/lib/common.php +++ b/lib/common.php @@ -22,7 +22,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } //exit with 200 response, if this is checking fancy from the installer if (isset($_REQUEST['p']) && $_REQUEST['p'] == 'check-fancy') { exit; } -define('STATUSNET_VERSION', '0.9.0beta5'); +define('STATUSNET_VERSION', '0.9.0beta6+bugfix1'); define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility define('STATUSNET_CODENAME', 'Stand'); @@ -71,6 +71,7 @@ if (!function_exists('dl')) { # global configuration object require_once('PEAR.php'); +require_once('PEAR/Exception.php'); require_once('DB/DataObject.php'); require_once('DB/DataObject/Cast.php'); # for dates @@ -123,10 +124,22 @@ require_once INSTALLDIR.'/lib/util.php'; require_once INSTALLDIR.'/lib/action.php'; require_once INSTALLDIR.'/lib/mail.php'; require_once INSTALLDIR.'/lib/subs.php'; +require_once INSTALLDIR.'/lib/activity.php'; require_once INSTALLDIR.'/lib/clientexception.php'; require_once INSTALLDIR.'/lib/serverexception.php'; + +//set PEAR error handling to use regular PHP exceptions +function PEAR_ErrorToPEAR_Exception($err) +{ + if ($err->getCode()) { + throw new PEAR_Exception($err->getMessage(), $err->getCode()); + } + throw new PEAR_Exception($err->getMessage()); +} +PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'PEAR_ErrorToPEAR_Exception'); + try { StatusNet::init(@$server, @$path, @$conffile); } catch (NoConfigException $e) { diff --git a/lib/default.php b/lib/default.php index 37eb6cafa..7b97c7dda 100644 --- a/lib/default.php +++ b/lib/default.php @@ -175,10 +175,10 @@ $default = array('enabled' => false), 'integration' => array('source' => 'StatusNet', # source attribute for Twitter - 'taguri' => $_server.',2009'), # base for tag URIs + 'taguri' => null), # base for tag URIs 'twitter' => - array('enabled' => true, - 'consumer_key' => null, + array('signin' => true, + 'consumer_key' => null, 'consumer_secret' => null), 'cache' => array('base' => null), @@ -278,9 +278,7 @@ $default = 'TightUrl' => array('shortenerName' => '2tu.us', 'freeService' => true,'serviceUrl'=>'http://2tu.us/?save=y&url=%1$s'), 'Geonames' => null, 'Mapstraction' => null, - 'Linkback' => null, 'WikiHashtags' => null, - 'PubSubHubBub' => null, 'RSSCloud' => null, 'OpenID' => null), ), diff --git a/lib/distribqueuehandler.php b/lib/distribqueuehandler.php index 4477468d0..d2be7a92c 100644 --- a/lib/distribqueuehandler.php +++ b/lib/distribqueuehandler.php @@ -63,31 +63,7 @@ class DistribQueueHandler // XXX: do we need to change this for remote users? try { - $notice->saveTags(); - } catch (Exception $e) { - $this->logit($notice, $e); - } - - try { - $groups = $notice->saveGroups(); - } catch (Exception $e) { - $this->logit($notice, $e); - } - - try { - $recipients = $notice->saveReplies(); - } catch (Exception $e) { - $this->logit($notice, $e); - } - - try { - $notice->addToInboxes($groups, $recipients); - } catch (Exception $e) { - $this->logit($notice, $e); - } - - try { - $notice->saveUrls(); + $notice->addToInboxes(); } catch (Exception $e) { $this->logit($notice, $e); } @@ -107,7 +83,7 @@ class DistribQueueHandler return true; } - + protected function logit($notice, $e) { common_log(LOG_ERR, "Distrib queue exception saving notice $notice->id: " . diff --git a/lib/imagefile.php b/lib/imagefile.php index 6bc8e599b..7b0479455 100644 --- a/lib/imagefile.php +++ b/lib/imagefile.php @@ -99,6 +99,10 @@ class ImageFile if ($info[2] !== IMAGETYPE_GIF && $info[2] !== IMAGETYPE_JPEG && + $info[2] !== IMAGETYPE_BMP && + $info[2] !== IMAGETYPE_WBMP && + $info[2] !== IMAGETYPE_XBM && + $info[2] !== IMAGETYPE_XPM && $info[2] !== IMAGETYPE_PNG) { @unlink($_FILES[$param]['tmp_name']); @@ -146,6 +150,18 @@ class ImageFile case IMAGETYPE_PNG: $image_src = imagecreatefrompng($this->filepath); break; + case IMAGETYPE_BMP: + $image_src = imagecreatefrombmp($this->filepath); + break; + case IMAGETYPE_WBMP: + $image_src = imagecreatefromwbmp($this->filepath); + break; + case IMAGETYPE_XBM: + $image_src = imagecreatefromxbm($this->filepath); + break; + case IMAGETYPE_XPM: + $image_src = imagecreatefromxpm($this->filepath); + break; default: throw new Exception(_('Unknown file type')); return; @@ -153,7 +169,7 @@ class ImageFile $image_dest = imagecreatetruecolor($size, $size); - if ($this->type == IMAGETYPE_GIF || $this->type == IMAGETYPE_PNG) { + if ($this->type == IMAGETYPE_GIF || $this->type == IMAGETYPE_PNG || $this->type == IMAGETYPE_BMP) { $transparent_idx = imagecolortransparent($image_src); @@ -176,6 +192,24 @@ class ImageFile imagecopyresampled($image_dest, $image_src, 0, 0, $x, $y, $size, $size, $w, $h); + if($this->type == IMAGETYPE_BMP) { + //we don't want to save BMP... it's an inefficient, rare, antiquated format + //save png instead + $this->type = IMAGETYPE_PNG; + } else if($this->type == IMAGETYPE_WBMP) { + //we don't want to save WBMP... it's a rare format that we can't guarantee clients will support + //save png instead + $this->type = IMAGETYPE_PNG; + } else if($this->type == IMAGETYPE_XBM) { + //we don't want to save XBM... it's a rare format that we can't guarantee clients will support + //save png instead + $this->type = IMAGETYPE_PNG; + } else if($this->type == IMAGETYPE_XPM) { + //we don't want to save XPM... it's a rare format that we can't guarantee clients will support + //save png instead + $this->type = IMAGETYPE_PNG; + } + $outname = Avatar::filename($this->id, image_type_to_extension($this->type), $size, @@ -245,4 +279,101 @@ class ImageFile return $num; } -}
\ No newline at end of file +} + +//PHP doesn't (as of 2/24/2010) have an imagecreatefrombmp so conditionally define one +if(!function_exists('imagecreatefrombmp')){ + //taken shamelessly from http://www.php.net/manual/en/function.imagecreatefromwbmp.php#86214 + function imagecreatefrombmp($p_sFile) + { + // Load the image into a string + $file = fopen($p_sFile,"rb"); + $read = fread($file,10); + while(!feof($file)&&($read<>"")) + $read .= fread($file,1024); + + $temp = unpack("H*",$read); + $hex = $temp[1]; + $header = substr($hex,0,108); + + // Process the header + // Structure: http://www.fastgraph.com/help/bmp_header_format.html + if (substr($header,0,4)=="424d") + { + // Cut it in parts of 2 bytes + $header_parts = str_split($header,2); + + // Get the width 4 bytes + $width = hexdec($header_parts[19].$header_parts[18]); + + // Get the height 4 bytes + $height = hexdec($header_parts[23].$header_parts[22]); + + // Unset the header params + unset($header_parts); + } + + // Define starting X and Y + $x = 0; + $y = 1; + + // Create newimage + $image = imagecreatetruecolor($width,$height); + + // Grab the body from the image + $body = substr($hex,108); + + // Calculate if padding at the end-line is needed + // Divided by two to keep overview. + // 1 byte = 2 HEX-chars + $body_size = (strlen($body)/2); + $header_size = ($width*$height); + + // Use end-line padding? Only when needed + $usePadding = ($body_size>($header_size*3)+4); + + // Using a for-loop with index-calculation instaid of str_split to avoid large memory consumption + // Calculate the next DWORD-position in the body + for ($i=0;$i<$body_size;$i+=3) + { + // Calculate line-ending and padding + if ($x>=$width) + { + // If padding needed, ignore image-padding + // Shift i to the ending of the current 32-bit-block + if ($usePadding) + $i += $width%4; + + // Reset horizontal position + $x = 0; + + // Raise the height-position (bottom-up) + $y++; + + // Reached the image-height? Break the for-loop + if ($y>$height) + break; + } + + // Calculation of the RGB-pixel (defined as BGR in image-data) + // Define $i_pos as absolute position in the body + $i_pos = $i*2; + $r = hexdec($body[$i_pos+4].$body[$i_pos+5]); + $g = hexdec($body[$i_pos+2].$body[$i_pos+3]); + $b = hexdec($body[$i_pos].$body[$i_pos+1]); + + // Calculate and draw the pixel + $color = imagecolorallocate($image,$r,$g,$b); + imagesetpixel($image,$x,$height-$y,$color); + + // Raise the horizontal position + $x++; + } + + // Unset the body / free the memory + unset($body); + + // Return image-object + return $image; + } +} diff --git a/lib/joinform.php b/lib/joinform.php index aefb553aa..aa8bc20e2 100644 --- a/lib/joinform.php +++ b/lib/joinform.php @@ -100,7 +100,7 @@ class JoinForm extends Form function action() { return common_local_url('joingroup', - array('nickname' => $this->group->nickname)); + array('id' => $this->group->id)); } /** diff --git a/lib/leaveform.php b/lib/leaveform.php index e63d96ee8..5469b5704 100644 --- a/lib/leaveform.php +++ b/lib/leaveform.php @@ -100,7 +100,7 @@ class LeaveForm extends Form function action() { return common_local_url('leavegroup', - array('nickname' => $this->group->nickname)); + array('id' => $this->group->id)); } /** diff --git a/lib/mediafile.php b/lib/mediafile.php index e3d5b1dbc..10d90d008 100644 --- a/lib/mediafile.php +++ b/lib/mediafile.php @@ -262,7 +262,7 @@ class MediaFile $filetype = MIME_Type::autoDetect($stream['uri']); } - if (in_array($filetype, common_config('attachments', 'supported'))) { + if (common_config('attachments', 'supported') === true || in_array($filetype, common_config('attachments', 'supported'))) { return $filetype; } $media = MIME_Type::getMedia($filetype); diff --git a/lib/messageform.php b/lib/messageform.php index 0c568e1bd..b116964da 100644 --- a/lib/messageform.php +++ b/lib/messageform.php @@ -175,6 +175,6 @@ class MessageForm extends Form 'class' => 'submit', 'name' => 'message_send', 'type' => 'submit', - 'value' => _('Send'))); + 'value' => _m('Send button for sending notice', 'Send'))); } } diff --git a/lib/noticeform.php b/lib/noticeform.php index 62df5c941..7278c41a9 100644 --- a/lib/noticeform.php +++ b/lib/noticeform.php @@ -233,6 +233,6 @@ class NoticeForm extends Form 'class' => 'submit', 'name' => 'status_submit', 'type' => 'submit', - 'value' => _('Send'))); + 'value' => _m('Send button for sending notice', 'Send'))); } } diff --git a/lib/noticelist.php b/lib/noticelist.php index 837cb90fa..88a925241 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -380,12 +380,12 @@ class NoticeListItem extends Widget function showNoticeLink() { - if($this->notice->is_local == Notice::LOCAL_PUBLIC || $this->notice->is_local == Notice::LOCAL_NONPUBLIC){ - $noticeurl = common_local_url('shownotice', - array('notice' => $this->notice->id)); - }else{ - $noticeurl = $this->notice->uri; - } + $noticeurl = $this->notice->bestUrl(); + + // above should always return an URL + + assert(!empty($noticeurl)); + $this->out->elementStart('a', array('rel' => 'bookmark', 'class' => 'timestamp', 'href' => $noticeurl)); @@ -438,14 +438,15 @@ class NoticeListItem extends Widget $this->out->text(_('at')); $this->out->text(' '); if (empty($url)) { - $this->out->element('span', array('class' => 'geo', + $this->out->element('abbr', array('class' => 'geo', 'title' => $latlon), $name); } else { - $this->out->element('a', array('class' => 'geo', - 'title' => $latlon, - 'href' => $url), + $this->out->elementStart('a', array('href' => $url)); + $this->out->element('abbr', array('class' => 'geo', + 'title' => $latlon), $name); + $this->out->elementEnd('a'); } $this->out->elementEnd('span'); } @@ -539,22 +540,40 @@ class NoticeListItem extends Widget function showContext() { $hasConversation = false; - if( !empty($this->notice->conversation) - && $this->notice->conversation != $this->notice->id){ - $hasConversation = true; - }else{ - $conversation = Notice::conversationStream($this->notice->id, 1, 1); - if($conversation->N > 0){ + if (!empty($this->notice->conversation)) { + $conversation = Notice::conversationStream( + $this->notice->conversation, + 1, + 1 + ); + if ($conversation->N > 0) { $hasConversation = true; } } - 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, - 'class' => 'response'), - _('in context')); + if ($hasConversation) { + $conv = Conversation::staticGet( + 'id', + $this->notice->conversation + ); + $convurl = $conv->uri; + if (!empty($convurl)) { + $this->out->text(' '); + $this->out->element( + 'a', + array( + 'href' => $convurl.'#notice-'.$this->notice->id, + 'class' => 'response'), + _('in context') + ); + } else { + $msg = sprintf( + "Couldn't find Conversation ID %d to make 'in context'" + . "link for Notice ID %d", + $this->notice->conversation, + $this->notice->id + ); + common_log(LOG_WARNING, $msg); + } } } diff --git a/lib/oauthstore.php b/lib/oauthstore.php index eabe37f9f..a6a6de750 100644 --- a/lib/oauthstore.php +++ b/lib/oauthstore.php @@ -390,7 +390,7 @@ class StatusNetOAuthDataStore extends OAuthDataStore $sub->subscribed = $user->id; if (!$sub->find(true)) { - return 0; + return array(); } /* Since we do not use OMB_Service_Provider’s action methods, there diff --git a/lib/omb.php b/lib/omb.php index 0f38a4936..db60fa0ef 100644 --- a/lib/omb.php +++ b/lib/omb.php @@ -29,11 +29,9 @@ require_once 'Auth/Yadis/Yadis.php'; function omb_oauth_consumer() { - static $con = null; - if (is_null($con)) { - $con = new OAuthConsumer(common_root_url(), ''); - } - return $con; + // Don't try to make this static. Leads to issues in + // multi-site setups - Z + return new OAuthConsumer(common_root_url(), ''); } function omb_oauth_server() @@ -79,7 +77,7 @@ function omb_broadcast_notice($notice) /* Get remote users subscribed to this profile. */ $rp = new Remote_profile(); - $rp->query('SELECT postnoticeurl, token, secret ' . + $rp->query('SELECT remote_profile.*, secret, token ' . 'FROM subscription JOIN remote_profile ' . 'ON subscription.subscriber = remote_profile.id ' . 'WHERE subscription.subscribed = ' . $notice->profile_id . ' '); @@ -95,7 +93,8 @@ function omb_broadcast_notice($notice) /* Post notice. */ $service = new StatusNet_OMB_Service_Consumer( - array(OMB_ENDPOINT_POSTNOTICE => $rp->postnoticeurl)); + array(OMB_ENDPOINT_POSTNOTICE => $rp->postnoticeurl), + $rp->uri); try { $service->setToken($rp->token, $rp->secret); $service->postNotice($omb_notice); @@ -127,7 +126,7 @@ function omb_broadcast_profile($profile) /* Get remote users subscribed to this profile. */ $rp = new Remote_profile(); - $rp->query('SELECT updateprofileurl, token, secret ' . + $rp->query('SELECT remote_profile.*, secret, token ' . 'FROM subscription JOIN remote_profile ' . 'ON subscription.subscriber = remote_profile.id ' . 'WHERE subscription.subscribed = ' . $profile->id . ' '); @@ -143,7 +142,8 @@ function omb_broadcast_profile($profile) /* Update profile. */ $service = new StatusNet_OMB_Service_Consumer( - array(OMB_ENDPOINT_UPDATEPROFILE => $rp->updateprofileurl)); + array(OMB_ENDPOINT_UPDATEPROFILE => $rp->updateprofileurl), + $rp->uri); try { $service->setToken($rp->token, $rp->secret); $service->updateProfile($omb_profile); @@ -161,13 +161,14 @@ function omb_broadcast_profile($profile) } class StatusNet_OMB_Service_Consumer extends OMB_Service_Consumer { - public function __construct($urls) + public function __construct($urls, $listener_uri=null) { $this->services = $urls; $this->datastore = omb_oauth_datastore(); $this->oauth_consumer = omb_oauth_consumer(); $this->fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); $this->fetcher->timeout = intval(common_config('omb', 'timeout')); + $this->listener_uri = $listener_uri; } } diff --git a/lib/profilelist.php b/lib/profilelist.php index 693cd6449..4f1e84a6a 100644 --- a/lib/profilelist.php +++ b/lib/profilelist.php @@ -273,13 +273,18 @@ class ProfileListItem extends Widget $usf = new UnsubscribeForm($this->out, $this->profile); $usf->show(); } else { - // Is it a local user? can't remote sub from a list - // XXX: make that possible! $other = User::staticGet('id', $this->profile->id); if (!empty($other)) { $sf = new SubscribeForm($this->out, $this->profile); $sf->show(); } + else { + $url = common_local_url('remotesubscribe', + array('nickname' => $this->profile->nickname)); + $this->out->element('a', array('href' => $url, + 'class' => 'entity_remote_subscribe'), + _('Subscribe')); + } } $this->out->elementEnd('li'); } diff --git a/lib/profilequeuehandler.php b/lib/profilequeuehandler.php new file mode 100644 index 000000000..6ce93229b --- /dev/null +++ b/lib/profilequeuehandler.php @@ -0,0 +1,52 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * @package QueueHandler + * @maintainer Brion Vibber <brion@status.net> + */ + +class ProfileQueueHandler extends QueueHandler +{ + + function transport() + { + return 'profile'; + } + + function handle($profile) + { + if (!($profile instanceof Profile)) { + common_log(LOG_ERR, "Got a bogus profile, not broadcasting"); + return true; + } + + if (Event::handle('StartBroadcastProfile', array($profile))) { + require_once(INSTALLDIR.'/lib/omb.php'); + try { + omb_broadcast_profile($profile); + } catch (Exception $e) { + common_log(LOG_ERR, "Failed sending OMB profiles: " . $e->getMessage()); + } + } + Event::handle('EndBroadcastProfile', array($profile)); + return true; + } + +} diff --git a/lib/queuemanager.php b/lib/queuemanager.php index 710829e49..87bd356aa 100644 --- a/lib/queuemanager.php +++ b/lib/queuemanager.php @@ -264,6 +264,9 @@ abstract class QueueManager extends IoManager $this->connect('sms', 'SmsQueueHandler'); } + // Broadcasting profile updates to OMB remote subscribers + $this->connect('profile', 'ProfileQueueHandler'); + // XMPP output handlers... if (common_config('xmpp', 'enabled')) { // Delivery prep, read by queuedaemon.php: diff --git a/lib/router.php b/lib/router.php index 987d0152e..abbce041d 100644 --- a/lib/router.php +++ b/lib/router.php @@ -247,6 +247,9 @@ class Router $m->connect('group/:nickname/'.$v, array('action' => $v.'group'), array('nickname' => '[a-zA-Z0-9]+')); + $m->connect('group/:id/id/'.$v, + array('action' => $v.'group'), + array('id' => '[0-9]+')); } foreach (array('members', 'logo', 'rss', 'designsettings') as $n) { @@ -668,7 +671,7 @@ class Router foreach (array('subscriptions', 'subscribers', 'all', 'foaf', 'xrds', - 'replies', 'microsummary') as $a) { + 'replies', 'microsummary', 'hcard') as $a) { $m->connect($a, array('action' => $a, 'nickname' => $nickname)); @@ -734,7 +737,7 @@ class Router foreach (array('subscriptions', 'subscribers', 'nudge', 'all', 'foaf', 'xrds', - 'replies', 'inbox', 'outbox', 'microsummary') as $a) { + 'replies', 'inbox', 'outbox', 'microsummary', 'hcard') as $a) { $m->connect(':nickname/'.$a, array('action' => $a), array('nickname' => '[a-zA-Z0-9]{1,64}')); diff --git a/lib/subs.php b/lib/subs.php index 5ac1a75a5..e2ce0667e 100644 --- a/lib/subs.php +++ b/lib/subs.php @@ -19,24 +19,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once('XMPPHP/XMPP.php'); - -/* Subscribe $user to nickname $other_nickname - Returns true or an error message. -*/ - -function subs_subscribe_user($user, $other_nickname) -{ - - $other = User::staticGet('nickname', $other_nickname); - - if (!$other) { - return _('No such user.'); - } - - return subs_subscribe_to($user, $other); -} - /* Subscribe user $user to other user $other. * Note: $other must be a local user, not a remote profile. * Because the other way is quite a bit more complicated. @@ -44,111 +26,40 @@ function subs_subscribe_user($user, $other_nickname) function subs_subscribe_to($user, $other) { - if (!$user->hasRight(Right::SUBSCRIBE)) { - return _('You have been banned from subscribing.'); - } - - if ($user->isSubscribed($other)) { - return _('Already subscribed!'); - } - - if ($other->hasBlocked($user)) { - return _('User has blocked you.'); - } - try { - if (Event::handle('StartSubscribe', array($user, $other))) { - - if (!$user->subscribeTo($other)) { - return _('Could not subscribe.'); - return; - } - - subs_notify($other, $user); - - $cache = common_memcache(); - - if ($cache) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id)); - } - - $profile = $user->getProfile(); - - $profile->blowSubscriptionsCount(); - $other->blowSubscribersCount(); - - if ($other->autosubscribe && !$other->isSubscribed($user) && !$user->hasBlocked($other)) { - if (!$other->subscribeTo($user)) { - return _('Could not subscribe other to you.'); - } - $cache = common_memcache(); - - if ($cache) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $other->id)); - } - - subs_notify($user, $other); - } - - Event::handle('EndSubscribe', array($user, $other)); - } + Subscription::start($user->getProfile(), $other); + return true; } catch (Exception $e) { return $e->getMessage(); } - - return true; -} - -function subs_notify($listenee, $listener) -{ - # XXX: add other notifications (Jabber, SMS) here - # XXX: queue this and handle it offline - # XXX: Whatever happens, do it in Twitter-like API, too - subs_notify_email($listenee, $listener); } -function subs_notify_email($listenee, $listener) -{ - mail_subscribe_notify($listenee, $listener); -} - -/* Unsubscribe $user from nickname $other_nickname - Returns true or an error message. -*/ - -function subs_unsubscribe_user($user, $other_nickname) -{ - - $other = User::staticGet('nickname', $other_nickname); - - if (!$other) { - return _('No such user.'); - } - - return subs_unsubscribe_to($user, $other->getProfile()); -} - -/* Unsubscribe user $user from profile $other - * NB: other can be a remote user. */ - function subs_unsubscribe_to($user, $other) { - if (!$user->isSubscribed($other)) - return _('Not subscribed!'); - - // Don't allow deleting self subs - - if ($user->id == $other->id) { - return _('Couldn\'t delete self-subscription.'); + try { + Subscription::cancel($user->getProfile(), $other); + return true; + } catch (Exception $e) { + return $e->getMessage(); } +} +function subs_unsubscribe_from($user, $other){ + $local = User::staticGet("nickname",$other); + if($local){ + return subs_unsubscribe_to($local,$user); + } else { try { - if (Event::handle('StartUnsubscribe', array($user, $other))) { + $remote = Profile::staticGet("nickname",$other); + if(is_string($remote)){ + return $remote; + } + if (Event::handle('StartUnsubscribe', array($remote,$user))) { $sub = DB_DataObject::factory('subscription'); - $sub->subscriber = $user->id; - $sub->subscribed = $other->id; + $sub->subscriber = $remote->id; + $sub->subscribed = $user->id; $sub->find(true); @@ -160,20 +71,18 @@ function subs_unsubscribe_to($user, $other) $cache = common_memcache(); if ($cache) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id)); + $cache->delete(common_cache_key('user:notices_with_friends:' . $remote->id)); } - $profile = $user->getProfile(); - $profile->blowSubscriptionsCount(); - $other->blowSubscribersCount(); + $user->blowSubscribersCount(); + $remote->blowSubscribersCount(); - Event::handle('EndUnsubscribe', array($user, $other)); + Event::handle('EndUnsubscribe', array($remote, $user)); } } catch (Exception $e) { return $e->getMessage(); } - - return true; + } } diff --git a/lib/taguri.php b/lib/taguri.php new file mode 100644 index 000000000..d8398eded --- /dev/null +++ b/lib/taguri.php @@ -0,0 +1,96 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Utility for creating new tag: URIs + * + * 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 URI + * @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); +} + +/** + * Mint tag: URIs + * + * tag: URIs are unique identifiers according to http://tools.ietf.org/html/rfc4151. + * + * We use them for creating URIs for things that can't be HTTP retrieved. + * + * @category URI + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class TagURI +{ + /** + * Return the base part of a tag URI + * + * Note: use mint() instead. + * + * @return string Tag URI base to use + */ + + static function base() + { + $base = common_config('integration', 'taguri'); + + if (empty($base)) { + + $base = common_config('site', 'server').','.date('Y-m-d'); + + $pathPart = trim(common_config('site', 'path'), '/'); + + if (!empty($pathPart)) { + $base .= ':'.str_replace('/', ':', $pathPart); + } + } + + return $base; + } + + /** + * Make a new tag URI + * + * Builds the proper base and creates all the parts + * + * @return string minted URI + */ + + static function mint() + { + $base = self::base(); + + $args = func_get_args(); + + $format = array_shift($args); + + $extra = vsprintf($format, $args); + + return 'tag:'.$base.':'.$extra; + } +} diff --git a/lib/util.php b/lib/util.php index ef7852953..add1b0ae6 100644 --- a/lib/util.php +++ b/lib/util.php @@ -134,7 +134,7 @@ function common_check_user($nickname, $password) $authenticatedUser = false; if (Event::handle('StartCheckPassword', array($nickname, $password, &$authenticatedUser))) { - $user = User::staticGet('nickname', $nickname); + $user = User::staticGet('nickname', common_canonical_nickname($nickname)); if (!empty($user)) { if (!empty($password)) { // never allow login with blank password if (0 == strcmp(common_munge_password($password, $user->id), @@ -426,13 +426,187 @@ function common_render_content($text, $notice) { $r = common_render_text($text); $id = $notice->profile_id; - $r = preg_replace('/(^|\s+)@(['.NICKNAME_FMT.']{1,64})/e', "'\\1@'.common_at_link($id, '\\2')", $r); - $r = preg_replace('/^T ([A-Z0-9]{1,64}) /e', "'T '.common_at_link($id, '\\1').' '", $r); - $r = preg_replace('/(^|[\s\.\,\:\;]+)@#([A-Za-z0-9]{1,64})/e', "'\\1@#'.common_at_hash_link($id, '\\2')", $r); + $r = common_linkify_mentions($r, $notice); $r = preg_replace('/(^|[\s\.\,\:\;]+)!([A-Za-z0-9]{1,64})/e', "'\\1!'.common_group_link($id, '\\2')", $r); return $r; } +function common_linkify_mentions($text, $notice) +{ + $mentions = common_find_mentions($text, $notice); + + // We need to go through in reverse order by position, + // so our positions stay valid despite our fudging with the + // string! + + $points = array(); + + foreach ($mentions as $mention) + { + $points[$mention['position']] = $mention; + } + + krsort($points); + + foreach ($points as $position => $mention) { + + $linkText = common_linkify_mention($mention); + + $text = substr_replace($text, $linkText, $position, mb_strlen($mention['text'])); + } + + return $text; +} + +function common_linkify_mention($mention) +{ + $output = null; + + if (Event::handle('StartLinkifyMention', array($mention, &$output))) { + + $xs = new XMLStringer(false); + + $attrs = array('href' => $mention['url'], + 'class' => 'url'); + + if (!empty($mention['title'])) { + $attrs['title'] = $mention['title']; + } + + $xs->elementStart('span', 'vcard'); + $xs->elementStart('a', $attrs); + $xs->element('span', 'fn nickname', $mention['text']); + $xs->elementEnd('a'); + $xs->elementEnd('span'); + + $output = $xs->getString(); + + Event::handle('EndLinkifyMention', array($mention, &$output)); + } + + return $output; +} + +function common_find_mentions($text, $notice) +{ + $mentions = array(); + + $sender = Profile::staticGet('id', $notice->profile_id); + + if (empty($sender)) { + return $mentions; + } + + if (Event::handle('StartFindMentions', array($sender, $text, &$mentions))) { + + // Get the context of the original notice, if any + + $originalAuthor = null; + $originalNotice = null; + $originalMentions = array(); + + // Is it a reply? + + if (!empty($notice) && !empty($notice->reply_to)) { + $originalNotice = Notice::staticGet('id', $notice->reply_to); + if (!empty($originalNotice)) { + $originalAuthor = Profile::staticGet('id', $originalNotice->profile_id); + + $ids = $originalNotice->getReplies(); + + foreach ($ids as $id) { + $repliedTo = Profile::staticGet('id', $id); + if (!empty($repliedTo)) { + $originalMentions[$repliedTo->nickname] = $repliedTo; + } + } + } + } + + preg_match_all('/^T ([A-Z0-9]{1,64}) /', + $text, + $tmatches, + PREG_OFFSET_CAPTURE); + + preg_match_all('/(?:^|\s+)@(['.NICKNAME_FMT.']{1,64})/', + $text, + $atmatches, + PREG_OFFSET_CAPTURE); + + $matches = array_merge($tmatches[1], $atmatches[1]); + + foreach ($matches as $match) { + + $nickname = common_canonical_nickname($match[0]); + + // Try to get a profile for this nickname. + // Start with conversation context, then go to + // sender context. + + if (!empty($originalAuthor) && $originalAuthor->nickname == $nickname) { + + $mentioned = $originalAuthor; + + } else if (!empty($originalMentions) && + array_key_exists($nickname, $originalMentions)) { + + $mentioned = $originalMentions[$nickname]; + } else { + $mentioned = common_relative_profile($sender, $nickname); + } + + if (!empty($mentioned)) { + + $user = User::staticGet('id', $mentioned->id); + + if ($user) { + $url = common_local_url('userbyid', array('id' => $user->id)); + } else { + $url = $mentioned->profileurl; + } + + $mention = array('mentioned' => array($mentioned), + 'text' => $match[0], + 'position' => $match[1], + 'url' => $url); + + if (!empty($mentioned->fullname)) { + $mention['title'] = $mentioned->fullname; + } + + $mentions[] = $mention; + } + } + + // @#tag => mention of all subscriptions tagged 'tag' + + preg_match_all('/(?:^|[\s\.\,\:\;]+)@#([\pL\pN_\-\.]{1,64})/', + $text, + $hmatches, + PREG_OFFSET_CAPTURE); + + foreach ($hmatches[1] as $hmatch) { + + $tag = common_canonical_tag($hmatch[0]); + + $tagged = Profile_tag::getTagged($sender->id, $tag); + + $url = common_local_url('subscriptions', + array('nickname' => $sender->nickname, + 'tag' => $tag)); + + $mentions[] = array('mentioned' => $tagged, + 'text' => $hmatch[0], + 'position' => $hmatch[1], + 'url' => $url); + } + + Event::handle('EndFindMentions', array($sender, $text, &$mentions)); + } + + return $mentions; +} + function common_render_text($text) { $r = htmlspecialchars($text); @@ -628,8 +802,28 @@ function common_shorten_links($text) function common_xml_safe_str($str) { - // Neutralize control codes and surrogates - return preg_replace('/[\p{Cc}\p{Cs}]/u', '*', $str); + // Replace common eol and extra whitespace input chars + $unWelcome = array( + "\t", // tab + "\n", // newline + "\r", // cr + "\0", // null byte eos + "\x0B" // vertical tab + ); + + $replacement = array( + ' ', // single space + ' ', + '', // nothing + '', + ' ' + ); + + $str = str_replace($unWelcome, $replacement, $str); + + // Neutralize any additional control codes and UTF-16 surrogates + // (Twitter uses '*') + return preg_replace('/[\p{Cc}\p{Cs}]/u', '*', $str); } function common_tag_link($tag) @@ -656,37 +850,6 @@ function common_valid_profile_tag($str) return preg_match('/^[A-Za-z0-9_\-\.]{1,64}$/', $str); } -function common_at_link($sender_id, $nickname) -{ - $sender = Profile::staticGet($sender_id); - if (!$sender) { - return $nickname; - } - $recipient = common_relative_profile($sender, common_canonical_nickname($nickname)); - if ($recipient) { - $user = User::staticGet('id', $recipient->id); - if ($user) { - $url = common_local_url('userbyid', array('id' => $user->id)); - } else { - $url = $recipient->profileurl; - } - $xs = new XMLStringer(false); - $attrs = array('href' => $url, - 'class' => 'url'); - if (!empty($recipient->fullname)) { - $attrs['title'] = $recipient->fullname . ' (' . $recipient->nickname . ')'; - } - $xs->elementStart('span', 'vcard'); - $xs->elementStart('a', $attrs); - $xs->element('span', 'fn nickname', $nickname); - $xs->elementEnd('a'); - $xs->elementEnd('span'); - return $xs->getString(); - } else { - return $nickname; - } -} - function common_group_link($sender_id, $nickname) { $sender = Profile::staticGet($sender_id); @@ -709,29 +872,6 @@ function common_group_link($sender_id, $nickname) } } -function common_at_hash_link($sender_id, $tag) -{ - $user = User::staticGet($sender_id); - if (!$user) { - return $tag; - } - $tagged = Profile_tag::getTagged($user->id, common_canonical_tag($tag)); - if ($tagged) { - $url = common_local_url('subscriptions', - array('nickname' => $user->nickname, - 'tag' => $tag)); - $xs = new XMLStringer(); - $xs->elementStart('span', 'tag'); - $xs->element('a', array('href' => $url, - 'rel' => $tag), - $tag); - $xs->elementEnd('span'); - return $xs->getString(); - } else { - return $tag; - } -} - function common_relative_profile($sender, $nickname, $dt=null) { // Try to find profiles this profile is subscribed to that have this nickname @@ -768,7 +908,7 @@ function common_relative_profile($sender, $nickname, $dt=null) return null; } -function common_local_url($action, $args=null, $params=null, $fragment=null) +function common_local_url($action, $args=null, $params=null, $fragment=null, $addSession=true) { $r = Router::get(); $path = $r->build($action, $args, $params, $fragment); @@ -776,12 +916,12 @@ function common_local_url($action, $args=null, $params=null, $fragment=null) $ssl = common_is_sensitive($action); if (common_config('site','fancy')) { - $url = common_path(mb_substr($path, 1), $ssl); + $url = common_path(mb_substr($path, 1), $ssl, $addSession); } else { if (mb_strpos($path, '/index.php') === 0) { - $url = common_path(mb_substr($path, 1), $ssl); + $url = common_path(mb_substr($path, 1), $ssl, $addSession); } else { - $url = common_path('index.php'.$path, $ssl); + $url = common_path('index.php'.$path, $ssl, $addSession); } } return $url; @@ -800,7 +940,7 @@ function common_is_sensitive($action) return $ssl; } -function common_path($relative, $ssl=false) +function common_path($relative, $ssl=false, $addSession=true) { $pathpart = (common_config('site', 'path')) ? common_config('site', 'path')."/" : ''; @@ -824,7 +964,9 @@ function common_path($relative, $ssl=false) } } - $relative = common_inject_session($relative, $serverpart); + if ($addSession) { + $relative = common_inject_session($relative, $serverpart); + } return $proto.'://'.$serverpart.'/'.$pathpart.$relative; } @@ -1031,25 +1173,30 @@ function common_enqueue_notice($notice) return true; } -function common_broadcast_profile($profile) +/** + * Broadcast profile updates to OMB and other remote subscribers. + * + * Since this may be slow with a lot of subscribers or bad remote sites, + * this is run through the background queues if possible. + */ +function common_broadcast_profile(Profile $profile) { - // XXX: optionally use a queue system like http://code.google.com/p/microapps/wiki/NQDQ - require_once(INSTALLDIR.'/lib/omb.php'); - omb_broadcast_profile($profile); - // XXX: Other broadcasts...? + $qm = QueueManager::get(); + $qm->enqueue($profile, "profile"); return true; } function common_profile_url($nickname) { - return common_local_url('showstream', array('nickname' => $nickname)); + return common_local_url('showstream', array('nickname' => $nickname), + null, null, false); } // Should make up a reasonable root URL function common_root_url($ssl=false) { - $url = common_path('', $ssl); + $url = common_path('', $ssl, false); $i = strpos($url, '?'); if ($i !== false) { $url = substr($url, 0, $i); @@ -1334,7 +1481,8 @@ function common_remove_magic_from_request() function common_user_uri(&$user) { - return common_local_url('userbyid', array('id' => $user->id)); + return common_local_url('userbyid', array('id' => $user->id), + null, null, false); } function common_notice_uri(&$notice) @@ -1514,6 +1662,7 @@ function common_database_tablename($tablename) */ function common_shorten_url($long_url) { + $long_url = trim($long_url); $user = common_current_user(); if (empty($user)) { // common current user does not find a user when called from the XMPP daemon @@ -1528,7 +1677,7 @@ function common_shorten_url($long_url) return $long_url; }else{ //URL was shortened, so return the result - return $shortenedUrl; + return trim($shortenedUrl); } } @@ -1605,7 +1754,8 @@ function common_url_to_nickname($url) # Strip starting, ending slashes $path = preg_replace('@/$@', '', $parts['path']); $path = preg_replace('@^/@', '', $path); - if (strpos($path, '/') === false) { + $path = basename($path); + if ($path) { return common_nicknamize($path); } } |