diff options
Diffstat (limited to 'lib')
38 files changed, 2656 insertions, 1349 deletions
diff --git a/lib/action.php b/lib/action.php index 10394c789..491d7d481 100644 --- a/lib/action.php +++ b/lib/action.php @@ -798,11 +798,14 @@ class Action extends HTMLOutputter // lawsuit { $this->element('dt', array('id' => 'site_statusnet_license'), _('StatusNet software license')); $this->elementStart('dd', null); + // @fixme drop the final spaces in the messages when at good spot + // to let translations get updated. if (common_config('site', 'broughtby')) { $instr = _('**%%site.name%%** is a microblogging service brought to you by [%%site.broughtby%%](%%site.broughtbyurl%%). '); } else { $instr = _('**%%site.name%%** is a microblogging service. '); } + $instr .= ' '; $instr .= sprintf(_('It runs the [StatusNet](http://status.net/) microblogging software, version %s, available under the [GNU Affero General Public License](http://www.fsf.org/licensing/licenses/agpl-3.0.html).'), STATUSNET_VERSION); $output = common_markup_to_html($instr); $this->raw($output); diff --git a/lib/activity.php b/lib/activity.php index 2cb80f9e1..bd1d5d56c 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -32,971 +32,6 @@ 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) - { - $els = $element->childNodes; - - foreach ($els as $link) { - if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) { - - $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) - { - $els = $element->childNodes; - $out = array(); - - foreach ($els as $link) { - if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) { - - $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 * @@ -1033,6 +68,21 @@ class Activity const PUBLISHED = 'published'; const UPDATED = 'updated'; + const RSS = null; // no namespace! + + const PUBDATE = 'pubDate'; + const DESCRIPTION = 'description'; + const GUID = 'guid'; + const SELF = 'self'; + const IMAGE = 'image'; + const URL = 'url'; + + const DC = 'http://purl.org/dc/elements/1.1/'; + + const CREATOR = 'creator'; + + const CONTENTNS = 'http://purl.org/rss/1.0/modules/content/'; + public $actor; // an ActivityObject public $verb; // a string (the URL) public $object; // an ActivityObject @@ -1063,21 +113,29 @@ class Activity return; } - $this->entry = $entry; - - // @fixme Don't send in a DOMDocument + // Insist on a feed's root DOMElement; don't allow a DOMDocument if ($feed instanceof DOMDocument) { - common_log( - LOG_WARNING, - 'Activity::__construct() - ' - . 'DOMDocument passed in for feed by mistake. ' - . "Expecting a 'feed' DOMElement." + throw new ClientException( + _("Expecting a root feed element but got a whole XML document.") ); - $feed = $feed->getElementsByTagName('feed')->item(0); } + $this->entry = $entry; $this->feed = $feed; + if ($entry->namespaceURI == Activity::ATOM && + $entry->localName == 'entry') { + $this->_fromAtomEntry($entry, $feed); + } else if ($entry->namespaceURI == Activity::RSS && + $entry->localName == 'item') { + $this->_fromRssItem($entry, $feed); + } else { + throw new Exception("Unknown DOM element: {$entry->namespaceURI} {$entry->localName}"); + } + } + + function _fromAtomEntry($entry, $feed) + { $pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM); if (!empty($pubEl)) { @@ -1163,6 +221,69 @@ class Activity } } + function _fromRssItem($item, $channel) + { + $verbEl = $this->_child($item, self::VERB); + + if (!empty($verbEl)) { + $this->verb = trim($verbEl->textContent); + } else { + $this->verb = ActivityVerb::POST; + // XXX: do other implied stuff here + } + + $pubDateEl = $this->_child($item, self::PUBDATE, self::RSS); + + if (!empty($pubDateEl)) { + $this->time = strtotime($pubDateEl->textContent); + } + + if ($authorEl = $this->_child($item, self::AUTHOR, self::RSS)) { + $this->actor = ActivityObject::fromRssAuthor($authorEl); + } else if ($dcCreatorEl = $this->_child($item, self::CREATOR, self::DC)) { + $this->actor = ActivityObject::fromDcCreator($dcCreatorEl); + } else if ($posterousEl = $this->_child($item, ActivityObject::AUTHOR, ActivityObject::POSTEROUS)) { + // Special case for Posterous.com + $this->actor = ActivityObject::fromPosterousAuthor($posterousEl); + } else if (!empty($channel)) { + $this->actor = ActivityObject::fromRssChannel($channel); + } else { + // No actor! + } + + $this->title = ActivityUtils::childContent($item, ActivityObject::TITLE, self::RSS); + + $contentEl = ActivityUtils::child($item, ActivityUtils::CONTENT, self::CONTENTNS); + + if (!empty($contentEl)) { + $this->content = htmlspecialchars_decode($contentEl->textContent, ENT_QUOTES); + } else { + $descriptionEl = ActivityUtils::child($item, self::DESCRIPTION, self::RSS); + if (!empty($descriptionEl)) { + $this->content = htmlspecialchars_decode($descriptionEl->textContent, ENT_QUOTES); + } + } + + $this->link = ActivityUtils::childContent($item, ActivityUtils::LINK, self::RSS); + + // @fixme enclosures + // @fixme thumbnails... maybe + + $guidEl = ActivityUtils::child($item, self::GUID, self::RSS); + + if (!empty($guidEl)) { + $this->id = $guidEl->textContent; + + if ($guidEl->hasAttribute('isPermaLink') && $guidEl->getAttribute('isPermaLink') != 'false') { + // overwrites <link> + $this->link = $this->id; + } + } + + $this->object = new ActivityObject($item); + $this->context = new ActivityContext($item); + } + /** * Returns an Atom <entry> based on this activity * @@ -1241,48 +362,3 @@ class Activity } } -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/activitycontext.php b/lib/activitycontext.php new file mode 100644 index 000000000..2df7613f7 --- /dev/null +++ b/lib/activitycontext.php @@ -0,0 +1,121 @@ +<?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 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; + } +} diff --git a/lib/activityobject.php b/lib/activityobject.php new file mode 100644 index 000000000..0a358ccab --- /dev/null +++ b/lib/activityobject.php @@ -0,0 +1,548 @@ +<?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); +} + +/** + * 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'; + + const POSTEROUS = 'http://posterous.com/help/rss/1.0'; + const AUTHOR = 'author'; + const USERIMAGE = 'userImage'; + const PROFILEURL = 'profileUrl'; + const NICKNAME = 'nickName'; + const DISPLAYNAME = 'displayName'; + + 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->_fromAuthor($element); + } else if ($element->tagName == 'item') { + $this->_fromRssItem($element); + } else { + $this->_fromAtomEntry($element); + } + + // Some per-type attributes... + if ($this->type == self::PERSON || $this->type == self::GROUP) { + $this->displayName = $this->title; + + $photos = ActivityUtils::getLinks($element, 'photo'); + if (count($photos)) { + foreach ($photos as $link) { + $this->avatarLinks[] = new AvatarLink($link); + } + } else { + $avatars = ActivityUtils::getLinks($element, 'avatar'); + foreach ($avatars as $link) { + $this->avatarLinks[] = new AvatarLink($link); + } + } + + $this->poco = new PoCo($element); + } + } + + private function _fromAuthor($element) + { + $this->type = self::PERSON; // XXX: is this fair? + $this->title = $this->_childContent($element, self::NAME); + + $id = $this->_childContent($element, self::URI); + if (ActivityUtils::validateUri($id)) { + $this->id = $id; + } + + if (empty($this->id)) { + $email = $this->_childContent($element, self::EMAIL); + if (!empty($email)) { + // XXX: acct: ? + $this->id = 'mailto:'.$email; + } + } + } + + private function _fromAtomEntry($element) + { + if ($element->localName == 'actor') { + // Old-fashioned <activity:actor>... + // First pull anything from <author>, then we'll add on top. + $author = ActivityUtils::child($element->parentNode, 'author'); + if ($author) { + $this->_fromAuthor($author); + } + } + + $this->type = $this->_childContent($element, Activity::OBJECTTYPE, + Activity::SPEC); + + if (empty($this->type)) { + $this->type = ActivityObject::NOTE; + } + + $id = $this->_childContent($element, self::ID); + if (ActivityUtils::validateUri($id)) { + $this->id = $id; + } + + $this->summary = ActivityUtils::childHtmlContent($element, self::SUMMARY); + $this->content = ActivityUtils::getContent($element); + + // We don't like HTML in our titles, although it's technically allowed + + $title = ActivityUtils::childHtmlContent($element, self::TITLE); + + $this->title = html_entity_decode(strip_tags($title)); + + $this->source = $this->_getSource($element); + + $this->link = ActivityUtils::getPermalink($element); + } + + // @fixme rationalize with Activity::_fromRssItem() + + private function _fromRssItem($item) + { + $this->title = ActivityUtils::childContent($item, ActivityObject::TITLE, Activity::RSS); + + $contentEl = ActivityUtils::child($item, ActivityUtils::CONTENT, Activity::CONTENTNS); + + if (!empty($contentEl)) { + $this->content = htmlspecialchars_decode($contentEl->textContent, ENT_QUOTES); + } else { + $descriptionEl = ActivityUtils::child($item, Activity::DESCRIPTION, Activity::RSS); + if (!empty($descriptionEl)) { + $this->content = htmlspecialchars_decode($descriptionEl->textContent, ENT_QUOTES); + } + } + + $this->link = ActivityUtils::childContent($item, ActivityUtils::LINK, Activity::RSS); + + $guidEl = ActivityUtils::child($item, Activity::GUID, Activity::RSS); + + if (!empty($guidEl)) { + $this->id = $guidEl->textContent; + + if ($guidEl->hasAttribute('isPermaLink')) { + // overwrites <link> + $this->link = $this->id; + } + } + } + + public static function fromRssAuthor($el) + { + $text = $el->textContent; + + if (preg_match('/^(.*?) \((.*)\)$/', $text, $match)) { + $email = $match[1]; + $name = $match[2]; + } else if (preg_match('/^(.*?) <(.*)>$/', $text, $match)) { + $name = $match[1]; + $email = $match[2]; + } else if (preg_match('/.*@.*/', $text)) { + $email = $text; + $name = null; + } else { + $name = $text; + $email = null; + } + + // Not really enough info + + $obj = new ActivityObject(); + + $obj->element = $el; + + $obj->type = ActivityObject::PERSON; + $obj->title = $name; + + if (!empty($email)) { + $obj->id = 'mailto:'.$email; + } + + return $obj; + } + + public static function fromDcCreator($el) + { + // Not really enough info + + $text = $el->textContent; + + $obj = new ActivityObject(); + + $obj->element = $el; + + $obj->title = $text; + $obj->type = ActivityObject::PERSON; + + return $obj; + } + + public static function fromRssChannel($el) + { + $obj = new ActivityObject(); + + $obj->element = $el; + + $obj->type = ActivityObject::PERSON; // @fixme guess better + + $obj->title = ActivityUtils::childContent($el, ActivityObject::TITLE, Activity::RSS); + $obj->link = ActivityUtils::childContent($el, ActivityUtils::LINK, Activity::RSS); + $obj->id = ActivityUtils::getLink($el, Activity::SELF); + + if (empty($obj->id)) { + $obj->id = $obj->link; + } + + $desc = ActivityUtils::childContent($el, Activity::DESCRIPTION, Activity::RSS); + + if (!empty($desc)) { + $obj->content = htmlspecialchars_decode($desc, ENT_QUOTES); + } + + $imageEl = ActivityUtils::child($el, Activity::IMAGE, Activity::RSS); + + if (!empty($imageEl)) { + $url = ActivityUtils::childContent($imageEl, Activity::URL, Activity::RSS); + $al = new AvatarLink(); + $al->url = $url; + $obj->avatarLinks[] = $al; + } + + return $obj; + } + + public static function fromPosterousAuthor($el) + { + $obj = new ActivityObject(); + + $obj->type = ActivityObject::PERSON; // @fixme any others...? + + $userImage = ActivityUtils::childContent($el, self::USERIMAGE, self::POSTEROUS); + + if (!empty($userImage)) { + $al = new AvatarLink(); + $al->url = $userImage; + $obj->avatarLinks[] = $al; + } + + $obj->link = ActivityUtils::childContent($el, self::PROFILEURL, self::POSTEROUS); + $obj->id = $obj->link; + + $obj->poco = new PoCo(); + + $obj->poco->preferredUsername = ActivityUtils::childContent($el, self::NICKNAME, self::POSTEROUS); + $obj->poco->displayName = ActivityUtils::childContent($el, self::DISPLAYNAME, self::POSTEROUS); + + $obj->title = $obj->poco->displayName; + + return $obj; + } + + 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 $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 $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, + common_xml_safe_str($this->title) + ); + } + + if (!empty($this->summary)) { + $xs->element( + self::SUMMARY, + null, + common_xml_safe_str($this->summary) + ); + } + + if (!empty($this->content)) { + // XXX: assuming HTML content here + $xs->element( + ActivityUtils::CONTENT, + array('type' => 'html'), + common_xml_safe_str($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(); + } +} diff --git a/lib/activityutils.php b/lib/activityutils.php new file mode 100644 index 000000000..a7e99fb11 --- /dev/null +++ b/lib/activityutils.php @@ -0,0 +1,265 @@ +<?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); +} + +/** + * 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) + { + $els = $element->childNodes; + + foreach ($els as $link) { + + if (!($link instanceof DOMElement)) { + continue; + } + + if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) { + + $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) + { + $els = $element->childNodes; + $out = array(); + + foreach ($els as $link) { + if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) { + + $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; + } + } + + static function childHtmlContent(DOMNode $element, $tag, $namespace=self::ATOM) + { + $el = self::child($element, $tag, $namespace); + + if (empty($el)) { + return null; + } else { + return self::textConstruct($el); + } + } + + /** + * 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) + { + return self::childHtmlContent($element, self::CONTENT, self::ATOM); + } + + static function textConstruct($el) + { + $src = $el->getAttribute(self::SRC); + + if (!empty($src)) { + throw new ClientException(_("Can't handle remote content yet.")); + } + + $type = $el->getAttribute(self::TYPE); + + // slavishly following http://atompub.org/rfc4287.html#rfc.section.4.1.3.3 + + if (empty($type) || $type == 'text') { + return $el->textContent; + } else if ($type == 'html') { + $text = $el->textContent; + return htmlspecialchars_decode($text, ENT_QUOTES); + } else if ($type == 'xhtml') { + $divEl = ActivityUtils::child($el, 'div', 'http://www.w3.org/1999/xhtml'); + 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($type, array('text/xml', 'application/xml')) || + preg_match('#(+|/)xml$#', $type)) { + throw new ClientException(_("Can't handle embedded XML content yet.")); + } else if (strncasecmp($type, 'text/', 5)) { + return $el->textContent; + } else { + throw new ClientException(_("Can't handle embedded Base64 content yet.")); + } + } + + /** + * Is this a valid URI for remote profile/notice identification? + * Does not have to be a resolvable URL. + * @param string $uri + * @return boolean + */ + static function validateUri($uri) + { + if (Validate::uri($uri)) { + return true; + } + + // Possibly an upstream bug; tag: URIs aren't validated properly + // unless you explicitly ask for them. All other schemes are accepted + // for basic URI validation without asking. + if (Validate::uri($uri, array('allowed_scheme' => array('tag')))) { + return true; + } + + return false; + } +} diff --git a/lib/activityverb.php b/lib/activityverb.php new file mode 100644 index 000000000..76f2b84e9 --- /dev/null +++ b/lib/activityverb.php @@ -0,0 +1,66 @@ +<?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); +} + +/** + * 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'; +} diff --git a/lib/apiaction.php b/lib/apiaction.php index e4a1df3d1..e6aaf9316 100644 --- a/lib/apiaction.php +++ b/lib/apiaction.php @@ -491,7 +491,7 @@ class ApiAction extends Action $this->showXmlAttachments($twitter_status['attachments']); break; case 'geo': - $this->showGeoRSS($value); + $this->showGeoXML($value); break; case 'retweeted_status': $this->showTwitterXmlStatus($value, 'retweeted_status'); @@ -539,7 +539,7 @@ class ApiAction extends Action } } - function showGeoRSS($geo) + function showGeoXML($geo) { if (empty($geo)) { // empty geo element @@ -551,6 +551,17 @@ class ApiAction extends Action } } + function showGeoRSS($geo) + { + if (!empty($geo)) { + $this->element( + 'georss:point', + null, + $geo['coordinates'][0] . ' ' . $geo['coordinates'][1] + ); + } + } + function showTwitterRssItem($entry) { $this->elementStart('item'); @@ -619,13 +630,25 @@ class ApiAction extends Action $this->endDocument('xml'); } - function showRssTimeline($notice, $title, $link, $subtitle, $suplink=null, $logo=null) + function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null) { $this->initDocument('rss'); $this->element('title', null, $title); $this->element('link', null, $link); + + if (!is_null($self)) { + $this->element( + 'atom:link', + array( + 'type' => 'application/rss+xml', + 'href' => $self, + 'rel' => 'self' + ) + ); + } + if (!is_null($suplink)) { // For FriendFeed's SUP protocol $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom', @@ -732,8 +755,12 @@ class ApiAction extends Action function showTwitterAtomEntry($entry) { $this->elementStart('entry'); - $this->element('title', null, $entry['title']); - $this->element('content', array('type' => 'html'), $entry['content']); + $this->element('title', null, common_xml_safe_str($entry['title'])); + $this->element( + 'content', + array('type' => 'html'), + common_xml_safe_str($entry['content']) + ); $this->element('id', null, $entry['id']); $this->element('published', null, $entry['published']); $this->element('updated', null, $entry['updated']); @@ -848,7 +875,7 @@ class ApiAction extends Action $this->initDocument('atom'); - $this->element('title', null, $title); + $this->element('title', null, common_xml_safe_str($title)); $this->element('id', null, $id); $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null); @@ -858,7 +885,7 @@ class ApiAction extends Action } $this->element('updated', null, common_date_iso8601('now')); - $this->element('subtitle', null, $subtitle); + $this->element('subtitle', null, common_xml_safe_str($subtitle)); if (is_array($group)) { foreach ($group as $g) { @@ -1138,7 +1165,14 @@ class ApiAction extends Action function initTwitterRss() { $this->startXML(); - $this->elementStart('rss', array('version' => '2.0', 'xmlns:atom'=>'http://www.w3.org/2005/Atom')); + $this->elementStart( + 'rss', + array( + 'version' => '2.0', + 'xmlns:atom' => 'http://www.w3.org/2005/Atom', + 'xmlns:georss' => 'http://www.georss.org/georss' + ) + ); $this->elementStart('channel'); Event::handle('StartApiRss', array($this)); } @@ -1336,8 +1370,27 @@ class ApiAction extends Action } } - function getSelfUri($action, $aargs) + /** + * Calculate the complete URI that called up this action. Used for + * Atom rel="self" links. Warning: this is funky. + * + * @return string URL a URL suitable for rel="self" Atom links + */ + function getSelfUri() { + $action = mb_substr(get_class($this), 0, -6); // remove 'Action' + + $id = $this->arg('id'); + $aargs = array('format' => $this->format); + if (!empty($id)) { + $aargs['id'] = $id; + } + + $tag = $this->arg('tag'); + if (!empty($tag)) { + $aargs['tag'] = $tag; + } + parse_str($_SERVER['QUERY_STRING'], $params); $pstring = ''; if (!empty($params)) { diff --git a/lib/apiauth.php b/lib/apiauth.php index 5090871cf..17f803a1c 100644 --- a/lib/apiauth.php +++ b/lib/apiauth.php @@ -235,9 +235,13 @@ class ApiAuthAction extends ApiAction { $this->basicAuthProcessHeader(); - $realm = common_config('site', 'name') . ' API'; + $realm = common_config('api', 'realm'); - if (!isset($this->auth_user_nickname) && $required) { + if (empty($realm)) { + $realm = common_config('site', 'name') . ' API'; + } + + if (empty($this->auth_user_nickname) && $required) { header('WWW-Authenticate: Basic realm="' . $realm . '"'); // show error if the user clicks 'cancel' @@ -290,11 +294,15 @@ class ApiAuthAction extends ApiAction function basicAuthProcessHeader() { - if (isset($_SERVER['AUTHORIZATION']) - || isset($_SERVER['HTTP_AUTHORIZATION']) - ) { - $authorization_header = isset($_SERVER['HTTP_AUTHORIZATION']) - ? $_SERVER['HTTP_AUTHORIZATION'] : $_SERVER['AUTHORIZATION']; + $authHeaders = array('AUTHORIZATION', + 'HTTP_AUTHORIZATION', + 'REDIRECT_HTTP_AUTHORIZATION'); // rewrite for CGI + $authorization_header = null; + foreach ($authHeaders as $header) { + if (isset($_SERVER[$header])) { + $authorization_header = $_SERVER[$header]; + break; + } } if (isset($_SERVER['PHP_AUTH_USER'])) { diff --git a/lib/atom10feed.php b/lib/atom10feed.php index 2d342e785..a46d49f35 100644 --- a/lib/atom10feed.php +++ b/lib/atom10feed.php @@ -178,7 +178,7 @@ class Atom10Feed extends XMLStringer $this->element( 'generator', array( - 'url' => 'http://status.net', + 'uri' => 'http://status.net', 'version' => STATUSNET_VERSION ), 'StatusNet' diff --git a/lib/atomcategory.php b/lib/atomcategory.php new file mode 100644 index 000000000..4cc3b4f4d --- /dev/null +++ b/lib/atomcategory.php @@ -0,0 +1,77 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * 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 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/attachmentlist.php b/lib/attachmentlist.php index 51ceca857..d29a5fa2f 100644 --- a/lib/attachmentlist.php +++ b/lib/attachmentlist.php @@ -248,9 +248,7 @@ class Attachment extends AttachmentListItem $this->out->elementStart('div', array('id' => 'attachment_view', 'class' => 'hentry')); $this->out->elementStart('div', 'entry-title'); - $this->out->elementStart('a', $this->linkAttr()); - $this->out->element('span', null, $this->linkTitle()); - $this->out->elementEnd('a'); + $this->out->element('a', $this->linkAttr(), $this->linkTitle()); $this->out->elementEnd('div'); $this->out->elementStart('div', 'entry-content'); @@ -296,7 +294,7 @@ class Attachment extends AttachmentListItem } function linkAttr() { - return array('class' => 'external', 'href' => $this->attachment->url); + return array('rel' => 'external', 'href' => $this->attachment->url); } function linkTitle() { @@ -306,7 +304,7 @@ class Attachment extends AttachmentListItem function showRepresentation() { if (empty($this->oembed->type)) { if (empty($this->attachment->mimetype)) { - $this->out->element('pre', null, 'oh well... not sure how to handle the following: ' . print_r($this->attachment, true)); + $this->showFallback(); } else { switch ($this->attachment->mimetype) { case 'image/gif': @@ -332,6 +330,17 @@ class Attachment extends AttachmentListItem $this->out->element('param', array('name' => 'autoStart', 'value' => 1)); $this->out->elementEnd('object'); break; + + case 'text/html': + if ($this->attachment->filename) { + // Locally-uploaded HTML. Scrub and display inline. + $this->showHtmlFile($this->attachment); + break; + } + // Fall through to default + + default: + $this->showFallback(); } } } else { @@ -354,9 +363,76 @@ class Attachment extends AttachmentListItem break; default: - $this->out->element('pre', null, 'oh well... not sure how to handle the following oembed: ' . print_r($this->oembed, true)); + $this->showFallback(); } } } + + protected function showHtmlFile(File $attachment) + { + $body = $this->scrubHtmlFile($attachment); + if ($body) { + $this->out->raw($body); + } + } + + /** + * @return mixed false on failure, HTML fragment string on success + */ + protected function scrubHtmlFile(File $attachment) + { + $path = File::path($attachment->filename); + if (!file_exists($path) || !is_readable($path)) { + common_log(LOG_ERR, "Missing local HTML attachment $path"); + return false; + } + $raw = file_get_contents($path); + + // Normalize... + $dom = new DOMDocument(); + if(!$dom->loadHTML($raw)) { + common_log(LOG_ERR, "Bad HTML in local HTML attachment $path"); + return false; + } + + // Remove <script>s or htmlawed will dump their contents into output! + // Note: removing child nodes while iterating seems to mess things up, + // hence the double loop. + $scripts = array(); + foreach ($dom->getElementsByTagName('script') as $script) { + $scripts[] = $script; + } + foreach ($scripts as $script) { + common_log(LOG_DEBUG, $script->textContent); + $script->parentNode->removeChild($script); + } + + // Trim out everything outside the body... + $body = $dom->saveHTML(); + $body = preg_replace('/^.*<body[^>]*>/is', '', $body); + $body = preg_replace('/<\/body[^>]*>.*$/is', '', $body); + + require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php'; + $config = array('safe' => 1, + 'deny_attribute' => 'id,style,on*', + 'comment' => 1); // remove comments + $scrubbed = htmLawed($body, $config); + + return $scrubbed; + } + + function showFallback() + { + // If we don't know how to display an attachment inline, we probably + // shouldn't have gotten to this point. + // + // But, here we are... displaying details on a file or remote URL + // either on the main view or in an ajax-loaded lightbox. As a lesser + // of several evils, we'll try redirecting to the actual target via + // client-side JS. + + common_log(LOG_ERR, "Empty or unknown type for file id {$this->attachment->id}; falling back to client-side redirect."); + $this->out->raw('<script>window.location = ' . json_encode($this->attachment->url) . ';</script>'); + } } diff --git a/lib/authorizationplugin.php b/lib/authorizationplugin.php index 07da9b2d1..3790bccf4 100644 --- a/lib/authorizationplugin.php +++ b/lib/authorizationplugin.php @@ -67,7 +67,7 @@ abstract class AuthorizationPlugin extends Plugin //------------Below are the methods that connect StatusNet to the implementing Auth plugin------------\\ - function onStartSetUser(&$user) { + function onStartSetUser($user) { $loginAllowed = $this->loginAllowed($user); if($loginAllowed === true){ return; @@ -84,7 +84,7 @@ abstract class AuthorizationPlugin extends Plugin } } - function onStartSetApiUser(&$user) { + function onStartSetApiUser($user) { return $this->onStartSetUser($user); } diff --git a/lib/avatarlink.php b/lib/avatarlink.php new file mode 100644 index 000000000..e67799e2e --- /dev/null +++ b/lib/avatarlink.php @@ -0,0 +1,102 @@ +<?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); +} + +// 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; + } +} diff --git a/lib/channel.php b/lib/channel.php index 05437b4e9..e83960ac5 100644 --- a/lib/channel.php +++ b/lib/channel.php @@ -47,6 +47,25 @@ class Channel } } +class CLIChannel extends Channel +{ + function source() + { + return 'cli'; + } + + function output($user, $text) + { + $site = common_config('site', 'name'); + print "[{$user->nickname}@{$site}] $text\n"; + } + + function error($user, $text) + { + $this->output($user, $text); + } +} + class WebChannel extends Channel { var $out = null; diff --git a/lib/command.php b/lib/command.php index 5be9cd6e8..c2116828c 100644 --- a/lib/command.php +++ b/lib/command.php @@ -1,7 +1,7 @@ <?php /* * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2008, 2009, StatusNet, Inc. + * Copyright (C) 2008, 2009, 2010 StatusNet, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -31,15 +31,147 @@ class Command $this->user = $user; } - function execute($channel) + /** + * Execute the command and send success or error results + * back via the given communications channel. + * + * @param Channel + */ + public function execute($channel) + { + try { + $this->handle($channel); + } catch (CommandException $e) { + $channel->error($this->user, $e->getMessage()); + } catch (Exception $e) { + common_log(LOG_ERR, "Error handling " . get_class($this) . ": " . $e->getMessage()); + $channel->error($this->user, $e->getMessage()); + } + } + + + /** + * Override this with the meat! + * + * An error to send back to the user may be sent by throwing + * a CommandException with a formatted message. + * + * @param Channel + * @throws CommandException + */ + function handle($channel) { return false; } + + /** + * Look up a notice from an argument, by poster's name to get last post + * or notice_id prefixed with #. + * + * @return Notice + * @throws CommandException + */ + function getNotice($arg) + { + $notice = null; + if (Event::handle('StartCommandGetNotice', array($this, $arg, &$notice))) { + if(substr($this->other,0,1)=='#'){ + // A specific notice_id #123 + + $notice = Notice::staticGet(substr($arg,1)); + if (!$notice) { + throw new CommandException(_('Notice with that id does not exist')); + } + } + + if (Validate::uri($this->other)) { + // A specific notice by URI lookup + $notice = Notice::staticGet('uri', $arg); + } + + if (!$notice) { + // Local or remote profile name to get their last notice. + // May throw an exception and report 'no such user' + $recipient = $this->getProfile($arg); + + $notice = $recipient->getCurrentNotice(); + if (!$notice) { + throw new CommandException(_('User has no last notice')); + } + } + } + Event::handle('EndCommandGetNotice', array($this, $arg, &$notice)); + if (!$notice) { + throw new CommandException(_('Notice with that id does not exist')); + } + return $notice; + } + + /** + * Look up a local or remote profile by nickname. + * + * @return Profile + * @throws CommandException + */ + function getProfile($arg) + { + $profile = null; + if (Event::handle('StartCommandGetProfile', array($this, $arg, &$profile))) { + $profile = + common_relative_profile($this->user, common_canonical_nickname($arg)); + } + Event::handle('EndCommandGetProfile', array($this, $arg, &$profile)); + if (!$profile) { + throw new CommandException(sprintf(_('Could not find a user with nickname %s'), $arg)); + } + return $profile; + } + + /** + * Get a local user by name + * @return User + * @throws CommandException + */ + function getUser($arg) + { + $user = null; + if (Event::handle('StartCommandGetUser', array($this, $arg, &$user))) { + $user = User::staticGet('nickname', $arg); + } + Event::handle('EndCommandGetUser', array($this, $arg, &$user)); + if (!$user){ + throw new CommandException(sprintf(_('Could not find a local user with nickname %s'), + $arg)); + } + return $user; + } + + /** + * Get a local or remote group by name. + * @return User_group + * @throws CommandException + */ + function getGroup($arg) + { + $group = null; + if (Event::handle('StartCommandGetGroup', array($this, $arg, &$group))) { + $group = User_group::getForNickname($arg, $this->user->getProfile()); + } + Event::handle('EndCommandGetGroup', array($this, $arg, &$group)); + if (!$group) { + throw new CommandException(_('No such group.')); + } + return $group; + } +} + +class CommandException extends Exception +{ } class UnimplementedCommand extends Command { - function execute($channel) + function handle($channel) { $channel->error($this->user, _("Sorry, this command is not yet implemented.")); } @@ -81,24 +213,20 @@ class NudgeCommand extends Command parent::__construct($user); $this->other = $other; } - function execute($channel) + + function handle($channel) { - $recipient = User::staticGet('nickname', $this->other); - if(! $recipient){ - $channel->error($this->user, sprintf(_('Could not find a user with nickname %s'), - $this->other)); - }else{ - if ($recipient->id == $this->user->id) { - $channel->error($this->user, _('It does not make a lot of sense to nudge yourself!')); - }else{ - if ($recipient->email && $recipient->emailnotifynudge) { - mail_notify_nudge($this->user, $recipient); - } - // XXX: notify by IM - // XXX: notify by SMS - $channel->output($this->user, sprintf(_('Nudge sent to %s'), - $recipient->nickname)); + $recipient = $this->getUser($this->other); + if ($recipient->id == $this->user->id) { + throw new CommandException(_('It does not make a lot of sense to nudge yourself!')); + } else { + if ($recipient->email && $recipient->emailnotifynudge) { + mail_notify_nudge($this->user, $recipient); } + // XXX: notify by IM + // XXX: notify by SMS + $channel->output($this->user, sprintf(_('Nudge sent to %s'), + $recipient->nickname)); } } } @@ -115,7 +243,7 @@ class InviteCommand extends UnimplementedCommand class StatsCommand extends Command { - function execute($channel) + function handle($channel) { $profile = $this->user->getProfile(); @@ -142,34 +270,9 @@ class FavCommand extends Command $this->other = $other; } - function execute($channel) + function handle($channel) { - if(substr($this->other,0,1)=='#'){ - //favoriting a specific notice_id - - $notice = Notice::staticGet(substr($this->other,1)); - if (!$notice) { - $channel->error($this->user, _('Notice with that id does not exist')); - return; - } - $recipient = $notice->getProfile(); - }else{ - //favoriting a given user's last notice - - $recipient = - common_relative_profile($this->user, common_canonical_nickname($this->other)); - - if (!$recipient) { - $channel->error($this->user, _('No such user.')); - return; - } - $notice = $recipient->getCurrentNotice(); - if (!$notice) { - $channel->error($this->user, _('User has no last notice')); - return; - } - } - + $notice = $this->getNotice($this->other); $fave = Fave::addNew($this->user, $notice); if (!$fave) { @@ -177,7 +280,10 @@ class FavCommand extends Command return; } - $other = User::staticGet('id', $recipient->id); + // @fixme favorite notification should be triggered + // at a lower level + + $other = User::staticGet('id', $notice->profile_id); if ($other && $other->id != $user->id) { if ($other->email && $other->emailnotifyfav) { @@ -191,6 +297,7 @@ class FavCommand extends Command } } + class JoinCommand extends Command { var $other = null; @@ -201,17 +308,10 @@ class JoinCommand extends Command $this->other = $other; } - function execute($channel) + function handle($channel) { - - $nickname = common_canonical_nickname($this->other); - $group = User_group::staticGet('nickname', $nickname); - $cur = $this->user; - - if (!$group) { - $channel->error($cur, _('No such group.')); - return; - } + $group = $this->getGroup($this->other); + $cur = $this->user; if ($cur->isMember($group)) { $channel->error($cur, _('You are already a member of that group')); @@ -249,12 +349,10 @@ class DropCommand extends Command $this->other = $other; } - function execute($channel) + function handle($channel) { - - $nickname = common_canonical_nickname($this->other); - $group = User_group::staticGet('nickname', $nickname); - $cur = $this->user; + $group = $this->getGroup($this->other); + $cur = $this->user; if (!$group) { $channel->error($cur, _('No such group.')); @@ -293,15 +391,9 @@ class WhoisCommand extends Command $this->other = $other; } - function execute($channel) + function handle($channel) { - $recipient = - common_relative_profile($this->user, common_canonical_nickname($this->other)); - - if (!$recipient) { - $channel->error($this->user, _('No such user.')); - return; - } + $recipient = $this->getProfile($this->other); $whois = sprintf(_("%1\$s (%2\$s)"), $recipient->nickname, $recipient->profileurl); @@ -332,9 +424,18 @@ class MessageCommand extends Command $this->text = $text; } - function execute($channel) + function handle($channel) { - $other = User::staticGet('nickname', common_canonical_nickname($this->other)); + try { + $other = $this->getUser($this->other); + } catch (CommandException $e) { + try { + $profile = $this->getProfile($this->other); + } catch (CommandException $f) { + throw $e; + } + throw new CommandException(sprintf(_('%s is a remote profile; you can only send direct messages to users on the same server.'), $this->other)); + } $len = mb_strlen($this->text); @@ -380,33 +481,9 @@ class RepeatCommand extends Command $this->other = $other; } - function execute($channel) + function handle($channel) { - if(substr($this->other,0,1)=='#'){ - //repeating a specific notice_id - - $notice = Notice::staticGet(substr($this->other,1)); - if (!$notice) { - $channel->error($this->user, _('Notice with that id does not exist')); - return; - } - $recipient = $notice->getProfile(); - }else{ - //repeating a given user's last notice - - $recipient = - common_relative_profile($this->user, common_canonical_nickname($this->other)); - - if (!$recipient) { - $channel->error($this->user, _('No such user.')); - return; - } - $notice = $recipient->getCurrentNotice(); - if (!$notice) { - $channel->error($this->user, _('User has no last notice')); - return; - } - } + $notice = $this->getNotice($this->other); if($this->user->id == $notice->profile_id) { @@ -414,7 +491,7 @@ class RepeatCommand extends Command return; } - if ($recipient->hasRepeated($notice->id)) { + if ($this->user->getProfile()->hasRepeated($notice->id)) { $channel->error($this->user, _('Already repeated that notice')); return; } @@ -441,33 +518,10 @@ class ReplyCommand extends Command $this->text = $text; } - function execute($channel) + function handle($channel) { - if(substr($this->other,0,1)=='#'){ - //replying to a specific notice_id - - $notice = Notice::staticGet(substr($this->other,1)); - if (!$notice) { - $channel->error($this->user, _('Notice with that id does not exist')); - return; - } - $recipient = $notice->getProfile(); - }else{ - //replying to a given user's last notice - - $recipient = - common_relative_profile($this->user, common_canonical_nickname($this->other)); - - if (!$recipient) { - $channel->error($this->user, _('No such user.')); - return; - } - $notice = $recipient->getCurrentNotice(); - if (!$notice) { - $channel->error($this->user, _('User has no last notice')); - return; - } - } + $notice = $this->getNotice($this->other); + $recipient = $notice->getProfile(); $len = mb_strlen($this->text); @@ -507,17 +561,10 @@ class GetCommand extends Command $this->other = $other; } - function execute($channel) + function handle($channel) { - $target_nickname = common_canonical_nickname($this->other); - - $target = - common_relative_profile($this->user, $target_nickname); + $target = $this->getProfile($this->other); - if (!$target) { - $channel->error($this->user, _('No such user.')); - return; - } $notice = $target->getCurrentNotice(); if (!$notice) { $channel->error($this->user, _('User has no last notice')); @@ -525,7 +572,7 @@ class GetCommand extends Command } $notice_content = $notice->content; - $channel->output($this->user, $target_nickname . ": " . $notice_content); + $channel->output($this->user, $target->nickname . ": " . $notice_content); } } @@ -540,7 +587,7 @@ class SubCommand extends Command $this->other = $other; } - function execute($channel) + function handle($channel) { if (!$this->other) { @@ -548,16 +595,16 @@ class SubCommand extends Command return; } - $otherUser = User::staticGet('nickname', $this->other); + $target = $this->getProfile($this->other); - if (empty($otherUser)) { - $channel->error($this->user, _('No such user')); - return; + $remote = Remote_profile::staticGet('id', $target->id); + if ($remote) { + throw new CommandException(_("Can't subscribe to OMB profiles by command.")); } try { Subscription::start($this->user->getProfile(), - $otherUser->getProfile()); + $target); $channel->output($this->user, sprintf(_('Subscribed to %s'), $this->other)); } catch (Exception $e) { $channel->error($this->user, $e->getMessage()); @@ -576,22 +623,18 @@ class UnsubCommand extends Command $this->other = $other; } - function execute($channel) + function handle($channel) { if(!$this->other) { $channel->error($this->user, _('Specify the name of the user to unsubscribe from')); return; } - $otherUser = User::staticGet('nickname', $this->other); - - if (empty($otherUser)) { - $channel->error($this->user, _('No such user')); - } + $target = $this->getProfile($this->other); try { Subscription::cancel($this->user->getProfile(), - $otherUser->getProfile()); + $target); $channel->output($this->user, sprintf(_('Unsubscribed from %s'), $this->other)); } catch (Exception $e) { $channel->error($this->user, $e->getMessage()); @@ -607,7 +650,7 @@ class OffCommand extends Command parent::__construct($user); $this->other = $other; } - function execute($channel) + function handle($channel) { if ($this->other) { $channel->error($this->user, _("Command not yet implemented.")); @@ -630,7 +673,7 @@ class OnCommand extends Command $this->other = $other; } - function execute($channel) + function handle($channel) { if ($this->other) { $channel->error($this->user, _("Command not yet implemented.")); @@ -646,7 +689,7 @@ class OnCommand extends Command class LoginCommand extends Command { - function execute($channel) + function handle($channel) { $disabled = common_config('logincommand','disabled'); $disabled = isset($disabled) && $disabled; @@ -686,7 +729,7 @@ class LoseCommand extends Command return; } - $result=subs_unsubscribe_from($this->user, $this->other); + $result = Subscription::cancel($this->getProfile($this->other), $this->user->getProfile()); if ($result) { $channel->output($this->user, sprintf(_('Unsubscribed %s'), $this->other)); @@ -698,7 +741,7 @@ class LoseCommand extends Command class SubscriptionsCommand extends Command { - function execute($channel) + function handle($channel) { $profile = $this->user->getSubscriptions(0); $nicknames=array(); @@ -720,7 +763,7 @@ class SubscriptionsCommand extends Command class SubscribersCommand extends Command { - function execute($channel) + function handle($channel) { $profile = $this->user->getSubscribers(); $nicknames=array(); @@ -742,7 +785,7 @@ class SubscribersCommand extends Command class GroupsCommand extends Command { - function execute($channel) + function handle($channel) { $group = $this->user->getGroups(); $groups=array(); @@ -763,7 +806,7 @@ class GroupsCommand extends Command class HelpCommand extends Command { - function execute($channel) + function handle($channel) { $channel->output($this->user, _("Commands:\n". diff --git a/lib/common.php b/lib/common.php index 047dc5a7b..334a88ffd 100644 --- a/lib/common.php +++ b/lib/common.php @@ -71,7 +71,6 @@ 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 @@ -124,22 +123,10 @@ 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 f22d8b24a..10f3f1a97 100644 --- a/lib/default.php +++ b/lib/default.php @@ -294,4 +294,6 @@ $default = array('crawldelay' => 0, 'disallow' => array('main', 'settings', 'admin', 'search', 'message') ), + 'api' => + array('realm' => null), ); diff --git a/lib/deluserqueuehandler.php b/lib/deluserqueuehandler.php new file mode 100644 index 000000000..4a1233a5e --- /dev/null +++ b/lib/deluserqueuehandler.php @@ -0,0 +1,95 @@ +<?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/>. + */ + +/** + * Background job to delete prolific users without disrupting front-end too much. + * + * Up to 50 messages are deleted on each run through; when all messages are gone, + * the actual account is deleted. + * + * @package QueueHandler + * @maintainer Brion Vibber <brion@status.net> + */ + +class DelUserQueueHandler extends QueueHandler +{ + const DELETION_WINDOW = 50; + + public function transport() + { + return 'deluser'; + } + + public function handle($user) + { + if (!($user instanceof User)) { + common_log(LOG_ERR, "Got a bogus user, not deleting"); + return true; + } + + $user = User::staticGet('id', $user->id); + if (!$user) { + common_log(LOG_INFO, "User {$user->nickname} was deleted before we got here."); + return true; + } + + if (!$user->hasRole(Profile_role::DELETED)) { + common_log(LOG_INFO, "User {$user->nickname} is not pending deletion; aborting."); + return true; + } + + $notice = $this->getNextBatch($user); + if ($notice->N) { + common_log(LOG_INFO, "Deleting next {$notice->N} notices by {$user->nickname}"); + while ($notice->fetch()) { + $del = clone($notice); + $del->delete(); + } + + // @todo improve reliability in case we died during the above deletions + // with a fatal error. If the job is lost, we should perform some kind + // of garbage collection later. + + // Queue up the next batch. + $qm = QueueManager::get(); + $qm->enqueue($user, 'deluser'); + } else { + // Out of notices? Let's finish deleting this guy! + $user->delete(); + common_log(LOG_INFO, "User $user->id $user->nickname deleted."); + return true; + } + + return true; + } + + /** + * Fetch the next self::DELETION_WINDOW messages for this user. + * @return Notice + */ + protected function getNextBatch(User $user) + { + $notice = new Notice(); + $notice->profile_id = $user->id; + $notice->limit(self::DELETION_WINDOW); + $notice->find(); + return $notice; + } + +} diff --git a/lib/httpclient.php b/lib/httpclient.php index 4c3af8d7d..64a51353c 100644 --- a/lib/httpclient.php +++ b/lib/httpclient.php @@ -120,6 +120,16 @@ class HTTPClient extends HTTP_Request2 { $this->config['max_redirs'] = 10; $this->config['follow_redirects'] = true; + + // We've had some issues with keepalive breaking with + // HEAD requests, such as to youtube which seems to be + // emitting chunked encoding info for an empty body + // instead of not emitting anything. This may be a + // bug on YouTube's end, but the upstream libray + // ought to be investigated to see if we can handle + // it gracefully in that case as well. + $this->config['protocol_version'] = '1.0'; + parent::__construct($url, $method, $config); $this->setHeader('User-Agent', $this->userAgent()); } diff --git a/lib/imagefile.php b/lib/imagefile.php index 7b0479455..e47287741 100644 --- a/lib/imagefile.php +++ b/lib/imagefile.php @@ -60,6 +60,19 @@ class ImageFile $this->filepath = $filepath; $info = @getimagesize($this->filepath); + + if (!( + ($info[2] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) || + ($info[2] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) || + $info[2] == IMAGETYPE_BMP || + ($info[2] == IMAGETYPE_WBMP && function_exists('imagecreatefromwbmp')) || + ($info[2] == IMAGETYPE_XBM && function_exists('imagecreatefromxbm')) || + ($info[2] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')))) { + + throw new Exception(_('Unsupported image file format.')); + return; + } + $this->type = ($info) ? $info[2]:$type; $this->width = ($info) ? $info[0]:$width; $this->height = ($info) ? $info[1]:$height; @@ -97,19 +110,6 @@ class ImageFile return; } - 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']); - throw new Exception(_('Unsupported image file format.')); - return; - } - return new ImageFile(null, $_FILES[$param]['tmp_name']); } @@ -159,9 +159,6 @@ class ImageFile 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; @@ -204,10 +201,6 @@ class ImageFile //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, diff --git a/lib/language.php b/lib/language.php index 64b59e739..76c788025 100644 --- a/lib/language.php +++ b/lib/language.php @@ -202,16 +202,19 @@ function _mdomain($backtrace) static $cached; $path = $backtrace[0]['file']; if (!isset($cached[$path])) { + $final = 'statusnet'; // assume default domain if (DIRECTORY_SEPARATOR !== '/') { $path = strtr($path, DIRECTORY_SEPARATOR, '/'); } - $cut = strpos($path, '/plugins/') + 9; - $cut2 = strpos($path, '/', $cut); - if ($cut && $cut2) { - $cached[$path] = substr($path, $cut, $cut2 - $cut); - } else { - return null; + $cut = strpos($path, '/plugins/'); + if ($cut) { + $cut += strlen('/plugins/'); + $cut2 = strpos($path, '/', $cut); + if ($cut && $cut2) { + $final = substr($path, $cut, $cut2 - $cut); + } } + $cached[$path] = $final; } return $cached[$path]; } diff --git a/lib/mysqlschema.php b/lib/mysqlschema.php index 485096ac4..455695366 100644 --- a/lib/mysqlschema.php +++ b/lib/mysqlschema.php @@ -90,15 +90,24 @@ class MysqlSchema extends Schema * @param string $name Name of the table to get * * @return TableDef tabledef for that table. + * @throws SchemaTableMissingException */ public function getTableDef($name) { - $res = $this->conn->query('DESCRIBE ' . $name); + $query = "SELECT * FROM INFORMATION_SCHEMA.COLUMNS " . + "WHERE TABLE_SCHEMA='%s' AND TABLE_NAME='%s'"; + $schema = $this->conn->dsn['database']; + $sql = sprintf($query, $schema, $name); + $res = $this->conn->query($sql); if (PEAR::isError($res)) { throw new Exception($res->getMessage()); } + if ($res->numRows() == 0) { + $res->free(); + throw new SchemaTableMissingException("No such table: $name"); + } $td = new TableDef(); @@ -111,9 +120,9 @@ class MysqlSchema extends Schema $cd = new ColumnDef(); - $cd->name = $row['Field']; + $cd->name = $row['COLUMN_NAME']; - $packed = $row['Type']; + $packed = $row['COLUMN_TYPE']; if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) { $cd->type = $match[1]; @@ -122,18 +131,58 @@ class MysqlSchema extends Schema $cd->type = $packed; } - $cd->nullable = ($row['Null'] == 'YES') ? true : false; - $cd->key = $row['Key']; - $cd->default = $row['Default']; - $cd->extra = $row['Extra']; + $cd->nullable = ($row['IS_NULLABLE'] == 'YES') ? true : false; + $cd->key = $row['COLUMN_KEY']; + $cd->default = $row['COLUMN_DEFAULT']; + $cd->extra = $row['EXTRA']; + + // Autoincrement is stuck into the extra column. + // Pull it out so we don't accidentally mod it every time... + $extra = preg_replace('/(^|\s)auto_increment(\s|$)/i', '$1$2', $cd->extra); + if ($extra != $cd->extra) { + $cd->extra = trim($extra); + $cd->auto_increment = true; + } + + // mysql extensions -- not (yet) used by base class + $cd->charset = $row['CHARACTER_SET_NAME']; + $cd->collate = $row['COLLATION_NAME']; $td->columns[] = $cd; } + $res->free(); return $td; } /** + * Pull the given table properties from INFORMATION_SCHEMA. + * Most of the good stuff is MySQL extensions. + * + * @return array + * @throws Exception if table info can't be looked up + */ + + function getTableProperties($table, $props) + { + $query = "SELECT %s FROM INFORMATION_SCHEMA.TABLES " . + "WHERE TABLE_SCHEMA='%s' AND TABLE_NAME='%s'"; + $schema = $this->conn->dsn['database']; + $sql = sprintf($query, implode(',', $props), $schema, $table); + $res = $this->conn->query($sql); + + $row = array(); + $ok = $res->fetchInto($row, DB_FETCHMODE_ASSOC); + $res->free(); + + if ($ok) { + return $row; + } else { + throw new SchemaTableMissingException("No such table: $table"); + } + } + + /** * Gets a ColumnDef object for a single column. * * Throws an exception if the table is not found. @@ -185,35 +234,26 @@ class MysqlSchema extends Schema } $sql .= $this->_columnSql($cd); - - switch ($cd->key) { - case 'UNI': - $uniques[] = $cd->name; - break; - case 'PRI': - $primary[] = $cd->name; - break; - case 'MUL': - $indices[] = $cd->name; - break; - } } - if (count($primary) > 0) { // it really should be... - $sql .= ",\nconstraint primary key (" . implode(',', $primary) . ")"; + $idx = $this->_indexList($columns); + + if ($idx['primary']) { + $sql .= ",\nconstraint primary key (" . implode(',', $idx['primary']) . ")"; } - foreach ($uniques as $u) { - $sql .= ",\nunique index {$name}_{$u}_idx ($u)"; + foreach ($idx['uniques'] as $u) { + $key = $this->_uniqueKey($name, $u); + $sql .= ",\nunique index $key ($u)"; } - foreach ($indices as $i) { - $sql .= ",\nindex {$name}_{$i}_idx ($i)"; + foreach ($idx['indices'] as $i) { + $key = $this->_key($name, $i); + $sql .= ",\nindex $key ($i)"; } - $sql .= "); "; + $sql .= ") ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; "; - common_log(LOG_INFO, $sql); $res = $this->conn->query($sql); if (PEAR::isError($res)) { @@ -224,6 +264,47 @@ class MysqlSchema extends Schema } /** + * Look over a list of column definitions and list up which + * indices will be present + */ + private function _indexList(array $columns) + { + $list = array('uniques' => array(), + 'primary' => array(), + 'indices' => array()); + foreach ($columns as $cd) { + switch ($cd->key) { + case 'UNI': + $list['uniques'][] = $cd->name; + break; + case 'PRI': + $list['primary'][] = $cd->name; + break; + case 'MUL': + $list['indices'][] = $cd->name; + break; + } + } + return $list; + } + + /** + * Get the unique index key name for a given column on this table + */ + function _uniqueKey($tableName, $columnName) + { + return $this->_key($tableName, $columnName); + } + + /** + * Get the index key name for a given column on this table + */ + function _key($tableName, $columnName) + { + return "{$tableName}_{$columnName}_idx"; + } + + /** * Drops a table from the schema * * Throws an exception if the table is not found. @@ -394,21 +475,20 @@ class MysqlSchema extends Schema try { $td = $this->getTableDef($tableName); - } catch (Exception $e) { - if (preg_match('/no such table/', $e->getMessage())) { - return $this->createTable($tableName, $columns); - } else { - throw $e; - } + } catch (SchemaTableMissingException $e) { + return $this->createTable($tableName, $columns); } $cur = $this->_names($td->columns); $new = $this->_names($columns); - $toadd = array_diff($new, $cur); - $todrop = array_diff($cur, $new); - $same = array_intersect($new, $cur); - $tomod = array(); + $dropIndex = array(); + $toadd = array_diff($new, $cur); + $todrop = array_diff($cur, $new); + $same = array_intersect($new, $cur); + $tomod = array(); + $addIndex = array(); + $tableProps = array(); foreach ($same as $m) { $curCol = $this->_byName($td->columns, $m); @@ -416,10 +496,64 @@ class MysqlSchema extends Schema if (!$newCol->equals($curCol)) { $tomod[] = $newCol->name; + continue; + } + + // Earlier versions may have accidentally left tables at default + // charsets which might be latin1 or other freakish things. + if ($this->_isString($curCol)) { + if ($curCol->charset != 'utf8') { + $tomod[] = $newCol->name; + continue; + } + } + } + + // Find any indices we have to change... + $curIdx = $this->_indexList($td->columns); + $newIdx = $this->_indexList($columns); + + if ($curIdx['primary'] != $newIdx['primary']) { + if ($curIdx['primary']) { + $dropIndex[] = 'drop primary key'; + } + if ($newIdx['primary']) { + $keys = implode(',', $newIdx['primary']); + $addIndex[] = "add constraint primary key ($keys)"; } } - if (count($toadd) + count($todrop) + count($tomod) == 0) { + $dropUnique = array_diff($curIdx['uniques'], $newIdx['uniques']); + $addUnique = array_diff($newIdx['uniques'], $curIdx['uniques']); + foreach ($dropUnique as $columnName) { + $dropIndex[] = 'drop key ' . $this->_uniqueKey($tableName, $columnName); + } + foreach ($addUnique as $columnName) { + $addIndex[] = 'add constraint unique key ' . $this->_uniqueKey($tableName, $columnName) . " ($columnName)";; + } + + $dropMultiple = array_diff($curIdx['indices'], $newIdx['indices']); + $addMultiple = array_diff($newIdx['indices'], $curIdx['indices']); + foreach ($dropMultiple as $columnName) { + $dropIndex[] = 'drop key ' . $this->_key($tableName, $columnName); + } + foreach ($addMultiple as $columnName) { + $addIndex[] = 'add key ' . $this->_key($tableName, $columnName) . " ($columnName)"; + } + + // Check for table properties: make sure we're using a sane + // engine type and charset/collation. + // @fixme make the default engine configurable? + $oldProps = $this->getTableProperties($tableName, array('ENGINE', 'TABLE_COLLATION')); + if (strtolower($oldProps['ENGINE']) != 'innodb') { + $tableProps['ENGINE'] = 'InnoDB'; + } + if (strtolower($oldProps['TABLE_COLLATION']) != 'utf8_bin') { + $tableProps['DEFAULT CHARSET'] = 'utf8'; + $tableProps['COLLATE'] = 'utf8_bin'; + } + + if (count($dropIndex) + count($toadd) + count($todrop) + count($tomod) + count($addIndex) + count($tableProps) == 0) { // nothing to do return true; } @@ -429,6 +563,10 @@ class MysqlSchema extends Schema $phrase = array(); + foreach ($dropIndex as $indexSql) { + $phrase[] = $indexSql; + } + foreach ($toadd as $columnName) { $cd = $this->_byName($columns, $columnName); @@ -445,8 +583,17 @@ class MysqlSchema extends Schema $phrase[] = 'MODIFY COLUMN ' . $this->_columnSql($cd); } + foreach ($addIndex as $indexSql) { + $phrase[] = $indexSql; + } + + foreach ($tableProps as $key => $val) { + $phrase[] = "$key=$val"; + } + $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase); + common_log(LOG_DEBUG, __METHOD__ . ': ' . $sql); $res = $this->conn->query($sql); if (PEAR::isError($res)) { @@ -519,6 +666,10 @@ class MysqlSchema extends Schema $sql .= "{$cd->type} "; } + if ($this->_isString($cd)) { + $sql .= " CHARACTER SET utf8 "; + } + if (!empty($cd->default)) { $sql .= "default {$cd->default} "; } else { @@ -535,4 +686,13 @@ class MysqlSchema extends Schema return $sql; } + + /** + * Is this column a string type? + */ + private function _isString(ColumnDef $cd) + { + $strings = array('char', 'varchar', 'text'); + return in_array(strtolower($cd->type), $strings); + } } diff --git a/lib/noticelist.php b/lib/noticelist.php index 88a925241..0d4cd4dd9 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -442,11 +442,14 @@ class NoticeListItem extends Widget 'title' => $latlon), $name); } else { - $this->out->elementStart('a', array('href' => $url)); - $this->out->element('abbr', array('class' => 'geo', - 'title' => $latlon), - $name); - $this->out->elementEnd('a'); + $xstr = new XMLStringer(false); + $xstr->elementStart('a', array('href' => $url, + 'rel' => 'external')); + $xstr->element('abbr', array('class' => 'geo', + 'title' => $latlon), + $name); + $xstr->elementEnd('a'); + $this->out->raw($xstr->getString()); } $this->out->elementEnd('span'); } diff --git a/lib/pgsqlschema.php b/lib/pgsqlschema.php index 91bc09667..715065d77 100644 --- a/lib/pgsqlschema.php +++ b/lib/pgsqlschema.php @@ -61,7 +61,8 @@ class PgsqlSchema extends Schema public function getTableDef($name) { - $res = $this->conn->query("select *, column_default as default, is_nullable as Null, udt_name as Type, column_name AS Field from INFORMATION_SCHEMA.COLUMNS where table_name = '$name'"); + $res = $this->conn->query("SELECT *, column_default as default, is_nullable as Null, + udt_name as Type, column_name AS Field from INFORMATION_SCHEMA.COLUMNS where table_name = '$name'"); if (PEAR::isError($res)) { throw new Exception($res->getMessage()); @@ -72,6 +73,9 @@ class PgsqlSchema extends Schema $td->name = $name; $td->columns = array(); + if ($res->numRows() == 0 ) { + throw new Exception('no such table'); //pretend to be the msyql error. yeah, this sucks. + } $row = array(); while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) { @@ -166,12 +170,10 @@ class PgsqlSchema extends Schema } if (count($primary) > 0) { // it really should be... - $sql .= ",\nconstraint primary key (" . implode(',', $primary) . ")"; + $sql .= ",\n primary key (" . implode(',', $primary) . ")"; } - foreach ($uniques as $u) { - $sql .= ",\nunique index {$name}_{$u}_idx ($u)"; - } + foreach ($indices as $i) { $sql .= ",\nindex {$name}_{$i}_idx ($i)"; @@ -179,6 +181,10 @@ class PgsqlSchema extends Schema $sql .= "); "; + + foreach ($uniques as $u) { + $sql .= "\n CREATE index {$name}_{$u}_idx ON {$name} ($u); "; + } $res = $this->conn->query($sql); if (PEAR::isError($res)) { @@ -210,6 +216,22 @@ class PgsqlSchema extends Schema } /** + * Translate the (mostly) mysql-ish column types into somethings more standard + * @param string column type + * + * @return string postgres happy column type + */ + private function _columnTypeTranslation($type) { + $map = array( + 'datetime' => 'timestamp' + ); + if(!empty($map[$type])) { + return $map[$type]; + } + return $type; + } + + /** * Adds an index to a table. * * If no name is provided, a name will be made up based @@ -359,6 +381,7 @@ class PgsqlSchema extends Schema try { $td = $this->getTableDef($tableName); + } catch (Exception $e) { if (preg_match('/no such table/', $e->getMessage())) { return $this->createTable($tableName, $columns); @@ -477,11 +500,12 @@ class PgsqlSchema extends Schema private function _columnSql($cd) { $sql = "{$cd->name} "; + $type = $this->_columnTypeTranslation($cd->type); if (!empty($cd->size)) { - $sql .= "{$cd->type}({$cd->size}) "; + $sql .= "{$type}({$cd->size}) "; } else { - $sql .= "{$cd->type} "; + $sql .= "{$type} "; } if (!empty($cd->default)) { diff --git a/lib/poco.php b/lib/poco.php new file mode 100644 index 000000000..2157062b3 --- /dev/null +++ b/lib/poco.php @@ -0,0 +1,240 @@ +<?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 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, common_xml_safe_str($this->note)); + } + + if (!empty($this->address)) { + $xs->raw($this->address->asString()); + } + + foreach ($this->urls as $url) { + $xs->raw($url->asString()); + } + + return $xs->getString(); + } +} diff --git a/lib/pocoaddress.php b/lib/pocoaddress.php new file mode 100644 index 000000000..60873bdc4 --- /dev/null +++ b/lib/pocoaddress.php @@ -0,0 +1,56 @@ +<?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 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, common_xml_safe_str($this->formatted)); + $xs->elementEnd('poco:address'); + return $xs->getString(); + } + + return null; + } +} diff --git a/lib/pocourl.php b/lib/pocourl.php new file mode 100644 index 000000000..803484d76 --- /dev/null +++ b/lib/pocourl.php @@ -0,0 +1,65 @@ +<?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(); + } +} diff --git a/lib/processmanager.php b/lib/processmanager.php new file mode 100644 index 000000000..6032bfc5c --- /dev/null +++ b/lib/processmanager.php @@ -0,0 +1,84 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * i/o manager to watch for a dead parent process + * + * 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 QueueManager + * @package StatusNet + * @author Brion Vibber <brion@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class ProcessManager extends IoManager +{ + protected $socket; + + public static function get() + { + throw new Exception("Must pass ProcessManager per-instance"); + } + + public function __construct($socket) + { + $this->socket = $socket; + } + + /** + * Tell the i/o queue master if and how we can handle multi-site + * processes. + * + * Return one of: + * IoManager::SINGLE_ONLY + * IoManager::INSTANCE_PER_SITE + * IoManager::INSTANCE_PER_PROCESS + */ + public static function multiSite() + { + return IoManager::INSTANCE_PER_PROCESS; + } + + /** + * We won't get any input on it, but if it's broken we'll + * know something's gone horribly awry. + * + * @return array of resources + */ + function getSockets() + { + return array($this->socket); + } + + /** + * See if the parent died and request a shutdown... + * + * @param resource $socket + * @return boolean success + */ + function handleInput($socket) + { + if (feof($socket)) { + common_log(LOG_INFO, "Parent process exited; shutting down child."); + $this->master->requestShutdown(); + } + return true; + } +} + diff --git a/lib/queuemanager.php b/lib/queuemanager.php index fe45e8bbf..6666a6cb5 100644 --- a/lib/queuemanager.php +++ b/lib/queuemanager.php @@ -239,6 +239,9 @@ abstract class QueueManager extends IoManager $this->connect('sms', 'SmsQueueHandler'); } + // Background user management tasks... + $this->connect('deluser', 'DelUserQueueHandler'); + // Broadcasting profile updates to OMB remote subscribers $this->connect('profile', 'ProfileQueueHandler'); diff --git a/lib/router.php b/lib/router.php index 706120e0b..a48ee875e 100644 --- a/lib/router.php +++ b/lib/router.php @@ -628,6 +628,12 @@ class Router array('action' => 'ApiTimelineTag', 'format' => '(xmljson|rss|atom)')); + // media related + $m->connect( + 'api/statusnet/media/upload', + array('action' => 'ApiMediaUpload') + ); + // search $m->connect('api/search.atom', array('action' => 'twitapisearchatom')); $m->connect('api/search.json', array('action' => 'twitapisearchjson')); diff --git a/lib/schema.php b/lib/schema.php index 137b814e0..1503c96d4 100644 --- a/lib/schema.php +++ b/lib/schema.php @@ -485,3 +485,9 @@ class Schema return $sql; } } + +class SchemaTableMissingException extends Exception +{ + // no-op +} + diff --git a/lib/servererroraction.php b/lib/servererroraction.php index 0993a63bc..9b5a553dc 100644 --- a/lib/servererroraction.php +++ b/lib/servererroraction.php @@ -62,15 +62,18 @@ class ServerErrorAction extends ErrorAction 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported'); - function __construct($message='Error', $code=500) + function __construct($message='Error', $code=500, $ex=null) { parent::__construct($message, $code); $this->default = 500; // Server errors must be logged. - - common_log(LOG_ERR, "ServerErrorAction: $code $message"); + $log = "ServerErrorAction: $code $message"; + if ($ex) { + $log .= "\n" . $ex->getTraceAsString(); + } + common_log(LOG_ERR, $log); } // XXX: Should these error actions even be invokable via URI? diff --git a/lib/spawningdaemon.php b/lib/spawningdaemon.php index fd9ae4355..2f9f6e32e 100644 --- a/lib/spawningdaemon.php +++ b/lib/spawningdaemon.php @@ -71,6 +71,8 @@ abstract class SpawningDaemon extends Daemon */ function run() { + $this->initPipes(); + $children = array(); for ($i = 1; $i <= $this->threads; $i++) { $pid = pcntl_fork(); @@ -129,6 +131,34 @@ abstract class SpawningDaemon extends Daemon } /** + * Create an IPC socket pair which child processes can use to detect + * if the parent process has been killed. + */ + function initPipes() + { + $sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + if ($sockets) { + $this->parentWriter = $sockets[0]; + $this->parentReader = $sockets[1]; + } else { + $this->log(LOG_ERROR, "Couldn't create inter-process sockets"); + exit(1); + } + } + + /** + * Build an IOManager that simply ensures that we have a connection + * to the parent process open. If it breaks, the child process will + * die. + * + * @return ProcessManager + */ + public function processManager() + { + return new ProcessManager($this->parentReader); + } + + /** * Determine whether to respawn an exited subprocess based on its exit code. * Otherwise we'll respawn all exits by default. * @@ -152,6 +182,8 @@ abstract class SpawningDaemon extends Daemon */ protected function initAndRunChild($thread) { + // Close the writer end of our parent<->children pipe. + fclose($this->parentWriter); $this->set_id($this->get_id() . "." . $thread); $this->resetDb(); $exitCode = $this->runThread(); diff --git a/lib/statusnet.php b/lib/statusnet.php index afb8f5af0..776cfb579 100644 --- a/lib/statusnet.php +++ b/lib/statusnet.php @@ -341,11 +341,7 @@ class StatusNet // Backwards compatibility if (array_key_exists('memcached', $config)) { if ($config['memcached']['enabled']) { - if(class_exists('Memcached')) { - addPlugin('Memcached', array('servers' => $config['memcached']['server'])); - } else { - addPlugin('Memcache', array('servers' => $config['memcached']['server'])); - } + addPlugin('Memcache', array('servers' => $config['memcached']['server'])); } if (!empty($config['memcached']['base'])) { diff --git a/lib/subs.php b/lib/subs.php index e2ce0667e..165bbaa8f 100644 --- a/lib/subs.php +++ b/lib/subs.php @@ -43,46 +43,3 @@ function subs_unsubscribe_to($user, $other) return $e->getMessage(); } } - -function subs_unsubscribe_from($user, $other){ - $local = User::staticGet("nickname",$other); - if($local){ - return subs_unsubscribe_to($local,$user); - } else { - try { - $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 = $remote->id; - $sub->subscribed = $user->id; - - $sub->find(true); - - // note we checked for existence above - - if (!$sub->delete()) - return _('Couldn\'t delete subscription.'); - - $cache = common_memcache(); - - if ($cache) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $remote->id)); - } - - - $user->blowSubscribersCount(); - $remote->blowSubscribersCount(); - - Event::handle('EndUnsubscribe', array($remote, $user)); - } - } catch (Exception $e) { - return $e->getMessage(); - } - } -} - diff --git a/lib/usernoprofileexception.php b/lib/usernoprofileexception.php new file mode 100644 index 000000000..6744d2529 --- /dev/null +++ b/lib/usernoprofileexception.php @@ -0,0 +1,74 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * class for an exception when the user profile is missing + * + * 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 Exception + * @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); +} + +/** + * Class for an exception when the user profile is missing + * + * @category Exception + * @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 UserNoProfileException extends ServerException +{ + var $user = null; + + /** + * constructor + * + * @param User $user User that's missing a profile + */ + + public function __construct($user) + { + $this->user = $user; + + $message = sprintf(_("User %s (%d) has no profile record."), + $user->nickname, $user->id); + + parent::__construct($message); + } + + /** + * Accessor for user + * + * @return User the user that triggered this exception + */ + + public function getUser() + { + return $this->user; + } +} diff --git a/lib/userprofile.php b/lib/userprofile.php index 8464c2446..ca060842b 100644 --- a/lib/userprofile.php +++ b/lib/userprofile.php @@ -71,7 +71,8 @@ class UserProfile extends Widget { if (Event::handle('StartProfilePageProfileSection', array(&$this->out, $this->profile))) { - $this->out->elementStart('div', 'entity_profile vcard author'); + $this->out->elementStart('div', array('id' => 'i', + 'class' => 'entity_profile vcard author')); $this->out->element('h2', null, _('User profile')); if (Event::handle('StartProfilePageProfileElements', array(&$this->out, $this->profile))) { @@ -228,6 +229,17 @@ class UserProfile extends Widget function showEntityActions() { + if ($this->profile->hasRole(Profile_role::DELETED)) { + $this->out->elementStart('div', 'entity_actions'); + $this->out->element('h2', null, _('User actions')); + $this->out->elementStart('ul'); + $this->out->elementStart('p', array('class' => 'profile_deleted')); + $this->out->text(_('User deletion in progress...')); + $this->out->elementEnd('p'); + $this->out->elementEnd('ul'); + $this->out->elementEnd('div'); + return; + } if (Event::handle('StartProfilePageActionsSection', array(&$this->out, $this->profile))) { $cur = common_current_user(); diff --git a/lib/util.php b/lib/util.php index 6a8494275..0f639df38 100644 --- a/lib/util.php +++ b/lib/util.php @@ -52,17 +52,43 @@ function common_init_language() { mb_internal_encoding('UTF-8'); - // gettext seems very picky... We first need to setlocale() - // to a locale which _does_ exist on the system, and _then_ - // we can set in another locale that may not be set up - // (say, ga_ES for Galego/Galician) it seems to take it. - common_init_locale("en_US"); - // Note that this setlocale() call may "fail" but this is harmless; // gettext will still select the right language. $language = common_language(); $locale_set = common_init_locale($language); + if (!$locale_set) { + // The requested locale doesn't exist on the system. + // + // gettext seems very picky... We first need to setlocale() + // to a locale which _does_ exist on the system, and _then_ + // we can set in another locale that may not be set up + // (say, ga_ES for Galego/Galician) it seems to take it. + // + // For some reason C and POSIX which are guaranteed to work + // don't do the job. en_US.UTF-8 should be there most of the + // time, but not guaranteed. + $ok = common_init_locale("en_US"); + if (!$ok) { + // Try to find a complete, working locale... + // @fixme shelling out feels awfully inefficient + // but I don't think there's a more standard way. + $all = `locale -a`; + foreach (explode("\n", $all) as $locale) { + if (preg_match('/\.utf[-_]?8$/i', $locale)) { + $ok = setlocale(LC_ALL, $locale); + if ($ok) { + break; + } + } + } + if (!$ok) { + common_log(LOG_ERR, "Unable to find a UTF-8 locale on this system; UI translations may not work."); + } + } + $locale_set = common_init_locale($language); + } + setlocale(LC_CTYPE, 'C'); // So we do not have to make people install the gettext locales $path = common_config('site','locale_path'); @@ -133,6 +159,11 @@ function common_munge_password($password, $id) function common_check_user($nickname, $password) { + // empty nickname always unacceptable + if (empty($nickname)) { + return false; + } + $authenticatedUser = false; if (Event::handle('StartCheckPassword', array($nickname, $password, &$authenticatedUser))) { @@ -1453,7 +1484,15 @@ function common_copy_args($from) $to = array(); $strip = get_magic_quotes_gpc(); foreach ($from as $k => $v) { - $to[$k] = ($strip) ? stripslashes($v) : $v; + if($strip) { + if(is_array($v)) { + $to[$k] = common_copy_args($v); + } else { + $to[$k] = stripslashes($v); + } + } else { + $to[$k] = $v; + } } return $to; } |