summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/action.php53
-rw-r--r--lib/activity.php1134
-rw-r--r--lib/activitycontext.php121
-rw-r--r--lib/activityobject.php568
-rw-r--r--lib/activityutils.php265
-rw-r--r--lib/activityverb.php66
-rw-r--r--lib/adminpanelaction.php51
-rw-r--r--lib/apiaction.php71
-rw-r--r--lib/apiauth.php14
-rw-r--r--lib/atom10feed.php2
-rw-r--r--lib/atomcategory.php77
-rw-r--r--lib/authenticationplugin.php68
-rw-r--r--lib/authorizationplugin.php2
-rw-r--r--lib/avatarlink.php102
-rw-r--r--lib/command.php29
-rw-r--r--lib/commandinterpreter.php11
-rw-r--r--lib/common.php1
-rw-r--r--lib/default.php1
-rw-r--r--lib/deluserqueuehandler.php95
-rw-r--r--lib/htmloutputter.php55
-rw-r--r--lib/imagefile.php146
-rw-r--r--lib/iomaster.php2
-rw-r--r--lib/language.php1
-rw-r--r--lib/mail.php67
-rw-r--r--lib/mediafile.php2
-rw-r--r--lib/messageform.php2
-rw-r--r--lib/mysqlschema.php236
-rw-r--r--lib/noticeform.php2
-rw-r--r--lib/noticelist.php12
-rw-r--r--lib/pgsqlschema.php38
-rw-r--r--lib/poco.php240
-rw-r--r--lib/pocoaddress.php56
-rw-r--r--lib/pocourl.php65
-rw-r--r--lib/queuemanager.php9
-rw-r--r--lib/router.php35
-rw-r--r--lib/schema.php6
-rw-r--r--lib/servererroraction.php9
-rw-r--r--lib/subs.php2
-rw-r--r--lib/userprofile.php11
-rw-r--r--lib/util.php10
40 files changed, 2545 insertions, 1192 deletions
diff --git a/lib/action.php b/lib/action.php
index 9884f529c..491d7d481 100644
--- a/lib/action.php
+++ b/lib/action.php
@@ -426,39 +426,69 @@ class Action extends HTMLOutputter // lawsuit
$this->elementStart('ul', array('class' => 'nav'));
if (Event::handle('StartPrimaryNav', array($this))) {
if ($user) {
+ // TRANS: Tooltip for main menu option "Personal"
+ $tooltip = _m('TOOLTIP', 'Personal profile and friends timeline');
+ // TRANS: Main menu option when logged in for access to personal profile and friends timeline
$this->menuItem(common_local_url('all', array('nickname' => $user->nickname)),
- _('Home'), _('Personal profile and friends timeline'), false, 'nav_home');
+ _m('MENU', 'Personal'), $tooltip, false, 'nav_home');
+ // TRANS: Tooltip for main menu option "Account"
+ $tooltip = _m('TOOLTIP', 'Change your email, avatar, password, profile');
+ // TRANS: Main menu option when logged in for access to user settings
$this->menuItem(common_local_url('profilesettings'),
- _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account');
+ _('Account'), $tooltip, false, 'nav_account');
+ // TRANS: Tooltip for main menu option "Services"
+ $tooltip = _m('TOOLTIP', 'Connect to services');
+ // TRANS: Main menu option when logged in and connection are possible for access to options to connect to other services
$this->menuItem(common_local_url('oauthconnectionssettings'),
- _('Connect'), _('Connect to services'), false, 'nav_connect');
+ _('Connect'), $tooltip, false, 'nav_connect');
if ($user->hasRight(Right::CONFIGURESITE)) {
+ // TRANS: Tooltip for menu option "Admin"
+ $tooltip = _m('TOOLTIP', 'Change site configuration');
+ // TRANS: Main menu option when logged in and site admin for access to site configuration
$this->menuItem(common_local_url('siteadminpanel'),
- _('Admin'), _('Change site configuration'), false, 'nav_admin');
+ _m('MENU', 'Admin'), $tooltip, false, 'nav_admin');
}
if (common_config('invite', 'enabled')) {
+ // TRANS: Tooltip for main menu option "Invite"
+ $tooltip = _m('TOOLTIP', 'Invite friends and colleagues to join you on %s');
+ // TRANS: Main menu option when logged in and invitations are allowed for inviting new users
$this->menuItem(common_local_url('invite'),
- _('Invite'),
- sprintf(_('Invite friends and colleagues to join you on %s'),
+ _m('MENU', 'Invite'),
+ sprintf($tooltip,
common_config('site', 'name')),
false, 'nav_invitecontact');
}
+ // TRANS: Tooltip for main menu option "Logout"
+ $tooltip = _m('TOOLTIP', 'Logout from the site');
+ // TRANS: Main menu option when logged in to log out the current user
$this->menuItem(common_local_url('logout'),
- _('Logout'), _('Logout from the site'), false, 'nav_logout');
+ _m('MENU', 'Logout'), $tooltip, false, 'nav_logout');
}
else {
if (!common_config('site', 'closed')) {
+ // TRANS: Tooltip for main menu option "Register"
+ $tooltip = _m('TOOLTIP', 'Create an account');
+ // TRANS: Main menu option when not logged in to register a new account
$this->menuItem(common_local_url('register'),
- _('Register'), _('Create an account'), false, 'nav_register');
+ _m('MENU', 'Register'), $tooltip, false, 'nav_register');
}
+ // TRANS: Tooltip for main menu option "Login"
+ $tooltip = _m('TOOLTIP', 'Login to the site');
+ // TRANS: Main menu option when not logged in to log in
$this->menuItem(common_local_url('login'),
- _('Login'), _('Login to the site'), false, 'nav_login');
+ _m('MENU', 'Login'), $tooltip, false, 'nav_login');
}
+ // TRANS: Tooltip for main menu option "Help"
+ $tooltip = _m('TOOLTIP', 'Help me!');
+ // TRANS: Main menu option for help on the StatusNet site
$this->menuItem(common_local_url('doc', array('title' => 'help')),
- _('Help'), _('Help me!'), false, 'nav_help');
+ _m('MENU', 'Help'), $tooltip, false, 'nav_help');
if ($user || !common_config('site', 'private')) {
+ // TRANS: Tooltip for main menu option "Search"
+ $tooltip = _m('TOOLTIP', 'Search for people or text');
+ // TRANS: Main menu option when logged in or when the StatusNet instance is not private
$this->menuItem(common_local_url('peoplesearch'),
- _('Search'), _('Search for people or text'), false, 'nav_search');
+ _m('MENU', 'Search'), $tooltip, false, 'nav_search');
}
Event::handle('EndPrimaryNav', array($this));
}
@@ -479,6 +509,7 @@ class Action extends HTMLOutputter // lawsuit
if ($text) {
$this->elementStart('dl', array('id' => 'site_notice',
'class' => 'system_notice'));
+ // TRANS: DT element for site notice. String is hidden in default CSS.
$this->element('dt', null, _('Site notice'));
$this->elementStart('dd', null);
$this->raw($text);
diff --git a/lib/activity.php b/lib/activity.php
index 2cb80f9e1..f9192c6b8 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 -&lt; 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
*
@@ -1018,6 +53,7 @@ class Activity
{
const SPEC = 'http://activitystrea.ms/spec/1.0/';
const SCHEMA = 'http://activitystrea.ms/schema/1.0/';
+ const MEDIA = 'http://purl.org/syndication/atommedia';
const VERB = 'verb';
const OBJECT = 'object';
@@ -1033,9 +69,24 @@ 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
+ public $objects = array(); // an array of ActivityObjects
public $target; // an ActivityObject
public $context; // an ActivityObject
public $time; // Time of the activity
@@ -1063,21 +114,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)) {
@@ -1103,12 +162,15 @@ class Activity
// XXX: do other implied stuff here
}
- $objectEl = $this->_child($entry, self::OBJECT);
+ $objectEls = $entry->getElementsByTagNameNS(self::SPEC, self::OBJECT);
- if (!empty($objectEl)) {
- $this->object = new ActivityObject($objectEl);
+ if ($objectEls->length > 0) {
+ for ($i = 0; $i < $objectEls->length; $i++) {
+ $objectEl = $objectEls->item($i);
+ $this->objects[] = new ActivityObject($objectEl);
+ }
} else {
- $this->object = new ActivityObject($entry);
+ $this->objects[] = new ActivityObject($entry);
}
$actorEl = $this->_child($entry, self::ACTOR);
@@ -1163,6 +225,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->objects[] = new ActivityObject($item);
+ $this->context = new ActivityContext($item);
+ }
+
/**
* Returns an Atom <entry> based on this activity
*
@@ -1218,8 +343,10 @@ class Activity
$xs->element('activity:verb', null, $this->verb);
- if ($this->object) {
- $xs->raw($this->object->asString());
+ if (!empty($this->objects)) {
+ foreach($this->objects as $object) {
+ $xs->raw($object->asString());
+ }
}
if ($this->target) {
@@ -1241,48 +368,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..34d1b9170
--- /dev/null
+++ b/lib/activityobject.php
@@ -0,0 +1,568 @@
+<?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;
+
+ // @todo move this stuff to it's own PHOTO activity object
+ const MEDIA_DESCRIPTION = 'description';
+
+ public $thumbnail;
+ public $largerImage;
+ public $description;
+
+ /**
+ * 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);
+ }
+
+ if ($this->type == self::PHOTO) {
+
+ $this->thumbnail = ActivityUtils::getLink($element, 'preview');
+ $this->largerImage = ActivityUtils::getLink($element, 'enclosure');
+
+ $this->description = ActivityUtils::childContent(
+ $element,
+ ActivityObject::MEDIA_DESCRIPTION,
+ Activity::MEDIA
+ );
+
+ }
+ }
+
+ 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 -&lt; 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/adminpanelaction.php b/lib/adminpanelaction.php
index d43ea7698..a927e2333 100644
--- a/lib/adminpanelaction.php
+++ b/lib/adminpanelaction.php
@@ -69,6 +69,7 @@ class AdminPanelAction extends Action
// User must be logged in.
if (!common_logged_in()) {
+ // TRANS: Client error message
$this->clientError(_('Not logged in.'));
return false;
}
@@ -93,6 +94,7 @@ class AdminPanelAction extends Action
// User must have the right to change admin settings
if (!$user->hasRight(Right::CONFIGURESITE)) {
+ // TRANS: Client error message
$this->clientError(_('You cannot make changes to this site.'));
return false;
}
@@ -104,6 +106,7 @@ class AdminPanelAction extends Action
$name = mb_substr($name, 0, -10);
if (!self::canAdmin($name)) {
+ // TRANS: Client error message
$this->clientError(_('Changes to that panel are not allowed.'), 403);
return false;
}
@@ -134,6 +137,7 @@ class AdminPanelAction extends Action
Config::loadSettings();
$this->success = true;
+ // TRANS: Message after successful saving of administrative settings.
$this->msg = _('Settings saved.');
} catch (Exception $e) {
$this->success = false;
@@ -221,6 +225,7 @@ class AdminPanelAction extends Action
function showForm()
{
+ // TRANS: Client error message
$this->clientError(_('showForm() not implemented.'));
return;
}
@@ -250,6 +255,7 @@ class AdminPanelAction extends Action
function saveSettings()
{
+ // TRANS: Client error message
$this->clientError(_('saveSettings() not implemented.'));
return;
}
@@ -273,6 +279,7 @@ class AdminPanelAction extends Action
$result = $config->delete();
if (!$result) {
common_log_db_error($config, 'DELETE', __FILE__);
+ // TRANS: Client error message
$this->clientError(_("Unable to delete design setting."));
return null;
}
@@ -337,43 +344,67 @@ class AdminPanelNav extends Widget
if (Event::handle('StartAdminPanelNav', array($this))) {
if (AdminPanelAction::canAdmin('site')) {
- $this->out->menuItem(common_local_url('siteadminpanel'), _('Site'),
- _('Basic site configuration'), $action_name == 'siteadminpanel', 'nav_site_admin_panel');
+ // TRANS: Menu item title/tooltip
+ $menu_title = _('Basic site configuration');
+ // TRANS: Menu item for site administration
+ $this->out->menuItem(common_local_url('siteadminpanel'), _m('MENU', 'Site'),
+ $menu_title, $action_name == 'siteadminpanel', 'nav_site_admin_panel');
}
if (AdminPanelAction::canAdmin('design')) {
- $this->out->menuItem(common_local_url('designadminpanel'), _('Design'),
- _('Design configuration'), $action_name == 'designadminpanel', 'nav_design_admin_panel');
+ // TRANS: Menu item title/tooltip
+ $menu_title = _('Design configuration');
+ // TRANS: Menu item for site administration
+ $this->out->menuItem(common_local_url('designadminpanel'), _m('MENU', 'Design'),
+ $menu_title, $action_name == 'designadminpanel', 'nav_design_admin_panel');
}
if (AdminPanelAction::canAdmin('user')) {
+ // TRANS: Menu item title/tooltip
+ $menu_title = _('User configuration');
+ // TRANS: Menu item for site administration
$this->out->menuItem(common_local_url('useradminpanel'), _('User'),
- _('User configuration'), $action_name == 'useradminpanel', 'nav_user_admin_panel');
+ $menu_title, $action_name == 'useradminpanel', 'nav_user_admin_panel');
}
if (AdminPanelAction::canAdmin('access')) {
+ // TRANS: Menu item title/tooltip
+ $menu_title = _('Access configuration');
+ // TRANS: Menu item for site administration
$this->out->menuItem(common_local_url('accessadminpanel'), _('Access'),
- _('Access configuration'), $action_name == 'accessadminpanel', 'nav_access_admin_panel');
+ $menu_title, $action_name == 'accessadminpanel', 'nav_access_admin_panel');
}
if (AdminPanelAction::canAdmin('paths')) {
+ // TRANS: Menu item title/tooltip
+ $menu_title = _('Paths configuration');
+ // TRANS: Menu item for site administration
$this->out->menuItem(common_local_url('pathsadminpanel'), _('Paths'),
- _('Paths configuration'), $action_name == 'pathsadminpanel', 'nav_paths_admin_panel');
+ $menu_title, $action_name == 'pathsadminpanel', 'nav_paths_admin_panel');
}
if (AdminPanelAction::canAdmin('sessions')) {
+ // TRANS: Menu item title/tooltip
+ $menu_title = _('Sessions configuration');
+ // TRANS: Menu item for site administration
$this->out->menuItem(common_local_url('sessionsadminpanel'), _('Sessions'),
- _('Sessions configuration'), $action_name == 'sessionsadminpanel', 'nav_sessions_admin_panel');
+ $menu_title, $action_name == 'sessionsadminpanel', 'nav_sessions_admin_panel');
}
if (AdminPanelAction::canAdmin('sitenotice')) {
+ // TRANS: Menu item title/tooltip
+ $menu_title = _('Edit site notice');
+ // TRANS: Menu item for site administration
$this->out->menuItem(common_local_url('sitenoticeadminpanel'), _('Site notice'),
- _('Edit site notice'), $action_name == 'sitenoticeadminpanel', 'nav_sitenotice_admin_panel');
+ $menu_title, $action_name == 'sitenoticeadminpanel', 'nav_sitenotice_admin_panel');
}
if (AdminPanelAction::canAdmin('snapshot')) {
+ // TRANS: Menu item title/tooltip
+ $menu_title = _('Snapshots configuration');
+ // TRANS: Menu item for site administration
$this->out->menuItem(common_local_url('snapshotadminpanel'), _('Snapshots'),
- _('Snapshots configuration'), $action_name == 'snapshotadminpanel', 'nav_snapshot_admin_panel');
+ $menu_title, $action_name == 'snapshotadminpanel', 'nav_snapshot_admin_panel');
}
Event::handle('EndAdminPanelNav', array($this));
diff --git a/lib/apiaction.php b/lib/apiaction.php
index 5cedfaefe..9fc1a0779 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 32502399f..17f803a1c 100644
--- a/lib/apiauth.php
+++ b/lib/apiauth.php
@@ -294,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/authenticationplugin.php b/lib/authenticationplugin.php
index de479a576..0a3763e2e 100644
--- a/lib/authenticationplugin.php
+++ b/lib/authenticationplugin.php
@@ -69,13 +69,17 @@ abstract class AuthenticationPlugin extends Plugin
/**
* Automatically register a user when they attempt to login with valid credentials.
* User::register($data) is a very useful method for this implementation
- * @param username
+ * @param username username (that is used to login and find the user in the authentication provider) of the user to be registered
+ * @param nickname nickname of the user in the SN system. If nickname is null, then set nickname = username
* @return mixed instance of User, or false (if user couldn't be created)
*/
- function autoRegister($username)
+ function autoRegister($username, $nickname = null)
{
+ if(is_null($nickname)){
+ $nickname = $username;
+ }
$registration_data = array();
- $registration_data['nickname'] = $username ;
+ $registration_data['nickname'] = $nickname;
return User::register($registration_data);
}
@@ -92,6 +96,21 @@ abstract class AuthenticationPlugin extends Plugin
return false;
}
+ /**
+ * Given a username, suggest what the nickname should be
+ * Used during autoregistration
+ * Useful if your usernames are ugly, and you want to suggest
+ * nice looking nicknames when users initially sign on
+ * All nicknames returned by this function should be valid
+ * implementations may want to use common_nicknamize() to ensure validity
+ * @param username
+ * @return string nickname
+ */
+ function suggestNicknameForUsername($username)
+ {
+ return common_nicknamize($username);
+ }
+
//------------Below are the methods that connect StatusNet to the implementing Auth plugin------------\\
function onInitializePlugin(){
if(!isset($this->provider_name)){
@@ -108,10 +127,22 @@ abstract class AuthenticationPlugin extends Plugin
function onAutoRegister($nickname, $provider_name, &$user)
{
if($provider_name == $this->provider_name && $this->autoregistration){
- $user = $this->autoregister($nickname);
- if($user){
- User_username::register($user,$nickname,$this->provider_name);
- return false;
+ $suggested_nickname = $this->suggestNicknameForUsername($nickname);
+ $test_user = User::staticGet('nickname', $suggested_nickname);
+ if($test_user) {
+ //someone already exists with the suggested nickname, so used the passed nickname
+ $suggested_nickname = common_nicknamize($nickname);
+ }
+ $test_user = User::staticGet('nickname', $suggested_nickname);
+ if($test_user) {
+ //someone already exists with the suggested nickname
+ //not much else we can do
+ }else{
+ $user = $this->autoRegister($nickname, $suggested_nickname);
+ if($user){
+ User_username::register($user,$nickname,$this->provider_name);
+ return false;
+ }
}
}
}
@@ -122,23 +153,30 @@ abstract class AuthenticationPlugin extends Plugin
$user_username->username=$nickname;
$user_username->provider_name=$this->provider_name;
if($user_username->find() && $user_username->fetch()){
- $username = $user_username->username;
- $authenticated = $this->checkPassword($username, $password);
+ $authenticated = $this->checkPassword($user_username->username, $password);
if($authenticated){
$authenticatedUser = User::staticGet('id', $user_username->user_id);
return false;
}
}else{
- $user = User::staticGet('nickname', $nickname);
+ //$nickname is the username used to login
+ //$suggested_nickname is the nickname the auth provider suggests for that username
+ $suggested_nickname = $this->suggestNicknameForUsername($nickname);
+ $user = User::staticGet('nickname', $suggested_nickname);
if($user){
- //make sure a different provider isn't handling this nickname
+ //make sure this user isn't claimed
$user_username = new User_username();
- $user_username->username=$nickname;
- if(!$user_username->find()){
- //no other provider claims this username, so it's safe for us to handle it
+ $user_username->user_id=$user->id;
+ $we_can_handle = false;
+ if($user_username->find()){
+ //either this provider, or another one, has already claimed this user
+ //so we cannot. Let another plugin try.
+ return;
+ }else{
+ //no other provider claims this user, so it's safe for us to handle it
$authenticated = $this->checkPassword($nickname, $password);
if($authenticated){
- $authenticatedUser = User::staticGet('nickname', $nickname);
+ $authenticatedUser = $user;
User_username::register($authenticatedUser,$nickname,$this->provider_name);
return false;
}
diff --git a/lib/authorizationplugin.php b/lib/authorizationplugin.php
index 733b0c065..07da9b2d1 100644
--- a/lib/authorizationplugin.php
+++ b/lib/authorizationplugin.php
@@ -85,7 +85,7 @@ abstract class AuthorizationPlugin extends Plugin
}
function onStartSetApiUser(&$user) {
- return $this->onStartSetUser(&$user);
+ return $this->onStartSetUser($user);
}
function onStartHasRole($profile, $name, &$has_role) {
diff --git a/lib/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/command.php b/lib/command.php
index 8080fb8bc..216f9e649 100644
--- a/lib/command.php
+++ b/lib/command.php
@@ -711,6 +711,34 @@ class LoginCommand extends Command
}
}
+class LoseCommand extends Command
+{
+
+ var $other = null;
+
+ function __construct($user, $other)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+
+ function execute($channel)
+ {
+ if(!$this->other) {
+ $channel->error($this->user, _('Specify the name of the user to unsubscribe from'));
+ return;
+ }
+
+ $result = Subscription::cancel($this->getProfile($this->other), $this->user->getProfile());
+
+ if ($result) {
+ $channel->output($this->user, sprintf(_('Unsubscribed %s'), $this->other));
+ } else {
+ $channel->error($this->user, $result);
+ }
+ }
+}
+
class SubscriptionsCommand extends Command
{
function handle($channel)
@@ -793,6 +821,7 @@ class HelpCommand extends Command
"d <nickname> <text> - direct message to user\n".
"get <nickname> - get last notice from user\n".
"whois <nickname> - get profile info on user\n".
+ "lose <nickname> - force user to stop following you\n".
"fav <nickname> - add user's last notice as a 'fave'\n".
"fav #<notice_id> - add notice with the given id as a 'fave'\n".
"repeat #<notice_id> - repeat a notice with a given id\n".
diff --git a/lib/commandinterpreter.php b/lib/commandinterpreter.php
index c2add7299..fbc6174bb 100644
--- a/lib/commandinterpreter.php
+++ b/lib/commandinterpreter.php
@@ -47,6 +47,17 @@ class CommandInterpreter
} else {
return new LoginCommand($user);
}
+ case 'lose':
+ if ($arg) {
+ list($other, $extra) = $this->split_arg($arg);
+ if ($extra) {
+ return null;
+ } else {
+ return new LoseCommand($user, $other);
+ }
+ } else {
+ return null;
+ }
case 'subscribers':
if ($arg) {
return null;
diff --git a/lib/common.php b/lib/common.php
index 5d53270e3..334a88ffd 100644
--- a/lib/common.php
+++ b/lib/common.php
@@ -123,7 +123,6 @@ require_once INSTALLDIR.'/lib/util.php';
require_once INSTALLDIR.'/lib/action.php';
require_once INSTALLDIR.'/lib/mail.php';
require_once INSTALLDIR.'/lib/subs.php';
-require_once INSTALLDIR.'/lib/activity.php';
require_once INSTALLDIR.'/lib/clientexception.php';
require_once INSTALLDIR.'/lib/serverexception.php';
diff --git a/lib/default.php b/lib/default.php
index 46d3d4774..10f3f1a97 100644
--- a/lib/default.php
+++ b/lib/default.php
@@ -282,6 +282,7 @@ $default =
'Mapstraction' => null,
'OStatus' => null,
'WikiHashtags' => null,
+ 'RSSCloud' => null,
'OpenID' => null),
),
'admin' =>
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/htmloutputter.php b/lib/htmloutputter.php
index 7315fe2ad..7786b5941 100644
--- a/lib/htmloutputter.php
+++ b/lib/htmloutputter.php
@@ -356,40 +356,47 @@ class HTMLOutputter extends XMLOutputter
if( empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment']))
{
- $path = common_config('javascript', 'path');
+ if (strpos($src, 'plugins/') === 0 || strpos($src, 'local/') === 0) {
- if (empty($path)) {
- $path = common_config('site', 'path') . '/js/';
- }
+ $src = common_path($src) . '?version=' . STATUSNET_VERSION;
- if ($path[strlen($path)-1] != '/') {
- $path .= '/';
- }
+ }else{
- if ($path[0] != '/') {
- $path = '/'.$path;
- }
+ $path = common_config('javascript', 'path');
- $server = common_config('javascript', 'server');
+ if (empty($path)) {
+ $path = common_config('site', 'path') . '/js/';
+ }
- if (empty($server)) {
- $server = common_config('site', 'server');
- }
+ if ($path[strlen($path)-1] != '/') {
+ $path .= '/';
+ }
- $ssl = common_config('javascript', 'ssl');
+ if ($path[0] != '/') {
+ $path = '/'.$path;
+ }
+
+ $server = common_config('javascript', 'server');
- if (is_null($ssl)) { // null -> guess
- if (common_config('site', 'ssl') == 'always' &&
- !common_config('javascript', 'server')) {
- $ssl = true;
- } else {
- $ssl = false;
+ if (empty($server)) {
+ $server = common_config('site', 'server');
}
- }
- $protocol = ($ssl) ? 'https' : 'http';
+ $ssl = common_config('javascript', 'ssl');
- $src = $protocol.'://'.$server.$path.$src . '?version=' . STATUSNET_VERSION;
+ if (is_null($ssl)) { // null -> guess
+ if (common_config('site', 'ssl') == 'always' &&
+ !common_config('javascript', 'server')) {
+ $ssl = true;
+ } else {
+ $ssl = false;
+ }
+ }
+
+ $protocol = ($ssl) ? 'https' : 'http';
+
+ $src = $protocol.'://'.$server.$path.$src . '?version=' . STATUSNET_VERSION;
+ }
}
$this->element('script', array('type' => $type,
diff --git a/lib/imagefile.php b/lib/imagefile.php
index 6bc8e599b..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,15 +110,6 @@ class ImageFile
return;
}
- if ($info[2] !== IMAGETYPE_GIF &&
- $info[2] !== IMAGETYPE_JPEG &&
- $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']);
}
@@ -146,6 +150,15 @@ class ImageFile
case IMAGETYPE_PNG:
$image_src = imagecreatefrompng($this->filepath);
break;
+ case IMAGETYPE_BMP:
+ $image_src = imagecreatefrombmp($this->filepath);
+ break;
+ case IMAGETYPE_WBMP:
+ $image_src = imagecreatefromwbmp($this->filepath);
+ break;
+ case IMAGETYPE_XBM:
+ $image_src = imagecreatefromxbm($this->filepath);
+ break;
default:
throw new Exception(_('Unknown file type'));
return;
@@ -153,7 +166,7 @@ class ImageFile
$image_dest = imagecreatetruecolor($size, $size);
- if ($this->type == IMAGETYPE_GIF || $this->type == IMAGETYPE_PNG) {
+ if ($this->type == IMAGETYPE_GIF || $this->type == IMAGETYPE_PNG || $this->type == IMAGETYPE_BMP) {
$transparent_idx = imagecolortransparent($image_src);
@@ -176,6 +189,20 @@ class ImageFile
imagecopyresampled($image_dest, $image_src, 0, 0, $x, $y, $size, $size, $w, $h);
+ if($this->type == IMAGETYPE_BMP) {
+ //we don't want to save BMP... it's an inefficient, rare, antiquated format
+ //save png instead
+ $this->type = IMAGETYPE_PNG;
+ } else if($this->type == IMAGETYPE_WBMP) {
+ //we don't want to save WBMP... it's a rare format that we can't guarantee clients will support
+ //save png instead
+ $this->type = IMAGETYPE_PNG;
+ } else if($this->type == IMAGETYPE_XBM) {
+ //we don't want to save XBM... it's a rare format that we can't guarantee clients will support
+ //save png instead
+ $this->type = IMAGETYPE_PNG;
+ }
+
$outname = Avatar::filename($this->id,
image_type_to_extension($this->type),
$size,
@@ -245,4 +272,101 @@ class ImageFile
return $num;
}
-} \ No newline at end of file
+}
+
+//PHP doesn't (as of 2/24/2010) have an imagecreatefrombmp so conditionally define one
+if(!function_exists('imagecreatefrombmp')){
+ //taken shamelessly from http://www.php.net/manual/en/function.imagecreatefromwbmp.php#86214
+ function imagecreatefrombmp($p_sFile)
+ {
+ // Load the image into a string
+ $file = fopen($p_sFile,"rb");
+ $read = fread($file,10);
+ while(!feof($file)&&($read<>""))
+ $read .= fread($file,1024);
+
+ $temp = unpack("H*",$read);
+ $hex = $temp[1];
+ $header = substr($hex,0,108);
+
+ // Process the header
+ // Structure: http://www.fastgraph.com/help/bmp_header_format.html
+ if (substr($header,0,4)=="424d")
+ {
+ // Cut it in parts of 2 bytes
+ $header_parts = str_split($header,2);
+
+ // Get the width 4 bytes
+ $width = hexdec($header_parts[19].$header_parts[18]);
+
+ // Get the height 4 bytes
+ $height = hexdec($header_parts[23].$header_parts[22]);
+
+ // Unset the header params
+ unset($header_parts);
+ }
+
+ // Define starting X and Y
+ $x = 0;
+ $y = 1;
+
+ // Create newimage
+ $image = imagecreatetruecolor($width,$height);
+
+ // Grab the body from the image
+ $body = substr($hex,108);
+
+ // Calculate if padding at the end-line is needed
+ // Divided by two to keep overview.
+ // 1 byte = 2 HEX-chars
+ $body_size = (strlen($body)/2);
+ $header_size = ($width*$height);
+
+ // Use end-line padding? Only when needed
+ $usePadding = ($body_size>($header_size*3)+4);
+
+ // Using a for-loop with index-calculation instaid of str_split to avoid large memory consumption
+ // Calculate the next DWORD-position in the body
+ for ($i=0;$i<$body_size;$i+=3)
+ {
+ // Calculate line-ending and padding
+ if ($x>=$width)
+ {
+ // If padding needed, ignore image-padding
+ // Shift i to the ending of the current 32-bit-block
+ if ($usePadding)
+ $i += $width%4;
+
+ // Reset horizontal position
+ $x = 0;
+
+ // Raise the height-position (bottom-up)
+ $y++;
+
+ // Reached the image-height? Break the for-loop
+ if ($y>$height)
+ break;
+ }
+
+ // Calculation of the RGB-pixel (defined as BGR in image-data)
+ // Define $i_pos as absolute position in the body
+ $i_pos = $i*2;
+ $r = hexdec($body[$i_pos+4].$body[$i_pos+5]);
+ $g = hexdec($body[$i_pos+2].$body[$i_pos+3]);
+ $b = hexdec($body[$i_pos].$body[$i_pos+1]);
+
+ // Calculate and draw the pixel
+ $color = imagecolorallocate($image,$r,$g,$b);
+ imagesetpixel($image,$x,$height-$y,$color);
+
+ // Raise the horizontal position
+ $x++;
+ }
+
+ // Unset the body / free the memory
+ unset($body);
+
+ // Return image-object
+ return $image;
+ }
+}
diff --git a/lib/iomaster.php b/lib/iomaster.php
index d20837ba5..7cfb2c9a0 100644
--- a/lib/iomaster.php
+++ b/lib/iomaster.php
@@ -330,7 +330,7 @@ abstract class IoMaster
* for per-queue and per-site records.
*
* @param string $key counter name
- * @param array $owners list of owner keys like 'queue:jabber' or 'site:stat01'
+ * @param array $owners list of owner keys like 'queue:xmpp' or 'site:stat01'
*/
public function stats($key, $owners=array())
{
diff --git a/lib/language.php b/lib/language.php
index f5ee7fac5..64b59e739 100644
--- a/lib/language.php
+++ b/lib/language.php
@@ -289,6 +289,7 @@ function get_all_languages() {
'ar' => array('q' => 0.8, 'lang' => 'ar', 'name' => 'Arabic', 'direction' => 'rtl'),
'arz' => array('q' => 0.8, 'lang' => 'arz', 'name' => 'Egyptian Spoken Arabic', 'direction' => 'rtl'),
'bg' => array('q' => 0.8, 'lang' => 'bg', 'name' => 'Bulgarian', 'direction' => 'ltr'),
+ 'br' => array('q' => 0.8, 'lang' => 'br', 'name' => 'Breton', 'direction' => 'ltr'),
'ca' => array('q' => 0.5, 'lang' => 'ca', 'name' => 'Catalan', 'direction' => 'ltr'),
'cs' => array('q' => 0.5, 'lang' => 'cs', 'name' => 'Czech', 'direction' => 'ltr'),
'de' => array('q' => 0.8, 'lang' => 'de', 'name' => 'German', 'direction' => 'ltr'),
diff --git a/lib/mail.php b/lib/mail.php
index c724764cc..807b6a363 100644
--- a/lib/mail.php
+++ b/lib/mail.php
@@ -133,12 +133,13 @@ function mail_notify_from()
* @param User &$user user to send email to
* @param string $subject subject of the email
* @param string $body body of the email
+ * @param array $headers optional list of email headers
* @param string $address optional specification of email address
*
* @return boolean success flag
*/
-function mail_to_user(&$user, $subject, $body, $address=null)
+function mail_to_user(&$user, $subject, $body, $headers=array(), $address=null)
{
if (!$address) {
$address = $user->email;
@@ -180,7 +181,9 @@ function mail_confirm_address($user, $code, $nickname, $address)
$nickname, common_config('site', 'name'),
common_local_url('confirmaddress', array('code' => $code)),
common_config('site', 'name'));
- return mail_to_user($user, $subject, $body, $address);
+ $headers = array();
+
+ return mail_to_user($user, $subject, $body, $headers, $address);
}
/**
@@ -231,6 +234,7 @@ function mail_subscribe_notify_profile($listenee, $other)
$recipients = $listenee->email;
+ $headers = _mail_prepare_headers('subscribe', $listenee->nickname, $other->nickname);
$headers['From'] = mail_notify_from();
$headers['To'] = $name . ' <' . $listenee->email . '>';
$headers['Subject'] = sprintf(_('%1$s is now listening to '.
@@ -476,7 +480,10 @@ function mail_notify_nudge($from, $to)
common_local_url('all', array('nickname' => $to->nickname)),
common_config('site', 'name'));
common_init_locale();
- return mail_to_user($to, $subject, $body);
+
+ $headers = _mail_prepare_headers('nudge', $to->nickname, $from->nickname);
+
+ return mail_to_user($to, $subject, $body, $headers);
}
/**
@@ -526,8 +533,10 @@ function mail_notify_message($message, $from=null, $to=null)
common_local_url('newmessage', array('to' => $from->id)),
common_config('site', 'name'));
+ $headers = _mail_prepare_headers('message', $to->nickname, $from->nickname);
+
common_init_locale();
- return mail_to_user($to, $subject, $body);
+ return mail_to_user($to, $subject, $body, $headers);
}
/**
@@ -578,8 +587,10 @@ function mail_notify_fave($other, $user, $notice)
common_config('site', 'name'),
$user->nickname);
+ $headers = _mail_prepare_headers('fave', $other->nickname, $user->nickname);
+
common_init_locale();
- mail_to_user($other, $subject, $body);
+ mail_to_user($other, $subject, $body, $headers);
}
/**
@@ -611,19 +622,19 @@ function mail_notify_attn($user, $notice)
common_init_locale($user->language);
- if ($notice->conversation != $notice->id) {
- $conversationEmailText = "The full conversation can be read here:\n\n".
- "\t%5\$s\n\n ";
- $conversationUrl = common_local_url('conversation',
+ if ($notice->conversation != $notice->id) {
+ $conversationEmailText = "The full conversation can be read here:\n\n".
+ "\t%5\$s\n\n ";
+ $conversationUrl = common_local_url('conversation',
array('id' => $notice->conversation)).'#notice-'.$notice->id;
- } else {
- $conversationEmailText = "%5\$s";
- $conversationUrl = null;
- }
+ } else {
+ $conversationEmailText = "%5\$s";
+ $conversationUrl = null;
+ }
$subject = sprintf(_('%s (@%s) sent a notice to your attention'), $bestname, $sender->nickname);
- $body = sprintf(_("%1\$s (@%9\$s) just sent a notice to your attention (an '@-reply') on %2\$s.\n\n".
+ $body = sprintf(_("%1\$s (@%9\$s) just sent a notice to your attention (an '@-reply') on %2\$s.\n\n".
"The notice is here:\n\n".
"\t%3\$s\n\n" .
"It reads:\n\n".
@@ -641,7 +652,7 @@ function mail_notify_attn($user, $notice)
common_local_url('shownotice',
array('notice' => $notice->id)),//%3
$notice->content,//%4
- $conversationUrl,//%5
+ $conversationUrl,//%5
common_local_url('newnotice',
array('replyto' => $sender->nickname, 'inreplyto' => $notice->id)),//%6
common_local_url('replies',
@@ -649,6 +660,30 @@ function mail_notify_attn($user, $notice)
common_local_url('emailsettings'), //%8
$sender->nickname); //%9
+ $headers = _mail_prepare_headers('mention', $user->nickname, $sender->nickname);
+
common_init_locale();
- mail_to_user($user, $subject, $body);
+ mail_to_user($user, $subject, $body, $headers);
}
+
+/**
+ * Prepare the common mail headers used in notification emails
+ *
+ * @param string $msg_type type of message being sent to the user
+ * @param string $to nickname of the receipient
+ * @param string $from nickname of the user triggering the notification
+ *
+ * @return array list of mail headers to include in the message
+ */
+function _mail_prepare_headers($msg_type, $to, $from)
+{
+ $headers = array(
+ 'X-StatusNet-MessageType' => $msg_type,
+ 'X-StatusNet-TargetUser' => $to,
+ 'X-StatusNet-SourceUser' => $from,
+ 'X-StatusNet-Domain' => common_config('site', 'server')
+ );
+
+ return $headers;
+}
+
diff --git a/lib/mediafile.php b/lib/mediafile.php
index e3d5b1dbc..10d90d008 100644
--- a/lib/mediafile.php
+++ b/lib/mediafile.php
@@ -262,7 +262,7 @@ class MediaFile
$filetype = MIME_Type::autoDetect($stream['uri']);
}
- if (in_array($filetype, common_config('attachments', 'supported'))) {
+ if (common_config('attachments', 'supported') === true || in_array($filetype, common_config('attachments', 'supported'))) {
return $filetype;
}
$media = MIME_Type::getMedia($filetype);
diff --git a/lib/messageform.php b/lib/messageform.php
index 0c568e1bd..b116964da 100644
--- a/lib/messageform.php
+++ b/lib/messageform.php
@@ -175,6 +175,6 @@ class MessageForm extends Form
'class' => 'submit',
'name' => 'message_send',
'type' => 'submit',
- 'value' => _('Send')));
+ 'value' => _m('Send button for sending notice', 'Send')));
}
}
diff --git a/lib/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/noticeform.php b/lib/noticeform.php
index 62df5c941..7278c41a9 100644
--- a/lib/noticeform.php
+++ b/lib/noticeform.php
@@ -233,6 +233,6 @@ class NoticeForm extends Form
'class' => 'submit',
'name' => 'status_submit',
'type' => 'submit',
- 'value' => _('Send')));
+ 'value' => _m('Send button for sending notice', 'Send')));
}
}
diff --git a/lib/noticelist.php b/lib/noticelist.php
index 88a925241..811b7e4f1 100644
--- a/lib/noticelist.php
+++ b/lib/noticelist.php
@@ -442,11 +442,13 @@ 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));
+ $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/queuemanager.php b/lib/queuemanager.php
index 9fdc80110..0829c8a8b 100644
--- a/lib/queuemanager.php
+++ b/lib/queuemanager.php
@@ -213,7 +213,9 @@ abstract class QueueManager extends IoManager
{
if (isset($this->handlers[$queue])) {
$class = $this->handlers[$queue];
- if (class_exists($class)) {
+ if(is_object($class)) {
+ return $class;
+ } else if (class_exists($class)) {
return new $class();
} else {
$this->_log(LOG_ERR, "Nonexistent handler class '$class' for queue '$queue'");
@@ -262,6 +264,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');
@@ -286,7 +291,7 @@ abstract class QueueManager extends IoManager
* Only registered transports will be reliably picked up!
*
* @param string $transport
- * @param string $class
+ * @param string $class class name or object instance
* @param string $group
*/
public function connect($transport, $class, $group='main')
diff --git a/lib/router.php b/lib/router.php
index 706120e0b..a9d07276f 100644
--- a/lib/router.php
+++ b/lib/router.php
@@ -33,6 +33,33 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
require_once 'Net/URL/Mapper.php';
+class StatusNet_URL_Mapper extends Net_URL_Mapper {
+
+ private static $_singleton = null;
+
+ private function __construct()
+ {
+ }
+
+ public static function getInstance($id = '__default__')
+ {
+ if (empty(self::$_singleton)) {
+ self::$_singleton = new StatusNet_URL_Mapper();
+ }
+ return self::$_singleton;
+ }
+
+ public function connect($path, $defaults = array(), $rules = array())
+ {
+ $result = null;
+ if (Event::handle('StartConnectPath', array(&$path, &$defaults, &$rules, &$result))) {
+ $result = parent::connect($path, $defaults, $rules);
+ Event::handle('EndConnectPath', array($path, $defaults, $rules, $result));
+ }
+ return $result;
+ }
+}
+
/**
* URL Router
*
@@ -69,7 +96,7 @@ class Router
function initialize()
{
- $m = Net_URL_Mapper::getInstance();
+ $m = StatusNet_URL_Mapper::getInstance();
if (Event::handle('StartInitializeRouter', array(&$m))) {
@@ -628,6 +655,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/subs.php b/lib/subs.php
index 1c240c475..165bbaa8f 100644
--- a/lib/subs.php
+++ b/lib/subs.php
@@ -42,4 +42,4 @@ function subs_unsubscribe_to($user, $other)
} catch (Exception $e) {
return $e->getMessage();
}
-} \ No newline at end of file
+}
diff --git a/lib/userprofile.php b/lib/userprofile.php
index 8464c2446..2c3b1ea45 100644
--- a/lib/userprofile.php
+++ b/lib/userprofile.php
@@ -228,6 +228,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 3d4ed087f..795997868 100644
--- a/lib/util.php
+++ b/lib/util.php
@@ -1493,7 +1493,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;
}