summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/accountsettingsaction.php30
-rw-r--r--lib/action.php105
-rw-r--r--lib/activity.php1149
-rw-r--r--lib/activitycontext.php121
-rw-r--r--lib/activityobject.php557
-rw-r--r--lib/activityutils.php273
-rw-r--r--lib/activityverb.php66
-rw-r--r--lib/adminpanelaction.php64
-rw-r--r--lib/apiaction.php318
-rw-r--r--lib/apiauth.php51
-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/atom10entry.php105
-rw-r--r--lib/atom10feed.php28
-rw-r--r--lib/atomcategory.php77
-rw-r--r--lib/atomgroupnoticefeed.php57
-rw-r--r--lib/atomnoticefeed.php38
-rw-r--r--lib/atomusernoticefeed.php58
-rw-r--r--lib/attachmentlist.php91
-rw-r--r--lib/authenticationplugin.php1
-rw-r--r--lib/authorizationplugin.php5
-rw-r--r--lib/avatarlink.php102
-rw-r--r--lib/channel.php19
-rw-r--r--lib/command.php395
-rw-r--r--lib/common.php17
-rw-r--r--lib/dbqueuemanager.php4
-rw-r--r--lib/default.php36
-rw-r--r--lib/deluserqueuehandler.php99
-rw-r--r--lib/distribqueuehandler.php17
-rw-r--r--lib/grantroleform.php93
-rw-r--r--lib/htmloutputter.php1
-rw-r--r--lib/httpclient.php43
-rw-r--r--lib/imagefile.php33
-rw-r--r--lib/installer.php585
-rw-r--r--lib/jabber.php213
-rw-r--r--lib/language.php35
-rw-r--r--lib/liberalstomp.php27
-rw-r--r--lib/mail.php128
-rw-r--r--lib/mailhandler.php4
-rw-r--r--lib/mediafile.php56
-rw-r--r--lib/mysqlschema.php236
-rw-r--r--lib/noticeform.php4
-rw-r--r--lib/noticelist.php125
-rw-r--r--lib/pgsqlschema.php81
-rw-r--r--lib/ping.php10
-rw-r--r--lib/plugin.php1
-rw-r--r--lib/poco.php240
-rw-r--r--lib/pocoaddress.php56
-rw-r--r--lib/pocourl.php65
-rw-r--r--lib/popularnoticesection.php2
-rw-r--r--lib/processmanager.php84
-rw-r--r--lib/profileaction.php135
-rw-r--r--lib/profileformaction.php38
-rw-r--r--lib/profilelist.php36
-rw-r--r--lib/profileminilist.php46
-rw-r--r--lib/queued_xmpp.php18
-rw-r--r--lib/queuemanager.php3
-rw-r--r--lib/redirectingaction.php97
-rw-r--r--lib/revokeroleform.php93
-rw-r--r--lib/right.php2
-rw-r--r--lib/router.php65
-rw-r--r--lib/schema.php6
-rw-r--r--lib/servererroraction.php9
-rw-r--r--lib/spawningdaemon.php32
-rw-r--r--lib/statusnet.php21
-rw-r--r--lib/stompqueuemanager.php50
-rw-r--r--lib/subs.php43
-rw-r--r--lib/subscriptionlist.php13
-rw-r--r--lib/theme.php82
-rw-r--r--lib/themeuploader.php311
-rw-r--r--lib/usernoprofileexception.php74
-rw-r--r--lib/userprofile.php40
-rw-r--r--lib/util.php238
-rw-r--r--lib/xmppmanager.php5
-rw-r--r--lib/xrdsoutputter.php1
77 files changed, 5567 insertions, 1958 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 a804b8e82..2b3b707c5 100644
--- a/lib/action.php
+++ b/lib/action.php
@@ -141,6 +141,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 +157,7 @@ class Action extends HTMLOutputter // lawsuit
function title()
{
+ // TRANS: Page title for a page without a title set.
return _("Untitled page");
}
@@ -198,8 +200,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');
- $this->cssLink('css/print.css','base','print');
+ $this->cssLink('css/display.css',null, 'screen, projection, tv, print');
Event::handle('EndShowStatusNetStyles', array($this));
Event::handle('EndShowLaconicaStyles', array($this));
}
@@ -234,6 +235,16 @@ 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));
+ }
+ }
}
}
@@ -420,14 +431,8 @@ class Action extends HTMLOutputter // lawsuit
function showPrimaryNav()
{
$user = common_current_user();
- $connect = '';
- if (common_config('xmpp', 'enabled')) {
- $connect = 'imsettings';
- } else if (common_config('sms', 'enabled')) {
- $connect = 'smssettings';
- }
-
$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'));
@@ -435,33 +440,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'),
- _m('MENU', 'Account'), $tooltip, false, 'nav_account');
- if ($connect) {
- // TRANS: Tooltip for main menu option "Services"
- $tooltip = _m('TOOLTIP', 'Connect to services');
- // TRANS: Main menu option when logged in and connection are possible for access to options to connect to other services
- $this->menuItem(common_local_url($connect),
- _m('MENU', 'Connect'), $tooltip, false, 'nav_connect');
- }
+ // 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');
+ $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')),
@@ -469,16 +472,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')) {
+ 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"
@@ -585,6 +588,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();
@@ -651,6 +655,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))) {
@@ -753,28 +758,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'));
@@ -805,13 +819,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);
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);
@@ -827,19 +846,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;
@@ -852,14 +877,16 @@ class Action extends HTMLOutputter // lawsuit
'width' => '80',
'height' => '15'));
$this->text(' ');
- //TODO: This is dirty: i18n
- $this->text(_('All '.common_config('site', 'name').' content and data are available under the '));
- $this->element('a', array('class' => 'license',
- 'rel' => 'external license',
- 'href' => common_config('license', 'url')),
- common_config('license', 'title'));
- $this->text(' ');
- $this->text(_('license.'));
+ // TRANS: license message in footer. %1$s is the site name, %2$s is a link to the license URL, with a licence name set in configuration.
+ $notice = _('All %1$s content and data are available under the %2$s license.');
+ $link = "<a class=\"license\" rel=\"external license\" href=\"" .
+ htmlspecialchars(common_config('license', 'url')) .
+ "\">" .
+ htmlspecialchars(common_config('license', 'title')) .
+ "</a>";
+ $this->raw(sprintf(htmlspecialchars($notice),
+ htmlspecialchars(common_config('site', 'name')),
+ $link));
$this->elementEnd('p');
break;
}
@@ -1153,11 +1180,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'));
@@ -1167,6 +1198,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');
}
@@ -1175,6 +1208,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');
}
@@ -1218,6 +1253,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 b20153213..8e2da99bb 100644
--- a/lib/activity.php
+++ b/lib/activity.php
@@ -32,967 +32,6 @@ if (!defined('STATUSNET')) {
exit(1);
}
-class PoCoURL
-{
- const URLS = 'urls';
- const TYPE = 'type';
- const VALUE = 'value';
- const PRIMARY = 'primary';
-
- public $type;
- public $value;
- public $primary;
-
- function __construct($type, $value, $primary = false)
- {
- $this->type = $type;
- $this->value = $value;
- $this->primary = $primary;
- }
-
- function asString()
- {
- $xs = new XMLStringer(true);
- $xs->elementStart('poco:urls');
- $xs->element('poco:type', null, $this->type);
- $xs->element('poco:value', null, $this->value);
- if (!empty($this->primary)) {
- $xs->element('poco:primary', null, 'true');
- }
- $xs->elementEnd('poco:urls');
- return $xs->getString();
- }
-}
-
-class PoCoAddress
-{
- const ADDRESS = 'address';
- const FORMATTED = 'formatted';
-
- public $formatted;
-
- // @todo Other address fields
-
- function asString()
- {
- if (!empty($this->formatted)) {
- $xs = new XMLStringer(true);
- $xs->elementStart('poco:address');
- $xs->element('poco:formatted', null, $this->formatted);
- $xs->elementEnd('poco:address');
- return $xs->getString();
- }
-
- return null;
- }
-}
-
-class PoCo
-{
- const NS = 'http://portablecontacts.net/spec/1.0';
-
- const USERNAME = 'preferredUsername';
- const DISPLAYNAME = 'displayName';
- const NOTE = 'note';
-
- public $preferredUsername;
- public $displayName;
- public $note;
- public $address;
- public $urls = array();
-
- function __construct($element = null)
- {
- if (empty($element)) {
- return;
- }
-
- $this->preferredUsername = ActivityUtils::childContent(
- $element,
- self::USERNAME,
- self::NS
- );
-
- $this->displayName = ActivityUtils::childContent(
- $element,
- self::DISPLAYNAME,
- self::NS
- );
-
- $this->note = ActivityUtils::childContent(
- $element,
- self::NOTE,
- self::NS
- );
-
- $this->address = $this->_getAddress($element);
- $this->urls = $this->_getURLs($element);
- }
-
- private function _getURLs($element)
- {
- $urlEls = $element->getElementsByTagnameNS(self::NS, PoCoURL::URLS);
- $urls = array();
-
- foreach ($urlEls as $urlEl) {
-
- $type = ActivityUtils::childContent(
- $urlEl,
- PoCoURL::TYPE,
- PoCo::NS
- );
-
- $value = ActivityUtils::childContent(
- $urlEl,
- PoCoURL::VALUE,
- PoCo::NS
- );
-
- $primary = ActivityUtils::childContent(
- $urlEl,
- PoCoURL::PRIMARY,
- PoCo::NS
- );
-
- $isPrimary = false;
-
- if (isset($primary) && $primary == 'true') {
- $isPrimary = true;
- }
-
- // @todo check to make sure a primary hasn't already been added
-
- array_push($urls, new PoCoURL($type, $value, $isPrimary));
- }
- return $urls;
- }
-
- private function _getAddress($element)
- {
- $addressEl = ActivityUtils::child(
- $element,
- PoCoAddress::ADDRESS,
- PoCo::NS
- );
-
- if (!empty($addressEl)) {
- $formatted = ActivityUtils::childContent(
- $addressEl,
- PoCoAddress::FORMATTED,
- self::NS
- );
-
- if (!empty($formatted)) {
- $address = new PoCoAddress();
- $address->formatted = $formatted;
- return $address;
- }
- }
-
- return null;
- }
-
- function fromProfile($profile)
- {
- if (empty($profile)) {
- return null;
- }
-
- $poco = new PoCo();
-
- $poco->preferredUsername = $profile->nickname;
- $poco->displayName = $profile->getBestName();
-
- $poco->note = $profile->bio;
-
- $paddy = new PoCoAddress();
- $paddy->formatted = $profile->location;
- $poco->address = $paddy;
-
- if (!empty($profile->homepage)) {
- array_push(
- $poco->urls,
- new PoCoURL(
- 'homepage',
- $profile->homepage,
- true
- )
- );
- }
-
- return $poco;
- }
-
- function fromGroup($group)
- {
- if (empty($group)) {
- return null;
- }
-
- $poco = new PoCo();
-
- $poco->preferredUsername = $group->nickname;
- $poco->displayName = $group->getBestName();
-
- $poco->note = $group->description;
-
- $paddy = new PoCoAddress();
- $paddy->formatted = $group->location;
- $poco->address = $paddy;
-
- if (!empty($group->homepage)) {
- array_push(
- $poco->urls,
- new PoCoURL(
- 'homepage',
- $group->homepage,
- true
- )
- );
- }
-
- return $poco;
- }
-
- function getPrimaryURL()
- {
- foreach ($this->urls as $url) {
- if ($url->primary) {
- return $url;
- }
- }
- }
-
- function asString()
- {
- $xs = new XMLStringer(true);
- $xs->element(
- 'poco:preferredUsername',
- null,
- $this->preferredUsername
- );
-
- $xs->element(
- 'poco:displayName',
- null,
- $this->displayName
- );
-
- if (!empty($this->note)) {
- $xs->element('poco:note', null, $this->note);
- }
-
- if (!empty($this->address)) {
- $xs->raw($this->address->asString());
- }
-
- foreach ($this->urls as $url) {
- $xs->raw($url->asString());
- }
-
- return $xs->getString();
- }
-}
-
-/**
- * Utilities for turning DOMish things into Activityish things
- *
- * Some common functions that I didn't have the bandwidth to try to factor
- * into some kind of reasonable superclass, so just dumped here. Might
- * be useful to have an ActivityObject parent class or something.
- *
- * @category OStatus
- * @package StatusNet
- * @author Evan Prodromou <evan@status.net>
- * @copyright 2010 StatusNet, Inc.
- * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
- * @link http://status.net/
- */
-
-class ActivityUtils
-{
- const ATOM = 'http://www.w3.org/2005/Atom';
-
- const LINK = 'link';
- const REL = 'rel';
- const TYPE = 'type';
- const HREF = 'href';
-
- const CONTENT = 'content';
- const SRC = 'src';
-
- /**
- * Get the permalink for an Activity object
- *
- * @param DOMElement $element A DOM element
- *
- * @return string related link, if any
- */
-
- static function getPermalink($element)
- {
- return self::getLink($element, 'alternate', 'text/html');
- }
-
- /**
- * Get the permalink for an Activity object
- *
- * @param DOMElement $element A DOM element
- *
- * @return string related link, if any
- */
-
- static function getLink(DOMNode $element, $rel, $type=null)
- {
- $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK);
-
- foreach ($links as $link) {
-
- $linkRel = $link->getAttribute(self::REL);
- $linkType = $link->getAttribute(self::TYPE);
-
- if ($linkRel == $rel &&
- (is_null($type) || $linkType == $type)) {
- return $link->getAttribute(self::HREF);
- }
- }
-
- return null;
- }
-
- static function getLinks(DOMNode $element, $rel, $type=null)
- {
- $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK);
- $out = array();
-
- foreach ($links as $link) {
-
- $linkRel = $link->getAttribute(self::REL);
- $linkType = $link->getAttribute(self::TYPE);
-
- if ($linkRel == $rel &&
- (is_null($type) || $linkType == $type)) {
- $out[] = $link;
- }
- }
-
- return $out;
- }
-
- /**
- * Gets the first child element with the given tag
- *
- * @param DOMElement $element element to pick at
- * @param string $tag tag to look for
- * @param string $namespace Namespace to look under
- *
- * @return DOMElement found element or null
- */
-
- static function child(DOMNode $element, $tag, $namespace=self::ATOM)
- {
- $els = $element->childNodes;
- if (empty($els) || $els->length == 0) {
- return null;
- } else {
- for ($i = 0; $i < $els->length; $i++) {
- $el = $els->item($i);
- if ($el->localName == $tag && $el->namespaceURI == $namespace) {
- return $el;
- }
- }
- }
- }
-
- /**
- * Grab the text content of a DOM element child of the current element
- *
- * @param DOMElement $element Element whose children we examine
- * @param string $tag Tag to look up
- * @param string $namespace Namespace to use, defaults to Atom
- *
- * @return string content of the child
- */
-
- static function childContent(DOMNode $element, $tag, $namespace=self::ATOM)
- {
- $el = self::child($element, $tag, $namespace);
-
- if (empty($el)) {
- return null;
- } else {
- return $el->textContent;
- }
- }
-
- /**
- * Get the content of an atom:entry-like object
- *
- * @param DOMElement $element The element to examine.
- *
- * @return string unencoded HTML content of the element, like "This -&lt; is <b>HTML</b>."
- *
- * @todo handle remote content
- * @todo handle embedded XML mime types
- * @todo handle base64-encoded non-XML and non-text mime types
- */
-
- static function getContent($element)
- {
- $contentEl = ActivityUtils::child($element, self::CONTENT);
-
- if (!empty($contentEl)) {
-
- $src = $contentEl->getAttribute(self::SRC);
-
- if (!empty($src)) {
- throw new ClientException(_("Can't handle remote content yet."));
- }
-
- $type = $contentEl->getAttribute(self::TYPE);
-
- // slavishly following http://atompub.org/rfc4287.html#rfc.section.4.1.3.3
-
- if ($type == 'text') {
- return $contentEl->textContent;
- } else if ($type == 'html') {
- $text = $contentEl->textContent;
- return htmlspecialchars_decode($text, ENT_QUOTES);
- } else if ($type == 'xhtml') {
- $divEl = ActivityUtils::child($contentEl, 'div');
- if (empty($divEl)) {
- return null;
- }
- $doc = $divEl->ownerDocument;
- $text = '';
- $children = $divEl->childNodes;
-
- for ($i = 0; $i < $children->length; $i++) {
- $child = $children->item($i);
- $text .= $doc->saveXML($child);
- }
- return trim($text);
- } else if (in_array(array('text/xml', 'application/xml'), $type) ||
- preg_match('#(+|/)xml$#', $type)) {
- throw new ClientException(_("Can't handle embedded XML content yet."));
- } else if (strncasecmp($type, 'text/', 5)) {
- return $contentEl->textContent;
- } else {
- throw new ClientException(_("Can't handle embedded Base64 content yet."));
- }
- }
- }
-}
-
-// XXX: Arg! This wouldn't be necessary if we used Avatars conistently
-class AvatarLink
-{
- public $url;
- public $type;
- public $size;
- public $width;
- public $height;
-
- function __construct($element=null)
- {
- if ($element) {
- // @fixme use correct namespaces
- $this->url = $element->getAttribute('href');
- $this->type = $element->getAttribute('type');
- $width = $element->getAttribute('media:width');
- if ($width != null) {
- $this->width = intval($width);
- }
- $height = $element->getAttribute('media:height');
- if ($height != null) {
- $this->height = intval($height);
- }
- }
- }
-
- static function fromAvatar($avatar)
- {
- if (empty($avatar)) {
- return null;
- }
- $alink = new AvatarLink();
- $alink->type = $avatar->mediatype;
- $alink->height = $avatar->height;
- $alink->width = $avatar->width;
- $alink->url = $avatar->displayUrl();
- return $alink;
- }
-
- static function fromFilename($filename, $size)
- {
- $alink = new AvatarLink();
- $alink->url = $filename;
- $alink->height = $size;
- if (!empty($filename)) {
- $alink->width = $size;
- $alink->type = self::mediatype($filename);
- } else {
- $alink->url = User_group::defaultLogo($size);
- $alink->type = 'image/png';
- }
- return $alink;
- }
-
- // yuck!
- static function mediatype($filename) {
- $ext = strtolower(end(explode('.', $filename)));
- if ($ext == 'jpeg') {
- $ext = 'jpg';
- }
- // hope we don't support any others
- $types = array('png', 'gif', 'jpg', 'jpeg');
- if (in_array($ext, $types)) {
- return 'image/' . $ext;
- }
- return null;
- }
-}
-
-/**
- * A noun-ish thing in the activity universe
- *
- * The activity streams spec talks about activity objects, while also having
- * a tag activity:object, which is in fact an activity object. Aaaaaah!
- *
- * This is just a thing in the activity universe. Can be the subject, object,
- * or indirect object (target!) of an activity verb. Rotten name, and I'm
- * propagating it. *sigh*
- *
- * @category OStatus
- * @package StatusNet
- * @author Evan Prodromou <evan@status.net>
- * @copyright 2010 StatusNet, Inc.
- * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
- * @link http://status.net/
- */
-
-class ActivityObject
-{
- const ARTICLE = 'http://activitystrea.ms/schema/1.0/article';
- const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry';
- const NOTE = 'http://activitystrea.ms/schema/1.0/note';
- const STATUS = 'http://activitystrea.ms/schema/1.0/status';
- const FILE = 'http://activitystrea.ms/schema/1.0/file';
- const PHOTO = 'http://activitystrea.ms/schema/1.0/photo';
- const ALBUM = 'http://activitystrea.ms/schema/1.0/photo-album';
- const PLAYLIST = 'http://activitystrea.ms/schema/1.0/playlist';
- const VIDEO = 'http://activitystrea.ms/schema/1.0/video';
- const AUDIO = 'http://activitystrea.ms/schema/1.0/audio';
- const BOOKMARK = 'http://activitystrea.ms/schema/1.0/bookmark';
- const PERSON = 'http://activitystrea.ms/schema/1.0/person';
- const GROUP = 'http://activitystrea.ms/schema/1.0/group';
- const PLACE = 'http://activitystrea.ms/schema/1.0/place';
- const COMMENT = 'http://activitystrea.ms/schema/1.0/comment';
- // ^^^^^^^^^^ tea!
-
- // Atom elements we snarf
-
- const TITLE = 'title';
- const SUMMARY = 'summary';
- const ID = 'id';
- const SOURCE = 'source';
-
- const NAME = 'name';
- const URI = 'uri';
- const EMAIL = 'email';
-
- public $element;
- public $type;
- public $id;
- public $title;
- public $summary;
- public $content;
- public $link;
- public $source;
- public $avatarLinks = array();
- public $geopoint;
- public $poco;
- public $displayName;
-
- /**
- * Constructor
- *
- * This probably needs to be refactored
- * to generate a local class (ActivityPerson, ActivityFile, ...)
- * based on the object type.
- *
- * @param DOMElement $element DOM thing to turn into an Activity thing
- */
-
- function __construct($element = null)
- {
- if (empty($element)) {
- return;
- }
-
- $this->element = $element;
-
- $this->geopoint = $this->_childContent(
- $element,
- ActivityContext::POINT,
- ActivityContext::GEORSS
- );
-
- if ($element->tagName == 'author') {
-
- $this->type = self::PERSON; // XXX: is this fair?
- $this->title = $this->_childContent($element, self::NAME);
- $this->id = $this->_childContent($element, self::URI);
-
- if (empty($this->id)) {
- $email = $this->_childContent($element, self::EMAIL);
- if (!empty($email)) {
- // XXX: acct: ?
- $this->id = 'mailto:'.$email;
- }
- }
-
- } else {
-
- $this->type = $this->_childContent($element, Activity::OBJECTTYPE,
- Activity::SPEC);
-
- if (empty($this->type)) {
- $this->type = ActivityObject::NOTE;
- }
-
- $this->id = $this->_childContent($element, self::ID);
- $this->title = $this->_childContent($element, self::TITLE);
- $this->summary = $this->_childContent($element, self::SUMMARY);
-
- $this->source = $this->_getSource($element);
-
- $this->content = ActivityUtils::getContent($element);
-
- $this->link = ActivityUtils::getPermalink($element);
-
- }
-
- // Some per-type attributes...
- if ($this->type == self::PERSON || $this->type == self::GROUP) {
- $this->displayName = $this->title;
-
- $avatars = ActivityUtils::getLinks($element, 'avatar');
- foreach ($avatars as $link) {
- $this->avatarLinks[] = new AvatarLink($link);
- }
-
- $this->poco = new PoCo($element);
- }
- }
-
- private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM)
- {
- return ActivityUtils::childContent($element, $tag, $namespace);
- }
-
- // Try to get a unique id for the source feed
-
- private function _getSource($element)
- {
- $sourceEl = ActivityUtils::child($element, 'source');
-
- if (empty($sourceEl)) {
- return null;
- } else {
- $href = ActivityUtils::getLink($sourceEl, 'self');
- if (!empty($href)) {
- return $href;
- } else {
- return ActivityUtils::childContent($sourceEl, 'id');
- }
- }
- }
-
- static function fromNotice($notice)
- {
- $object = new ActivityObject();
-
- $object->type = ActivityObject::NOTE;
-
- $object->id = $notice->uri;
- $object->title = $notice->content;
- $object->content = $notice->rendered;
- $object->link = $notice->bestUrl();
-
- return $object;
- }
-
- static function fromProfile($profile)
- {
- $object = new ActivityObject();
-
- $object->type = ActivityObject::PERSON;
- $object->id = $profile->getUri();
- $object->title = $profile->getBestName();
- $object->link = $profile->profileurl;
-
- $orig = $profile->getOriginalAvatar();
-
- if (!empty($orig)) {
- $object->avatarLinks[] = AvatarLink::fromAvatar($orig);
- }
-
- $sizes = array(
- AVATAR_PROFILE_SIZE,
- AVATAR_STREAM_SIZE,
- AVATAR_MINI_SIZE
- );
-
- foreach ($sizes as $size) {
-
- $alink = null;
- $avatar = $profile->getAvatar($size);
-
- if (!empty($avatar)) {
- $alink = AvatarLink::fromAvatar($avatar);
- } else {
- $alink = new AvatarLink();
- $alink->type = 'image/png';
- $alink->height = $size;
- $alink->width = $size;
- $alink->url = Avatar::defaultImage($size);
- }
-
- $object->avatarLinks[] = $alink;
- }
-
- if (isset($profile->lat) && isset($profile->lon)) {
- $object->geopoint = (float)$profile->lat
- . ' ' . (float)$profile->lon;
- }
-
- $object->poco = PoCo::fromProfile($profile);
-
- return $object;
- }
-
- static function fromGroup($group)
- {
- $object = new ActivityObject();
-
- $object->type = ActivityObject::GROUP;
- $object->id = $group->getUri();
- $object->title = $group->getBestName();
- $object->link = $group->getUri();
-
- $object->avatarLinks[] = AvatarLink::fromFilename(
- $group->homepage_logo,
- AVATAR_PROFILE_SIZE
- );
-
- $object->avatarLinks[] = AvatarLink::fromFilename(
- $group->stream_logo,
- AVATAR_STREAM_SIZE
- );
-
- $object->avatarLinks[] = AvatarLink::fromFilename(
- $group->mini_logo,
- AVATAR_MINI_SIZE
- );
-
- $object->poco = PoCo::fromGroup($group);
-
- return $object;
- }
-
-
- function asString($tag='activity:object')
- {
- $xs = new XMLStringer(true);
-
- $xs->elementStart($tag);
-
- $xs->element('activity:object-type', null, $this->type);
-
- $xs->element(self::ID, null, $this->id);
-
- if (!empty($this->title)) {
- $xs->element(self::TITLE, null, $this->title);
- }
-
- if (!empty($this->summary)) {
- $xs->element(self::SUMMARY, null, $this->summary);
- }
-
- if (!empty($this->content)) {
- // XXX: assuming HTML content here
- $xs->element(ActivityUtils::CONTENT, array('type' => 'html'), $this->content);
- }
-
- if (!empty($this->link)) {
- $xs->element(
- 'link',
- array(
- 'rel' => 'alternate',
- 'type' => 'text/html',
- 'href' => $this->link
- ),
- null
- );
- }
-
- if ($this->type == ActivityObject::PERSON
- || $this->type == ActivityObject::GROUP) {
-
- foreach ($this->avatarLinks as $avatar) {
- $xs->element(
- 'link', array(
- 'rel' => 'avatar',
- 'type' => $avatar->type,
- 'media:width' => $avatar->width,
- 'media:height' => $avatar->height,
- 'href' => $avatar->url
- ),
- null
- );
- }
- }
-
- if (!empty($this->geopoint)) {
- $xs->element(
- 'georss:point',
- null,
- $this->geopoint
- );
- }
-
- if (!empty($this->poco)) {
- $xs->raw($this->poco->asString());
- }
-
- $xs->elementEnd($tag);
-
- return $xs->getString();
- }
-}
-
-/**
- * Utility class to hold a bunch of constant defining default verb types
- *
- * @category OStatus
- * @package StatusNet
- * @author Evan Prodromou <evan@status.net>
- * @copyright 2010 StatusNet, Inc.
- * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
- * @link http://status.net/
- */
-
-class ActivityVerb
-{
- const POST = 'http://activitystrea.ms/schema/1.0/post';
- const SHARE = 'http://activitystrea.ms/schema/1.0/share';
- const SAVE = 'http://activitystrea.ms/schema/1.0/save';
- const FAVORITE = 'http://activitystrea.ms/schema/1.0/favorite';
- const PLAY = 'http://activitystrea.ms/schema/1.0/play';
- const FOLLOW = 'http://activitystrea.ms/schema/1.0/follow';
- const FRIEND = 'http://activitystrea.ms/schema/1.0/make-friend';
- const JOIN = 'http://activitystrea.ms/schema/1.0/join';
- const TAG = 'http://activitystrea.ms/schema/1.0/tag';
-
- // Custom OStatus verbs for the flipside until they're standardized
- const DELETE = 'http://ostatus.org/schema/1.0/unfollow';
- const UNFAVORITE = 'http://ostatus.org/schema/1.0/unfavorite';
- const UNFOLLOW = 'http://ostatus.org/schema/1.0/unfollow';
- const LEAVE = 'http://ostatus.org/schema/1.0/leave';
-
- // For simple profile-update pings; no content to share.
- const UPDATE_PROFILE = 'http://ostatus.org/schema/1.0/update-profile';
-}
-
-class ActivityContext
-{
- public $replyToID;
- public $replyToUrl;
- public $location;
- public $attention = array();
- public $conversation;
-
- const THR = 'http://purl.org/syndication/thread/1.0';
- const GEORSS = 'http://www.georss.org/georss';
- const OSTATUS = 'http://ostatus.org/schema/1.0';
-
- const INREPLYTO = 'in-reply-to';
- const REF = 'ref';
- const HREF = 'href';
-
- const POINT = 'point';
-
- const ATTENTION = 'ostatus:attention';
- const CONVERSATION = 'ostatus:conversation';
-
- function __construct($element)
- {
- $replyToEl = ActivityUtils::child($element, self::INREPLYTO, self::THR);
-
- if (!empty($replyToEl)) {
- $this->replyToID = $replyToEl->getAttribute(self::REF);
- $this->replyToUrl = $replyToEl->getAttribute(self::HREF);
- }
-
- $this->location = $this->getLocation($element);
-
- $this->conversation = ActivityUtils::getLink($element, self::CONVERSATION);
-
- // Multiple attention links allowed
-
- $links = $element->getElementsByTagNameNS(ActivityUtils::ATOM, ActivityUtils::LINK);
-
- for ($i = 0; $i < $links->length; $i++) {
-
- $link = $links->item($i);
-
- $linkRel = $link->getAttribute(ActivityUtils::REL);
-
- if ($linkRel == self::ATTENTION) {
- $this->attention[] = $link->getAttribute(self::HREF);
- }
- }
- }
-
- /**
- * Parse location given as a GeoRSS-simple point, if provided.
- * http://www.georss.org/simple
- *
- * @param feed item $entry
- * @return mixed Location or false
- */
- function getLocation($dom)
- {
- $points = $dom->getElementsByTagNameNS(self::GEORSS, self::POINT);
-
- for ($i = 0; $i < $points->length; $i++) {
- $point = $points->item($i)->textContent;
- return self::locationFromPoint($point);
- }
-
- return null;
- }
-
- // XXX: Move to ActivityUtils or Location?
- static function locationFromPoint($point)
- {
- $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace"
- $point = preg_replace('/\s+/', ' ', $point);
- $point = trim($point);
- $coords = explode(' ', $point);
- if (count($coords) == 2) {
- list($lat, $lon) = $coords;
- if (is_numeric($lat) && is_numeric($lon)) {
- common_log(LOG_INFO, "Looking up location for $lat $lon from georss point");
- return Location::fromLatLon($lat, $lon);
- }
- }
- common_log(LOG_ERR, "Ignoring bogus georss:point value $point");
- return null;
- }
-}
-
/**
* An activity in the ActivityStrea.ms world
*
@@ -1014,6 +53,7 @@ class Activity
{
const SPEC = 'http://activitystrea.ms/spec/1.0/';
const SCHEMA = 'http://activitystrea.ms/schema/1.0/';
+ const MEDIA = 'http://purl.org/syndication/atommedia';
const VERB = 'verb';
const OBJECT = 'object';
@@ -1029,9 +69,25 @@ class Activity
const PUBLISHED = 'published';
const UPDATED = 'updated';
+ const RSS = null; // no namespace!
+
+ const PUBDATE = 'pubDate';
+ const DESCRIPTION = 'description';
+ const GUID = 'guid';
+ const SELF = 'self';
+ const IMAGE = 'image';
+ const URL = 'url';
+
+ const DC = 'http://purl.org/dc/elements/1.1/';
+
+ const CREATOR = 'creator';
+
+ const CONTENTNS = 'http://purl.org/rss/1.0/modules/content/';
+ const ENCODED = 'encoded';
+
public $actor; // an ActivityObject
public $verb; // a string (the URL)
- public $object; // an ActivityObject
+ public $objects = array(); // an array of ActivityObjects
public $target; // an ActivityObject
public $context; // an ActivityObject
public $time; // Time of the activity
@@ -1044,6 +100,7 @@ class Activity
public $id; // ID of the activity
public $title; // title of the activity
public $categories = array(); // list of AtomCategory objects
+ public $enclosures = array(); // list of enclosure URL references
/**
* Turns a regular old Atom <entry> into a magical activity
@@ -1058,9 +115,30 @@ class Activity
return;
}
+ // Insist on a feed's root DOMElement; don't allow a DOMDocument
+ if ($feed instanceof DOMDocument) {
+ throw new ClientException(
+ // TRANS: Client exception thrown when a feed instance is a DOMDocument.
+ _('Expecting a root feed element but got a whole XML document.')
+ );
+ }
+
$this->entry = $entry;
$this->feed = $feed;
+ if ($entry->namespaceURI == Activity::ATOM &&
+ $entry->localName == 'entry') {
+ $this->_fromAtomEntry($entry, $feed);
+ } else if ($entry->namespaceURI == Activity::RSS &&
+ $entry->localName == 'item') {
+ $this->_fromRssItem($entry, $feed);
+ } else {
+ throw new Exception("Unknown DOM element: {$entry->namespaceURI} {$entry->localName}");
+ }
+ }
+
+ function _fromAtomEntry($entry, $feed)
+ {
$pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM);
if (!empty($pubEl)) {
@@ -1086,12 +164,15 @@ class Activity
// XXX: do other implied stuff here
}
- $objectEl = $this->_child($entry, self::OBJECT);
+ $objectEls = $entry->getElementsByTagNameNS(self::SPEC, self::OBJECT);
- if (!empty($objectEl)) {
- $this->object = new ActivityObject($objectEl);
+ if ($objectEls->length > 0) {
+ for ($i = 0; $i < $objectEls->length; $i++) {
+ $objectEl = $objectEls->item($i);
+ $this->objects[] = new ActivityObject($objectEl);
+ }
} else {
- $this->object = new ActivityObject($entry);
+ $this->objects[] = new ActivityObject($entry);
}
$actorEl = $this->_child($entry, self::ACTOR);
@@ -1100,6 +181,17 @@ class Activity
$this->actor = new ActivityObject($actorEl);
+ // Cliqset has bad actor IDs (just nickname of user). We
+ // work around it by getting the author data and using its
+ // id instead
+
+ if (!preg_match('/^\w+:/', $this->actor->id)) {
+ $authorEl = ActivityUtils::child($entry, 'author');
+ if (!empty($authorEl)) {
+ $authorObj = new ActivityObject($authorEl);
+ $this->actor->id = $authorObj->id;
+ }
+ }
} else if (!empty($feed) &&
$subjectEl = $this->_child($feed, self::SUBJECT)) {
@@ -1140,6 +232,80 @@ class Activity
$this->categories[] = new AtomCategory($catEl);
}
}
+
+ foreach (ActivityUtils::getLinks($entry, 'enclosure') as $link) {
+ $this->enclosures[] = $link->getAttribute('href');
+ }
+ }
+
+ function _fromRssItem($item, $channel)
+ {
+ $verbEl = $this->_child($item, self::VERB);
+
+ if (!empty($verbEl)) {
+ $this->verb = trim($verbEl->textContent);
+ } else {
+ $this->verb = ActivityVerb::POST;
+ // XXX: do other implied stuff here
+ }
+
+ $pubDateEl = $this->_child($item, self::PUBDATE, self::RSS);
+
+ if (!empty($pubDateEl)) {
+ $this->time = strtotime($pubDateEl->textContent);
+ }
+
+ if ($authorEl = $this->_child($item, self::AUTHOR, self::RSS)) {
+ $this->actor = ActivityObject::fromRssAuthor($authorEl);
+ } else if ($dcCreatorEl = $this->_child($item, self::CREATOR, self::DC)) {
+ $this->actor = ActivityObject::fromDcCreator($dcCreatorEl);
+ } else if ($posterousEl = $this->_child($item, ActivityObject::AUTHOR, ActivityObject::POSTEROUS)) {
+ // Special case for Posterous.com
+ $this->actor = ActivityObject::fromPosterousAuthor($posterousEl);
+ } else if (!empty($channel)) {
+ $this->actor = ActivityObject::fromRssChannel($channel);
+ } else {
+ // No actor!
+ }
+
+ $this->title = ActivityUtils::childContent($item, ActivityObject::TITLE, self::RSS);
+
+ $contentEl = ActivityUtils::child($item, self::ENCODED, self::CONTENTNS);
+
+ if (!empty($contentEl)) {
+ // <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)) {
+ // 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);
+ }
+ }
+
+ $this->link = ActivityUtils::childContent($item, ActivityUtils::LINK, self::RSS);
+
+ // @fixme enclosures
+ // @fixme thumbnails... maybe
+
+ $guidEl = ActivityUtils::child($item, self::GUID, self::RSS);
+
+ if (!empty($guidEl)) {
+ $this->id = $guidEl->textContent;
+
+ if ($guidEl->hasAttribute('isPermaLink') && $guidEl->getAttribute('isPermaLink') != 'false') {
+ // overwrites <link>
+ $this->link = $this->id;
+ }
+ }
+
+ $this->objects[] = new ActivityObject($item);
+ $this->context = new ActivityContext($item);
}
/**
@@ -1197,8 +363,10 @@ class Activity
$xs->element('activity:verb', null, $this->verb);
- if ($this->object) {
- $xs->raw($this->object->asString());
+ if (!empty($this->objects)) {
+ foreach($this->objects as $object) {
+ $xs->raw($object->asString());
+ }
}
if ($this->target) {
@@ -1220,48 +388,3 @@ class Activity
}
}
-class AtomCategory
-{
- public $term;
- public $scheme;
- public $label;
-
- function __construct($element=null)
- {
- if ($element && $element->attributes) {
- $this->term = $this->extract($element, 'term');
- $this->scheme = $this->extract($element, 'scheme');
- $this->label = $this->extract($element, 'label');
- }
- }
-
- protected function extract($element, $attrib)
- {
- $node = $element->attributes->getNamedItemNS(Activity::ATOM, $attrib);
- if ($node) {
- return trim($node->textContent);
- }
- $node = $element->attributes->getNamedItem($attrib);
- if ($node) {
- return trim($node->textContent);
- }
- return null;
- }
-
- function asString()
- {
- $attribs = array();
- if ($this->term !== null) {
- $attribs['term'] = $this->term;
- }
- if ($this->scheme !== null) {
- $attribs['scheme'] = $this->scheme;
- }
- if ($this->label !== null) {
- $attribs['label'] = $this->label;
- }
- $xs = new XMLStringer();
- $xs->element('category', $attribs);
- return $xs->asString();
- }
-}
diff --git a/lib/activitycontext.php b/lib/activitycontext.php
new file mode 100644
index 000000000..2df7613f7
--- /dev/null
+++ b/lib/activitycontext.php
@@ -0,0 +1,121 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+class ActivityContext
+{
+ public $replyToID;
+ public $replyToUrl;
+ public $location;
+ public $attention = array();
+ public $conversation;
+
+ const THR = 'http://purl.org/syndication/thread/1.0';
+ const GEORSS = 'http://www.georss.org/georss';
+ const OSTATUS = 'http://ostatus.org/schema/1.0';
+
+ const INREPLYTO = 'in-reply-to';
+ const REF = 'ref';
+ const HREF = 'href';
+
+ const POINT = 'point';
+
+ const ATTENTION = 'ostatus:attention';
+ const CONVERSATION = 'ostatus:conversation';
+
+ function __construct($element)
+ {
+ $replyToEl = ActivityUtils::child($element, self::INREPLYTO, self::THR);
+
+ if (!empty($replyToEl)) {
+ $this->replyToID = $replyToEl->getAttribute(self::REF);
+ $this->replyToUrl = $replyToEl->getAttribute(self::HREF);
+ }
+
+ $this->location = $this->getLocation($element);
+
+ $this->conversation = ActivityUtils::getLink($element, self::CONVERSATION);
+
+ // Multiple attention links allowed
+
+ $links = $element->getElementsByTagNameNS(ActivityUtils::ATOM, ActivityUtils::LINK);
+
+ for ($i = 0; $i < $links->length; $i++) {
+
+ $link = $links->item($i);
+
+ $linkRel = $link->getAttribute(ActivityUtils::REL);
+
+ if ($linkRel == self::ATTENTION) {
+ $this->attention[] = $link->getAttribute(self::HREF);
+ }
+ }
+ }
+
+ /**
+ * Parse location given as a GeoRSS-simple point, if provided.
+ * http://www.georss.org/simple
+ *
+ * @param feed item $entry
+ * @return mixed Location or false
+ */
+ function getLocation($dom)
+ {
+ $points = $dom->getElementsByTagNameNS(self::GEORSS, self::POINT);
+
+ for ($i = 0; $i < $points->length; $i++) {
+ $point = $points->item($i)->textContent;
+ return self::locationFromPoint($point);
+ }
+
+ return null;
+ }
+
+ // XXX: Move to ActivityUtils or Location?
+ static function locationFromPoint($point)
+ {
+ $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace"
+ $point = preg_replace('/\s+/', ' ', $point);
+ $point = trim($point);
+ $coords = explode(' ', $point);
+ if (count($coords) == 2) {
+ list($lat, $lon) = $coords;
+ if (is_numeric($lat) && is_numeric($lon)) {
+ common_log(LOG_INFO, "Looking up location for $lat $lon from georss point");
+ return Location::fromLatLon($lat, $lon);
+ }
+ }
+ common_log(LOG_ERR, "Ignoring bogus georss:point value $point");
+ return null;
+ }
+}
diff --git a/lib/activityobject.php b/lib/activityobject.php
new file mode 100644
index 000000000..677a48197
--- /dev/null
+++ b/lib/activityobject.php
@@ -0,0 +1,557 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * A noun-ish thing in the activity universe
+ *
+ * The activity streams spec talks about activity objects, while also having
+ * a tag activity:object, which is in fact an activity object. Aaaaaah!
+ *
+ * This is just a thing in the activity universe. Can be the subject, object,
+ * or indirect object (target!) of an activity verb. Rotten name, and I'm
+ * propagating it. *sigh*
+ *
+ * @category OStatus
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+class ActivityObject
+{
+ const ARTICLE = 'http://activitystrea.ms/schema/1.0/article';
+ const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry';
+ const NOTE = 'http://activitystrea.ms/schema/1.0/note';
+ const STATUS = 'http://activitystrea.ms/schema/1.0/status';
+ const FILE = 'http://activitystrea.ms/schema/1.0/file';
+ const PHOTO = 'http://activitystrea.ms/schema/1.0/photo';
+ const ALBUM = 'http://activitystrea.ms/schema/1.0/photo-album';
+ const PLAYLIST = 'http://activitystrea.ms/schema/1.0/playlist';
+ const VIDEO = 'http://activitystrea.ms/schema/1.0/video';
+ const AUDIO = 'http://activitystrea.ms/schema/1.0/audio';
+ const BOOKMARK = 'http://activitystrea.ms/schema/1.0/bookmark';
+ const PERSON = 'http://activitystrea.ms/schema/1.0/person';
+ const GROUP = 'http://activitystrea.ms/schema/1.0/group';
+ const PLACE = 'http://activitystrea.ms/schema/1.0/place';
+ const COMMENT = 'http://activitystrea.ms/schema/1.0/comment';
+ // ^^^^^^^^^^ tea!
+
+ // Atom elements we snarf
+
+ const TITLE = 'title';
+ const SUMMARY = 'summary';
+ const ID = 'id';
+ const SOURCE = 'source';
+
+ const NAME = 'name';
+ const URI = 'uri';
+ const EMAIL = 'email';
+
+ const POSTEROUS = 'http://posterous.com/help/rss/1.0';
+ const AUTHOR = 'author';
+ const USERIMAGE = 'userImage';
+ const PROFILEURL = 'profileUrl';
+ const NICKNAME = 'nickName';
+ const DISPLAYNAME = 'displayName';
+
+ public $element;
+ public $type;
+ public $id;
+ public $title;
+ public $summary;
+ public $content;
+ public $link;
+ public $source;
+ public $avatarLinks = array();
+ public $geopoint;
+ public $poco;
+ public $displayName;
+
+ // @todo move this stuff to it's own PHOTO activity object
+ const MEDIA_DESCRIPTION = 'description';
+
+ public $thumbnail;
+ public $largerImage;
+ public $description;
+
+ /**
+ * Constructor
+ *
+ * This probably needs to be refactored
+ * to generate a local class (ActivityPerson, ActivityFile, ...)
+ * based on the object type.
+ *
+ * @param DOMElement $element DOM thing to turn into an Activity thing
+ */
+
+ function __construct($element = null)
+ {
+ if (empty($element)) {
+ return;
+ }
+
+ $this->element = $element;
+
+ $this->geopoint = $this->_childContent(
+ $element,
+ ActivityContext::POINT,
+ ActivityContext::GEORSS
+ );
+
+ if ($element->tagName == 'author') {
+ $this->_fromAuthor($element);
+ } else if ($element->tagName == 'item') {
+ $this->_fromRssItem($element);
+ } else {
+ $this->_fromAtomEntry($element);
+ }
+
+ // Some per-type attributes...
+ if ($this->type == self::PERSON || $this->type == self::GROUP) {
+ $this->displayName = $this->title;
+
+ $photos = ActivityUtils::getLinks($element, 'photo');
+ if (count($photos)) {
+ foreach ($photos as $link) {
+ $this->avatarLinks[] = new AvatarLink($link);
+ }
+ } else {
+ $avatars = ActivityUtils::getLinks($element, 'avatar');
+ foreach ($avatars as $link) {
+ $this->avatarLinks[] = new AvatarLink($link);
+ }
+ }
+
+ $this->poco = new PoCo($element);
+ }
+
+ if ($this->type == self::PHOTO) {
+
+ $this->thumbnail = ActivityUtils::getLink($element, 'preview');
+ $this->largerImage = ActivityUtils::getLink($element, 'enclosure');
+
+ $this->description = ActivityUtils::childContent(
+ $element,
+ ActivityObject::MEDIA_DESCRIPTION,
+ Activity::MEDIA
+ );
+
+ }
+ }
+
+ private function _fromAuthor($element)
+ {
+ $this->type = self::PERSON; // XXX: is this fair?
+ $this->title = $this->_childContent($element, self::NAME);
+
+ $this->id = $this->_childContent($element, self::URI);
+
+ if (empty($this->id)) {
+ $email = $this->_childContent($element, self::EMAIL);
+ if (!empty($email)) {
+ // XXX: acct: ?
+ $this->id = 'mailto:'.$email;
+ }
+ }
+ }
+
+ private function _fromAtomEntry($element)
+ {
+ $this->type = $this->_childContent($element, Activity::OBJECTTYPE,
+ Activity::SPEC);
+
+ if (empty($this->type)) {
+ $this->type = ActivityObject::NOTE;
+ }
+
+ $this->summary = ActivityUtils::childHtmlContent($element, self::SUMMARY);
+ $this->content = ActivityUtils::getContent($element);
+
+ // We don't like HTML in our titles, although it's technically allowed
+
+ $title = ActivityUtils::childHtmlContent($element, self::TITLE);
+
+ $this->title = html_entity_decode(strip_tags($title));
+
+ $this->source = $this->_getSource($element);
+
+ $this->link = ActivityUtils::getPermalink($element);
+
+ $this->id = $this->_childContent($element, self::ID);
+
+ if (empty($this->id) && !empty($this->link)) { // fallback if there's no ID
+ $this->id = $this->link;
+ }
+ }
+
+ // @fixme rationalize with Activity::_fromRssItem()
+
+ private function _fromRssItem($item)
+ {
+ $this->title = ActivityUtils::childContent($item, ActivityObject::TITLE, Activity::RSS);
+
+ $contentEl = ActivityUtils::child($item, ActivityUtils::CONTENT, Activity::CONTENTNS);
+
+ if (!empty($contentEl)) {
+ $this->content = htmlspecialchars_decode($contentEl->textContent, ENT_QUOTES);
+ } else {
+ $descriptionEl = ActivityUtils::child($item, Activity::DESCRIPTION, Activity::RSS);
+ if (!empty($descriptionEl)) {
+ $this->content = htmlspecialchars_decode($descriptionEl->textContent, ENT_QUOTES);
+ }
+ }
+
+ $this->link = ActivityUtils::childContent($item, ActivityUtils::LINK, Activity::RSS);
+
+ $guidEl = ActivityUtils::child($item, Activity::GUID, Activity::RSS);
+
+ if (!empty($guidEl)) {
+ $this->id = $guidEl->textContent;
+
+ if ($guidEl->hasAttribute('isPermaLink')) {
+ // overwrites <link>
+ $this->link = $this->id;
+ }
+ }
+ }
+
+ public static function fromRssAuthor($el)
+ {
+ $text = $el->textContent;
+
+ if (preg_match('/^(.*?) \((.*)\)$/', $text, $match)) {
+ $email = $match[1];
+ $name = $match[2];
+ } else if (preg_match('/^(.*?) <(.*)>$/', $text, $match)) {
+ $name = $match[1];
+ $email = $match[2];
+ } else if (preg_match('/.*@.*/', $text)) {
+ $email = $text;
+ $name = null;
+ } else {
+ $name = $text;
+ $email = null;
+ }
+
+ // Not really enough info
+
+ $obj = new ActivityObject();
+
+ $obj->element = $el;
+
+ $obj->type = ActivityObject::PERSON;
+ $obj->title = $name;
+
+ if (!empty($email)) {
+ $obj->id = 'mailto:'.$email;
+ }
+
+ return $obj;
+ }
+
+ public static function fromDcCreator($el)
+ {
+ // Not really enough info
+
+ $text = $el->textContent;
+
+ $obj = new ActivityObject();
+
+ $obj->element = $el;
+
+ $obj->title = $text;
+ $obj->type = ActivityObject::PERSON;
+
+ return $obj;
+ }
+
+ public static function fromRssChannel($el)
+ {
+ $obj = new ActivityObject();
+
+ $obj->element = $el;
+
+ $obj->type = ActivityObject::PERSON; // @fixme guess better
+
+ $obj->title = ActivityUtils::childContent($el, ActivityObject::TITLE, Activity::RSS);
+ $obj->link = ActivityUtils::childContent($el, ActivityUtils::LINK, Activity::RSS);
+ $obj->id = ActivityUtils::getLink($el, Activity::SELF);
+
+ if (empty($obj->id)) {
+ $obj->id = $obj->link;
+ }
+
+ $desc = ActivityUtils::childContent($el, Activity::DESCRIPTION, Activity::RSS);
+
+ if (!empty($desc)) {
+ $obj->content = htmlspecialchars_decode($desc, ENT_QUOTES);
+ }
+
+ $imageEl = ActivityUtils::child($el, Activity::IMAGE, Activity::RSS);
+
+ if (!empty($imageEl)) {
+ $url = ActivityUtils::childContent($imageEl, Activity::URL, Activity::RSS);
+ $al = new AvatarLink();
+ $al->url = $url;
+ $obj->avatarLinks[] = $al;
+ }
+
+ return $obj;
+ }
+
+ public static function fromPosterousAuthor($el)
+ {
+ $obj = new ActivityObject();
+
+ $obj->type = ActivityObject::PERSON; // @fixme any others...?
+
+ $userImage = ActivityUtils::childContent($el, self::USERIMAGE, self::POSTEROUS);
+
+ if (!empty($userImage)) {
+ $al = new AvatarLink();
+ $al->url = $userImage;
+ $obj->avatarLinks[] = $al;
+ }
+
+ $obj->link = ActivityUtils::childContent($el, self::PROFILEURL, self::POSTEROUS);
+ $obj->id = $obj->link;
+
+ $obj->poco = new PoCo();
+
+ $obj->poco->preferredUsername = ActivityUtils::childContent($el, self::NICKNAME, self::POSTEROUS);
+ $obj->poco->displayName = ActivityUtils::childContent($el, self::DISPLAYNAME, self::POSTEROUS);
+
+ $obj->title = $obj->poco->displayName;
+
+ return $obj;
+ }
+
+ private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM)
+ {
+ return ActivityUtils::childContent($element, $tag, $namespace);
+ }
+
+ // Try to get a unique id for the source feed
+
+ private function _getSource($element)
+ {
+ $sourceEl = ActivityUtils::child($element, 'source');
+
+ if (empty($sourceEl)) {
+ return null;
+ } else {
+ $href = ActivityUtils::getLink($sourceEl, 'self');
+ if (!empty($href)) {
+ return $href;
+ } else {
+ return ActivityUtils::childContent($sourceEl, 'id');
+ }
+ }
+ }
+
+ static function fromNotice(Notice $notice)
+ {
+ $object = new ActivityObject();
+
+ $object->type = ActivityObject::NOTE;
+
+ $object->id = $notice->uri;
+ $object->title = $notice->content;
+ $object->content = $notice->rendered;
+ $object->link = $notice->bestUrl();
+
+ return $object;
+ }
+
+ static function fromProfile(Profile $profile)
+ {
+ $object = new ActivityObject();
+
+ $object->type = ActivityObject::PERSON;
+ $object->id = $profile->getUri();
+ $object->title = $profile->getBestName();
+ $object->link = $profile->profileurl;
+
+ $orig = $profile->getOriginalAvatar();
+
+ if (!empty($orig)) {
+ $object->avatarLinks[] = AvatarLink::fromAvatar($orig);
+ }
+
+ $sizes = array(
+ AVATAR_PROFILE_SIZE,
+ AVATAR_STREAM_SIZE,
+ AVATAR_MINI_SIZE
+ );
+
+ foreach ($sizes as $size) {
+
+ $alink = null;
+ $avatar = $profile->getAvatar($size);
+
+ if (!empty($avatar)) {
+ $alink = AvatarLink::fromAvatar($avatar);
+ } else {
+ $alink = new AvatarLink();
+ $alink->type = 'image/png';
+ $alink->height = $size;
+ $alink->width = $size;
+ $alink->url = Avatar::defaultImage($size);
+ }
+
+ $object->avatarLinks[] = $alink;
+ }
+
+ if (isset($profile->lat) && isset($profile->lon)) {
+ $object->geopoint = (float)$profile->lat
+ . ' ' . (float)$profile->lon;
+ }
+
+ $object->poco = PoCo::fromProfile($profile);
+
+ return $object;
+ }
+
+ static function fromGroup($group)
+ {
+ $object = new ActivityObject();
+
+ $object->type = ActivityObject::GROUP;
+ $object->id = $group->getUri();
+ $object->title = $group->getBestName();
+ $object->link = $group->getUri();
+
+ $object->avatarLinks[] = AvatarLink::fromFilename(
+ $group->homepage_logo,
+ AVATAR_PROFILE_SIZE
+ );
+
+ $object->avatarLinks[] = AvatarLink::fromFilename(
+ $group->stream_logo,
+ AVATAR_STREAM_SIZE
+ );
+
+ $object->avatarLinks[] = AvatarLink::fromFilename(
+ $group->mini_logo,
+ AVATAR_MINI_SIZE
+ );
+
+ $object->poco = PoCo::fromGroup($group);
+
+ return $object;
+ }
+
+ function asString($tag='activity:object')
+ {
+ $xs = new XMLStringer(true);
+
+ $xs->elementStart($tag);
+
+ $xs->element('activity:object-type', null, $this->type);
+
+ $xs->element(self::ID, null, $this->id);
+
+ if (!empty($this->title)) {
+ $xs->element(
+ self::TITLE,
+ null,
+ common_xml_safe_str($this->title)
+ );
+ }
+
+ if (!empty($this->summary)) {
+ $xs->element(
+ self::SUMMARY,
+ null,
+ common_xml_safe_str($this->summary)
+ );
+ }
+
+ if (!empty($this->content)) {
+ // XXX: assuming HTML content here
+ $xs->element(
+ ActivityUtils::CONTENT,
+ array('type' => 'html'),
+ common_xml_safe_str($this->content)
+ );
+ }
+
+ if (!empty($this->link)) {
+ $xs->element(
+ 'link',
+ array(
+ 'rel' => 'alternate',
+ 'type' => 'text/html',
+ 'href' => $this->link
+ ),
+ null
+ );
+ }
+
+ if ($this->type == ActivityObject::PERSON
+ || $this->type == ActivityObject::GROUP) {
+
+ foreach ($this->avatarLinks as $avatar) {
+ $xs->element(
+ 'link', array(
+ 'rel' => 'avatar',
+ 'type' => $avatar->type,
+ 'media:width' => $avatar->width,
+ 'media:height' => $avatar->height,
+ 'href' => $avatar->url
+ ),
+ null
+ );
+ }
+ }
+
+ if (!empty($this->geopoint)) {
+ $xs->element(
+ 'georss:point',
+ null,
+ $this->geopoint
+ );
+ }
+
+ if (!empty($this->poco)) {
+ $xs->raw($this->poco->asString());
+ }
+
+ $xs->elementEnd($tag);
+
+ return $xs->getString();
+ }
+}
diff --git a/lib/activityutils.php b/lib/activityutils.php
new file mode 100644
index 000000000..401fd7fc2
--- /dev/null
+++ b/lib/activityutils.php
@@ -0,0 +1,273 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Utilities for turning DOMish things into Activityish things
+ *
+ * Some common functions that I didn't have the bandwidth to try to factor
+ * into some kind of reasonable superclass, so just dumped here. Might
+ * be useful to have an ActivityObject parent class or something.
+ *
+ * @category OStatus
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+class ActivityUtils
+{
+ const ATOM = 'http://www.w3.org/2005/Atom';
+
+ const LINK = 'link';
+ const REL = 'rel';
+ const TYPE = 'type';
+ const HREF = 'href';
+
+ const CONTENT = 'content';
+ const SRC = 'src';
+
+ /**
+ * Get the permalink for an Activity object
+ *
+ * @param DOMElement $element A DOM element
+ *
+ * @return string related link, if any
+ */
+
+ static function getPermalink($element)
+ {
+ return self::getLink($element, 'alternate', 'text/html');
+ }
+
+ /**
+ * Get the permalink for an Activity object
+ *
+ * @param DOMElement $element A DOM element
+ *
+ * @return string related link, if any
+ */
+
+ static function getLink(DOMNode $element, $rel, $type=null)
+ {
+ $els = $element->childNodes;
+
+ foreach ($els as $link) {
+
+ if (!($link instanceof DOMElement)) {
+ continue;
+ }
+
+ if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) {
+
+ $linkRel = $link->getAttribute(self::REL);
+ $linkType = $link->getAttribute(self::TYPE);
+
+ if ($linkRel == $rel &&
+ (is_null($type) || $linkType == $type)) {
+ return $link->getAttribute(self::HREF);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ static function getLinks(DOMNode $element, $rel, $type=null)
+ {
+ $els = $element->childNodes;
+ $out = array();
+
+ foreach ($els as $link) {
+ if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) {
+
+ $linkRel = $link->getAttribute(self::REL);
+ $linkType = $link->getAttribute(self::TYPE);
+
+ if ($linkRel == $rel &&
+ (is_null($type) || $linkType == $type)) {
+ $out[] = $link;
+ }
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * Gets the first child element with the given tag
+ *
+ * @param DOMElement $element element to pick at
+ * @param string $tag tag to look for
+ * @param string $namespace Namespace to look under
+ *
+ * @return DOMElement found element or null
+ */
+
+ static function child(DOMNode $element, $tag, $namespace=self::ATOM)
+ {
+ $els = $element->childNodes;
+ if (empty($els) || $els->length == 0) {
+ return null;
+ } else {
+ for ($i = 0; $i < $els->length; $i++) {
+ $el = $els->item($i);
+ if ($el->localName == $tag && $el->namespaceURI == $namespace) {
+ return $el;
+ }
+ }
+ }
+ }
+
+ /**
+ * Grab the text content of a DOM element child of the current element
+ *
+ * @param DOMElement $element Element whose children we examine
+ * @param string $tag Tag to look up
+ * @param string $namespace Namespace to use, defaults to Atom
+ *
+ * @return string content of the child
+ */
+
+ static function childContent(DOMNode $element, $tag, $namespace=self::ATOM)
+ {
+ $el = self::child($element, $tag, $namespace);
+
+ if (empty($el)) {
+ return null;
+ } else {
+ return $el->textContent;
+ }
+ }
+
+ static function childHtmlContent(DOMNode $element, $tag, $namespace=self::ATOM)
+ {
+ $el = self::child($element, $tag, $namespace);
+
+ if (empty($el)) {
+ return null;
+ } else {
+ return self::textConstruct($el);
+ }
+ }
+
+ /**
+ * Get the content of an atom:entry-like object
+ *
+ * @param DOMElement $element The element to examine.
+ *
+ * @return string unencoded HTML content of the element, like "This -&lt; is <b>HTML</b>."
+ *
+ * @todo handle remote content
+ * @todo handle embedded XML mime types
+ * @todo handle base64-encoded non-XML and non-text mime types
+ */
+
+ static function getContent($element)
+ {
+ return self::childHtmlContent($element, self::CONTENT, self::ATOM);
+ }
+
+ static function textConstruct($el)
+ {
+ $src = $el->getAttribute(self::SRC);
+
+ if (!empty($src)) {
+ throw new ClientException(_("Can't handle remote content yet."));
+ }
+
+ $type = $el->getAttribute(self::TYPE);
+
+ // slavishly following http://atompub.org/rfc4287.html#rfc.section.4.1.3.3
+
+ if (empty($type) || $type == 'text') {
+ // 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 $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;
+ }
+ $doc = $divEl->ownerDocument;
+ $text = '';
+ $children = $divEl->childNodes;
+
+ for ($i = 0; $i < $children->length; $i++) {
+ $child = $children->item($i);
+ $text .= $doc->saveXML($child);
+ }
+ return trim($text);
+ } else if (in_array($type, array('text/xml', 'application/xml')) ||
+ preg_match('#(+|/)xml$#', $type)) {
+ throw new ClientException(_("Can't handle embedded XML content yet."));
+ } else if (strncasecmp($type, 'text/', 5)) {
+ return $el->textContent;
+ } else {
+ throw new ClientException(_("Can't handle embedded Base64 content yet."));
+ }
+ }
+
+ /**
+ * Is this a valid URI for remote profile/notice identification?
+ * Does not have to be a resolvable URL.
+ * @param string $uri
+ * @return boolean
+ */
+ static function validateUri($uri)
+ {
+ if (Validate::uri($uri)) {
+ return true;
+ }
+
+ // Possibly an upstream bug; tag: URIs aren't validated properly
+ // unless you explicitly ask for them. All other schemes are accepted
+ // for basic URI validation without asking.
+ if (Validate::uri($uri, array('allowed_scheme' => array('tag')))) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/lib/activityverb.php b/lib/activityverb.php
new file mode 100644
index 000000000..76f2b84e9
--- /dev/null
+++ b/lib/activityverb.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Utility class to hold a bunch of constant defining default verb types
+ *
+ * @category OStatus
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+class ActivityVerb
+{
+ const POST = 'http://activitystrea.ms/schema/1.0/post';
+ const SHARE = 'http://activitystrea.ms/schema/1.0/share';
+ const SAVE = 'http://activitystrea.ms/schema/1.0/save';
+ const FAVORITE = 'http://activitystrea.ms/schema/1.0/favorite';
+ const PLAY = 'http://activitystrea.ms/schema/1.0/play';
+ const FOLLOW = 'http://activitystrea.ms/schema/1.0/follow';
+ const FRIEND = 'http://activitystrea.ms/schema/1.0/make-friend';
+ const JOIN = 'http://activitystrea.ms/schema/1.0/join';
+ const TAG = 'http://activitystrea.ms/schema/1.0/tag';
+
+ // Custom OStatus verbs for the flipside until they're standardized
+ const DELETE = 'http://ostatus.org/schema/1.0/unfollow';
+ const UNFAVORITE = 'http://ostatus.org/schema/1.0/unfavorite';
+ const UNFOLLOW = 'http://ostatus.org/schema/1.0/unfollow';
+ const LEAVE = 'http://ostatus.org/schema/1.0/leave';
+
+ // For simple profile-update pings; no content to share.
+ const UPDATE_PROFILE = 'http://ostatus.org/schema/1.0/update-profile';
+}
diff --git a/lib/adminpanelaction.php b/lib/adminpanelaction.php
index a0cdab8a4..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;
}
@@ -176,6 +176,24 @@ class AdminPanelAction extends Action
}
/**
+ * Show content block. Overrided just to add a special class
+ * to the content div to allow styling.
+ *
+ * @return nothing
+ */
+ function showContentBlock()
+ {
+ $this->elementStart('div', array('id' => 'content', 'class' => 'admin'));
+ $this->showPageTitle();
+ $this->showPageNoticeBlock();
+ $this->elementStart('div', array('id' => 'content_inner'));
+ // show the actual content (forms, lists, whatever)
+ $this->showContent();
+ $this->elementEnd('div');
+ $this->elementEnd('div');
+ }
+
+ /**
* show human-readable instructions for the page, or
* a success/failure on save.
*
@@ -207,7 +225,7 @@ class AdminPanelAction extends Action
function showForm()
{
- // TRANS: Client error message
+ // TRANS: Client error message.
$this->clientError(_('showForm() not implemented.'));
return;
}
@@ -261,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)
@@ -345,32 +365,48 @@ class AdminPanelNav extends Widget
// TRANS: Menu item title/tooltip
$menu_title = _('User configuration');
// TRANS: Menu item for site administration
- $this->out->menuItem(common_local_url('useradminpanel'), _m('MENU', 'User'),
- $menu_title, $action_name == 'useradminpanel', 'nav_design_admin_panel');
+ $this->out->menuItem(common_local_url('useradminpanel'), _('User'),
+ $menu_title, $action_name == 'useradminpanel', 'nav_user_admin_panel');
}
if (AdminPanelAction::canAdmin('access')) {
// TRANS: Menu item title/tooltip
$menu_title = _('Access configuration');
// TRANS: Menu item for site administration
- $this->out->menuItem(common_local_url('accessadminpanel'), _m('MENU', 'Access'),
- $menu_title, $action_name == 'accessadminpanel', 'nav_design_admin_panel');
+ $this->out->menuItem(common_local_url('accessadminpanel'), _('Access'),
+ $menu_title, $action_name == 'accessadminpanel', 'nav_access_admin_panel');
}
if (AdminPanelAction::canAdmin('paths')) {
// TRANS: Menu item title/tooltip
$menu_title = _('Paths configuration');
// TRANS: Menu item for site administration
- $this->out->menuItem(common_local_url('pathsadminpanel'), _m('MENU', 'Paths'),
- $menu_title, $action_name == 'pathsadminpanel', 'nav_design_admin_panel');
+ $this->out->menuItem(common_local_url('pathsadminpanel'), _('Paths'),
+ $menu_title, $action_name == 'pathsadminpanel', 'nav_paths_admin_panel');
}
if (AdminPanelAction::canAdmin('sessions')) {
// TRANS: Menu item title/tooltip
$menu_title = _('Sessions configuration');
// TRANS: Menu item for site administration
- $this->out->menuItem(common_local_url('sessionsadminpanel'), _m('MENU', 'Sessions'),
- $menu_title, $action_name == 'sessionsadminpanel', 'nav_design_admin_panel');
+ $this->out->menuItem(common_local_url('sessionsadminpanel'), _('Sessions'),
+ $menu_title, $action_name == 'sessionsadminpanel', 'nav_sessions_admin_panel');
+ }
+
+ if (AdminPanelAction::canAdmin('sitenotice')) {
+ // TRANS: Menu item title/tooltip
+ $menu_title = _('Edit site notice');
+ // TRANS: Menu item for site administration
+ $this->out->menuItem(common_local_url('sitenoticeadminpanel'), _('Site notice'),
+ $menu_title, $action_name == 'sitenoticeadminpanel', 'nav_sitenotice_admin_panel');
+ }
+
+ if (AdminPanelAction::canAdmin('snapshot')) {
+ // TRANS: Menu item title/tooltip
+ $menu_title = _('Snapshots configuration');
+ // TRANS: Menu item for site administration
+ $this->out->menuItem(common_local_url('snapshotadminpanel'), _('Snapshots'),
+ $menu_title, $action_name == 'snapshotadminpanel', 'nav_snapshot_admin_panel');
}
Event::handle('EndAdminPanelNav', array($this));
diff --git a/lib/apiaction.php b/lib/apiaction.php
index eef0ba637..8de13a62d 100644
--- a/lib/apiaction.php
+++ b/lib/apiaction.php
@@ -28,10 +28,72 @@
* @author Toby Inkster <mail@tobyinkster.co.uk>
* @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/
*/
+/* 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,12 @@ class ApiAction extends Action
var $count = null;
var $max_id = null;
var $since_id = null;
+ var $source = null;
var $access = self::READ_ONLY; // read (default) or read-write
+ static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
+
/**
* Initialization.
*
@@ -86,7 +151,13 @@ class ApiAction extends Action
$this->since_id = (int)$this->arg('since_id', 0);
if ($this->arg('since')) {
- $this->clientError(_("since parameter is disabled for performance; use since_id"), 403);
+ 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 +173,7 @@ class ApiAction extends Action
function handle($args)
{
+ header('Access-Control-Allow-Origin: *');
parent::handle($args);
}
@@ -199,11 +271,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' =>
@@ -223,6 +297,10 @@ class ApiAction extends Action
}
}
+ // StatusNet-specific
+
+ $twitter_user['statusnet:profile_url'] = $profile->profileurl;
+
return $twitter_user;
}
@@ -251,7 +329,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;
@@ -308,26 +402,41 @@ class ApiAction extends Action
$twitter_status['user'] = $twitter_user;
}
+ // StatusNet-specific
+
+ $twitter_status['statusnet:html'] = $notice->rendered;
+
return $twitter_status;
}
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['homepage_url']=$group->homepage_url;
- $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;
}
@@ -476,9 +585,13 @@ class ApiAction extends Action
}
}
- function showTwitterXmlStatus($twitter_status, $tag='status')
+ function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
{
- $this->elementStart($tag);
+ $attrs = array();
+ if ($namespaces) {
+ $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
+ }
+ $this->elementStart($tag, $attrs);
foreach($twitter_status as $element => $value) {
switch ($element) {
case 'user':
@@ -491,7 +604,7 @@ class ApiAction extends Action
$this->showXmlAttachments($twitter_status['attachments']);
break;
case 'geo':
- $this->showGeoRSS($value);
+ $this->showGeoXML($value);
break;
case 'retweeted_status':
$this->showTwitterXmlStatus($value, 'retweeted_status');
@@ -512,9 +625,13 @@ class ApiAction extends Action
$this->elementEnd('group');
}
- function showTwitterXmlUser($twitter_user, $role='user')
+ function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
{
- $this->elementStart($role);
+ $attrs = array();
+ if ($namespaces) {
+ $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
+ }
+ $this->elementStart($role, $attrs);
foreach($twitter_user as $element => $value) {
if ($element == 'status') {
$this->showTwitterXmlStatus($twitter_user['status']);
@@ -539,7 +656,7 @@ class ApiAction extends Action
}
}
- function showGeoRSS($geo)
+ function showGeoXML($geo)
{
if (empty($geo)) {
// empty geo element
@@ -551,6 +668,17 @@ class ApiAction extends Action
}
}
+ function showGeoRSS($geo)
+ {
+ if (!empty($geo)) {
+ $this->element(
+ 'georss:point',
+ null,
+ $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
+ );
+ }
+ }
+
function showTwitterRssItem($entry)
{
$this->elementStart('item');
@@ -585,7 +713,7 @@ class ApiAction extends Action
{
$this->initDocument('xml');
$twitter_status = $this->twitterStatusArray($notice);
- $this->showTwitterXmlStatus($twitter_status);
+ $this->showTwitterXmlStatus($twitter_status, 'status', true);
$this->endDocument('xml');
}
@@ -601,7 +729,8 @@ class ApiAction extends Action
{
$this->initDocument('xml');
- $this->elementStart('statuses', array('type' => 'array'));
+ $this->elementStart('statuses', array('type' => 'array',
+ 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
if (is_array($notice)) {
foreach ($notice as $n) {
@@ -619,13 +748,25 @@ class ApiAction extends Action
$this->endDocument('xml');
}
- function showRssTimeline($notice, $title, $link, $subtitle, $suplink=null, $logo=null)
+ function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
{
$this->initDocument('rss');
$this->element('title', null, $title);
$this->element('link', null, $link);
+
+ if (!is_null($self)) {
+ $this->element(
+ 'atom:link',
+ array(
+ 'type' => 'application/rss+xml',
+ 'href' => $self,
+ 'rel' => 'self'
+ )
+ );
+ }
+
if (!is_null($suplink)) {
// For FriendFeed's SUP protocol
$this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
@@ -732,8 +873,12 @@ class ApiAction extends Action
function showTwitterAtomEntry($entry)
{
$this->elementStart('entry');
- $this->element('title', null, $entry['title']);
- $this->element('content', array('type' => 'html'), $entry['content']);
+ $this->element('title', null, common_xml_safe_str($entry['title']));
+ $this->element(
+ 'content',
+ array('type' => 'html'),
+ common_xml_safe_str($entry['content'])
+ );
$this->element('id', null, $entry['id']);
$this->element('published', null, $entry['published']);
$this->element('updated', null, $entry['updated']);
@@ -752,9 +897,13 @@ class ApiAction extends Action
$this->elementEnd('entry');
}
- function showXmlDirectMessage($dm)
+ function showXmlDirectMessage($dm, $namespaces=false)
{
- $this->elementStart('direct_message');
+ $attrs = array();
+ if ($namespaces) {
+ $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
+ }
+ $this->elementStart('direct_message', $attrs);
foreach($dm as $element => $value) {
switch ($element) {
case 'sender':
@@ -831,7 +980,7 @@ class ApiAction extends Action
{
$this->initDocument('xml');
$dmsg = $this->directMessageArray($message);
- $this->showXmlDirectMessage($dmsg);
+ $this->showXmlDirectMessage($dmsg, true);
$this->endDocument('xml');
}
@@ -848,7 +997,7 @@ class ApiAction extends Action
$this->initDocument('atom');
- $this->element('title', null, $title);
+ $this->element('title', null, common_xml_safe_str($title));
$this->element('id', null, $id);
$this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
@@ -858,7 +1007,7 @@ class ApiAction extends Action
}
$this->element('updated', null, common_date_iso8601('now'));
- $this->element('subtitle', null, $subtitle);
+ $this->element('subtitle', null, common_xml_safe_str($subtitle));
if (is_array($group)) {
foreach ($group as $g) {
@@ -948,7 +1097,8 @@ class ApiAction extends Action
{
$this->initDocument('xml');
- $this->elementStart('users', array('type' => 'array'));
+ $this->elementStart('users', array('type' => 'array',
+ 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
if (is_array($user)) {
foreach ($user as $u) {
@@ -1039,6 +1189,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;
}
@@ -1067,6 +1218,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;
}
@@ -1138,7 +1290,14 @@ class ApiAction extends Action
function initTwitterRss()
{
$this->startXML();
- $this->elementStart('rss', array('version' => '2.0', 'xmlns:atom'=>'http://www.w3.org/2005/Atom'));
+ $this->elementStart(
+ 'rss',
+ array(
+ 'version' => '2.0',
+ 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
+ 'xmlns:georss' => 'http://www.georss.org/georss'
+ )
+ );
$this->elementStart('channel');
Event::handle('StartApiRss', array($this));
}
@@ -1176,6 +1335,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;
}
@@ -1214,6 +1374,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)) {
@@ -1239,7 +1427,7 @@ class ApiAction extends Action
if (empty($local)) {
return null;
} else {
- return User_group::staticGet('id', $local->id);
+ return User_group::staticGet('id', $local->group_id);
}
}
@@ -1256,43 +1444,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
@@ -1336,8 +1487,27 @@ class ApiAction extends Action
}
}
- function getSelfUri($action, $aargs)
+ /**
+ * Calculate the complete URI that called up this action. Used for
+ * Atom rel="self" links. Warning: this is funky.
+ *
+ * @return string URL a URL suitable for rel="self" Atom links
+ */
+ function getSelfUri()
{
+ $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
+
+ $id = $this->arg('id');
+ $aargs = array('format' => $this->format);
+ if (!empty($id)) {
+ $aargs['id'] = $id;
+ }
+
+ $tag = $this->arg('tag');
+ if (!empty($tag)) {
+ $aargs['tag'] = $tag;
+ }
+
parse_str($_SERVER['QUERY_STRING'], $params);
$pstring = '';
if (!empty($params)) {
diff --git a/lib/apiauth.php b/lib/apiauth.php
index 5090871cf..91cb64262 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);
@@ -235,9 +254,13 @@ class ApiAuthAction extends ApiAction
{
$this->basicAuthProcessHeader();
- $realm = common_config('site', 'name') . ' API';
+ $realm = common_config('api', 'realm');
- if (!isset($this->auth_user_nickname) && $required) {
+ if (empty($realm)) {
+ $realm = common_config('site', 'name') . ' API';
+ }
+
+ if (empty($this->auth_user_nickname) && $required) {
header('WWW-Authenticate: Basic realm="' . $realm . '"');
// show error if the user clicks 'cancel'
@@ -263,14 +286,14 @@ class ApiAuthAction extends ApiAction
$this->access = self::READ_WRITE;
- if (empty($this->auth_user) && $required) {
+ if (empty($this->auth_user) && ($required || isset($_SERVER['PHP_AUTH_USER']))) {
// basic authentication failed
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);
@@ -290,11 +313,15 @@ class ApiAuthAction extends ApiAction
function basicAuthProcessHeader()
{
- if (isset($_SERVER['AUTHORIZATION'])
- || isset($_SERVER['HTTP_AUTHORIZATION'])
- ) {
- $authorization_header = isset($_SERVER['HTTP_AUTHORIZATION'])
- ? $_SERVER['HTTP_AUTHORIZATION'] : $_SERVER['AUTHORIZATION'];
+ $authHeaders = array('AUTHORIZATION',
+ 'HTTP_AUTHORIZATION',
+ 'REDIRECT_HTTP_AUTHORIZATION'); // rewrite for CGI
+ $authorization_header = null;
+ foreach ($authHeaders as $header) {
+ if (isset($_SERVER[$header])) {
+ $authorization_header = $_SERVER[$header];
+ break;
+ }
}
if (isset($_SERVER['PHP_AUTH_USER'])) {
diff --git a/lib/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/atom10entry.php b/lib/atom10entry.php
deleted file mode 100644
index f8f16d594..000000000
--- a/lib/atom10entry.php
+++ /dev/null
@@ -1,105 +0,0 @@
-<?php
-/**
- * StatusNet, the distributed open-source microblogging tool
- *
- * Class for building / manipulating an Atom entry in memory
- *
- * PHP version 5
- *
- * LICENCE: This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- * @category Feed
- * @package StatusNet
- * @author Zach Copley <zach@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')) {
- exit(1);
-}
-
-class Atom10EntryException extends Exception
-{
-}
-
-/**
- * Class for manipulating an Atom entry in memory. Get the entry as an XML
- * string with Atom10Entry::getString().
- *
- * @category Feed
- * @package StatusNet
- * @author Zach Copley <zach@status.net>
- * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
- * @link http://status.net/
- */
-class Atom10Entry extends XMLStringer
-{
- private $namespaces;
- private $categories;
- private $content;
- private $contributors;
- private $id;
- private $links;
- private $published;
- private $rights;
- private $source;
- private $summary;
- private $title;
-
- function __construct($indent = true) {
- parent::__construct($indent);
- $this->namespaces = array();
- }
-
- function addNamespace($namespace, $uri)
- {
- $ns = array($namespace => $uri);
- $this->namespaces = array_merge($this->namespaces, $ns);
- }
-
- function initEntry()
- {
-
- }
-
- function endEntry()
- {
-
- }
-
- /**
- * Check that all required elements have been set, etc.
- * Throws an Atom10EntryException if something's missing.
- *
- * @return void
- */
- function validate()
- {
-
- }
-
- function getString()
- {
- $this->validate();
-
- $this->initEntry();
- $this->renderEntries();
- $this->endEntry();
-
- return $this->xw->outputMemory();
- }
-
-} \ No newline at end of file
diff --git a/lib/atom10feed.php b/lib/atom10feed.php
index 8842840d5..a46d49f35 100644
--- a/lib/atom10feed.php
+++ b/lib/atom10feed.php
@@ -49,6 +49,8 @@ class Atom10FeedException extends Exception
class Atom10Feed extends XMLStringer
{
public $xw;
+
+ // @fixme most of these should probably be read-only properties
private $namespaces;
private $authors;
private $subject;
@@ -57,10 +59,12 @@ class Atom10Feed extends XMLStringer
private $generator;
private $icon;
private $links;
- private $logo;
+ private $selfLink;
+ private $selfLinkType;
+ public $logo;
private $rights;
- private $subtitle;
- private $title;
+ public $subtitle;
+ public $title;
private $published;
private $updated;
private $entries;
@@ -172,6 +176,14 @@ class Atom10Feed extends XMLStringer
}
$this->elementStart('feed', $commonAttrs);
+ $this->element(
+ 'generator', array(
+ 'uri' => 'http://status.net',
+ 'version' => STATUSNET_VERSION
+ ),
+ 'StatusNet'
+ );
+
$this->element('id', null, $this->id);
$this->element('title', null, $this->title);
$this->element('subtitle', null, $this->subtitle);
@@ -184,6 +196,10 @@ class Atom10Feed extends XMLStringer
$this->renderAuthors();
+ if ($this->selfLink) {
+ $this->addLink($this->selfLink, array('rel' => 'self',
+ 'type' => $this->selfLinkType));
+ }
$this->renderLinks();
}
@@ -253,6 +269,12 @@ class Atom10Feed extends XMLStringer
$this->id = $id;
}
+ function setSelfLink($url, $type='application/atom+xml')
+ {
+ $this->selfLink = $url;
+ $this->selfLinkType = $type;
+ }
+
function setTitle($title)
{
$this->title = $title;
diff --git a/lib/atomcategory.php b/lib/atomcategory.php
new file mode 100644
index 000000000..4cc3b4f4d
--- /dev/null
+++ b/lib/atomcategory.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+class AtomCategory
+{
+ public $term;
+ public $scheme;
+ public $label;
+
+ function __construct($element=null)
+ {
+ if ($element && $element->attributes) {
+ $this->term = $this->extract($element, 'term');
+ $this->scheme = $this->extract($element, 'scheme');
+ $this->label = $this->extract($element, 'label');
+ }
+ }
+
+ protected function extract($element, $attrib)
+ {
+ $node = $element->attributes->getNamedItemNS(Activity::ATOM, $attrib);
+ if ($node) {
+ return trim($node->textContent);
+ }
+ $node = $element->attributes->getNamedItem($attrib);
+ if ($node) {
+ return trim($node->textContent);
+ }
+ return null;
+ }
+
+ function asString()
+ {
+ $attribs = array();
+ if ($this->term !== null) {
+ $attribs['term'] = $this->term;
+ }
+ if ($this->scheme !== null) {
+ $attribs['scheme'] = $this->scheme;
+ }
+ if ($this->label !== null) {
+ $attribs['label'] = $this->label;
+ }
+ $xs = new XMLStringer();
+ $xs->element('category', $attribs);
+ return $xs->asString();
+ }
+}
diff --git a/lib/atomgroupnoticefeed.php b/lib/atomgroupnoticefeed.php
index 52ee4c7d6..39a1fd456 100644
--- a/lib/atomgroupnoticefeed.php
+++ b/lib/atomgroupnoticefeed.php
@@ -49,14 +49,46 @@ class AtomGroupNoticeFeed extends AtomNoticeFeed
/**
* Constructor
*
- * @param Group $group the group for the feed (optional)
+ * @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 = null, $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
+ );
+ $this->setSubtitle($subtitle);
+
+ $avatar = $group->homepage_logo;
+ $logo = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE);
+ $this->setLogo($logo);
+
+ $this->setUpdated('now');
+
+ $self = common_local_url('ApiTimelineGroup',
+ array('id' => $group->id,
+ 'format' => 'atom'));
+ $this->setId($self);
+ $this->setSelfLink($self);
+
+ $this->addAuthorRaw($group->asAtomAuthor());
+ $this->setActivitySubject($group->asActivitySubject());
+
+ $this->addLink($group->homeUrl());
}
function getGroup()
@@ -64,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 3c3556cb9..6ed803ce4 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,9 +125,21 @@ class AtomNoticeFeed extends Atom10Feed
*/
function addEntryFromNotice($notice)
{
- $this->addEntryRaw($notice->asAtomEntry());
- }
+ $source = $this->showSource();
+ $author = $this->showAuthor();
-}
+ $cur = empty($this->cur) ? common_current_user() : $this->cur;
+ $this->addEntryRaw($notice->asAtomEntry(false, $source, $author, $cur));
+ }
+ function showSource()
+ {
+ return true;
+ }
+
+ function showAuthor()
+ {
+ return true;
+ }
+}
diff --git a/lib/atomusernoticefeed.php b/lib/atomusernoticefeed.php
index 2ad8de455..785db4915 100644
--- a/lib/atomusernoticefeed.php
+++ b/lib/atomusernoticefeed.php
@@ -49,23 +49,75 @@ class AtomUserNoticeFeed extends AtomNoticeFeed
/**
* Constructor
*
- * @param User $user the user for the feed (optional)
+ * @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 = null, $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();
$this->addAuthor($profile->nickname, $user->uri);
+ $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
+ );
+ $this->setSubtitle($subtitle);
+
+ $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
+ $logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
+ $this->setLogo($logo);
+
+ $this->setUpdated('now');
+
+ $this->addLink(
+ common_local_url(
+ 'showstream',
+ array('nickname' => $user->nickname)
+ )
+ );
+
+ $self = common_local_url('ApiTimelineUser',
+ array('id' => $user->id,
+ 'format' => 'atom'));
+ $this->setId($self);
+ $this->setSelfLink($self);
+
+ $this->addLink(
+ common_local_url('sup', null, null, $user->id),
+ array(
+ 'rel' => 'http://api.friendfeed.com/2008/03#sup',
+ 'type' => 'application/json'
+ )
+ );
}
function getUser()
{
return $this->user;
}
+
+ function showSource()
+ {
+ return false;
+ }
+
+ function showAuthor()
+ {
+ return false;
+ }
}
diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php
index 51ceca857..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'));
@@ -248,9 +249,7 @@ class Attachment extends AttachmentListItem
$this->out->elementStart('div', array('id' => 'attachment_view',
'class' => 'hentry'));
$this->out->elementStart('div', 'entry-title');
- $this->out->elementStart('a', $this->linkAttr());
- $this->out->element('span', null, $this->linkTitle());
- $this->out->elementEnd('a');
+ $this->out->element('a', $this->linkAttr(), $this->linkTitle());
$this->out->elementEnd('div');
$this->out->elementStart('div', 'entry-content');
@@ -262,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)) {
@@ -275,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)) {
@@ -296,7 +297,7 @@ class Attachment extends AttachmentListItem
}
function linkAttr() {
- return array('class' => 'external', 'href' => $this->attachment->url);
+ return array('rel' => 'external', 'href' => $this->attachment->url);
}
function linkTitle() {
@@ -306,7 +307,7 @@ class Attachment extends AttachmentListItem
function showRepresentation() {
if (empty($this->oembed->type)) {
if (empty($this->attachment->mimetype)) {
- $this->out->element('pre', null, 'oh well... not sure how to handle the following: ' . print_r($this->attachment, true));
+ $this->showFallback();
} else {
switch ($this->attachment->mimetype) {
case 'image/gif':
@@ -332,6 +333,17 @@ class Attachment extends AttachmentListItem
$this->out->element('param', array('name' => 'autoStart', 'value' => 1));
$this->out->elementEnd('object');
break;
+
+ case 'text/html':
+ if ($this->attachment->filename) {
+ // Locally-uploaded HTML. Scrub and display inline.
+ $this->showHtmlFile($this->attachment);
+ break;
+ }
+ // Fall through to default.
+
+ default:
+ $this->showFallback();
}
}
} else {
@@ -354,9 +366,76 @@ class Attachment extends AttachmentListItem
break;
default:
- $this->out->element('pre', null, 'oh well... not sure how to handle the following oembed: ' . print_r($this->oembed, true));
+ $this->showFallback();
}
}
}
+
+ protected function showHtmlFile(File $attachment)
+ {
+ $body = $this->scrubHtmlFile($attachment);
+ if ($body) {
+ $this->out->raw($body);
+ }
+ }
+
+ /**
+ * @return mixed false on failure, HTML fragment string on success
+ */
+ protected function scrubHtmlFile(File $attachment)
+ {
+ $path = File::path($attachment->filename);
+ if (!file_exists($path) || !is_readable($path)) {
+ common_log(LOG_ERR, "Missing local HTML attachment $path");
+ return false;
+ }
+ $raw = file_get_contents($path);
+
+ // Normalize...
+ $dom = new DOMDocument();
+ if(!$dom->loadHTML($raw)) {
+ common_log(LOG_ERR, "Bad HTML in local HTML attachment $path");
+ return false;
+ }
+
+ // Remove <script>s or htmlawed will dump their contents into output!
+ // Note: removing child nodes while iterating seems to mess things up,
+ // hence the double loop.
+ $scripts = array();
+ foreach ($dom->getElementsByTagName('script') as $script) {
+ $scripts[] = $script;
+ }
+ foreach ($scripts as $script) {
+ common_log(LOG_DEBUG, $script->textContent);
+ $script->parentNode->removeChild($script);
+ }
+
+ // Trim out everything outside the body...
+ $body = $dom->saveHTML();
+ $body = preg_replace('/^.*<body[^>]*>/is', '', $body);
+ $body = preg_replace('/<\/body[^>]*>.*$/is', '', $body);
+
+ require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
+ $config = array('safe' => 1,
+ 'deny_attribute' => 'id,style,on*',
+ 'comment' => 1); // remove comments
+ $scrubbed = htmLawed($body, $config);
+
+ return $scrubbed;
+ }
+
+ function showFallback()
+ {
+ // If we don't know how to display an attachment inline, we probably
+ // shouldn't have gotten to this point.
+ //
+ // But, here we are... displaying details on a file or remote URL
+ // either on the main view or in an ajax-loaded lightbox. As a lesser
+ // of several evils, we'll try redirecting to the actual target via
+ // client-side JS.
+
+ common_log(LOG_ERR, "Empty or unknown type for file id {$this->attachment->id}; falling back to client-side redirect.");
+ $this->out->raw('<script>window.location = ' . json_encode($this->attachment->url) . ';</script>');
+ }
}
diff --git a/lib/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
new file mode 100644
index 000000000..7d4256d6e
--- /dev/null
+++ b/lib/avatarlink.php
@@ -0,0 +1,102 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+// XXX: Arg! This wouldn't be necessary if we used Avatars conistently
+class AvatarLink
+{
+ public $url;
+ public $type;
+ public $size;
+ public $width;
+ public $height;
+
+ function __construct($element=null)
+ {
+ if ($element) {
+ // @fixme use correct namespaces
+ $this->url = $element->getAttribute('href');
+ $this->type = $element->getAttribute('type');
+ $width = $element->getAttribute('media:width');
+ if ($width != null) {
+ $this->width = intval($width);
+ }
+ $height = $element->getAttribute('media:height');
+ if ($height != null) {
+ $this->height = intval($height);
+ }
+ }
+ }
+
+ static function fromAvatar($avatar)
+ {
+ if (empty($avatar)) {
+ return null;
+ }
+ $alink = new AvatarLink();
+ $alink->type = $avatar->mediatype;
+ $alink->height = $avatar->height;
+ $alink->width = $avatar->width;
+ $alink->url = $avatar->displayUrl();
+ return $alink;
+ }
+
+ static function fromFilename($filename, $size)
+ {
+ $alink = new AvatarLink();
+ $alink->url = $filename;
+ $alink->height = $size;
+ $alink->width = $size;
+ if (!empty($filename)) {
+ $alink->type = self::mediatype($filename);
+ } else {
+ $alink->url = User_group::defaultLogo($size);
+ $alink->type = 'image/png';
+ }
+ return $alink;
+ }
+
+ // yuck!
+ static function mediatype($filename) {
+ $ext = strtolower(end(explode('.', $filename)));
+ if ($ext == 'jpeg') {
+ $ext = 'jpg';
+ }
+ // hope we don't support any others
+ $types = array('png', 'gif', 'jpg', 'jpeg');
+ if (in_array($ext, $types)) {
+ return 'image/' . $ext;
+ }
+ return null;
+ }
+}
diff --git a/lib/channel.php b/lib/channel.php
index 3cd168786..689bca0be 100644
--- a/lib/channel.php
+++ b/lib/channel.php
@@ -47,6 +47,25 @@ class Channel
}
}
+class CLIChannel extends Channel
+{
+ function source()
+ {
+ return 'cli';
+ }
+
+ function output($user, $text)
+ {
+ $site = common_config('site', 'name');
+ print "[{$user->nickname}@{$site}] $text\n";
+ }
+
+ function error($user, $text)
+ {
+ $this->output($user, $text);
+ }
+}
+
class XMPPChannel extends Channel
{
diff --git a/lib/command.php b/lib/command.php
index db8e80030..a769dc1fc 100644
--- a/lib/command.php
+++ b/lib/command.php
@@ -1,7 +1,7 @@
<?php
/*
* StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2008, 2009, StatusNet, Inc.
+ * Copyright (C) 2008, 2009, 2010 StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -31,15 +31,151 @@ class Command
$this->user = $user;
}
- function execute($channel)
+ /**
+ * Execute the command and send success or error results
+ * back via the given communications channel.
+ *
+ * @param Channel
+ */
+ public function execute($channel)
+ {
+ try {
+ $this->handle($channel);
+ } catch (CommandException $e) {
+ $channel->error($this->user, $e->getMessage());
+ } catch (Exception $e) {
+ common_log(LOG_ERR, "Error handling " . get_class($this) . ": " . $e->getMessage());
+ $channel->error($this->user, $e->getMessage());
+ }
+ }
+
+
+ /**
+ * Override this with the meat!
+ *
+ * An error to send back to the user may be sent by throwing
+ * a CommandException with a formatted message.
+ *
+ * @param Channel
+ * @throws CommandException
+ */
+ function handle($channel)
{
return false;
}
+
+ /**
+ * Look up a notice from an argument, by poster's name to get last post
+ * or notice_id prefixed with #.
+ *
+ * @return Notice
+ * @throws CommandException
+ */
+ function getNotice($arg)
+ {
+ $notice = null;
+ if (Event::handle('StartCommandGetNotice', array($this, $arg, &$notice))) {
+ if(substr($this->other,0,1)=='#'){
+ // A specific notice_id #123
+
+ $notice = Notice::staticGet(substr($arg,1));
+ if (!$notice) {
+ throw new CommandException(_('Notice with that id does not exist'));
+ }
+ }
+
+ if (Validate::uri($this->other)) {
+ // A specific notice by URI lookup
+ $notice = Notice::staticGet('uri', $arg);
+ }
+
+ if (!$notice) {
+ // Local or remote profile name to get their last notice.
+ // May throw an exception and report 'no such user'
+ $recipient = $this->getProfile($arg);
+
+ $notice = $recipient->getCurrentNotice();
+ if (!$notice) {
+ throw new CommandException(_('User has no last notice'));
+ }
+ }
+ }
+ Event::handle('EndCommandGetNotice', array($this, $arg, &$notice));
+ if (!$notice) {
+ throw new CommandException(_('Notice with that id does not exist'));
+ }
+ return $notice;
+ }
+
+ /**
+ * Look up a local or remote profile by nickname.
+ *
+ * @return Profile
+ * @throws CommandException
+ */
+ function getProfile($arg)
+ {
+ $profile = null;
+ if (Event::handle('StartCommandGetProfile', array($this, $arg, &$profile))) {
+ $profile =
+ common_relative_profile($this->user, common_canonical_nickname($arg));
+ }
+ Event::handle('EndCommandGetProfile', array($this, $arg, &$profile));
+ if (!$profile) {
+ // 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;
+ }
+
+ /**
+ * Get a local user by name
+ * @return User
+ * @throws CommandException
+ */
+ function getUser($arg)
+ {
+ $user = null;
+ if (Event::handle('StartCommandGetUser', array($this, $arg, &$user))) {
+ $user = User::staticGet('nickname', $arg);
+ }
+ Event::handle('EndCommandGetUser', array($this, $arg, &$user));
+ if (!$user){
+ // 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;
+ }
+
+ /**
+ * Get a local or remote group by name.
+ * @return User_group
+ * @throws CommandException
+ */
+ function getGroup($arg)
+ {
+ $group = null;
+ if (Event::handle('StartCommandGetGroup', array($this, $arg, &$group))) {
+ $group = User_group::getForNickname($arg, $this->user->getProfile());
+ }
+ Event::handle('EndCommandGetGroup', array($this, $arg, &$group));
+ if (!$group) {
+ throw new CommandException(_('No such group.'));
+ }
+ return $group;
+ }
+}
+
+class CommandException extends Exception
+{
}
class UnimplementedCommand extends Command
{
- function execute($channel)
+ function handle($channel)
{
$channel->error($this->user, _("Sorry, this command is not yet implemented."));
}
@@ -81,24 +217,22 @@ class NudgeCommand extends Command
parent::__construct($user);
$this->other = $other;
}
- function execute($channel)
+
+ function handle($channel)
{
- $recipient = User::staticGet('nickname', $this->other);
- if(! $recipient){
- $channel->error($this->user, sprintf(_('Could not find a user with nickname %s'),
- $this->other));
- }else{
- if ($recipient->id == $this->user->id) {
- $channel->error($this->user, _('It does not make a lot of sense to nudge yourself!'));
- }else{
- if ($recipient->email && $recipient->emailnotifynudge) {
- mail_notify_nudge($this->user, $recipient);
- }
- // XXX: notify by IM
- // XXX: notify by SMS
- $channel->output($this->user, sprintf(_('Nudge sent to %s'),
- $recipient->nickname));
+ $recipient = $this->getUser($this->other);
+ if ($recipient->id == $this->user->id) {
+ throw new CommandException(_('It does not make a lot of sense to nudge yourself!'));
+ } else {
+ if ($recipient->email && $recipient->emailnotifynudge) {
+ mail_notify_nudge($this->user, $recipient);
}
+ // XXX: notify by IM
+ // XXX: notify by SMS
+ // 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));
}
}
}
@@ -115,7 +249,7 @@ class InviteCommand extends UnimplementedCommand
class StatsCommand extends Command
{
- function execute($channel)
+ function handle($channel)
{
$profile = $this->user->getProfile();
@@ -142,42 +276,20 @@ class FavCommand extends Command
$this->other = $other;
}
- function execute($channel)
+ function handle($channel)
{
- if(substr($this->other,0,1)=='#'){
- //favoriting a specific notice_id
-
- $notice = Notice::staticGet(substr($this->other,1));
- if (!$notice) {
- $channel->error($this->user, _('Notice with that id does not exist'));
- return;
- }
- $recipient = $notice->getProfile();
- }else{
- //favoriting a given user's last notice
-
- $recipient =
- common_relative_profile($this->user, common_canonical_nickname($this->other));
-
- if (!$recipient) {
- $channel->error($this->user, _('No such user.'));
- return;
- }
- $notice = $recipient->getCurrentNotice();
- if (!$notice) {
- $channel->error($this->user, _('User has no last notice'));
- return;
- }
- }
-
- $fave = Fave::addNew($this->user, $notice);
+ $notice = $this->getNotice($this->other);
+ $fave = Fave::addNew($this->user->getProfile(), $notice);
if (!$fave) {
$channel->error($this->user, _('Could not create favorite.'));
return;
}
- $other = User::staticGet('id', $recipient->id);
+ // @fixme favorite notification should be triggered
+ // at a lower level
+
+ $other = User::staticGet('id', $notice->profile_id);
if ($other && $other->id != $user->id) {
if ($other->email && $other->emailnotifyfav) {
@@ -191,6 +303,7 @@ class FavCommand extends Command
}
}
+
class JoinCommand extends Command
{
var $other = null;
@@ -201,17 +314,10 @@ class JoinCommand extends Command
$this->other = $other;
}
- function execute($channel)
+ function handle($channel)
{
-
- $nickname = common_canonical_nickname($this->other);
- $group = User_group::staticGet('nickname', $nickname);
- $cur = $this->user;
-
- if (!$group) {
- $channel->error($cur, _('No such group.'));
- return;
- }
+ $group = $this->getGroup($this->other);
+ $cur = $this->user;
if ($cur->isMember($group)) {
$channel->error($cur, _('You are already a member of that group'));
@@ -228,12 +334,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));
}
@@ -249,12 +359,10 @@ class DropCommand extends Command
$this->other = $other;
}
- function execute($channel)
+ function handle($channel)
{
-
- $nickname = common_canonical_nickname($this->other);
- $group = User_group::staticGet('nickname', $nickname);
- $cur = $this->user;
+ $group = $this->getGroup($this->other);
+ $cur = $this->user;
if (!$group) {
$channel->error($cur, _('No such group.'));
@@ -272,12 +380,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));
}
@@ -293,28 +405,28 @@ class WhoisCommand extends Command
$this->other = $other;
}
- function execute($channel)
+ function handle($channel)
{
- $recipient =
- common_relative_profile($this->user, common_canonical_nickname($this->other));
-
- if (!$recipient) {
- $channel->error($this->user, _('No such user.'));
- return;
- }
+ $recipient = $this->getProfile($this->other);
+ // 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);
@@ -332,9 +444,18 @@ class MessageCommand extends Command
$this->text = $text;
}
- function execute($channel)
+ function handle($channel)
{
- $other = User::staticGet('nickname', common_canonical_nickname($this->other));
+ try {
+ $other = $this->getUser($this->other);
+ } catch (CommandException $e) {
+ try {
+ $profile = $this->getProfile($this->other);
+ } catch (CommandException $f) {
+ throw $e;
+ }
+ throw new CommandException(sprintf(_('%s is a remote profile; you can only send direct messages to users on the same server.'), $this->other));
+ }
$len = mb_strlen($this->text);
@@ -346,7 +467,9 @@ 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'),
+ // 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;
}
@@ -364,6 +487,8 @@ class MessageCommand extends Command
$message = Message::saveNew($this->user->id, $other->id, $this->text, $channel->source());
if ($message) {
$message->notify();
+ // 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 {
$channel->error($this->user, _('Error sending direct message.'));
@@ -380,33 +505,9 @@ class RepeatCommand extends Command
$this->other = $other;
}
- function execute($channel)
+ function handle($channel)
{
- if(substr($this->other,0,1)=='#'){
- //repeating a specific notice_id
-
- $notice = Notice::staticGet(substr($this->other,1));
- if (!$notice) {
- $channel->error($this->user, _('Notice with that id does not exist'));
- return;
- }
- $recipient = $notice->getProfile();
- }else{
- //repeating a given user's last notice
-
- $recipient =
- common_relative_profile($this->user, common_canonical_nickname($this->other));
-
- if (!$recipient) {
- $channel->error($this->user, _('No such user.'));
- return;
- }
- $notice = $recipient->getCurrentNotice();
- if (!$notice) {
- $channel->error($this->user, _('User has no last notice'));
- return;
- }
- }
+ $notice = $this->getNotice($this->other);
if($this->user->id == $notice->profile_id)
{
@@ -414,7 +515,7 @@ class RepeatCommand extends Command
return;
}
- if ($recipient->hasRepeated($notice->id)) {
+ if ($this->user->getProfile()->hasRepeated($notice->id)) {
$channel->error($this->user, _('Already repeated that notice'));
return;
}
@@ -423,6 +524,8 @@ class RepeatCommand extends Command
if ($repeat) {
+ // 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 {
$channel->error($this->user, _('Error repeating notice.'));
@@ -441,33 +544,10 @@ class ReplyCommand extends Command
$this->text = $text;
}
- function execute($channel)
+ function handle($channel)
{
- if(substr($this->other,0,1)=='#'){
- //replying to a specific notice_id
-
- $notice = Notice::staticGet(substr($this->other,1));
- if (!$notice) {
- $channel->error($this->user, _('Notice with that id does not exist'));
- return;
- }
- $recipient = $notice->getProfile();
- }else{
- //replying to a given user's last notice
-
- $recipient =
- common_relative_profile($this->user, common_canonical_nickname($this->other));
-
- if (!$recipient) {
- $channel->error($this->user, _('No such user.'));
- return;
- }
- $notice = $recipient->getCurrentNotice();
- if (!$notice) {
- $channel->error($this->user, _('User has no last notice'));
- return;
- }
- }
+ $notice = $this->getNotice($this->other);
+ $recipient = $notice->getProfile();
$len = mb_strlen($this->text);
@@ -507,17 +587,10 @@ class GetCommand extends Command
$this->other = $other;
}
- function execute($channel)
+ function handle($channel)
{
- $target_nickname = common_canonical_nickname($this->other);
-
- $target =
- common_relative_profile($this->user, $target_nickname);
+ $target = $this->getProfile($this->other);
- if (!$target) {
- $channel->error($this->user, _('No such user.'));
- return;
- }
$notice = $target->getCurrentNotice();
if (!$notice) {
$channel->error($this->user, _('User has no last notice'));
@@ -525,7 +598,7 @@ class GetCommand extends Command
}
$notice_content = $notice->content;
- $channel->output($this->user, $target_nickname . ": " . $notice_content);
+ $channel->output($this->user, $target->nickname . ": " . $notice_content);
}
}
@@ -540,7 +613,7 @@ class SubCommand extends Command
$this->other = $other;
}
- function execute($channel)
+ function handle($channel)
{
if (!$this->other) {
@@ -548,16 +621,16 @@ class SubCommand extends Command
return;
}
- $otherUser = User::staticGet('nickname', $this->other);
+ $target = $this->getProfile($this->other);
- if (empty($otherUser)) {
- $channel->error($this->user, _('No such user'));
- return;
+ $remote = Remote_profile::staticGet('id', $target->id);
+ if ($remote) {
+ throw new CommandException(_("Can't subscribe to OMB profiles by command."));
}
try {
Subscription::start($this->user->getProfile(),
- $otherUser->getProfile());
+ $target);
$channel->output($this->user, sprintf(_('Subscribed to %s'), $this->other));
} catch (Exception $e) {
$channel->error($this->user, $e->getMessage());
@@ -576,22 +649,18 @@ class UnsubCommand extends Command
$this->other = $other;
}
- function execute($channel)
+ function handle($channel)
{
if(!$this->other) {
$channel->error($this->user, _('Specify the name of the user to unsubscribe from'));
return;
}
- $otherUser = User::staticGet('nickname', $this->other);
-
- if (empty($otherUser)) {
- $channel->error($this->user, _('No such user'));
- }
+ $target = $this->getProfile($this->other);
try {
Subscription::cancel($this->user->getProfile(),
- $otherUser->getProfile());
+ $target);
$channel->output($this->user, sprintf(_('Unsubscribed from %s'), $this->other));
} catch (Exception $e) {
$channel->error($this->user, $e->getMessage());
@@ -607,7 +676,7 @@ class OffCommand extends Command
parent::__construct($user);
$this->other = $other;
}
- function execute($channel)
+ function handle($channel)
{
if ($other) {
$channel->error($this->user, _("Command not yet implemented."));
@@ -630,7 +699,7 @@ class OnCommand extends Command
$this->other = $other;
}
- function execute($channel)
+ function handle($channel)
{
if ($other) {
$channel->error($this->user, _("Command not yet implemented."));
@@ -646,7 +715,7 @@ class OnCommand extends Command
class LoginCommand extends Command
{
- function execute($channel)
+ function handle($channel)
{
$disabled = common_config('logincommand','disabled');
$disabled = isset($disabled) && $disabled;
@@ -686,7 +755,7 @@ class LoseCommand extends Command
return;
}
- $result=subs_unsubscribe_from($this->user, $this->other);
+ $result = Subscription::cancel($this->getProfile($this->other), $this->user->getProfile());
if ($result) {
$channel->output($this->user, sprintf(_('Unsubscribed %s'), $this->other));
@@ -698,7 +767,7 @@ class LoseCommand extends Command
class SubscriptionsCommand extends Command
{
- function execute($channel)
+ function handle($channel)
{
$profile = $this->user->getSubscriptions(0);
$nicknames=array();
@@ -720,7 +789,7 @@ class SubscriptionsCommand extends Command
class SubscribersCommand extends Command
{
- function execute($channel)
+ function handle($channel)
{
$profile = $this->user->getSubscribers();
$nicknames=array();
@@ -742,7 +811,7 @@ class SubscribersCommand extends Command
class GroupsCommand extends Command
{
- function execute($channel)
+ function handle($channel)
{
$group = $this->user->getGroups();
$groups=array();
@@ -763,7 +832,7 @@ class GroupsCommand extends Command
class HelpCommand extends Command
{
- function execute($channel)
+ function handle($channel)
{
$channel->output($this->user,
_("Commands:\n".
diff --git a/lib/common.php b/lib/common.php
index 6c01c7db4..817434b97 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.0beta6+bugfix1');
+define('STATUSNET_VERSION', '0.9.3');
define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility
-define('STATUSNET_CODENAME', 'Stand');
+define('STATUSNET_CODENAME', 'Half a World Away');
define('AVATAR_PROFILE_SIZE', 96);
define('AVATAR_STREAM_SIZE', 48);
@@ -71,7 +71,6 @@ if (!function_exists('dl')) {
# global configuration object
require_once('PEAR.php');
-require_once('PEAR/Exception.php');
require_once('DB/DataObject.php');
require_once('DB/DataObject/Cast.php'); # for dates
@@ -124,22 +123,10 @@ require_once INSTALLDIR.'/lib/util.php';
require_once INSTALLDIR.'/lib/action.php';
require_once INSTALLDIR.'/lib/mail.php';
require_once INSTALLDIR.'/lib/subs.php';
-require_once INSTALLDIR.'/lib/activity.php';
require_once INSTALLDIR.'/lib/clientexception.php';
require_once INSTALLDIR.'/lib/serverexception.php';
-
-//set PEAR error handling to use regular PHP exceptions
-function PEAR_ErrorToPEAR_Exception($err)
-{
- if ($err->getCode()) {
- throw new PEAR_Exception($err->getMessage(), $err->getCode());
- }
- throw new PEAR_Exception($err->getMessage());
-}
-PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'PEAR_ErrorToPEAR_Exception');
-
try {
StatusNet::init(@$server, @$path, @$conffile);
} catch (NoConfigException $e) {
diff --git a/lib/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 7b97c7dda..dcf225d1f 100644
--- a/lib/default.php
+++ b/lib/default.php
@@ -40,7 +40,8 @@ $default =
'logdebug' => false,
'fancy' => false,
'locale_path' => INSTALLDIR.'/locale',
- 'language' => 'en_US',
+ 'language' => 'en',
+ 'langdetect' => true,
'languages' => get_all_languages(),
'email' =>
array_key_exists('SERVER_ADMIN', $_SERVER) ? $_SERVER['SERVER_ADMIN'] : null,
@@ -53,10 +54,11 @@ $default =
'ssl' => 'never',
'sslserver' => null,
'shorturllength' => 30,
- 'dupelimit' => 60, # default for same person saying the same thing
+ 'dupelimit' => 60, // default for same person saying the same thing
'textlimit' => 140,
'indent' => true,
- 'use_x_sendfile' => false
+ 'use_x_sendfile' => false,
+ 'notice' => null // site wide notice text
),
'db' =>
array('database' => 'YOU HAVE TO SET THIS IN config.php',
@@ -70,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' =>
@@ -85,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
@@ -136,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
@@ -183,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' =>
@@ -254,6 +267,9 @@ $default =
'linkcolor' => null,
'backgroundimage' => null,
'disposition' => null),
+ 'custom_css' =>
+ array('enabled' => true,
+ 'css' => ''),
'notice' =>
array('contentlimit' => null),
'message' =>
@@ -278,12 +294,13 @@ $default =
'TightUrl' => array('shortenerName' => '2tu.us', 'freeService' => true,'serviceUrl'=>'http://2tu.us/?save=y&url=%1$s'),
'Geonames' => null,
'Mapstraction' => null,
+ 'OStatus' => null,
'WikiHashtags' => null,
'RSSCloud' => null,
'OpenID' => null),
),
'admin' =>
- array('panels' => array('design', 'site', 'user', 'paths', 'access', 'sessions')),
+ array('panels' => array('design', 'site', 'user', 'paths', 'access', 'sessions', 'sitenotice')),
'singleuser' =>
array('enabled' => false,
'nickname' => null),
@@ -291,4 +308,13 @@ $default =
array('crawldelay' => 0,
'disallow' => array('main', 'settings', 'admin', 'search', 'message')
),
+ 'api' =>
+ array('realm' => null),
+ 'nofollow' =>
+ 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')
+ ),
);
diff --git a/lib/deluserqueuehandler.php b/lib/deluserqueuehandler.php
new file mode 100644
index 000000000..710303938
--- /dev/null
+++ b/lib/deluserqueuehandler.php
@@ -0,0 +1,99 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Background job to delete prolific users without disrupting front-end too much.
+ *
+ * Up to 50 messages are deleted on each run through; when all messages are gone,
+ * the actual account is deleted.
+ *
+ * @package QueueHandler
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+class DelUserQueueHandler extends QueueHandler
+{
+ const DELETION_WINDOW = 50;
+
+ public function transport()
+ {
+ return 'deluser';
+ }
+
+ public function handle($user)
+ {
+ if (!($user instanceof User)) {
+ common_log(LOG_ERR, "Got a bogus user, not deleting");
+ return true;
+ }
+
+ $user = User::staticGet('id', $user->id);
+ if (!$user) {
+ common_log(LOG_INFO, "User {$user->nickname} was deleted before we got here.");
+ return true;
+ }
+
+ try {
+ if (!$user->hasRole(Profile_role::DELETED)) {
+ common_log(LOG_INFO, "User {$user->nickname} is not pending deletion; aborting.");
+ return true;
+ }
+ } catch (UserNoProfileException $unp) {
+ common_log(LOG_INFO, "Deleting user {$user->nickname} with no profile... probably a good idea!");
+ }
+
+ $notice = $this->getNextBatch($user);
+ if ($notice->N) {
+ common_log(LOG_INFO, "Deleting next {$notice->N} notices by {$user->nickname}");
+ while ($notice->fetch()) {
+ $del = clone($notice);
+ $del->delete();
+ }
+
+ // @todo improve reliability in case we died during the above deletions
+ // with a fatal error. If the job is lost, we should perform some kind
+ // of garbage collection later.
+
+ // Queue up the next batch.
+ $qm = QueueManager::get();
+ $qm->enqueue($user, 'deluser');
+ } else {
+ // Out of notices? Let's finish deleting this guy!
+ $user->delete();
+ common_log(LOG_INFO, "User $user->id $user->nickname deleted.");
+ return true;
+ }
+
+ return true;
+ }
+
+ /**
+ * Fetch the next self::DELETION_WINDOW messages for this user.
+ * @return Notice
+ */
+ protected function getNextBatch(User $user)
+ {
+ $notice = new Notice();
+ $notice->profile_id = $user->id;
+ $notice->limit(self::DELETION_WINDOW);
+ $notice->find();
+ return $notice;
+ }
+
+}
diff --git a/lib/distribqueuehandler.php b/lib/distribqueuehandler.php
index d2be7a92c..8f4b72d5c 100644
--- a/lib/distribqueuehandler.php
+++ b/lib/distribqueuehandler.php
@@ -49,19 +49,22 @@ class DistribQueueHandler
}
/**
- * Here's the meat of your queue handler -- you're handed a Notice
- * object, which you may do as you will with.
+ * Handle distribution of a notice after we've saved it:
+ * @li add to local recipient inboxes
+ * @li send email notifications to local @-reply targets
+ * @li run final EndNoticeSave plugin events
+ * @li put any remaining post-processing into the queues
*
* If this function indicates failure, a warning will be logged
* and the item is placed back in the queue to be re-run.
*
+ * @fixme addToInboxes is known to fail sometimes with large recipient sets
+ *
* @param Notice $notice
* @return boolean true on success, false on failure
*/
function handle($notice)
{
- // XXX: do we need to change this for remote users?
-
try {
$notice->addToInboxes();
} catch (Exception $e) {
@@ -69,6 +72,12 @@ class DistribQueueHandler
}
try {
+ $notice->sendReplyNotifications();
+ } catch (Exception $e) {
+ $this->logit($notice, $e);
+ }
+
+ try {
Event::handle('EndNoticeSave', array($notice));
// Enqueue for other handlers
} catch (Exception $e) {
diff --git a/lib/grantroleform.php b/lib/grantroleform.php
new file mode 100644
index 000000000..b5f952746
--- /dev/null
+++ b/lib/grantroleform.php
@@ -0,0 +1,93 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Form for granting a role
+ *
+ * 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 Form
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>, Brion Vibber <brion@status.net>
+ * @copyright 2009-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')) {
+ exit(1);
+}
+
+/**
+ * Form for sandboxing a user
+ *
+ * @category Form
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ *
+ * @see UnSandboxForm
+ */
+
+class GrantRoleForm extends ProfileActionForm
+{
+ function __construct($role, $label, $writer, $profile, $r2args)
+ {
+ parent::__construct($writer, $profile, $r2args);
+ $this->role = $role;
+ $this->label = $label;
+ }
+
+ /**
+ * Action this form provides
+ *
+ * @return string Name of the action, lowercased.
+ */
+
+ function target()
+ {
+ return 'grantrole';
+ }
+
+ /**
+ * Title of the form
+ *
+ * @return string Title of the form, internationalized
+ */
+
+ function title()
+ {
+ return $this->label;
+ }
+
+ function formData()
+ {
+ parent::formData();
+ $this->out->hidden('role', $this->role);
+ }
+
+ /**
+ * Description of the form
+ *
+ * @return string description of the form, internationalized
+ */
+
+ function description()
+ {
+ return sprintf(_('Grant this user the "%s" role'), $this->label);
+ }
+}
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 4c3af8d7d..b69f718e5 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)
{
@@ -120,6 +123,28 @@ class HTTPClient extends HTTP_Request2
{
$this->config['max_redirs'] = 10;
$this->config['follow_redirects'] = true;
+
+ // We've had some issues with keepalive breaking with
+ // HEAD requests, such as to youtube which seems to be
+ // emitting chunked encoding info for an empty body
+ // instead of not emitting anything. This may be a
+ // bug on YouTube's end, but the upstream libray
+ // ought to be investigated to see if we can handle
+ // it gracefully in that case as well.
+ $this->config['protocol_version'] = '1.0';
+
+ // 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;
+ }
+
parent::__construct($url, $method, $config);
$this->setHeader('User-Agent', $this->userAgent());
}
@@ -136,7 +161,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())
@@ -147,7 +172,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())
@@ -161,7 +186,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())
@@ -173,7 +198,7 @@ class HTTPClient extends HTTP_Request2
}
/**
- * @return HTTPResponse
+ * @return StatusNet_HTTPResponse
* @throws HTTP_Request2_Exception
*/
protected function doRequest($url, $method, $headers)
@@ -207,12 +232,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()
@@ -255,6 +280,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/imagefile.php b/lib/imagefile.php
index 7b0479455..e47287741 100644
--- a/lib/imagefile.php
+++ b/lib/imagefile.php
@@ -60,6 +60,19 @@ class ImageFile
$this->filepath = $filepath;
$info = @getimagesize($this->filepath);
+
+ if (!(
+ ($info[2] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) ||
+ ($info[2] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) ||
+ $info[2] == IMAGETYPE_BMP ||
+ ($info[2] == IMAGETYPE_WBMP && function_exists('imagecreatefromwbmp')) ||
+ ($info[2] == IMAGETYPE_XBM && function_exists('imagecreatefromxbm')) ||
+ ($info[2] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')))) {
+
+ throw new Exception(_('Unsupported image file format.'));
+ return;
+ }
+
$this->type = ($info) ? $info[2]:$type;
$this->width = ($info) ? $info[0]:$width;
$this->height = ($info) ? $info[1]:$height;
@@ -97,19 +110,6 @@ class ImageFile
return;
}
- if ($info[2] !== IMAGETYPE_GIF &&
- $info[2] !== IMAGETYPE_JPEG &&
- $info[2] !== IMAGETYPE_BMP &&
- $info[2] !== IMAGETYPE_WBMP &&
- $info[2] !== IMAGETYPE_XBM &&
- $info[2] !== IMAGETYPE_XPM &&
- $info[2] !== IMAGETYPE_PNG) {
-
- @unlink($_FILES[$param]['tmp_name']);
- throw new Exception(_('Unsupported image file format.'));
- return;
- }
-
return new ImageFile(null, $_FILES[$param]['tmp_name']);
}
@@ -159,9 +159,6 @@ class ImageFile
case IMAGETYPE_XBM:
$image_src = imagecreatefromxbm($this->filepath);
break;
- case IMAGETYPE_XPM:
- $image_src = imagecreatefromxpm($this->filepath);
- break;
default:
throw new Exception(_('Unknown file type'));
return;
@@ -204,10 +201,6 @@ class ImageFile
//we don't want to save XBM... it's a rare format that we can't guarantee clients will support
//save png instead
$this->type = IMAGETYPE_PNG;
- } else if($this->type == IMAGETYPE_XPM) {
- //we don't want to save XPM... it's a rare format that we can't guarantee clients will support
- //save png instead
- $this->type = IMAGETYPE_PNG;
}
$outname = Avatar::filename($this->id,
diff --git a/lib/installer.php b/lib/installer.php
new file mode 100644
index 000000000..bd9d69cd4
--- /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', '<')) {
+ $errors[] = '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 %d 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 e1bf06ba6..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
@@ -88,22 +247,30 @@ class Sharing_XMPP extends XMPPHP_XMPP
/**
* Build an XMPP proxy connection that'll save outgoing messages
* to the 'xmppout' queue to be picked up by xmppdaemon later.
+ *
+ * If queueing is disabled, we'll grab a live connection.
+ *
+ * @return XMPPHP
*/
function jabber_proxy()
{
- $proxy = new Queued_XMPP(common_config('xmpp', 'host') ?
- common_config('xmpp', 'host') :
- common_config('xmpp', 'server'),
- common_config('xmpp', 'port'),
- common_config('xmpp', 'user'),
- common_config('xmpp', 'password'),
- common_config('xmpp', 'resource') . 'daemon',
- common_config('xmpp', 'server'),
- common_config('xmpp', 'debug') ?
- true : false,
- common_config('xmpp', 'debug') ?
- XMPPHP_Log::LEVEL_VERBOSE : null);
- return $proxy;
+ if (common_config('queue', 'enabled')) {
+ $proxy = new Queued_XMPP(common_config('xmpp', 'host') ?
+ common_config('xmpp', 'host') :
+ common_config('xmpp', 'server'),
+ common_config('xmpp', 'port'),
+ common_config('xmpp', 'user'),
+ common_config('xmpp', 'password'),
+ common_config('xmpp', 'resource') . 'daemon',
+ common_config('xmpp', 'server'),
+ common_config('xmpp', 'debug') ?
+ true : false,
+ common_config('xmpp', 'debug') ?
+ XMPPHP_Log::LEVEL_VERBOSE : null);
+ return $proxy;
+ } else {
+ return jabber_connect();
+ }
}
/**
diff --git a/lib/language.php b/lib/language.php
index f5ee7fac5..6840148d2 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,9 +300,11 @@ 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'),
'de' => array('q' => 0.8, 'lang' => 'de', 'name' => 'German', 'direction' => 'ltr'),
@@ -300,7 +316,8 @@ 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'),
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 c724764cc..ab5742e33 100644
--- a/lib/mail.php
+++ b/lib/mail.php
@@ -133,12 +133,13 @@ function mail_notify_from()
* @param User &$user user to send email to
* @param string $subject subject of the email
* @param string $body body of the email
+ * @param array $headers optional list of email headers
* @param string $address optional specification of email address
*
* @return boolean success flag
*/
-function mail_to_user(&$user, $subject, $body, $address=null)
+function mail_to_user(&$user, $subject, $body, $headers=array(), $address=null)
{
if (!$address) {
$address = $user->email;
@@ -169,8 +170,10 @@ function mail_to_user(&$user, $subject, $body, $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, ".
@@ -180,7 +183,9 @@ function mail_confirm_address($user, $code, $nickname, $address)
$nickname, common_config('site', 'name'),
common_local_url('confirmaddress', array('code' => $code)),
common_config('site', 'name'));
- return mail_to_user($user, $subject, $body, $address);
+ $headers = array();
+
+ return mail_to_user($user, $subject, $body, $headers, $address);
}
/**
@@ -219,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();
@@ -231,13 +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'.
@@ -251,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);
}
}
@@ -283,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".
@@ -410,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());
@@ -436,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";
@@ -459,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".
@@ -475,8 +497,11 @@ function mail_notify_nudge($from, $to)
$from->nickname,
common_local_url('all', array('nickname' => $to->nickname)),
common_config('site', 'name'));
- common_init_locale();
- return mail_to_user($to, $subject, $body);
+ common_switch_locale();
+
+ $headers = _mail_prepare_headers('nudge', $to->nickname, $from->nickname);
+
+ return mail_to_user($to, $subject, $body, $headers);
}
/**
@@ -506,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".
@@ -526,8 +553,10 @@ function mail_notify_message($message, $from=null, $to=null)
common_local_url('newmessage', array('to' => $from->id)),
common_config('site', 'name'));
- common_init_locale();
- return mail_to_user($to, $subject, $body);
+ $headers = _mail_prepare_headers('message', $to->nickname, $from->nickname);
+
+ common_switch_locale();
+ return mail_to_user($to, $subject, $body, $headers);
}
/**
@@ -554,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" .
@@ -578,8 +609,10 @@ function mail_notify_fave($other, $user, $notice)
common_config('site', 'name'),
$user->nickname);
- common_init_locale();
- mail_to_user($other, $subject, $body);
+ $headers = _mail_prepare_headers('fave', $other->nickname, $user->nickname);
+
+ common_switch_locale();
+ mail_to_user($other, $subject, $body, $headers);
}
/**
@@ -609,26 +642,27 @@ function mail_notify_attn($user, $notice)
$bestname = $sender->getBestName();
- common_init_locale($user->language);
+ 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);
- $body = sprintf(_("%1\$s (@%9\$s) just sent a notice to your attention (an '@-reply') on %2\$s.\n\n".
+ // 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" .
@@ -641,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',
@@ -649,6 +683,30 @@ function mail_notify_attn($user, $notice)
common_local_url('emailsettings'), //%8
$sender->nickname); //%9
- common_init_locale();
- mail_to_user($user, $subject, $body);
+ $headers = _mail_prepare_headers('mention', $user->nickname, $sender->nickname);
+
+ common_switch_locale();
+ mail_to_user($user, $subject, $body, $headers);
}
+
+/**
+ * Prepare the common mail headers used in notification emails
+ *
+ * @param string $msg_type type of message being sent to the user
+ * @param string $to nickname of the receipient
+ * @param string $from nickname of the user triggering the notification
+ *
+ * @return array list of mail headers to include in the message
+ */
+function _mail_prepare_headers($msg_type, $to, $from)
+{
+ $headers = array(
+ 'X-StatusNet-MessageType' => $msg_type,
+ 'X-StatusNet-TargetUser' => $to,
+ 'X-StatusNet-SourceUser' => $from,
+ 'X-StatusNet-Domain' => common_config('site', 'server')
+ );
+
+ return $headers;
+}
+
diff --git a/lib/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 485096ac4..455695366 100644
--- a/lib/mysqlschema.php
+++ b/lib/mysqlschema.php
@@ -90,15 +90,24 @@ class MysqlSchema extends Schema
* @param string $name Name of the table to get
*
* @return TableDef tabledef for that table.
+ * @throws SchemaTableMissingException
*/
public function getTableDef($name)
{
- $res = $this->conn->query('DESCRIBE ' . $name);
+ $query = "SELECT * FROM INFORMATION_SCHEMA.COLUMNS " .
+ "WHERE TABLE_SCHEMA='%s' AND TABLE_NAME='%s'";
+ $schema = $this->conn->dsn['database'];
+ $sql = sprintf($query, $schema, $name);
+ $res = $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
+ if ($res->numRows() == 0) {
+ $res->free();
+ throw new SchemaTableMissingException("No such table: $name");
+ }
$td = new TableDef();
@@ -111,9 +120,9 @@ class MysqlSchema extends Schema
$cd = new ColumnDef();
- $cd->name = $row['Field'];
+ $cd->name = $row['COLUMN_NAME'];
- $packed = $row['Type'];
+ $packed = $row['COLUMN_TYPE'];
if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) {
$cd->type = $match[1];
@@ -122,18 +131,58 @@ class MysqlSchema extends Schema
$cd->type = $packed;
}
- $cd->nullable = ($row['Null'] == 'YES') ? true : false;
- $cd->key = $row['Key'];
- $cd->default = $row['Default'];
- $cd->extra = $row['Extra'];
+ $cd->nullable = ($row['IS_NULLABLE'] == 'YES') ? true : false;
+ $cd->key = $row['COLUMN_KEY'];
+ $cd->default = $row['COLUMN_DEFAULT'];
+ $cd->extra = $row['EXTRA'];
+
+ // Autoincrement is stuck into the extra column.
+ // Pull it out so we don't accidentally mod it every time...
+ $extra = preg_replace('/(^|\s)auto_increment(\s|$)/i', '$1$2', $cd->extra);
+ if ($extra != $cd->extra) {
+ $cd->extra = trim($extra);
+ $cd->auto_increment = true;
+ }
+
+ // mysql extensions -- not (yet) used by base class
+ $cd->charset = $row['CHARACTER_SET_NAME'];
+ $cd->collate = $row['COLLATION_NAME'];
$td->columns[] = $cd;
}
+ $res->free();
return $td;
}
/**
+ * Pull the given table properties from INFORMATION_SCHEMA.
+ * Most of the good stuff is MySQL extensions.
+ *
+ * @return array
+ * @throws Exception if table info can't be looked up
+ */
+
+ function getTableProperties($table, $props)
+ {
+ $query = "SELECT %s FROM INFORMATION_SCHEMA.TABLES " .
+ "WHERE TABLE_SCHEMA='%s' AND TABLE_NAME='%s'";
+ $schema = $this->conn->dsn['database'];
+ $sql = sprintf($query, implode(',', $props), $schema, $table);
+ $res = $this->conn->query($sql);
+
+ $row = array();
+ $ok = $res->fetchInto($row, DB_FETCHMODE_ASSOC);
+ $res->free();
+
+ if ($ok) {
+ return $row;
+ } else {
+ throw new SchemaTableMissingException("No such table: $table");
+ }
+ }
+
+ /**
* Gets a ColumnDef object for a single column.
*
* Throws an exception if the table is not found.
@@ -185,35 +234,26 @@ class MysqlSchema extends Schema
}
$sql .= $this->_columnSql($cd);
-
- switch ($cd->key) {
- case 'UNI':
- $uniques[] = $cd->name;
- break;
- case 'PRI':
- $primary[] = $cd->name;
- break;
- case 'MUL':
- $indices[] = $cd->name;
- break;
- }
}
- if (count($primary) > 0) { // it really should be...
- $sql .= ",\nconstraint primary key (" . implode(',', $primary) . ")";
+ $idx = $this->_indexList($columns);
+
+ if ($idx['primary']) {
+ $sql .= ",\nconstraint primary key (" . implode(',', $idx['primary']) . ")";
}
- foreach ($uniques as $u) {
- $sql .= ",\nunique index {$name}_{$u}_idx ($u)";
+ foreach ($idx['uniques'] as $u) {
+ $key = $this->_uniqueKey($name, $u);
+ $sql .= ",\nunique index $key ($u)";
}
- foreach ($indices as $i) {
- $sql .= ",\nindex {$name}_{$i}_idx ($i)";
+ foreach ($idx['indices'] as $i) {
+ $key = $this->_key($name, $i);
+ $sql .= ",\nindex $key ($i)";
}
- $sql .= "); ";
+ $sql .= ") ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; ";
- common_log(LOG_INFO, $sql);
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
@@ -224,6 +264,47 @@ class MysqlSchema extends Schema
}
/**
+ * Look over a list of column definitions and list up which
+ * indices will be present
+ */
+ private function _indexList(array $columns)
+ {
+ $list = array('uniques' => array(),
+ 'primary' => array(),
+ 'indices' => array());
+ foreach ($columns as $cd) {
+ switch ($cd->key) {
+ case 'UNI':
+ $list['uniques'][] = $cd->name;
+ break;
+ case 'PRI':
+ $list['primary'][] = $cd->name;
+ break;
+ case 'MUL':
+ $list['indices'][] = $cd->name;
+ break;
+ }
+ }
+ return $list;
+ }
+
+ /**
+ * Get the unique index key name for a given column on this table
+ */
+ function _uniqueKey($tableName, $columnName)
+ {
+ return $this->_key($tableName, $columnName);
+ }
+
+ /**
+ * Get the index key name for a given column on this table
+ */
+ function _key($tableName, $columnName)
+ {
+ return "{$tableName}_{$columnName}_idx";
+ }
+
+ /**
* Drops a table from the schema
*
* Throws an exception if the table is not found.
@@ -394,21 +475,20 @@ class MysqlSchema extends Schema
try {
$td = $this->getTableDef($tableName);
- } catch (Exception $e) {
- if (preg_match('/no such table/', $e->getMessage())) {
- return $this->createTable($tableName, $columns);
- } else {
- throw $e;
- }
+ } catch (SchemaTableMissingException $e) {
+ return $this->createTable($tableName, $columns);
}
$cur = $this->_names($td->columns);
$new = $this->_names($columns);
- $toadd = array_diff($new, $cur);
- $todrop = array_diff($cur, $new);
- $same = array_intersect($new, $cur);
- $tomod = array();
+ $dropIndex = array();
+ $toadd = array_diff($new, $cur);
+ $todrop = array_diff($cur, $new);
+ $same = array_intersect($new, $cur);
+ $tomod = array();
+ $addIndex = array();
+ $tableProps = array();
foreach ($same as $m) {
$curCol = $this->_byName($td->columns, $m);
@@ -416,10 +496,64 @@ class MysqlSchema extends Schema
if (!$newCol->equals($curCol)) {
$tomod[] = $newCol->name;
+ continue;
+ }
+
+ // Earlier versions may have accidentally left tables at default
+ // charsets which might be latin1 or other freakish things.
+ if ($this->_isString($curCol)) {
+ if ($curCol->charset != 'utf8') {
+ $tomod[] = $newCol->name;
+ continue;
+ }
+ }
+ }
+
+ // Find any indices we have to change...
+ $curIdx = $this->_indexList($td->columns);
+ $newIdx = $this->_indexList($columns);
+
+ if ($curIdx['primary'] != $newIdx['primary']) {
+ if ($curIdx['primary']) {
+ $dropIndex[] = 'drop primary key';
+ }
+ if ($newIdx['primary']) {
+ $keys = implode(',', $newIdx['primary']);
+ $addIndex[] = "add constraint primary key ($keys)";
}
}
- if (count($toadd) + count($todrop) + count($tomod) == 0) {
+ $dropUnique = array_diff($curIdx['uniques'], $newIdx['uniques']);
+ $addUnique = array_diff($newIdx['uniques'], $curIdx['uniques']);
+ foreach ($dropUnique as $columnName) {
+ $dropIndex[] = 'drop key ' . $this->_uniqueKey($tableName, $columnName);
+ }
+ foreach ($addUnique as $columnName) {
+ $addIndex[] = 'add constraint unique key ' . $this->_uniqueKey($tableName, $columnName) . " ($columnName)";;
+ }
+
+ $dropMultiple = array_diff($curIdx['indices'], $newIdx['indices']);
+ $addMultiple = array_diff($newIdx['indices'], $curIdx['indices']);
+ foreach ($dropMultiple as $columnName) {
+ $dropIndex[] = 'drop key ' . $this->_key($tableName, $columnName);
+ }
+ foreach ($addMultiple as $columnName) {
+ $addIndex[] = 'add key ' . $this->_key($tableName, $columnName) . " ($columnName)";
+ }
+
+ // Check for table properties: make sure we're using a sane
+ // engine type and charset/collation.
+ // @fixme make the default engine configurable?
+ $oldProps = $this->getTableProperties($tableName, array('ENGINE', 'TABLE_COLLATION'));
+ if (strtolower($oldProps['ENGINE']) != 'innodb') {
+ $tableProps['ENGINE'] = 'InnoDB';
+ }
+ if (strtolower($oldProps['TABLE_COLLATION']) != 'utf8_bin') {
+ $tableProps['DEFAULT CHARSET'] = 'utf8';
+ $tableProps['COLLATE'] = 'utf8_bin';
+ }
+
+ if (count($dropIndex) + count($toadd) + count($todrop) + count($tomod) + count($addIndex) + count($tableProps) == 0) {
// nothing to do
return true;
}
@@ -429,6 +563,10 @@ class MysqlSchema extends Schema
$phrase = array();
+ foreach ($dropIndex as $indexSql) {
+ $phrase[] = $indexSql;
+ }
+
foreach ($toadd as $columnName) {
$cd = $this->_byName($columns, $columnName);
@@ -445,8 +583,17 @@ class MysqlSchema extends Schema
$phrase[] = 'MODIFY COLUMN ' . $this->_columnSql($cd);
}
+ foreach ($addIndex as $indexSql) {
+ $phrase[] = $indexSql;
+ }
+
+ foreach ($tableProps as $key => $val) {
+ $phrase[] = "$key=$val";
+ }
+
$sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase);
+ common_log(LOG_DEBUG, __METHOD__ . ': ' . $sql);
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
@@ -519,6 +666,10 @@ class MysqlSchema extends Schema
$sql .= "{$cd->type} ";
}
+ if ($this->_isString($cd)) {
+ $sql .= " CHARACTER SET utf8 ";
+ }
+
if (!empty($cd->default)) {
$sql .= "default {$cd->default} ";
} else {
@@ -535,4 +686,13 @@ class MysqlSchema extends Schema
return $sql;
}
+
+ /**
+ * Is this column a string type?
+ */
+ private function _isString(ColumnDef $cd)
+ {
+ $strings = array('char', 'varchar', 'text');
+ return in_array(strtolower($cd->type), $strings);
+ }
}
diff --git a/lib/noticeform.php b/lib/noticeform.php
index 7278c41a9..84c20a5b3 100644
--- a/lib/noticeform.php
+++ b/lib/noticeform.php
@@ -212,8 +212,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 88a925241..e23cf3b6d 100644
--- a/lib/noticelist.php
+++ b/lib/noticelist.php
@@ -340,8 +340,9 @@ class NoticeListItem extends Widget
function showNickname()
{
- $this->out->element('span', array('class' => 'nickname fn'),
- $this->profile->nickname);
+ $this->out->raw('<span class="nickname fn">' .
+ htmlspecialchars($this->profile->nickname) .
+ '</span>');
}
/**
@@ -425,10 +426,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();
@@ -442,21 +451,26 @@ class NoticeListItem extends Widget
'title' => $latlon),
$name);
} else {
- $this->out->elementStart('a', array('href' => $url));
- $this->out->element('abbr', array('class' => 'geo',
- 'title' => $latlon),
- $name);
- $this->out->elementEnd('a');
+ $xstr = new XMLStringer(false);
+ $xstr->elementStart('a', array('href' => $url,
+ 'rel' => 'external'));
+ $xstr->element('abbr', array('class' => 'geo',
+ 'title' => $latlon),
+ $name);
+ $xstr->elementEnd('a');
+ $this->out->raw($xstr->getString());
}
$this->out->elementEnd('span');
}
+ /**
+ * @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);
@@ -476,54 +490,46 @@ class NoticeListItem extends Widget
function showNoticeSource()
{
- if ($this->notice->source) {
+ $ns = $this->notice->getSource();
+
+ if ($ns) {
+ $source_name = _($ns->code);
$this->out->text(' ');
$this->out->elementStart('span', 'source');
$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');
}
}
@@ -539,18 +545,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 91bc09667..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/
*/
@@ -61,7 +62,8 @@ class PgsqlSchema extends Schema
public function getTableDef($name)
{
- $res = $this->conn->query("select *, column_default as default, is_nullable as Null, udt_name as Type, column_name AS Field from INFORMATION_SCHEMA.COLUMNS where table_name = '$name'");
+ $res = $this->conn->query("SELECT *, column_default as default, is_nullable as Null,
+ udt_name as Type, column_name AS Field from INFORMATION_SCHEMA.COLUMNS where table_name = '$name'");
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
@@ -72,10 +74,12 @@ class PgsqlSchema extends Schema
$td->name = $name;
$td->columns = array();
+ if ($res->numRows() == 0 ) {
+ throw new Exception('no such table'); //pretend to be the msyql error. yeah, this sucks.
+ }
$row = array();
while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
-// var_dump($row);
$cd = new ColumnDef();
$cd->name = $row['field'];
@@ -139,6 +143,7 @@ class PgsqlSchema extends Schema
$uniques = array();
$primary = array();
$indices = array();
+ $onupdate = array();
$sql = "CREATE TABLE $name (\n";
@@ -151,7 +156,6 @@ class PgsqlSchema extends Schema
}
$sql .= $this->_columnSql($cd);
-
switch ($cd->key) {
case 'UNI':
$uniques[] = $cd->name;
@@ -166,23 +170,23 @@ class PgsqlSchema extends Schema
}
if (count($primary) > 0) { // it really should be...
- $sql .= ",\nconstraint primary key (" . implode(',', $primary) . ")";
+ $sql .= ",\n PRIMARY KEY (" . implode(',', $primary) . ")";
}
+ $sql .= "); ";
+
+
foreach ($uniques as $u) {
- $sql .= ",\nunique index {$name}_{$u}_idx ($u)";
+ $sql .= "\n CREATE index {$name}_{$u}_idx ON {$name} ($u); ";
}
foreach ($indices as $i) {
- $sql .= ",\nindex {$name}_{$i}_idx ($i)";
+ $sql .= "CREATE index {$name}_{$i}_idx ON {$name} ($i)";
}
-
- $sql .= "); ";
-
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
- throw new Exception($res->getMessage());
+ throw new Exception($res->getMessage(). ' SQL was '. $sql);
}
return true;
@@ -210,6 +214,22 @@ class PgsqlSchema extends Schema
}
/**
+ * Translate the (mostly) mysql-ish column types into somethings more standard
+ * @param string column type
+ *
+ * @return string postgres happy column type
+ */
+ private function _columnTypeTranslation($type) {
+ $map = array(
+ 'datetime' => 'timestamp',
+ );
+ if(!empty($map[$type])) {
+ return $map[$type];
+ }
+ return $type;
+ }
+
+ /**
* Adds an index to a table.
*
* If no name is provided, a name will be made up based
@@ -302,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);
@@ -359,6 +379,7 @@ class PgsqlSchema extends Schema
try {
$td = $this->getTableDef($tableName);
+
} catch (Exception $e) {
if (preg_match('/no such table/', $e->getMessage())) {
return $this->createTable($tableName, $columns);
@@ -374,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;
@@ -407,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)) {
@@ -473,15 +496,25 @@ 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 .= "{$cd->type}({$cd->size}) ";
+ $sql .= "{$type}({$cd->size}) ";
} else {
- $sql .= "{$cd->type} ";
+ $sql .= "{$type} ";
}
if (!empty($cd->default)) {
@@ -489,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/poco.php b/lib/poco.php
new file mode 100644
index 000000000..2157062b3
--- /dev/null
+++ b/lib/poco.php
@@ -0,0 +1,240 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+class PoCo
+{
+ const NS = 'http://portablecontacts.net/spec/1.0';
+
+ const USERNAME = 'preferredUsername';
+ const DISPLAYNAME = 'displayName';
+ const NOTE = 'note';
+
+ public $preferredUsername;
+ public $displayName;
+ public $note;
+ public $address;
+ public $urls = array();
+
+ function __construct($element = null)
+ {
+ if (empty($element)) {
+ return;
+ }
+
+ $this->preferredUsername = ActivityUtils::childContent(
+ $element,
+ self::USERNAME,
+ self::NS
+ );
+
+ $this->displayName = ActivityUtils::childContent(
+ $element,
+ self::DISPLAYNAME,
+ self::NS
+ );
+
+ $this->note = ActivityUtils::childContent(
+ $element,
+ self::NOTE,
+ self::NS
+ );
+
+ $this->address = $this->_getAddress($element);
+ $this->urls = $this->_getURLs($element);
+ }
+
+ private function _getURLs($element)
+ {
+ $urlEls = $element->getElementsByTagnameNS(self::NS, PoCoURL::URLS);
+ $urls = array();
+
+ foreach ($urlEls as $urlEl) {
+
+ $type = ActivityUtils::childContent(
+ $urlEl,
+ PoCoURL::TYPE,
+ PoCo::NS
+ );
+
+ $value = ActivityUtils::childContent(
+ $urlEl,
+ PoCoURL::VALUE,
+ PoCo::NS
+ );
+
+ $primary = ActivityUtils::childContent(
+ $urlEl,
+ PoCoURL::PRIMARY,
+ PoCo::NS
+ );
+
+ $isPrimary = false;
+
+ if (isset($primary) && $primary == 'true') {
+ $isPrimary = true;
+ }
+
+ // @todo check to make sure a primary hasn't already been added
+
+ array_push($urls, new PoCoURL($type, $value, $isPrimary));
+ }
+ return $urls;
+ }
+
+ private function _getAddress($element)
+ {
+ $addressEl = ActivityUtils::child(
+ $element,
+ PoCoAddress::ADDRESS,
+ PoCo::NS
+ );
+
+ if (!empty($addressEl)) {
+ $formatted = ActivityUtils::childContent(
+ $addressEl,
+ PoCoAddress::FORMATTED,
+ self::NS
+ );
+
+ if (!empty($formatted)) {
+ $address = new PoCoAddress();
+ $address->formatted = $formatted;
+ return $address;
+ }
+ }
+
+ return null;
+ }
+
+ function fromProfile($profile)
+ {
+ if (empty($profile)) {
+ return null;
+ }
+
+ $poco = new PoCo();
+
+ $poco->preferredUsername = $profile->nickname;
+ $poco->displayName = $profile->getBestName();
+
+ $poco->note = $profile->bio;
+
+ $paddy = new PoCoAddress();
+ $paddy->formatted = $profile->location;
+ $poco->address = $paddy;
+
+ if (!empty($profile->homepage)) {
+ array_push(
+ $poco->urls,
+ new PoCoURL(
+ 'homepage',
+ $profile->homepage,
+ true
+ )
+ );
+ }
+
+ return $poco;
+ }
+
+ function fromGroup($group)
+ {
+ if (empty($group)) {
+ return null;
+ }
+
+ $poco = new PoCo();
+
+ $poco->preferredUsername = $group->nickname;
+ $poco->displayName = $group->getBestName();
+
+ $poco->note = $group->description;
+
+ $paddy = new PoCoAddress();
+ $paddy->formatted = $group->location;
+ $poco->address = $paddy;
+
+ if (!empty($group->homepage)) {
+ array_push(
+ $poco->urls,
+ new PoCoURL(
+ 'homepage',
+ $group->homepage,
+ true
+ )
+ );
+ }
+
+ return $poco;
+ }
+
+ function getPrimaryURL()
+ {
+ foreach ($this->urls as $url) {
+ if ($url->primary) {
+ return $url;
+ }
+ }
+ }
+
+ function asString()
+ {
+ $xs = new XMLStringer(true);
+ $xs->element(
+ 'poco:preferredUsername',
+ null,
+ $this->preferredUsername
+ );
+
+ $xs->element(
+ 'poco:displayName',
+ null,
+ $this->displayName
+ );
+
+ if (!empty($this->note)) {
+ $xs->element('poco:note', null, common_xml_safe_str($this->note));
+ }
+
+ if (!empty($this->address)) {
+ $xs->raw($this->address->asString());
+ }
+
+ foreach ($this->urls as $url) {
+ $xs->raw($url->asString());
+ }
+
+ return $xs->getString();
+ }
+}
diff --git a/lib/pocoaddress.php b/lib/pocoaddress.php
new file mode 100644
index 000000000..60873bdc4
--- /dev/null
+++ b/lib/pocoaddress.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+class PoCoAddress
+{
+ const ADDRESS = 'address';
+ const FORMATTED = 'formatted';
+
+ public $formatted;
+
+ // @todo Other address fields
+
+ function asString()
+ {
+ if (!empty($this->formatted)) {
+ $xs = new XMLStringer(true);
+ $xs->elementStart('poco:address');
+ $xs->element('poco:formatted', null, common_xml_safe_str($this->formatted));
+ $xs->elementEnd('poco:address');
+ return $xs->getString();
+ }
+
+ return null;
+ }
+}
diff --git a/lib/pocourl.php b/lib/pocourl.php
new file mode 100644
index 000000000..803484d76
--- /dev/null
+++ b/lib/pocourl.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+class PoCoURL
+{
+ const URLS = 'urls';
+ const TYPE = 'type';
+ const VALUE = 'value';
+ const PRIMARY = 'primary';
+
+ public $type;
+ public $value;
+ public $primary;
+
+ function __construct($type, $value, $primary = false)
+ {
+ $this->type = $type;
+ $this->value = $value;
+ $this->primary = $primary;
+ }
+
+ function asString()
+ {
+ $xs = new XMLStringer(true);
+ $xs->elementStart('poco:urls');
+ $xs->element('poco:type', null, $this->type);
+ $xs->element('poco:value', null, $this->value);
+ if (!empty($this->primary)) {
+ $xs->element('poco:primary', null, 'true');
+ }
+ $xs->elementEnd('poco:urls');
+ return $xs->getString();
+ }
+}
diff --git a/lib/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/processmanager.php b/lib/processmanager.php
new file mode 100644
index 000000000..6032bfc5c
--- /dev/null
+++ b/lib/processmanager.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * i/o manager to watch for a dead parent process
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category QueueManager
+ * @package StatusNet
+ * @author Brion Vibber <brion@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class ProcessManager extends IoManager
+{
+ protected $socket;
+
+ public static function get()
+ {
+ throw new Exception("Must pass ProcessManager per-instance");
+ }
+
+ public function __construct($socket)
+ {
+ $this->socket = $socket;
+ }
+
+ /**
+ * Tell the i/o queue master if and how we can handle multi-site
+ * processes.
+ *
+ * Return one of:
+ * IoManager::SINGLE_ONLY
+ * IoManager::INSTANCE_PER_SITE
+ * IoManager::INSTANCE_PER_PROCESS
+ */
+ public static function multiSite()
+ {
+ return IoManager::INSTANCE_PER_PROCESS;
+ }
+
+ /**
+ * We won't get any input on it, but if it's broken we'll
+ * know something's gone horribly awry.
+ *
+ * @return array of resources
+ */
+ function getSockets()
+ {
+ return array($this->socket);
+ }
+
+ /**
+ * See if the parent died and request a shutdown...
+ *
+ * @param resource $socket
+ * @return boolean success
+ */
+ function handleInput($socket)
+ {
+ if (feof($socket)) {
+ common_log(LOG_INFO, "Parent process exited; shutting down child.");
+ $this->master->requestShutdown();
+ }
+ return true;
+ }
+}
+
diff --git a/lib/profileaction.php b/lib/profileaction.php
index 2d4d23265..504b77566 100644
--- a/lib/profileaction.php
+++ b/lib/profileaction.php
@@ -105,28 +105,30 @@ class ProfileAction extends OwnerDesignAction
$this->elementStart('div', array('id' => 'entity_subscriptions',
'class' => 'section'));
+ if (Event::handle('StartShowSubscriptionsMiniList', array($this))) {
+ $this->element('h2', null, _('Subscriptions'));
- $this->element('h2', null, _('Subscriptions'));
+ $cnt = 0;
- $cnt = 0;
+ if (!empty($profile)) {
+ $pml = new ProfileMiniList($profile, $this);
+ $cnt = $pml->show();
+ if ($cnt == 0) {
+ $this->element('p', null, _('(None)'));
+ }
+ }
- if (!empty($profile)) {
- $pml = new ProfileMiniList($profile, $this);
- $cnt = $pml->show();
- if ($cnt == 0) {
- $this->element('p', null, _('(None)'));
+ if ($cnt > PROFILES_PER_MINILIST) {
+ $this->elementStart('p');
+ $this->element('a', array('href' => common_local_url('subscriptions',
+ array('nickname' => $this->profile->nickname)),
+ 'class' => 'more'),
+ _('All subscriptions'));
+ $this->elementEnd('p');
}
- }
- if ($cnt > PROFILES_PER_MINILIST) {
- $this->elementStart('p');
- $this->element('a', array('href' => common_local_url('subscriptions',
- array('nickname' => $this->profile->nickname)),
- 'class' => 'more'),
- _('All subscriptions'));
- $this->elementEnd('p');
+ Event::handle('EndShowSubscriptionsMiniList', array($this));
}
-
$this->elementEnd('div');
}
@@ -137,25 +139,30 @@ class ProfileAction extends OwnerDesignAction
$this->elementStart('div', array('id' => 'entity_subscribers',
'class' => 'section'));
- $this->element('h2', null, _('Subscribers'));
+ if (Event::handle('StartShowSubscribersMiniList', array($this))) {
- $cnt = 0;
+ $this->element('h2', null, _('Subscribers'));
- if (!empty($profile)) {
- $pml = new ProfileMiniList($profile, $this);
- $cnt = $pml->show();
- if ($cnt == 0) {
- $this->element('p', null, _('(None)'));
+ $cnt = 0;
+
+ if (!empty($profile)) {
+ $sml = new SubscribersMiniList($profile, $this);
+ $cnt = $sml->show();
+ if ($cnt == 0) {
+ $this->element('p', null, _('(None)'));
+ }
+ }
+
+ if ($cnt > PROFILES_PER_MINILIST) {
+ $this->elementStart('p');
+ $this->element('a', array('href' => common_local_url('subscribers',
+ array('nickname' => $this->profile->nickname)),
+ 'class' => 'more'),
+ _('All subscribers'));
+ $this->elementEnd('p');
}
- }
- if ($cnt > PROFILES_PER_MINILIST) {
- $this->elementStart('p');
- $this->element('a', array('href' => common_local_url('subscribers',
- array('nickname' => $this->profile->nickname)),
- 'class' => 'more'),
- _('All subscribers'));
- $this->elementEnd('p');
+ Event::handle('EndShowSubscribersMiniList', array($this));
}
$this->elementEnd('div');
@@ -167,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'));
@@ -217,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');
}
@@ -226,27 +245,49 @@ class ProfileAction extends OwnerDesignAction
$this->elementStart('div', array('id' => 'entity_groups',
'class' => 'section'));
+ if (Event::handle('StartShowGroupsMiniList', array($this))) {
+ $this->element('h2', null, _('Groups'));
+
+ if ($groups) {
+ $gml = new GroupMiniList($groups, $this->user, $this);
+ $cnt = $gml->show();
+ if ($cnt == 0) {
+ $this->element('p', null, _('(None)'));
+ }
+ }
- $this->element('h2', null, _('Groups'));
-
- if ($groups) {
- $gml = new GroupMiniList($groups, $this->user, $this);
- $cnt = $gml->show();
- if ($cnt == 0) {
- $this->element('p', null, _('(None)'));
+ if ($cnt > GROUPS_PER_MINILIST) {
+ $this->elementStart('p');
+ $this->element('a', array('href' => common_local_url('usergroups',
+ array('nickname' => $this->profile->nickname)),
+ 'class' => 'more'),
+ _('All groups'));
+ $this->elementEnd('p');
}
- }
- if ($cnt > GROUPS_PER_MINILIST) {
- $this->elementStart('p');
- $this->element('a', array('href' => common_local_url('usergroups',
- array('nickname' => $this->profile->nickname)),
- 'class' => 'more'),
- _('All groups'));
- $this->elementEnd('p');
+ Event::handle('EndShowGroupsMiniList', array($this));
}
+ $this->elementEnd('div');
+ }
+}
- $this->elementEnd('div');
+class SubscribersMiniList extends ProfileMiniList
+{
+ function newListItem($profile)
+ {
+ return new SubscribersMiniListItem($profile, $this->action);
+ }
+}
+
+class SubscribersMiniListItem extends ProfileMiniListItem
+{
+ function linkAttributes()
+ {
+ $aAttrs = parent::linkAttributes();
+ if (common_config('nofollow', 'subscribers')) {
+ $aAttrs['rel'] .= ' nofollow';
+ }
+ return $aAttrs;
}
}
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/profilelist.php b/lib/profilelist.php
index 4f1e84a6a..b010fb724 100644
--- a/lib/profilelist.php
+++ b/lib/profilelist.php
@@ -181,9 +181,8 @@ class ProfileListItem extends Widget
function showAvatar()
{
$avatar = $this->profile->getAvatar(AVATAR_STREAM_SIZE);
- $this->out->elementStart('a', array('href' => $this->profile->profileurl,
- 'class' => 'url entry-title',
- 'rel' => 'contact'));
+ $aAttrs = $this->linkAttributes();
+ $this->out->elementStart('a', $aAttrs);
$this->out->element('img', array('src' => ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_STREAM_SIZE),
'class' => 'photo avatar',
'width' => AVATAR_STREAM_SIZE,
@@ -213,7 +212,7 @@ class ProfileListItem extends Widget
{
if (!empty($this->profile->location)) {
$this->out->text(' ');
- $this->out->elementStart('span', 'location');
+ $this->out->elementStart('span', 'label');
$this->out->raw($this->highlight($this->profile->location));
$this->out->elementEnd('span');
}
@@ -223,8 +222,8 @@ class ProfileListItem extends Widget
{
if (!empty($this->profile->homepage)) {
$this->out->text(' ');
- $this->out->elementStart('a', array('href' => $this->profile->homepage,
- 'class' => 'url'));
+ $aAttrs = $this->homepageAttributes();
+ $this->out->elementStart('a', $aAttrs);
$this->out->raw($this->highlight($this->profile->homepage));
$this->out->elementEnd('a');
}
@@ -273,18 +272,12 @@ class ProfileListItem extends Widget
$usf = new UnsubscribeForm($this->out, $this->profile);
$usf->show();
} else {
- $other = User::staticGet('id', $this->profile->id);
- if (!empty($other)) {
+ // We can't initiate sub for a remote OMB profile.
+ $remote = Remote_profile::staticGet('id', $this->profile->id);
+ if (empty($remote)) {
$sf = new SubscribeForm($this->out, $this->profile);
$sf->show();
}
- else {
- $url = common_local_url('remotesubscribe',
- array('nickname' => $this->profile->nickname));
- $this->out->element('a', array('href' => $url,
- 'class' => 'entity_remote_subscribe'),
- _('Subscribe'));
- }
}
$this->out->elementEnd('li');
}
@@ -305,4 +298,17 @@ class ProfileListItem extends Widget
{
return htmlspecialchars($text);
}
+
+ function linkAttributes()
+ {
+ return array('href' => $this->profile->profileurl,
+ 'class' => 'url entry-title',
+ 'rel' => 'contact');
+ }
+
+ function homepageAttributes()
+ {
+ return array('href' => $this->profile->homepage,
+ 'class' => 'url');
+ }
}
diff --git a/lib/profileminilist.php b/lib/profileminilist.php
index 079170d80..a98953474 100644
--- a/lib/profileminilist.php
+++ b/lib/profileminilist.php
@@ -81,20 +81,36 @@ class ProfileMiniListItem extends ProfileListItem
function show()
{
$this->out->elementStart('li', 'vcard');
- $this->out->elementStart('a', array('title' => $this->profile->getBestName(),
- 'href' => $this->profile->profileurl,
- 'rel' => 'contact member',
- 'class' => 'url'));
- $avatar = $this->profile->getAvatar(AVATAR_MINI_SIZE);
- $this->out->element('img', array('src' => (($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_MINI_SIZE)),
- 'width' => AVATAR_MINI_SIZE,
- 'height' => AVATAR_MINI_SIZE,
- 'class' => 'avatar photo',
- 'alt' => ($this->profile->fullname) ?
- $this->profile->fullname :
- $this->profile->nickname));
- $this->out->element('span', 'fn nickname', $this->profile->nickname);
- $this->out->elementEnd('a');
- $this->out->elementEnd('li');
+ if (Event::handle('StartProfileListItemProfileElements', array($this))) {
+ if (Event::handle('StartProfileListItemAvatar', array($this))) {
+ $aAttrs = $this->linkAttributes();
+ $this->out->elementStart('a', $aAttrs);
+ $avatar = $this->profile->getAvatar(AVATAR_MINI_SIZE);
+ $this->out->element('img', array('src' => (($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_MINI_SIZE)),
+ 'width' => AVATAR_MINI_SIZE,
+ 'height' => AVATAR_MINI_SIZE,
+ 'class' => 'avatar photo',
+ 'alt' => ($this->profile->fullname) ?
+ $this->profile->fullname :
+ $this->profile->nickname));
+ $this->out->element('span', 'fn nickname', $this->profile->nickname);
+ $this->out->elementEnd('a');
+ Event::handle('EndProfileListItemAvatar', array($this));
+ }
+ $this->out->elementEnd('li');
+ }
+ }
+
+ // default; overridden for nofollow lists
+
+ function linkAttributes()
+ {
+ $aAttrs = parent::linkAttributes();
+
+ $aAttrs['title'] = $this->profile->getBestName();
+ $aAttrs['rel'] = 'contact member'; // @todo: member? always?
+ $aAttrs['class'] = 'url';
+
+ return $aAttrs;
}
}
diff --git a/lib/queued_xmpp.php b/lib/queued_xmpp.php
index fdd074db2..f6bccfd5b 100644
--- a/lib/queued_xmpp.php
+++ b/lib/queued_xmpp.php
@@ -49,10 +49,20 @@ class Queued_XMPP extends XMPPHP_XMPP
*/
public function __construct($host, $port, $user, $password, $resource, $server = null, $printlog = false, $loglevel = null)
{
- parent::__construct($host, $port, $user, $password, $resource, $server, $printlog, $loglevel);
- // Normally the fulljid isn't filled out until resource binding time;
- // we need to save it here since we're not talking to a real server.
- $this->fulljid = "{$this->basejid}/{$this->resource}";
+ parent::__construct($host, $port, $user, $password, $resource, $server, $printlog, $loglevel);
+
+ // We use $host to connect, but $server to build JIDs if specified.
+ // This seems to fix an upstream bug where $host was used to build
+ // $this->basejid, never seen since it isn't actually used in the base
+ // classes.
+ if (!$server) {
+ $server = $this->host;
+ }
+ $this->basejid = $this->user . '@' . $server;
+
+ // Normally the fulljid is filled out by the server at resource binding
+ // time, but we need to do it since we're not talking to a real server.
+ $this->fulljid = "{$this->basejid}/{$this->resource}";
}
/**
diff --git a/lib/queuemanager.php b/lib/queuemanager.php
index 87bd356aa..0829c8a8b 100644
--- a/lib/queuemanager.php
+++ b/lib/queuemanager.php
@@ -264,6 +264,9 @@ abstract class QueueManager extends IoManager
$this->connect('sms', 'SmsQueueHandler');
}
+ // Background user management tasks...
+ $this->connect('deluser', 'DelUserQueueHandler');
+
// Broadcasting profile updates to OMB remote subscribers
$this->connect('profile', 'ProfileQueueHandler');
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/revokeroleform.php b/lib/revokeroleform.php
new file mode 100644
index 000000000..ec24b9910
--- /dev/null
+++ b/lib/revokeroleform.php
@@ -0,0 +1,93 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Form for revoking a role
+ *
+ * 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 Form
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>, Brion Vibber <brion@status.net>
+ * @copyright 2009-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')) {
+ exit(1);
+}
+
+/**
+ * Form for sandboxing a user
+ *
+ * @category Form
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ *
+ * @see UnSandboxForm
+ */
+
+class RevokeRoleForm extends ProfileActionForm
+{
+ function __construct($role, $label, $writer, $profile, $r2args)
+ {
+ parent::__construct($writer, $profile, $r2args);
+ $this->role = $role;
+ $this->label = $label;
+ }
+
+ /**
+ * Action this form provides
+ *
+ * @return string Name of the action, lowercased.
+ */
+
+ function target()
+ {
+ return 'revokerole';
+ }
+
+ /**
+ * Title of the form
+ *
+ * @return string Title of the form, internationalized
+ */
+
+ function title()
+ {
+ return $this->label;
+ }
+
+ function formData()
+ {
+ parent::formData();
+ $this->out->hidden('role', $this->role);
+ }
+
+ /**
+ * Description of the form
+ *
+ * @return string description of the form, internationalized
+ */
+
+ function description()
+ {
+ return sprintf(_('Revoke the "%s" role from this user'), $this->label);
+ }
+}
diff --git a/lib/right.php b/lib/right.php
index 4e9c5a918..deb451fde 100644
--- a/lib/right.php
+++ b/lib/right.php
@@ -58,5 +58,7 @@ class Right
const EMAILONSUBSCRIBE = 'emailonsubscribe';
const EMAILONFAVE = 'emailonfave';
const MAKEGROUPADMIN = 'makegroupadmin';
+ const GRANTROLE = 'grantrole';
+ const REVOKEROLE = 'revokerole';
}
diff --git a/lib/router.php b/lib/router.php
index abbce041d..7e1e6a2a4 100644
--- a/lib/router.php
+++ b/lib/router.php
@@ -33,6 +33,33 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
require_once 'Net/URL/Mapper.php';
+class StatusNet_URL_Mapper extends Net_URL_Mapper {
+
+ private static $_singleton = null;
+
+ private function __construct()
+ {
+ }
+
+ public static function getInstance($id = '__default__')
+ {
+ if (empty(self::$_singleton)) {
+ self::$_singleton = new StatusNet_URL_Mapper();
+ }
+ return self::$_singleton;
+ }
+
+ public function connect($path, $defaults = array(), $rules = array())
+ {
+ $result = null;
+ if (Event::handle('StartConnectPath', array(&$path, &$defaults, &$rules, &$result))) {
+ $result = parent::connect($path, $defaults, $rules);
+ Event::handle('EndConnectPath', array($path, $defaults, $rules, $result));
+ }
+ return $result;
+ }
+}
+
/**
* URL Router
*
@@ -69,7 +96,7 @@ class Router
function initialize()
{
- $m = Net_URL_Mapper::getInstance();
+ $m = StatusNet_URL_Mapper::getInstance();
if (Event::handle('StartInitializeRouter', array(&$m))) {
@@ -98,6 +125,7 @@ class Router
'groupblock', 'groupunblock',
'sandbox', 'unsandbox',
'silence', 'unsilence',
+ 'grantrole', 'revokerole',
'repeat',
'deleteuser',
'geocode',
@@ -108,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]+'));
@@ -230,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}'));
@@ -507,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',
@@ -564,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',
@@ -625,12 +658,18 @@ 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(
+ 'api/statusnet/media/upload',
+ array('action' => 'ApiMediaUpload')
+ );
// 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'));
@@ -649,6 +688,8 @@ class Router
$m->connect('admin/access', array('action' => 'accessadminpanel'));
$m->connect('admin/paths', array('action' => 'pathsadminpanel'));
$m->connect('admin/sessions', array('action' => 'sessionsadminpanel'));
+ $m->connect('admin/sitenotice', array('action' => 'sitenoticeadminpanel'));
+ $m->connect('admin/snapshot', array('action' => 'snapshotadminpanel'));
$m->connect('getfile/:filename',
array('action' => 'getfile'),
@@ -708,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',
@@ -774,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/schema.php b/lib/schema.php
index 137b814e0..1503c96d4 100644
--- a/lib/schema.php
+++ b/lib/schema.php
@@ -485,3 +485,9 @@ class Schema
return $sql;
}
}
+
+class SchemaTableMissingException extends Exception
+{
+ // no-op
+}
+
diff --git a/lib/servererroraction.php b/lib/servererroraction.php
index 0993a63bc..9b5a553dc 100644
--- a/lib/servererroraction.php
+++ b/lib/servererroraction.php
@@ -62,15 +62,18 @@ class ServerErrorAction extends ErrorAction
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported');
- function __construct($message='Error', $code=500)
+ function __construct($message='Error', $code=500, $ex=null)
{
parent::__construct($message, $code);
$this->default = 500;
// Server errors must be logged.
-
- common_log(LOG_ERR, "ServerErrorAction: $code $message");
+ $log = "ServerErrorAction: $code $message";
+ if ($ex) {
+ $log .= "\n" . $ex->getTraceAsString();
+ }
+ common_log(LOG_ERR, $log);
}
// XXX: Should these error actions even be invokable via URI?
diff --git a/lib/spawningdaemon.php b/lib/spawningdaemon.php
index fd9ae4355..2f9f6e32e 100644
--- a/lib/spawningdaemon.php
+++ b/lib/spawningdaemon.php
@@ -71,6 +71,8 @@ abstract class SpawningDaemon extends Daemon
*/
function run()
{
+ $this->initPipes();
+
$children = array();
for ($i = 1; $i <= $this->threads; $i++) {
$pid = pcntl_fork();
@@ -129,6 +131,34 @@ abstract class SpawningDaemon extends Daemon
}
/**
+ * Create an IPC socket pair which child processes can use to detect
+ * if the parent process has been killed.
+ */
+ function initPipes()
+ {
+ $sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0);
+ if ($sockets) {
+ $this->parentWriter = $sockets[0];
+ $this->parentReader = $sockets[1];
+ } else {
+ $this->log(LOG_ERROR, "Couldn't create inter-process sockets");
+ exit(1);
+ }
+ }
+
+ /**
+ * Build an IOManager that simply ensures that we have a connection
+ * to the parent process open. If it breaks, the child process will
+ * die.
+ *
+ * @return ProcessManager
+ */
+ public function processManager()
+ {
+ return new ProcessManager($this->parentReader);
+ }
+
+ /**
* Determine whether to respawn an exited subprocess based on its exit code.
* Otherwise we'll respawn all exits by default.
*
@@ -152,6 +182,8 @@ abstract class SpawningDaemon extends Daemon
*/
protected function initAndRunChild($thread)
{
+ // Close the writer end of our parent<->children pipe.
+ fclose($this->parentWriter);
$this->set_id($this->get_id() . "." . $thread);
$this->resetDb();
$exitCode = $this->runThread();
diff --git a/lib/statusnet.php b/lib/statusnet.php
index 7c4df84b4..2aa73486e 100644
--- a/lib/statusnet.php
+++ b/lib/statusnet.php
@@ -31,6 +31,7 @@ class StatusNet
{
protected static $have_config;
protected static $is_api;
+ protected static $plugins = array();
/**
* Configure and instantiate a plugin into the current configuration.
@@ -74,10 +75,23 @@ class StatusNet
$inst->$aname = $avalue;
}
}
+
+ // Record activated plugins for later display/config dump
+ self::$plugins[] = array($name, $attrs);
+
return true;
}
/**
+ * Get a list of activated plugins in this process.
+ * @return array of (string $name, array $args) pairs
+ */
+ public static function getActivePlugins()
+ {
+ return self::$plugins;
+ }
+
+ /**
* Initialize, or re-initialize, StatusNet global configuration
* and plugins.
*
@@ -232,6 +246,7 @@ class StatusNet
global $_server, $_path, $config;
Event::clearHandlers();
+ self::$plugins = array();
// try to figure out where we are. $server and $path
// can be set by including module, else we guess based
@@ -354,10 +369,10 @@ class StatusNet
class NoConfigException extends Exception
{
- public $config_files;
+ public $configFiles;
- function __construct($msg, $config_files) {
+ function __construct($msg, $configFiles) {
parent::__construct($msg);
- $this->config_files = $config_files;
+ $this->configFiles = $configFiles;
}
}
diff --git a/lib/stompqueuemanager.php b/lib/stompqueuemanager.php
index 9af8b2f48..91faa8c36 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);
@@ -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/subs.php b/lib/subs.php
index e2ce0667e..165bbaa8f 100644
--- a/lib/subs.php
+++ b/lib/subs.php
@@ -43,46 +43,3 @@ function subs_unsubscribe_to($user, $other)
return $e->getMessage();
}
}
-
-function subs_unsubscribe_from($user, $other){
- $local = User::staticGet("nickname",$other);
- if($local){
- return subs_unsubscribe_to($local,$user);
- } else {
- try {
- $remote = Profile::staticGet("nickname",$other);
- if(is_string($remote)){
- return $remote;
- }
- if (Event::handle('StartUnsubscribe', array($remote,$user))) {
-
- $sub = DB_DataObject::factory('subscription');
-
- $sub->subscriber = $remote->id;
- $sub->subscribed = $user->id;
-
- $sub->find(true);
-
- // note we checked for existence above
-
- if (!$sub->delete())
- return _('Couldn\'t delete subscription.');
-
- $cache = common_memcache();
-
- if ($cache) {
- $cache->delete(common_cache_key('user:notices_with_friends:' . $remote->id));
- }
-
-
- $user->blowSubscribersCount();
- $remote->blowSubscribersCount();
-
- Event::handle('EndUnsubscribe', array($remote, $user));
- }
- } catch (Exception $e) {
- return $e->getMessage();
- }
- }
-}
-
diff --git a/lib/subscriptionlist.php b/lib/subscriptionlist.php
index e1207774f..fc8f33f2e 100644
--- a/lib/subscriptionlist.php
+++ b/lib/subscriptionlist.php
@@ -113,12 +113,13 @@ class SubscriptionListItem extends ProfileListItem
$this->out->elementStart('ul', 'tags xoxo');
foreach ($tags as $tag) {
$this->out->elementStart('li');
- $this->out->element('span', 'mark_hash', '#');
- $this->out->element('a', array('rel' => 'tag',
- 'href' => common_local_url($this->action->trimmed('action'),
- array('nickname' => $this->owner->nickname,
- 'tag' => $tag))),
- $tag);
+ // Avoid space by using raw output.
+ $pt = '<span class="mark_hash">#</span><a rel="tag" href="' .
+ common_local_url($this->action->trimmed('action'),
+ array('nickname' => $this->owner->nickname,
+ 'tag' => $tag)) .
+ '">' . $tag . '</a>';
+ $this->out->raw($pt);
$this->out->elementEnd('li');
}
$this->out->elementEnd('ul');
diff --git a/lib/theme.php b/lib/theme.php
index 0be8c3b9d..a9d0cbc84 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.
@@ -76,7 +79,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 +92,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;
}
/**
@@ -236,7 +260,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';
}
/**
diff --git a/lib/themeuploader.php b/lib/themeuploader.php
new file mode 100644
index 000000000..370965db0
--- /dev/null
+++ b/lib/themeuploader.php
@@ -0,0 +1,311 @@
+<?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;
+ }
+
+ // Check the directory structure...
+ $path = pathinfo($name);
+ $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);
+ }
+
+ // Is this a safe or skippable file?
+ if ($this->skippable($path['filename'], $path['extension'])) {
+ // Documentation and such... booooring
+ continue;
+ } else {
+ $this->validateFile($path['filename'], $path['extension']);
+ }
+
+ $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"));
+ }
+ }
+
+ protected function skippable($filename, $ext)
+ {
+ $skip = array('txt', 'rtf', 'doc', 'docx', 'odt');
+ if (strtolower($filename) == 'readme') {
+ return true;
+ }
+ if (in_array(strtolower($ext), $skip)) {
+ return true;
+ }
+ return false;
+ }
+
+ protected function validateFile($filename, $ext)
+ {
+ $this->validateFileOrFolder($filename);
+ $this->validateExtension($ext);
+ // @fixme validate content
+ }
+
+ protected function validateFileOrFolder($name)
+ {
+ if (!preg_match('/^[a-z0-9_-]+$/i', $name)) {
+ $msg = _("Theme contains invalid file or folder name. " .
+ "Stick with ASCII letters, digits, underscore, and minus sign.");
+ throw new ClientException($msg);
+ }
+ return true;
+ }
+
+ protected function validateExtension($ext)
+ {
+ $allowed = array('css', 'png', 'gif', 'jpg', 'jpeg');
+ if (!in_array(strtolower($ext), $allowed)) {
+ $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
new file mode 100644
index 000000000..6744d2529
--- /dev/null
+++ b/lib/usernoprofileexception.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * class for an exception when the user profile is missing
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Exception
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Class for an exception when the user profile is missing
+ *
+ * @category Exception
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+class UserNoProfileException extends ServerException
+{
+ var $user = null;
+
+ /**
+ * constructor
+ *
+ * @param User $user User that's missing a profile
+ */
+
+ public function __construct($user)
+ {
+ $this->user = $user;
+
+ $message = sprintf(_("User %s (%d) has no profile record."),
+ $user->nickname, $user->id);
+
+ parent::__construct($message);
+ }
+
+ /**
+ * Accessor for user
+ *
+ * @return User the user that triggered this exception
+ */
+
+ public function getUser()
+ {
+ return $this->user;
+ }
+}
diff --git a/lib/userprofile.php b/lib/userprofile.php
index 43dfd05be..ca060842b 100644
--- a/lib/userprofile.php
+++ b/lib/userprofile.php
@@ -71,7 +71,8 @@ class UserProfile extends Widget
{
if (Event::handle('StartProfilePageProfileSection', array(&$this->out, $this->profile))) {
- $this->out->elementStart('div', 'entity_profile vcard author');
+ $this->out->elementStart('div', array('id' => 'i',
+ 'class' => 'entity_profile vcard author'));
$this->out->element('h2', null, _('User profile'));
if (Event::handle('StartProfilePageProfileElements', array(&$this->out, $this->profile))) {
@@ -228,6 +229,17 @@ class UserProfile extends Widget
function showEntityActions()
{
+ if ($this->profile->hasRole(Profile_role::DELETED)) {
+ $this->out->elementStart('div', 'entity_actions');
+ $this->out->element('h2', null, _('User actions'));
+ $this->out->elementStart('ul');
+ $this->out->elementStart('p', array('class' => 'profile_deleted'));
+ $this->out->text(_('User deletion in progress...'));
+ $this->out->elementEnd('p');
+ $this->out->elementEnd('ul');
+ $this->out->elementEnd('div');
+ return;
+ }
if (Event::handle('StartProfilePageActionsSection', array(&$this->out, $this->profile))) {
$cur = common_current_user();
@@ -346,6 +358,16 @@ class UserProfile extends Widget
$this->out->elementEnd('ul');
$this->out->elementEnd('li');
}
+
+ if ($cur->hasRight(Right::GRANTROLE)) {
+ $this->out->elementStart('li', 'entity_role');
+ $this->out->element('p', null, _('User role'));
+ $this->out->elementStart('ul');
+ $this->roleButton('administrator', _m('role', 'Administrator'));
+ $this->roleButton('moderator', _m('role', 'Moderator'));
+ $this->out->elementEnd('ul');
+ $this->out->elementEnd('li');
+ }
}
}
@@ -359,6 +381,22 @@ class UserProfile extends Widget
}
}
+ function roleButton($role, $label)
+ {
+ list($action, $r2args) = $this->out->returnToArgs();
+ $r2args['action'] = $action;
+
+ $this->out->elementStart('li', "entity_role_$role");
+ if ($this->user->hasRole($role)) {
+ $rf = new RevokeRoleForm($role, $label, $this->out, $this->profile, $r2args);
+ $rf->show();
+ } else {
+ $rf = new GrantRoleForm($role, $label, $this->out, $this->profile, $r2args);
+ $rf->show();
+ }
+ $this->out->elementEnd('li');
+ }
+
function showRemoteSubscribeLink()
{
$url = common_local_url('remotesubscribe',
diff --git a/lib/util.php b/lib/util.php
index add1b0ae6..9f62097d5 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) {
@@ -41,28 +49,91 @@ function common_init_locale($language=null)
}
putenv('LANGUAGE='.$language);
putenv('LANG='.$language);
- return setlocale(LC_ALL, $language . ".utf8",
+ $ok = setlocale(LC_ALL, $language . ".utf8",
$language . ".UTF8",
$language . ".utf-8",
$language . ".UTF-8",
$language);
+
+ 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');
- // gettext seems very picky... We first need to setlocale()
- // to a locale which _does_ exist on the system, and _then_
- // we can set in another locale that may not be set up
- // (say, ga_ES for Galego/Galician) it seems to take it.
- common_init_locale("en_US");
-
// Note that this setlocale() call may "fail" but this is harmless;
// gettext will still select the right language.
$language = common_language();
$locale_set = common_init_locale($language);
+ if (!$locale_set) {
+ // The requested locale doesn't exist on the system.
+ //
+ // gettext seems very picky... We first need to setlocale()
+ // to a locale which _does_ exist on the system, and _then_
+ // we can set in another locale that may not be set up
+ // (say, ga_ES for Galego/Galician) it seems to take it.
+ //
+ // For some reason C and POSIX which are guaranteed to work
+ // don't do the job. en_US.UTF-8 should be there most of the
+ // time, but not guaranteed.
+ $ok = common_init_locale("en_US");
+ if (!$ok && 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`;
+ foreach (explode("\n", $all) as $locale) {
+ if (preg_match('/\.utf[-_]?8$/i', $locale)) {
+ $ok = setlocale(LC_ALL, $locale);
+ if ($ok) {
+ break;
+ }
+ }
+ }
+ }
+ if (!$ok) {
+ common_log(LOG_ERR, "Unable to find a UTF-8 locale on this system; UI translations may not work.");
+ }
+ $locale_set = common_init_locale($language);
+ }
+
+ common_init_gettext();
+}
+
+/**
+ * @access private
+ */
+function common_init_gettext()
+{
+ setlocale(LC_CTYPE, 'C');
+ // So we do not have to make people install the gettext locales
+ $path = common_config('site','locale_path');
+ bindtextdomain("statusnet", $path);
+ bind_textdomain_codeset("statusnet", "UTF-8");
+ textdomain("statusnet");
+}
+
+/**
+ * Switch locale during runtime, and poke gettext until it cries uncle.
+ * Otherwise, sometimes it doesn't actually switch away from the old language.
+ *
+ * @param string $language code for locale ('en', 'fr', 'pt_BR' etc)
+ */
+function common_switch_locale($language=null)
+{
+ common_init_locale($language);
+
setlocale(LC_CTYPE, 'C');
// So we do not have to make people install the gettext locales
$path = common_config('site','locale_path');
@@ -71,6 +142,7 @@ function common_init_language()
textdomain("statusnet");
}
+
function common_timezone()
{
if (common_logged_in()) {
@@ -105,11 +177,13 @@ function common_language()
// Otherwise, find the best match for the languages requested by the
// user's browser...
- $httplang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : null;
- if (!empty($httplang)) {
- $language = client_prefered_language($httplang);
- if ($language)
- return $language;
+ if (common_config('site', 'langdetect')) {
+ $httplang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : null;
+ if (!empty($httplang)) {
+ $language = client_prefered_language($httplang);
+ if ($language)
+ return $language;
+ }
}
// Finally, if none of the above worked, use the site's default...
@@ -131,6 +205,11 @@ function common_munge_password($password, $id)
function common_check_user($nickname, $password)
{
+ // empty nickname always unacceptable
+ if (empty($nickname)) {
+ return false;
+ }
+
$authenticatedUser = false;
if (Event::handle('StartCheckPassword', array($nickname, $password, &$authenticatedUser))) {
@@ -770,7 +849,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;
@@ -793,10 +872,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'));
}
@@ -829,7 +908,14 @@ function common_xml_safe_str($str)
function common_tag_link($tag)
{
$canonical = common_canonical_tag($tag);
- $url = common_local_url('tag', array('tag' => $canonical));
+ if (common_config('singleuser', 'enabled')) {
+ // regular TagAction isn't set up in 1user mode
+ $url = common_local_url('showstream',
+ array('nickname' => common_config('singleuser', 'nickname'),
+ 'tag' => $canonical));
+ } else {
+ $url = common_local_url('tag', array('tag' => $canonical));
+ }
$xs = new XMLStringer();
$xs->elementStart('span', 'tag');
$xs->element('a', array('href' => $url,
@@ -853,7 +939,7 @@ function common_valid_profile_tag($str)
function common_group_link($sender_id, $nickname)
{
$sender = Profile::staticGet($sender_id);
- $group = User_group::getForNickname($nickname);
+ $group = User_group::getForNickname($nickname, $sender);
if ($sender && $group && $sender->isMember($group)) {
$attrs = array('href' => $group->permalink(),
'class' => 'url');
@@ -1010,24 +1096,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);
@@ -1149,9 +1249,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';
@@ -1239,12 +1338,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();
@@ -1270,7 +1395,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()
@@ -1364,6 +1489,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. */
@@ -1460,7 +1634,15 @@ function common_copy_args($from)
$to = array();
$strip = get_magic_quotes_gpc();
foreach ($from as $k => $v) {
- $to[$k] = ($strip) ? stripslashes($v) : $v;
+ if($strip) {
+ if(is_array($v)) {
+ $to[$k] = common_copy_args($v);
+ } else {
+ $to[$k] = stripslashes($v);
+ }
+ } else {
+ $to[$k] = $v;
+ }
}
return $to;
}
@@ -1488,7 +1670,8 @@ function common_user_uri(&$user)
function common_notice_uri(&$notice)
{
return common_local_url('shownotice',
- array('notice' => $notice->id));
+ array('notice' => $notice->id),
+ null, null, false);
}
// 36 alphanums - lookalikes (0, O, 1, I) = 32 chars = 5 bits
@@ -1755,6 +1938,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 f37635855..829eaa36c 100644
--- a/lib/xmppmanager.php
+++ b/lib/xmppmanager.php
@@ -36,6 +36,7 @@ class XmppManager extends IoManager
protected $site = null;
protected $pingid = 0;
protected $lastping = null;
+ protected $conn = null;
static protected $singletons = array();
@@ -252,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/
*/