summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/accountsettingsaction.php30
-rw-r--r--lib/action.php146
-rw-r--r--lib/activity.php17
-rw-r--r--lib/activitycontext.php9
-rw-r--r--lib/activityutils.php18
-rw-r--r--lib/adminpanelaction.php14
-rw-r--r--lib/apiaction.php379
-rw-r--r--lib/apiauth.php65
-rw-r--r--lib/apibareauth.php3
-rw-r--r--lib/apiprivateauth.php1
-rw-r--r--lib/applicationeditform.php41
-rw-r--r--lib/applicationlist.php17
-rw-r--r--lib/atomgroupnoticefeed.php27
-rw-r--r--lib/atomnoticefeed.php33
-rw-r--r--lib/atomusernoticefeed.php8
-rw-r--r--lib/attachmentlist.php3
-rw-r--r--lib/authenticationplugin.php1
-rw-r--r--lib/authorizationplugin.php5
-rw-r--r--lib/avatarlink.php2
-rw-r--r--lib/command.php155
-rw-r--r--lib/common.php4
-rw-r--r--lib/dbqueuemanager.php4
-rw-r--r--lib/default.php20
-rw-r--r--lib/htmloutputter.php1
-rw-r--r--lib/httpclient.php48
-rw-r--r--lib/installer.php585
-rw-r--r--lib/jabber.php179
-rw-r--r--lib/language.php37
-rw-r--r--lib/liberalstomp.php27
-rw-r--r--lib/mail.php75
-rw-r--r--lib/mailbox.php1
-rw-r--r--lib/mailhandler.php4
-rw-r--r--lib/mediafile.php56
-rw-r--r--lib/mysqlschema.php17
-rw-r--r--lib/noticeform.php7
-rw-r--r--lib/noticelist.php118
-rw-r--r--lib/pgsqlschema.php55
-rw-r--r--lib/ping.php10
-rw-r--r--lib/plugin.php1
-rw-r--r--lib/popularnoticesection.php2
-rw-r--r--lib/profileaction.php12
-rw-r--r--lib/profileformaction.php38
-rw-r--r--lib/redirectingaction.php97
-rw-r--r--lib/router.php27
-rw-r--r--lib/rssaction.php10
-rw-r--r--lib/schema.php37
-rw-r--r--lib/statusnet.php2
-rw-r--r--lib/stompqueuemanager.php52
-rw-r--r--lib/theme.php144
-rw-r--r--lib/themeuploader.php336
-rw-r--r--lib/usernoprofileexception.php4
-rw-r--r--lib/util.php146
-rw-r--r--lib/xmppmanager.php4
-rw-r--r--lib/xrdsoutputter.php1
54 files changed, 2573 insertions, 562 deletions
diff --git a/lib/accountsettingsaction.php b/lib/accountsettingsaction.php
index c79a1f5d7..57740f8b8 100644
--- a/lib/accountsettingsaction.php
+++ b/lib/accountsettingsaction.php
@@ -105,27 +105,45 @@ class AccountSettingsNav extends Widget
$user = common_current_user();
if(Event::handle('StartAccountSettingsProfileMenuItem', array($this, &$menu))){
- $this->showMenuItem('profilesettings',_('Profile'),_('Change your profile settings'));
+ // TRANS: Link title attribute in user account settings menu.
+ $title = _('Change your profile settings');
+ // TRANS: Link description in user account settings menu.
+ $this->showMenuItem('profilesettings',_('Profile'),$title);
Event::handle('EndAccountSettingsProfileMenuItem', array($this, &$menu));
}
if(Event::handle('StartAccountSettingsAvatarMenuItem', array($this, &$menu))){
- $this->showMenuItem('avatarsettings',_('Avatar'),_('Upload an avatar'));
+ // TRANS: Link title attribute in user account settings menu.
+ $title = _('Upload an avatar');
+ // TRANS: Link description in user account settings menu.
+ $this->showMenuItem('avatarsettings',_('Avatar'),$title);
Event::handle('EndAccountSettingsAvatarMenuItem', array($this, &$menu));
}
if(Event::handle('StartAccountSettingsPasswordMenuItem', array($this, &$menu))){
- $this->showMenuItem('passwordsettings',_('Password'),_('Change your password'));
+ // TRANS: Link title attribute in user account settings menu.
+ $title = _('Change your password');
+ // TRANS: Link description in user account settings menu.
+ $this->showMenuItem('passwordsettings',_('Password'),$title);
Event::handle('EndAccountSettingsPasswordMenuItem', array($this, &$menu));
}
if(Event::handle('StartAccountSettingsEmailMenuItem', array($this, &$menu))){
- $this->showMenuItem('emailsettings',_('Email'),_('Change email handling'));
+ // TRANS: Link title attribute in user account settings menu.
+ $title = _('Change email handling');
+ // TRANS: Link description in user account settings menu.
+ $this->showMenuItem('emailsettings',_('Email'),$title);
Event::handle('EndAccountSettingsEmailMenuItem', array($this, &$menu));
}
if(Event::handle('StartAccountSettingsDesignMenuItem', array($this, &$menu))){
- $this->showMenuItem('userdesignsettings',_('Design'),_('Design your profile'));
+ // TRANS: Link title attribute in user account settings menu.
+ $title = _('Design your profile');
+ // TRANS: Link description in user account settings menu.
+ $this->showMenuItem('userdesignsettings',_('Design'),$title);
Event::handle('EndAccountSettingsDesignMenuItem', array($this, &$menu));
}
if(Event::handle('StartAccountSettingsOtherMenuItem', array($this, &$menu))){
- $this->showMenuItem('othersettings',_('Other'),_('Other options'));
+ // TRANS: Link title attribute in user account settings menu.
+ $title = _('Other options');
+ // TRANS: Link description in user account settings menu.
+ $this->showMenuItem('othersettings',_('Other'),$title);
Event::handle('EndAccountSettingsOtherMenuItem', array($this, &$menu));
}
diff --git a/lib/action.php b/lib/action.php
index c4d9fd5cb..a2638993f 100644
--- a/lib/action.php
+++ b/lib/action.php
@@ -121,7 +121,10 @@ class Action extends HTMLOutputter // lawsuit
// XXX: attributes (profile?)
$this->elementStart('head');
if (Event::handle('StartShowHeadElements', array($this))) {
- $this->showTitle();
+ if (Event::handle('StartShowHeadTitle', array($this))) {
+ $this->showTitle();
+ Event::handle('EndShowHeadTitle', array($this));
+ }
$this->showShortcutIcon();
$this->showStylesheets();
$this->showOpenSearch();
@@ -141,6 +144,7 @@ class Action extends HTMLOutputter // lawsuit
function showTitle()
{
$this->element('title', null,
+ // TRANS: Page title. %1$s is the title, %2$s is the site name.
sprintf(_("%1\$s - %2\$s"),
$this->title(),
common_config('site', 'name')));
@@ -156,6 +160,7 @@ class Action extends HTMLOutputter // lawsuit
function title()
{
+ // TRANS: Page title for a page without a title set.
return _("Untitled page");
}
@@ -198,7 +203,7 @@ class Action extends HTMLOutputter // lawsuit
if (Event::handle('StartShowStatusNetStyles', array($this)) &&
Event::handle('StartShowLaconicaStyles', array($this))) {
- $this->cssLink('css/display.css',null, 'screen, projection, tv, print');
+ $this->primaryCssLink(null, 'screen, projection, tv, print');
Event::handle('EndShowStatusNetStyles', array($this));
Event::handle('EndShowLaconicaStyles', array($this));
}
@@ -233,7 +238,29 @@ class Action extends HTMLOutputter // lawsuit
Event::handle('EndShowDesign', array($this));
}
Event::handle('EndShowStyles', array($this));
+
+ if (common_config('custom_css', 'enabled')) {
+ $css = common_config('custom_css', 'css');
+ if (Event::handle('StartShowCustomCss', array($this, &$css))) {
+ if (trim($css) != '') {
+ $this->style($css);
+ }
+ Event::handle('EndShowCustomCss', array($this));
+ }
+ }
+ }
+ }
+
+ function primaryCssLink($mainTheme=null, $media=null)
+ {
+ // If the currently-selected theme has dependencies on other themes,
+ // we'll need to load their display.css files as well in order.
+ $theme = new Theme($mainTheme);
+ $baseThemes = $theme->getDeps();
+ foreach ($baseThemes as $baseTheme) {
+ $this->cssLink('css/display.css', $baseTheme, $media);
}
+ $this->cssLink('css/display.css', $mainTheme, $media);
}
/**
@@ -254,9 +281,7 @@ class Action extends HTMLOutputter // lawsuit
}
if (Event::handle('StartShowStatusNetScripts', array($this)) &&
Event::handle('StartShowLaconicaScripts', array($this))) {
- $this->script('xbImportNode.js');
$this->script('util.js');
- $this->script('geometa.js');
// Frame-busting code to avoid clickjacking attacks.
$this->inlineScript('if (window.top !== window.self) { window.top.location.href = window.self.location.href; }');
Event::handle('EndShowStatusNetScripts', array($this));
@@ -420,6 +445,7 @@ class Action extends HTMLOutputter // lawsuit
{
$user = common_current_user();
$this->elementStart('dl', array('id' => 'site_nav_global_primary'));
+ // TRANS: DT element for primary navigation menu. String is hidden in default CSS.
$this->element('dt', null, _('Primary site navigation'));
$this->elementStart('dd');
$this->elementStart('ul', array('class' => 'nav'));
@@ -427,31 +453,31 @@ class Action extends HTMLOutputter // lawsuit
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)),
+ // TRANS: Main menu option when logged in for access to personal profile and friends timeline
_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'),
+ // TRANS: Main menu option when logged in for access to user settings
_('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'),
+ // TRANS: Main menu option when logged in and connection are possible for access to options to connect to other services
_('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'),
+ // TRANS: Main menu option when logged in and site admin for access to site configuration
_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'),
+ // TRANS: Main menu option when logged in and invitations are allowed for inviting new users
_m('MENU', 'Invite'),
sprintf($tooltip,
common_config('site', 'name')),
@@ -459,16 +485,16 @@ class Action extends HTMLOutputter // lawsuit
}
// 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'),
+ // TRANS: Main menu option when logged in to log out the current user
_m('MENU', 'Logout'), $tooltip, false, 'nav_logout');
}
else {
if (!common_config('site', 'closed') && !common_config('site', 'inviteonly')) {
// 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'),
+ // TRANS: Main menu option when not logged in to register a new account
_m('MENU', 'Register'), $tooltip, false, 'nav_register');
}
// TRANS: Tooltip for main menu option "Login"
@@ -575,6 +601,7 @@ class Action extends HTMLOutputter // lawsuit
function showLocalNavBlock()
{
$this->elementStart('dl', array('id' => 'site_nav_local_views'));
+ // TRANS: DT element for local views block. String is hidden in default CSS.
$this->element('dt', null, _('Local views'));
$this->elementStart('dd');
$this->showLocalNav();
@@ -602,7 +629,10 @@ class Action extends HTMLOutputter // lawsuit
function showContentBlock()
{
$this->elementStart('div', array('id' => 'content'));
- $this->showPageTitle();
+ if (Event::handle('StartShowPageTitle', array($this))) {
+ $this->showPageTitle();
+ Event::handle('EndShowPageTitle', array($this));
+ }
$this->showPageNoticeBlock();
$this->elementStart('div', array('id' => 'content_inner'));
// show the actual content (forms, lists, whatever)
@@ -641,6 +671,7 @@ class Action extends HTMLOutputter // lawsuit
$this->elementStart('dl', array('id' => 'page_notice',
'class' => 'system_notice'));
+ // TRANS: DT element for page notice. String is hidden in default CSS.
$this->element('dt', null, _('Page notice'));
$this->elementStart('dd');
if (Event::handle('StartShowPageNotice', array($this))) {
@@ -743,28 +774,37 @@ class Action extends HTMLOutputter // lawsuit
function showSecondaryNav()
{
$this->elementStart('dl', array('id' => 'site_nav_global_secondary'));
+ // TRANS: DT element for secondary navigation menu. String is hidden in default CSS.
$this->element('dt', null, _('Secondary site navigation'));
$this->elementStart('dd', null);
$this->elementStart('ul', array('class' => 'nav'));
if (Event::handle('StartSecondaryNav', array($this))) {
$this->menuItem(common_local_url('doc', array('title' => 'help')),
+ // TRANS: Secondary navigation menu option leading to help on StatusNet.
_('Help'));
$this->menuItem(common_local_url('doc', array('title' => 'about')),
+ // TRANS: Secondary navigation menu option leading to text about StatusNet site.
_('About'));
$this->menuItem(common_local_url('doc', array('title' => 'faq')),
+ // TRANS: Secondary navigation menu option leading to Frequently Asked Questions.
_('FAQ'));
$bb = common_config('site', 'broughtby');
if (!empty($bb)) {
$this->menuItem(common_local_url('doc', array('title' => 'tos')),
+ // TRANS: Secondary navigation menu option leading to Terms of Service.
_('TOS'));
}
$this->menuItem(common_local_url('doc', array('title' => 'privacy')),
+ // TRANS: Secondary navigation menu option leading to privacy policy.
_('Privacy'));
$this->menuItem(common_local_url('doc', array('title' => 'source')),
+ // TRANS: Secondary navigation menu option.
_('Source'));
$this->menuItem(common_local_url('version'),
+ // TRANS: Secondary navigation menu option leading to version information on the StatusNet site.
_('Version'));
$this->menuItem(common_local_url('doc', array('title' => 'contact')),
+ // TRANS: Secondary navigation menu option leading to contact information on the StatusNet site.
_('Contact'));
$this->menuItem(common_local_url('doc', array('title' => 'badge')),
_('Badge'));
@@ -795,16 +835,18 @@ class Action extends HTMLOutputter // lawsuit
*/
function showStatusNetLicense()
{
+ // TRANS: DT element for StatusNet software license.
$this->element('dt', array('id' => 'site_statusnet_license'), _('StatusNet software license'));
$this->elementStart('dd', null);
- // @fixme drop the final spaces in the messages when at good spot
- // to let translations get updated.
if (common_config('site', 'broughtby')) {
- $instr = _('**%%site.name%%** is a microblogging service brought to you by [%%site.broughtby%%](%%site.broughtbyurl%%). ');
+ // TRANS: First sentence of the StatusNet site license. Used if 'broughtby' is set.
+ $instr = _('**%%site.name%%** is a microblogging service brought to you by [%%site.broughtby%%](%%site.broughtbyurl%%).');
} else {
- $instr = _('**%%site.name%%** is a microblogging service. ');
+ // TRANS: First sentence of the StatusNet site license. Used if 'broughtby' is not set.
+ $instr = _('**%%site.name%%** is a microblogging service.');
}
$instr .= ' ';
+ // TRANS: Second sentence of the StatusNet site license. Mentions the StatusNet source code license.
$instr .= sprintf(_('It runs the [StatusNet](http://status.net/) microblogging software, version %s, available under the [GNU Affero General Public License](http://www.fsf.org/licensing/licenses/agpl-3.0.html).'), STATUSNET_VERSION);
$output = common_markup_to_html($instr);
$this->raw($output);
@@ -820,19 +862,25 @@ class Action extends HTMLOutputter // lawsuit
function showContentLicense()
{
if (Event::handle('StartShowContentLicense', array($this))) {
+ // TRANS: DT element for StatusNet site content license.
$this->element('dt', array('id' => 'site_content_license'), _('Site content license'));
$this->elementStart('dd', array('id' => 'site_content_license_cc'));
switch (common_config('license', 'type')) {
case 'private':
+ // TRANS: Content license displayed when license is set to 'private'.
+ // TRANS: %1$s is the site name.
$this->element('p', null, sprintf(_('Content and data of %1$s are private and confidential.'),
common_config('site', 'name')));
// fall through
case 'allrightsreserved':
if (common_config('license', 'owner')) {
+ // TRANS: Content license displayed when license is set to 'allrightsreserved'.
+ // TRANS: %1$s is the copyright owner.
$this->element('p', null, sprintf(_('Content and data copyright by %1$s. All rights reserved.'),
common_config('license', 'owner')));
} else {
+ // TRANS: Content license displayed when license is set to 'allrightsreserved' and no owner is set.
$this->element('p', null, _('Content and data copyright by contributors. All rights reserved.'));
}
break;
@@ -946,32 +994,60 @@ class Action extends HTMLOutputter // lawsuit
function handle($argarray=null)
{
header('Vary: Accept-Encoding,Cookie');
+
$lm = $this->lastModified();
$etag = $this->etag();
+
if ($etag) {
header('ETag: ' . $etag);
}
+
if ($lm) {
header('Last-Modified: ' . date(DATE_RFC1123, $lm));
- if (array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER)) {
- $if_modified_since = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
- $ims = strtotime($if_modified_since);
- if ($lm <= $ims) {
- $if_none_match = (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER)) ?
- $_SERVER['HTTP_IF_NONE_MATCH'] : null;
- if (!$if_none_match ||
- !$etag ||
- $this->_hasEtag($etag, $if_none_match)) {
- header('HTTP/1.1 304 Not Modified');
- // Better way to do this?
- exit(0);
- }
- }
+ if ($this->isCacheable()) {
+ header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
+ header( "Cache-Control: private, must-revalidate, max-age=0" );
+ header( "Pragma: underwear-catapult");
+ }
+ }
+
+ if ($etag) {
+ $if_none_match = (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER)) ?
+ $_SERVER['HTTP_IF_NONE_MATCH'] : null;
+ if ($if_none_match && $this->_hasEtag($etag, $if_none_match)) {
+ header('HTTP/1.1 304 Not Modified');
+ // Better way to do this?
+ exit(0);
+ }
+ }
+
+ if ($lm && array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER)) {
+ $if_modified_since = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
+ $ims = strtotime($if_modified_since);
+ if ($lm <= $ims) {
+ header('HTTP/1.1 304 Not Modified');
+ // Better way to do this?
+ exit(0);
}
}
}
/**
+ * Is this action cacheable?
+ *
+ * If the action returns a last-modified
+ *
+ * @param array $argarray is ignored since it's now passed in in prepare()
+ *
+ * @return boolean is read only action?
+ */
+
+ function isCacheable()
+ {
+ return true;
+ }
+
+ /**
* HasĀ etag? (private)
*
* @param string $etag etag http header
@@ -1148,11 +1224,15 @@ class Action extends HTMLOutputter // lawsuit
*
* @return nothing
*/
+ // XXX: The messages in this pagination method only tailor to navigating
+ // notices. In other lists, "Previous"/"Next" type navigation is
+ // desirable, but not available.
function pagination($have_before, $have_after, $page, $action, $args=null)
{
// Does a little before-after block for next/prev page
if ($have_before || $have_after) {
$this->elementStart('dl', 'pagination');
+ // TRANS: DT element for pagination (previous/next, etc.).
$this->element('dt', null, _('Pagination'));
$this->elementStart('dd', null);
$this->elementStart('ul', array('class' => 'nav'));
@@ -1162,6 +1242,8 @@ class Action extends HTMLOutputter // lawsuit
$this->elementStart('li', array('class' => 'nav_prev'));
$this->element('a', array('href' => common_local_url($action, $args, $pargs),
'rel' => 'prev'),
+ // TRANS: Pagination message to go to a page displaying information more in the
+ // TRANS: present than the currently displayed information.
_('After'));
$this->elementEnd('li');
}
@@ -1170,6 +1252,8 @@ class Action extends HTMLOutputter // lawsuit
$this->elementStart('li', array('class' => 'nav_next'));
$this->element('a', array('href' => common_local_url($action, $args, $pargs),
'rel' => 'next'),
+ // TRANS: Pagination message to go to a page displaying information more in the
+ // TRANS: past than the currently displayed information.
_('Before'));
$this->elementEnd('li');
}
@@ -1213,6 +1297,8 @@ class Action extends HTMLOutputter // lawsuit
* @return void
*/
+ // XXX: Finding this type of check with the same message about 50 times.
+ // Possible to refactor?
function checkSessionToken()
{
// CSRF protection
diff --git a/lib/activity.php b/lib/activity.php
index 5d6230c6d..8e2da99bb 100644
--- a/lib/activity.php
+++ b/lib/activity.php
@@ -83,6 +83,7 @@ class Activity
const CREATOR = 'creator';
const CONTENTNS = 'http://purl.org/rss/1.0/modules/content/';
+ const ENCODED = 'encoded';
public $actor; // an ActivityObject
public $verb; // a string (the URL)
@@ -117,7 +118,8 @@ class Activity
// Insist on a feed's root DOMElement; don't allow a DOMDocument
if ($feed instanceof DOMDocument) {
throw new ClientException(
- _("Expecting a root feed element but got a whole XML document.")
+ // TRANS: Client exception thrown when a feed instance is a DOMDocument.
+ _('Expecting a root feed element but got a whole XML document.')
);
}
@@ -268,14 +270,21 @@ class Activity
$this->title = ActivityUtils::childContent($item, ActivityObject::TITLE, self::RSS);
- $contentEl = ActivityUtils::child($item, ActivityUtils::CONTENT, self::CONTENTNS);
+ $contentEl = ActivityUtils::child($item, self::ENCODED, self::CONTENTNS);
if (!empty($contentEl)) {
- $this->content = htmlspecialchars_decode($contentEl->textContent, ENT_QUOTES);
+ // <content:encoded> XML node's text content is HTML; no further processing needed.
+ $this->content = $contentEl->textContent;
} else {
$descriptionEl = ActivityUtils::child($item, self::DESCRIPTION, self::RSS);
if (!empty($descriptionEl)) {
- $this->content = htmlspecialchars_decode($descriptionEl->textContent, ENT_QUOTES);
+ // Per spec, <description> must be plaintext.
+ // In practice, often there's HTML... but these days good
+ // feeds are using <content:encoded> which is explicitly
+ // real HTML.
+ // We'll treat this following spec, and do HTML escaping
+ // to convert from plaintext to HTML.
+ $this->content = htmlspecialchars($descriptionEl->textContent);
}
}
diff --git a/lib/activitycontext.php b/lib/activitycontext.php
index 2df7613f7..09a457924 100644
--- a/lib/activitycontext.php
+++ b/lib/activitycontext.php
@@ -51,6 +51,7 @@ class ActivityContext
const POINT = 'point';
const ATTENTION = 'ostatus:attention';
+ const MENTIONED = 'mentioned';
const CONVERSATION = 'ostatus:conversation';
function __construct($element)
@@ -70,16 +71,22 @@ class ActivityContext
$links = $element->getElementsByTagNameNS(ActivityUtils::ATOM, ActivityUtils::LINK);
+ $attention = array();
for ($i = 0; $i < $links->length; $i++) {
$link = $links->item($i);
$linkRel = $link->getAttribute(ActivityUtils::REL);
+ // XXX: Deprecate this in favour of "mentioned" from Salmon spec
+ // http://salmon-protocol.googlecode.com/svn/trunk/draft-panzer-salmon-00.html#SALR
if ($linkRel == self::ATTENTION) {
- $this->attention[] = $link->getAttribute(self::HREF);
+ $attention[] = $link->getAttribute(self::HREF);
+ } elseif ($linkRel == self::MENTIONED) {
+ $attention[] = $link->getAttribute(self::HREF);
}
}
+ $this->attention = array_unique($attention);
}
/**
diff --git a/lib/activityutils.php b/lib/activityutils.php
index a7e99fb11..dd38d4e14 100644
--- a/lib/activityutils.php
+++ b/lib/activityutils.php
@@ -213,11 +213,19 @@ class ActivityUtils
// slavishly following http://atompub.org/rfc4287.html#rfc.section.4.1.3.3
if (empty($type) || $type == 'text') {
- return $el->textContent;
+ // We have plaintext saved as the XML text content.
+ // Since we want HTML, we need to escape any special chars.
+ return htmlspecialchars($el->textContent);
} else if ($type == 'html') {
+ // We have HTML saved as the XML text content.
+ // No additional processing required once we've got it.
$text = $el->textContent;
- return htmlspecialchars_decode($text, ENT_QUOTES);
+ return $text;
} else if ($type == 'xhtml') {
+ // Per spec, the <content type="xhtml"> contains a single
+ // HTML <div> with XHTML namespace on it as a child node.
+ // We need to pull all of that <div>'s child nodes and
+ // serialize them back to an (X)HTML source fragment.
$divEl = ActivityUtils::child($el, 'div', 'http://www.w3.org/1999/xhtml');
if (empty($divEl)) {
return null;
@@ -249,6 +257,12 @@ class ActivityUtils
*/
static function validateUri($uri)
{
+ // Check mailto: URIs first
+
+ if (preg_match('/^mailto:(.*)$/', $uri, $match)) {
+ return Validate::email($match[1], common_config('email', 'check_domain'));
+ }
+
if (Validate::uri($uri)) {
return true;
}
diff --git a/lib/adminpanelaction.php b/lib/adminpanelaction.php
index a927e2333..41cfe5851 100644
--- a/lib/adminpanelaction.php
+++ b/lib/adminpanelaction.php
@@ -69,7 +69,7 @@ class AdminPanelAction extends Action
// User must be logged in.
if (!common_logged_in()) {
- // TRANS: Client error message
+ // TRANS: Client error message thrown when trying to access the admin panel while not logged in.
$this->clientError(_('Not logged in.'));
return false;
}
@@ -94,7 +94,7 @@ class AdminPanelAction extends Action
// User must have the right to change admin settings
if (!$user->hasRight(Right::CONFIGURESITE)) {
- // TRANS: Client error message
+ // TRANS: Client error message thrown when a user tries to change admin settings but has no access rights.
$this->clientError(_('You cannot make changes to this site.'));
return false;
}
@@ -106,7 +106,7 @@ class AdminPanelAction extends Action
$name = mb_substr($name, 0, -10);
if (!self::canAdmin($name)) {
- // TRANS: Client error message
+ // TRANS: Client error message throw when a certain panel's settings cannot be changed.
$this->clientError(_('Changes to that panel are not allowed.'), 403);
return false;
}
@@ -225,7 +225,7 @@ class AdminPanelAction extends Action
function showForm()
{
- // TRANS: Client error message
+ // TRANS: Client error message.
$this->clientError(_('showForm() not implemented.'));
return;
}
@@ -279,13 +279,15 @@ class AdminPanelAction extends Action
$result = $config->delete();
if (!$result) {
common_log_db_error($config, 'DELETE', __FILE__);
- // TRANS: Client error message
+ // TRANS: Client error message thrown if design settings could not be deleted in
+ // TRANS: the admin panel Design.
$this->clientError(_("Unable to delete design setting."));
return null;
}
+ return $result;
}
- return $result;
+ return null;
}
function canAdmin($name)
diff --git a/lib/apiaction.php b/lib/apiaction.php
index bb4884b45..5e0cd5518 100644
--- a/lib/apiaction.php
+++ b/lib/apiaction.php
@@ -27,11 +27,73 @@
* @author Jeffery To <jeffery.to@gmail.com>
* @author Toby Inkster <mail@tobyinkster.co.uk>
* @author Zach Copley <zach@status.net>
- * @copyright 2009 StatusNet, Inc.
+ * @copyright 2009-2010 StatusNet, Inc.
+ * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
+/* External API usage documentation. Please update when you change how the API works. */
+
+/*! @mainpage StatusNet REST API
+
+ @section Introduction
+
+ Some explanatory text about the API would be nice.
+
+ @section API Methods
+
+ @subsection timelinesmethods_sec Timeline Methods
+
+ @li @ref publictimeline
+ @li @ref friendstimeline
+
+ @subsection statusmethods_sec Status Methods
+
+ @li @ref statusesupdate
+
+ @subsection usermethods_sec User Methods
+
+ @subsection directmessagemethods_sec Direct Message Methods
+
+ @subsection friendshipmethods_sec Friendship Methods
+
+ @subsection socialgraphmethods_sec Social Graph Methods
+
+ @subsection accountmethods_sec Account Methods
+
+ @subsection favoritesmethods_sec Favorites Methods
+
+ @subsection blockmethods_sec Block Methods
+
+ @subsection oauthmethods_sec OAuth Methods
+
+ @subsection helpmethods_sec Help Methods
+
+ @subsection groupmethods_sec Group Methods
+
+ @page apiroot API Root
+
+ The URLs for methods referred to in this API documentation are
+ relative to the StatusNet API root. The API root is determined by the
+ site's @b server and @b path variables, which are generally specified
+ in config.php. For example:
+
+ @code
+ $config['site']['server'] = 'example.org';
+ $config['site']['path'] = 'statusnet'
+ @endcode
+
+ The pattern for a site's API root is: @c protocol://server/path/api E.g:
+
+ @c http://example.org/statusnet/api
+
+ The @b path can be empty. In that case the API root would simply be:
+
+ @c http://example.org/api
+
+*/
+
if (!defined('STATUSNET')) {
exit(1);
}
@@ -63,9 +125,13 @@ class ApiAction extends Action
var $count = null;
var $max_id = null;
var $since_id = null;
+ var $source = null;
+ var $callback = null;
var $access = self::READ_ONLY; // read (default) or read-write
+ static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
+
/**
* Initialization.
*
@@ -80,6 +146,7 @@ class ApiAction extends Action
parent::prepare($args);
$this->format = $this->arg('format');
+ $this->callback = $this->arg('callback');
$this->page = (int)$this->arg('page', 1);
$this->count = (int)$this->arg('count', 20);
$this->max_id = (int)$this->arg('max_id', 0);
@@ -89,6 +156,12 @@ class ApiAction extends Action
header('X-StatusNet-Warning: since parameter is disabled; use since_id');
}
+ $this->source = $this->trimmed('source');
+
+ if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
+ $this->source = 'api';
+ }
+
return true;
}
@@ -102,6 +175,7 @@ class ApiAction extends Action
function handle($args)
{
+ header('Access-Control-Allow-Origin: *');
parent::handle($args);
}
@@ -199,11 +273,13 @@ class ApiAction extends Action
// Is the requesting user following this user?
$twitter_user['following'] = false;
+ $twitter_user['statusnet:blocking'] = false;
$twitter_user['notifications'] = false;
if (isset($this->auth_user)) {
$twitter_user['following'] = $this->auth_user->isSubscribed($profile);
+ $twitter_user['statusnet:blocking'] = $this->auth_user->hasBlocked($profile);
// Notifications on?
$sub = Subscription::pkeyGet(array('subscriber' =>
@@ -255,7 +331,23 @@ class ApiAction extends Action
$twitter_status['created_at'] = $this->dateTwitter($notice->created);
$twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
intval($notice->reply_to) : null;
- $twitter_status['source'] = $this->sourceLink($notice->source);
+
+ $source = null;
+
+ $ns = $notice->getSource();
+ if ($ns) {
+ if (!empty($ns->name) && !empty($ns->url)) {
+ $source = '<a href="'
+ . htmlspecialchars($ns->url)
+ . '" rel="nofollow">'
+ . htmlspecialchars($ns->name)
+ . '</a>';
+ } else {
+ $source = $ns->code;
+ }
+ }
+
+ $twitter_status['source'] = $source;
$twitter_status['id'] = intval($notice->id);
$replier_profile = null;
@@ -321,20 +413,32 @@ class ApiAction extends Action
function twitterGroupArray($group)
{
- $twitter_group=array();
- $twitter_group['id']=$group->id;
- $twitter_group['url']=$group->permalink();
- $twitter_group['nickname']=$group->nickname;
- $twitter_group['fullname']=$group->fullname;
- $twitter_group['original_logo']=$group->original_logo;
- $twitter_group['homepage_logo']=$group->homepage_logo;
- $twitter_group['stream_logo']=$group->stream_logo;
- $twitter_group['mini_logo']=$group->mini_logo;
- $twitter_group['homepage']=$group->homepage;
- $twitter_group['description']=$group->description;
- $twitter_group['location']=$group->location;
- $twitter_group['created']=$this->dateTwitter($group->created);
- $twitter_group['modified']=$this->dateTwitter($group->modified);
+ $twitter_group = array();
+
+ $twitter_group['id'] = $group->id;
+ $twitter_group['url'] = $group->permalink();
+ $twitter_group['nickname'] = $group->nickname;
+ $twitter_group['fullname'] = $group->fullname;
+
+ if (isset($this->auth_user)) {
+ $twitter_group['member'] = $this->auth_user->isMember($group);
+ $twitter_group['blocked'] = Group_block::isBlocked(
+ $group,
+ $this->auth_user->getProfile()
+ );
+ }
+
+ $twitter_group['member_count'] = $group->getMemberCount();
+ $twitter_group['original_logo'] = $group->original_logo;
+ $twitter_group['homepage_logo'] = $group->homepage_logo;
+ $twitter_group['stream_logo'] = $group->stream_logo;
+ $twitter_group['mini_logo'] = $group->mini_logo;
+ $twitter_group['homepage'] = $group->homepage;
+ $twitter_group['description'] = $group->description;
+ $twitter_group['location'] = $group->location;
+ $twitter_group['created'] = $this->dateTwitter($group->created);
+ $twitter_group['modified'] = $this->dateTwitter($group->modified);
+
return $twitter_group;
}
@@ -358,65 +462,71 @@ class ApiAction extends Action
function twitterRssEntryArray($notice)
{
- $profile = $notice->getProfile();
$entry = array();
- // We trim() to avoid extraneous whitespace in the output
+ if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
- $entry['content'] = common_xml_safe_str(trim($notice->rendered));
- $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
- $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
- $entry['published'] = common_date_iso8601($notice->created);
+ $profile = $notice->getProfile();
- $taguribase = TagURI::base();
- $entry['id'] = "tag:$taguribase:$entry[link]";
+ // We trim() to avoid extraneous whitespace in the output
- $entry['updated'] = $entry['published'];
- $entry['author'] = $profile->getBestName();
+ $entry['content'] = common_xml_safe_str(trim($notice->rendered));
+ $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
+ $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
+ $entry['published'] = common_date_iso8601($notice->created);
- // Enclosures
- $attachments = $notice->attachments();
- $enclosures = array();
-
- foreach ($attachments as $attachment) {
- $enclosure_o=$attachment->getEnclosure();
- if ($enclosure_o) {
- $enclosure = array();
- $enclosure['url'] = $enclosure_o->url;
- $enclosure['mimetype'] = $enclosure_o->mimetype;
- $enclosure['size'] = $enclosure_o->size;
- $enclosures[] = $enclosure;
- }
- }
+ $taguribase = TagURI::base();
+ $entry['id'] = "tag:$taguribase:$entry[link]";
- if (!empty($enclosures)) {
- $entry['enclosures'] = $enclosures;
- }
+ $entry['updated'] = $entry['published'];
+ $entry['author'] = $profile->getBestName();
+
+ // Enclosures
+ $attachments = $notice->attachments();
+ $enclosures = array();
- // Tags/Categories
- $tag = new Notice_tag();
- $tag->notice_id = $notice->id;
- if ($tag->find()) {
- $entry['tags']=array();
- while ($tag->fetch()) {
- $entry['tags'][]=$tag->tag;
+ foreach ($attachments as $attachment) {
+ $enclosure_o=$attachment->getEnclosure();
+ if ($enclosure_o) {
+ $enclosure = array();
+ $enclosure['url'] = $enclosure_o->url;
+ $enclosure['mimetype'] = $enclosure_o->mimetype;
+ $enclosure['size'] = $enclosure_o->size;
+ $enclosures[] = $enclosure;
+ }
}
- }
- $tag->free();
- // RSS Item specific
- $entry['description'] = $entry['content'];
- $entry['pubDate'] = common_date_rfc2822($notice->created);
- $entry['guid'] = $entry['link'];
+ if (!empty($enclosures)) {
+ $entry['enclosures'] = $enclosures;
+ }
- if (isset($notice->lat) && isset($notice->lon)) {
- // This is the format that GeoJSON expects stuff to be in.
- // showGeoRSS() below uses it for XML output, so we reuse it
- $entry['geo'] = array('type' => 'Point',
- 'coordinates' => array((float) $notice->lat,
- (float) $notice->lon));
- } else {
- $entry['geo'] = null;
+ // Tags/Categories
+ $tag = new Notice_tag();
+ $tag->notice_id = $notice->id;
+ if ($tag->find()) {
+ $entry['tags']=array();
+ while ($tag->fetch()) {
+ $entry['tags'][]=$tag->tag;
+ }
+ }
+ $tag->free();
+
+ // RSS Item specific
+ $entry['description'] = $entry['content'];
+ $entry['pubDate'] = common_date_rfc2822($notice->created);
+ $entry['guid'] = $entry['link'];
+
+ if (isset($notice->lat) && isset($notice->lon)) {
+ // This is the format that GeoJSON expects stuff to be in.
+ // showGeoRSS() below uses it for XML output, so we reuse it
+ $entry['geo'] = array('type' => 'Point',
+ 'coordinates' => array((float) $notice->lat,
+ (float) $notice->lon));
+ } else {
+ $entry['geo'] = null;
+ }
+
+ Event::handle('EndRssEntryArray', array($notice, &$entry));
}
return $entry;
@@ -637,14 +747,16 @@ class ApiAction extends Action
'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
if (is_array($notice)) {
- foreach ($notice as $n) {
- $twitter_status = $this->twitterStatusArray($n);
- $this->showTwitterXmlStatus($twitter_status);
- }
- } else {
- while ($notice->fetch()) {
+ $notice = new ArrayWrapper($notice);
+ }
+
+ while ($notice->fetch()) {
+ try {
$twitter_status = $this->twitterStatusArray($notice);
$this->showTwitterXmlStatus($twitter_status);
+ } catch (Exception $e) {
+ common_log(LOG_ERR, $e->getMessage());
+ continue;
}
}
@@ -692,14 +804,16 @@ class ApiAction extends Action
$this->element('ttl', null, '40');
if (is_array($notice)) {
- foreach ($notice as $n) {
- $entry = $this->twitterRssEntryArray($n);
- $this->showTwitterRssItem($entry);
- }
- } else {
- while ($notice->fetch()) {
+ $notice = new ArrayWrapper($notice);
+ }
+
+ while ($notice->fetch()) {
+ try {
$entry = $this->twitterRssEntryArray($notice);
$this->showTwitterRssItem($entry);
+ } catch (Exception $e) {
+ common_log(LOG_ERR, $e->getMessage());
+ // continue on exceptions
}
}
@@ -735,12 +849,15 @@ class ApiAction extends Action
$this->element('subtitle', null, $subtitle);
if (is_array($notice)) {
- foreach ($notice as $n) {
- $this->raw($n->asAtomEntry());
- }
- } else {
- while ($notice->fetch()) {
+ $notice = new ArrayWrapper($notice);
+ }
+
+ while ($notice->fetch()) {
+ try {
$this->raw($notice->asAtomEntry());
+ } catch (Exception $e) {
+ common_log(LOG_ERR, $e->getMessage());
+ continue;
}
}
@@ -935,14 +1052,16 @@ class ApiAction extends Action
$statuses = array();
if (is_array($notice)) {
- foreach ($notice as $n) {
- $twitter_status = $this->twitterStatusArray($n);
- array_push($statuses, $twitter_status);
- }
- } else {
- while ($notice->fetch()) {
+ $notice = new ArrayWrapper($notice);
+ }
+
+ while ($notice->fetch()) {
+ try {
$twitter_status = $this->twitterStatusArray($notice);
array_push($statuses, $twitter_status);
+ } catch (Exception $e) {
+ common_log(LOG_ERR, $e->getMessage());
+ continue;
}
}
@@ -1079,9 +1198,8 @@ class ApiAction extends Action
header('Content-Type: application/json; charset=utf-8');
// Check for JSONP callback
- $callback = $this->arg('callback');
- if ($callback) {
- print $callback . '(';
+ if (isset($this->callback)) {
+ print $this->callback . '(';
}
break;
case 'rss':
@@ -1093,6 +1211,7 @@ class ApiAction extends Action
$this->initTwitterAtom();
break;
default:
+ // TRANS: Client error on an API request with an unsupported data format.
$this->clientError(_('Not a supported data format.'));
break;
}
@@ -1109,8 +1228,7 @@ class ApiAction extends Action
case 'json':
// Check for JSONP callback
- $callback = $this->arg('callback');
- if ($callback) {
+ if (isset($this->callback)) {
print ')';
}
break;
@@ -1121,6 +1239,7 @@ class ApiAction extends Action
$this->endTwitterRss();
break;
default:
+ // TRANS: Client error on an API request with an unsupported data format.
$this->clientError(_('Not a supported data format.'));
break;
}
@@ -1139,7 +1258,10 @@ class ApiAction extends Action
$status_string = ClientErrorAction::$status[$code];
- header('HTTP/1.1 '.$code.' '.$status_string);
+ // Do not emit error header for JSONP
+ if (!isset($this->callback)) {
+ header('HTTP/1.1 '.$code.' '.$status_string);
+ }
if ($format == 'xml') {
$this->initDocument('xml');
@@ -1172,7 +1294,10 @@ class ApiAction extends Action
$status_string = ServerErrorAction::$status[$code];
- header('HTTP/1.1 '.$code.' '.$status_string);
+ // Do not emit error header for JSONP
+ if (!isset($this->callback)) {
+ header('HTTP/1.1 '.$code.' '.$status_string);
+ }
if ($content_type == 'xml') {
$this->initDocument('xml');
@@ -1237,6 +1362,7 @@ class ApiAction extends Action
$this->showJsonObjects($profile_array);
break;
default:
+ // TRANS: Client error on an API request with an unsupported data format.
$this->clientError(_('Not a supported data format.'));
return;
}
@@ -1275,6 +1401,34 @@ class ApiAction extends Action
}
}
+ function getTargetProfile($id)
+ {
+ if (empty($id)) {
+
+ // Twitter supports these other ways of passing the user ID
+ if (is_numeric($this->arg('id'))) {
+ return Profile::staticGet($this->arg('id'));
+ } else if ($this->arg('id')) {
+ $nickname = common_canonical_nickname($this->arg('id'));
+ return Profile::staticGet('nickname', $nickname);
+ } else if ($this->arg('user_id')) {
+ // This is to ensure that a non-numeric user_id still
+ // overrides screen_name even if it doesn't get used
+ if (is_numeric($this->arg('user_id'))) {
+ return Profile::staticGet('id', $this->arg('user_id'));
+ }
+ } else if ($this->arg('screen_name')) {
+ $nickname = common_canonical_nickname($this->arg('screen_name'));
+ return Profile::staticGet('nickname', $nickname);
+ }
+ } else if (is_numeric($id)) {
+ return Profile::staticGet($id);
+ } else {
+ $nickname = common_canonical_nickname($id);
+ return Profile::staticGet('nickname', $nickname);
+ }
+ }
+
function getTargetGroup($id)
{
if (empty($id)) {
@@ -1317,43 +1471,6 @@ class ApiAction extends Action
}
}
- function sourceLink($source)
- {
- $source_name = _($source);
- switch ($source) {
- case 'web':
- case 'xmpp':
- case 'mail':
- case 'omb':
- case 'api':
- break;
- default:
-
- $name = null;
- $url = null;
-
- $ns = Notice_source::staticGet($source);
-
- if ($ns) {
- $name = $ns->name;
- $url = $ns->url;
- } else {
- $app = Oauth_application::staticGet('name', $source);
- if ($app) {
- $name = $app->name;
- $url = $app->source_url;
- }
- }
-
- if (!empty($name) && !empty($url)) {
- $source_name = '<a href="' . $url . '">' . $name . '</a>';
- }
-
- break;
- }
- return $source_name;
- }
-
/**
* Returns query argument or default value if not found. Certain
* parameters used throughout the API are lightly scrubbed and
diff --git a/lib/apiauth.php b/lib/apiauth.php
index e78de618e..cf7a2692c 100644
--- a/lib/apiauth.php
+++ b/lib/apiauth.php
@@ -30,10 +30,29 @@
* @author Sarven Capadisli <csarven@status.net>
* @author Zach Copley <zach@status.net>
* @copyright 2009-2010 StatusNet, Inc.
+ * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
+/* External API usage documentation. Please update when you change how this method works. */
+
+/*! @page authentication Authentication
+
+ StatusNet supports HTTP Basic Authentication and OAuth for API calls.
+
+ @warning Currently, users who have created accounts without setting a
+ password via OpenID, Facebook Connect, etc., cannot use the API until
+ they set a password with their account settings panel.
+
+ @section HTTP Basic Auth
+
+
+
+ @section OAuth
+
+*/
+
if (!defined('STATUSNET')) {
exit(1);
}
@@ -54,7 +73,6 @@ class ApiAuthAction extends ApiAction
{
var $auth_user_nickname = null;
var $auth_user_password = null;
- var $oauth_source = null;
/**
* Take arguments for running, looks for an OAuth request,
@@ -91,6 +109,7 @@ class ApiAuthAction extends ApiAction
if ($this->isReadOnly($args) == false) {
if ($this->access != self::READ_WRITE) {
+ // TRANS: Client error 401.
$msg = _('API resource requires read-write access, ' .
'but you only have read access.');
$this->clientError($msg, 401, $this->format);
@@ -162,7 +181,7 @@ class ApiAuthAction extends ApiAction
// set the source attr
- $this->oauth_source = $app->name;
+ $this->source = $app->name;
$appUser = Oauth_application_user::staticGet('token', $access_token);
@@ -208,7 +227,7 @@ class ApiAuthAction extends ApiAction
} catch (OAuthException $e) {
common_log(LOG_WARNING, 'API OAuthException - ' . $e->getMessage());
- $this->showAuthError();
+ $this->clientError($e->getMessage(), 401, $this->format);
exit;
}
}
@@ -246,7 +265,7 @@ class ApiAuthAction extends ApiAction
// show error if the user clicks 'cancel'
- $this->showAuthError();
+ $this->clientError("Could not authenticate you.", 401, $this->format);
exit;
} else {
@@ -273,13 +292,13 @@ class ApiAuthAction extends ApiAction
list($proxy, $ip) = common_client_ip();
- $msg = sprintf(_('Failed API auth attempt, nickname = %1$s, ' .
- 'proxy = %2$s, ip = %3$s'),
+ $msg = sprintf( 'Failed API auth attempt, nickname = %1$s, ' .
+ 'proxy = %2$s, ip = %3$s',
$this->auth_user_nickname,
$proxy,
$ip);
common_log(LOG_WARNING, $msg);
- $this->showAuthError();
+ $this->clientError("Could not authenticate you.", 401, $this->format);
exit;
}
}
@@ -326,36 +345,4 @@ class ApiAuthAction extends ApiAction
}
}
}
-
- /**
- * Output an authentication error message. Use XML or JSON if one
- * of those formats is specified, otherwise output plain text
- *
- * @return void
- */
-
- function showAuthError()
- {
- header('HTTP/1.1 401 Unauthorized');
- $msg = 'Could not authenticate you.';
-
- if ($this->format == 'xml') {
- header('Content-Type: application/xml; charset=utf-8');
- $this->startXML();
- $this->elementStart('hash');
- $this->element('error', null, $msg);
- $this->element('request', null, $_SERVER['REQUEST_URI']);
- $this->elementEnd('hash');
- $this->endXML();
- } elseif ($this->format == 'json') {
- header('Content-Type: application/json; charset=utf-8');
- $error_array = array('error' => $msg,
- 'request' => $_SERVER['REQUEST_URI']);
- print(json_encode($error_array));
- } else {
- header('Content-type: text/plain');
- print "$msg\n";
- }
- }
-
}
diff --git a/lib/apibareauth.php b/lib/apibareauth.php
index 2d29c1ddd..da7af1261 100644
--- a/lib/apibareauth.php
+++ b/lib/apibareauth.php
@@ -32,6 +32,7 @@
* @author Sarven Capadisli <csarven@status.net>
* @author Zach Copley <zach@status.net>
* @copyright 2009 StatusNet, Inc.
+ * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
@@ -106,4 +107,4 @@ class ApiBareAuthAction extends ApiAuthAction
return false;
}
-} \ No newline at end of file
+}
diff --git a/lib/apiprivateauth.php b/lib/apiprivateauth.php
index 5d0033005..5e78c65a1 100644
--- a/lib/apiprivateauth.php
+++ b/lib/apiprivateauth.php
@@ -31,6 +31,7 @@
* @author Sarven Capadisli <csarven@status.net>
* @author Zach Copley <zach@status.net>
* @copyright 2009 StatusNet, Inc.
+ * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
diff --git a/lib/applicationeditform.php b/lib/applicationeditform.php
index 9b7d05861..81c8fb018 100644
--- a/lib/applicationeditform.php
+++ b/lib/applicationeditform.php
@@ -133,6 +133,7 @@ class ApplicationEditForm extends Form
function formLegend()
{
+ // TRANS: Form legend.
$this->out->element('legend', null, _('Edit application'));
}
@@ -177,10 +178,12 @@ class ApplicationEditForm extends Form
}
$this->out->element('label', array('for' => 'app_icon'),
+ // TRANS: Form input field label for application icon.
_('Icon'));
$this->out->element('input', array('name' => 'app_icon',
'type' => 'file',
'id' => 'app_icon'));
+ // TRANS: Form guide.
$this->out->element('p', 'form_guide', _('Icon for this application'));
$this->out->element('input', array('name' => 'MAX_FILE_SIZE',
'type' => 'hidden',
@@ -192,6 +195,7 @@ class ApplicationEditForm extends Form
$this->out->hidden('application_id', $id);
+ // TRANS: Form input field label for application name.
$this->out->input('name', _('Name'),
($this->out->arg('name')) ? $this->out->arg('name') : $name);
@@ -201,11 +205,14 @@ class ApplicationEditForm extends Form
$maxDesc = Oauth_application::maxDesc();
if ($maxDesc > 0) {
+ // TRANS: Form input field instructions.
$descInstr = sprintf(_('Describe your application in %d characters'),
$maxDesc);
} else {
+ // TRANS: Form input field instructions.
$descInstr = _('Describe your application');
}
+ // TRANS: Form input field label.
$this->out->textarea('description', _('Description'),
($this->out->arg('description')) ? $this->out->arg('description') : $description,
$descInstr);
@@ -213,27 +220,39 @@ class ApplicationEditForm extends Form
$this->out->elementEnd('li');
$this->out->elementStart('li');
+ // TRANS: Form input field instructions.
+ $instruction = _('URL of the homepage of this application');
+ // TRANS: Form input field label.
$this->out->input('source_url', _('Source URL'),
($this->out->arg('source_url')) ? $this->out->arg('source_url') : $source_url,
- _('URL of the homepage of this application'));
+ $instruction);
$this->out->elementEnd('li');
$this->out->elementStart('li');
+ // TRANS: Form input field instructions.
+ $instruction = _('Organization responsible for this application');
+ // TRANS: Form input field label.
$this->out->input('organization', _('Organization'),
($this->out->arg('organization')) ? $this->out->arg('organization') : $organization,
- _('Organization responsible for this application'));
+ $instruction);
$this->out->elementEnd('li');
$this->out->elementStart('li');
+ // TRANS: Form input field instructions.
+ $instruction = _('URL for the homepage of the organization');
+ // TRANS: Form input field label.
$this->out->input('homepage', _('Homepage'),
($this->out->arg('homepage')) ? $this->out->arg('homepage') : $homepage,
- _('URL for the homepage of the organization'));
+ $instruction);
$this->out->elementEnd('li');
$this->out->elementStart('li');
+ // TRANS: Form input field instructions.
+ $instruction = _('URL to redirect to after authentication');
+ // TRANS: Form input field label.
$this->out->input('callback_url', ('Callback URL'),
($this->out->arg('callback_url')) ? $this->out->arg('callback_url') : $callback_url,
- _('URL to redirect to after authentication'));
+ $instruction);
$this->out->elementEnd('li');
$this->out->elementStart('li', array('id' => 'application_types'));
@@ -255,6 +274,7 @@ class ApplicationEditForm extends Form
$this->out->element('label', array('for' => 'app_type-browser',
'class' => 'radio'),
+ // TRANS: Radio button label for application type
_('Browser'));
$attrs = array('name' => 'app_type',
@@ -271,7 +291,9 @@ class ApplicationEditForm extends Form
$this->out->element('label', array('for' => 'app_type-desktop',
'class' => 'radio'),
+ // TRANS: Radio button label for application type
_('Desktop'));
+ // TRANS: Form guide.
$this->out->element('p', 'form_guide', _('Type of application, browser or desktop'));
$this->out->elementEnd('li');
@@ -294,6 +316,7 @@ class ApplicationEditForm extends Form
$this->out->element('label', array('for' => 'default_access_type-ro',
'class' => 'radio'),
+ // TRANS: Radio button label for access type.
_('Read-only'));
$attrs = array('name' => 'default_access_type',
@@ -312,7 +335,9 @@ class ApplicationEditForm extends Form
$this->out->element('label', array('for' => 'default_access_type-rw',
'class' => 'radio'),
+ // TRANS: Radio button label for access type.
_('Read-write'));
+ // TRANS: Form guide.
$this->out->element('p', 'form_guide', _('Default access for this application: read-only, or read-write'));
$this->out->elementEnd('li');
@@ -328,9 +353,13 @@ class ApplicationEditForm extends Form
function formActions()
{
- $this->out->submit('cancel', _('Cancel'), 'submit form_action-primary',
+ // TRANS: Button label
+ $this->out->submit('cancel', _m('BUTTON','Cancel'), 'submit form_action-primary',
+ // TRANS: Submit button title
'cancel', _('Cancel'));
- $this->out->submit('save', _('Save'), 'submit form_action-secondary',
+ // TRANS: Button label
+ $this->out->submit('save', _m('BUTTON','Save'), 'submit form_action-secondary',
+ // TRANS: Submit button title
'save', _('Save'));
}
}
diff --git a/lib/applicationlist.php b/lib/applicationlist.php
index 3abb1f8aa..904f8981d 100644
--- a/lib/applicationlist.php
+++ b/lib/applicationlist.php
@@ -88,7 +88,6 @@ class ApplicationList extends Widget
function showApplication()
{
-
$user = common_current_user();
$this->out->elementStart('li', array('class' => 'application',
@@ -133,11 +132,16 @@ class ApplicationList extends Widget
$this->out->elementStart('li');
- $access = ($this->application->access_type & Oauth_application::$writeAccess)
- ? 'read-write' : 'read-only';
+ // TRANS: Application access type
+ $readWriteText = _('read-write');
+ // TRANS: Application access type
+ $readOnlyText = _('read-only');
- $txt = 'Approved ' . common_date_string($appUser->modified) .
- " - $access access.";
+ $access = ($this->application->access_type & Oauth_application::$writeAccess)
+ ? $readWriteText : $readOnlyText;
+ $modifiedDate = common_date_string($appUser->modified);
+ // TRANS: Used in application list. %1$s is a modified date, %2$s is access type (read-write or read-only)
+ $txt = sprintf(_('Approved %1$s - "%2$s" access.'),$modifiedDate,$access);
$this->out->raw($txt);
$this->out->elementEnd('li');
@@ -151,7 +155,8 @@ class ApplicationList extends Widget
$this->out->elementStart('fieldset');
$this->out->hidden('id', $this->application->id);
$this->out->hidden('token', common_session_token());
- $this->out->submit('revoke', _('Revoke'));
+ // TRANS: Button label
+ $this->out->submit('revoke', _m('BUTTON','Revoke'));
$this->out->elementEnd('fieldset');
$this->out->elementEnd('form');
$this->out->elementEnd('li');
diff --git a/lib/atomgroupnoticefeed.php b/lib/atomgroupnoticefeed.php
index 08c1c707c..39a1fd456 100644
--- a/lib/atomgroupnoticefeed.php
+++ b/lib/atomgroupnoticefeed.php
@@ -50,19 +50,23 @@ class AtomGroupNoticeFeed extends AtomNoticeFeed
* Constructor
*
* @param Group $group the group for the feed
+ * @param User $cur the current authenticated user, if any
* @param boolean $indent flag to turn indenting on or off
*
* @return void
*/
- function __construct($group, $indent = true) {
- parent::__construct($indent);
+ function __construct($group, $cur = null, $indent = true) {
+ parent::__construct($cur, $indent);
$this->group = $group;
+ // TRANS: Title in atom group notice feed. %s is a group name.
$title = sprintf(_("%s timeline"), $group->nickname);
$this->setTitle($title);
$sitename = common_config('site', 'name');
$subtitle = sprintf(
+ // TRANS: Message is used as a subtitle in atom group notice feed.
+ // TRANS: %1$s is a group name, %2$s is a site name.
_('Updates from %1$s on %2$s!'),
$group->nickname,
$sitename
@@ -92,4 +96,23 @@ class AtomGroupNoticeFeed extends AtomNoticeFeed
return $this->group;
}
+ function initFeed()
+ {
+ parent::initFeed();
+
+ $attrs = array();
+
+ if (!empty($this->cur)) {
+ $attrs['member'] = $this->cur->isMember($this->group)
+ ? 'true' : 'false';
+ $attrs['blocked'] = Group_block::isBlocked(
+ $this->group,
+ $this->cur->getProfile()
+ ) ? 'true' : 'false';
+ }
+
+ $attrs['member_count'] = $this->group->getMemberCount();
+
+ $this->element('statusnet:group_info', $attrs, null);
+ }
}
diff --git a/lib/atomnoticefeed.php b/lib/atomnoticefeed.php
index e4df731fe..b88217291 100644
--- a/lib/atomnoticefeed.php
+++ b/lib/atomnoticefeed.php
@@ -44,9 +44,22 @@ if (!defined('STATUSNET'))
*/
class AtomNoticeFeed extends Atom10Feed
{
- function __construct($indent = true) {
+ var $cur;
+
+ /**
+ * Constructor - adds a bunch of XML namespaces we need in our
+ * notice-specific Atom feeds, and allows setting the current
+ * authenticated user (useful for API methods).
+ *
+ * @param User $cur the current authenticated user (optional)
+ * @param boolean $indent Whether to indent XML output
+ *
+ */
+ function __construct($cur = null, $indent = true) {
parent::__construct($indent);
+ $this->cur = $cur;
+
// Feeds containing notice info use these namespaces
$this->addNamespace(
@@ -79,6 +92,11 @@ class AtomNoticeFeed extends Atom10Feed
'ostatus',
'http://ostatus.org/schema/1.0'
);
+
+ $this->addNamespace(
+ 'statusnet',
+ 'http://status.net/schema/api/1/'
+ );
}
/**
@@ -107,10 +125,17 @@ class AtomNoticeFeed extends Atom10Feed
*/
function addEntryFromNotice($notice)
{
- $source = $this->showSource();
- $author = $this->showAuthor();
+ try {
+ $source = $this->showSource();
+ $author = $this->showAuthor();
- $this->addEntryRaw($notice->asAtomEntry(false, $source, $author));
+ $cur = empty($this->cur) ? common_current_user() : $this->cur;
+
+ $this->addEntryRaw($notice->asAtomEntry(false, $source, $author, $cur));
+ } catch (Exception $e) {
+ common_log(LOG_ERR, $e->getMessage());
+ // we continue on exceptions
+ }
}
function showSource()
diff --git a/lib/atomusernoticefeed.php b/lib/atomusernoticefeed.php
index 428cc2de2..785db4915 100644
--- a/lib/atomusernoticefeed.php
+++ b/lib/atomusernoticefeed.php
@@ -50,13 +50,14 @@ class AtomUserNoticeFeed extends AtomNoticeFeed
* Constructor
*
* @param User $user the user for the feed
+ * @param User $cur the current authenticated user, if any
* @param boolean $indent flag to turn indenting on or off
*
* @return void
*/
- function __construct($user, $indent = true) {
- parent::__construct($indent);
+ function __construct($user, $cur = null, $indent = true) {
+ parent::__construct($cur, $indent);
$this->user = $user;
if (!empty($user)) {
$profile = $user->getProfile();
@@ -64,11 +65,14 @@ class AtomUserNoticeFeed extends AtomNoticeFeed
$this->setActivitySubject($profile->asActivityNoun('subject'));
}
+ // TRANS: Title in atom user notice feed. %s is a user name.
$title = sprintf(_("%s timeline"), $user->nickname);
$this->setTitle($title);
$sitename = common_config('site', 'name');
$subtitle = sprintf(
+ // TRANS: Message is used as a subtitle in atom user notice feed.
+ // TRANS: %1$s is a user name, %2$s is a site name.
_('Updates from %1$s on %2$s!'),
$user->nickname, $sitename
);
diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php
index 43f836e15..59cab9532 100644
--- a/lib/attachmentlist.php
+++ b/lib/attachmentlist.php
@@ -84,6 +84,7 @@ class AttachmentList extends Widget
if (empty($att)) return 0;
$this->out->elementStart('dl', array('id' =>'attachments',
'class' => 'entry-content'));
+ // TRANS: DT element label in attachment list.
$this->out->element('dt', null, _('Attachments'));
$this->out->elementStart('dd');
$this->out->elementStart('ol', array('class' => 'attachments'));
@@ -260,6 +261,7 @@ class Attachment extends AttachmentListItem
'class' => 'entry-content'));
if (!empty($this->oembed->author_name)) {
$this->out->elementStart('dl', 'vcard author');
+ // TRANS: DT element label in attachment list item.
$this->out->element('dt', null, _('Author'));
$this->out->elementStart('dd', 'fn');
if (empty($this->oembed->author_url)) {
@@ -273,6 +275,7 @@ class Attachment extends AttachmentListItem
}
if (!empty($this->oembed->provider)) {
$this->out->elementStart('dl', 'vcard');
+ // TRANS: DT element label in attachment list item.
$this->out->element('dt', null, _('Provider'));
$this->out->elementStart('dd', 'fn');
if (empty($this->oembed->provider_url)) {
diff --git a/lib/authenticationplugin.php b/lib/authenticationplugin.php
index 0a3763e2e..dbdf20629 100644
--- a/lib/authenticationplugin.php
+++ b/lib/authenticationplugin.php
@@ -22,6 +22,7 @@
* @category Plugin
* @package StatusNet
* @author Craig Andrews <candrews@integralblue.com>
+ * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
diff --git a/lib/authorizationplugin.php b/lib/authorizationplugin.php
index 07da9b2d1..d71f77243 100644
--- a/lib/authorizationplugin.php
+++ b/lib/authorizationplugin.php
@@ -22,6 +22,7 @@
* @category Plugin
* @package StatusNet
* @author Craig Andrews <candrews@integralblue.com>
+ * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
@@ -67,7 +68,7 @@ abstract class AuthorizationPlugin extends Plugin
//------------Below are the methods that connect StatusNet to the implementing Auth plugin------------\\
- function onStartSetUser(&$user) {
+ function onStartSetUser($user) {
$loginAllowed = $this->loginAllowed($user);
if($loginAllowed === true){
return;
@@ -84,7 +85,7 @@ abstract class AuthorizationPlugin extends Plugin
}
}
- function onStartSetApiUser(&$user) {
+ function onStartSetApiUser($user) {
return $this->onStartSetUser($user);
}
diff --git a/lib/avatarlink.php b/lib/avatarlink.php
index e67799e2e..7d4256d6e 100644
--- a/lib/avatarlink.php
+++ b/lib/avatarlink.php
@@ -76,8 +76,8 @@ class AvatarLink
$alink = new AvatarLink();
$alink->url = $filename;
$alink->height = $size;
+ $alink->width = $size;
if (!empty($filename)) {
- $alink->width = $size;
$alink->type = self::mediatype($filename);
} else {
$alink->url = User_group::defaultLogo($size);
diff --git a/lib/command.php b/lib/command.php
index 216f9e649..2b61a0c80 100644
--- a/lib/command.php
+++ b/lib/command.php
@@ -49,7 +49,7 @@ class Command
}
}
-
+
/**
* Override this with the meat!
*
@@ -80,15 +80,16 @@ class Command
$notice = Notice::staticGet(substr($arg,1));
if (!$notice) {
- throw new CommandException(_('Notice with that id does not exist'));
+ // TRANS: Command exception text shown when a notice ID is requested that does not exist.
+ throw new CommandException(_('Notice with that id does not exist.'));
}
}
-
+
if (Validate::uri($this->other)) {
// A specific notice by URI lookup
$notice = Notice::staticGet('uri', $arg);
}
-
+
if (!$notice) {
// Local or remote profile name to get their last notice.
// May throw an exception and report 'no such user'
@@ -96,13 +97,15 @@ class Command
$notice = $recipient->getCurrentNotice();
if (!$notice) {
- throw new CommandException(_('User has no last notice'));
+ // TRANS: Command exception text shown when a last user notice is requested and it does not exist.
+ throw new CommandException(_('User has no last notice.'));
}
}
}
Event::handle('EndCommandGetNotice', array($this, $arg, &$notice));
if (!$notice) {
- throw new CommandException(_('Notice with that id does not exist'));
+ // TRANS: Command exception text shown when a notice ID is requested that does not exist.
+ throw new CommandException(_('Notice with that id does not exist.'));
}
return $notice;
}
@@ -122,7 +125,9 @@ class Command
}
Event::handle('EndCommandGetProfile', array($this, $arg, &$profile));
if (!$profile) {
- throw new CommandException(sprintf(_('Could not find a user with nickname %s'), $arg));
+ // TRANS: Message given requesting a profile for a non-existing user.
+ // TRANS: %s is the nickname of the user for which the profile could not be found.
+ throw new CommandException(sprintf(_('Could not find a user with nickname %s.'), $arg));
}
return $profile;
}
@@ -140,7 +145,9 @@ class Command
}
Event::handle('EndCommandGetUser', array($this, $arg, &$user));
if (!$user){
- throw new CommandException(sprintf(_('Could not find a local user with nickname %s'),
+ // TRANS: Message given getting a non-existing user.
+ // TRANS: %s is the nickname of the user that could not be found.
+ throw new CommandException(sprintf(_('Could not find a local user with nickname %s.'),
$arg));
}
return $user;
@@ -159,6 +166,7 @@ class Command
}
Event::handle('EndCommandGetGroup', array($this, $arg, &$group));
if (!$group) {
+ // TRANS: Command exception text shown when a group is requested that does not exist.
throw new CommandException(_('No such group.'));
}
return $group;
@@ -173,6 +181,7 @@ class UnimplementedCommand extends Command
{
function handle($channel)
{
+ // TRANS: Error text shown when an unimplemented command is given.
$channel->error($this->user, _("Sorry, this command is not yet implemented."));
}
}
@@ -218,6 +227,7 @@ class NudgeCommand extends Command
{
$recipient = $this->getUser($this->other);
if ($recipient->id == $this->user->id) {
+ // TRANS: Command exception text shown when a user tries to nudge themselves.
throw new CommandException(_('It does not make a lot of sense to nudge yourself!'));
} else {
if ($recipient->email && $recipient->emailnotifynudge) {
@@ -225,7 +235,9 @@ class NudgeCommand extends Command
}
// XXX: notify by IM
// XXX: notify by SMS
- $channel->output($this->user, sprintf(_('Nudge sent to %s'),
+ // TRANS: Message given having nudged another user.
+ // TRANS: %s is the nickname of the user that was nudged.
+ $channel->output($this->user, sprintf(_('Nudge sent to %s.'),
$recipient->nickname));
}
}
@@ -251,6 +263,10 @@ class StatsCommand extends Command
$subbed_count = $profile->subscriberCount();
$notice_count = $profile->noticeCount();
+ // TRANS: User statistics text.
+ // TRANS: %1$s is the number of other user the user is subscribed to.
+ // TRANS: %2$s is the number of users that are subscribed to the user.
+ // TRANS: %3$s is the number of notices the user has sent.
$channel->output($this->user, sprintf(_("Subscriptions: %1\$s\n".
"Subscribers: %2\$s\n".
"Notices: %3\$s"),
@@ -276,6 +292,7 @@ class FavCommand extends Command
$fave = Fave::addNew($this->user->getProfile(), $notice);
if (!$fave) {
+ // TRANS: Error message text shown when a favorite could not be set.
$channel->error($this->user, _('Could not create favorite.'));
return;
}
@@ -293,6 +310,7 @@ class FavCommand extends Command
$this->user->blowFavesCache();
+ // TRANS: Text shown when a notice has been marked as favourite successfully.
$channel->output($this->user, _('Notice marked as fave.'));
}
@@ -314,10 +332,12 @@ class JoinCommand extends Command
$cur = $this->user;
if ($cur->isMember($group)) {
- $channel->error($cur, _('You are already a member of that group'));
+ // TRANS: Error text shown a user tries to join a group they already are a member of.
+ $channel->error($cur, _('You are already a member of that group.'));
return;
}
if (Group_block::isBlocked($group, $cur->getProfile())) {
+ // TRANS: Error text shown when a user tries to join a group they are blocked from joining.
$channel->error($cur, _('You have been blocked from that group by the admin.'));
return;
}
@@ -328,12 +348,16 @@ class JoinCommand extends Command
Event::handle('EndJoinGroup', array($group, $cur));
}
} catch (Exception $e) {
- $channel->error($cur, sprintf(_('Could not join user %s to group %s'),
+ // TRANS: Message given having failed to add a user to a group.
+ // TRANS: %1$s is the nickname of the user, %2$s is the nickname of the group.
+ $channel->error($cur, sprintf(_('Could not join user %1$s to group %2$s.'),
$cur->nickname, $group->nickname));
return;
}
- $channel->output($cur, sprintf(_('%s joined group %s'),
+ // TRANS: Message given having added a user to a group.
+ // TRANS: %1$s is the nickname of the user, %2$s is the nickname of the group.
+ $channel->output($cur, sprintf(_('%1$s joined group %2$s.'),
$cur->nickname,
$group->nickname));
}
@@ -355,11 +379,13 @@ class DropCommand extends Command
$cur = $this->user;
if (!$group) {
+ // TRANS: Error text shown when trying to leave a group that does not exist.
$channel->error($cur, _('No such group.'));
return;
}
if (!$cur->isMember($group)) {
+ // TRANS: Error text shown when trying to leave an existing group the user is not a member of.
$channel->error($cur, _('You are not a member of that group.'));
return;
}
@@ -370,12 +396,16 @@ class DropCommand extends Command
Event::handle('EndLeaveGroup', array($group, $cur));
}
} catch (Exception $e) {
- $channel->error($cur, sprintf(_('Could not remove user %s to group %s'),
+ // TRANS: Message given having failed to remove a user from a group.
+ // TRANS: %1$s is the nickname of the user, %2$s is the nickname of the group.
+ $channel->error($cur, sprintf(_('Could not remove user %1$s from group %2$s.'),
$cur->nickname, $group->nickname));
return;
}
- $channel->output($cur, sprintf(_('%s left group %s'),
+ // TRANS: Message given having removed a user from a group.
+ // TRANS: %1$s is the nickname of the user, %2$s is the nickname of the group.
+ $channel->output($cur, sprintf(_('%1$s left group %2$s.'),
$cur->nickname,
$group->nickname));
}
@@ -395,18 +425,24 @@ class WhoisCommand extends Command
{
$recipient = $this->getProfile($this->other);
+ // TRANS: Whois output.
+ // TRANS: %1$s nickname of the queried user, %2$s is their profile URL.
$whois = sprintf(_("%1\$s (%2\$s)"), $recipient->nickname,
$recipient->profileurl);
if ($recipient->fullname) {
+ // TRANS: Whois output. %s is the full name of the queried user.
$whois .= "\n" . sprintf(_('Fullname: %s'), $recipient->fullname);
}
if ($recipient->location) {
+ // TRANS: Whois output. %s is the location of the queried user.
$whois .= "\n" . sprintf(_('Location: %s'), $recipient->location);
}
if ($recipient->homepage) {
+ // TRANS: Whois output. %s is the homepage of the queried user.
$whois .= "\n" . sprintf(_('Homepage: %s'), $recipient->homepage);
}
if ($recipient->bio) {
+ // TRANS: Whois output. %s is the bio information of the queried user.
$whois .= "\n" . sprintf(_('About: %s'), $recipient->bio);
}
$channel->output($this->user, $whois);
@@ -434,12 +470,14 @@ class MessageCommand extends Command
} catch (CommandException $f) {
throw $e;
}
+ // TRANS: Command exception text shown when trying to send a direct message to a remote user (a user not registered at the current server).
throw new CommandException(sprintf(_('%s is a remote profile; you can only send direct messages to users on the same server.'), $this->other));
}
$len = mb_strlen($this->text);
if ($len == 0) {
+ // TRANS: Command exception text shown when trying to send a direct message to another user without content.
$channel->error($this->user, _('No content!'));
return;
}
@@ -447,26 +485,35 @@ class MessageCommand extends Command
$this->text = common_shorten_links($this->text);
if (Message::contentTooLong($this->text)) {
- $channel->error($this->user, sprintf(_('Message too long - maximum is %d characters, you sent %d'),
+ // XXX: i18n. Needs plural support.
+ // TRANS: Message given if content is too long.
+ // TRANS: %1$d is the maximum number of characters, %2$d is the number of submitted characters.
+ $channel->error($this->user, sprintf(_('Message too long - maximum is %1$d characters, you sent %2$d.'),
Message::maxContent(), mb_strlen($this->text)));
return;
}
if (!$other) {
+ // TRANS: Error text shown when trying to send a direct message to a user that does not exist.
$channel->error($this->user, _('No such user.'));
return;
} else if (!$this->user->mutuallySubscribed($other)) {
+ // TRANS: Error text shown when trying to send a direct message to a user without a mutual subscription (each user must be subscribed to the other).
$channel->error($this->user, _('You can\'t send a message to this user.'));
return;
} else if ($this->user->id == $other->id) {
+ // TRANS: Error text shown when trying to send a direct message to self.
$channel->error($this->user, _('Don\'t send a message to yourself; just say it to yourself quietly instead.'));
return;
}
$message = Message::saveNew($this->user->id, $other->id, $this->text, $channel->source());
if ($message) {
$message->notify();
- $channel->output($this->user, sprintf(_('Direct message to %s sent'), $this->other));
+ // TRANS: Message given have sent a direct message to another user.
+ // TRANS: %s is the name of the other user.
+ $channel->output($this->user, sprintf(_('Direct message to %s sent.'), $this->other));
} else {
+ // TRANS: Error text shown sending a direct message fails with an unknown reason.
$channel->error($this->user, _('Error sending direct message.'));
}
}
@@ -487,12 +534,14 @@ class RepeatCommand extends Command
if($this->user->id == $notice->profile_id)
{
- $channel->error($this->user, _('Cannot repeat your own notice'));
+ // TRANS: Error text shown when trying to repeat an own notice.
+ $channel->error($this->user, _('Cannot repeat your own notice.'));
return;
}
if ($this->user->getProfile()->hasRepeated($notice->id)) {
- $channel->error($this->user, _('Already repeated that notice'));
+ // TRANS: Error text shown when trying to repeat an notice that was already repeated by the user.
+ $channel->error($this->user, _('Already repeated that notice.'));
return;
}
@@ -500,8 +549,11 @@ class RepeatCommand extends Command
if ($repeat) {
- $channel->output($this->user, sprintf(_('Notice from %s repeated'), $recipient->nickname));
+ // TRANS: Message given having repeated a notice from another user.
+ // TRANS: %s is the name of the user for which the notice was repeated.
+ $channel->output($this->user, sprintf(_('Notice from %s repeated.'), $recipient->nickname));
} else {
+ // TRANS: Error text shown when repeating a notice fails with an unknown reason.
$channel->error($this->user, _('Error repeating notice.'));
}
}
@@ -526,6 +578,7 @@ class ReplyCommand extends Command
$len = mb_strlen($this->text);
if ($len == 0) {
+ // TRANS: Command exception text shown when trying to reply to a notice without providing content for the reply.
$channel->error($this->user, _('No content!'));
return;
}
@@ -533,7 +586,10 @@ class ReplyCommand extends Command
$this->text = common_shorten_links($this->text);
if (Notice::contentTooLong($this->text)) {
- $channel->error($this->user, sprintf(_('Notice too long - maximum is %d characters, you sent %d'),
+ // XXX: i18n. Needs plural support.
+ // TRANS: Message given if content of a notice for a reply is too long.
+ // TRANS: %1$d is the maximum number of characters, %2$d is the number of submitted characters.
+ $channel->error($this->user, sprintf(_('Notice too long - maximum is %1$d characters, you sent %2$d.'),
Notice::maxContent(), mb_strlen($this->text)));
return;
}
@@ -542,8 +598,11 @@ class ReplyCommand extends Command
array('reply_to' => $notice->id));
if ($notice) {
- $channel->output($this->user, sprintf(_('Reply to %s sent'), $recipient->nickname));
+ // TRANS: Text shown having sent a reply to a notice successfully.
+ // TRANS: %s is the nickname of the user of the notice the reply was sent to.
+ $channel->output($this->user, sprintf(_('Reply to %s sent.'), $recipient->nickname));
} else {
+ // TRANS: Error text shown when a reply to a notice fails with an unknown reason.
$channel->error($this->user, _('Error saving notice.'));
}
@@ -567,7 +626,8 @@ class GetCommand extends Command
$notice = $target->getCurrentNotice();
if (!$notice) {
- $channel->error($this->user, _('User has no last notice'));
+ // TRANS: Error text shown when a last user notice is requested and it does not exist.
+ $channel->error($this->user, _('User has no last notice.'));
return;
}
$notice_content = $notice->content;
@@ -591,7 +651,8 @@ class SubCommand extends Command
{
if (!$this->other) {
- $channel->error($this->user, _('Specify the name of the user to subscribe to'));
+ // TRANS: Error text shown when no username was provided when issuing a subscribe command.
+ $channel->error($this->user, _('Specify the name of the user to subscribe to.'));
return;
}
@@ -599,13 +660,16 @@ class SubCommand extends Command
$remote = Remote_profile::staticGet('id', $target->id);
if ($remote) {
+ // TRANS: Command exception text shown when trying to subscribe to an OMB profile using the subscribe command.
throw new CommandException(_("Can't subscribe to OMB profiles by command."));
}
try {
Subscription::start($this->user->getProfile(),
$target);
- $channel->output($this->user, sprintf(_('Subscribed to %s'), $this->other));
+ // TRANS: Text shown after having subscribed to another user successfully.
+ // TRANS: %s is the name of the user the subscription was requested for.
+ $channel->output($this->user, sprintf(_('Subscribed to %s.'), $this->other));
} catch (Exception $e) {
$channel->error($this->user, $e->getMessage());
}
@@ -626,7 +690,8 @@ class UnsubCommand extends Command
function handle($channel)
{
if(!$this->other) {
- $channel->error($this->user, _('Specify the name of the user to unsubscribe from'));
+ // TRANS: Error text shown when no username was provided when issuing an unsubscribe command.
+ $channel->error($this->user, _('Specify the name of the user to unsubscribe from.'));
return;
}
@@ -635,7 +700,9 @@ class UnsubCommand extends Command
try {
Subscription::cancel($this->user->getProfile(),
$target);
- $channel->output($this->user, sprintf(_('Unsubscribed from %s'), $this->other));
+ // TRANS: Text shown after having unsubscribed from another user successfully.
+ // TRANS: %s is the name of the user the unsubscription was requested for.
+ $channel->output($this->user, sprintf(_('Unsubscribed from %s.'), $this->other));
} catch (Exception $e) {
$channel->error($this->user, $e->getMessage());
}
@@ -653,11 +720,14 @@ class OffCommand extends Command
function handle($channel)
{
if ($other) {
+ // TRANS: Error text shown when issuing the command "off" with a setting which has not yet been implemented.
$channel->error($this->user, _("Command not yet implemented."));
} else {
if ($channel->off($this->user)) {
+ // TRANS: Text shown when issuing the command "off" successfully.
$channel->output($this->user, _('Notification off.'));
} else {
+ // TRANS: Error text shown when the command "off" fails for an unknown reason.
$channel->error($this->user, _('Can\'t turn off notification.'));
}
}
@@ -676,11 +746,14 @@ class OnCommand extends Command
function handle($channel)
{
if ($other) {
+ // TRANS: Error text shown when issuing the command "on" with a setting which has not yet been implemented.
$channel->error($this->user, _("Command not yet implemented."));
} else {
if ($channel->on($this->user)) {
+ // TRANS: Text shown when issuing the command "on" successfully.
$channel->output($this->user, _('Notification on.'));
} else {
+ // TRANS: Error text shown when the command "on" fails for an unknown reason.
$channel->error($this->user, _('Can\'t turn on notification.'));
}
}
@@ -694,7 +767,8 @@ class LoginCommand extends Command
$disabled = common_config('logincommand','disabled');
$disabled = isset($disabled) && $disabled;
if($disabled) {
- $channel->error($this->user, _('Login command is disabled'));
+ // TRANS: Error text shown when issuing the login command while login is disabled.
+ $channel->error($this->user, _('Login command is disabled.'));
return;
}
@@ -705,7 +779,9 @@ class LoginCommand extends Command
}
$channel->output($this->user,
- sprintf(_('This link is useable only once, and is good for only 2 minutes: %s'),
+ // TRANS: Text shown after issuing the login command successfully.
+ // TRANS: %s is a logon link..
+ sprintf(_('This link is useable only once and is valid for only 2 minutes: %s.'),
common_local_url('otp',
array('user_id' => $login_token->user_id, 'token' => $login_token->token))));
}
@@ -713,7 +789,6 @@ class LoginCommand extends Command
class LoseCommand extends Command
{
-
var $other = null;
function __construct($user, $other)
@@ -725,14 +800,17 @@ class LoseCommand extends Command
function execute($channel)
{
if(!$this->other) {
- $channel->error($this->user, _('Specify the name of the user to unsubscribe from'));
+ // TRANS: Error text shown when no username was provided when issuing the command.
+ $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));
+ // TRANS: Text shown after issuing the lose command successfully (stop another user from following the current user).
+ // TRANS: %s is the name of the user the unsubscription was requested for.
+ $channel->output($this->user, sprintf(_('Unsubscribed %s.'), $this->other));
} else {
$channel->error($this->user, $result);
}
@@ -749,8 +827,12 @@ class SubscriptionsCommand extends Command
$nicknames[]=$profile->nickname;
}
if(count($nicknames)==0){
+ // TRANS: Text shown after requesting other users a user is subscribed to without having any subscriptions.
$out=_('You are not subscribed to anyone.');
}else{
+ // TRANS: Text shown after requesting other users a user is subscribed to.
+ // TRANS: This message support plural forms. This message is followed by a
+ // TRANS: hard coded space and a comma separated list of subscribed users.
$out = ngettext('You are subscribed to this person:',
'You are subscribed to these people:',
count($nicknames));
@@ -771,8 +853,13 @@ class SubscribersCommand extends Command
$nicknames[]=$profile->nickname;
}
if(count($nicknames)==0){
+ // TRANS: Text shown after requesting other users that are subscribed to a user
+ // TRANS: (followers) without having any subscribers.
$out=_('No one is subscribed to you.');
}else{
+ // TRANS: Text shown after requesting other users that are subscribed to a user (followers).
+ // TRANS: This message support plural forms. This message is followed by a
+ // TRANS: hard coded space and a comma separated list of subscribing users.
$out = ngettext('This person is subscribed to you:',
'These people are subscribed to you:',
count($nicknames));
@@ -793,8 +880,13 @@ class GroupsCommand extends Command
$groups[]=$group->nickname;
}
if(count($groups)==0){
+ // TRANS: Text shown after requesting groups a user is subscribed to without having
+ // TRANS: any group subscriptions.
$out=_('You are not a member of any groups.');
}else{
+ // TRANS: Text shown after requesting groups a user is subscribed to.
+ // TRANS: This message support plural forms. This message is followed by a
+ // TRANS: hard coded space and a comma separated list of subscribed groups.
$out = ngettext('You are a member of this group:',
'You are a member of these groups:',
count($nicknames));
@@ -808,6 +900,7 @@ class HelpCommand extends Command
{
function handle($channel)
{
+ // TRANS: Help text for commands.
$channel->output($this->user,
_("Commands:\n".
"on - turn on notifications\n".
diff --git a/lib/common.php b/lib/common.php
index 8d2e6b420..097f19268 100644
--- a/lib/common.php
+++ b/lib/common.php
@@ -22,10 +22,10 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
//exit with 200 response, if this is checking fancy from the installer
if (isset($_REQUEST['p']) && $_REQUEST['p'] == 'check-fancy') { exit; }
-define('STATUSNET_VERSION', '0.9.1');
+define('STATUSNET_VERSION', '0.9.4');
define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility
-define('STATUSNET_CODENAME', 'Everybody Hurts');
+define('STATUSNET_CODENAME', 'Orange Crush');
define('AVATAR_PROFILE_SIZE', 96);
define('AVATAR_STREAM_SIZE', 48);
diff --git a/lib/dbqueuemanager.php b/lib/dbqueuemanager.php
index 3032e4ec7..3dda9fd1a 100644
--- a/lib/dbqueuemanager.php
+++ b/lib/dbqueuemanager.php
@@ -135,9 +135,7 @@ class DBQueueManager extends QueueManager
if (empty($qi->claimed)) {
$this->_log(LOG_WARNING, "[$queue:item $qi->id] Ignoring failure for unclaimed queue item");
} else {
- $orig = clone($qi);
- $qi->claimed = null;
- $qi->update($orig);
+ $qi->releaseClaim();
}
$this->stats('error', $queue);
diff --git a/lib/default.php b/lib/default.php
index ffeab70b3..45a4560ff 100644
--- a/lib/default.php
+++ b/lib/default.php
@@ -72,6 +72,7 @@ $default =
'quote_identifiers' => false,
'type' => 'mysql',
'schemacheck' => 'runtime', // 'runtime' or 'script'
+ 'annotate_queries' => false, // true to add caller comments to queries, eg /* POST Notice::saveNew */
'log_queries' => false, // true to log all DB queries
'log_slow_queries' => 0), // if set, log queries taking over N seconds
'syslog' =>
@@ -87,6 +88,8 @@ $default =
'stomp_username' => null,
'stomp_password' => null,
'stomp_persistent' => true, // keep items across queue server restart, if persistence is enabled
+ 'stomp_transactions' => true, // use STOMP transactions to aid in detecting failures (supported by ActiveMQ, but not by all)
+ 'stomp_acks' => true, // send acknowledgements after successful processing (supported by ActiveMQ, but not by all)
'stomp_manual_failover' => true, // if multiple servers are listed, treat them as separate (enqueue on one randomly, listen on all)
'monitor' => null, // URL to monitor ping endpoint (work in progress)
'softlimit' => '90%', // total size or % of memory_limit at which to restart queue threads gracefully
@@ -138,10 +141,17 @@ $default =
'dir' => null,
'path'=> null,
'ssl' => null),
+ 'theme_upload' =>
+ array('enabled' => extension_loaded('zip')),
'javascript' =>
array('server' => null,
'path'=> null,
'ssl' => null),
+ 'local' => // To override path/server for themes in 'local' dir (not currently applied to local plugins)
+ array('server' => null,
+ 'dir' => null,
+ 'path' => null,
+ 'ssl' => null),
'throttle' =>
array('enabled' => false, // whether to throttle edits; false by default
'count' => 20, // number of allowed messages in timespan
@@ -185,7 +195,8 @@ $default =
'cache' =>
array('base' => null),
'ping' =>
- array('notify' => array()),
+ array('notify' => array(),
+ 'timeout' => 2),
'inboxes' =>
array('enabled' => true), # ignored after 0.9.x
'newuser' =>
@@ -256,6 +267,9 @@ $default =
'linkcolor' => null,
'backgroundimage' => null,
'disposition' => null),
+ 'custom_css' =>
+ array('enabled' => true,
+ 'css' => ''),
'notice' =>
array('contentlimit' => null),
'message' =>
@@ -300,4 +314,8 @@ $default =
array('subscribers' => true,
'members' => true,
'peopletag' => true),
+ 'http' => // HTTP client settings when contacting other sites
+ array('ssl_cafile' => false, // To enable SSL cert validation, point to a CA bundle (eg '/usr/lib/ssl/certs/ca-certificates.crt')
+ 'curl' => false, // Use CURL backend for HTTP fetches if available. (If not, PHP's socket streams will be used.)
+ ),
);
diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php
index 7786b5941..5dc2b38da 100644
--- a/lib/htmloutputter.php
+++ b/lib/htmloutputter.php
@@ -100,6 +100,7 @@ class HTMLOutputter extends XMLOutputter
$type = common_negotiate_type($cp, $sp);
if (!$type) {
+ // TRANS: Client exception 406
throw new ClientException(_('This page is not available in a '.
'media type you accept'), 406);
}
diff --git a/lib/httpclient.php b/lib/httpclient.php
index 64a51353c..514a5afeb 100644
--- a/lib/httpclient.php
+++ b/lib/httpclient.php
@@ -43,6 +43,9 @@ require_once 'HTTP/Request2/Response.php';
*
* This extends the HTTP_Request2_Response class with methods to get info
* about any followed redirects.
+ *
+ * Originally used the name 'HTTPResponse' to match earlier code, but
+ * this conflicts with a class in in the PECL HTTP extension.
*
* @category HTTP
* @package StatusNet
@@ -51,7 +54,7 @@ require_once 'HTTP/Request2/Response.php';
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
-class HTTPResponse extends HTTP_Request2_Response
+class StatusNet_HTTPResponse extends HTTP_Request2_Response
{
function __construct(HTTP_Request2_Response $response, $url, $redirects=0)
{
@@ -129,7 +132,23 @@ class HTTPClient extends HTTP_Request2
// ought to be investigated to see if we can handle
// it gracefully in that case as well.
$this->config['protocol_version'] = '1.0';
-
+
+ // Default state of OpenSSL seems to have no trusted
+ // SSL certificate authorities, which breaks hostname
+ // verification and means we have a hard time communicating
+ // with other sites' HTTPS interfaces.
+ //
+ // Turn off verification unless we've configured a CA bundle.
+ if (common_config('http', 'ssl_cafile')) {
+ $this->config['ssl_cafile'] = common_config('http', 'ssl_cafile');
+ } else {
+ $this->config['ssl_verify_peer'] = false;
+ }
+
+ if (common_config('http', 'curl') && extension_loaded('curl')) {
+ $this->config['adapter'] = 'HTTP_Request2_Adapter_Curl';
+ }
+
parent::__construct($url, $method, $config);
$this->setHeader('User-Agent', $this->userAgent());
}
@@ -146,7 +165,7 @@ class HTTPClient extends HTTP_Request2
/**
* Convenience function to run a GET request.
*
- * @return HTTPResponse
+ * @return StatusNet_HTTPResponse
* @throws HTTP_Request2_Exception
*/
public function get($url, $headers=array())
@@ -157,7 +176,7 @@ class HTTPClient extends HTTP_Request2
/**
* Convenience function to run a HEAD request.
*
- * @return HTTPResponse
+ * @return StatusNet_HTTPResponse
* @throws HTTP_Request2_Exception
*/
public function head($url, $headers=array())
@@ -171,7 +190,7 @@ class HTTPClient extends HTTP_Request2
* @param string $url
* @param array $headers optional associative array of HTTP headers
* @param array $data optional associative array or blob of form data to submit
- * @return HTTPResponse
+ * @return StatusNet_HTTPResponse
* @throws HTTP_Request2_Exception
*/
public function post($url, $headers=array(), $data=array())
@@ -183,12 +202,21 @@ class HTTPClient extends HTTP_Request2
}
/**
- * @return HTTPResponse
+ * @return StatusNet_HTTPResponse
* @throws HTTP_Request2_Exception
*/
protected function doRequest($url, $method, $headers)
{
$this->setUrl($url);
+
+ // Workaround for HTTP_Request2 not setting up SNI in socket contexts;
+ // This fixes cert validation for SSL virtual hosts using SNI.
+ // Requires PHP 5.3.2 or later and OpenSSL with SNI support.
+ if ($this->url->getScheme() == 'https' && defined('OPENSSL_TLSEXT_SERVER_NAME')) {
+ $this->config['ssl_SNI_enabled'] = true;
+ $this->config['ssl_SNI_server_name'] = $this->url->getHost();
+ }
+
$this->setMethod($method);
if ($headers) {
foreach ($headers as $header) {
@@ -217,12 +245,12 @@ class HTTPClient extends HTTP_Request2
}
/**
- * Actually performs the HTTP request and returns an HTTPResponse object
- * with response body and header info.
+ * Actually performs the HTTP request and returns a
+ * StatusNet_HTTPResponse object with response body and header info.
*
* Wraps around parent send() to add logging and redirection processing.
*
- * @return HTTPResponse
+ * @return StatusNet_HTTPResponse
* @throw HTTP_Request2_Exception
*/
public function send()
@@ -265,6 +293,6 @@ class HTTPClient extends HTTP_Request2
}
break;
} while ($maxRedirs);
- return new HTTPResponse($response, $this->getUrl(), $redirs);
+ return new StatusNet_HTTPResponse($response, $this->getUrl(), $redirs);
}
}
diff --git a/lib/installer.php b/lib/installer.php
new file mode 100644
index 000000000..2eff2d85a
--- /dev/null
+++ b/lib/installer.php
@@ -0,0 +1,585 @@
+<?php
+
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, 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/>.
+ *
+ * @category Installation
+ * @package Installation
+ *
+ * @author Adrian Lang <mail@adrianlang.de>
+ * @author Brenda Wallace <shiny@cpan.org>
+ * @author Brett Taylor <brett@webfroot.co.nz>
+ * @author Brion Vibber <brion@pobox.com>
+ * @author CiaranG <ciaran@ciarang.com>
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @author Eric Helgeson <helfire@Erics-MBP.local>
+ * @author Evan Prodromou <evan@status.net>
+ * @author Robin Millette <millette@controlyourself.ca>
+ * @author Sarven Capadisli <csarven@status.net>
+ * @author Tom Adams <tom@holizz.com>
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
+ * @license GNU Affero General Public License http://www.gnu.org/licenses/
+ * @version 0.9.x
+ * @link http://status.net
+ */
+
+abstract class Installer
+{
+ /** Web site info */
+ public $sitename, $server, $path, $fancy;
+ /** DB info */
+ public $host, $dbname, $dbtype, $username, $password, $db;
+ /** Administrator info */
+ public $adminNick, $adminPass, $adminEmail, $adminUpdates;
+ /** Should we skip writing the configuration file? */
+ public $skipConfig = false;
+
+ public static $dbModules = array(
+ 'mysql' => array(
+ 'name' => 'MySQL',
+ 'check_module' => 'mysqli',
+ 'installer' => 'mysql_db_installer',
+ ),
+ 'pgsql' => array(
+ 'name' => 'PostgreSQL',
+ 'check_module' => 'pgsql',
+ 'installer' => 'pgsql_db_installer',
+ ),
+ );
+
+ /**
+ * Attempt to include a PHP file and report if it worked, while
+ * suppressing the annoying warning messages on failure.
+ */
+ private function haveIncludeFile($filename) {
+ $old = error_reporting(error_reporting() & ~E_WARNING);
+ $ok = include_once($filename);
+ error_reporting($old);
+ return $ok;
+ }
+
+ /**
+ * Check if all is ready for installation
+ *
+ * @return void
+ */
+ function checkPrereqs()
+ {
+ $pass = true;
+
+ $config = INSTALLDIR.'/config.php';
+ if (file_exists($config)) {
+ if (!is_writable($config) || filesize($config) > 0) {
+ $this->warning('Config file "config.php" already exists.');
+ $pass = false;
+ }
+ }
+
+ if (version_compare(PHP_VERSION, '5.2.3', '<')) {
+ $this->warning('Require PHP version 5.2.3 or greater.');
+ $pass = false;
+ }
+
+ // Look for known library bugs
+ $str = "abcdefghijklmnopqrstuvwxyz";
+ $replaced = preg_replace('/[\p{Cc}\p{Cs}]/u', '*', $str);
+ if ($str != $replaced) {
+ $this->warning('PHP is linked to a version of the PCRE library ' .
+ 'that does not support Unicode properties. ' .
+ 'If you are running Red Hat Enterprise Linux / ' .
+ 'CentOS 5.4 or earlier, see <a href="' .
+ 'http://status.net/wiki/Red_Hat_Enterprise_Linux#PCRE_library' .
+ '">our documentation page</a> on fixing this.');
+ $pass = false;
+ }
+
+ $reqs = array('gd', 'curl',
+ 'xmlwriter', 'mbstring', 'xml', 'dom', 'simplexml');
+
+ foreach ($reqs as $req) {
+ if (!$this->checkExtension($req)) {
+ $this->warning(sprintf('Cannot load required extension: <code>%s</code>', $req));
+ $pass = false;
+ }
+ }
+
+ // Make sure we have at least one database module available
+ $missingExtensions = array();
+ foreach (self::$dbModules as $type => $info) {
+ if (!$this->checkExtension($info['check_module'])) {
+ $missingExtensions[] = $info['check_module'];
+ }
+ }
+
+ if (count($missingExtensions) == count(self::$dbModules)) {
+ $req = implode(', ', $missingExtensions);
+ $this->warning(sprintf('Cannot find a database extension. You need at least one of %s.', $req));
+ $pass = false;
+ }
+
+ // @fixme this check seems to be insufficient with Windows ACLs
+ if (!is_writable(INSTALLDIR)) {
+ $this->warning(sprintf('Cannot write config file to: <code>%s</code></p>', INSTALLDIR),
+ sprintf('On your server, try this command: <code>chmod a+w %s</code>', INSTALLDIR));
+ $pass = false;
+ }
+
+ // Check the subdirs used for file uploads
+ $fileSubdirs = array('avatar', 'background', 'file');
+ foreach ($fileSubdirs as $fileSubdir) {
+ $fileFullPath = INSTALLDIR."/$fileSubdir/";
+ if (!is_writable($fileFullPath)) {
+ $this->warning(sprintf('Cannot write to %s directory: <code>%s</code>', $fileSubdir, $fileFullPath),
+ sprintf('On your server, try this command: <code>chmod a+w %s</code>', $fileFullPath));
+ $pass = false;
+ }
+ }
+
+ return $pass;
+ }
+
+ /**
+ * Checks if a php extension is both installed and loaded
+ *
+ * @param string $name of extension to check
+ *
+ * @return boolean whether extension is installed and loaded
+ */
+ function checkExtension($name)
+ {
+ if (extension_loaded($name)) {
+ return true;
+ } elseif (function_exists('dl') && ini_get('enable_dl') && !ini_get('safe_mode')) {
+ // dl will throw a fatal error if it's disabled or we're in safe mode.
+ // More fun, it may not even exist under some SAPIs in 5.3.0 or later...
+ $soname = $name . '.' . PHP_SHLIB_SUFFIX;
+ if (PHP_SHLIB_SUFFIX == 'dll') {
+ $soname = "php_" . $soname;
+ }
+ return @dl($soname);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Basic validation on the database paramters
+ * Side effects: error output if not valid
+ *
+ * @return boolean success
+ */
+ function validateDb()
+ {
+ $fail = false;
+
+ if (empty($this->host)) {
+ $this->updateStatus("No hostname specified.", true);
+ $fail = true;
+ }
+
+ if (empty($this->database)) {
+ $this->updateStatus("No database specified.", true);
+ $fail = true;
+ }
+
+ if (empty($this->username)) {
+ $this->updateStatus("No username specified.", true);
+ $fail = true;
+ }
+
+ if (empty($this->sitename)) {
+ $this->updateStatus("No sitename specified.", true);
+ $fail = true;
+ }
+
+ return !$fail;
+ }
+
+ /**
+ * Basic validation on the administrator user paramters
+ * Side effects: error output if not valid
+ *
+ * @return boolean success
+ */
+ function validateAdmin()
+ {
+ $fail = false;
+
+ if (empty($this->adminNick)) {
+ $this->updateStatus("No initial StatusNet user nickname specified.", true);
+ $fail = true;
+ }
+ if ($this->adminNick && !preg_match('/^[0-9a-z]{1,64}$/', $this->adminNick)) {
+ $this->updateStatus('The user nickname "' . htmlspecialchars($this->adminNick) .
+ '" is invalid; should be plain letters and numbers no longer than 64 characters.', true);
+ $fail = true;
+ }
+ // @fixme hardcoded list; should use User::allowed_nickname()
+ // if/when it's safe to have loaded the infrastructure here
+ $blacklist = array('main', 'admin', 'twitter', 'settings', 'rsd.xml', 'favorited', 'featured', 'favoritedrss', 'featuredrss', 'rss', 'getfile', 'api', 'groups', 'group', 'peopletag', 'tag', 'user', 'message', 'conversation', 'bookmarklet', 'notice', 'attachment', 'search', 'index.php', 'doc', 'opensearch', 'robots.txt', 'xd_receiver.html', 'facebook');
+ if (in_array($this->adminNick, $blacklist)) {
+ $this->updateStatus('The user nickname "' . htmlspecialchars($this->adminNick) .
+ '" is reserved.', true);
+ $fail = true;
+ }
+
+ if (empty($this->adminPass)) {
+ $this->updateStatus("No initial StatusNet user password specified.", true);
+ $fail = true;
+ }
+
+ return !$fail;
+ }
+
+ /**
+ * Set up the database with the appropriate function for the selected type...
+ * Saves database info into $this->db.
+ *
+ * @return mixed array of database connection params on success, false on failure
+ */
+ function setupDatabase()
+ {
+ if ($this->db) {
+ throw new Exception("Bad order of operations: DB already set up.");
+ }
+ $method = self::$dbModules[$this->dbtype]['installer'];
+ $db = call_user_func(array($this, $method),
+ $this->host,
+ $this->database,
+ $this->username,
+ $this->password);
+ $this->db = $db;
+ return $this->db;
+ }
+
+ /**
+ * Set up a database on PostgreSQL.
+ * Will output status updates during the operation.
+ *
+ * @param string $host
+ * @param string $database
+ * @param string $username
+ * @param string $password
+ * @return mixed array of database connection params on success, false on failure
+ *
+ * @fixme escape things in the connection string in case we have a funny pass etc
+ */
+ function Pgsql_Db_installer($host, $database, $username, $password)
+ {
+ $connstring = "dbname=$database host=$host user=$username";
+
+ //No password would mean trust authentication used.
+ if (!empty($password)) {
+ $connstring .= " password=$password";
+ }
+ $this->updateStatus("Starting installation...");
+ $this->updateStatus("Checking database...");
+ $conn = pg_connect($connstring);
+
+ if ($conn ===false) {
+ $this->updateStatus("Failed to connect to database: $connstring");
+ return false;
+ }
+
+ //ensure database encoding is UTF8
+ $record = pg_fetch_object(pg_query($conn, 'SHOW server_encoding'));
+ if ($record->server_encoding != 'UTF8') {
+ $this->updateStatus("StatusNet requires UTF8 character encoding. Your database is ". htmlentities($record->server_encoding));
+ return false;
+ }
+
+ $this->updateStatus("Running database script...");
+ //wrap in transaction;
+ pg_query($conn, 'BEGIN');
+ $res = $this->runDbScript('statusnet_pg.sql', $conn, 'pgsql');
+
+ if ($res === false) {
+ $this->updateStatus("Can't run database script.", true);
+ return false;
+ }
+ foreach (array('sms_carrier' => 'SMS carrier',
+ 'notice_source' => 'notice source',
+ 'foreign_services' => 'foreign service')
+ as $scr => $name) {
+ $this->updateStatus(sprintf("Adding %s data to database...", $name));
+ $res = $this->runDbScript($scr.'.sql', $conn, 'pgsql');
+ if ($res === false) {
+ $this->updateStatus(sprintf("Can't run %s script.", $name), true);
+ return false;
+ }
+ }
+ pg_query($conn, 'COMMIT');
+
+ if (empty($password)) {
+ $sqlUrl = "pgsql://$username@$host/$database";
+ } else {
+ $sqlUrl = "pgsql://$username:$password@$host/$database";
+ }
+
+ $db = array('type' => 'pgsql', 'database' => $sqlUrl);
+
+ return $db;
+ }
+
+ /**
+ * Set up a database on MySQL.
+ * Will output status updates during the operation.
+ *
+ * @param string $host
+ * @param string $database
+ * @param string $username
+ * @param string $password
+ * @return mixed array of database connection params on success, false on failure
+ *
+ * @fixme escape things in the connection string in case we have a funny pass etc
+ */
+ function Mysql_Db_installer($host, $database, $username, $password)
+ {
+ $this->updateStatus("Starting installation...");
+ $this->updateStatus("Checking database...");
+
+ $conn = mysqli_init();
+ if (!$conn->real_connect($host, $username, $password)) {
+ $this->updateStatus("Can't connect to server '$host' as '$username'.", true);
+ return false;
+ }
+ $this->updateStatus("Changing to database...");
+ if (!$conn->select_db($database)) {
+ $this->updateStatus("Can't change to database.", true);
+ return false;
+ }
+
+ $this->updateStatus("Running database script...");
+ $res = $this->runDbScript('statusnet.sql', $conn);
+ if ($res === false) {
+ $this->updateStatus("Can't run database script.", true);
+ return false;
+ }
+ foreach (array('sms_carrier' => 'SMS carrier',
+ 'notice_source' => 'notice source',
+ 'foreign_services' => 'foreign service')
+ as $scr => $name) {
+ $this->updateStatus(sprintf("Adding %s data to database...", $name));
+ $res = $this->runDbScript($scr.'.sql', $conn);
+ if ($res === false) {
+ $this->updateStatus(sprintf("Can't run %d script.", $name), true);
+ return false;
+ }
+ }
+
+ $sqlUrl = "mysqli://$username:$password@$host/$database";
+ $db = array('type' => 'mysql', 'database' => $sqlUrl);
+ return $db;
+ }
+
+ /**
+ * Write a stock configuration file.
+ *
+ * @return boolean success
+ *
+ * @fixme escape variables in output in case we have funny chars, apostrophes etc
+ */
+ function writeConf()
+ {
+ // assemble configuration file in a string
+ $cfg = "<?php\n".
+ "if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }\n\n".
+
+ // site name
+ "\$config['site']['name'] = '{$this->sitename}';\n\n".
+
+ // site location
+ "\$config['site']['server'] = '{$this->server}';\n".
+ "\$config['site']['path'] = '{$this->path}'; \n\n".
+
+ // checks if fancy URLs are enabled
+ ($this->fancy ? "\$config['site']['fancy'] = true;\n\n":'').
+
+ // database
+ "\$config['db']['database'] = '{$this->db['database']}';\n\n".
+ ($this->db['type'] == 'pgsql' ? "\$config['db']['quote_identifiers'] = true;\n\n":'').
+ "\$config['db']['type'] = '{$this->db['type']}';\n\n";
+
+ // Normalize line endings for Windows servers
+ $cfg = str_replace("\n", PHP_EOL, $cfg);
+
+ // write configuration file out to install directory
+ $res = file_put_contents(INSTALLDIR.'/config.php', $cfg);
+
+ return $res;
+ }
+
+ /**
+ * Install schema into the database
+ *
+ * @param string $filename location of database schema file
+ * @param dbconn $conn connection to database
+ * @param string $type type of database, currently mysql or pgsql
+ *
+ * @return boolean - indicating success or failure
+ */
+ function runDbScript($filename, $conn, $type = 'mysqli')
+ {
+ $sql = trim(file_get_contents(INSTALLDIR . '/db/' . $filename));
+ $stmts = explode(';', $sql);
+ foreach ($stmts as $stmt) {
+ $stmt = trim($stmt);
+ if (!mb_strlen($stmt)) {
+ continue;
+ }
+ // FIXME: use PEAR::DB or PDO instead of our own switch
+ switch ($type) {
+ case 'mysqli':
+ $res = $conn->query($stmt);
+ if ($res === false) {
+ $error = $conn->error;
+ }
+ break;
+ case 'pgsql':
+ $res = pg_query($conn, $stmt);
+ if ($res === false) {
+ $error = pg_last_error();
+ }
+ break;
+ default:
+ $this->updateStatus("runDbScript() error: unknown database type ". $type ." provided.");
+ }
+ if ($res === false) {
+ $this->updateStatus("ERROR ($error) for SQL '$stmt'");
+ return $res;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Create the initial admin user account.
+ * Side effect: may load portions of StatusNet framework.
+ * Side effect: outputs program info
+ */
+ function registerInitialUser()
+ {
+ define('STATUSNET', true);
+ define('LACONICA', true); // compatibility
+
+ require_once INSTALLDIR . '/lib/common.php';
+
+ $data = array('nickname' => $this->adminNick,
+ 'password' => $this->adminPass,
+ 'fullname' => $this->adminNick);
+ if ($this->adminEmail) {
+ $data['email'] = $this->adminEmail;
+ }
+ $user = User::register($data);
+
+ if (empty($user)) {
+ return false;
+ }
+
+ // give initial user carte blanche
+
+ $user->grantRole('owner');
+ $user->grantRole('moderator');
+ $user->grantRole('administrator');
+
+ // Attempt to do a remote subscribe to update@status.net
+ // Will fail if instance is on a private network.
+
+ if ($this->adminUpdates && class_exists('Ostatus_profile')) {
+ try {
+ $oprofile = Ostatus_profile::ensureProfileURL('http://update.status.net/');
+ Subscription::start($user->getProfile(), $oprofile->localProfile());
+ $this->updateStatus("Set up subscription to <a href='http://update.status.net/'>update@status.net</a>.");
+ } catch (Exception $e) {
+ $this->updateStatus("Could not set up subscription to <a href='http://update.status.net/'>update@status.net</a>.", true);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * The beef of the installer!
+ * Create database, config file, and admin user.
+ *
+ * Prerequisites: validation of input data.
+ *
+ * @return boolean success
+ */
+ function doInstall()
+ {
+ $this->db = $this->setupDatabase();
+
+ if (!$this->db) {
+ // database connection failed, do not move on to create config file.
+ return false;
+ }
+
+ if (!$this->skipConfig) {
+ $this->updateStatus("Writing config file...");
+ $res = $this->writeConf();
+
+ if (!$res) {
+ $this->updateStatus("Can't write config file.", true);
+ return false;
+ }
+ }
+
+ if (!empty($this->adminNick)) {
+ // Okay, cross fingers and try to register an initial user
+ if ($this->registerInitialUser()) {
+ $this->updateStatus(
+ "An initial user with the administrator role has been created."
+ );
+ } else {
+ $this->updateStatus(
+ "Could not create initial StatusNet user (administrator).",
+ true
+ );
+ return false;
+ }
+ }
+
+ /*
+ TODO https needs to be considered
+ */
+ $link = "http://".$this->server.'/'.$this->path;
+
+ $this->updateStatus("StatusNet has been installed at $link");
+ $this->updateStatus(
+ "<strong>DONE!</strong> You can visit your <a href='$link'>new StatusNet site</a> (login as '$this->adminNick'). If this is your first StatusNet install, you may want to poke around our <a href='http://status.net/wiki/Getting_started'>Getting Started guide</a>."
+ );
+
+ return true;
+ }
+
+ /**
+ * Output a pre-install-time warning message
+ * @param string $message HTML ok, but should be plaintext-able
+ * @param string $submessage HTML ok, but should be plaintext-able
+ */
+ abstract function warning($message, $submessage='');
+
+ /**
+ * Output an install-time progress message
+ * @param string $message HTML ok, but should be plaintext-able
+ * @param boolean $error true if this should be marked as an error condition
+ */
+ abstract function updateStatus($status, $error=false);
+
+}
diff --git a/lib/jabber.php b/lib/jabber.php
index db4e2e9a7..cdcfc4423 100644
--- a/lib/jabber.php
+++ b/lib/jabber.php
@@ -34,39 +34,198 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
require_once 'XMPPHP/XMPP.php';
/**
- * checks whether a string is a syntactically valid Jabber ID (JID)
+ * Splits a Jabber ID (JID) into node, domain, and resource portions.
+ *
+ * Based on validation routine submitted by:
+ * @copyright 2009 Patrick Georgi <patrick@georgi-clan.de>
+ * @license Licensed under ISC-L, which is compatible with everything else that keeps the copyright notice intact.
*
* @param string $jid string to check
*
+ * @return array with "node", "domain", and "resource" indices
+ * @throws Exception if input is not valid
+ */
+
+function jabber_split_jid($jid)
+{
+ $chars = '';
+ /* the following definitions come from stringprep, Appendix C,
+ which is used in its entirety by nodeprop, Chapter 5, "Prohibited Output" */
+ /* C1.1 ASCII space characters */
+ $chars .= "\x{20}";
+ /* C1.2 Non-ASCII space characters */
+ $chars .= "\x{a0}\x{1680}\x{2000}-\x{200b}\x{202f}\x{205f}\x{3000a}";
+ /* C2.1 ASCII control characters */
+ $chars .= "\x{00}-\x{1f}\x{7f}";
+ /* C2.2 Non-ASCII control characters */
+ $chars .= "\x{80}-\x{9f}\x{6dd}\x{70f}\x{180e}\x{200c}\x{200d}\x{2028}\x{2029}\x{2060}-\x{2063}\x{206a}-\x{206f}\x{feff}\x{fff9}-\x{fffc}\x{1d173}-\x{1d17a}";
+ /* C3 - Private Use */
+ $chars .= "\x{e000}-\x{f8ff}\x{f0000}-\x{ffffd}\x{100000}-\x{10fffd}";
+ /* C4 - Non-character code points */
+ $chars .= "\x{fdd0}-\x{fdef}\x{fffe}\x{ffff}\x{1fffe}\x{1ffff}\x{2fffe}\x{2ffff}\x{3fffe}\x{3ffff}\x{4fffe}\x{4ffff}\x{5fffe}\x{5ffff}\x{6fffe}\x{6ffff}\x{7fffe}\x{7ffff}\x{8fffe}\x{8ffff}\x{9fffe}\x{9ffff}\x{afffe}\x{affff}\x{bfffe}\x{bffff}\x{cfffe}\x{cffff}\x{dfffe}\x{dffff}\x{efffe}\x{effff}\x{ffffe}\x{fffff}\x{10fffe}\x{10ffff}";
+ /* C5 - Surrogate codes */
+ $chars .= "\x{d800}-\x{dfff}";
+ /* C6 - Inappropriate for plain text */
+ $chars .= "\x{fff9}-\x{fffd}";
+ /* C7 - Inappropriate for canonical representation */
+ $chars .= "\x{2ff0}-\x{2ffb}";
+ /* C8 - Change display properties or are deprecated */
+ $chars .= "\x{340}\x{341}\x{200e}\x{200f}\x{202a}-\x{202e}\x{206a}-\x{206f}";
+ /* C9 - Tagging characters */
+ $chars .= "\x{e0001}\x{e0020}-\x{e007f}";
+
+ /* Nodeprep forbids some more characters */
+ $nodeprepchars = $chars;
+ $nodeprepchars .= "\x{22}\x{26}\x{27}\x{2f}\x{3a}\x{3c}\x{3e}\x{40}";
+
+ $parts = explode("/", $jid, 2);
+ if (count($parts) > 1) {
+ $resource = $parts[1];
+ if ($resource == '') {
+ // Warning: empty resource isn't legit.
+ // But if we're normalizing, we may as well take it...
+ }
+ } else {
+ $resource = null;
+ }
+
+ $node = explode("@", $parts[0]);
+ if ((count($node) > 2) || (count($node) == 0)) {
+ throw new Exception("Invalid JID: too many @s");
+ } else if (count($node) == 1) {
+ $domain = $node[0];
+ $node = null;
+ } else {
+ $domain = $node[1];
+ $node = $node[0];
+ if ($node == '') {
+ throw new Exception("Invalid JID: @ but no node");
+ }
+ }
+
+ // Length limits per http://xmpp.org/rfcs/rfc3920.html#addressing
+ if ($node !== null) {
+ if (strlen($node) > 1023) {
+ throw new Exception("Invalid JID: node too long.");
+ }
+ if (preg_match("/[".$nodeprepchars."]/u", $node)) {
+ throw new Exception("Invalid JID node '$node'");
+ }
+ }
+
+ if (strlen($domain) > 1023) {
+ throw new Exception("Invalid JID: domain too long.");
+ }
+ if (!common_valid_domain($domain)) {
+ throw new Exception("Invalid JID domain name '$domain'");
+ }
+
+ if ($resource !== null) {
+ if (strlen($resource) > 1023) {
+ throw new Exception("Invalid JID: resource too long.");
+ }
+ if (preg_match("/[".$chars."]/u", $resource)) {
+ throw new Exception("Invalid JID resource '$resource'");
+ }
+ }
+
+ return array('node' => is_null($node) ? null : mb_strtolower($node),
+ 'domain' => is_null($domain) ? null : mb_strtolower($domain),
+ 'resource' => $resource);
+}
+
+/**
+ * Checks whether a string is a syntactically valid Jabber ID (JID),
+ * either with or without a resource.
+ *
+ * Note that a bare domain can be a valid JID.
+ *
+ * @param string $jid string to check
+ * @param bool $check_domain whether we should validate that domain...
+ *
* @return boolean whether the string is a valid JID
*/
+function jabber_valid_full_jid($jid, $check_domain=false)
+{
+ try {
+ $parts = jabber_split_jid($jid);
+ if ($check_domain) {
+ if (!jabber_check_domain($parts['domain'])) {
+ return false;
+ }
+ }
+ return $parts['resource'] !== ''; // missing or present; empty ain't kosher
+ } catch (Exception $e) {
+ return false;
+ }
+}
-function jabber_valid_base_jid($jid)
+/**
+ * Checks whether a string is a syntactically valid base Jabber ID (JID).
+ * A base JID won't include a resource specifier on the end; since we
+ * take it off when reading input we can't really use them reliably
+ * to direct outgoing messages yet (sorry guys!)
+ *
+ * Note that a bare domain can be a valid JID.
+ *
+ * @param string $jid string to check
+ * @param bool $check_domain whether we should validate that domain...
+ *
+ * @return boolean whether the string is a valid JID
+ */
+function jabber_valid_base_jid($jid, $check_domain=false)
{
- // Cheap but effective
- return Validate::email($jid);
+ try {
+ $parts = jabber_split_jid($jid);
+ if ($check_domain) {
+ if (!jabber_check_domain($parts['domain'])) {
+ return false;
+ }
+ }
+ return ($parts['resource'] === null); // missing; empty ain't kosher
+ } catch (Exception $e) {
+ return false;
+ }
}
/**
- * normalizes a Jabber ID for comparison
+ * Normalizes a Jabber ID for comparison, dropping the resource component if any.
*
* @param string $jid JID to check
+ * @param bool $check_domain if true, reject if the domain isn't findable
*
* @return string an equivalent JID in normalized (lowercase) form
*/
function jabber_normalize_jid($jid)
{
- if (preg_match("/(?:([^\@]+)\@)?([^\/]+)(?:\/(.*))?$/", $jid, $matches)) {
- $node = $matches[1];
- $server = $matches[2];
- return strtolower($node.'@'.$server);
- } else {
+ try {
+ $parts = jabber_split_jid($jid);
+ if ($parts['node'] !== null) {
+ return $parts['node'] . '@' . $parts['domain'];
+ } else {
+ return $parts['domain'];
+ }
+ } catch (Exception $e) {
return null;
}
}
/**
+ * Check if this domain's got some legit DNS record
+ */
+function jabber_check_domain($domain)
+{
+ if (checkdnsrr("_xmpp-server._tcp." . $domain, "SRV")) {
+ return true;
+ }
+ if (checkdnsrr($domain, "ANY")) {
+ return true;
+ }
+ return false;
+}
+
+/**
* the JID of the Jabber daemon for this StatusNet instance
*
* @return string JID of the Jabber daemon
diff --git a/lib/language.php b/lib/language.php
index 64b59e739..12b56be9a 100644
--- a/lib/language.php
+++ b/lib/language.php
@@ -61,7 +61,7 @@ if (!function_exists('dpgettext')) {
* Not currently exposed in PHP's gettext module; implemented to be compat
* with gettext.h's macros.
*
- * @param string $domain domain identifier, or null for default domain
+ * @param string $domain domain identifier
* @param string $context context identifier, should be some key like "menu|file"
* @param string $msgid English source text
* @return string original or translated message
@@ -106,7 +106,7 @@ if (!function_exists('dnpgettext')) {
* Not currently exposed in PHP's gettext module; implemented to be compat
* with gettext.h's macros.
*
- * @param string $domain domain identifier, or null for default domain
+ * @param string $domain domain identifier
* @param string $context context identifier, should be some key like "menu|file"
* @param string $msg singular English source text
* @param string $plural plural English source text
@@ -180,7 +180,11 @@ function _m($msg/*, ...*/)
}
/**
- * Looks for which plugin we've been called from to set the gettext domain.
+ * Looks for which plugin we've been called from to set the gettext domain;
+ * if not in a plugin subdirectory, we'll use the default 'statusnet'.
+ *
+ * Note: we can't return null for default domain since most of the PHP gettext
+ * wrapper functions turn null into "" before passing to the backend library.
*
* @param array $backtrace debug_backtrace() output
* @return string
@@ -202,16 +206,26 @@ function _mdomain($backtrace)
static $cached;
$path = $backtrace[0]['file'];
if (!isset($cached[$path])) {
+ $final = 'statusnet'; // assume default domain
if (DIRECTORY_SEPARATOR !== '/') {
$path = strtr($path, DIRECTORY_SEPARATOR, '/');
}
- $cut = strpos($path, '/plugins/') + 9;
- $cut2 = strpos($path, '/', $cut);
- if ($cut && $cut2) {
- $cached[$path] = substr($path, $cut, $cut2 - $cut);
+ $plug = strpos($path, '/plugins/');
+ if ($plug === false) {
+ // We're not in a plugin; return default domain.
+ $final = 'statusnet';
} else {
- return null;
+ $cut = $plug + 9;
+ $cut2 = strpos($path, '/', $cut);
+ if ($cut2) {
+ $final = substr($path, $cut, $cut2 - $cut);
+ } else {
+ // We might be running directly from the plugins dir?
+ // If so, there's no place to store locale info.
+ $final = 'statusnet';
+ }
}
+ $cached[$path] = $final;
}
return $cached[$path];
}
@@ -286,14 +300,17 @@ function get_nice_language_list()
*/
function get_all_languages() {
return array(
+ 'af' => array('q' => 0.8, 'lang' => 'af', 'name' => 'Afrikaans', 'direction' => 'ltr'),
'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'),
+ 'da' => array('q' => 0.8, 'lang' => 'da', 'name' => 'Danish', 'direction' => 'ltr'),
'de' => array('q' => 0.8, 'lang' => 'de', 'name' => 'German', 'direction' => 'ltr'),
'el' => array('q' => 0.1, 'lang' => 'el', 'name' => 'Greek', 'direction' => 'ltr'),
+ 'eo' => array('q' => 0.8, 'lang' => 'eo', 'name' => 'Esperanto', 'direction' => 'ltr'),
'en-us' => array('q' => 1, 'lang' => 'en', 'name' => 'English (US)', 'direction' => 'ltr'),
'en-gb' => array('q' => 1, 'lang' => 'en_GB', 'name' => 'English (British)', 'direction' => 'ltr'),
'en' => array('q' => 1, 'lang' => 'en', 'name' => 'English (US)', 'direction' => 'ltr'),
@@ -301,13 +318,15 @@ function get_all_languages() {
'fi' => array('q' => 1, 'lang' => 'fi', 'name' => 'Finnish', 'direction' => 'ltr'),
'fa' => array('q' => 1, 'lang' => 'fa', 'name' => 'Persian', 'direction' => 'rtl'),
'fr-fr' => array('q' => 1, 'lang' => 'fr', 'name' => 'French', 'direction' => 'ltr'),
- 'ga' => array('q' => 0.5, 'lang' => 'ga', 'name' => 'Galician', 'direction' => 'ltr'),
+ 'ga' => array('q' => 0.5, 'lang' => 'ga', 'name' => 'Irish', 'direction' => 'ltr'),
+ 'gl' => array('q' => 0.8, 'lang' => 'gl', 'name' => 'Galician', 'direction' => 'ltr'),
'he' => array('q' => 0.5, 'lang' => 'he', 'name' => 'Hebrew', 'direction' => 'rtl'),
'hsb' => array('q' => 0.8, 'lang' => 'hsb', 'name' => 'Upper Sorbian', 'direction' => 'ltr'),
'ia' => array('q' => 0.8, 'lang' => 'ia', 'name' => 'Interlingua', 'direction' => 'ltr'),
'is' => array('q' => 0.1, 'lang' => 'is', 'name' => 'Icelandic', 'direction' => 'ltr'),
'it' => array('q' => 1, 'lang' => 'it', 'name' => 'Italian', 'direction' => 'ltr'),
'jp' => array('q' => 0.5, 'lang' => 'ja', 'name' => 'Japanese', 'direction' => 'ltr'),
+ 'ka' => array('q' => 0.8, 'lang' => 'ka', 'name' => 'Georgian', 'direction' => 'ltr'),
'ko' => array('q' => 0.9, 'lang' => 'ko', 'name' => 'Korean', 'direction' => 'ltr'),
'mk' => array('q' => 0.5, 'lang' => 'mk', 'name' => 'Macedonian', 'direction' => 'ltr'),
'nb' => array('q' => 0.1, 'lang' => 'nb', 'name' => 'Norwegian (BokmƄl)', 'direction' => 'ltr'),
diff --git a/lib/liberalstomp.php b/lib/liberalstomp.php
index 3d38953fd..70c22c17e 100644
--- a/lib/liberalstomp.php
+++ b/lib/liberalstomp.php
@@ -147,5 +147,30 @@ class LiberalStomp extends Stomp
}
return $frame;
}
-}
+
+ /**
+ * Write frame to server
+ *
+ * @param StompFrame $stompFrame
+ */
+ protected function _writeFrame (StompFrame $stompFrame)
+ {
+ if (!is_resource($this->_socket)) {
+ require_once 'Stomp/Exception.php';
+ throw new StompException('Socket connection hasn\'t been established');
+ }
+
+ $data = $stompFrame->__toString();
+
+ // Make sure the socket's in a writable state; if not, wait a bit.
+ stream_set_blocking($this->_socket, 1);
+
+ $r = fwrite($this->_socket, $data, strlen($data));
+ stream_set_blocking($this->_socket, 0);
+ if ($r === false || $r == 0) {
+ $this->_reconnect();
+ $this->_writeFrame($stompFrame);
+ }
+ }
+ }
diff --git a/lib/mail.php b/lib/mail.php
index d73603694..ab5742e33 100644
--- a/lib/mail.php
+++ b/lib/mail.php
@@ -170,8 +170,10 @@ function mail_to_user(&$user, $subject, $body, $headers=array(), $address=null)
function mail_confirm_address($user, $code, $nickname, $address)
{
+ // TRANS: Subject for address confirmation email
$subject = _('Email address confirmation');
+ // TRANS: Body for address confirmation email.
$body = sprintf(_("Hey, %s.\n\n".
"Someone just entered this email address on %s.\n\n" .
"If it was you, and you want to confirm your entry, ".
@@ -222,9 +224,6 @@ function mail_subscribe_notify_profile($listenee, $other)
if ($other->hasRight(Right::EMAILONSUBSCRIBE) &&
$listenee->email && $listenee->emailnotifysub) {
- // use the recipient's localization
- common_init_locale($listenee->language);
-
$profile = $listenee->getProfile();
$name = $profile->getBestName();
@@ -234,14 +233,24 @@ function mail_subscribe_notify_profile($listenee, $other)
$recipients = $listenee->email;
+ // use the recipient's localization
+ common_switch_locale($listenee->language);
+
$headers = _mail_prepare_headers('subscribe', $listenee->nickname, $other->nickname);
$headers['From'] = mail_notify_from();
$headers['To'] = $name . ' <' . $listenee->email . '>';
+ // TRANS: Subject of new-subscriber notification e-mail
$headers['Subject'] = sprintf(_('%1$s is now listening to '.
'your notices on %2$s.'),
$other->getBestName(),
common_config('site', 'name'));
+ $blocklink = sprintf(_("If you believe this account is being used abusively, " .
+ "you can block them from your subscribers list and " .
+ "report as spam to site administrators at %s"),
+ common_local_url('block', array('profileid' => $other->id)));
+
+ // TRANS: Main body of new-subscriber notification e-mail
$body = sprintf(_('%1$s is now listening to your notices on %2$s.'."\n\n".
"\t".'%3$s'."\n\n".
'%4$s'.
@@ -255,16 +264,20 @@ function mail_subscribe_notify_profile($listenee, $other)
common_config('site', 'name'),
$other->profileurl,
($other->location) ?
+ // TRANS: Profile info line in new-subscriber notification e-mail
sprintf(_("Location: %s"), $other->location) . "\n" : '',
($other->homepage) ?
+ // TRANS: Profile info line in new-subscriber notification e-mail
sprintf(_("Homepage: %s"), $other->homepage) . "\n" : '',
- ($other->bio) ?
- sprintf(_("Bio: %s"), $other->bio) . "\n\n" : '',
+ (($other->bio) ?
+ // TRANS: Profile info line in new-subscriber notification e-mail
+ sprintf(_("Bio: %s"), $other->bio) . "\n" : '') .
+ "\n\n" . $blocklink . "\n",
common_config('site', 'name'),
common_local_url('emailsettings'));
// reset localization
- common_init_locale();
+ common_switch_locale();
mail_send($recipients, $headers, $body);
}
}
@@ -287,9 +300,11 @@ function mail_new_incoming_notify($user)
$headers['From'] = $user->incomingemail;
$headers['To'] = $name . ' <' . $user->email . '>';
+ // TRANS: Subject of notification mail for new posting email address
$headers['Subject'] = sprintf(_('New email address for posting to %s'),
common_config('site', 'name'));
+ // TRANS: Body of notification mail for new posting email address
$body = sprintf(_("You have a new posting address on %1\$s.\n\n".
"Send email to %2\$s to post new messages.\n\n".
"More email instructions at %3\$s.\n\n".
@@ -414,6 +429,7 @@ function mail_send_sms_notice_address($notice, $smsemail, $incomingemail)
$headers['From'] = ($incomingemail) ? $incomingemail : mail_notify_from();
$headers['To'] = $to;
+ // TRANS: Subject line for SMS-by-email notification messages
$headers['Subject'] = sprintf(_('%s status'),
$other->getBestName());
@@ -440,11 +456,11 @@ function mail_confirm_sms($code, $nickname, $address)
$headers['From'] = mail_notify_from();
$headers['To'] = $nickname . ' <' . $address . '>';
+ // TRANS: Subject line for SMS-by-email address confirmation message
$headers['Subject'] = _('SMS confirmation');
- // FIXME: I18N
-
- $body = "$nickname: confirm you own this phone number with this code:";
+ // TRANS: Main body heading for SMS-by-email address confirmation message
+ $body = sprintf(_("%s: confirm you own this phone number with this code:"), $nickname);
$body .= "\n\n";
$body .= $code;
$body .= "\n\n";
@@ -463,11 +479,13 @@ function mail_confirm_sms($code, $nickname, $address)
function mail_notify_nudge($from, $to)
{
- common_init_locale($to->language);
+ common_switch_locale($to->language);
+ // TRANS: Subject for 'nudge' notification email
$subject = sprintf(_('You\'ve been nudged by %s'), $from->nickname);
$from_profile = $from->getProfile();
+ // TRANS: Body for 'nudge' notification email
$body = sprintf(_("%1\$s (%2\$s) is wondering what you are up to ".
"these days and is inviting you to post some news.\n\n".
"So let's hear from you :)\n\n".
@@ -479,7 +497,7 @@ function mail_notify_nudge($from, $to)
$from->nickname,
common_local_url('all', array('nickname' => $to->nickname)),
common_config('site', 'name'));
- common_init_locale();
+ common_switch_locale();
$headers = _mail_prepare_headers('nudge', $to->nickname, $from->nickname);
@@ -513,11 +531,13 @@ function mail_notify_message($message, $from=null, $to=null)
return true;
}
- common_init_locale($to->language);
+ common_switch_locale($to->language);
+ // TRANS: Subject for direct-message notification email
$subject = sprintf(_('New private message from %s'), $from->nickname);
$from_profile = $from->getProfile();
+ // TRANS: Body for direct-message notification email
$body = sprintf(_("%1\$s (%2\$s) sent you a private message:\n\n".
"------------------------------------------------------\n".
"%3\$s\n".
@@ -535,7 +555,7 @@ function mail_notify_message($message, $from=null, $to=null)
$headers = _mail_prepare_headers('message', $to->nickname, $from->nickname);
- common_init_locale();
+ common_switch_locale();
return mail_to_user($to, $subject, $body, $headers);
}
@@ -563,10 +583,12 @@ function mail_notify_fave($other, $user, $notice)
$bestname = $profile->getBestName();
- common_init_locale($other->language);
+ common_switch_locale($other->language);
+ // TRANS: Subject for favorite notification email
$subject = sprintf(_('%s (@%s) added your notice as a favorite'), $bestname, $user->nickname);
+ // TRANS: Body for favorite notification email
$body = sprintf(_("%1\$s (@%7\$s) just added your notice from %2\$s".
" as one of their favorites.\n\n" .
"The URL of your notice is:\n\n" .
@@ -589,7 +611,7 @@ function mail_notify_fave($other, $user, $notice)
$headers = _mail_prepare_headers('fave', $other->nickname, $user->nickname);
- common_init_locale();
+ common_switch_locale();
mail_to_user($other, $subject, $body, $headers);
}
@@ -622,24 +644,25 @@ function mail_notify_attn($user, $notice)
common_switch_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',
- array('id' => $notice->conversation)).'#notice-'.$notice->id;
- } else {
- $conversationEmailText = "%5\$s";
- $conversationUrl = null;
- }
+ if ($notice->hasConversation()) {
+ $conversationUrl = common_local_url('conversation',
+ array('id' => $notice->conversation)).'#notice-'.$notice->id;
+ // TRANS: Line in @-reply notification e-mail. %s is conversation URL.
+ $conversationEmailText = sprintf(_("The full conversation can be read here:\n\n".
+ "\t%s"), $conversationUrl) . "\n\n";
+ } else {
+ $conversationEmailText = '';
+ }
$subject = sprintf(_('%s (@%s) sent a notice to your attention'), $bestname, $sender->nickname);
+ // TRANS: Body of @-reply notification e-mail.
$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".
"\t%4\$s\n\n" .
- $conversationEmailText .
+ "%5\$s" .
"You can reply back here:\n\n".
"\t%6\$s\n\n" .
"The list of all @-replies for you here:\n\n" .
@@ -652,7 +675,7 @@ function mail_notify_attn($user, $notice)
common_local_url('shownotice',
array('notice' => $notice->id)),//%3
$notice->content,//%4
- $conversationUrl,//%5
+ $conversationEmailText,//%5
common_local_url('newnotice',
array('replyto' => $sender->nickname, 'inreplyto' => $notice->id)),//%6
common_local_url('replies',
diff --git a/lib/mailbox.php b/lib/mailbox.php
index 90a58b4c4..2b00f5ffd 100644
--- a/lib/mailbox.php
+++ b/lib/mailbox.php
@@ -224,6 +224,7 @@ class MailboxAction extends CurrentUserDesignAction
if ($message->source) {
$this->elementStart('span', 'source');
+ // FIXME: bad i18n. Device should be a parameter (from %s).
$this->text(_('from'));
$this->element('span', 'device', $this->showSource($message->source));
$this->elementEnd('span');
diff --git a/lib/mailhandler.php b/lib/mailhandler.php
index 890f6d5b4..e9ba41839 100644
--- a/lib/mailhandler.php
+++ b/lib/mailhandler.php
@@ -265,6 +265,10 @@ class MailHandler
if (preg_match('/^\s*Begin\s+forward/', $line)) {
break;
}
+ // skip everything after a blank line if we already have content
+ if ($output !== '' && $line === '') {
+ break;
+ }
$output .= ' ' . $line;
}
diff --git a/lib/mediafile.php b/lib/mediafile.php
index 10d90d008..c96c78ab5 100644
--- a/lib/mediafile.php
+++ b/lib/mediafile.php
@@ -171,7 +171,7 @@ class MediaFile
return;
}
- if (!MediaFile::respectsQuota($user, $_FILES['attach']['size'])) {
+ if (!MediaFile::respectsQuota($user, $_FILES[$param]['size'])) {
// Should never actually get here
@@ -180,7 +180,8 @@ class MediaFile
return;
}
- $mimetype = MediaFile::getUploadedFileType($_FILES[$param]['tmp_name']);
+ $mimetype = MediaFile::getUploadedFileType($_FILES[$param]['tmp_name'],
+ $_FILES[$param]['name']);
$filename = null;
@@ -241,19 +242,41 @@ class MediaFile
return new MediaFile($user, $filename, $mimetype);
}
- static function getUploadedFileType($f) {
+ /**
+ * Attempt to identify the content type of a given file.
+ *
+ * @param mixed $f file handle resource, or filesystem path as string
+ * @param string $originalFilename (optional) for extension-based detection
+ * @return string
+ *
+ * @fixme is this an internal or public method? It's called from GetFileAction
+ * @fixme this seems to tie a front-end error message in, kinda confusing
+ * @fixme this looks like it could return a PEAR_Error in some cases, if
+ * type can't be identified and $config['attachments']['supported'] is true
+ *
+ * @throws ClientException if type is known, but not supported for local uploads
+ */
+ static function getUploadedFileType($f, $originalFilename=false) {
require_once 'MIME/Type.php';
+ require_once 'MIME/Type/Extension.php';
+ $mte = new MIME_Type_Extension();
$cmd = &PEAR::getStaticProperty('MIME_Type', 'fileCmd');
$cmd = common_config('attachments', 'filecommand');
$filetype = null;
+ // If we couldn't get a clear type from the file extension,
+ // we'll go ahead and try checking the content. Content checks
+ // are unambiguous for most image files, but nearly useless
+ // for office document formats.
+
if (is_string($f)) {
// assuming a filename
$filetype = MIME_Type::autoDetect($f);
+
} else {
// assuming a filehandle
@@ -262,7 +285,32 @@ class MediaFile
$filetype = MIME_Type::autoDetect($stream['uri']);
}
- if (common_config('attachments', 'supported') === true || in_array($filetype, common_config('attachments', 'supported'))) {
+ // The content-based sources for MIME_Type::autoDetect()
+ // are wildly unreliable for office-type documents. If we've
+ // gotten an unclear reponse back or just couldn't identify it,
+ // we'll try detecting a type from its extension...
+ $unclearTypes = array('application/octet-stream',
+ 'application/vnd.ms-office',
+ 'application/zip');
+
+ if ($originalFilename && (!$filetype || in_array($filetype, $unclearTypes))) {
+ $type = $mte->getMIMEType($originalFilename);
+ if (is_string($type)) {
+ $filetype = $type;
+ }
+ }
+
+ $supported = common_config('attachments', 'supported');
+ if (is_array($supported)) {
+ // Normalize extensions to mime types
+ foreach ($supported as $i => $entry) {
+ if (strpos($entry, '/') === false) {
+ common_log(LOG_INFO, "sample.$entry");
+ $supported[$i] = $mte->getMIMEType("sample.$entry");
+ }
+ }
+ }
+ if ($supported === true || in_array($filetype, $supported)) {
return $filetype;
}
$media = MIME_Type::getMedia($filetype);
diff --git a/lib/mysqlschema.php b/lib/mysqlschema.php
index 455695366..f9552c1dc 100644
--- a/lib/mysqlschema.php
+++ b/lib/mysqlschema.php
@@ -50,21 +50,6 @@ class MysqlSchema extends Schema
static $_single = null;
protected $conn = null;
- /**
- * Constructor. Only run once for singleton object.
- */
-
- protected function __construct()
- {
- // XXX: there should be an easier way to do this.
- $user = new User();
-
- $this->conn = $user->getDatabaseConnection();
-
- $user->free();
-
- unset($user);
- }
/**
* Main public entry point. Use this to get
@@ -348,7 +333,7 @@ class MysqlSchema extends Schema
}
if (empty($name)) {
- $name = "$table_".implode("_", $columnNames)."_idx";
+ $name = "{$table}_".implode("_", $columnNames)."_idx";
}
$res = $this->conn->query("ALTER TABLE $table ".
diff --git a/lib/noticeform.php b/lib/noticeform.php
index 7278c41a9..514066356 100644
--- a/lib/noticeform.php
+++ b/lib/noticeform.php
@@ -169,7 +169,8 @@ class NoticeForm extends Form
function formData()
{
if (Event::handle('StartShowNoticeFormData', array($this))) {
- $this->out->element('label', array('for' => 'notice_data-text'),
+ $this->out->element('label', array('for' => 'notice_data-text',
+ 'id' => 'notice_data-text-label'),
sprintf(_('What\'s up, %s?'), $this->user->nickname));
// XXX: vary by defined max size
$this->out->element('textarea', array('id' => 'notice_data-text',
@@ -212,8 +213,8 @@ class NoticeForm extends Form
$this->out->checkbox('notice_data-geo', _('Share my location'), true);
$this->out->elementEnd('div');
$this->out->inlineScript(' var NoticeDataGeo_text = {'.
- 'ShareDisable: "'._('Do not share my location').'",'.
- 'ErrorTimeout: "'._('Sorry, retrieving your geo location is taking longer than expected, please try again later').'"'.
+ 'ShareDisable: ' .json_encode(_('Do not share my location')).','.
+ 'ErrorTimeout: ' .json_encode(_('Sorry, retrieving your geo location is taking longer than expected, please try again later')).
'}');
}
diff --git a/lib/noticelist.php b/lib/noticelist.php
index 83c8de9f6..529d6a3f9 100644
--- a/lib/noticelist.php
+++ b/lib/noticelist.php
@@ -96,8 +96,14 @@ class NoticeList extends Widget
break;
}
- $item = $this->newListItem($this->notice);
- $item->show();
+ try {
+ $item = $this->newListItem($this->notice);
+ $item->show();
+ } catch (Exception $e) {
+ // we log exceptions and continue
+ common_log(LOG_ERR, $e->getMessage());
+ continue;
+ }
}
$this->out->elementEnd('ol');
@@ -426,10 +432,18 @@ class NoticeListItem extends Widget
if (empty($name)) {
$latdms = $this->decimalDegreesToDMS(abs($lat));
$londms = $this->decimalDegreesToDMS(abs($lon));
+ // TRANS: Used in coordinates as abbreviation of north
+ $north = _('N');
+ // TRANS: Used in coordinates as abbreviation of south
+ $south = _('S');
+ // TRANS: Used in coordinates as abbreviation of east
+ $east = _('E');
+ // TRANS: Used in coordinates as abbreviation of west
+ $west = _('W');
$name = sprintf(
_('%1$uĀ°%2$u\'%3$u"%4$s %5$uĀ°%6$u\'%7$u"%8$s'),
- $latdms['deg'],$latdms['min'], $latdms['sec'],($lat>0?_('N'):_('S')),
- $londms['deg'],$londms['min'], $londms['sec'],($lon>0?_('E'):_('W')));
+ $latdms['deg'],$latdms['min'], $latdms['sec'],($lat>0? $north:$south),
+ $londms['deg'],$londms['min'], $londms['sec'],($lon>0? $east:$west));
}
$url = $location->getUrl();
@@ -455,12 +469,14 @@ class NoticeListItem extends Widget
$this->out->elementEnd('span');
}
+ /**
+ * @param number $dec decimal degrees
+ * @return array split into 'deg', 'min', and 'sec'
+ */
function decimalDegreesToDMS($dec)
{
-
- $vars = explode(".",$dec);
- $deg = $vars[0];
- $tempma = "0.".$vars[1];
+ $deg = intval($dec);
+ $tempma = abs($dec) - abs($deg);
$tempma = $tempma * 3600;
$min = floor($tempma / 60);
@@ -480,54 +496,47 @@ class NoticeListItem extends Widget
function showNoticeSource()
{
- if ($this->notice->source) {
+ $ns = $this->notice->getSource();
+
+ if ($ns) {
+ $source_name = (empty($ns->name)) ? ($ns->code ? _($ns->code) : _('web')) : _($ns->name);
$this->out->text(' ');
$this->out->elementStart('span', 'source');
+ // FIXME: probably i18n issue. If "from" is followed by text, that should be a parameter to "from" (from %s).
$this->out->text(_('from'));
- $source_name = _($this->notice->source);
$this->out->text(' ');
- switch ($this->notice->source) {
- case 'web':
- case 'xmpp':
- case 'mail':
- case 'omb':
- case 'system':
- case 'api':
- $this->out->element('span', 'device', $source_name);
- break;
- default:
+ $name = $source_name;
+ $url = $ns->url;
+ $title = null;
+
+ if (Event::handle('StartNoticeSourceLink', array($this->notice, &$name, &$url, &$title))) {
$name = $source_name;
- $url = null;
-
- if (Event::handle('StartNoticeSourceLink', array($this->notice, &$name, &$url, &$title))) {
- $ns = Notice_source::staticGet($this->notice->source);
-
- if ($ns) {
- $name = $ns->name;
- $url = $ns->url;
- } else {
- $app = Oauth_application::staticGet('name', $this->notice->source);
- if ($app) {
- $name = $app->name;
- $url = $app->source_url;
- }
- }
- }
- Event::handle('EndNoticeSourceLink', array($this->notice, &$name, &$url, &$title));
-
- if (!empty($name) && !empty($url)) {
- $this->out->elementStart('span', 'device');
- $this->out->element('a', array('href' => $url,
- 'rel' => 'external',
- 'title' => $title),
- $name);
- $this->out->elementEnd('span');
- } else {
- $this->out->element('span', 'device', $name);
+ $url = $ns->url;
+ }
+ Event::handle('EndNoticeSourceLink', array($this->notice, &$name, &$url, &$title));
+
+ // if $ns->name and $ns->url are populated we have
+ // configured a source attr somewhere
+ if (!empty($name) && !empty($url)) {
+
+ $this->out->elementStart('span', 'device');
+
+ $attrs = array(
+ 'href' => $url,
+ 'rel' => 'external'
+ );
+
+ if (!empty($title)) {
+ $attrs['title'] = $title;
}
- break;
+
+ $this->out->element('a', $attrs, $name);
+ $this->out->elementEnd('span');
+ } else {
+ $this->out->element('span', 'device', $name);
}
+
$this->out->elementEnd('span');
}
}
@@ -543,18 +552,7 @@ class NoticeListItem extends Widget
function showContext()
{
- $hasConversation = false;
- if (!empty($this->notice->conversation)) {
- $conversation = Notice::conversationStream(
- $this->notice->conversation,
- 1,
- 1
- );
- if ($conversation->N > 0) {
- $hasConversation = true;
- }
- }
- if ($hasConversation) {
+ if ($this->notice->hasConversation()) {
$conv = Conversation::staticGet(
'id',
$this->notice->conversation
diff --git a/lib/pgsqlschema.php b/lib/pgsqlschema.php
index 715065d77..272f7eff6 100644
--- a/lib/pgsqlschema.php
+++ b/lib/pgsqlschema.php
@@ -41,6 +41,7 @@ if (!defined('STATUSNET')) {
* @category Database
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
+ * @author Brenda Wallace <shiny@cpan.org>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
@@ -79,7 +80,6 @@ class PgsqlSchema extends Schema
$row = array();
while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
-// var_dump($row);
$cd = new ColumnDef();
$cd->name = $row['field'];
@@ -143,6 +143,7 @@ class PgsqlSchema extends Schema
$uniques = array();
$primary = array();
$indices = array();
+ $onupdate = array();
$sql = "CREATE TABLE $name (\n";
@@ -155,7 +156,6 @@ class PgsqlSchema extends Schema
}
$sql .= $this->_columnSql($cd);
-
switch ($cd->key) {
case 'UNI':
$uniques[] = $cd->name;
@@ -170,13 +170,7 @@ class PgsqlSchema extends Schema
}
if (count($primary) > 0) { // it really should be...
- $sql .= ",\n primary key (" . implode(',', $primary) . ")";
- }
-
-
-
- foreach ($indices as $i) {
- $sql .= ",\nindex {$name}_{$i}_idx ($i)";
+ $sql .= ",\n PRIMARY KEY (" . implode(',', $primary) . ")";
}
$sql .= "); ";
@@ -185,10 +179,14 @@ class PgsqlSchema extends Schema
foreach ($uniques as $u) {
$sql .= "\n CREATE index {$name}_{$u}_idx ON {$name} ($u); ";
}
+
+ foreach ($indices as $i) {
+ $sql .= "CREATE index {$name}_{$i}_idx ON {$name} ($i)";
+ }
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
- throw new Exception($res->getMessage());
+ throw new Exception($res->getMessage(). ' SQL was '. $sql);
}
return true;
@@ -223,7 +221,7 @@ class PgsqlSchema extends Schema
*/
private function _columnTypeTranslation($type) {
$map = array(
- 'datetime' => 'timestamp'
+ 'datetime' => 'timestamp',
);
if(!empty($map[$type])) {
return $map[$type];
@@ -324,7 +322,7 @@ class PgsqlSchema extends Schema
public function modifyColumn($table, $columndef)
{
- $sql = "ALTER TABLE $table MODIFY COLUMN " .
+ $sql = "ALTER TABLE $table ALTER COLUMN TYPE " .
$this->_columnSql($columndef);
$res = $this->conn->query($sql);
@@ -397,16 +395,17 @@ class PgsqlSchema extends Schema
$todrop = array_diff($cur, $new);
$same = array_intersect($new, $cur);
$tomod = array();
-
foreach ($same as $m) {
$curCol = $this->_byName($td->columns, $m);
$newCol = $this->_byName($columns, $m);
+
if (!$newCol->equals($curCol)) {
- $tomod[] = $newCol->name;
+ // BIG GIANT TODO!
+ // stop it detecting different types and trying to modify on every page request
+// $tomod[] = $newCol->name;
}
}
-
if (count($toadd) + count($todrop) + count($tomod) == 0) {
// nothing to do
return true;
@@ -430,11 +429,12 @@ class PgsqlSchema extends Schema
foreach ($tomod as $columnName) {
$cd = $this->_byName($columns, $columnName);
- $phrase[] = 'MODIFY COLUMN ' . $this->_columnSql($cd);
+ /* brute force */
+ $phrase[] = 'DROP COLUMN ' . $columnName;
+ $phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd);
}
$sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase);
-
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
@@ -496,12 +496,21 @@ class PgsqlSchema extends Schema
*
* @return string correct SQL for that column
*/
-
private function _columnSql($cd)
{
$sql = "{$cd->name} ";
$type = $this->_columnTypeTranslation($cd->type);
+ //handle those mysql enum fields that postgres doesn't support
+ if (preg_match('!^enum!', $type)) {
+ $allowed_values = preg_replace('!^enum!', '', $type);
+ $sql .= " text check ({$cd->name} in $allowed_values)";
+ return $sql;
+ }
+ if (!empty($cd->auto_increment)) {
+ $type = "bigserial"; // FIXME: creates the wrong name for the sequence for some internal sequence-lookup function, so better fix this to do the real 'create sequence' dance.
+ }
+
if (!empty($cd->size)) {
$sql .= "{$type}({$cd->size}) ";
} else {
@@ -513,14 +522,10 @@ class PgsqlSchema extends Schema
} else {
$sql .= ($cd->nullable) ? "null " : "not null ";
}
-
- if (!empty($cd->auto_increment)) {
- $sql .= " auto_increment ";
- }
- if (!empty($cd->extra)) {
- $sql .= "{$cd->extra} ";
- }
+// if (!empty($cd->extra)) {
+// $sql .= "{$cd->extra} ";
+// }
return $sql;
}
diff --git a/lib/ping.php b/lib/ping.php
index 735af9ef1..be2933ae3 100644
--- a/lib/ping.php
+++ b/lib/ping.php
@@ -45,7 +45,15 @@ function ping_broadcast_notice($notice) {
$tags));
$request = HTTPClient::start();
- $httpResponse = $request->post($notify_url, array('Content-Type: text/xml'), $req);
+ $request->setConfig('connect_timeout', common_config('ping', 'timeout'));
+ $request->setConfig('timeout', common_config('ping', 'timeout'));
+ try {
+ $httpResponse = $request->post($notify_url, array('Content-Type: text/xml'), $req);
+ } catch (Exception $e) {
+ common_log(LOG_ERR,
+ "Exception pinging $notify_url: " . $e->getMessage());
+ continue;
+ }
if (!$httpResponse || mb_strlen($httpResponse->getBody()) == 0) {
common_log(LOG_WARNING,
diff --git a/lib/plugin.php b/lib/plugin.php
index 65ccdafbb..f63bdf309 100644
--- a/lib/plugin.php
+++ b/lib/plugin.php
@@ -91,6 +91,7 @@ class Plugin
$path = INSTALLDIR . "/plugins/$name/locale";
if (file_exists($path) && is_dir($path)) {
bindtextdomain($name, $path);
+ bind_textdomain_codeset($name, 'UTF-8');
}
}
}
diff --git a/lib/popularnoticesection.php b/lib/popularnoticesection.php
index 296ddbbb5..f70a972ef 100644
--- a/lib/popularnoticesection.php
+++ b/lib/popularnoticesection.php
@@ -72,7 +72,7 @@ class PopularNoticeSection extends NoticeSection
$qry .= ' GROUP BY notice.id,notice.profile_id,notice.content,notice.uri,' .
'notice.rendered,notice.url,notice.created,notice.modified,' .
'notice.reply_to,notice.is_local,notice.source,notice.conversation, ' .
- 'notice.lat,notice.lon,location_id,location_ns' .
+ 'notice.lat,notice.lon,location_id,location_ns,notice.repeat_of' .
' ORDER BY weight DESC';
$offset = 0;
diff --git a/lib/profileaction.php b/lib/profileaction.php
index 82e0224af..504b77566 100644
--- a/lib/profileaction.php
+++ b/lib/profileaction.php
@@ -174,6 +174,12 @@ class ProfileAction extends OwnerDesignAction
$subbed_count = $this->profile->subscriberCount();
$notice_count = $this->profile->noticeCount();
$group_count = $this->user->getGroups()->N;
+ $age_days = (time() - strtotime($this->profile->created)) / 86400;
+ if ($age_days < 1) {
+ // Rather than extrapolating out to a bajillion...
+ $age_days = 1;
+ }
+ $daily_count = round($notice_count / $age_days);
$this->elementStart('div', array('id' => 'entity_statistics',
'class' => 'section'));
@@ -224,6 +230,12 @@ class ProfileAction extends OwnerDesignAction
$this->element('dd', null, $notice_count);
$this->elementEnd('dl');
+ $this->elementStart('dl', 'entity_daily_notices');
+ // TRANS: Average count of posts made per day since account registration
+ $this->element('dt', null, _('Daily average'));
+ $this->element('dd', null, $daily_count);
+ $this->elementEnd('dl');
+
$this->elementEnd('div');
}
diff --git a/lib/profileformaction.php b/lib/profileformaction.php
index 8a934666e..51c89a922 100644
--- a/lib/profileformaction.php
+++ b/lib/profileformaction.php
@@ -41,7 +41,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
* @link http://status.net/
*/
-class ProfileFormAction extends Action
+class ProfileFormAction extends RedirectingAction
{
var $profile = null;
@@ -60,7 +60,16 @@ class ProfileFormAction extends Action
$this->checkSessionToken();
if (!common_logged_in()) {
- $this->clientError(_('Not logged in.'));
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $this->clientError(_('Not logged in.'));
+ } else {
+ // Redirect to login.
+ common_set_returnto($this->selfUrl());
+ $user = common_current_user();
+ if (Event::handle('RedirectToLogin', array($this, $user))) {
+ common_redirect(common_local_url('login'), 303);
+ }
+ }
return false;
}
@@ -97,30 +106,7 @@ class ProfileFormAction extends Action
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$this->handlePost();
- $this->returnToArgs();
- }
- }
-
- /**
- * Return to the calling page based on hidden arguments
- *
- * @return void
- */
-
- function returnToArgs()
- {
- foreach ($this->args as $k => $v) {
- if ($k == 'returnto-action') {
- $action = $v;
- } else if (substr($k, 0, 9) == 'returnto-') {
- $args[substr($k, 9)] = $v;
- }
- }
-
- if ($action) {
- common_redirect(common_local_url($action, $args), 303);
- } else {
- $this->clientError(_("No return-to arguments."));
+ $this->returnToPrevious();
}
}
diff --git a/lib/redirectingaction.php b/lib/redirectingaction.php
new file mode 100644
index 000000000..3a358f891
--- /dev/null
+++ b/lib/redirectingaction.php
@@ -0,0 +1,97 @@
+<?php
+/**
+ * Superclass for actions that redirect to a given return-to page on completion.
+ *
+ * PHP version 5
+ *
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009-2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @author Brion Vibber <brion@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Superclass for actions that redirect to a given return-to page on completion.
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @author Brion Vibber <brion@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ */
+
+
+class RedirectingAction extends Action
+{
+
+ /**
+ * Redirect browser to the page our hidden parameters requested,
+ * or if none given, to the url given by $this->defaultReturnTo().
+ *
+ * To be called only after successful processing.
+ *
+ * Note: this was named returnToArgs() up through 0.9.2, which
+ * caused problems because there's an Action::returnToArgs()
+ * already which does something different.
+ *
+ * @return void
+ */
+ function returnToPrevious()
+ {
+ // Now, gotta figure where we go back to
+ $action = false;
+ $args = array();
+ $params = array();
+ foreach ($this->args as $k => $v) {
+ if ($k == 'returnto-action') {
+ $action = $v;
+ } else if (substr($k, 0, 15) == 'returnto-param-') {
+ $params[substr($k, 15)] = $v;
+ } elseif (substr($k, 0, 9) == 'returnto-') {
+ $args[substr($k, 9)] = $v;
+ }
+ }
+
+ if ($action) {
+ common_redirect(common_local_url($action, $args, $params), 303);
+ } else {
+ $url = $this->defaultReturnTo();
+ }
+ common_redirect($url, 303);
+ }
+
+ /**
+ * If we reached this form without returnto arguments, where should
+ * we go? May be overridden by subclasses to a reasonable destination
+ * for that action; default implementation throws an exception.
+ *
+ * @return string URL
+ */
+ function defaultReturnTo()
+ {
+ $this->clientError(_("No return-to arguments."));
+ }
+}
diff --git a/lib/router.php b/lib/router.php
index a9d07276f..7e1e6a2a4 100644
--- a/lib/router.php
+++ b/lib/router.php
@@ -136,6 +136,11 @@ class Router
$m->connect('main/'.$a, array('action' => $a));
}
+ // Also need a block variant accepting ID on URL for mail links
+ $m->connect('main/block/:profileid',
+ array('action' => 'block'),
+ array('profileid' => '[0-9]+'));
+
$m->connect('main/sup/:seconds', array('action' => 'sup'),
array('seconds' => '[0-9]+'));
@@ -258,7 +263,7 @@ class Router
$m->connect('tag', array('action' => 'publictagcloud'));
$m->connect('tag/:tag/rss',
array('action' => 'tagrss'),
- array('tag' => '[a-zA-Z0-9]+'));
+ array('tag' => '[\pL\pN_\-\.]{1,64}'));
$m->connect('tag/:tag',
array('action' => 'tag'),
array('tag' => '[\pL\pN_\-\.]{1,64}'));
@@ -535,7 +540,7 @@ class Router
$m->connect('api/favorites/:id.:format',
array('action' => 'ApiTimelineFavorites',
'id' => '[a-zA-Z0-9]+',
- 'format' => '(xmljson|rss|atom)'));
+ 'format' => '(xml|json|rss|atom)'));
$m->connect('api/favorites/create/:id.:format',
array('action' => 'ApiFavoriteCreate',
@@ -592,7 +597,7 @@ class Router
$m->connect('api/statusnet/groups/timeline/:id.:format',
array('action' => 'ApiTimelineGroup',
'id' => '[a-zA-Z0-9]+',
- 'format' => '(xmljson|rss|atom)'));
+ 'format' => '(xml|json|rss|atom)'));
$m->connect('api/statusnet/groups/show.:format',
array('action' => 'ApiGroupShow',
@@ -653,7 +658,7 @@ class Router
// Tags
$m->connect('api/statusnet/tags/timeline/:tag.:format',
array('action' => 'ApiTimelineTag',
- 'format' => '(xmljson|rss|atom)'));
+ 'format' => '(xml|json|rss|atom)'));
// media related
$m->connect(
@@ -662,9 +667,9 @@ class Router
);
// search
- $m->connect('api/search.atom', array('action' => 'twitapisearchatom'));
- $m->connect('api/search.json', array('action' => 'twitapisearchjson'));
- $m->connect('api/trends.json', array('action' => 'twitapitrends'));
+ $m->connect('api/search.atom', array('action' => 'ApiSearchAtom'));
+ $m->connect('api/search.json', array('action' => 'ApiSearchJSON'));
+ $m->connect('api/trends.json', array('action' => 'ApiTrends'));
$m->connect('api/oauth/request_token',
array('action' => 'apioauthrequesttoken'));
@@ -744,12 +749,12 @@ class Router
$m->connect('tag/:tag/rss',
array('action' => 'userrss',
'nickname' => $nickname),
- array('tag' => '[a-zA-Z0-9]+'));
+ array('tag' => '[\pL\pN_\-\.]{1,64}'));
$m->connect('tag/:tag',
array('action' => 'showstream',
'nickname' => $nickname),
- array('tag' => '[a-zA-Z0-9]+'));
+ array('tag' => '[\pL\pN_\-\.]{1,64}'));
$m->connect('rsd.xml',
array('action' => 'rsd',
@@ -810,12 +815,12 @@ class Router
$m->connect(':nickname/tag/:tag/rss',
array('action' => 'userrss'),
array('nickname' => '[a-zA-Z0-9]{1,64}'),
- array('tag' => '[a-zA-Z0-9]+'));
+ array('tag' => '[\pL\pN_\-\.]{1,64}'));
$m->connect(':nickname/tag/:tag',
array('action' => 'showstream'),
array('nickname' => '[a-zA-Z0-9]{1,64}'),
- array('tag' => '[a-zA-Z0-9]+'));
+ array('tag' => '[\pL\pN_\-\.]{1,64}'));
$m->connect(':nickname/rsd.xml',
array('action' => 'rsd'),
diff --git a/lib/rssaction.php b/lib/rssaction.php
index 62e3f21b6..f366db972 100644
--- a/lib/rssaction.php
+++ b/lib/rssaction.php
@@ -178,7 +178,13 @@ class Rss10Action extends Action
if (count($this->notices)) {
foreach ($this->notices as $n) {
- $this->showItem($n);
+ try {
+ $this->showItem($n);
+ } catch (Exception $e) {
+ // log exceptions and continue
+ common_log(LOG_ERR, $e->getMessage());
+ continue;
+ }
}
}
@@ -232,7 +238,7 @@ class Rss10Action extends Action
function showItem($notice)
{
- $profile = Profile::staticGet($notice->profile_id);
+ $profile = $notice->getProfile();
$nurl = common_local_url('shownotice', array('notice' => $notice->id));
$creator_uri = common_profile_uri($profile);
$this->elementStart('item', array('rdf:about' => $notice->uri,
diff --git a/lib/schema.php b/lib/schema.php
index 1503c96d4..e5def514e 100644
--- a/lib/schema.php
+++ b/lib/schema.php
@@ -47,40 +47,47 @@ if (!defined('STATUSNET')) {
class Schema
{
- static $_single = null;
+ static $_static = null;
protected $conn = null;
/**
* Constructor. Only run once for singleton object.
*/
- protected function __construct()
+ protected function __construct($conn = null)
{
- // XXX: there should be an easier way to do this.
- $user = new User();
-
- $this->conn = $user->getDatabaseConnection();
-
- $user->free();
+ if (is_null($conn)) {
+ // XXX: there should be an easier way to do this.
+ $user = new User();
+ $conn = $user->getDatabaseConnection();
+ $user->free();
+ unset($user);
+ }
- unset($user);
+ $this->conn = $conn;
}
/**
* Main public entry point. Use this to get
- * the singleton object.
+ * the schema object.
*
- * @return Schema the (single) Schema object
+ * @return Schema the Schema object for the connection
*/
- static function get()
+ static function get($conn = null)
{
+ if (is_null($conn)) {
+ $key = 'default';
+ } else {
+ $key = md5(serialize($conn->dsn));
+ }
+
$type = common_config('db', 'type');
- if (empty(self::$_single)) {
+ if (empty(self::$_static[$key])) {
$schemaClass = ucfirst($type).'Schema';
- self::$_single = new $schemaClass();
+ self::$_static[$key] = new $schemaClass($conn);
}
- return self::$_single;
+ return self::$_static[$key];
}
/**
diff --git a/lib/statusnet.php b/lib/statusnet.php
index 2aa73486e..7212a4a47 100644
--- a/lib/statusnet.php
+++ b/lib/statusnet.php
@@ -141,7 +141,7 @@ class StatusNet
return true;
}
- $sn = Status_network::staticGet($nickname);
+ $sn = Status_network::staticGet('nickname', $nickname);
if (empty($sn)) {
return false;
throw new Exception("No such site nickname '$nickname'");
diff --git a/lib/stompqueuemanager.php b/lib/stompqueuemanager.php
index 9af8b2f48..fc98c77d4 100644
--- a/lib/stompqueuemanager.php
+++ b/lib/stompqueuemanager.php
@@ -39,7 +39,8 @@ class StompQueueManager extends QueueManager
protected $base;
protected $control;
- protected $useTransactions = true;
+ protected $useTransactions;
+ protected $useAcks;
protected $sites = array();
protected $subscriptions = array();
@@ -59,11 +60,13 @@ class StompQueueManager extends QueueManager
} else {
$this->servers = array($server);
}
- $this->username = common_config('queue', 'stomp_username');
- $this->password = common_config('queue', 'stomp_password');
- $this->base = common_config('queue', 'queue_basename');
- $this->control = common_config('queue', 'control_channel');
- $this->breakout = common_config('queue', 'breakout');
+ $this->username = common_config('queue', 'stomp_username');
+ $this->password = common_config('queue', 'stomp_password');
+ $this->base = common_config('queue', 'queue_basename');
+ $this->control = common_config('queue', 'control_channel');
+ $this->breakout = common_config('queue', 'breakout');
+ $this->useTransactions = common_config('queue', 'stomp_transactions');
+ $this->useAcks = common_config('queue', 'stomp_acks');
}
/**
@@ -112,14 +115,27 @@ class StompQueueManager extends QueueManager
*
* @param mixed $object
* @param string $queue
+ * @param string $siteNickname optional override to drop into another site's queue
*
* @return boolean true on success
* @throws StompException on connection or send error
*/
- public function enqueue($object, $queue)
+ public function enqueue($object, $queue, $siteNickname=null)
{
$this->_connect();
- return $this->_doEnqueue($object, $queue, $this->defaultIdx);
+ if (common_config('queue', 'stomp_enqueue_on')) {
+ // We're trying to force all writes to a single server.
+ // WARNING: this might do odd things if that server connection dies.
+ $idx = array_search(common_config('queue', 'stomp_enqueue_on'),
+ $this->servers);
+ if ($idx === false) {
+ common_log(LOG_ERR, 'queue stomp_enqueue_on setting does not match our server list.');
+ $idx = $this->defaultIdx;
+ }
+ } else {
+ $idx = $this->defaultIdx;
+ }
+ return $this->_doEnqueue($object, $queue, $idx, $siteNickname);
}
/**
@@ -129,10 +145,10 @@ class StompQueueManager extends QueueManager
* @return boolean true on success
* @throws StompException on connection or send error
*/
- protected function _doEnqueue($object, $queue, $idx)
+ protected function _doEnqueue($object, $queue, $idx, $siteNickname=null)
{
$rep = $this->logrep($object);
- $envelope = array('site' => common_config('site', 'nickname'),
+ $envelope = array('site' => $siteNickname ? $siteNickname : common_config('site', 'nickname'),
'handler' => $queue,
'payload' => $this->encode($object));
$msg = serialize($envelope);
@@ -633,7 +649,7 @@ class StompQueueManager extends QueueManager
*/
protected function updateSiteConfig($nickname)
{
- $sn = Status_network::staticGet($nickname);
+ $sn = Status_network::staticGet('nickname', $nickname);
if ($sn) {
$this->switchSite($nickname);
if (!in_array($nickname, $this->sites)) {
@@ -703,13 +719,15 @@ class StompQueueManager extends QueueManager
protected function ack($idx, $frame)
{
- if ($this->useTransactions) {
- if (empty($this->transaction[$idx])) {
- throw new Exception("Tried to ack but not in a transaction");
+ if ($this->useAcks) {
+ if ($this->useTransactions) {
+ if (empty($this->transaction[$idx])) {
+ throw new Exception("Tried to ack but not in a transaction");
+ }
+ $this->cons[$idx]->ack($frame, $this->transaction[$idx]);
+ } else {
+ $this->cons[$idx]->ack($frame);
}
- $this->cons[$idx]->ack($frame, $this->transaction[$idx]);
- } else {
- $this->cons[$idx]->ack($frame);
}
}
diff --git a/lib/theme.php b/lib/theme.php
index 0be8c3b9d..992fce870 100644
--- a/lib/theme.php
+++ b/lib/theme.php
@@ -38,6 +38,9 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
* Themes are directories with some expected sub-directories and files
* in them. They're found in either local/theme (for locally-installed themes)
* or theme/ subdir of installation dir.
+ *
+ * Note that the 'local' directory can be overridden as $config['local']['path']
+ * and $config['local']['dir'] etc.
*
* This used to be a couple of functions, but for various reasons it's nice
* to have a class instead.
@@ -51,6 +54,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
class Theme
{
+ var $name = null;
var $dir = null;
var $path = null;
@@ -67,6 +71,10 @@ class Theme
if (empty($name)) {
$name = common_config('site', 'theme');
}
+ if (!self::validName($name)) {
+ throw new ServerException("Invalid theme name.");
+ }
+ $this->name = $name;
// Check to see if it's in the local dir
@@ -76,7 +84,7 @@ class Theme
if (file_exists($fulldir) && is_dir($fulldir)) {
$this->dir = $fulldir;
- $this->path = common_path('local/theme/'.$name.'/');
+ $this->path = $this->relativeThemePath('local', 'local', 'theme/' . $name);
return;
}
@@ -89,42 +97,63 @@ class Theme
if (file_exists($fulldir) && is_dir($fulldir)) {
$this->dir = $fulldir;
+ $this->path = $this->relativeThemePath('theme', 'theme', $name);
+ }
+ }
- $path = common_config('theme', 'path');
+ /**
+ * Build a full URL to the given theme's base directory, possibly
+ * using an offsite theme server path.
+ *
+ * @param string $group configuration section name to pull paths from
+ * @param string $fallbackSubdir default subdirectory under INSTALLDIR
+ * @param string $name theme name
+ *
+ * @return string URL
+ *
+ * @todo consolidate code with that for other customizable paths
+ */
- if (empty($path)) {
- $path = common_config('site', 'path') . '/theme/';
- }
+ protected function relativeThemePath($group, $fallbackSubdir, $name)
+ {
+ $path = common_config($group, 'path');
- if ($path[strlen($path)-1] != '/') {
- $path .= '/';
+ if (empty($path)) {
+ $path = common_config('site', 'path') . '/';
+ if ($fallbackSubdir) {
+ $path .= $fallbackSubdir . '/';
}
+ }
- if ($path[0] != '/') {
- $path = '/'.$path;
- }
+ if ($path[strlen($path)-1] != '/') {
+ $path .= '/';
+ }
- $server = common_config('theme', 'server');
+ if ($path[0] != '/') {
+ $path = '/'.$path;
+ }
- if (empty($server)) {
- $server = common_config('site', 'server');
- }
+ $server = common_config($group, 'server');
- $ssl = common_config('theme', 'ssl');
+ if (empty($server)) {
+ $server = common_config('site', 'server');
+ }
- if (is_null($ssl)) { // null -> guess
- if (common_config('site', 'ssl') == 'always' &&
- !common_config('theme', 'server')) {
- $ssl = true;
- } else {
- $ssl = false;
- }
+ $ssl = common_config($group, 'ssl');
+
+ if (is_null($ssl)) { // null -> guess
+ if (common_config('site', 'ssl') == 'always' &&
+ !common_config($group, 'server')) {
+ $ssl = true;
+ } else {
+ $ssl = false;
}
+ }
- $protocol = ($ssl) ? 'https' : 'http';
+ $protocol = ($ssl) ? 'https' : 'http';
- $this->path = $protocol . '://'.$server.$path.$name;
- }
+ $path = $protocol . '://'.$server.$path.$name;
+ return $path;
}
/**
@@ -154,6 +183,58 @@ class Theme
}
/**
+ * Fetch a list of other themes whose CSS needs to be pulled in before
+ * this theme's, based on following the theme.ini 'include' settings.
+ * (May be empty if this theme has no include dependencies.)
+ *
+ * @return array of strings with theme names
+ */
+ function getDeps()
+ {
+ $chain = $this->doGetDeps(array($this->name));
+ array_pop($chain); // Drop us back off
+ return $chain;
+ }
+
+ protected function doGetDeps($chain)
+ {
+ $data = $this->getMetadata();
+ if (!empty($data['include'])) {
+ $include = $data['include'];
+
+ // Protect against cycles!
+ if (!in_array($include, $chain)) {
+ try {
+ $theme = new Theme($include);
+ array_unshift($chain, $include);
+ return $theme->doGetDeps($chain);
+ } catch (Exception $e) {
+ common_log(LOG_ERR,
+ "Exception while fetching theme dependencies " .
+ "for $this->name: " . $e->getMessage());
+ }
+ }
+ }
+ return $chain;
+ }
+
+ /**
+ * Pull data from the theme's theme.ini file.
+ * @fixme calling getFile will fall back to default theme, this may be unsafe.
+ *
+ * @return associative array of strings
+ */
+ function getMetadata()
+ {
+ $iniFile = $this->getFile('theme.ini');
+ if (file_exists($iniFile)) {
+ return parse_ini_file($iniFile);
+ } else {
+ return array();
+ }
+ }
+
+ /**
* Gets the full path of a file in a theme dir based on its relative name
*
* @param string $relative relative path within the theme directory
@@ -236,7 +317,13 @@ class Theme
protected static function localRoot()
{
- return INSTALLDIR.'/local/theme';
+ $basedir = common_config('local', 'dir');
+
+ if (empty($basedir)) {
+ $basedir = INSTALLDIR . '/local';
+ }
+
+ return $basedir . '/theme';
}
/**
@@ -255,4 +342,9 @@ class Theme
return $instroot;
}
+
+ static function validName($name)
+ {
+ return preg_match('/^[a-z0-9][a-z0-9_-]*$/i', $name);
+ }
}
diff --git a/lib/themeuploader.php b/lib/themeuploader.php
new file mode 100644
index 000000000..5a48e884e
--- /dev/null
+++ b/lib/themeuploader.php
@@ -0,0 +1,336 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Utilities for theme files and paths
+ *
+ * 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 Paths
+ * @package StatusNet
+ * @author Brion Vibber <brion@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Encapsulation of the validation-and-save process when dealing with
+ * a user-uploaded StatusNet theme archive...
+ *
+ * @todo extract theme metadata from css/display.css
+ * @todo allow saving multiple themes
+ */
+class ThemeUploader
+{
+ protected $sourceFile;
+ protected $isUpload;
+ private $prevErrorReporting;
+
+ public function __construct($filename)
+ {
+ if (!class_exists('ZipArchive')) {
+ throw new Exception(_("This server cannot handle theme uploads without ZIP support."));
+ }
+ $this->sourceFile = $filename;
+ }
+
+ public static function fromUpload($name)
+ {
+ if (!isset($_FILES[$name]['error'])) {
+ throw new ServerException(_("The theme file is missing or the upload failed."));
+ }
+ if ($_FILES[$name]['error'] != UPLOAD_ERR_OK) {
+ throw new ServerException(_("The theme file is missing or the upload failed."));
+ }
+ return new ThemeUploader($_FILES[$name]['tmp_name']);
+ }
+
+ /**
+ * @param string $destDir
+ * @throws Exception on bogus files
+ */
+ public function extract($destDir)
+ {
+ $zip = $this->openArchive();
+
+ // First pass: validate but don't save anything to disk.
+ // Any errors will trip an exception.
+ $this->traverseArchive($zip);
+
+ // Second pass: now that we know we're good, actually extract!
+ $tmpDir = $destDir . '.tmp' . getmypid();
+ $this->traverseArchive($zip, $tmpDir);
+
+ $zip->close();
+
+ if (file_exists($destDir)) {
+ $killDir = $tmpDir . '.old';
+ $this->quiet();
+ $ok = rename($destDir, $killDir);
+ $this->loud();
+ if (!$ok) {
+ common_log(LOG_ERR, "Could not move old custom theme from $destDir to $killDir");
+ throw new ServerException(_("Failed saving theme."));
+ }
+ } else {
+ $killDir = false;
+ }
+
+ $this->quiet();
+ $ok = rename($tmpDir, $destDir);
+ $this->loud();
+ if (!$ok) {
+ common_log(LOG_ERR, "Could not move saved theme from $tmpDir to $destDir");
+ throw new ServerException(_("Failed saving theme."));
+ }
+
+ if ($killDir) {
+ $this->recursiveRmdir($killDir);
+ }
+ }
+
+ /**
+ *
+ */
+ protected function traverseArchive($zip, $outdir=false)
+ {
+ $sizeLimit = 2 * 1024 * 1024; // 2 megabyte space limit?
+ $blockSize = 4096; // estimated; any entry probably takes this much space
+
+ $totalSize = 0;
+ $hasMain = false;
+ $commonBaseDir = false;
+
+ for ($i = 0; $i < $zip->numFiles; $i++) {
+ $data = $zip->statIndex($i);
+ $name = str_replace('\\', '/', $data['name']);
+
+ if (substr($name, -1) == '/') {
+ // A raw directory... skip!
+ continue;
+ }
+
+ // Is this a safe or skippable file?
+ $path = pathinfo($name);
+ if ($this->skippable($path['filename'], $path['extension'])) {
+ // Documentation and such... booooring
+ continue;
+ } else {
+ $this->validateFile($path['filename'], $path['extension']);
+ }
+
+ // Check the directory structure...
+ $dirs = explode('/', $path['dirname']);
+ $baseDir = array_shift($dirs);
+ if ($commonBaseDir === false) {
+ $commonBaseDir = $baseDir;
+ } else {
+ if ($commonBaseDir != $baseDir) {
+ throw new ClientException(_("Invalid theme: bad directory structure."));
+ }
+ }
+
+ foreach ($dirs as $dir) {
+ $this->validateFileOrFolder($dir);
+ }
+
+ $fullPath = $dirs;
+ $fullPath[] = $path['basename'];
+ $localFile = implode('/', $fullPath);
+ if ($localFile == 'css/display.css') {
+ $hasMain = true;
+ }
+
+ $size = $data['size'];
+ $estSize = $blockSize * max(1, intval(ceil($size / $blockSize)));
+ $totalSize += $estSize;
+ if ($totalSize > $sizeLimit) {
+ $msg = sprintf(_("Uploaded theme is too large; " .
+ "must be less than %d bytes uncompressed."),
+ $sizeLimit);
+ throw new ClientException($msg);
+ }
+
+ if ($outdir) {
+ $this->extractFile($zip, $data['name'], "$outdir/$localFile");
+ }
+ }
+
+ if (!$hasMain) {
+ throw new ClientException(_("Invalid theme archive: " .
+ "missing file css/display.css"));
+ }
+ }
+
+ /**
+ * @fixme Probably most unrecognized files should just be skipped...
+ */
+ protected function skippable($filename, $ext)
+ {
+ $skip = array('txt', 'html', 'rtf', 'doc', 'docx', 'odt', 'xcf');
+ if (strtolower($filename) == 'readme') {
+ return true;
+ }
+ if (in_array(strtolower($ext), $skip)) {
+ return true;
+ }
+ if ($filename == '' || substr($filename, 0, 1) == '.') {
+ // Skip Unix-style hidden files
+ return true;
+ }
+ if ($filename == '__MACOSX') {
+ // Skip awful metadata files Mac OS X slips in for you.
+ // Thanks Apple!
+ return true;
+ }
+ return false;
+ }
+
+ protected function validateFile($filename, $ext)
+ {
+ $this->validateFileOrFolder($filename);
+ $this->validateExtension($filename, $ext);
+ // @fixme validate content
+ }
+
+ protected function validateFileOrFolder($name)
+ {
+ if (!preg_match('/^[a-z0-9_\.-]+$/i', $name)) {
+ common_log(LOG_ERR, "Bad theme filename: $name");
+ $msg = _("Theme contains invalid file or folder name. " .
+ "Stick with ASCII letters, digits, underscore, and minus sign.");
+ throw new ClientException($msg);
+ }
+ if (preg_match('/\.(php|cgi|asp|aspx|js|vb)\w/i', $name)) {
+ common_log(LOG_ERR, "Unsafe theme filename: $name");
+ $msg = _("Theme contains unsafe file extension names; may be unsafe.");
+ throw new ClientException($msg);
+ }
+ return true;
+ }
+
+ protected function validateExtension($base, $ext)
+ {
+ $allowed = array('css', // CSS may need validation
+ 'png', 'gif', 'jpg', 'jpeg',
+ 'svg', // SVG images/fonts may need validation
+ 'ttf', 'eot', 'woff');
+ if (!in_array(strtolower($ext), $allowed)) {
+ if ($ext == 'ini' && $base == 'theme') {
+ // theme.ini exception
+ return true;
+ }
+ $msg = sprintf(_("Theme contains file of type '.%s', " .
+ "which is not allowed."),
+ $ext);
+ throw new ClientException($msg);
+ }
+ return true;
+ }
+
+ /**
+ * @return ZipArchive
+ */
+ protected function openArchive()
+ {
+ $zip = new ZipArchive;
+ $ok = $zip->open($this->sourceFile);
+ if ($ok !== true) {
+ common_log(LOG_ERR, "Error opening theme zip archive: " .
+ "{$this->sourceFile} code: {$ok}");
+ throw new Exception(_("Error opening theme archive."));
+ }
+ return $zip;
+ }
+
+ /**
+ * @param ZipArchive $zip
+ * @param string $from original path inside ZIP archive
+ * @param string $to final destination path in filesystem
+ */
+ protected function extractFile($zip, $from, $to)
+ {
+ $dir = dirname($to);
+ if (!file_exists($dir)) {
+ $this->quiet();
+ $ok = mkdir($dir, 0755, true);
+ $this->loud();
+ if (!$ok) {
+ common_log(LOG_ERR, "Failed to mkdir $dir while uploading theme");
+ throw new ServerException(_("Failed saving theme."));
+ }
+ } else if (!is_dir($dir)) {
+ common_log(LOG_ERR, "Output directory $dir not a directory while uploading theme");
+ throw new ServerException(_("Failed saving theme."));
+ }
+
+ // ZipArchive::extractTo would be easier, but won't let us alter
+ // the directory structure.
+ $in = $zip->getStream($from);
+ if (!$in) {
+ common_log(LOG_ERR, "Couldn't open archived file $from while uploading theme");
+ throw new ServerException(_("Failed saving theme."));
+ }
+ $this->quiet();
+ $out = fopen($to, "wb");
+ $this->loud();
+ if (!$out) {
+ common_log(LOG_ERR, "Couldn't open output file $to while uploading theme");
+ throw new ServerException(_("Failed saving theme."));
+ }
+ while (!feof($in)) {
+ $buffer = fread($in, 65536);
+ fwrite($out, $buffer);
+ }
+ fclose($in);
+ fclose($out);
+ }
+
+ private function quiet()
+ {
+ $this->prevErrorReporting = error_reporting();
+ error_reporting($this->prevErrorReporting & ~E_WARNING);
+ }
+
+ private function loud()
+ {
+ error_reporting($this->prevErrorReporting);
+ }
+
+ private function recursiveRmdir($dir)
+ {
+ $list = dir($dir);
+ while (($file = $list->read()) !== false) {
+ if ($file == '.' || $file == '..') {
+ continue;
+ }
+ $full = "$dir/$file";
+ if (is_dir($full)) {
+ $this->recursiveRmdir($full);
+ } else {
+ unlink($full);
+ }
+ }
+ $list->close();
+ rmdir($dir);
+ }
+
+}
diff --git a/lib/usernoprofileexception.php b/lib/usernoprofileexception.php
index 6744d2529..e0186fef9 100644
--- a/lib/usernoprofileexception.php
+++ b/lib/usernoprofileexception.php
@@ -55,7 +55,9 @@ class UserNoProfileException extends ServerException
{
$this->user = $user;
- $message = sprintf(_("User %s (%d) has no profile record."),
+ // TRANS: Exception text shown when no profile can be found for a user.
+ // TRANS: %1$s is a user nickname, $2$d is a user ID (number).
+ $message = sprintf(_("User %1$s (%2$d) has no profile record."),
$user->nickname, $user->id);
parent::__construct($message);
diff --git a/lib/util.php b/lib/util.php
index 58d54cda3..f63e152e3 100644
--- a/lib/util.php
+++ b/lib/util.php
@@ -34,6 +34,14 @@ function common_user_error($msg, $code=400)
$err->showPage();
}
+/**
+ * This should only be used at setup; processes switching languages
+ * to send text to other users should use common_switch_locale().
+ *
+ * @param string $language Locale language code (optional; empty uses
+ * current user's preference or site default)
+ * @return mixed success
+ */
function common_init_locale($language=null)
{
if(!$language) {
@@ -50,6 +58,15 @@ function common_init_locale($language=null)
return $ok;
}
+/**
+ * Initialize locale and charset settings and gettext with our message catalog,
+ * using the current user's language preference or the site default.
+ *
+ * This should generally only be run at framework initialization; code switching
+ * languages at runtime should call common_switch_language().
+ *
+ * @access private
+ */
function common_init_language()
{
mb_internal_encoding('UTF-8');
@@ -71,8 +88,8 @@ function common_init_language()
// don't do the job. en_US.UTF-8 should be there most of the
// time, but not guaranteed.
$ok = common_init_locale("en_US");
- if (!$ok) {
- // Try to find a complete, working locale...
+ if (!$ok && strtolower(substr(PHP_OS, 0, 3)) != 'win') {
+ // Try to find a complete, working locale on Unix/Linux...
// @fixme shelling out feels awfully inefficient
// but I don't think there's a more standard way.
$all = `locale -a`;
@@ -84,9 +101,9 @@ function common_init_language()
}
}
}
- if (!$ok) {
- common_log(LOG_ERR, "Unable to find a UTF-8 locale on this system; UI translations may not work.");
- }
+ }
+ if (!$ok) {
+ common_log(LOG_ERR, "Unable to find a UTF-8 locale on this system; UI translations may not work.");
}
$locale_set = common_init_locale($language);
}
@@ -813,7 +830,10 @@ function common_linkify($url) {
} elseif (is_string($longurl_data)) {
$longurl = $longurl_data;
} else {
- throw new ServerException("Can't linkify url '$url'");
+ // Unable to reach the server to verify contents, etc
+ // Just pass the link on through for now.
+ common_log(LOG_ERR, "Can't linkify url '$url'");
+ $longurl = $url;
}
}
$attrs = array('href' => $canon, 'title' => $longurl, 'rel' => 'external');
@@ -832,7 +852,7 @@ function common_linkify($url) {
}
if (!empty($f)) {
- if ($f->getEnclosure()) {
+ if ($f->getEnclosure() || File_oembed::staticGet('file_id',$f->id)) {
$is_attachment = true;
$attachment_id = $f->id;
@@ -855,10 +875,10 @@ function common_linkify($url) {
return XMLStringer::estring('a', $attrs, $url);
}
-function common_shorten_links($text)
+function common_shorten_links($text, $always = false)
{
$maxLength = Notice::maxContent();
- if ($maxLength == 0 || mb_strlen($text) <= $maxLength) return $text;
+ if (!$always && ($maxLength == 0 || mb_strlen($text) <= $maxLength)) return $text;
return common_replace_urls_callback($text, array('File_redirection', 'makeShort'));
}
@@ -998,8 +1018,7 @@ function common_local_url($action, $args=null, $params=null, $fragment=null, $ad
function common_is_sensitive($action)
{
- static $sensitive = array('login', 'register', 'passwordsettings',
- 'twittersettings', 'api');
+ static $sensitive = array('login', 'register', 'passwordsettings', 'api');
$ssl = null;
if (Event::handle('SensitiveAction', array($action, &$ssl))) {
@@ -1079,24 +1098,38 @@ function common_date_string($dt)
if ($now < $t) { // that shouldn't happen!
return common_exact_date($dt);
} else if ($diff < 60) {
+ // TRANS: Used in notices to indicate when the notice was made compared to now.
return _('a few seconds ago');
} else if ($diff < 92) {
+ // TRANS: Used in notices to indicate when the notice was made compared to now.
return _('about a minute ago');
} else if ($diff < 3300) {
+ // XXX: should support plural.
+ // TRANS: Used in notices to indicate when the notice was made compared to now.
return sprintf(_('about %d minutes ago'), round($diff/60));
} else if ($diff < 5400) {
+ // TRANS: Used in notices to indicate when the notice was made compared to now.
return _('about an hour ago');
} else if ($diff < 22 * 3600) {
+ // XXX: should support plural.
+ // TRANS: Used in notices to indicate when the notice was made compared to now.
return sprintf(_('about %d hours ago'), round($diff/3600));
} else if ($diff < 37 * 3600) {
+ // TRANS: Used in notices to indicate when the notice was made compared to now.
return _('about a day ago');
} else if ($diff < 24 * 24 * 3600) {
+ // XXX: should support plural.
+ // TRANS: Used in notices to indicate when the notice was made compared to now.
return sprintf(_('about %d days ago'), round($diff/(24*3600)));
} else if ($diff < 46 * 24 * 3600) {
+ // TRANS: Used in notices to indicate when the notice was made compared to now.
return _('about a month ago');
} else if ($diff < 330 * 24 * 3600) {
+ // XXX: should support plural.
+ // TRANS: Used in notices to indicate when the notice was made compared to now.
return sprintf(_('about %d months ago'), round($diff/(30*24*3600)));
} else if ($diff < 480 * 24 * 3600) {
+ // TRANS: Used in notices to indicate when the notice was made compared to now.
return _('about a year ago');
} else {
return common_exact_date($dt);
@@ -1218,9 +1251,8 @@ function common_enqueue_notice($notice)
$transports[] = 'jabber';
}
- // @fixme move these checks into QueueManager and/or individual handlers
- if ($notice->is_local == Notice::LOCAL_PUBLIC ||
- $notice->is_local == Notice::LOCAL_NONPUBLIC) {
+ // We can skip these for gatewayed notices.
+ if ($notice->isLocal()) {
$transports = array_merge($transports, $localTransports);
if ($xmpp) {
$transports[] = 'public';
@@ -1308,12 +1340,38 @@ function common_mtrand($bytes)
return $enc;
}
+/**
+ * Record the given URL as the return destination for a future
+ * form submission, to be read by common_get_returnto().
+ *
+ * @param string $url
+ *
+ * @fixme as a session-global setting, this can allow multiple forms
+ * to conflict and overwrite each others' returnto destinations if
+ * the user has multiple tabs or windows open.
+ *
+ * Should refactor to index with a token or otherwise only pass the
+ * data along its intended path.
+ */
function common_set_returnto($url)
{
common_ensure_session();
$_SESSION['returnto'] = $url;
}
+/**
+ * Fetch a return-destination URL previously recorded by
+ * common_set_returnto().
+ *
+ * @return mixed URL string or null
+ *
+ * @fixme as a session-global setting, this can allow multiple forms
+ * to conflict and overwrite each others' returnto destinations if
+ * the user has multiple tabs or windows open.
+ *
+ * Should refactor to index with a token or otherwise only pass the
+ * data along its intended path.
+ */
function common_get_returnto()
{
common_ensure_session();
@@ -1339,7 +1397,7 @@ function common_log_line($priority, $msg)
{
static $syslog_priorities = array('LOG_EMERG', 'LOG_ALERT', 'LOG_CRIT', 'LOG_ERR',
'LOG_WARNING', 'LOG_NOTICE', 'LOG_INFO', 'LOG_DEBUG');
- return date('Y-m-d H:i:s') . ' ' . $syslog_priorities[$priority] . ': ' . $msg . "\n";
+ return date('Y-m-d H:i:s') . ' ' . $syslog_priorities[$priority] . ': ' . $msg . PHP_EOL;
}
function common_request_id()
@@ -1433,6 +1491,55 @@ function common_valid_tag($tag)
return false;
}
+/**
+ * Determine if given domain or address literal is valid
+ * eg for use in JIDs and URLs. Does not check if the domain
+ * exists!
+ *
+ * @param string $domain
+ * @return boolean valid or not
+ */
+function common_valid_domain($domain)
+{
+ $octet = "(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])";
+ $ipv4 = "(?:$octet(?:\.$octet){3})";
+ if (preg_match("/^$ipv4$/u", $domain)) return true;
+
+ $group = "(?:[0-9a-f]{1,4})";
+ $ipv6 = "(?:\[($group(?::$group){0,7})?(::)?($group(?::$group){0,7})?\])"; // http://tools.ietf.org/html/rfc3513#section-2.2
+
+ if (preg_match("/^$ipv6$/ui", $domain, $matches)) {
+ $before = explode(":", $matches[1]);
+ $zeroes = $matches[2];
+ $after = explode(":", $matches[3]);
+ if ($zeroes) {
+ $min = 0;
+ $max = 7;
+ } else {
+ $min = 1;
+ $max = 8;
+ }
+ $explicit = count($before) + count($after);
+ if ($explicit < $min || $explicit > $max) {
+ return false;
+ }
+ return true;
+ }
+
+ try {
+ require_once "Net/IDNA.php";
+ $idn = Net_IDNA::getInstance();
+ $domain = $idn->encode($domain);
+ } catch (Exception $e) {
+ return false;
+ }
+
+ $subdomain = "(?:[a-z0-9][a-z0-9-]*)"; // @fixme
+ $fqdn = "(?:$subdomain(?:\.$subdomain)*\.?)";
+
+ return preg_match("/^$fqdn$/ui", $domain);
+}
+
/* Following functions are copied from MediaWiki GlobalFunctions.php
* and written by Evan Prodromou. */
@@ -1833,6 +1940,15 @@ function common_url_to_nickname($url)
$path = preg_replace('@/$@', '', $parts['path']);
$path = preg_replace('@^/@', '', $path);
$path = basename($path);
+
+ // Hack for MediaWiki user pages, in the form:
+ // http://example.com/wiki/User:Myname
+ // ('User' may be localized.)
+ if (strpos($path, ':')) {
+ $parts = array_filter(explode(':', $path));
+ $path = $parts[count($parts) - 1];
+ }
+
if ($path) {
return common_nicknamize($path);
}
diff --git a/lib/xmppmanager.php b/lib/xmppmanager.php
index cca54db08..829eaa36c 100644
--- a/lib/xmppmanager.php
+++ b/lib/xmppmanager.php
@@ -253,12 +253,12 @@ class XmppManager extends IoManager
$from = jabber_normalize_jid($pl['from']);
if ($pl['type'] != 'chat') {
- $this->log(LOG_WARNING, "Ignoring message of type ".$pl['type']." from $from.");
+ $this->log(LOG_WARNING, "Ignoring message of type ".$pl['type']." from $from: " . $pl['xml']->toString());
return;
}
if (mb_strlen($pl['body']) == 0) {
- $this->log(LOG_WARNING, "Ignoring message with empty body from $from.");
+ $this->log(LOG_WARNING, "Ignoring message with empty body from $from: " . $pl['xml']->toString());
return;
}
diff --git a/lib/xrdsoutputter.php b/lib/xrdsoutputter.php
index 4b77ed5a3..95dc73300 100644
--- a/lib/xrdsoutputter.php
+++ b/lib/xrdsoutputter.php
@@ -23,6 +23,7 @@
* @package StatusNet
* @author Craig Andrews <candrews@integralblue.com>
* @copyright 2008 StatusNet, Inc.
+ * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/