diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/action.php | 5 | ||||
-rw-r--r-- | lib/activity.php | 993 | ||||
-rw-r--r-- | lib/api.php | 4 | ||||
-rw-r--r-- | lib/atom10entry.php | 5 | ||||
-rw-r--r-- | lib/atom10feed.php | 15 | ||||
-rw-r--r-- | lib/atomnoticefeed.php | 13 | ||||
-rw-r--r-- | lib/atomusernoticefeed.php | 5 | ||||
-rw-r--r-- | lib/command.php | 58 | ||||
-rw-r--r-- | lib/commandinterpreter.php | 11 | ||||
-rw-r--r-- | lib/common.php | 1 | ||||
-rw-r--r-- | lib/default.php | 14 | ||||
-rw-r--r-- | lib/distribqueuehandler.php | 16 | ||||
-rw-r--r-- | lib/htmloutputter.php | 57 | ||||
-rw-r--r-- | lib/iomaster.php | 23 | ||||
-rw-r--r-- | lib/noticelist.php | 51 | ||||
-rw-r--r-- | lib/omb.php | 8 | ||||
-rw-r--r-- | lib/profilequeuehandler.php | 48 | ||||
-rw-r--r-- | lib/queuemanager.php | 3 | ||||
-rw-r--r-- | lib/stompqueuemanager.php | 119 | ||||
-rw-r--r-- | lib/subs.php | 141 | ||||
-rw-r--r-- | lib/taguri.php | 96 | ||||
-rw-r--r-- | lib/util.php | 209 |
22 files changed, 1547 insertions, 348 deletions
diff --git a/lib/action.php b/lib/action.php index b85f353a3..fa9ddb911 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)); } diff --git a/lib/activity.php b/lib/activity.php new file mode 100644 index 000000000..25727bf2b --- /dev/null +++ b/lib/activity.php @@ -0,0 +1,993 @@ +<?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 + ); + + array_push($urls, new PoCoURL($type, $value, $primary)); + } + 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 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; + } + + /** + * 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.")); + } + } + } +} + +/** + * 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 $avatar; + 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; + + 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; + + // @fixme we may have multiple avatars with different resolutions specified + $this->avatar = ActivityUtils::getLink($element, 'avatar'); + + $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; + $object->avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); + + if (isset($profile->lat) && isset($profile->lon)) { + $object->geopoint = (float)$profile->lat . ' ' . (float)$profile->lon; + } + + $object->poco = PoCo::fromProfile($profile); + + 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) { + $xs->element( + 'link', array( + 'type' => empty($this->avatar) ? 'image/png' : $this->avatar->mediatype, + 'rel' => 'avatar', + 'href' => empty($this->avatar) + ? Avatar::defaultImage(AVATAR_PROFILE_SIZE) + : $this->avatar->displayUrl() + ), + 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; + $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"); + 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 + + /** + * 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); + } + + /** + * 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'); + } 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')); + } + + $xs->elementEnd('entry'); + + return $xs->getString(); + } + + private function _child($element, $tag, $namespace=self::SPEC) + { + return ActivityUtils::child($element, $tag, $namespace); + } +}
\ No newline at end of file diff --git a/lib/api.php b/lib/api.php index 9000fb4ba..26977c90f 100644 --- a/lib/api.php +++ b/lib/api.php @@ -359,7 +359,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 +803,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']; 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..d2bf2a416 100644 --- a/lib/atomnoticefeed.php +++ b/lib/atomnoticefeed.php @@ -50,23 +50,28 @@ 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( + '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/command.php b/lib/command.php index 44b7b2274..5be9cd6e8 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..68723955e 100644 --- a/lib/common.php +++ b/lib/common.php @@ -123,6 +123,7 @@ 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'; diff --git a/lib/default.php b/lib/default.php index a9be3438b..70e00ea75 100644 --- a/lib/default.php +++ b/lib/default.php @@ -91,10 +91,13 @@ $default = 'spawndelay' => 1, // Wait at least N seconds between (re)spawns of child processes to avoid slamming the queue server with subscription startup 'debug_memory' => false, // true to spit memory usage to log 'inboxes' => true, // true to do inbox distribution & output queueing from in background via 'distrib' queue - 'breakout' => array('*' => 'shared'), // set global or per-handler queue breakout - // 'shared': use a shared queue for all sites - // 'handler': share each/this handler over multiple sites - // 'site': break out for each/this handler on this site + 'breakout' => array(), // List queue specifiers to break out when using Stomp queue. + // Default will share all queues for all sites within each group. + // Specify as <group>/<queue> or <group>/<queue>/<site>, + // using nickname identifier as site. + // + // 'main/distrib' separate "distrib" queue covering all sites + // 'xmpp/xmppout/mysite' separate "xmppout" queue covering just 'mysite' 'max_retries' => 10, // drop messages after N failed attempts to process (Stomp) 'dead_letter_dir' => false, // set to directory to save dropped messages into (Stomp) ), @@ -172,7 +175,7 @@ $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, @@ -277,7 +280,6 @@ $default = 'Mapstraction' => null, 'Linkback' => null, 'WikiHashtags' => null, - 'PubSubHubBub' => null, 'RSSCloud' => null, 'OpenID' => null), ), diff --git a/lib/distribqueuehandler.php b/lib/distribqueuehandler.php index 4477468d0..dc183fb36 100644 --- a/lib/distribqueuehandler.php +++ b/lib/distribqueuehandler.php @@ -69,19 +69,7 @@ class DistribQueueHandler } 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); + $notice->addToInboxes(); } catch (Exception $e) { $this->logit($notice, $e); } @@ -107,7 +95,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/htmloutputter.php b/lib/htmloutputter.php index 47e56fc8f..7786b5941 100644 --- a/lib/htmloutputter.php +++ b/lib/htmloutputter.php @@ -356,40 +356,47 @@ class HTMLOutputter extends XMLOutputter if( empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment'])) { - $path = common_config('javascript', 'path'); + if (strpos($src, 'plugins/') === 0 || strpos($src, 'local/') === 0) { - if (empty($path)) { - $path = common_config('site', 'path') . '/js/'; - } + $src = common_path($src) . '?version=' . STATUSNET_VERSION; - if ($path[strlen($path)-1] != '/') { - $path .= '/'; - } + }else{ - if ($path[0] != '/') { - $path = '/'.$path; - } + $path = common_config('javascript', 'path'); - $server = common_config('javascript', 'server'); + if (empty($path)) { + $path = common_config('site', 'path') . '/js/'; + } - if (empty($server)) { - $server = common_config('site', 'server'); - } + if ($path[strlen($path)-1] != '/') { + $path .= '/'; + } - $ssl = common_config('javascript', 'ssl'); + if ($path[0] != '/') { + $path = '/'.$path; + } - if (is_null($ssl)) { // null -> guess - if (common_config('site', 'ssl') == 'always' && - !common_config('javascript', 'server')) { - $ssl = true; - } else { - $ssl = false; + $server = common_config('javascript', 'server'); + + if (empty($server)) { + $server = common_config('site', 'server'); } - } - $protocol = ($ssl) ? 'https' : 'http'; + $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; + } + } - $src = $protocol.'://'.$server.$path.$src . '?version=' . STATUSNET_VERSION; + $protocol = ($ssl) ? 'https' : 'http'; + + $src = $protocol.'://'.$server.$path.$src . '?version=' . STATUSNET_VERSION; + } } $this->element('script', array('type' => $type, @@ -439,7 +446,7 @@ class HTMLOutputter extends XMLOutputter { if(Event::handle('StartCssLinkElement', array($this,&$src,&$theme,&$media))) { $url = parse_url($src); - if( empty($url->scheme) && empty($url->host) && empty($url->query) && empty($url->fragment)) + if( empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment'])) { if(file_exists(Theme::file($src,$theme))){ $src = Theme::path($src, $theme); diff --git a/lib/iomaster.php b/lib/iomaster.php index 3745a5c7a..7cfb2c9a0 100644 --- a/lib/iomaster.php +++ b/lib/iomaster.php @@ -55,27 +55,18 @@ abstract class IoMaster if ($multiSite !== null) { $this->multiSite = $multiSite; } - if ($this->multiSite) { - $this->sites = StatusNet::findAllSites(); - } else { - $this->sites = array(StatusNet::currentSite()); - } - - if (empty($this->sites)) { - throw new Exception("Empty status_network table, cannot init"); - } - foreach ($this->sites as $site) { - StatusNet::switchSite($site); - $this->initManagers(); - } + $this->initManagers(); } /** - * Initialize IoManagers for the currently configured site - * which are appropriate to this instance. + * Initialize IoManagers which are appropriate to this instance; + * pass class names or instances into $this->instantiate(). + * + * If setup and configuration may vary between sites in multi-site + * mode, it's the subclass's responsibility to set them up here. * - * Pass class names into $this->instantiate() + * Switching site configurations is an acceptable side effect. */ abstract function initManagers(); diff --git a/lib/noticelist.php b/lib/noticelist.php index c05b99024..28a563d87 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'); } @@ -492,30 +493,34 @@ class NoticeListItem extends Widget break; default: - $name = null; + $name = $source_name; $url = null; - $ns = Notice_source::staticGet($this->notice->source); - - if ($ns) { - $name = $ns->name; - $url = $ns->url; - } else { - $app = Oauth_application::staticGet('name', $this->notice->source); - if ($app) { - $name = $app->name; - $url = $app->source_url; + if (Event::handle('StartNoticeSourceLink', array($this->notice, &$name, &$url, &$title))) { + $ns = Notice_source::staticGet($this->notice->source); + + if ($ns) { + $name = $ns->name; + $url = $ns->url; + } else { + $app = Oauth_application::staticGet('name', $this->notice->source); + if ($app) { + $name = $app->name; + $url = $app->source_url; + } } } + Event::handle('EndNoticeSourceLink', array($this->notice, &$name, &$url, &$title)); if (!empty($name) && !empty($url)) { $this->out->elementStart('span', 'device'); $this->out->element('a', array('href' => $url, - 'rel' => 'external'), + 'rel' => 'external', + 'title' => $title), $name); $this->out->elementEnd('span'); } else { - $this->out->element('span', 'device', $source_name); + $this->out->element('span', 'device', $name); } break; } diff --git a/lib/omb.php b/lib/omb.php index 0f38a4936..17132a594 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() diff --git a/lib/profilequeuehandler.php b/lib/profilequeuehandler.php new file mode 100644 index 000000000..e8a00aef3 --- /dev/null +++ b/lib/profilequeuehandler.php @@ -0,0 +1,48 @@ +<?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'); + omb_broadcast_profile($profile); + } + Event::handle('EndBroadcastProfile', array($profile)); + return true; + } + +} diff --git a/lib/queuemanager.php b/lib/queuemanager.php index 576c7c0af..fe45e8bbf 100644 --- a/lib/queuemanager.php +++ b/lib/queuemanager.php @@ -239,6 +239,9 @@ abstract class QueueManager extends IoManager $this->connect('sms', 'SmsQueueHandler'); } + // Broadcasting profile updates to OMB remote subscribers + $this->connect('profile', 'ProfileQueueHandler'); + // For compat with old plugins not registering their own handlers. $this->connect('plugin', 'PluginQueueHandler'); } diff --git a/lib/stompqueuemanager.php b/lib/stompqueuemanager.php index bfeeb23b7..9af8b2f48 100644 --- a/lib/stompqueuemanager.php +++ b/lib/stompqueuemanager.php @@ -63,7 +63,7 @@ class StompQueueManager extends QueueManager $this->password = common_config('queue', 'stomp_password'); $this->base = common_config('queue', 'queue_basename'); $this->control = common_config('queue', 'control_channel'); - $this->subscriptions = array($this->control => $this->control); + $this->breakout = common_config('queue', 'breakout'); } /** @@ -76,28 +76,6 @@ class StompQueueManager extends QueueManager } /** - * Record queue subscriptions we'll need to handle the current site. - */ - public function addSite() - { - $this->sites[] = StatusNet::currentSite(); - - // Set up handlers active for this site... - $this->initialize(); - - foreach ($this->activeGroups as $group) { - if (isset($this->groups[$group])) { - // Actual queues may be broken out or consolidated... - // Subscribe to all the target queues we'll need. - foreach ($this->groups[$group] as $transport => $class) { - $target = $this->queueName($transport); - $this->subscriptions[$target] = $target; - } - } - } - } - - /** * Optional; ping any running queue handler daemons with a notification * such as announcing a new site to handle or requesting clean shutdown. * This avoids having to restart all the daemons manually to update configs @@ -166,14 +144,15 @@ class StompQueueManager extends QueueManager $con = $this->cons[$idx]; $host = $con->getServer(); - $result = $con->send($this->queueName($queue), $msg, $props); + $target = $this->queueName($queue); + $result = $con->send($target, $msg, $props); if (!$result) { - $this->_log(LOG_ERR, "Error sending $rep to $queue queue on $host"); + $this->_log(LOG_ERR, "Error sending $rep to $queue queue on $host $target"); return false; } - $this->_log(LOG_DEBUG, "complete remote queueing $rep for $queue on $host"); + $this->_log(LOG_DEBUG, "complete remote queueing $rep for $queue on $host $target"); $this->stats('enqueued', $queue); return true; } @@ -432,11 +411,42 @@ class StompQueueManager extends QueueManager protected function doSubscribe(LiberalStomp $con) { $host = $con->getServer(); - foreach ($this->subscriptions as $queue) { - $this->_log(LOG_INFO, "Subscribing to $queue on $host"); - $con->subscribe($queue); + foreach ($this->subscriptions() as $sub) { + $this->_log(LOG_INFO, "Subscribing to $sub on $host"); + $con->subscribe($sub); } } + + /** + * Grab a full list of stomp-side queue subscriptions. + * Will include: + * - control broadcast channel + * - shared group queues for active groups + * - per-handler and per-site breakouts from $config['queue']['breakout'] + * that are rooted in the active groups. + * + * @return array of strings + */ + protected function subscriptions() + { + $subs = array(); + $subs[] = $this->control; + + foreach ($this->activeGroups as $group) { + $subs[] = $this->base . $group; + } + + foreach ($this->breakout as $spec) { + $parts = explode('/', $spec); + if (count($parts) < 2 || count($parts) > 3) { + common_log(LOG_ERR, "Bad queue breakout specifier $spec"); + } + if (in_array($parts[0], $this->activeGroups)) { + $subs[] = $this->base . $spec; + } + } + return array_unique($subs); + } /** * Handle and acknowledge an event that's come in through a queue. @@ -612,32 +622,26 @@ class StompQueueManager extends QueueManager } /** - * Set us up with queue subscriptions for a new site added at runtime, + * (Re)load runtime configuration for a given site by nickname, * triggered by a broadcast to the 'statusnet-control' topic. * + * Configuration changes in database should update, but config + * files might not. + * * @param array $frame Stomp frame * @return bool true to continue; false to stop further processing. */ protected function updateSiteConfig($nickname) { - if (empty($this->sites)) { - if ($nickname == common_config('site', 'nickname')) { - StatusNet::init(common_config('site', 'server')); - } else { - $this->_log(LOG_INFO, "Ignoring update ping for other site $nickname"); + $sn = Status_network::staticGet($nickname); + if ($sn) { + $this->switchSite($nickname); + if (!in_array($nickname, $this->sites)) { + $this->addSite(); } + $this->stats('siteupdate'); } else { - $sn = Status_network::staticGet($nickname); - if ($sn) { - $this->switchSite($nickname); - if (!in_array($nickname, $this->sites)) { - $this->addSite(); - } - // @fixme update subscriptions, if applicable - $this->stats('siteupdate'); - } else { - $this->_log(LOG_ERR, "Ignoring ping for unrecognized new site $nickname"); - } + $this->_log(LOG_ERR, "Ignoring ping for unrecognized new site $nickname"); } } @@ -646,24 +650,25 @@ class StompQueueManager extends QueueManager * group name for this queue to give eg: * * /queue/statusnet/main + * /queue/statusnet/main/distrib + * /queue/statusnet/xmpp/xmppout/site01 * * @param string $queue * @return string */ protected function queueName($queue) { - $base = common_config('queue', 'queue_basename'); $group = $this->queueGroup($queue); - $breakout = $this->breakoutMode($queue); - if ($breakout == 'shared') { - return $base . "$group"; - } else if ($breakout == 'handler') { - return $base . "$group/$queue"; - } else if ($breakout == 'site') { - $site = StatusNet::currentSite(); - return $base . "$group/$queue/$site"; - } - throw Exception("Unrecognized queue breakout mode '$breakout' for '$queue'"); + $site = StatusNet::currentSite(); + + $specs = array("$group/$queue/$site", + "$group/$queue"); + foreach ($specs as $spec) { + if (in_array($spec, $this->breakout)) { + return $this->base . $spec; + } + } + return $this->base . $group; } /** 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 5bfeebdc3..82dec0f0c 100644 --- a/lib/util.php +++ b/lib/util.php @@ -426,13 +426,148 @@ 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($id, $r); $r = preg_replace('/(^|[\s\.\,\:\;]+)!([A-Za-z0-9]{1,64})/e', "'\\1!'.common_group_link($id, '\\2')", $r); return $r; } +function common_linkify_mentions($profile_id, $text) +{ + $mentions = common_find_mentions($profile_id, $text); + + // 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($profile_id, $text) +{ + $mentions = array(); + + $sender = Profile::staticGet('id', $profile_id); + + if (empty($sender)) { + return $mentions; + } + + if (Event::handle('StartFindMentions', array($sender, $text, &$mentions))) { + + 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]); + $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); @@ -656,37 +791,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 +813,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 @@ -1022,12 +1103,16 @@ 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; } |