diff options
author | Zach Copley <zach@status.net> | 2009-11-19 20:12:46 -0800 |
---|---|---|
committer | Zach Copley <zach@status.net> | 2009-11-19 20:12:46 -0800 |
commit | 4b98edf75f4e255f8c61087bd1525d89653a521f (patch) | |
tree | b2a7eb6d77429eadb1beabe2d5e6ae1c1a2831d6 /lib | |
parent | f92574dbcb1f2d7cd0aaf3c9362db46fa066e888 (diff) | |
parent | c213477081afefb1720c8ae729d1965e7a1dac63 (diff) |
Merge branch '0.9-release'
* 0.9-release: (874 commits)
Removed call to NewDirectMessage() until IE return is fixed i.e.,
Don't show flag user button your own profile
Fixed HXR response for flag user
Using the right form class name
Using common_redirect
Left a form_data class of a <ul> in the user admin panel
Added validation to fields in user admin panel
Added a user admin panel
Added mobile logos for default and identica themes
Changed gif to png
Changed this to action. THANKS zach!
Doing content negotiation only once
Add execute bit to pingqueuehandler
Localisation updates for !StatusNet from !translatewiki.net
Use the browser's geolocation API to set the location on the notice form
Add geometa library, and include it.
Add location form elements to the noticeform, and save their values on submission
Use the $user object nickname, as login name doesnt have to == nickname anymore with plugins such as ldap/etc
Revert "Re added NICKNAME_FMT constant to router.php."
Moved most path and server settings to a new paths admin panel
...
Conflicts:
js/util.js
locale/it_IT/LC_MESSAGES/statusnet.mo
locale/mk_MK/LC_MESSAGES/statusnet.mo
locale/mk_MK/LC_MESSAGES/statusnet.po
locale/pt_BR/LC_MESSAGES/statusnet.mo
locale/vi_VN/LC_MESSAGES/statusnet.mo
plugins/InfiniteScroll/infinitescroll.js
plugins/Realtime/realtimeupdate.js
Diffstat (limited to 'lib')
68 files changed, 6542 insertions, 4198 deletions
diff --git a/lib/Shorturl_api.php b/lib/Shorturl_api.php deleted file mode 100644 index 6402dbc09..000000000 --- a/lib/Shorturl_api.php +++ /dev/null @@ -1,131 +0,0 @@ -<?php -/* - * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2008, 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/>. - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - -class ShortUrlApi -{ - protected $service_url; - protected $long_limit = 27; - - function __construct($service_url) - { - $this->service_url = $service_url; - } - - function shorten($url) - { - if ($this->is_long($url)) return $this->shorten_imp($url); - return $url; - } - - protected function shorten_imp($url) { - return "To Override"; - } - - private function is_long($url) { - return strlen($url) >= common_config('site', 'shorturllength'); - } - - protected function http_post($data) { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $this->service_url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $data); - $response = curl_exec($ch); - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - if (($code < 200) || ($code >= 400)) return false; - return $response; - } - - protected function http_get($url) { - $encoded_url = urlencode($url); - return file_get_contents("{$this->service_url}$encoded_url"); - } - - protected function tidy($response) { - $response = str_replace(' ', ' ', $response); - $config = array('output-xhtml' => true); - $tidy = new tidy; - $tidy->parseString($response, $config, 'utf8'); - $tidy->cleanRepair(); - return (string)$tidy; - } -} - -class LilUrl extends ShortUrlApi -{ - function __construct() - { - parent::__construct('http://ur1.ca/'); - } - - protected function shorten_imp($url) { - $data['longurl'] = $url; - $response = $this->http_post($data); - if (!$response) return $url; - $y = @simplexml_load_string($response); - if (!isset($y->body)) return $url; - $x = $y->body->p[0]->a->attributes(); - if (isset($x['href'])) return $x['href']; - return $url; - } -} - - -class PtitUrl extends ShortUrlApi -{ - function __construct() - { - parent::__construct('http://ptiturl.com/?creer=oui&action=Reduire&url='); - } - - protected function shorten_imp($url) { - $response = $this->http_get($url); - if (!$response) return $url; - $response = $this->tidy($response); - $y = @simplexml_load_string($response); - if (!isset($y->body)) return $url; - $xml = $y->body->center->table->tr->td->pre->a->attributes(); - if (isset($xml['href'])) return $xml['href']; - return $url; - } -} - -class TightUrl extends ShortUrlApi -{ - function __construct() - { - parent::__construct('http://2tu.us/?save=y&url='); - } - - protected function shorten_imp($url) { - $response = $this->http_get($url); - if (!$response) return $url; - $response = $this->tidy($response); - $y = @simplexml_load_string($response); - if (!isset($y->body)) return $url; - $xml = $y->body->p[0]->code[0]->a->attributes(); - if (isset($xml['href'])) return $xml['href']; - return $url; - } -} - diff --git a/lib/accountsettingsaction.php b/lib/accountsettingsaction.php index 798116163..c79a1f5d7 100644 --- a/lib/accountsettingsaction.php +++ b/lib/accountsettingsaction.php @@ -98,44 +98,49 @@ class AccountSettingsNav extends Widget function show() { - # action => array('prompt', 'title') - $menu = - array('profilesettings' => - array(_('Profile'), - _('Change your profile settings')), - 'avatarsettings' => - array(_('Avatar'), - _('Upload an avatar')), - 'passwordsettings' => - array(_('Password'), - _('Change your password')), - 'emailsettings' => - array(_('Email'), - _('Change email handling')), - 'openidsettings' => - array(_('OpenID'), - _('Add or remove OpenIDs')), - 'userdesignsettings' => - array(_('Design'), - _('Design your profile')), - 'othersettings' => - array(_('Other'), - _('Other options'))); - $action_name = $this->action->trimmed('action'); $this->action->elementStart('ul', array('class' => 'nav')); - foreach ($menu as $menuaction => $menudesc) { - if ($menuaction == 'openidsettings' && - !common_config('openid', 'enabled')) { - continue; + if (Event::handle('StartAccountSettingsNav', array(&$this->action))) { + $user = common_current_user(); + + if(Event::handle('StartAccountSettingsProfileMenuItem', array($this, &$menu))){ + $this->showMenuItem('profilesettings',_('Profile'),_('Change your profile settings')); + Event::handle('EndAccountSettingsProfileMenuItem', array($this, &$menu)); + } + if(Event::handle('StartAccountSettingsAvatarMenuItem', array($this, &$menu))){ + $this->showMenuItem('avatarsettings',_('Avatar'),_('Upload an avatar')); + Event::handle('EndAccountSettingsAvatarMenuItem', array($this, &$menu)); + } + if(Event::handle('StartAccountSettingsPasswordMenuItem', array($this, &$menu))){ + $this->showMenuItem('passwordsettings',_('Password'),_('Change your password')); + Event::handle('EndAccountSettingsPasswordMenuItem', array($this, &$menu)); + } + if(Event::handle('StartAccountSettingsEmailMenuItem', array($this, &$menu))){ + $this->showMenuItem('emailsettings',_('Email'),_('Change email handling')); + Event::handle('EndAccountSettingsEmailMenuItem', array($this, &$menu)); + } + if(Event::handle('StartAccountSettingsDesignMenuItem', array($this, &$menu))){ + $this->showMenuItem('userdesignsettings',_('Design'),_('Design your profile')); + Event::handle('EndAccountSettingsDesignMenuItem', array($this, &$menu)); } - $this->action->menuItem(common_local_url($menuaction), - $menudesc[0], - $menudesc[1], - $action_name === $menuaction); + if(Event::handle('StartAccountSettingsOtherMenuItem', array($this, &$menu))){ + $this->showMenuItem('othersettings',_('Other'),_('Other options')); + Event::handle('EndAccountSettingsOtherMenuItem', array($this, &$menu)); + } + + Event::handle('EndAccountSettingsNav', array(&$this->action)); } $this->action->elementEnd('ul'); } + + function showMenuItem($menuaction, $desc1, $desc2) + { + $action_name = $this->action->trimmed('action'); + $this->action->menuItem(common_local_url($menuaction), + $desc1, + $desc2, + $action_name === $menuaction); + } } diff --git a/lib/action.php b/lib/action.php index 670eb498c..8ad391755 100644 --- a/lib/action.php +++ b/lib/action.php @@ -120,14 +120,16 @@ class Action extends HTMLOutputter // lawsuit { // XXX: attributes (profile?) $this->elementStart('head'); - $this->showTitle(); - $this->showShortcutIcon(); - $this->showStylesheets(); - $this->showScripts(); - $this->showOpenSearch(); - $this->showFeeds(); - $this->showDescription(); - $this->extraHead(); + if (Event::handle('StartShowHeadElements', array($this))) { + $this->showTitle(); + $this->showShortcutIcon(); + $this->showStylesheets(); + $this->showOpenSearch(); + $this->showFeeds(); + $this->showDescription(); + $this->extraHead(); + Event::handle('EndShowHeadElements', array($this)); + } $this->elementEnd('head'); } @@ -166,7 +168,7 @@ class Action extends HTMLOutputter // lawsuit { if (is_readable(INSTALLDIR . '/theme/' . common_config('site', 'theme') . '/favicon.ico')) { $this->element('link', array('rel' => 'shortcut icon', - 'href' => theme_path('favicon.ico'))); + 'href' => Theme::path('favicon.ico'))); } else { $this->element('link', array('rel' => 'shortcut icon', 'href' => common_path('favicon.ico'))); @@ -175,7 +177,7 @@ class Action extends HTMLOutputter // lawsuit if (common_config('site', 'mobile')) { if (is_readable(INSTALLDIR . '/theme/' . common_config('site', 'theme') . '/apple-touch-icon.png')) { $this->element('link', array('rel' => 'apple-touch-icon', - 'href' => theme_path('apple-touch-icon.png'))); + 'href' => Theme::path('apple-touch-icon.png'))); } else { $this->element('link', array('rel' => 'apple-touch-icon', 'href' => common_path('apple-touch-icon.png'))); @@ -208,16 +210,16 @@ class Action extends HTMLOutputter // lawsuit if (Event::handle('StartShowUAStyles', array($this))) { $this->comment('[if IE]><link rel="stylesheet" type="text/css" '. - 'href="'.theme_path('css/ie.css', 'base').'?version='.STATUSNET_VERSION.'" /><![endif]'); + 'href="'.Theme::path('css/ie.css', 'base').'?version='.STATUSNET_VERSION.'" /><![endif]'); foreach (array(6,7) as $ver) { - if (file_exists(theme_file('css/ie'.$ver.'.css', 'base'))) { + if (file_exists(Theme::file('css/ie'.$ver.'.css', 'base'))) { // Yes, IE people should be put in jail. $this->comment('[if lte IE '.$ver.']><link rel="stylesheet" type="text/css" '. - 'href="'.theme_path('css/ie'.$ver.'.css', 'base').'?version='.STATUSNET_VERSION.'" /><![endif]'); + 'href="'.Theme::path('css/ie'.$ver.'.css', 'base').'?version='.STATUSNET_VERSION.'" /><![endif]'); } } $this->comment('[if IE]><link rel="stylesheet" type="text/css" '. - 'href="'.theme_path('css/ie.css', null).'?version='.STATUSNET_VERSION.'" /><![endif]'); + 'href="'.Theme::path('css/ie.css', null).'?version='.STATUSNET_VERSION.'" /><![endif]'); Event::handle('EndShowUAStyles', array($this)); } @@ -257,6 +259,7 @@ class Action extends HTMLOutputter // lawsuit Event::handle('StartShowLaconicaScripts', array($this))) { $this->script('js/xbImportNode.js'); $this->script('js/util.js'); + $this->script('js/geometa.js'); // Frame-busting code to avoid clickjacking attacks. $this->element('script', array('type' => 'text/javascript'), 'if (window.top !== window.self) { window.top.location.href = window.self.location.href; }'); @@ -352,6 +355,7 @@ class Action extends HTMLOutputter // lawsuit Event::handle('EndShowFooter', array($this)); } $this->elementEnd('div'); + $this->showScripts(); $this->elementEnd('body'); } @@ -388,9 +392,9 @@ class Action extends HTMLOutputter // lawsuit if (Event::handle('StartAddressData', array($this))) { $this->elementStart('a', array('class' => 'url home bookmark', 'href' => common_local_url('public'))); - if (common_config('site', 'logo') || file_exists(theme_file('logo.png'))) { + if (common_config('site', 'logo') || file_exists(Theme::file('logo.png'))) { $this->element('img', array('class' => 'logo photo', - 'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : theme_path('logo.png'), + 'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png'), 'alt' => common_config('site', 'name'))); } $this->element('span', array('class' => 'fn org'), common_config('site', 'name')); @@ -431,6 +435,10 @@ class Action extends HTMLOutputter // lawsuit $this->menuItem(common_local_url($connect), _('Connect'), _('Connect to services'), false, 'nav_connect'); } + if ($user->hasRight(Right::CONFIGURESITE)) { + $this->menuItem(common_local_url('siteadminpanel'), + _('Admin'), _('Change site configuration'), false, 'nav_admin'); + } if (common_config('invite', 'enabled')) { $this->menuItem(common_local_url('invite'), _('Invite'), @@ -442,17 +450,12 @@ class Action extends HTMLOutputter // lawsuit _('Logout'), _('Logout from the site'), false, 'nav_logout'); } else { - if (!common_config('site', 'openidonly')) { - if (!common_config('site', 'closed')) { - $this->menuItem(common_local_url('register'), - _('Register'), _('Create an account'), false, 'nav_register'); - } - $this->menuItem(common_local_url('login'), - _('Login'), _('Login to the site'), false, 'nav_login'); - } else { - $this->menuItem(common_local_url('openidlogin'), - _('OpenID'), _('Login with OpenID'), false, 'nav_openid'); + if (!common_config('site', 'closed')) { + $this->menuItem(common_local_url('register'), + _('Register'), _('Create an account'), false, 'nav_register'); } + $this->menuItem(common_local_url('login'), + _('Login'), _('Login to the site'), false, 'nav_login'); } $this->menuItem(common_local_url('doc', array('title' => 'help')), _('Help'), _('Help me!'), false, 'nav_help'); @@ -530,7 +533,10 @@ class Action extends HTMLOutputter // lawsuit $this->showContentBlock(); Event::handle('EndShowContentBlock', array($this)); } - $this->showAside(); + if (Event::handle('StartShowAside', array($this))) { + $this->showAside(); + Event::handle('EndShowAside', array($this)); + } $this->elementEnd('div'); } @@ -985,6 +991,18 @@ class Action extends HTMLOutputter // lawsuit function selfUrl() { + list($action, $args) = $this->returnToArgs(); + return common_local_url($action, $args); + } + + /** + * Returns arguments sufficient for re-constructing URL + * + * @return array two elements: action, other args + */ + + function returnToArgs() + { $action = $this->trimmed('action'); $args = $this->args; unset($args['action']); @@ -997,8 +1015,7 @@ class Action extends HTMLOutputter // lawsuit foreach (array_keys($_COOKIE) as $cookie) { unset($args[$cookie]); } - - return common_local_url($action, $args); + return array($action, $args); } /** @@ -1047,8 +1064,7 @@ class Action extends HTMLOutputter // lawsuit { // Does a little before-after block for next/prev page if ($have_before || $have_after) { - $this->elementStart('div', array('class' => 'pagination')); - $this->elementStart('dl', null); + $this->elementStart('dl', 'pagination'); $this->element('dt', null, _('Pagination')); $this->elementStart('dd', null); $this->elementStart('ul', array('class' => 'nav')); @@ -1073,7 +1089,6 @@ class Action extends HTMLOutputter // lawsuit $this->elementEnd('ul'); $this->elementEnd('dd'); $this->elementEnd('dl'); - $this->elementEnd('div'); } } @@ -1100,4 +1115,22 @@ class Action extends HTMLOutputter // lawsuit { return Design::siteDesign(); } + + /** + * Check the session token. + * + * Checks that the current form has the correct session token, + * and throw an exception if it does not. + * + * @return void + */ + + function checkSessionToken() + { + // CSRF protection + $token = $this->trimmed('token'); + if (empty($token) || $token != common_session_token()) { + $this->clientError(_('There was a problem with your session token.')); + } + } } diff --git a/lib/adminform.php b/lib/adminform.php new file mode 100644 index 000000000..3934f6351 --- /dev/null +++ b/lib/adminform.php @@ -0,0 +1,86 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Base class for administrative forms + * + * 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 Widget + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @copyright 2009 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); +} + +/** + * Base class for Administrative forms + * + * Just a place holder for some utility methods to simply some + * repetitive form building code + * + * @category Widget + * @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/ + * + * @see Form + */ + +class AdminForm extends Form +{ + /** + * Utility to simplify some of the duplicated code around + * params and settings. + * + * @param string $setting Name of the setting + * @param string $title Title to use for the input + * @param string $instructions Instructions for this field + * @param string $section config section, default = 'site' + * + * @return void + */ + + function input($setting, $title, $instructions, $section='site') + { + $this->out->input($setting, $title, $this->value($setting, $section), $instructions); + } + + /** + * Utility to simplify getting the posted-or-stored setting value + * + * @param string $setting Name of the setting + * @param string $main configuration section, default = 'site' + * + * @return string param value if posted, or current config value + */ + + function value($setting, $main='site') + { + $value = $this->out->trimmed($setting); + if (empty($value)) { + $value = common_config($main, $setting); + } + return $value; + } + +} diff --git a/lib/adminpanelaction.php b/lib/adminpanelaction.php new file mode 100644 index 000000000..7997eb2b1 --- /dev/null +++ b/lib/adminpanelaction.php @@ -0,0 +1,315 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Superclass for admin panel actions + * + * 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 UI + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 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); +} + +/** + * superclass for admin panel actions + * + * Common code for all admin panel actions. + * + * @category UI + * @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/ + * + * @todo Find some commonalities with SettingsAction and combine + */ + +class AdminPanelAction extends Action +{ + var $success = true; + var $msg = null; + + /** + * Prepare for the action + * + * We check to see that the user is logged in, has + * authenticated in this session, and has the right + * to configure the site. + * + * @param array $args Array of arguments from Web driver + * + * @return boolean success flag + */ + + function prepare($args) + { + parent::prepare($args); + + // User must be logged in. + + if (!common_logged_in()) { + $this->clientError(_('Not logged in.')); + return; + } + + $user = common_current_user(); + + // ...because they're logged in + + assert(!empty($user)); + + // It must be a "real" login, not saved cookie login + + if (!common_is_real_login()) { + // Cookie theft is too easy; we require automatic + // logins to re-authenticate before admining the site + common_set_returnto($this->selfUrl()); + if (Event::handle('RedirectToLogin', array($this, $user))) { + common_redirect(common_local_url('login'), 303); + } + } + + // User must have the right to change admin settings + + if (!$user->hasRight(Right::CONFIGURESITE)) { + $this->clientError(_('You cannot make changes to this site.')); + return; + } + + return true; + } + + /** + * handle the action + * + * Check session token and try to save the settings if this is a + * POST. Otherwise, show the form. + * + * @param array $args unused. + * + * @return void + */ + + function handle($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $this->checkSessionToken(); + try { + $this->saveSettings(); + + // Reload settings + + Config::loadSettings(); + + $this->success = true; + $this->msg = _('Settings saved.'); + } catch (Exception $e) { + $this->success = false; + $this->msg = $e->getMessage(); + } + } + $this->showPage(); + } + + /** + * Show tabset for this page + * + * Uses the AdminPanelNav widget + * + * @return void + * @see AdminPanelNav + */ + + function showLocalNav() + { + $nav = new AdminPanelNav($this); + $nav->show(); + } + + /** + * Show the content section of the page + * + * Here, we show the admin panel's form. + * + * @return void. + */ + + function showContent() + { + $this->showForm(); + } + + /** + * show human-readable instructions for the page, or + * a success/failure on save. + * + * @return void + */ + + function showPageNotice() + { + if ($this->msg) { + $this->element('div', ($this->success) ? 'success' : 'error', + $this->msg); + } else { + $inst = $this->getInstructions(); + $output = common_markup_to_html($inst); + + $this->elementStart('div', 'instructions'); + $this->raw($output); + $this->elementEnd('div'); + } + } + + /** + * Show the admin panel form + * + * Sub-classes should overload this. + * + * @return void + */ + + function showForm() + { + $this->clientError(_('showForm() not implemented.')); + return; + } + + /** + * Instructions for using this form. + * + * String with instructions for using the form. + * + * Subclasses should overload this. + * + * @return void + */ + + function getInstructions() + { + return ''; + } + + /** + * Save settings from the form + * + * Validate and save the settings from the user. + * + * @return void + */ + + function saveSettings() + { + $this->clientError(_('saveSettings() not implemented.')); + return; + } + + /** + * Delete a design setting + * + * // XXX: Maybe this should go in Design? --Z + * + * @return mixed $result false if something didn't work + */ + + function deleteSetting($section, $setting) + { + $config = new Config(); + + $config->section = $section; + $config->setting = $setting; + + if ($config->find(true)) { + $result = $config->delete(); + if (!$result) { + common_log_db_error($config, 'DELETE', __FILE__); + $this->clientError(_("Unable to delete design setting.")); + return null; + } + } + + return $result; + } +} + +/** + * Menu for public group of actions + * + * @category Output + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @author Sarven Capadisli <csarven@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 Widget + */ + +class AdminPanelNav extends Widget +{ + var $action = null; + + /** + * Construction + * + * @param Action $action current action, used for output + */ + + function __construct($action=null) + { + parent::__construct($action); + $this->action = $action; + } + + /** + * Show the menu + * + * @return void + */ + + function show() + { + $action_name = $this->action->trimmed('action'); + + $this->action->elementStart('ul', array('class' => 'nav')); + + if (Event::handle('StartAdminPanelNav', array($this))) { + + $this->out->menuItem(common_local_url('siteadminpanel'), _('Site'), + _('Basic site configuration'), $action_name == 'siteadminpanel', 'nav_site_admin_panel'); + + $this->out->menuItem(common_local_url('designadminpanel'), _('Design'), + _('Design configuration'), $action_name == 'designadminpanel', 'nav_design_admin_panel'); + + $this->out->menuItem(common_local_url('useradminpanel'), _('User'), + _('Paths configuration'), $action_name == 'useradminpanel', 'nav_design_admin_panel'); + + $this->out->menuItem(common_local_url('pathsadminpanel'), _('Paths'), + _('Paths configuration'), $action_name == 'pathsadminpanel', 'nav_design_admin_panel'); + + Event::handle('EndAdminPanelNav', array($this)); + } + $this->action->elementEnd('ul'); + } +} diff --git a/lib/twitterapi.php b/lib/api.php index 3bac400e2..e2ea87b43 100644 --- a/lib/twitterapi.php +++ b/lib/api.php @@ -1,9 +1,12 @@ <?php -/* - * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2008, 2009, StatusNet, Inc. +/** + * StatusNet, the distributed open-source microblogging tool * - * This program is free software: you can redistribute it and/or modify + * Base API action + * + * 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. @@ -15,16 +18,48 @@ * * 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 API + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @author Dan Moore <dan@moore.cx> + * @author Evan Prodromou <evan@status.net> + * @author Jeffery To <jeffery.to@gmail.com> + * @author Toby Inkster <mail@tobyinkster.co.uk> + * @author Zach Copley <zach@status.net> + * @copyright 2009 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')) { +if (!defined('STATUSNET')) { exit(1); } -class TwitterapiAction extends Action -{ +/** + * Contains most of the Twitter-compatible API output functions. + * + * @category API + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @author Dan Moore <dan@moore.cx> + * @author Evan Prodromou <evan@status.net> + * @author Jeffery To <jeffery.to@gmail.com> + * @author Toby Inkster <mail@tobyinkster.co.uk> + * @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/ + */ - var $auth_user; +class ApiAction extends Action +{ + var $format = null; + var $user = null; + var $page = null; + var $count = null; + var $max_id = null; + var $since_id = null; + var $since = null; /** * Initialization. @@ -37,6 +72,14 @@ class TwitterapiAction extends Action function prepare($args) { parent::prepare($args); + + $this->format = $this->arg('format'); + $this->page = (int)$this->arg('page', 1); + $this->count = (int)$this->arg('count', 20); + $this->max_id = (int)$this->arg('max_id', 0); + $this->since_id = (int)$this->arg('since_id', 0); + $this->since = $this->arg('since'); + return true; } @@ -73,7 +116,7 @@ class TwitterapiAction extends Action return parent::element($tag, $attrs, $content); } - function twitter_user_array($profile, $get_notice=false) + function twitterUserArray($profile, $get_notice=false) { $twitter_user = array(); @@ -91,22 +134,36 @@ class TwitterapiAction extends Action $twitter_user['protected'] = false; # not supported by StatusNet yet $twitter_user['followers_count'] = $profile->subscriberCount(); - // To be supported soon... - $twitter_user['profile_background_color'] = ''; - $twitter_user['profile_text_color'] = ''; - $twitter_user['profile_link_color'] = ''; - $twitter_user['profile_sidebar_fill_color'] = ''; + $defaultDesign = Design::siteDesign(); + $design = null; + $user = $profile->getUser(); + + // Note: some profiles don't have an associated user + + if (!empty($user)) { + $design = $user->getDesign(); + } + + if (empty($design)) { + $design = $defaultDesign; + } + + $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor); + $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue(); + $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor); + $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue(); + $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor); + $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue(); + $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor); + $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue(); $twitter_user['profile_sidebar_border_color'] = ''; $twitter_user['friends_count'] = $profile->subscriptionCount(); - $twitter_user['created_at'] = $this->date_twitter($profile->created); + $twitter_user['created_at'] = $this->dateTwitter($profile->created); $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling! - // Need to pull up the user for some of this - $user = User::staticGet($profile->id); - $timezone = 'UTC'; if ($user->timezone) { @@ -119,9 +176,14 @@ class TwitterapiAction extends Action $twitter_user['utc_offset'] = $t->format('Z'); $twitter_user['time_zone'] = $timezone; - // To be supported some day, perhaps - $twitter_user['profile_background_image_url'] = ''; - $twitter_user['profile_background_tile'] = false; + $twitter_user['profile_background_image_url'] + = empty($design->backgroundimage) + ? '' : ($design->disposition & BACKGROUND_ON) + ? Design::url($design->backgroundimage) : ''; + + $twitter_user['profile_background_tile'] + = empty($design->disposition) + ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false'; $twitter_user['statuses_count'] = $profile->noticeCount(); @@ -146,24 +208,24 @@ class TwitterapiAction extends Action $notice = $profile->getCurrentNotice(); if ($notice) { # don't get user! - $twitter_user['status'] = $this->twitter_status_array($notice, false); + $twitter_user['status'] = $this->twitterStatusArray($notice, false); } } return $twitter_user; } - function twitter_status_array($notice, $include_user=true) + function twitterStatusArray($notice, $include_user=true) { $profile = $notice->getProfile(); $twitter_status = array(); $twitter_status['text'] = $notice->content; $twitter_status['truncated'] = false; # Not possible on StatusNet - $twitter_status['created_at'] = $this->date_twitter($notice->created); + $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->source_link($notice->source); + $twitter_status['source'] = $this->sourceLink($notice->source); $twitter_status['id'] = intval($notice->id); $replier_profile = null; @@ -180,6 +242,15 @@ class TwitterapiAction extends Action $twitter_status['in_reply_to_screen_name'] = ($replier_profile) ? $replier_profile->nickname : null; + if (isset($notice->lat) && isset($notice->lon)) { + // This is the format that GeoJSON expects stuff to be in + $twitter_status['geo'] = array('type' => 'Point', + 'coordinates' => array((float) $notice->lat, + (float) $notice->lon)); + } else { + $twitter_status['geo'] = null; + } + if (isset($this->auth_user)) { $twitter_status['favorited'] = $this->auth_user->hasFave($notice); } else { @@ -206,14 +277,14 @@ class TwitterapiAction extends Action if ($include_user) { # Don't get notice (recursive!) - $twitter_user = $this->twitter_user_array($profile, false); + $twitter_user = $this->twitterUserArray($profile, false); $twitter_status['user'] = $twitter_user; } return $twitter_status; } - function twitter_group_array($group) + function twitterGroupArray($group) { $twitter_group=array(); $twitter_group['id']=$group->id; @@ -228,12 +299,12 @@ class TwitterapiAction extends Action $twitter_group['homepage']=$group->homepage; $twitter_group['description']=$group->description; $twitter_group['location']=$group->location; - $twitter_group['created']=$this->date_twitter($group->created); - $twitter_group['modified']=$this->date_twitter($group->modified); + $twitter_group['created']=$this->dateTwitter($group->created); + $twitter_group['modified']=$this->dateTwitter($group->modified); return $twitter_group; } - function twitter_rss_group_array($group) + function twitterRssGroupArray($group) { $entry = array(); $entry['content']=$group->description; @@ -251,7 +322,7 @@ class TwitterapiAction extends Action return $entry; } - function twitter_rss_entry_array($notice) + function twitterRssEntryArray($notice) { $profile = $notice->getProfile(); $entry = array(); @@ -274,11 +345,12 @@ class TwitterapiAction extends Action $enclosures = array(); foreach ($attachments as $attachment) { - if ($attachment->isEnclosure()) { + $enclosure_o=$attachment->getEnclosure(); + if ($enclosure_o) { $enclosure = array(); - $enclosure['url'] = $attachment->url; - $enclosure['mimetype'] = $attachment->mimetype; - $enclosure['size'] = $attachment->size; + $enclosure['url'] = $enclosure_o->url; + $enclosure['mimetype'] = $enclosure_o->mimetype; + $enclosure['size'] = $enclosure_o->size; $enclosures[] = $enclosure; } } @@ -287,23 +359,6 @@ class TwitterapiAction extends Action $entry['enclosures'] = $enclosures; } -/* - // Enclosure - $attachments = $notice->attachments(); - if($attachments){ - $entry['enclosures']=array(); - foreach($attachments as $attachment){ - if ($attachment->isEnclosure()) { - $enclosure=array(); - $enclosure['url']=$attachment->url; - $enclosure['mimetype']=$attachment->mimetype; - $enclosure['size']=$attachment->size; - $entry['enclosures'][]=$enclosure; - } - } - } -*/ - // Tags/Categories $tag = new Notice_tag(); $tag->notice_id = $notice->id; @@ -320,68 +375,32 @@ class TwitterapiAction extends Action $entry['pubDate'] = common_date_rfc2822($notice->created); $entry['guid'] = $entry['link']; - return $entry; - } - - function twitter_rss_dmsg_array($message) - { - - $entry = array(); - - $entry['title'] = sprintf('Message from %s to %s', - $message->getFrom()->nickname, $message->getTo()->nickname); - - $entry['content'] = common_xml_safe_str(trim($message->content)); - $entry['link'] = common_local_url('showmessage', array('message' => $message->id)); - $entry['published'] = common_date_iso8601($message->created); - - $taguribase = common_config('integration', 'taguri'); - - $entry['id'] = "tag:$taguribase,:$entry[link]"; - $entry['updated'] = $entry['published']; - $entry['author'] = $message->getFrom()->getBestName(); - - # RSS Item specific - $entry['description'] = $entry['content']; - $entry['pubDate'] = common_date_rfc2822($message->created); - $entry['guid'] = $entry['link']; + if (isset($notice->lat) && isset($notice->lon)) { + // This is the format that GeoJSON expects stuff to be in. + // showGeoRSS() below uses it for XML output, so we reuse it + $entry['geo'] = array('type' => 'Point', + 'coordinates' => array((float) $notice->lat, + (float) $notice->lon)); + } else { + $entry['geo'] = null; + } return $entry; } - function twitter_dmsg_array($message) - { - $twitter_dm = array(); - - $from_profile = $message->getFrom(); - $to_profile = $message->getTo(); - - $twitter_dm['id'] = $message->id; - $twitter_dm['sender_id'] = $message->from_profile; - $twitter_dm['text'] = trim($message->content); - $twitter_dm['recipient_id'] = $message->to_profile; - $twitter_dm['created_at'] = $this->date_twitter($message->created); - $twitter_dm['sender_screen_name'] = $from_profile->nickname; - $twitter_dm['recipient_screen_name'] = $to_profile->nickname; - $twitter_dm['sender'] = $this->twitter_user_array($from_profile, false); - $twitter_dm['recipient'] = $this->twitter_user_array($to_profile, false); - - return $twitter_dm; - } - - function twitter_relationship_array($source, $target) + function twitterRelationshipArray($source, $target) { $relationship = array(); $relationship['source'] = - $this->relationship_details_array($source, $target); + $this->relationshipDetailsArray($source, $target); $relationship['target'] = - $this->relationship_details_array($target, $source); + $this->relationshipDetailsArray($target, $source); return array('relationship' => $relationship); } - function relationship_details_array($source, $target) + function relationshipDetailsArray($source, $target) { $details = array(); @@ -408,14 +427,14 @@ class TwitterapiAction extends Action return $details; } - function show_twitter_xml_relationship($relationship) + function showTwitterXmlRelationship($relationship) { $this->elementStart('relationship'); foreach($relationship as $element => $value) { if ($element == 'source' || $element == 'target') { $this->elementStart($element); - $this->show_xml_relationship_details($value); + $this->showXmlRelationshipDetails($value); $this->elementEnd($element); } } @@ -423,26 +442,29 @@ class TwitterapiAction extends Action $this->elementEnd('relationship'); } - function show_xml_relationship_details($details) + function showXmlRelationshipDetails($details) { foreach($details as $element => $value) { $this->element($element, null, $value); } } - function show_twitter_xml_status($twitter_status) + function showTwitterXmlStatus($twitter_status) { $this->elementStart('status'); foreach($twitter_status as $element => $value) { switch ($element) { case 'user': - $this->show_twitter_xml_user($twitter_status['user']); + $this->showTwitterXmlUser($twitter_status['user']); break; case 'text': $this->element($element, null, common_xml_safe_str($value)); break; case 'attachments': - $this->show_xml_attachments($twitter_status['attachments']); + $this->showXmlAttachments($twitter_status['attachments']); + break; + case 'geo': + $this->showGeoRSS($value); break; default: $this->element($element, null, $value); @@ -451,7 +473,7 @@ class TwitterapiAction extends Action $this->elementEnd('status'); } - function show_twitter_xml_group($twitter_group) + function showTwitterXmlGroup($twitter_group) { $this->elementStart('group'); foreach($twitter_group as $element => $value) { @@ -460,12 +482,12 @@ class TwitterapiAction extends Action $this->elementEnd('group'); } - function show_twitter_xml_user($twitter_user, $role='user') + function showTwitterXmlUser($twitter_user, $role='user') { $this->elementStart($role); foreach($twitter_user as $element => $value) { if ($element == 'status') { - $this->show_twitter_xml_status($twitter_user['status']); + $this->showTwitterXmlStatus($twitter_user['status']); } else { $this->element($element, null, $value); } @@ -473,7 +495,7 @@ class TwitterapiAction extends Action $this->elementEnd($role); } - function show_xml_attachments($attachments) { + function showXmlAttachments($attachments) { if (!empty($attachments)) { $this->elementStart('attachments', array('type' => 'array')); foreach ($attachments as $attachment) { @@ -487,7 +509,19 @@ class TwitterapiAction extends Action } } - function show_twitter_rss_item($entry) + function showGeoRSS($geo) + { + if (empty($geo)) { + // empty geo element + $this->element('geo'); + } else { + $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss')); + $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]); + $this->elementEnd('geo'); + } + } + + function showTwitterRssItem($entry) { $this->elementStart('item'); $this->element('title', null, $entry['title']); @@ -508,97 +542,62 @@ class TwitterapiAction extends Action } } + $this->showGeoRSS($entry['geo']); $this->elementEnd('item'); } - function show_json_objects($objects) + function showJsonObjects($objects) { print(json_encode($objects)); } - function show_single_xml_status($notice) + function showSingleXmlStatus($notice) { - $this->init_document('xml'); - $twitter_status = $this->twitter_status_array($notice); - $this->show_twitter_xml_status($twitter_status); - $this->end_document('xml'); + $this->initDocument('xml'); + $twitter_status = $this->twitterStatusArray($notice); + $this->showTwitterXmlStatus($twitter_status); + $this->endDocument('xml'); } function show_single_json_status($notice) { - $this->init_document('json'); - $status = $this->twitter_status_array($notice); - $this->show_json_objects($status); - $this->end_document('json'); + $this->initDocument('json'); + $status = $this->twitterStatusArray($notice); + $this->showJsonObjects($status); + $this->endDocument('json'); } - function show_single_xml_dmsg($message) + function showXmlTimeline($notice) { - $this->init_document('xml'); - $dmsg = $this->twitter_dmsg_array($message); - $this->show_twitter_xml_dmsg($dmsg); - $this->end_document('xml'); - } - function show_single_json_dmsg($message) - { - $this->init_document('json'); - $dmsg = $this->twitter_dmsg_array($message); - $this->show_json_objects($dmsg); - $this->end_document('json'); - } - - function show_twitter_xml_dmsg($twitter_dm) - { - $this->elementStart('direct_message'); - foreach($twitter_dm as $element => $value) { - switch ($element) { - case 'sender': - case 'recipient': - $this->show_twitter_xml_user($value, $element); - break; - case 'text': - $this->element($element, null, common_xml_safe_str($value)); - break; - default: - $this->element($element, null, $value); - } - } - $this->elementEnd('direct_message'); - } - - function show_xml_timeline($notice) - { - - $this->init_document('xml'); + $this->initDocument('xml'); $this->elementStart('statuses', array('type' => 'array')); if (is_array($notice)) { foreach ($notice as $n) { - $twitter_status = $this->twitter_status_array($n); - $this->show_twitter_xml_status($twitter_status); + $twitter_status = $this->twitterStatusArray($n); + $this->showTwitterXmlStatus($twitter_status); } } else { while ($notice->fetch()) { - $twitter_status = $this->twitter_status_array($notice); - $this->show_twitter_xml_status($twitter_status); + $twitter_status = $this->twitterStatusArray($notice); + $this->showTwitterXmlStatus($twitter_status); } } $this->elementEnd('statuses'); - $this->end_document('xml'); + $this->endDocument('xml'); } - function show_rss_timeline($notice, $title, $link, $subtitle, $suplink=null) + function showRssTimeline($notice, $title, $link, $subtitle, $suplink=null) { - $this->init_document('rss'); + $this->initDocument('rss'); - $this->elementStart('channel'); $this->element('title', null, $title); $this->element('link', null, $link); if (!is_null($suplink)) { - # For FriendFeed's SUP protocol + // For FriendFeed's SUP protocol $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom', 'rel' => 'http://api.friendfeed.com/2008/03#sup', 'href' => $suplink, @@ -610,24 +609,23 @@ class TwitterapiAction extends Action if (is_array($notice)) { foreach ($notice as $n) { - $entry = $this->twitter_rss_entry_array($n); - $this->show_twitter_rss_item($entry); + $entry = $this->twitterRssEntryArray($n); + $this->showTwitterRssItem($entry); } } else { while ($notice->fetch()) { - $entry = $this->twitter_rss_entry_array($notice); - $this->show_twitter_rss_item($entry); + $entry = $this->twitterRssEntryArray($notice); + $this->showTwitterRssItem($entry); } } - $this->elementEnd('channel'); - $this->end_twitter_rss(); + $this->endTwitterRss(); } - function show_atom_timeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null) + function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null) { - $this->init_document('atom'); + $this->initDocument('atom'); $this->element('title', null, $title); $this->element('id', null, $id); @@ -658,16 +656,15 @@ class TwitterapiAction extends Action } } - $this->end_document('atom'); + $this->endDocument('atom'); } - function show_rss_groups($group, $title, $link, $subtitle) + function showRssGroups($group, $title, $link, $subtitle) { - $this->init_document('rss'); + $this->initDocument('rss'); - $this->elementStart('channel'); $this->element('title', null, $title); $this->element('link', null, $link); $this->element('description', null, $subtitle); @@ -676,24 +673,137 @@ class TwitterapiAction extends Action if (is_array($group)) { foreach ($group as $g) { - $twitter_group = $this->twitter_rss_group_array($g); - $this->show_twitter_rss_item($twitter_group); + $twitter_group = $this->twitterRssGroupArray($g); + $this->showTwitterRssItem($twitter_group); } } else { while ($group->fetch()) { - $twitter_group = $this->twitter_rss_group_array($group); - $this->show_twitter_rss_item($twitter_group); + $twitter_group = $this->twitterRssGroupArray($group); + $this->showTwitterRssItem($twitter_group); } } - $this->elementEnd('channel'); - $this->end_twitter_rss(); + $this->endTwitterRss(); + } + + function showTwitterAtomEntry($entry) + { + $this->elementStart('entry'); + $this->element('title', null, $entry['title']); + $this->element('content', array('type' => 'html'), $entry['content']); + $this->element('id', null, $entry['id']); + $this->element('published', null, $entry['published']); + $this->element('updated', null, $entry['updated']); + $this->element('link', array('type' => 'text/html', + 'href' => $entry['link'], + 'rel' => 'alternate')); + $this->element('link', array('type' => $entry['avatar-type'], + 'href' => $entry['avatar'], + 'rel' => 'image')); + $this->elementStart('author'); + + $this->element('name', null, $entry['author-name']); + $this->element('uri', null, $entry['author-uri']); + + $this->elementEnd('author'); + $this->elementEnd('entry'); + } + + function showXmlDirectMessage($dm) + { + $this->elementStart('direct_message'); + foreach($dm as $element => $value) { + switch ($element) { + case 'sender': + case 'recipient': + $this->showTwitterXmlUser($value, $element); + break; + case 'text': + $this->element($element, null, common_xml_safe_str($value)); + break; + default: + $this->element($element, null, $value); + break; + } + } + $this->elementEnd('direct_message'); } - function show_atom_groups($group, $title, $id, $link, $subtitle=null, $selfuri=null) + function directMessageArray($message) { + $dmsg = array(); + + $from_profile = $message->getFrom(); + $to_profile = $message->getTo(); + + $dmsg['id'] = $message->id; + $dmsg['sender_id'] = $message->from_profile; + $dmsg['text'] = trim($message->content); + $dmsg['recipient_id'] = $message->to_profile; + $dmsg['created_at'] = $this->dateTwitter($message->created); + $dmsg['sender_screen_name'] = $from_profile->nickname; + $dmsg['recipient_screen_name'] = $to_profile->nickname; + $dmsg['sender'] = $this->twitterUserArray($from_profile, false); + $dmsg['recipient'] = $this->twitterUserArray($to_profile, false); + + return $dmsg; + } + + function rssDirectMessageArray($message) + { + $entry = array(); + + $from = $message->getFrom(); + + $entry['title'] = sprintf('Message from %s to %s', + $from->nickname, $message->getTo()->nickname); + + $entry['content'] = common_xml_safe_str($message->rendered); + $entry['link'] = common_local_url('showmessage', array('message' => $message->id)); + $entry['published'] = common_date_iso8601($message->created); + + $taguribase = common_config('integration', 'taguri'); + + $entry['id'] = "tag:$taguribase:$entry[link]"; + $entry['updated'] = $entry['published']; + + $entry['author-name'] = $from->getBestName(); + $entry['author-uri'] = $from->homepage; + + $avatar = $from->getAvatar(AVATAR_STREAM_SIZE); - $this->init_document('atom'); + $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE); + $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png'; + + // RSS item specific + + $entry['description'] = $entry['content']; + $entry['pubDate'] = common_date_rfc2822($message->created); + $entry['guid'] = $entry['link']; + + return $entry; + } + + function showSingleXmlDirectMessage($message) + { + $this->initDocument('xml'); + $dmsg = $this->directMessageArray($message); + $this->showXmlDirectMessage($dmsg); + $this->endDocument('xml'); + } + + function showSingleJsonDirectMessage($message) + { + $this->initDocument('json'); + $dmsg = $this->directMessageArray($message); + $this->showJsonObjects($dmsg); + $this->endDocument('json'); + } + + function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null) + { + + $this->initDocument('atom'); $this->element('title', null, $title); $this->element('id', null, $id); @@ -717,168 +827,151 @@ class TwitterapiAction extends Action } } - $this->end_document('atom'); + $this->endDocument('atom'); } - function show_json_timeline($notice) + function showJsonTimeline($notice) { - $this->init_document('json'); + $this->initDocument('json'); $statuses = array(); if (is_array($notice)) { foreach ($notice as $n) { - $twitter_status = $this->twitter_status_array($n); + $twitter_status = $this->twitterStatusArray($n); array_push($statuses, $twitter_status); } } else { while ($notice->fetch()) { - $twitter_status = $this->twitter_status_array($notice); + $twitter_status = $this->twitterStatusArray($notice); array_push($statuses, $twitter_status); } } - $this->show_json_objects($statuses); + $this->showJsonObjects($statuses); - $this->end_document('json'); + $this->endDocument('json'); } - function show_json_groups($group) + function showJsonGroups($group) { - $this->init_document('json'); + $this->initDocument('json'); $groups = array(); if (is_array($group)) { foreach ($group as $g) { - $twitter_group = $this->twitter_group_array($g); + $twitter_group = $this->twitterGroupArray($g); array_push($groups, $twitter_group); } } else { while ($group->fetch()) { - $twitter_group = $this->twitter_group_array($group); + $twitter_group = $this->twitterGroupArray($group); array_push($groups, $twitter_group); } } - $this->show_json_objects($groups); + $this->showJsonObjects($groups); - $this->end_document('json'); + $this->endDocument('json'); } - function show_xml_groups($group) + function showXmlGroups($group) { - $this->init_document('xml'); + $this->initDocument('xml'); $this->elementStart('groups', array('type' => 'array')); if (is_array($group)) { foreach ($group as $g) { - $twitter_group = $this->twitter_group_array($g); - $this->show_twitter_xml_group($twitter_group); + $twitter_group = $this->twitterGroupArray($g); + $this->showTwitterXmlGroup($twitter_group); } } else { while ($group->fetch()) { - $twitter_group = $this->twitter_group_array($group); - $this->show_twitter_xml_group($twitter_group); + $twitter_group = $this->twitterGroupArray($group); + $this->showTwitterXmlGroup($twitter_group); } } $this->elementEnd('groups'); - $this->end_document('xml'); + $this->endDocument('xml'); } - function show_twitter_xml_users($user) + function showTwitterXmlUsers($user) { - $this->init_document('xml'); + $this->initDocument('xml'); $this->elementStart('users', array('type' => 'array')); if (is_array($user)) { - foreach ($group as $g) { - $twitter_user = $this->twitter_user_array($g); - $this->show_twitter_xml_user($twitter_user,'user'); + foreach ($user as $u) { + $twitter_user = $this->twitterUserArray($u); + $this->showTwitterXmlUser($twitter_user); } } else { while ($user->fetch()) { - $twitter_user = $this->twitter_user_array($user); - $this->show_twitter_xml_user($twitter_user); + $twitter_user = $this->twitterUserArray($user); + $this->showTwitterXmlUser($twitter_user); } } $this->elementEnd('users'); - $this->end_document('xml'); + $this->endDocument('xml'); } - function show_json_users($user) + function showJsonUsers($user) { - $this->init_document('json'); + $this->initDocument('json'); $users = array(); if (is_array($user)) { foreach ($user as $u) { - $twitter_user = $this->twitter_user_array($u); + $twitter_user = $this->twitterUserArray($u); array_push($users, $twitter_user); } } else { while ($user->fetch()) { - $twitter_user = $this->twitter_user_array($user); + $twitter_user = $this->twitterUserArray($user); array_push($users, $twitter_user); } } - $this->show_json_objects($users); - - $this->end_document('json'); - } + $this->showJsonObjects($users); - function show_single_json_group($group) - { - $this->init_document('json'); - $twitter_group = $this->twitter_group_array($group); - $this->show_json_objects($twitter_group); - $this->end_document('json'); + $this->endDocument('json'); } - function show_single_xml_group($group) + function showSingleJsonGroup($group) { - $this->init_document('xml'); - $twitter_group = $this->twitter_group_array($group); - $this->show_twitter_xml_group($twitter_group); - $this->end_document('xml'); + $this->initDocument('json'); + $twitter_group = $this->twitterGroupArray($group); + $this->showJsonObjects($twitter_group); + $this->endDocument('json'); } - // Anyone know what date format this is? - // Twitter's dates look like this: "Mon Jul 14 23:52:38 +0000 2008" -- Zach - function date_twitter($dt) + function showSingleXmlGroup($group) { - $t = strtotime($dt); - return date("D M d H:i:s O Y", $t); + $this->initDocument('xml'); + $twitter_group = $this->twitterGroupArray($group); + $this->showTwitterXmlGroup($twitter_group); + $this->endDocument('xml'); } - // XXX: Candidate for a general utility method somewhere? - function count_subscriptions($profile) + function dateTwitter($dt) { - - $count = 0; - $sub = new Subscription(); - $sub->subscribed = $profile->id; - - $count = $sub->find(); - - if ($count > 0) { - return $count - 1; - } else { - return 0; - } + $dateStr = date('d F Y H:i:s', strtotime($dt)); + $d = new DateTime($dateStr, new DateTimeZone('UTC')); + $d->setTimezone(new DateTimeZone(common_timezone())); + return $d->format('D M d H:i:s O Y'); } - function init_document($type='xml') + function initDocument($type='xml') { switch ($type) { case 'xml': @@ -896,11 +989,11 @@ class TwitterapiAction extends Action break; case 'rss': header("Content-Type: application/rss+xml; charset=utf-8"); - $this->init_twitter_rss(); + $this->initTwitterRss(); break; case 'atom': header('Content-Type: application/atom+xml; charset=utf-8'); - $this->init_twitter_atom(); + $this->initTwitterAtom(); break; default: $this->clientError(_('Not a supported data format.')); @@ -910,7 +1003,7 @@ class TwitterapiAction extends Action return; } - function end_document($type='xml') + function endDocument($type='xml') { switch ($type) { case 'xml': @@ -925,10 +1018,10 @@ class TwitterapiAction extends Action } break; case 'rss': - $this->end_twitter_rss(); + $this->endTwitterRss(); break; case 'atom': - $this->end_twitter_rss(); + $this->endTwitterRss(); break; default: $this->clientError(_('Not a supported data format.')); @@ -937,7 +1030,7 @@ class TwitterapiAction extends Action return; } - function clientError($msg, $code = 400, $content_type = 'json') + function clientError($msg, $code = 400, $format = 'xml') { $action = $this->trimmed('action'); @@ -951,20 +1044,23 @@ class TwitterapiAction extends Action header('HTTP/1.1 '.$code.' '.$status_string); - if ($content_type == 'xml') { - $this->init_document('xml'); + if ($format == 'xml') { + $this->initDocument('xml'); $this->elementStart('hash'); $this->element('error', null, $msg); $this->element('request', null, $_SERVER['REQUEST_URI']); $this->elementEnd('hash'); - $this->end_document('xml'); - } else { - $this->init_document('json'); + $this->endDocument('xml'); + } elseif ($format == 'json'){ + $this->initDocument('json'); $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']); print(json_encode($error_array)); - $this->end_document('json'); - } + $this->endDocument('json'); + } else { + // If user didn't request a useful format, throw a regular client error + throw new ClientException($msg, $code); + } } function serverError($msg, $code = 500, $content_type = 'json') @@ -982,56 +1078,60 @@ class TwitterapiAction extends Action header('HTTP/1.1 '.$code.' '.$status_string); if ($content_type == 'xml') { - $this->init_document('xml'); + $this->initDocument('xml'); $this->elementStart('hash'); $this->element('error', null, $msg); $this->element('request', null, $_SERVER['REQUEST_URI']); $this->elementEnd('hash'); - $this->end_document('xml'); + $this->endDocument('xml'); } else { - $this->init_document('json'); + $this->initDocument('json'); $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']); print(json_encode($error_array)); - $this->end_document('json'); + $this->endDocument('json'); } } - function init_twitter_rss() + function initTwitterRss() { $this->startXML(); - $this->elementStart('rss', array('version' => '2.0')); + $this->elementStart('rss', array('version' => '2.0', 'xmlns:atom'=>'http://www.w3.org/2005/Atom')); + $this->elementStart('channel'); + Event::handle('StartApiRss', array($this)); } - function end_twitter_rss() + function endTwitterRss() { + $this->elementEnd('channel'); $this->elementEnd('rss'); $this->endXML(); } - function init_twitter_atom() + function initTwitterAtom() { $this->startXML(); // FIXME: don't hardcode the language here! $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom', 'xml:lang' => 'en-US', 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0')); + Event::handle('StartApiAtom', array($this)); } - function end_twitter_atom() + function endTwitterAtom() { $this->elementEnd('feed'); $this->endXML(); } - function show_profile($profile, $content_type='xml', $notice=null, $includeStatuses=true) + function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true) { - $profile_array = $this->twitter_user_array($profile, $includeStatuses); + $profile_array = $this->twitterUserArray($profile, $includeStatuses); switch ($content_type) { case 'xml': - $this->show_twitter_xml_user($profile_array); + $this->showTwitterXmlUser($profile_array); break; case 'json': - $this->show_json_objects($profile_array); + $this->showJsonObjects($profile_array); break; default: $this->clientError(_('Not a supported data format.')); @@ -1040,7 +1140,7 @@ class TwitterapiAction extends Action return; } - function get_user($id, $apidata=null) + function getTargetUser($id) { if (empty($id)) { @@ -1061,7 +1161,7 @@ class TwitterapiAction extends Action return User::staticGet('nickname', $nickname); } else { // Fall back to trying the currently authenticated user - return $apidata['user']; + return $this->auth_user; } } else if (is_numeric($id)) { @@ -1072,10 +1172,9 @@ class TwitterapiAction extends Action } } - function get_group($id, $apidata=null) + function getTargetGroup($id) { if (empty($id)) { - if (is_numeric($this->arg('id'))) { return User_group::staticGet($this->arg('id')); } else if ($this->arg('id')) { @@ -1100,21 +1199,7 @@ class TwitterapiAction extends Action } } - function get_profile($id) - { - if (is_numeric($id)) { - return Profile::staticGet($id); - } else { - $user = User::staticGet('nickname', $id); - if ($user) { - return $user->getProfile(); - } else { - return null; - } - } - } - - function source_link($source) + function sourceLink($source) { $source_name = _($source); switch ($source) { diff --git a/lib/apiauth.php b/lib/apiauth.php new file mode 100644 index 000000000..0d1613d38 --- /dev/null +++ b/lib/apiauth.php @@ -0,0 +1,207 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Base class for API actions that require authentication + * + * 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 API + * @package StatusNet + * @author Adrian Lang <mail@adrianlang.de> + * @author Brenda Wallace <shiny@cpan.org> + * @author Craig Andrews <candrews@integralblue.com> + * @author Dan Moore <dan@moore.cx> + * @author Evan Prodromou <evan@status.net> + * @author mEDI <medi@milaro.net> + * @author Sarven Capadisli <csarven@status.net> + * @author Zach Copley <zach@status.net> + * @copyright 2009 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); +} + +require_once INSTALLDIR . '/lib/api.php'; + +/** + * Actions extending this class will require auth + * + * @category API + * @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 ApiAuthAction extends ApiAction +{ + + var $auth_user = null; + + /** + * Take arguments for running, and output basic auth header if needed + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + + function prepare($args) + { + parent::prepare($args); + + if ($this->requiresAuth()) { + $this->checkBasicAuthUser(); + } + + return true; + } + + /** + * Does this API resource require authentication? + * + * @return boolean true + */ + + function requiresAuth() + { + return true; + } + + /** + * Check for a user specified via HTTP basic auth. If there isn't + * one, try to get one by outputting the basic auth header. + * + * @return boolean true or false + */ + + function checkBasicAuthUser() + { + $this->basicAuthProcessHeader(); + + $realm = common_config('site', 'name') . ' API'; + + if (!isset($this->auth_user)) { + header('WWW-Authenticate: Basic realm="' . $realm . '"'); + + // show error if the user clicks 'cancel' + + $this->showBasicAuthError(); + exit; + + } else { + $nickname = $this->auth_user; + $password = $this->auth_pw; + $user = common_check_user($nickname, $password); + if (Event::handle('StartSetApiUser', array(&$user))) { + $this->auth_user = $user; + Event::handle('EndSetApiUser', array($user)); + } + + if (empty($this->auth_user)) { + + // basic authentication failed + + list($proxy, $ip) = common_client_ip(); + common_log( + LOG_WARNING, + 'Failed API auth attempt, nickname = ' . + "$nickname, proxy = $proxy, ip = $ip." + ); + $this->showBasicAuthError(); + exit; + } + } + return true; + } + + /** + * Read the HTTP headers and set the auth user. Decodes HTTP_AUTHORIZATION + * param to support basic auth when PHP is running in CGI mode. + * + * @return void + */ + + function basicAuthProcessHeader() + { + if (isset($_SERVER['AUTHORIZATION']) + || isset($_SERVER['HTTP_AUTHORIZATION']) + ) { + $authorization_header = isset($_SERVER['HTTP_AUTHORIZATION']) + ? $_SERVER['HTTP_AUTHORIZATION'] : $_SERVER['AUTHORIZATION']; + } + + if (isset($_SERVER['PHP_AUTH_USER'])) { + $this->auth_user = $_SERVER['PHP_AUTH_USER']; + $this->auth_pw = $_SERVER['PHP_AUTH_PW']; + } elseif (isset($authorization_header) + && strstr(substr($authorization_header, 0, 5), 'Basic')) { + + // decode the HTTP_AUTHORIZATION header on php-cgi server self + // on fcgid server the header name is AUTHORIZATION + + $auth_hash = base64_decode(substr($authorization_header, 6)); + list($this->auth_user, $this->auth_pw) = explode(':', $auth_hash); + + // set all to null on a empty basic auth request + + if ($this->auth_user == "") { + $this->auth_user = null; + $this->auth_pw = null; + } + } else { + $this->auth_user = null; + $this->auth_pw = null; + } + } + + /** + * Output an authentication error message. Use XML or JSON if one + * of those formats is specified, otherwise output plain text + * + * @return void + */ + + function showBasicAuthError() + { + header('HTTP/1.1 401 Unauthorized'); + $msg = 'Could not authenticate you.'; + + if ($this->format == 'xml') { + header('Content-Type: application/xml; charset=utf-8'); + $this->startXML(); + $this->elementStart('hash'); + $this->element('error', null, $msg); + $this->element('request', null, $_SERVER['REQUEST_URI']); + $this->elementEnd('hash'); + $this->endXML(); + } elseif ($this->format == 'json') { + header('Content-Type: application/json; charset=utf-8'); + $error_array = array('error' => $msg, + 'request' => $_SERVER['REQUEST_URI']); + print(json_encode($error_array)); + } else { + header('Content-type: text/plain'); + print "$msg\n"; + } + } + +} diff --git a/lib/apibareauth.php b/lib/apibareauth.php new file mode 100644 index 000000000..2d29c1ddd --- /dev/null +++ b/lib/apibareauth.php @@ -0,0 +1,109 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Base class for API actions that require "bare auth". Bare auth means + * authentication is required only if the action is called without an argument + * or query param specifying user id. + * + * 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 API + * @package StatusNet + * @author Adrian Lang <mail@adrianlang.de> + * @author Brenda Wallace <shiny@cpan.org> + * @author Craig Andrews <candrews@integralblue.com> + * @author Dan Moore <dan@moore.cx> + * @author Evan Prodromou <evan@status.net> + * @author mEDI <medi@milaro.net> + * @author Sarven Capadisli <csarven@status.net> + * @author Zach Copley <zach@status.net> + * @copyright 2009 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); +} + +require_once INSTALLDIR.'/lib/apiauth.php'; + +/** + * Actions extending this class will require auth unless a target + * user ID has been specified + * + * @category API + * @package StatusNet + * @author Adrian Lang <mail@adrianlang.de> + * @author Brenda Wallace <shiny@cpan.org> + * @author Craig Andrews <candrews@integralblue.com> + * @author Dan Moore <dan@moore.cx> + * @author Evan Prodromou <evan@status.net> + * @author mEDI <medi@milaro.net> + * @author Sarven Capadisli <csarven@status.net> + * @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 ApiBareAuthAction extends ApiAuthAction +{ + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + + function prepare($args) + { + parent::prepare($args); + return true; + } + + /** + * Does this API resource require authentication? + * + * @return boolean true or false + */ + + function requiresAuth() + { + // If the site is "private", all API methods except statusnet/config + // need authentication + + if (common_config('site', 'private')) { + return true; + } + + // check whether a user has been specified somehow + + $id = $this->arg('id'); + $user_id = $this->arg('user_id'); + $screen_name = $this->arg('screen_name'); + + if (empty($id) && empty($user_id) && empty($screen_name)) { + return true; + } + + return false; + } + +}
\ No newline at end of file diff --git a/lib/apiprivateauth.php b/lib/apiprivateauth.php new file mode 100644 index 000000000..5d0033005 --- /dev/null +++ b/lib/apiprivateauth.php @@ -0,0 +1,82 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Base class for API actions that only require auth when a site + * is configured to be private + * + * 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 API + * @package StatusNet + * @author Adrian Lang <mail@adrianlang.de> + * @author Brenda Wallace <shiny@cpan.org> + * @author Craig Andrews <candrews@integralblue.com> + * @author Dan Moore <dan@moore.cx> + * @author Evan Prodromou <evan@status.net> + * @author mEDI <medi@milaro.net> + * @author Sarven Capadisli <csarven@status.net> + * @author Zach Copley <zach@status.net> + * @copyright 2009 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); +} + +require_once INSTALLDIR.'/lib/apiauth.php'; + +/** + * Actions extending this class will require auth only if a site is private + * + * @category API + * @package StatusNet + * @author Adrian Lang <mail@adrianlang.de> + * @author Brenda Wallace <shiny@cpan.org> + * @author Craig Andrews <candrews@integralblue.com> + * @author Dan Moore <dan@moore.cx> + * @author Evan Prodromou <evan@status.net> + * @author mEDI <medi@milaro.net> + * @author Sarven Capadisli <csarven@status.net> + * @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 ApiPrivateAuthAction extends ApiAuthAction +{ + + /** + * Does this API resource require authentication? + * + * @return boolean true or false + */ + + function requiresAuth() + { + // If the site is "private", all API methods except statusnet/config + // need authentication + + if (common_config('site', 'private')) { + return true; + } + + return false; + } + +} diff --git a/lib/blockform.php b/lib/blockform.php index 4820d09af..b6652b1f6 100644 --- a/lib/blockform.php +++ b/lib/blockform.php @@ -32,8 +32,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR.'/lib/form.php'; - /** * Form for blocking a user * @@ -47,109 +45,38 @@ require_once INSTALLDIR.'/lib/form.php'; * @see UnblockForm */ -class BlockForm extends Form +class BlockForm extends ProfileActionForm { /** - * Profile of user to block - */ - - var $profile = null; - - /** - * Return-to args - */ - - var $args = null; - - /** - * Constructor + * Action this form provides * - * @param HTMLOutputter $out output channel - * @param Profile $profile profile of user to block - * @param array $args return-to args + * @return string Name of the action, lowercased. */ - function __construct($out=null, $profile=null, $args=null) + function target() { - parent::__construct($out); - - $this->profile = $profile; - $this->args = $args; + return 'block'; } /** - * ID of the form - * - * @return int ID of the form - */ - - function id() - { - return 'block-' . $this->profile->id; - } - - - /** - * class of the form - * - * @return string class of the form - */ - - function formClass() - { - return 'form_user_block'; - } - - - /** - * Action of the form - * - * @return string URL of the action - */ - - function action() - { - return common_local_url('block'); - } - - - /** - * Legend of the Form - * - * @return void - */ - function formLegend() - { - $this->out->element('legend', null, _('Block this user')); - } - - - /** - * Data elements of the form + * Title of the form * - * @return void + * @return string Title of the form, internationalized */ - function formData() + function title() { - $this->out->hidden('blockto-' . $this->profile->id, - $this->profile->id, - 'blockto'); - if ($this->args) { - foreach ($this->args as $k => $v) { - $this->out->hidden('returnto-' . $k, $v); - } - } + return _('Block'); } /** - * Action elements + * Description of the form * - * @return void + * @return string description of the form, internationalized */ - function formActions() + function description() { - $this->out->submit('submit', _('Block'), 'submit', null, _('Block this user')); + return _('Block this user'); } } diff --git a/lib/command.php b/lib/command.php index 01b14f83e..7e98156b6 100644 --- a/lib/command.php +++ b/lib/command.php @@ -73,7 +73,7 @@ class UntrackCommand extends UnimplementedCommand } } -class NudgeCommand extends UnimplementedCommand +class NudgeCommand extends Command { var $other = null; function __construct($user, $other) @@ -81,6 +81,26 @@ class NudgeCommand extends UnimplementedCommand parent::__construct($user); $this->other = $other; } + function execute($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)); + } + } + } } class InviteCommand extends UnimplementedCommand @@ -124,18 +144,30 @@ class FavCommand extends Command function execute($channel) { + if(substr($this->other,0,1)=='#'){ + //favoriting a specific notice_id - $recipient = - common_relative_profile($this->user, common_canonical_nickname($this->other)); + $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 - 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; + $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); @@ -312,16 +344,20 @@ class MessageCommand extends Command function execute($channel) { $other = User::staticGet('nickname', common_canonical_nickname($this->other)); + $len = mb_strlen($this->text); + if ($len == 0) { $channel->error($this->user, _('No content!')); return; - } else if ($len > 140) { - $content = common_shorten_links($content); - if (mb_strlen($content) > 140) { - $channel->error($this->user, sprintf(_('Message too long - maximum is 140 characters, you sent %d'), $len)); - return; - } + } + + $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'), + Message::maxContent(), mb_strlen($this->text))); + return; } if (!$other) { @@ -343,6 +379,71 @@ class MessageCommand extends Command } } +class ReplyCommand extends Command +{ + var $other = null; + var $text = null; + function __construct($user, $other, $text) + { + parent::__construct($user); + $this->other = $other; + $this->text = $text; + } + + function execute($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; + } + } + + $len = mb_strlen($this->text); + + if ($len == 0) { + $channel->error($this->user, _('No content!')); + return; + } + + $this->text = common_shorten_links($this->text); + + if (Notice::contentTooLong($this->text)) { + $channel->error($this->user, sprintf(_('Notice too long - maximum is %d characters, you sent %d'), + Notice::maxContent(), mb_strlen($this->text))); + return; + } + + $notice = Notice::saveNew($this->user->id, $this->text, $channel->source(), 1, + $notice->id); + if ($notice) { + $channel->output($this->user, sprintf(_('Reply to %s sent'), $recipient->nickname)); + } else { + $channel->error($this->user, _('Error saving notice.')); + } + common_broadcast_notice($notice); + } +} + class GetCommand extends Command { @@ -478,6 +579,97 @@ class OnCommand extends Command } } +class LoginCommand extends Command +{ + function execute($channel) + { + $login_token = Login_token::staticGet('user_id',$this->user->id); + if($login_token){ + $login_token->delete(); + } + $login_token = new Login_token(); + $login_token->user_id = $this->user->id; + $login_token->token = common_good_rand(16); + $login_token->created = common_sql_now(); + $result = $login_token->insert(); + if (!$result) { + common_log_db_error($login_token, 'INSERT', __FILE__); + $channel->error($this->user, sprintf(_('Could not create login token for %s'), + $this->user->nickname)); + return; + } + $channel->output($this->user, + sprintf(_('This link is useable only once, and is good for only 2 minutes: %s'), + common_local_url('login', + array('user_id'=>$login_token->user_id, 'token'=>$login_token->token)))); + } +} + +class SubscriptionsCommand extends Command +{ + function execute($channel) + { + $profile = $this->user->getSubscriptions(0); + $nicknames=array(); + while ($profile->fetch()) { + $nicknames[]=$profile->nickname; + } + if(count($nicknames)==0){ + $out=_('You are not subscribed to anyone.'); + }else{ + $out = ngettext('You are subscribed to this person:', + 'You are subscribed to these people:', + count($nicknames)); + $out .= ' '; + $out .= implode(', ',$nicknames); + } + $channel->output($this->user,$out); + } +} + +class SubscribersCommand extends Command +{ + function execute($channel) + { + $profile = $this->user->getSubscribers(); + $nicknames=array(); + while ($profile->fetch()) { + $nicknames[]=$profile->nickname; + } + if(count($nicknames)==0){ + $out=_('No one is subscribed to you.'); + }else{ + $out = ngettext('This person is subscribed to you:', + 'These people are subscribed to you:', + count($nicknames)); + $out .= ' '; + $out .= implode(', ',$nicknames); + } + $channel->output($this->user,$out); + } +} + +class GroupsCommand extends Command +{ + function execute($channel) + { + $group = $this->user->getGroups(); + $groups=array(); + while ($group->fetch()) { + $groups[]=$group->nickname; + } + if(count($groups)==0){ + $out=_('You are not a member of any groups.'); + }else{ + $out = ngettext('You are a member of this group:', + 'You are a member of these groups:', + count($nicknames)); + $out.=implode(', ',$groups); + } + $channel->output($this->user,$out); + } +} + class HelpCommand extends Command { function execute($channel) @@ -488,12 +680,19 @@ class HelpCommand extends Command "off - turn off notifications\n". "help - show this help\n". "follow <nickname> - subscribe to user\n". + "groups - lists the groups you have joined\n". + "subscriptions - list the people you follow\n". + "subscribers - list the people that follow you\n". "leave <nickname> - unsubscribe from user\n". "d <nickname> <text> - direct message to user\n". "get <nickname> - get last notice from user\n". "whois <nickname> - get profile info on user\n". "fav <nickname> - add user's last notice as a 'fave'\n". + "fav #<notice_id> - add notice with the given id as a 'fave'\n". + "reply #<notice_id> - reply to notice with a given id\n". + "reply <nickname> - reply to the last notice from user\n". "join <group> - join group\n". + "login - Get a link to login to the web interface\n". "drop <group> - leave group\n". "stats - get your stats\n". "stop - same as 'off'\n". @@ -503,7 +702,7 @@ class HelpCommand extends Command "last <nickname> - same as 'get'\n". "on <nickname> - not yet implemented.\n". "off <nickname> - not yet implemented.\n". - "nudge <nickname> - not yet implemented.\n". + "nudge <nickname> - remind a user to update.\n". "invite <phone number> - not yet implemented.\n". "track <word> - not yet implemented.\n". "untrack <word> - not yet implemented.\n". diff --git a/lib/commandinterpreter.php b/lib/commandinterpreter.php index 6e4340e5d..665015afc 100644 --- a/lib/commandinterpreter.php +++ b/lib/commandinterpreter.php @@ -28,7 +28,7 @@ class CommandInterpreter # XXX: localise $text = preg_replace('/\s+/', ' ', trim($text)); - list($cmd, $arg) = explode(' ', $text, 2); + list($cmd, $arg) = $this->split_arg($text); # We try to support all the same commands as Twitter, see # http://getsatisfaction.com/twitter/topics/what_are_the_twitter_commands @@ -41,9 +41,33 @@ class CommandInterpreter return null; } return new HelpCommand($user); + case 'login': + if ($arg) { + return null; + } else { + return new LoginCommand($user); + } + case 'subscribers': + if ($arg) { + return null; + } else { + return new SubscribersCommand($user); + } + case 'subscriptions': + if ($arg) { + return null; + } else { + return new SubscriptionsCommand($user); + } + case 'groups': + if ($arg) { + return null; + } else { + return new GroupsCommand($user); + } case 'on': if ($arg) { - list($other, $extra) = explode(' ', $arg, 2); + list($other, $extra) = $this->split_arg($arg); if ($extra) { return null; } else { @@ -54,7 +78,7 @@ class CommandInterpreter } case 'off': if ($arg) { - list($other, $extra) = explode(' ', $arg, 2); + list($other, $extra) = $this->split_arg($arg); if ($extra) { return null; } else { @@ -74,7 +98,7 @@ class CommandInterpreter if (!$arg) { return null; } - list($other, $extra) = explode(' ', $arg, 2); + list($other, $extra) = $this->split_arg($arg); if ($extra) { return null; } else { @@ -84,7 +108,7 @@ class CommandInterpreter if (!$arg) { return null; } - list($other, $extra) = explode(' ', $arg, 2); + list($other, $extra) = $this->split_arg($arg); if ($extra) { return null; } else { @@ -95,7 +119,7 @@ class CommandInterpreter if (!$arg) { return null; } - list($other, $extra) = explode(' ', $arg, 2); + list($other, $extra) = $this->split_arg($arg); if ($extra) { return null; } else { @@ -106,7 +130,7 @@ class CommandInterpreter if (!$arg) { return null; } - list($other, $extra) = explode(' ', $arg, 2); + list($other, $extra) = $this->split_arg($arg); if ($extra) { return null; } else { @@ -117,7 +141,7 @@ class CommandInterpreter if (!$arg) { return null; } - list($other, $extra) = explode(' ', $arg, 2); + list($other, $extra) = $this->split_arg($arg); if ($extra) { return null; } else { @@ -128,17 +152,28 @@ class CommandInterpreter if (!$arg) { return null; } - list($other, $extra) = explode(' ', $arg, 2); + list($other, $extra) = $this->split_arg($arg); if (!$extra) { return null; } else { return new MessageCommand($user, $other, $extra); } + case 'r': + case 'reply': + if (!$arg) { + return null; + } + list($other, $extra) = $this->split_arg($arg); + if (!$extra) { + return null; + } else { + return new ReplyCommand($user, $other, $extra); + } case 'whois': if (!$arg) { return null; } - list($other, $extra) = explode(' ', $arg, 2); + list($other, $extra) = $this->split_arg($arg); if ($extra) { return null; } else { @@ -148,7 +183,7 @@ class CommandInterpreter if (!$arg) { return null; } - list($other, $extra) = explode(' ', $arg, 2); + list($other, $extra) = $this->split_arg($arg); if ($extra) { return null; } else { @@ -158,7 +193,7 @@ class CommandInterpreter if (!$arg) { return null; } - list($other, $extra) = explode(' ', $arg, 2); + list($other, $extra) = $this->split_arg($arg); if ($extra) { return null; } else { @@ -173,7 +208,7 @@ class CommandInterpreter if (!$arg) { return null; } - list($other, $extra) = explode(' ', $arg, 2); + list($other, $extra) = $this->split_arg($arg); if ($extra) { return null; } else { @@ -183,7 +218,7 @@ class CommandInterpreter if (!$arg) { return null; } - list($word, $extra) = explode(' ', $arg, 2); + list($word, $extra) = $this->split_arg($arg); if ($extra) { return null; } else if ($word == 'off') { @@ -195,7 +230,7 @@ class CommandInterpreter if (!$arg) { return null; } - list($word, $extra) = explode(' ', $arg, 2); + list($word, $extra) = $this->split_arg($arg); if ($extra) { return null; } else if ($word == 'all') { @@ -213,5 +248,17 @@ class CommandInterpreter return false; } } + + /** + * Split arguments without triggering a PHP notice warning + */ + function split_arg($text) + { + $pieces = explode(' ', $text, 2); + if (count($pieces) == 1) { + $pieces[] = null; + } + return $pieces; + } } diff --git a/lib/common.php b/lib/common.php index 7af376d1b..732c22bfd 100644 --- a/lib/common.php +++ b/lib/common.php @@ -19,10 +19,13 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -define('STATUSNET_VERSION', '0.8.2'); +//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.0dev'); define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility -define('STATUSNET_CODENAME', 'Life and How to Live It'); +define('STATUSNET_CODENAME', 'Stand'); define('AVATAR_PROFILE_SIZE', 96); define('AVATAR_STREAM_SIZE', 48); @@ -38,21 +41,24 @@ define('FOREIGN_NOTICE_SEND_REPLY', 4); define('FOREIGN_FRIEND_SEND', 1); define('FOREIGN_FRIEND_RECV', 2); -define_syslog_variables(); - # append our extlib dir as the last-resort place to find libs set_include_path(get_include_path() . PATH_SEPARATOR . INSTALLDIR . '/extlib/'); +# To protect against upstream libraries which haven't updated +# for PHP 5.3 where dl() function may not be present... +if (!function_exists('dl')) { + function dl($library) { + return false; + } +} + # global configuration object require_once('PEAR.php'); require_once('DB/DataObject.php'); require_once('DB/DataObject/Cast.php'); # for dates -if (!function_exists('gettext')) { - require_once("php-gettext/gettext.inc"); -} require_once(INSTALLDIR.'/lib/language.php'); // This gets included before the config file, so that admin code and plugins @@ -73,6 +79,9 @@ function _sn_to_path($sn) return $p; } +// Save our sanity when code gets loaded through subroutines such as PHPUnit tests +global $default, $config, $_server, $_path; + // try to figure out where we are. $server and $path // can be set by including module, else we guess based // on HTTP info. @@ -93,206 +102,17 @@ if (isset($path)) { null; } -// default configuration, overwritten in config.php +require_once(INSTALLDIR.'/lib/default.php'); + +// Set config values initially to default values -$config = - array('site' => - array('name' => 'Just another StatusNet microblog', - 'server' => $_server, - 'theme' => 'default', - 'path' => $_path, - 'logfile' => null, - 'logo' => null, - 'logdebug' => false, - 'fancy' => false, - 'locale_path' => INSTALLDIR.'/locale', - 'language' => 'en_US', - 'languages' => get_all_languages(), - 'email' => - array_key_exists('SERVER_ADMIN', $_SERVER) ? $_SERVER['SERVER_ADMIN'] : null, - 'broughtby' => null, - 'timezone' => 'UTC', - 'broughtbyurl' => null, - 'closed' => false, - 'inviteonly' => false, - 'openidonly' => false, - 'private' => false, - 'ssl' => 'never', - 'sslserver' => null, - 'shorturllength' => 30, - 'dupelimit' => 60), # default for same person saying the same thing - 'syslog' => - array('appname' => 'statusnet', # for syslog - 'priority' => 'debug', # XXX: currently ignored - 'facility' => LOG_USER), - 'queue' => - array('enabled' => false, - 'subsystem' => 'db', # default to database, or 'stomp' - 'stomp_server' => null, - 'queue_basename' => 'statusnet', - 'stomp_username' => null, - 'stomp_password' => null, - ), - 'license' => - array('url' => 'http://creativecommons.org/licenses/by/3.0/', - 'title' => 'Creative Commons Attribution 3.0', - 'image' => 'http://i.creativecommons.org/l/by/3.0/80x15.png'), - 'mail' => - array('backend' => 'mail', - 'params' => null), - 'nickname' => - array('blacklist' => array(), - 'featured' => array()), - 'profile' => - array('banned' => array()), - 'avatar' => - array('server' => null, - 'dir' => INSTALLDIR . '/avatar/', - 'path' => $_path . '/avatar/'), - 'background' => - array('server' => null, - 'dir' => INSTALLDIR . '/background/', - 'path' => $_path . '/background/'), - 'public' => - array('localonly' => true, - 'blacklist' => array(), - 'autosource' => array()), - 'theme' => - array('server' => null, - 'dir' => null, - 'path'=> null), - 'throttle' => - array('enabled' => false, // whether to throttle edits; false by default - 'count' => 20, // number of allowed messages in timespan - 'timespan' => 600), // timespan for throttling - 'xmpp' => - array('enabled' => false, - 'server' => 'INVALID SERVER', - 'port' => 5222, - 'user' => 'update', - 'encryption' => true, - 'resource' => 'uniquename', - 'password' => 'blahblahblah', - 'host' => null, # only set if != server - 'debug' => false, # print extra debug info - 'public' => array()), # JIDs of users who want to receive the public stream - 'openid' => - array('enabled' => true), - 'invite' => - array('enabled' => true), - 'sphinx' => - array('enabled' => false, - 'server' => 'localhost', - 'port' => 3312), - 'tag' => - array('dropoff' => 864000.0), - 'popular' => - array('dropoff' => 864000.0), - 'daemon' => - array('piddir' => '/var/run', - 'user' => false, - 'group' => false), - 'emailpost' => - array('enabled' => true), - 'sms' => - array('enabled' => true), - 'twitterbridge' => - array('enabled' => false), - 'integration' => - array('source' => 'StatusNet', # source attribute for Twitter - 'taguri' => $_server.',2009'), # base for tag URIs - 'twitter' => - array('enabled' => true, - 'consumer_key' => null, - 'consumer_secret' => null), - 'memcached' => - array('enabled' => false, - 'server' => 'localhost', - 'base' => null, - 'port' => 11211), - 'ping' => - array('notify' => array()), - 'inboxes' => - array('enabled' => true), # on by default for new sites - 'newuser' => - array('default' => null, - 'welcome' => null), - 'snapshot' => - array('run' => 'web', - 'frequency' => 10000, - 'reporturl' => 'http://status.net/stats/report'), - 'attachments' => - array('server' => null, - 'dir' => INSTALLDIR . '/file/', - 'path' => $_path . '/file/', - 'supported' => array('image/png', - 'image/jpeg', - 'image/gif', - 'image/svg+xml', - 'audio/mpeg', - 'audio/x-speex', - 'application/ogg', - 'application/pdf', - 'application/vnd.oasis.opendocument.text', - 'application/vnd.oasis.opendocument.text-template', - 'application/vnd.oasis.opendocument.graphics', - 'application/vnd.oasis.opendocument.graphics-template', - 'application/vnd.oasis.opendocument.presentation', - 'application/vnd.oasis.opendocument.presentation-template', - 'application/vnd.oasis.opendocument.spreadsheet', - 'application/vnd.oasis.opendocument.spreadsheet-template', - 'application/vnd.oasis.opendocument.chart', - 'application/vnd.oasis.opendocument.chart-template', - 'application/vnd.oasis.opendocument.image', - 'application/vnd.oasis.opendocument.image-template', - 'application/vnd.oasis.opendocument.formula', - 'application/vnd.oasis.opendocument.formula-template', - 'application/vnd.oasis.opendocument.text-master', - 'application/vnd.oasis.opendocument.text-web', - 'application/x-zip', - 'application/zip', - 'text/plain', - 'video/mpeg', - 'video/mp4', - 'video/quicktime', - 'video/mpeg'), - 'file_quota' => 5000000, - 'user_quota' => 50000000, - 'monthly_quota' => 15000000, - 'uploads' => true, - 'filecommand' => '/usr/bin/file', - ), - 'group' => - array('maxaliases' => 3), - 'oohembed' => array('endpoint' => 'http://oohembed.com/oohembed/'), - 'search' => - array('type' => 'fulltext'), - 'sessions' => - array('handle' => false, // whether to handle sessions ourselves - 'debug' => false), // debugging output for sessions - 'design' => - array('backgroundcolor' => null, // null -> 'use theme default' - 'contentcolor' => null, - 'sidebarcolor' => null, - 'textcolor' => null, - 'linkcolor' => null, - 'backgroundimage' => null, - 'disposition' => null), - ); +$config = $default; + +// default configuration, overwritten in config.php $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options'); -$config['db'] = - array('database' => 'YOU HAVE TO SET THIS IN config.php', - 'schema_location' => INSTALLDIR . '/classes', - 'class_location' => INSTALLDIR . '/classes', - 'require_prefix' => 'classes/', - 'class_prefix' => '', - 'mirror' => null, - 'utf8' => true, - 'db_driver' => 'DB', # XXX: JanRain libs only work with DB - 'quote_identifiers' => false, - 'type' => 'mysql' ); +$config['db'] = $default['db']; // Backward compatibility @@ -357,6 +177,7 @@ if (isset($conffile)) { $_config_files[] = INSTALLDIR.'/config.php'; } +global $_have_a_config; $_have_a_config = false; foreach ($_config_files as $_config_file) { @@ -373,7 +194,14 @@ function _have_config() } // XXX: Throw a conniption if database not installed - +// XXX: Find a way to use htmlwriter for this instead of handcoded markup +if (!_have_config()) { + echo '<p>'. _('No configuration file found. ') .'</p>'; + echo '<p>'. _('I looked for configuration files in the following places: ') .'<br/> '. implode($_config_files, '<br/>'); + echo '<p>'. _('You may wish to run the installer to fix this.') .'</p>'; + echo '<a href="install.php">'. _('Go to the installer.') .'</a>'; + exit; +} // Fixup for statusnet.ini $_db_name = substr($config['db']['database'], strrpos($config['db']['database'], '/') + 1); @@ -382,46 +210,51 @@ if ($_db_name != 'statusnet' && !array_key_exists('ini_'.$_db_name, $config['db' $config['db']['ini_'.$_db_name] = INSTALLDIR.'/classes/statusnet.ini'; } -// Ignore openidonly if OpenID is disabled - -if (!$config['openid']['enabled']) { - $config['site']['openidonly'] = false; +function __autoload($cls) +{ + if (file_exists(INSTALLDIR.'/classes/' . $cls . '.php')) { + require_once(INSTALLDIR.'/classes/' . $cls . '.php'); + } else if (file_exists(INSTALLDIR.'/lib/' . strtolower($cls) . '.php')) { + require_once(INSTALLDIR.'/lib/' . strtolower($cls) . '.php'); + } else if (mb_substr($cls, -6) == 'Action' && + file_exists(INSTALLDIR.'/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php')) { + require_once(INSTALLDIR.'/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php'); + } else if ($cls == 'OAuthRequest') { + require_once('OAuth.php'); + } else { + Event::handle('Autoload', array(&$cls)); + } } // XXX: how many of these could be auto-loaded on use? +// XXX: note that these files should not use config options +// at compile time since DB config options are not yet loaded. require_once 'Validate.php'; require_once 'markdown.php'; require_once INSTALLDIR.'/lib/util.php'; require_once INSTALLDIR.'/lib/action.php'; -require_once INSTALLDIR.'/lib/theme.php'; require_once INSTALLDIR.'/lib/mail.php'; require_once INSTALLDIR.'/lib/subs.php'; -require_once INSTALLDIR.'/lib/Shorturl_api.php'; -require_once INSTALLDIR.'/lib/twitter.php'; require_once INSTALLDIR.'/lib/clientexception.php'; require_once INSTALLDIR.'/lib/serverexception.php'; -// XXX: other formats here +// Load settings from database; note we need autoload for this -define('NICKNAME_FMT', VALIDATE_NUM.VALIDATE_ALPHA_LOWER); +Config::loadSettings(); -function __autoload($class) -{ - if ($class == 'OAuthRequest') { - require_once('OAuth.php'); - } else if (file_exists(INSTALLDIR.'/classes/' . $class . '.php')) { - require_once(INSTALLDIR.'/classes/' . $class . '.php'); - } else if (file_exists(INSTALLDIR.'/lib/' . strtolower($class) . '.php')) { - require_once(INSTALLDIR.'/lib/' . strtolower($class) . '.php'); - } else if (mb_substr($class, -6) == 'Action' && - file_exists(INSTALLDIR.'/actions/' . strtolower(mb_substr($class, 0, -6)) . '.php')) { - require_once(INSTALLDIR.'/actions/' . strtolower(mb_substr($class, 0, -6)) . '.php'); - } +// XXX: if plugins should check the schema at runtime, do that here. + +if ($config['db']['schemacheck'] == 'runtime') { + Event::handle('CheckSchema'); } +// XXX: other formats here + +define('NICKNAME_FMT', VALIDATE_NUM.VALIDATE_ALPHA_LOWER); + // Give plugins a chance to initialize in a fully-prepared environment Event::handle('InitializePlugin'); diff --git a/lib/connectsettingsaction.php b/lib/connectsettingsaction.php index 2095a7ceb..e5fb8727b 100644 --- a/lib/connectsettingsaction.php +++ b/lib/connectsettingsaction.php @@ -98,34 +98,37 @@ class ConnectSettingsNav extends Widget function show() { - # action => array('prompt', 'title') - $menu = array(); - if (common_config('xmpp', 'enabled')) { - $menu['imsettings'] = - array(_('IM'), - _('Updates by instant messenger (IM)')); - } - if (common_config('sms', 'enabled')) { - $menu['smssettings'] = - array(_('SMS'), - _('Updates by SMS')); - } - if (common_config('twitter', 'enabled')) { - $menu['twittersettings'] = - array(_('Twitter'), - _('Twitter integration options')); - } - $action_name = $this->action->trimmed('action'); $this->action->elementStart('ul', array('class' => 'nav')); - foreach ($menu as $menuaction => $menudesc) { - $this->action->menuItem(common_local_url($menuaction), - $menudesc[0], - $menudesc[1], - $action_name === $menuaction); + if (Event::handle('StartConnectSettingsNav', array(&$this->action))) { + + # action => array('prompt', 'title') + $menu = array(); + if (common_config('xmpp', 'enabled')) { + $menu['imsettings'] = + array(_('IM'), + _('Updates by instant messenger (IM)')); + } + if (common_config('sms', 'enabled')) { + $menu['smssettings'] = + array(_('SMS'), + _('Updates by SMS')); + } + + foreach ($menu as $menuaction => $menudesc) { + $this->action->menuItem(common_local_url($menuaction), + $menudesc[0], + $menudesc[1], + $action_name === $menuaction); + } + + Event::handle('EndConnectSettingsNav', array(&$this->action)); } $this->action->elementEnd('ul'); } + } + + diff --git a/lib/default.php b/lib/default.php new file mode 100644 index 000000000..95366e0b3 --- /dev/null +++ b/lib/default.php @@ -0,0 +1,229 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Default settings for core configuration + * + * 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 Config + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2008-9 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/ + */ + +$default = + array('site' => + array('name' => 'Just another StatusNet microblog', + 'server' => $_server, + 'theme' => 'default', + 'path' => $_path, + 'logfile' => null, + 'logo' => null, + 'logdebug' => false, + 'fancy' => false, + 'locale_path' => INSTALLDIR.'/locale', + 'language' => 'en_US', + 'languages' => get_all_languages(), + 'email' => + array_key_exists('SERVER_ADMIN', $_SERVER) ? $_SERVER['SERVER_ADMIN'] : null, + 'broughtby' => null, + 'timezone' => 'UTC', + 'broughtbyurl' => null, + 'closed' => false, + 'inviteonly' => false, + 'private' => false, + 'ssl' => 'never', + 'sslserver' => null, + 'shorturllength' => 30, + 'dupelimit' => 60, # default for same person saying the same thing + 'textlimit' => 140, + ), + 'db' => + array('database' => 'YOU HAVE TO SET THIS IN config.php', + 'schema_location' => INSTALLDIR . '/classes', + 'class_location' => INSTALLDIR . '/classes', + 'require_prefix' => 'classes/', + 'class_prefix' => '', + 'mirror' => null, + 'utf8' => true, + 'db_driver' => 'DB', # XXX: JanRain libs only work with DB + 'quote_identifiers' => false, + 'type' => 'mysql', + 'schemacheck' => 'runtime'), // 'runtime' or 'script' + 'syslog' => + array('appname' => 'statusnet', # for syslog + 'priority' => 'debug', # XXX: currently ignored + 'facility' => LOG_USER), + 'queue' => + array('enabled' => false, + 'subsystem' => 'db', # default to database, or 'stomp' + 'stomp_server' => null, + 'queue_basename' => 'statusnet', + 'stomp_username' => null, + 'stomp_password' => null, + ), + 'license' => + array('url' => 'http://creativecommons.org/licenses/by/3.0/', + 'title' => 'Creative Commons Attribution 3.0', + 'image' => 'http://i.creativecommons.org/l/by/3.0/80x15.png'), + 'mail' => + array('backend' => 'mail', + 'params' => null, + 'domain_check' => true), + 'nickname' => + array('blacklist' => array(), + 'featured' => array()), + 'profile' => + array('banned' => array(), + 'biolimit' => null), + 'avatar' => + array('server' => null, + 'dir' => INSTALLDIR . '/avatar/', + 'path' => $_path . '/avatar/'), + 'background' => + array('server' => null, + 'dir' => INSTALLDIR . '/background/', + 'path' => $_path . '/background/'), + 'public' => + array('localonly' => true, + 'blacklist' => array(), + 'autosource' => array()), + 'theme' => + array('server' => null, + 'dir' => null, + 'path'=> null), + 'throttle' => + array('enabled' => false, // whether to throttle edits; false by default + 'count' => 20, // number of allowed messages in timespan + 'timespan' => 600), // timespan for throttling + 'xmpp' => + array('enabled' => false, + 'server' => 'INVALID SERVER', + 'port' => 5222, + 'user' => 'update', + 'encryption' => true, + 'resource' => 'uniquename', + 'password' => 'blahblahblah', + 'host' => null, # only set if != server + 'debug' => false, # print extra debug info + 'public' => array()), # JIDs of users who want to receive the public stream + 'invite' => + array('enabled' => true), + 'tag' => + array('dropoff' => 864000.0), + 'popular' => + array('dropoff' => 864000.0), + 'daemon' => + array('piddir' => '/var/run', + 'user' => false, + 'group' => false), + 'emailpost' => + array('enabled' => true), + 'sms' => + array('enabled' => true), + 'twitterimport' => + array('enabled' => false), + 'integration' => + array('source' => 'StatusNet', # source attribute for Twitter + 'taguri' => $_server.',2009'), # base for tag URIs + 'twitter' => + array('enabled' => true, + 'consumer_key' => null, + 'consumer_secret' => null), + 'memcached' => + array('enabled' => false, + 'server' => 'localhost', + 'base' => null, + 'port' => 11211), + 'ping' => + array('notify' => array()), + 'inboxes' => + array('enabled' => true), # ignored after 0.9.x + 'newuser' => + array('default' => null, + 'welcome' => null), + 'snapshot' => + array('run' => 'web', + 'frequency' => 10000, + 'reporturl' => 'http://status.net/stats/report'), + 'attachments' => + array('server' => null, + 'dir' => INSTALLDIR . '/file/', + 'path' => $_path . '/file/', + 'supported' => array('image/png', + 'image/jpeg', + 'image/gif', + 'image/svg+xml', + 'audio/mpeg', + 'audio/x-speex', + 'application/ogg', + 'application/pdf', + 'application/vnd.oasis.opendocument.text', + 'application/vnd.oasis.opendocument.text-template', + 'application/vnd.oasis.opendocument.graphics', + 'application/vnd.oasis.opendocument.graphics-template', + 'application/vnd.oasis.opendocument.presentation', + 'application/vnd.oasis.opendocument.presentation-template', + 'application/vnd.oasis.opendocument.spreadsheet', + 'application/vnd.oasis.opendocument.spreadsheet-template', + 'application/vnd.oasis.opendocument.chart', + 'application/vnd.oasis.opendocument.chart-template', + 'application/vnd.oasis.opendocument.image', + 'application/vnd.oasis.opendocument.image-template', + 'application/vnd.oasis.opendocument.formula', + 'application/vnd.oasis.opendocument.formula-template', + 'application/vnd.oasis.opendocument.text-master', + 'application/vnd.oasis.opendocument.text-web', + 'application/x-zip', + 'application/zip', + 'text/plain', + 'video/mpeg', + 'video/mp4', + 'video/quicktime', + 'video/mpeg'), + 'file_quota' => 5000000, + 'user_quota' => 50000000, + 'monthly_quota' => 15000000, + 'uploads' => true, + 'filecommand' => '/usr/bin/file', + ), + 'group' => + array('maxaliases' => 3, + 'desclimit' => null), + 'oohembed' => array('endpoint' => 'http://oohembed.com/oohembed/'), + 'search' => + array('type' => 'fulltext'), + 'sessions' => + array('handle' => false, // whether to handle sessions ourselves + 'debug' => false), // debugging output for sessions + 'design' => + array('backgroundcolor' => null, // null -> 'use theme default' + 'contentcolor' => null, + 'sidebarcolor' => null, + 'textcolor' => null, + 'linkcolor' => null, + 'backgroundimage' => null, + 'disposition' => null), + 'notice' => + array('contentlimit' => null), + 'message' => + array('contentlimit' => null), + 'location' => + array('namespace' => 1), // 1 = geonames, 2 = Yahoo Where on Earth + ); diff --git a/lib/deleteaction.php b/lib/deleteaction.php deleted file mode 100644 index f702820c6..000000000 --- a/lib/deleteaction.php +++ /dev/null @@ -1,74 +0,0 @@ -<?php -/** - * StatusNet, the distributed open-source microblogging tool - * - * Base class for deleting things - * - * 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 Personal - * @package StatusNet - * @author Evan Prodromou <evan@status.net> - * @author Sarven Capadisli <csarven@status.net> - * @copyright 2008 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); -} - -class DeleteAction extends Action -{ - var $user = null; - var $notice = null; - var $profile = null; - var $user_profile = null; - - function prepare($args) - { - parent::prepare($args); - - $this->user = common_current_user(); - $notice_id = $this->trimmed('notice'); - $this->notice = Notice::staticGet($notice_id); - - if (!$this->notice) { - common_user_error(_('No such notice.')); - exit; - } - - $this->profile = $this->notice->getProfile(); - $this->user_profile = $this->user->getProfile(); - - return true; - } - - function handle($args) - { - parent::handle($args); - - if (!common_logged_in()) { - common_user_error(_('Not logged in.')); - exit; - } else if ($this->notice->profile_id != $this->user_profile->id) { - common_user_error(_('Can\'t delete this notice.')); - exit; - } - } - -} diff --git a/lib/deleteuserform.php b/lib/deleteuserform.php new file mode 100644 index 000000000..09ea8f68d --- /dev/null +++ b/lib/deleteuserform.php @@ -0,0 +1,79 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Form for deleting a user + * + * 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> + * @copyright 2009 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 deleting 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/ + * + */ + +class DeleteUserForm extends ProfileActionForm +{ + /** + * Action this form provides + * + * @return string Name of the action, lowercased. + */ + + function target() + { + return 'deleteuser'; + } + + /** + * Title of the form + * + * @return string Title of the form, internationalized + */ + + function title() + { + return _('Delete'); + } + + /** + * Description of the form + * + * @return string description of the form, internationalized + */ + + function description() + { + return _('Delete this user'); + } +} diff --git a/lib/designsettings.php b/lib/designsettings.php index 820d534f2..5ce9ddeda 100644 --- a/lib/designsettings.php +++ b/lib/designsettings.php @@ -271,17 +271,20 @@ class DesignSettingsAction extends AccountSettingsAction function handlePost() { - // XXX: Robin's workaround for a bug in PHP where $_POST - // and $_FILE are empty in the case that the uploaded - // file is bigger than PHP is configured to handle. - if ($_SERVER['REQUEST_METHOD'] == 'POST') { - if (empty($_POST) && $_SERVER['CONTENT_LENGTH']) { + // Workaround for PHP returning empty $_POST and $_FILES when POST + // length > post_max_size in php.ini + + if (empty($_FILES) + && empty($_POST) + && ($_SERVER['CONTENT_LENGTH'] > 0) + ) { $msg = _('The server was unable to handle that much POST ' . 'data (%s bytes) due to its current configuration.'); $this->showForm(sprintf($msg, $_SERVER['CONTENT_LENGTH'])); + return; } } diff --git a/lib/error.php b/lib/error.php index 6a9b76be1..3162cfe65 100644 --- a/lib/error.php +++ b/lib/error.php @@ -70,7 +70,7 @@ class ErrorAction extends Action */ function extraHeaders() { - $status_string = $this->status[$this->code]; + $status_string = @self::$status[$this->code]; header('HTTP/1.1 '.$this->code.' '.$status_string); } @@ -92,7 +92,7 @@ class ErrorAction extends Action function title() { - return self::$status[$this->code]; + return @self::$status[$this->code]; } function isReadOnly($args) diff --git a/lib/facebookaction.php b/lib/facebookaction.php deleted file mode 100644 index 5cbb9be53..000000000 --- a/lib/facebookaction.php +++ /dev/null @@ -1,682 +0,0 @@ -<?php -/** - * StatusNet, the distributed open-source microblogging tool - * - * Low-level generator for HTML - * - * 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 Faceboook - * @package StatusNet - * @author Zach Copley <zach@status.net> - * @copyright 2008 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); -} - -require_once INSTALLDIR.'/lib/facebookutil.php'; -require_once INSTALLDIR.'/lib/noticeform.php'; - - -class FacebookAction extends Action -{ - - var $facebook = null; - var $fbuid = null; - var $flink = null; - var $action = null; - var $app_uri = null; - var $app_name = null; - - /** - * Constructor - * - * Just wraps the HTMLOutputter constructor. - * - * @param string $output URI to output to, default = stdout - * @param boolean $indent Whether to indent output, default true - * - * @see XMLOutputter::__construct - * @see HTMLOutputter::__construct - */ - function __construct($output='php://output', $indent=true, $facebook=null, $flink=null) - { - parent::__construct($output, $indent); - - $this->facebook = $facebook; - $this->flink = $flink; - - if ($this->flink) { - $this->fbuid = $flink->foreign_id; - $this->user = $flink->getUser(); - } - - $this->args = array(); - } - - function prepare($argarray) - { - parent::prepare($argarray); - - $this->facebook = getFacebook(); - $this->fbuid = $this->facebook->require_login(); - - $this->action = $this->trimmed('action'); - - $app_props = $this->facebook->api_client->Admin_getAppProperties( - array('canvas_name', 'application_name')); - - $this->app_uri = 'http://apps.facebook.com/' . $app_props['canvas_name']; - $this->app_name = $app_props['application_name']; - - $this->flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_SERVICE); - - return true; - - } - - function showStylesheets() - { - $this->cssLink('css/display.css', 'base'); - $this->cssLink('css/display.css',null,'screen, projection, tv'); - $this->cssLink('css/facebookapp.css', 'base'); - } - - function showScripts() - { - $this->script('js/facebookapp.js'); - } - - /** - * Start an Facebook ready HTML document - * - * For Facebook we don't want to actually output any headers, - * DTD info, etc. Just Stylesheet and JavaScript links. - * - * If $type isn't specified, will attempt to do content negotiation. - * - * @param string $type MIME type to use; default is to do negotation. - * - * @return void - */ - - function startHTML($type=null) - { - $this->showStylesheets(); - $this->showScripts(); - - $this->elementStart('div', array('class' => 'facebook-page')); - } - - /** - * Ends a Facebook ready HTML document - * - * @return void - */ - function endHTML() - { - $this->elementEnd('div'); - $this->endXML(); - } - - /** - * Show notice form. - * - * MAY overload if no notice form needed... or direct message box???? - * - * @return nothing - */ - function showNoticeForm() - { - // don't do it for most of the Facebook pages - } - - function showBody() - { - $this->elementStart('div', array('id' => 'wrap')); - $this->showHeader(); - $this->showCore(); - $this->showFooter(); - $this->elementEnd('div'); - } - - function showAside() - { - } - - function showHead($error, $success) - { - - if ($error) { - $this->element("h1", null, $error); - } - - if ($success) { - $this->element("h1", null, $success); - } - - $this->elementStart('fb:if-section-not-added', array('section' => 'profile')); - $this->elementStart('span', array('id' => 'add_to_profile')); - $this->element('fb:add-section-button', array('section' => 'profile')); - $this->elementEnd('span'); - $this->elementEnd('fb:if-section-not-added'); - - } - - - // Make this into a widget later - function showLocalNav() - { - $this->elementStart('ul', array('class' => 'nav')); - - $this->elementStart('li', array('class' => - ($this->action == 'facebookhome') ? 'current' : 'facebook_home')); - $this->element('a', - array('href' => 'index.php', 'title' => _('Home')), _('Home')); - $this->elementEnd('li'); - - if (common_config('invite', 'enabled')) { - $this->elementStart('li', - array('class' => - ($this->action == 'facebookinvite') ? 'current' : 'facebook_invite')); - $this->element('a', - array('href' => 'invite.php', 'title' => _('Invite')), _('Invite')); - $this->elementEnd('li'); - } - - $this->elementStart('li', - array('class' => - ($this->action == 'facebooksettings') ? 'current' : 'facebook_settings')); - $this->element('a', - array('href' => 'settings.php', - 'title' => _('Settings')), _('Settings')); - $this->elementEnd('li'); - - $this->elementEnd('ul'); - } - - /** - * Show header of the page. - * - * Calls template methods - * - * @return nothing - */ - function showHeader() - { - $this->elementStart('div', array('id' => 'header')); - $this->showLogo(); - $this->showNoticeForm(); - $this->elementEnd('div'); - } - - /** - * Show page, a template method. - * - * @return nothing - */ - function showPage($error = null, $success = null) - { - $this->startHTML(); - $this->showHead($error, $success); - $this->showBody(); - $this->endHTML(); - } - - - function showInstructions() - { - - $this->elementStart('div', array('class' => 'facebook_guide')); - - $this->elementStart('dl', array('class' => 'system_notice')); - $this->element('dt', null, 'Page Notice'); - - $loginmsg_part1 = _('To use the %s Facebook Application you need to login ' . - 'with your username and password. Don\'t have a username yet? '); - $loginmsg_part2 = _(' a new account.'); - - $this->elementStart('dd'); - $this->elementStart('p'); - $this->text(sprintf($loginmsg_part1, common_config('site', 'name'))); - if (!common_config('site', 'openidonly')) { - $this->element('a', - array('href' => common_local_url('register')), _('Register')); - } else { - $this->element('a', - array('href' => common_local_url('openidlogin')), _('Register')); - } - $this->text($loginmsg_part2); - $this->elementEnd('p'); - $this->elementEnd('dd'); - - $this->elementEnd('dl'); - $this->elementEnd('div'); - } - - - function showLoginForm($msg = null) - { - - $this->elementStart('div', array('id' => 'content')); - $this->element('h1', null, _('Login')); - - if ($msg) { - $this->element('fb:error', array('message' => $msg)); - } - - $this->showInstructions(); - - $this->elementStart('div', array('id' => 'content_inner')); - - $this->elementStart('form', array('method' => 'post', - 'class' => 'form_settings', - 'id' => 'login', - 'action' => 'index.php')); - - $this->elementStart('fieldset'); - - $this->elementStart('ul', array('class' => 'form_datas')); - $this->elementStart('li'); - $this->input('nickname', _('Nickname')); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->password('password', _('Password')); - $this->elementEnd('li'); - $this->elementEnd('ul'); - - $this->submit('submit', _('Login')); - $this->elementEnd('fieldset'); - $this->elementEnd('form'); - - $this->elementStart('p'); - $this->element('a', array('href' => common_local_url('recoverpassword')), - _('Lost or forgotten password?')); - $this->elementEnd('p'); - - $this->elementEnd('div'); - $this->elementEnd('div'); - - } - - - function updateProfileBox($notice) - { - - // Need to include inline CSS for styling the Profile box - - $app_props = $this->facebook->api_client->Admin_getAppProperties(array('icon_url')); - $icon_url = $app_props['icon_url']; - - $style = '<style> - .entry-title *, - .entry-content * { - font-size:14px; - font-family:"Lucida Sans Unicode", "Lucida Grande", sans-serif; - } - .entry-title a, - .entry-content a { - color:#002E6E; - } - - .entry-title .vcard .photo { - float:left; - display:inline; - margin-right:11px; - margin-bottom:11px - } - .entry-title { - margin-bottom:11px; - } - .entry-title p.entry-content { - display:inline; - margin-left:5px; - } - - div.entry-content { - clear:both; - } - div.entry-content dl, - div.entry-content dt, - div.entry-content dd { - display:inline; - text-transform:lowercase; - } - - div.entry-content dd, - div.entry-content .device dt { - margin-left:0; - margin-right:5px; - } - div.entry-content dl.timestamp dt, - div.entry-content dl.response dt { - display:none; - } - div.entry-content dd a { - display:inline-block; - } - - #facebook_statusnet_app { - text-indent:-9999px; - height:16px; - width:16px; - display:block; - background:url('.$icon_url.') no-repeat 0 0; - float:right; - } - </style>'; - - $this->xw->openMemory(); - - $item = new FacebookProfileBoxNotice($notice, $this); - $item->show(); - - $fbml = "<fb:wide>$style " . $this->xw->outputMemory(false) . "</fb:wide>"; - $fbml .= "<fb:narrow>$style " . $this->xw->outputMemory(false) . "</fb:narrow>"; - - $fbml_main = "<fb:narrow>$style " . $this->xw->outputMemory(false) . "</fb:narrow>"; - - $this->facebook->api_client->profile_setFBML(null, $this->fbuid, $fbml, null, null, $fbml_main); - - $this->xw->openURI('php://output'); - } - - - /** - * Generate pagination links - * - * @param boolean $have_before is there something before? - * @param boolean $have_after is there something after? - * @param integer $page current page - * @param string $action current action - * @param array $args rest of query arguments - * - * @return nothing - */ - 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('div', array('class' => 'pagination')); - $this->elementStart('dl', null); - $this->element('dt', null, _('Pagination')); - $this->elementStart('dd', null); - $this->elementStart('ul', array('class' => 'nav')); - } - if ($have_before) { - $pargs = array('page' => $page-1); - $newargs = $args ? array_merge($args, $pargs) : $pargs; - $this->elementStart('li', array('class' => 'nav_prev')); - $this->element('a', array('href' => "$this->app_uri/$action?page=$newargs[page]", 'rel' => 'prev'), - _('After')); - $this->elementEnd('li'); - } - if ($have_after) { - $pargs = array('page' => $page+1); - $newargs = $args ? array_merge($args, $pargs) : $pargs; - $this->elementStart('li', array('class' => 'nav_next')); - $this->element('a', array('href' => "$this->app_uri/$action?page=$newargs[page]", 'rel' => 'next'), - _('Before')); - $this->elementEnd('li'); - } - if ($have_before || $have_after) { - $this->elementEnd('ul'); - $this->elementEnd('dd'); - $this->elementEnd('dl'); - $this->elementEnd('div'); - } - } - - function saveNewNotice() - { - - $user = $this->flink->getUser(); - - $content = $this->trimmed('status_textarea'); - - if (!$content) { - $this->showPage(_('No notice content!')); - return; - } else { - $content_shortened = common_shorten_links($content); - - if (mb_strlen($content_shortened) > 140) { - $this->showPage(_('That\'s too long. Max notice size is 140 chars.')); - return; - } - } - - $inter = new CommandInterpreter(); - - $cmd = $inter->handle_command($user, $content_shortened); - - if ($cmd) { - - // XXX fix this - - $cmd->execute(new WebChannel()); - return; - } - - $replyto = $this->trimmed('inreplyto'); - - $notice = Notice::saveNew($user->id, $content, - 'web', 1, ($replyto == 'false') ? null : $replyto); - - if (is_string($notice)) { - $this->showPage($notice); - return; - } - - common_broadcast_notice($notice); - - // Also update the user's Facebook status - facebookBroadcastNotice($notice); - - } - -} - -class FacebookNoticeForm extends NoticeForm -{ - - var $post_action = null; - - /** - * Constructor - * - * @param HTMLOutputter $out output channel - * @param string $action action to return to, if any - * @param string $content content to pre-fill - */ - - function __construct($out=null, $action=null, $content=null, - $post_action=null, $user=null) - { - parent::__construct($out, $action, $content, $user); - $this->post_action = $post_action; - } - - /** - * Action of the form - * - * @return string URL of the action - */ - - function action() - { - return $this->post_action; - } - -} - -class FacebookNoticeList extends NoticeList -{ - - /** - * constructor - * - * @param Notice $notice stream of notices from DB_DataObject - */ - - function __construct($notice, $out=null) - { - parent::__construct($notice, $out); - } - - /** - * show the list of notices - * - * "Uses up" the stream by looping through it. So, probably can't - * be called twice on the same list. - * - * @return int count of notices listed. - */ - - function show() - { - $this->out->elementStart('div', array('id' =>'notices_primary')); - $this->out->element('h2', null, _('Notices')); - $this->out->elementStart('ul', array('class' => 'notices')); - - $cnt = 0; - - while ($this->notice->fetch() && $cnt <= NOTICES_PER_PAGE) { - $cnt++; - - if ($cnt > NOTICES_PER_PAGE) { - break; - } - - $item = $this->newListItem($this->notice); - $item->show(); - } - - $this->out->elementEnd('ul'); - $this->out->elementEnd('div'); - - return $cnt; - } - - /** - * returns a new list item for the current notice - * - * Overridden to return a Facebook specific list item. - * - * @param Notice $notice the current notice - * - * @return FacebookNoticeListItem a list item for displaying the notice - * formatted for display in the Facebook App. - */ - - function newListItem($notice) - { - return new FacebookNoticeListItem($notice, $this); - } - -} - -class FacebookNoticeListItem extends NoticeListItem -{ - - /** - * constructor - * - * Also initializes the profile attribute. - * - * @param Notice $notice The notice we'll display - */ - - function __construct($notice, $out=null) - { - parent::__construct($notice, $out); - } - - /** - * recipe function for displaying a single notice in the Facebook App. - * - * Overridden to strip out some of the controls that we don't - * want to be available. - * - * @return void - */ - - function show() - { - $this->showStart(); - $this->showNotice(); - $this->showNoticeInfo(); - - // XXX: Need to update to show attachements and controls - - $this->showEnd(); - } - -} - -class FacebookProfileBoxNotice extends FacebookNoticeListItem -{ - - /** - * constructor - * - * Also initializes the profile attribute. - * - * @param Notice $notice The notice we'll display - */ - - function __construct($notice, $out=null) - { - parent::__construct($notice, $out); - } - - /** - * Recipe function for displaying a single notice in the - * Facebook App profile notice box - * - * @return void - */ - - function show() - { - $this->showNotice(); - $this->showNoticeInfo(); - $this->showAppLink(); - } - - function showAppLink() - { - - $this->facebook = getFacebook(); - - $app_props = $this->facebook->api_client->Admin_getAppProperties( - array('canvas_name', 'application_name')); - - $this->app_uri = 'http://apps.facebook.com/' . $app_props['canvas_name']; - $this->app_name = $app_props['application_name']; - - $this->out->elementStart('a', array('id' => 'facebook_statusnet_app', - 'href' => $this->app_uri)); - $this->out->text($this->app_name); - $this->out->elementEnd('a'); - } - -} diff --git a/lib/facebookutil.php b/lib/facebookutil.php deleted file mode 100644 index c29576b64..000000000 --- a/lib/facebookutil.php +++ /dev/null @@ -1,257 +0,0 @@ -<?php -/* - * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2008, 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/>. - */ - -require_once INSTALLDIR.'/extlib/facebook/facebook.php'; -require_once INSTALLDIR.'/lib/facebookaction.php'; -require_once INSTALLDIR.'/lib/noticelist.php'; - -define("FACEBOOK_SERVICE", 2); // Facebook is foreign_service ID 2 -define("FACEBOOK_NOTICE_PREFIX", 1); -define("FACEBOOK_PROMPTED_UPDATE_PREF", 2); - -function getFacebook() -{ - static $facebook = null; - - $apikey = common_config('facebook', 'apikey'); - $secret = common_config('facebook', 'secret'); - - if ($facebook === null) { - $facebook = new Facebook($apikey, $secret); - } - - if (empty($facebook)) { - common_log(LOG_ERR, 'Could not make new Facebook client obj!', - __FILE__); - } - - return $facebook; -} - -function isFacebookBound($notice, $flink) { - - if (empty($flink)) { - return false; - } - - // Avoid a loop - - if ($notice->source == 'Facebook') { - common_log(LOG_INFO, "Skipping notice $notice->id because its " . - 'source is Facebook.'); - return false; - } - - // If the user does not want to broadcast to Facebook, move along - - if (!($flink->noticesync & FOREIGN_NOTICE_SEND == FOREIGN_NOTICE_SEND)) { - common_log(LOG_INFO, "Skipping notice $notice->id " . - 'because user has FOREIGN_NOTICE_SEND bit off.'); - return false; - } - - // If it's not a reply, or if the user WANTS to send @-replies, - // then, yeah, it can go to Facebook. - - if (!preg_match('/@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) || - ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) { - return true; - } - - return false; - -} - -function facebookBroadcastNotice($notice) -{ - $facebook = getFacebook(); - $flink = Foreign_link::getByUserID($notice->profile_id, FACEBOOK_SERVICE); - - if (isFacebookBound($notice, $flink)) { - - // Okay, we're good to go, update the FB status - - $status = null; - $fbuid = $flink->foreign_id; - $user = $flink->getUser(); - $attachments = $notice->attachments(); - - try { - - // Get the status 'verb' (prefix) the user has set - - // XXX: Does this call count against our per user FB request limit? - // If so we should consider storing verb elsewhere or not storing - - $prefix = trim($facebook->api_client->data_getUserPreference(FACEBOOK_NOTICE_PREFIX, - $fbuid)); - - $status = "$prefix $notice->content"; - - $can_publish = $facebook->api_client->users_hasAppPermission('publish_stream', - $fbuid); - - $can_update = $facebook->api_client->users_hasAppPermission('status_update', - $fbuid); - - if (!empty($attachments) && $can_publish == 1) { - $fbattachment = format_attachments($attachments); - $facebook->api_client->stream_publish($status, $fbattachment, - null, null, $fbuid); - common_log(LOG_INFO, - "Posted notice $notice->id w/attachment " . - "to Facebook user's stream (fbuid = $fbuid)."); - } elseif ($can_update == 1 || $can_publish == 1) { - $facebook->api_client->users_setStatus($status, $fbuid, false, true); - common_log(LOG_INFO, - "Posted notice $notice->id to Facebook " . - "as a status update (fbuid = $fbuid)."); - } else { - $msg = "Not sending notice $notice->id to Facebook " . - "because user $user->nickname hasn't given the " . - 'Facebook app \'status_update\' or \'publish_stream\' permission.'; - common_log(LOG_WARNING, $msg); - } - - // Finally, attempt to update the user's profile box - - if ($can_publish == 1 || $can_update == 1) { - updateProfileBox($facebook, $flink, $notice); - } - - } catch (FacebookRestClientException $e) { - - $code = $e->getCode(); - - common_log(LOG_WARNING, 'Facebook returned error code ' . - $code . ': ' . $e->getMessage()); - common_log(LOG_WARNING, - 'Unable to update Facebook status for ' . - "$user->nickname (user id: $user->id)!"); - - if ($code == 200 || $code == 250) { - - // 200 The application does not have permission to operate on the passed in uid parameter. - // 250 Updating status requires the extended permission status_update or publish_stream. - // see: http://wiki.developers.facebook.com/index.php/Users.setStatus#Example_Return_XML - - remove_facebook_app($flink); - - } else { - - // Try sending again later. - - return false; - } - - } - } - - return true; - -} - -function updateProfileBox($facebook, $flink, $notice) { - $fbaction = new FacebookAction($output = 'php://output', - $indent = true, $facebook, $flink); - $fbaction->updateProfileBox($notice); -} - -function format_attachments($attachments) -{ - $fbattachment = array(); - $fbattachment['media'] = array(); - - foreach($attachments as $attachment) - { - $fbmedia = get_fbmedia_for_attachment($attachment); - if($fbmedia){ - $fbattachment['media'][]=$fbmedia; - }else{ - $fbattachment['name'] = ($attachment->title ? - $attachment->title : $attachment->url); - $fbattachment['href'] = $attachment->url; - } - } - if(count($fbattachment['media'])>0){ - unset($fbattachment['name']); - unset($fbattachment['href']); - } - return $fbattachment; -} - -/** -* given an File objects, returns an associative array suitable for Facebook media -*/ -function get_fbmedia_for_attachment($attachment) -{ - $fbmedia = array(); - - if (strncmp($attachment->mimetype, 'image/', strlen('image/')) == 0) { - $fbmedia['type'] = 'image'; - $fbmedia['src'] = $attachment->url; - $fbmedia['href'] = $attachment->url; - } else if ($attachment->mimetype == 'audio/mpeg') { - $fbmedia['type'] = 'mp3'; - $fbmedia['src'] = $attachment->url; - }else if ($attachment->mimetype == 'application/x-shockwave-flash') { - $fbmedia['type'] = 'flash'; - - // http://wiki.developers.facebook.com/index.php/Attachment_%28Streams%29 - // says that imgsrc is required... but we have no value to put in it - // $fbmedia['imgsrc']=''; - - $fbmedia['swfsrc'] = $attachment->url; - }else{ - return false; - } - return $fbmedia; -} - -function remove_facebook_app($flink) -{ - - $user = $flink->getUser(); - - common_log(LOG_INFO, 'Removing Facebook App Foreign link for ' . - "user $user->nickname (user id: $user->id)."); - - $result = $flink->delete(); - - if (empty($result)) { - common_log(LOG_ERR, 'Could not remove Facebook App ' . - "Foreign_link for $user->nickname (user id: $user->id)!"); - common_log_db_error($flink, 'DELETE', __FILE__); - } - - // Notify the user that we are removing their FB app access - - $result = mail_facebook_app_removed($user); - - if (!$result) { - - $msg = 'Unable to send email to notify ' . - "$user->nickname (user id: $user->id) " . - 'that their Facebook app link was ' . - 'removed!'; - - common_log(LOG_WARNING, $msg); - } - -} diff --git a/lib/form.php b/lib/form.php index 87b7a5cba..f6501dc6d 100644 --- a/lib/form.php +++ b/lib/form.php @@ -67,7 +67,7 @@ class Form extends Widget { $attributes = array('id' => $this->id(), 'class' => $this->formClass(), - 'method' => 'post', + 'method' => $this->method(), 'action' => $this->action()); if (!empty($this->enctype)) { @@ -120,6 +120,18 @@ class Form extends Widget } /** + * HTTP method used to submit the form + * + * Defaults to post. Subclasses can override if they need to. + * + * @return string the method to use for submitting + */ + function method() + { + return 'post'; + } + + /** * Buttons for form actions * * Submit and cancel buttons (or whatever) @@ -169,4 +181,14 @@ class Form extends Widget { return 'form'; } + + function li() + { + $this->out->elementStart('li'); + } + + function unli() + { + $this->out->elementEnd('li'); + } } diff --git a/lib/groupeditform.php b/lib/groupeditform.php index a649c2ee3..433f6a138 100644 --- a/lib/groupeditform.php +++ b/lib/groupeditform.php @@ -150,27 +150,33 @@ class GroupEditForm extends Form $this->out->elementStart('li'); $this->out->hidden('groupid', $id); $this->out->input('nickname', _('Nickname'), - ($this->out->arg('nickname')) ? $this->out->arg('nickname') : $nickname, - _('1-64 lowercase letters or numbers, no punctuation or spaces')); + ($this->out->arg('nickname')) ? $this->out->arg('nickname') : $nickname, + _('1-64 lowercase letters or numbers, no punctuation or spaces')); $this->out->elementEnd('li'); $this->out->elementStart('li'); $this->out->input('fullname', _('Full name'), - ($this->out->arg('fullname')) ? $this->out->arg('fullname') : $fullname); + ($this->out->arg('fullname')) ? $this->out->arg('fullname') : $fullname); $this->out->elementEnd('li'); $this->out->elementStart('li'); $this->out->input('homepage', _('Homepage'), - ($this->out->arg('homepage')) ? $this->out->arg('homepage') : $homepage, - _('URL of the homepage or blog of the group or topic')); + ($this->out->arg('homepage')) ? $this->out->arg('homepage') : $homepage, + _('URL of the homepage or blog of the group or topic')); $this->out->elementEnd('li'); $this->out->elementStart('li'); + $desclimit = User_group::maxDescription(); + if ($desclimit == 0) { + $descinstr = _('Describe the group or topic'); + } else { + $descinstr = sprintf(_('Describe the group or topic in %d characters'), $desclimit); + } $this->out->textarea('description', _('Description'), - ($this->out->arg('description')) ? $this->out->arg('description') : $description, - _('Describe the group or topic in 140 chars')); + ($this->out->arg('description')) ? $this->out->arg('description') : $description, + $descinstr); $this->out->elementEnd('li'); $this->out->elementStart('li'); $this->out->input('location', _('Location'), - ($this->out->arg('location')) ? $this->out->arg('location') : $location, - _('Location for the group, if any, like "City, State (or Region), Country"')); + ($this->out->arg('location')) ? $this->out->arg('location') : $location, + _('Location for the group, if any, like "City, State (or Region), Country"')); $this->out->elementEnd('li'); if (common_config('group', 'maxaliases') > 0) { $aliases = (empty($this->group)) ? array() : $this->group->getAliases(); diff --git a/lib/grouplist.php b/lib/grouplist.php index b41c5b5f8..99bff9cdc 100644 --- a/lib/grouplist.php +++ b/lib/grouplist.php @@ -85,19 +85,19 @@ class GroupList extends Widget function showGroup() { - $this->out->elementStart('li', array('class' => 'profile', + $this->out->elementStart('li', array('class' => 'profile hentry', 'id' => 'group-' . $this->group->id)); $user = common_current_user(); - $this->out->elementStart('div', 'entity_profile vcard'); + $this->out->elementStart('div', 'entity_profile vcard entry-content'); $logo = ($this->group->stream_logo) ? $this->group->stream_logo : User_group::defaultLogo(AVATAR_STREAM_SIZE); $this->out->elementStart('a', array('href' => $this->group->homeUrl(), - 'class' => 'url', - 'rel' => 'group')); + 'class' => 'url entry-title', + 'rel' => 'contact group')); $this->out->element('img', array('src' => $logo, 'class' => 'photo avatar', 'width' => AVATAR_STREAM_SIZE, @@ -105,48 +105,32 @@ class GroupList extends Widget 'alt' => ($this->group->fullname) ? $this->group->fullname : $this->group->nickname)); - $hasFN = ($this->group->fullname) ? 'nickname url uid' : 'fn org nickname url uid'; + $hasFN = ($this->group->fullname) ? 'nickname' : 'fn org nickname'; $this->out->elementStart('span', $hasFN); $this->out->raw($this->highlight($this->group->nickname)); $this->out->elementEnd('span'); $this->out->elementEnd('a'); if ($this->group->fullname) { - $this->out->elementStart('dl', 'entity_fn'); - $this->out->element('dt', null, 'Full name'); - $this->out->elementStart('dd'); $this->out->elementStart('span', 'fn org'); $this->out->raw($this->highlight($this->group->fullname)); $this->out->elementEnd('span'); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); } if ($this->group->location) { - $this->out->elementStart('dl', 'entity_location'); - $this->out->element('dt', null, _('Location')); - $this->out->elementStart('dd', 'label'); + $this->out->elementStart('span', 'label'); $this->out->raw($this->highlight($this->group->location)); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); + $this->out->elementEnd('span'); } if ($this->group->homepage) { - $this->out->elementStart('dl', 'entity_url'); - $this->out->element('dt', null, _('URL')); - $this->out->elementStart('dd'); $this->out->elementStart('a', array('href' => $this->group->homepage, 'class' => 'url')); $this->out->raw($this->highlight($this->group->homepage)); $this->out->elementEnd('a'); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); } if ($this->group->description) { - $this->out->elementStart('dl', 'entity_note'); - $this->out->element('dt', null, _('Note')); - $this->out->elementStart('dd', 'note'); + $this->out->elementStart('p', 'note'); $this->out->raw($this->highlight($this->group->description)); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); + $this->out->elementEnd('p'); } # If we're on a list with an owner (subscriptions or subscribers)... diff --git a/lib/groupnav.php b/lib/groupnav.php index 31cf378c8..131b38fa2 100644 --- a/lib/groupnav.php +++ b/lib/groupnav.php @@ -79,46 +79,49 @@ class GroupNav extends Widget $nickname = $this->group->nickname; $this->out->elementStart('ul', array('class' => 'nav')); - $this->out->menuItem(common_local_url('showgroup', array('nickname' => - $nickname)), - _('Group'), - sprintf(_('%s group'), $nickname), - $action_name == 'showgroup', - 'nav_group_group'); - $this->out->menuItem(common_local_url('groupmembers', array('nickname' => - $nickname)), - _('Members'), - sprintf(_('%s group members'), $nickname), - $action_name == 'groupmembers', - 'nav_group_members'); + if (Event::handle('StartGroupGroupNav', array($this))) { + $this->out->menuItem(common_local_url('showgroup', array('nickname' => + $nickname)), + _('Group'), + sprintf(_('%s group'), $nickname), + $action_name == 'showgroup', + 'nav_group_group'); + $this->out->menuItem(common_local_url('groupmembers', array('nickname' => + $nickname)), + _('Members'), + sprintf(_('%s group members'), $nickname), + $action_name == 'groupmembers', + 'nav_group_members'); - $cur = common_current_user(); + $cur = common_current_user(); - if ($cur && $cur->isAdmin($this->group)) { - $this->out->menuItem(common_local_url('blockedfromgroup', array('nickname' => - $nickname)), - _('Blocked'), - sprintf(_('%s blocked users'), $nickname), - $action_name == 'blockedfromgroup', - 'nav_group_blocked'); - $this->out->menuItem(common_local_url('editgroup', array('nickname' => - $nickname)), - _('Admin'), - sprintf(_('Edit %s group properties'), $nickname), - $action_name == 'editgroup', - 'nav_group_admin'); - $this->out->menuItem(common_local_url('grouplogo', array('nickname' => - $nickname)), - _('Logo'), - sprintf(_('Add or edit %s logo'), $nickname), - $action_name == 'grouplogo', - 'nav_group_logo'); - $this->out->menuItem(common_local_url('groupdesignsettings', array('nickname' => - $nickname)), - _('Design'), - sprintf(_('Add or edit %s design'), $nickname), - $action_name == 'groupdesignsettings', - 'nav_group_design'); + if ($cur && $cur->isAdmin($this->group)) { + $this->out->menuItem(common_local_url('blockedfromgroup', array('nickname' => + $nickname)), + _('Blocked'), + sprintf(_('%s blocked users'), $nickname), + $action_name == 'blockedfromgroup', + 'nav_group_blocked'); + $this->out->menuItem(common_local_url('editgroup', array('nickname' => + $nickname)), + _('Admin'), + sprintf(_('Edit %s group properties'), $nickname), + $action_name == 'editgroup', + 'nav_group_admin'); + $this->out->menuItem(common_local_url('grouplogo', array('nickname' => + $nickname)), + _('Logo'), + sprintf(_('Add or edit %s logo'), $nickname), + $action_name == 'grouplogo', + 'nav_group_logo'); + $this->out->menuItem(common_local_url('groupdesignsettings', array('nickname' => + $nickname)), + _('Design'), + sprintf(_('Add or edit %s design'), $nickname), + $action_name == 'groupdesignsettings', + 'nav_group_design'); + } + Event::handle('EndGroupGroupNav', array($this)); } $this->out->elementEnd('ul'); } diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php index 2ff9380cc..c2ec83c28 100644 --- a/lib/htmloutputter.php +++ b/lib/htmloutputter.php @@ -106,7 +106,7 @@ class HTMLOutputter extends XMLOutputter } } - header('Content-Type: '.$type.'; charset=UTF-8'); + header('Content-Type: '.$type); $this->extraHeaders(); if (preg_match("/.*\/.*xml/", $type)) { @@ -375,8 +375,8 @@ class HTMLOutputter extends XMLOutputter $url = parse_url($src); if( empty($url->scheme) && empty($url->host) && empty($url->query) && empty($url->fragment)) { - if(file_exists(theme_file($src,$theme))){ - $src = theme_path($src, $theme) . '?version=' . STATUSNET_VERSION; + if(file_exists(Theme::file($src,$theme))){ + $src = Theme::path($src, $theme) . '?version=' . STATUSNET_VERSION; }else{ $src = common_path($src); } diff --git a/lib/httpclient.php b/lib/httpclient.php new file mode 100644 index 000000000..3f8262076 --- /dev/null +++ b/lib/httpclient.php @@ -0,0 +1,259 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Utility for doing HTTP-related things + * + * 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 Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 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); +} + +require_once 'HTTP/Request2.php'; +require_once 'HTTP/Request2/Response.php'; + +/** + * Useful structure for HTTP responses + * + * We make HTTP calls in several places, and we have several different + * ways of doing them. This class hides the specifics of what underlying + * library (curl or PHP-HTTP or whatever) that's used. + * + * This extends the HTTP_Request2_Response class with methods to get info + * about any followed redirects. + * + * @category HTTP + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @author Brion Vibber <brion@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 HTTPResponse extends HTTP_Request2_Response +{ + function __construct(HTTP_Request2_Response $response, $url, $redirects=0) + { + foreach (get_object_vars($response) as $key => $val) { + $this->$key = $val; + } + $this->url = strval($url); + $this->redirectCount = intval($redirects); + } + + /** + * Get the count of redirects that have been followed, if any. + * @return int + */ + function getRedirectCount() + { + return $this->redirectCount; + } + + /** + * Gets the final target URL, after any redirects have been followed. + * @return string URL + */ + function getUrl() + { + return $this->url; + } + + /** + * Check if the response is OK, generally a 200 status code. + * @return bool + */ + function isOk() + { + return ($this->getStatus() == 200); + } +} + +/** + * Utility class for doing HTTP client stuff + * + * We make HTTP calls in several places, and we have several different + * ways of doing them. This class hides the specifics of what underlying + * library (curl or PHP-HTTP or whatever) that's used. + * + * This extends the PEAR HTTP_Request2 package: + * - sends StatusNet-specific User-Agent header + * - 'follow_redirects' config option, defaulting off + * - 'max_redirs' config option, defaulting to 10 + * - extended response class adds getRedirectCount() and getUrl() methods + * - get() and post() convenience functions return body content directly + * + * @category HTTP + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @author Brion Vibber <brion@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 HTTPClient extends HTTP_Request2 +{ + + function __construct($url=null, $method=self::METHOD_GET, $config=array()) + { + $this->config['max_redirs'] = 10; + $this->config['follow_redirects'] = true; + parent::__construct($url, $method, $config); + $this->setHeader('User-Agent', $this->userAgent()); + } + + /** + * Convenience/back-compat instantiator + * @return HTTPClient + */ + public static function start() + { + return new HTTPClient(); + } + + /** + * Convenience function to run a GET request. + * + * @return HTTPResponse + * @throws HTTP_Request2_Exception + */ + public function get($url, $headers=array()) + { + return $this->doRequest($url, self::METHOD_GET, $headers); + } + + /** + * Convenience function to run a HEAD request. + * + * @return HTTPResponse + * @throws HTTP_Request2_Exception + */ + public function head($url, $headers=array()) + { + return $this->doRequest($url, self::METHOD_HEAD, $headers); + } + + /** + * Convenience function to POST form data. + * + * @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 + * @throws HTTP_Request2_Exception + */ + public function post($url, $headers=array(), $data=array()) + { + if ($data) { + $this->addPostParameter($data); + } + return $this->doRequest($url, self::METHOD_POST, $headers); + } + + /** + * @return HTTPResponse + * @throws HTTP_Request2_Exception + */ + protected function doRequest($url, $method, $headers) + { + $this->setUrl($url); + $this->setMethod($method); + if ($headers) { + foreach ($headers as $header) { + $this->setHeader($header); + } + } + $response = $this->send(); + return $response; + } + + protected function log($level, $detail) { + $method = $this->getMethod(); + $url = $this->getUrl(); + common_log($level, __CLASS__ . ": HTTP $method $url - $detail"); + } + + /** + * Pulls up StatusNet's customized user-agent string, so services + * we hit can track down the responsible software. + * + * @return string + */ + function userAgent() + { + return "StatusNet/".STATUSNET_VERSION." (".STATUSNET_CODENAME.")"; + } + + /** + * Actually performs the HTTP request and returns an HTTPResponse object + * with response body and header info. + * + * Wraps around parent send() to add logging and redirection processing. + * + * @return HTTPResponse + * @throw HTTP_Request2_Exception + */ + public function send() + { + $maxRedirs = intval($this->config['max_redirs']); + if (empty($this->config['follow_redirects'])) { + $maxRedirs = 0; + } + $redirs = 0; + do { + try { + $response = parent::send(); + } catch (HTTP_Request2_Exception $e) { + $this->log(LOG_ERR, $e->getMessage()); + throw $e; + } + $code = $response->getStatus(); + if ($code >= 200 && $code < 300) { + $reason = $response->getReasonPhrase(); + $this->log(LOG_INFO, "$code $reason"); + } elseif ($code >= 300 && $code < 400) { + $url = $this->getUrl(); + $target = $response->getHeader('Location'); + + if (++$redirs >= $maxRedirs) { + common_log(LOG_ERR, __CLASS__ . ": Too many redirects: skipping $code redirect from $url to $target"); + break; + } + try { + $this->setUrl($target); + $this->setHeader('Referer', $url); + common_log(LOG_INFO, __CLASS__ . ": Following $code redirect from $url to $target"); + continue; + } catch (HTTP_Request2_Exception $e) { + common_log(LOG_ERR, __CLASS__ . ": Invalid $code redirect from $url to $target"); + } + } else { + $reason = $response->getReasonPhrase(); + $this->log(LOG_ERR, "$code $reason"); + } + break; + } while ($maxRedirs); + return new HTTPResponse($response, $this->getUrl(), $redirs); + } +} diff --git a/lib/imagefile.php b/lib/imagefile.php index 88f461481..cf1668f20 100644 --- a/lib/imagefile.php +++ b/lib/imagefile.php @@ -72,14 +72,19 @@ class ImageFile break; case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: - throw new Exception(sprintf(_('That file is too big. The maximum file size is %d.'), + throw new Exception(sprintf(_('That file is too big. The maximum file size is %s.'), ImageFile::maxFileSize())); return; case UPLOAD_ERR_PARTIAL: @unlink($_FILES[$param]['tmp_name']); throw new Exception(_('Partial upload.')); return; + case UPLOAD_ERR_NO_FILE: + // No file; probably just a non-AJAX submission. + return; default: + common_log(LOG_ERR, __METHOD__ . ": Unknown upload error " . + $_FILES[$param]['error']); throw new Exception(_('System error uploading file.')); return; } diff --git a/lib/jabber.php b/lib/jabber.php index 3dcdce5db..a8e295ea5 100644 --- a/lib/jabber.php +++ b/lib/jabber.php @@ -176,6 +176,7 @@ function jabber_format_entry($profile, $notice) $xs = new XMLStringer(); $xs->elementStart('html', array('xmlns' => 'http://jabber.org/protocol/xhtml-im')); $xs->elementStart('body', array('xmlns' => 'http://www.w3.org/1999/xhtml')); + $xs->element("img", array('src'=> $profile->avatarUrl(AVATAR_MINI_SIZE))); $xs->element('a', array('href' => $profile->profileurl), $profile->nickname); $xs->text(": "); @@ -184,6 +185,11 @@ function jabber_format_entry($profile, $notice) } else { $xs->raw(common_render_content($notice->content, $notice)); } + $xs->text(" "); + $xs->element('a', array( + 'href'=>common_local_url('conversation', + array('id' => $notice->conversation)).'#notice-'.$notice->id + ),sprintf(_('[%s]'),$notice->id)); $xs->elementEnd('body'); $xs->elementEnd('html'); @@ -475,5 +481,5 @@ function jabber_public_notice($notice) function jabber_format_notice(&$profile, &$notice) { - return $profile->nickname . ': ' . $notice->content; + return $profile->nickname . ': ' . $notice->content . ' [' . $notice->id . ']'; } diff --git a/lib/language.php b/lib/language.php index 561a4ddb8..a99bf89e3 100644 --- a/lib/language.php +++ b/lib/language.php @@ -32,6 +32,63 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +if (!function_exists('gettext')) { + require_once("php-gettext/gettext.inc"); +} + +if (!function_exists('pgettext')) { + /** + * Context-aware gettext wrapper; use when messages in different contexts + * won't be distinguished from the English source but need different translations. + * The context string will appear as msgctxt in the .po files. + * + * Not currently exposed in PHP's gettext module; implemented to be compat + * with gettext.h's macros. + * + * @param string $context context identifier, should be some key like "menu|file" + * @param string $msgid English source text + * @return string original or translated message + */ + function pgettext($context, $msg) + { + $msgid = $context . "\004" . $msg; + $out = dcgettext(textdomain(NULL), $msgid, LC_MESSAGES); + if ($out == $msgid) { + return $msg; + } else { + return $out; + } + } +} + +if (!function_exists('npgettext')) { + /** + * Context-aware ngettext wrapper; use when messages in different contexts + * won't be distinguished from the English source but need different translations. + * The context string will appear as msgctxt in the .po files. + * + * Not currently exposed in PHP's gettext module; implemented to be compat + * with gettext.h's macros. + * + * @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 + * @param int $n number of items to control plural selection + * @return string original or translated message + */ + function npgettext($context, $msg, $plural, $n) + { + $msgid = $context . "\004" . $msg; + $out = dcngettext(textdomain(NULL), $msgid, $plural, $n, LC_MESSAGES); + if ($out == $msgid) { + return $msg; + } else { + return $out; + } + } +} + + /** * Content negotiation for language codes * @@ -100,37 +157,40 @@ function get_nice_language_list() * @return array mapping of language codes to language info */ function get_all_languages() { - return array( - 'bg' => array('q' => 0.8, 'lang' => 'bg_BG', 'name' => 'Bulgarian', 'direction' => 'ltr'), - 'ca' => array('q' => 0.5, 'lang' => 'ca_ES', 'name' => 'Catalan', 'direction' => 'ltr'), - 'cs' => array('q' => 0.5, 'lang' => 'cs_CZ', 'name' => 'Czech', 'direction' => 'ltr'), - 'de' => array('q' => 0.8, 'lang' => 'de_DE', 'name' => 'German', 'direction' => 'ltr'), - 'el' => array('q' => 0.1, 'lang' => 'el', 'name' => 'Greek', 'direction' => 'ltr'), - 'en-us' => array('q' => 1, 'lang' => 'en_US', 'name' => 'English (US)', 'direction' => 'ltr'), - 'en-gb' => array('q' => 1, 'lang' => 'en_GB', 'name' => 'English (British)', 'direction' => 'ltr'), - 'en' => array('q' => 1, 'lang' => 'en_US', 'name' => 'English (US)', 'direction' => 'ltr'), - 'es' => array('q' => 1, 'lang' => 'es', 'name' => 'Spanish', 'direction' => 'ltr'), - 'fi' => array('q' => 1, 'lang' => 'fi', 'name' => 'Finnish', 'direction' => 'ltr'), - 'fr-fr' => array('q' => 1, 'lang' => 'fr_FR', 'name' => 'French', 'direction' => 'ltr'), - 'he' => array('q' => 0.5, 'lang' => 'he_IL', 'name' => 'Hebrew', 'direction' => 'rtl'), - 'it' => array('q' => 1, 'lang' => 'it_IT', 'name' => 'Italian', 'direction' => 'ltr'), - 'jp' => array('q' => 0.5, 'lang' => 'ja_JP', 'name' => 'Japanese', 'direction' => 'ltr'), - 'ko' => array('q' => 0.9, 'lang' => 'ko_KR', 'name' => 'Korean', 'direction' => 'ltr'), - 'mk' => array('q' => 0.5, 'lang' => 'mk_MK', 'name' => 'Macedonian', 'direction' => 'ltr'), - 'nb' => array('q' => 0.1, 'lang' => 'nb_NO', 'name' => 'Norwegian (Bokmål)', 'direction' => 'ltr'), - 'no' => array('q' => 0.1, 'lang' => 'nb_NO', 'name' => 'Norwegian (Bokmål)', 'direction' => 'ltr'), - 'nn' => array('q' => 1, 'lang' => 'nn_NO', 'name' => 'Norwegian (Nynorsk)', 'direction' => 'ltr'), - 'nl' => array('q' => 0.5, 'lang' => 'nl_NL', 'name' => 'Dutch', 'direction' => 'ltr'), - 'pl' => array('q' => 0.5, 'lang' => 'pl_PL', 'name' => 'Polish', 'direction' => 'ltr'), - 'pt' => array('q' => 0.1, 'lang' => 'pt', 'name' => 'Portuguese', 'direction' => 'ltr'), - 'pt-br' => array('q' => 0.9, 'lang' => 'pt_BR', 'name' => 'Portuguese Brazil', 'direction' => 'ltr'), - 'ru' => array('q' => 0.9, 'lang' => 'ru_RU', 'name' => 'Russian', 'direction' => 'ltr'), - 'sv' => array('q' => 0.8, 'lang' => 'sv_SE', 'name' => 'Swedish', 'direction' => 'ltr'), - 'te' => array('q' => 0.3, 'lang' => 'te_IN', 'name' => 'Telugu', 'direction' => 'ltr'), - 'tr' => array('q' => 0.5, 'lang' => 'tr_TR', 'name' => 'Turkish', 'direction' => 'ltr'), - 'uk' => array('q' => 1, 'lang' => 'uk_UA', 'name' => 'Ukrainian', 'direction' => 'ltr'), - 'vi' => array('q' => 0.8, 'lang' => 'vi_VN', 'name' => 'Vietnamese', 'direction' => 'ltr'), - 'zh-cn' => array('q' => 0.9, 'lang' => 'zh_CN', 'name' => 'Chinese (Simplified)', 'direction' => 'ltr'), - 'zh-hant' => array('q' => 0.2, 'lang' => 'zh_TW', 'name' => 'Chinese (Taiwanese)', 'direction' => 'ltr'), - ); + return array( + 'ar' => array('q' => 0.8, 'lang' => 'ar', 'name' => 'Arabic', 'direction' => 'rtl'), + 'bg' => array('q' => 0.8, 'lang' => 'bg', 'name' => 'Bulgarian', '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'), + 'el' => array('q' => 0.1, 'lang' => 'el', 'name' => 'Greek', 'direction' => 'ltr'), + 'en-us' => array('q' => 1, 'lang' => 'en', 'name' => 'English (US)', 'direction' => 'ltr'), + 'en-gb' => array('q' => 1, 'lang' => 'en_GB', 'name' => 'English (British)', 'direction' => 'ltr'), + 'en' => array('q' => 1, 'lang' => 'en', 'name' => 'English (US)', 'direction' => 'ltr'), + 'es' => array('q' => 1, 'lang' => 'es', 'name' => 'Spanish', 'direction' => 'ltr'), + 'fi' => array('q' => 1, 'lang' => 'fi', 'name' => 'Finnish', 'direction' => 'ltr'), + 'fr-fr' => array('q' => 1, 'lang' => 'fr', 'name' => 'French', 'direction' => 'ltr'), + 'ga' => array('q' => 0.5, 'lang' => 'ga', 'name' => 'Galician', 'direction' => 'ltr'), + 'he' => array('q' => 0.5, 'lang' => 'he', 'name' => 'Hebrew', 'direction' => 'rtl'), + 'is' => array('q' => 0.1, 'lang' => 'is', 'name' => 'Icelandic', 'direction' => 'ltr'), + 'it' => array('q' => 1, 'lang' => 'it', 'name' => 'Italian', 'direction' => 'ltr'), + 'jp' => array('q' => 0.5, 'lang' => 'ja', 'name' => 'Japanese', 'direction' => 'ltr'), + 'ko' => array('q' => 0.9, 'lang' => 'ko', 'name' => 'Korean', 'direction' => 'ltr'), + 'mk' => array('q' => 0.5, 'lang' => 'mk', 'name' => 'Macedonian', 'direction' => 'ltr'), + 'nb' => array('q' => 0.1, 'lang' => 'nb', 'name' => 'Norwegian (Bokmål)', 'direction' => 'ltr'), + 'no' => array('q' => 0.1, 'lang' => 'nb', 'name' => 'Norwegian (Bokmål)', 'direction' => 'ltr'), + 'nn' => array('q' => 1, 'lang' => 'nn', 'name' => 'Norwegian (Nynorsk)', 'direction' => 'ltr'), + 'nl' => array('q' => 0.5, 'lang' => 'nl', 'name' => 'Dutch', 'direction' => 'ltr'), + 'pl' => array('q' => 0.5, 'lang' => 'pl', 'name' => 'Polish', 'direction' => 'ltr'), + 'pt' => array('q' => 0.1, 'lang' => 'pt', 'name' => 'Portuguese', 'direction' => 'ltr'), + 'pt-br' => array('q' => 0.9, 'lang' => 'pt_BR', 'name' => 'Portuguese Brazil', 'direction' => 'ltr'), + 'ru' => array('q' => 0.9, 'lang' => 'ru', 'name' => 'Russian', 'direction' => 'ltr'), + 'sv' => array('q' => 0.8, 'lang' => 'sv', 'name' => 'Swedish', 'direction' => 'ltr'), + 'te' => array('q' => 0.3, 'lang' => 'te', 'name' => 'Telugu', 'direction' => 'ltr'), + 'tr' => array('q' => 0.5, 'lang' => 'tr', 'name' => 'Turkish', 'direction' => 'ltr'), + 'uk' => array('q' => 1, 'lang' => 'uk', 'name' => 'Ukrainian', 'direction' => 'ltr'), + 'vi' => array('q' => 0.8, 'lang' => 'vi', 'name' => 'Vietnamese', 'direction' => 'ltr'), + 'zh-cn' => array('q' => 0.9, 'lang' => 'zh_CN', 'name' => 'Chinese (Simplified)', 'direction' => 'ltr'), + 'zh-hant' => array('q' => 0.2, 'lang' => 'zh_TW', 'name' => 'Chinese (Taiwanese)', 'direction' => 'ltr'), + ); } diff --git a/lib/location.php b/lib/location.php new file mode 100644 index 000000000..191550d6d --- /dev/null +++ b/lib/location.php @@ -0,0 +1,212 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Class for locations + * + * 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 Location + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 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); +} + +/** + * class for locations + * + * These are stored in the DB as part of notice and profile records, + * but since they're about the same in both, we have a separate class + * for them. + * + * @category Location + * @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/ + */ + +class Location +{ + public $lat; + public $lon; + public $location_id; + public $location_ns; + private $_url; + private $_rdfurl; + + var $names = array(); + + /** + * Constructor that makes a Location from a string name + * + * @param string $name Human-readable name (any kind) + * @param string $language Language, default = common_language() + * + * @return Location Location with that name (or null if not found) + */ + + static function fromName($name, $language=null) + { + if (is_null($language)) { + $language = common_language(); + } + + $location = null; + + // Let a third-party handle it + + Event::handle('LocationFromName', array($name, $language, &$location)); + + return $location; + } + + /** + * Constructor that makes a Location from an ID + * + * @param integer $id Identifier ID + * @param integer $ns Namespace of the identifier + * @param string $language Language to return name in (default is common) + * + * @return Location The location with this ID (or null if none) + */ + + static function fromId($id, $ns, $language=null) + { + if (is_null($language)) { + $language = common_language(); + } + + $location = null; + + // Let a third-party handle it + + Event::handle('LocationFromId', array($id, $ns, $language, &$location)); + + return $location; + } + + /** + * Constructor that finds the nearest location to a lat/lon pair + * + * @param float $lat Latitude + * @param float $lon Longitude + * @param string $language Language for results, default = current + * + * @return Location the location found, or null if none found + */ + + static function fromLatLon($lat, $lon, $language=null) + { + if (is_null($language)) { + $language = common_language(); + } + + $location = null; + + // Let a third-party handle it + + if (Event::handle('LocationFromLatLon', + array($lat, $lon, $language, &$location))) { + // Default is just the lat/lon pair + + $location = new Location(); + + $location->lat = $lat; + $location->lon = $lon; + } + + return $location; + } + + /** + * Get the name for this location in the given language + * + * @param string $language language to use, default = current + * + * @return string location name or null if not found + */ + + function getName($language=null) + { + if (is_null($language)) { + $language = common_language(); + } + + if (array_key_exists($language, $this->names)) { + return $this->names[$language]; + } else { + $name = null; + Event::handle('LocationNameLanguage', array($this, $language, &$name)); + if (!empty($name)) { + $this->names[$language] = $name; + return $name; + } + } + } + + /** + * Get an URL suitable for this location + * + * @return string URL for this location or NULL + */ + + function getURL() + { + // Keep one cached + + if (is_string($this->_url)) { + return $this->_url; + } + + $url = null; + + Event::handle('LocationUrl', array($this, &$url)); + + $this->_url = $url; + + return $url; + } + + /** + * Get an URL for this location, suitable for embedding in RDF + * + * @return string URL for this location or NULL + */ + + function getRdfURL() + { + // Keep one cached + + if (is_string($this->_rdfurl)) { + return $this->_rdfurl; + } + + $url = null; + + Event::handle('LocationRdfUrl', array($this, &$url)); + + $this->_rdfurl = $url; + + return $url; + } +} diff --git a/lib/logingroupnav.php b/lib/logingroupnav.php index f740e329a..b545fbf26 100644 --- a/lib/logingroupnav.php +++ b/lib/logingroupnav.php @@ -69,30 +69,25 @@ class LoginGroupNav extends Widget function show() { - // action => array('prompt', 'title') - $menu = array(); + $action_name = $this->action->trimmed('action'); + + $this->action->elementStart('ul', array('class' => 'nav')); + + if (Event::handle('StartLoginGroupNav', array(&$this->action))) { + + $this->action->menuItem(common_local_url('login'), + _('Login'), + _('Login with a username and password'), + $action_name === 'login'); - if (!common_config('site','openidonly')) { - $menu['login'] = array(_('Login'), - _('Login with a username and password')); if (!(common_config('site','closed') || common_config('site','inviteonly'))) { - $menu['register'] = array(_('Register'), - _('Sign up for a new account')); + $this->action->menuItem(common_local_url('register'), + _('Register'), + _('Sign up for a new account'), + $action_name === 'register'); } - } - if (common_config('openid', 'enabled')) { - $menu['openidlogin'] = array(_('OpenID'), - _('Login or register with OpenID')); - } - - $action_name = $this->action->trimmed('action'); - $this->action->elementStart('ul', array('class' => 'nav')); - foreach ($menu as $menuaction => $menudesc) { - $this->action->menuItem(common_local_url($menuaction), - $menudesc[0], - $menudesc[1], - $action_name === $menuaction); + Event::handle('EndLoginGroupNav', array(&$this->action)); } $this->action->elementEnd('ul'); diff --git a/lib/mail.php b/lib/mail.php index 5bf4d7425..dffac3262 100644 --- a/lib/mail.php +++ b/lib/mail.php @@ -216,7 +216,8 @@ function mail_subscribe_notify($listenee, $listener) function mail_subscribe_notify_profile($listenee, $other) { - if ($listenee->email && $listenee->emailnotifysub) { + if ($other->hasRight(Right::EMAILONSUBSCRIBE) && + $listenee->email && $listenee->emailnotifysub) { // use the recipient's localization common_init_locale($listenee->language); @@ -545,6 +546,10 @@ function mail_notify_message($message, $from=null, $to=null) function mail_notify_fave($other, $user, $notice) { + if (!$user->hasRight(Right::EMAILONFAVE)) { + return; + } + $profile = $user->getProfile(); $bestname = $profile->getBestName(); @@ -594,10 +599,14 @@ function mail_notify_attn($user, $notice) $sender = $notice->getProfile(); + if (!$sender->hasRight(Right::EMAILONREPLY)) { + return; + } + $bestname = $sender->getBestName(); common_init_locale($user->language); - + if ($notice->conversation != $notice->id) { $conversationEmailText = "The full conversation can be read here:\n\n". "\t%5\$s\n\n "; @@ -607,9 +616,9 @@ function mail_notify_attn($user, $notice) $conversationEmailText = "%5\$s"; $conversationUrl = null; } - + $subject = sprintf(_('%s (@%s) sent a notice to your attention'), $bestname, $sender->nickname); - + $body = sprintf(_("%1\$s (@%9\$s) just sent a notice to your attention (an '@-reply') on %2\$s.\n\n". "The notice is here:\n\n". "\t%3\$s\n\n" . @@ -635,80 +644,8 @@ function mail_notify_attn($user, $notice) array('nickname' => $user->nickname)),//%7 common_local_url('emailsettings'), //%8 $sender->nickname); //%9 - - common_init_locale(); - mail_to_user($user, $subject, $body); -} - -/** - * Send a mail message to notify a user that her Twitter bridge link - * has stopped working, and therefore has been removed. This can - * happen when the user changes her Twitter password, or otherwise - * revokes access. - * - * @param User $user user whose Twitter bridge link has been removed - * - * @return boolean success flag - */ - -function mail_twitter_bridge_removed($user) -{ - common_init_locale($user->language); - - $profile = $user->getProfile(); - - $subject = sprintf(_('Your Twitter bridge has been disabled.')); - - $site_name = common_config('site', 'name'); - - $body = sprintf(_('Hi, %1$s. We\'re sorry to inform you that your ' . - 'link to Twitter has been disabled. We no longer seem to have ' . - 'permission to update your Twitter status. (Did you revoke ' . - '%3$s\'s access?)' . "\n\n" . - 'You can re-enable your Twitter bridge by visiting your ' . - "Twitter settings page:\n\n\t%2\$s\n\n" . - "Regards,\n%3\$s\n"), - $profile->getBestName(), - common_local_url('twittersettings'), - common_config('site', 'name')); common_init_locale(); - return mail_to_user($user, $subject, $body); -} - -/** - * Send a mail message to notify a user that her Facebook Application - * access has been removed. - * - * @param User $user user whose Facebook app link has been removed - * - * @return boolean success flag - */ - -function mail_facebook_app_removed($user) -{ - common_init_locale($user->language); - - $profile = $user->getProfile(); - - $site_name = common_config('site', 'name'); - - $subject = sprintf( - _('Your %1$s Facebook application access has been disabled.', - $site_name)); - - $body = sprintf(_("Hi, %1\$s. We're sorry to inform you that we are " . - 'unable to update your Facebook status from %2$s, and have disabled ' . - 'the Facebook application for your account. This may be because ' . - 'you have removed the Facebook application\'s authorization, or ' . - 'have deleted your Facebook account. You can re-enable the ' . - 'Facebook application and automatic status updating by ' . - "re-installing the %2\$s Facebook application.\n\nRegards,\n\n%2\$s"), - $user->nickname, $site_name); - - common_init_locale(); - return mail_to_user($user, $subject, $body); - + mail_to_user($user, $subject, $body); } - diff --git a/lib/mediafile.php b/lib/mediafile.php new file mode 100644 index 000000000..29d752f0c --- /dev/null +++ b/lib/mediafile.php @@ -0,0 +1,289 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Abstraction for media files in general + * + * TODO: combine with ImageFile? + * + * 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 Media + * @package StatusNet + * @author Robin Millette <robin@millette.info> + * @author Zach Copley <zach@status.net> + * @copyright 2008-2009 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); +} + +class MediaFile +{ + + var $filename = null; + var $fileRecord = null; + var $user = null; + var $fileurl = null; + var $short_fileurl = null; + var $mimetype = null; + + function __construct($user = null, $filename = null, $mimetype = null) + { + if ($user == null) { + $this->user = common_current_user(); + } + + $this->filename = $filename; + $this->mimetype = $mimetype; + $this->fileRecord = $this->storeFile(); + + $this->fileurl = common_local_url('attachment', + array('attachment' => $this->fileRecord->id)); + + $this->maybeAddRedir($this->fileRecord->id, $this->fileurl); + $this->short_fileurl = common_shorten_url($this->fileurl); + $this->maybeAddRedir($this->fileRecord->id, $this->short_fileurl); + } + + function attachToNotice($notice) + { + File_to_post::processNew($this->fileRecord->id, $notice->id); + $this->maybeAddRedir($this->fileRecord->id, + common_local_url('file', array('notice' => $notice->id))); + } + + function shortUrl() + { + return $this->short_fileurl; + } + + function delete() + { + $filepath = File::path($this->filename); + @unlink($filepath); + } + + function storeFile() { + + $file = new File; + + $file->filename = $this->filename; + $file->url = File::url($this->filename); + $filepath = File::path($this->filename); + $file->size = filesize($filepath); + $file->date = time(); + $file->mimetype = $this->mimetype; + + $file_id = $file->insert(); + + if (!$file_id) { + common_log_db_error($file, "INSERT", __FILE__); + throw new ClientException(_('There was a database error while saving your file. Please try again.')); + } + + return $file; + } + + function rememberFile($file, $short) + { + $this->maybeAddRedir($file->id, $short); + } + + function maybeAddRedir($file_id, $url) + { + $file_redir = File_redirection::staticGet('url', $url); + + if (empty($file_redir)) { + + $file_redir = new File_redirection; + $file_redir->url = $url; + $file_redir->file_id = $file_id; + + $result = $file_redir->insert(); + + if (!$result) { + common_log_db_error($file_redir, "INSERT", __FILE__); + throw new ClientException(_('There was a database error while saving your file. Please try again.')); + } + } + } + + static function fromUpload($param = 'media', $user = null) + { + if (empty($user)) { + $user = common_current_user(); + } + + if (!isset($_FILES[$param]['error'])){ + return; + } + + switch ($_FILES[$param]['error']) { + case UPLOAD_ERR_OK: // success, jump out + break; + case UPLOAD_ERR_INI_SIZE: + throw new ClientException(_('The uploaded file exceeds the ' . + 'upload_max_filesize directive in php.ini.')); + return; + case UPLOAD_ERR_FORM_SIZE: + throw new ClientException( + _('The uploaded file exceeds the MAX_FILE_SIZE directive' . + ' that was specified in the HTML form.')); + return; + case UPLOAD_ERR_PARTIAL: + @unlink($_FILES[$param]['tmp_name']); + throw new ClientException(_('The uploaded file was only' . + ' partially uploaded.')); + return; + case UPLOAD_ERR_NO_FILE: + // No file; probably just a non-AJAX submission. + return; + case UPLOAD_ERR_NO_TMP_DIR: + throw new ClientException(_('Missing a temporary folder.')); + return; + case UPLOAD_ERR_CANT_WRITE: + throw new ClientException(_('Failed to write file to disk.')); + return; + case UPLOAD_ERR_EXTENSION: + throw new ClientException(_('File upload stopped by extension.')); + return; + default: + common_log(LOG_ERR, __METHOD__ . ": Unknown upload error " . + $_FILES[$param]['error']); + throw new ClientException(_('System error uploading file.')); + return; + } + + if (!MediaFile::respectsQuota($user, $_FILES['attach']['size'])) { + + // Should never actually get here + + @unlink($_FILES[$param]['tmp_name']); + throw new ClientException(_('File exceeds user\'s quota!')); + return; + } + + $mimetype = MediaFile::getUploadedFileType($_FILES[$param]['tmp_name']); + + $filename = null; + + if (isset($mimetype)) { + + $basename = basename($_FILES[$param]['name']); + $filename = File::filename($user->getProfile(), $basename, $mimetype); + $filepath = File::path($filename); + + $result = move_uploaded_file($_FILES[$param]['tmp_name'], $filepath); + + if (!$result) { + throw new ClientException(_('File could not be moved to destination directory.')); + return; + } + + } else { + throw new ClientException(_('Could not determine file\'s mime-type!')); + return; + } + + return new MediaFile($user, $filename, $mimetype); + } + + static function fromFilehandle($fh, $user) { + + $stream = stream_get_meta_data($fh); + + if (!MediaFile::respectsQuota($user, filesize($stream['uri']))) { + + // Should never actually get here + + throw new ClientException(_('File exceeds user\'s quota!')); + return; + } + + $mimetype = MediaFile::getUploadedFileType($fh); + + $filename = null; + + if (isset($mimetype)) { + + $filename = File::filename($user->getProfile(), "email", $mimetype); + + $filepath = File::path($filename); + + $result = copy($stream['uri'], $filepath) && chmod($filepath, 0664); + + if (!$result) { + throw new ClientException(_('File could not be moved to destination directory.' . + $stream['uri'] . ' ' . $filepath)); + } + } else { + throw new ClientException(_('Could not determine file\'s mime-type!')); + return; + } + + return new MediaFile($user, $filename, $mimetype); + } + + static function getUploadedFileType($f) { + require_once 'MIME/Type.php'; + + $cmd = &PEAR::getStaticProperty('MIME_Type', 'fileCmd'); + $cmd = common_config('attachments', 'filecommand'); + + $filetype = null; + + if (is_string($f)) { + + // assuming a filename + + $filetype = MIME_Type::autoDetect($f); + } else { + + // assuming a filehandle + + $stream = stream_get_meta_data($f); + $filetype = MIME_Type::autoDetect($stream['uri']); + } + + if (in_array($filetype, common_config('attachments', 'supported'))) { + return $filetype; + } + $media = MIME_Type::getMedia($filetype); + if ('application' !== $media) { + $hint = sprintf(_(' Try using another %s format.'), $media); + } else { + $hint = ''; + } + throw new ClientException(sprintf( + _('%s is not a supported filetype on this server.'), $filetype) . $hint); + } + + static function respectsQuota($user, $filesize) + { + $file = new File; + $result = $file->isRespectsQuota($user, $filesize); + if ($result === true) { + return true; + } else { + throw new ClientException($result); + } + } + +}
\ No newline at end of file diff --git a/lib/messageform.php b/lib/messageform.php index 6431bfdcc..b034be312 100644 --- a/lib/messageform.php +++ b/lib/messageform.php @@ -80,11 +80,22 @@ class MessageForm extends Form /** * ID of the form * - * @return int ID of the form + * @return string ID of the form */ function id() { + return 'form_notice-direct'; + } + + /** + * Class of the form + * + * @return string class of the form + */ + + function formClass() + { return 'form_notice'; } @@ -140,12 +151,19 @@ class MessageForm extends Form 'rows' => 4, 'name' => 'content'), ($this->content) ? $this->content : ''); - $this->out->elementStart('dl', 'form_note'); - $this->out->element('dt', null, _('Available characters')); - $this->out->element('dd', array('id' => 'notice_text-count'), - '140'); - $this->out->elementEnd('dl'); + $contentLimit = Message::maxContent(); + + $this->out->element('script', array('type' => 'text/javascript'), + 'maxLength = ' . $contentLimit . ';'); + + if ($contentLimit > 0) { + $this->out->elementStart('dl', 'form_note'); + $this->out->element('dt', null, _('Available characters')); + $this->out->element('dd', array('id' => 'notice_text-count'), + $contentLimit); + $this->out->elementEnd('dl'); + } } /** diff --git a/lib/noticeform.php b/lib/noticeform.php index 350e37db8..ec8624597 100644 --- a/lib/noticeform.php +++ b/lib/noticeform.php @@ -76,6 +76,15 @@ class NoticeForm extends Form var $inreplyto = null; /** + * Pre-filled location content of the form + */ + + var $lat; + var $lon; + var $location_id; + var $location_ns; + + /** * Constructor * * @param HTMLOutputter $out output channel @@ -83,14 +92,18 @@ class NoticeForm extends Form * @param string $content content to pre-fill */ - function __construct($out=null, $action=null, $content=null, $user=null, $inreplyto=null) + function __construct($out=null, $action=null, $content=null, $user=null, $inreplyto=null, $lat=null, $lon=null, $location_id=null, $location_ns=null) { parent::__construct($out); $this->action = $action; $this->content = $content; $this->inreplyto = $inreplyto; - + $this->lat = $lat; + $this->lon = $lon; + $this->location_id = $location_id; + $this->location_ns = $location_ns; + if ($user) { $this->user = $user; } else { @@ -105,7 +118,7 @@ class NoticeForm extends Form /** * ID of the form * - * @return int ID of the form + * @return string ID of the form */ function id() @@ -113,6 +126,17 @@ class NoticeForm extends Form return 'form_notice'; } + /** + * Class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'form_notice'; + } + /** * Action of the form * @@ -124,7 +148,6 @@ class NoticeForm extends Form return common_local_url('newnotice'); } - /** * Legend of the Form * @@ -135,7 +158,6 @@ class NoticeForm extends Form $this->out->element('legend', null, _('Send a notice')); } - /** * Data elements * @@ -144,31 +166,48 @@ class NoticeForm extends Form function formData() { - $this->out->element('label', array('for' => 'notice_data-text'), - sprintf(_('What\'s up, %s?'), $this->user->nickname)); - // XXX: vary by defined max size - $this->out->element('textarea', array('id' => 'notice_data-text', - 'cols' => 35, - 'rows' => 4, - 'name' => 'status_textarea'), - ($this->content) ? $this->content : ''); - $this->out->elementStart('dl', 'form_note'); - $this->out->element('dt', null, _('Available characters')); - $this->out->element('dd', array('id' => 'notice_text-count'), - '140'); - $this->out->elementEnd('dl'); - if (common_config('attachments', 'uploads')) { - $this->out->element('label', array('for' => 'notice_data-attach'),_('Attach')); - $this->out->element('input', array('id' => 'notice_data-attach', - 'type' => 'file', - 'name' => 'attach', - 'title' => _('Attach a file'))); - $this->out->hidden('MAX_FILE_SIZE', common_config('attachments', 'file_quota')); - } - if ($this->action) { - $this->out->hidden('notice_return-to', $this->action, 'returnto'); + if (Event::handle('StartShowNoticeFormData', array($this))) { + $this->out->element('label', array('for' => 'notice_data-text'), + sprintf(_('What\'s up, %s?'), $this->user->nickname)); + // XXX: vary by defined max size + $this->out->element('textarea', array('id' => 'notice_data-text', + 'cols' => 35, + 'rows' => 4, + 'name' => 'status_textarea'), + ($this->content) ? $this->content : ''); + + $contentLimit = Notice::maxContent(); + + $this->out->element('script', array('type' => 'text/javascript'), + 'maxLength = ' . $contentLimit . ';'); + + if ($contentLimit > 0) { + $this->out->elementStart('dl', 'form_note'); + $this->out->element('dt', null, _('Available characters')); + $this->out->element('dd', array('id' => 'notice_text-count'), + $contentLimit); + $this->out->elementEnd('dl'); + } + + if (common_config('attachments', 'uploads')) { + $this->out->element('label', array('for' => 'notice_data-attach'),_('Attach')); + $this->out->element('input', array('id' => 'notice_data-attach', + 'type' => 'file', + 'name' => 'attach', + 'title' => _('Attach a file'))); + $this->out->hidden('MAX_FILE_SIZE', common_config('attachments', 'file_quota')); + } + if ($this->action) { + $this->out->hidden('notice_return-to', $this->action, 'returnto'); + } + $this->out->hidden('notice_in-reply-to', $this->inreplyto, 'inreplyto'); + $this->out->hidden('notice_data-lat', empty($this->lat) ? null : $this->lat, 'lat'); + $this->out->hidden('notice_data-lon', empty($this->lon) ? null : $this->lon, 'lon'); + $this->out->hidden('notice_data-location_id', empty($this->location_id) ? null : $this->location_id, 'location_id'); + $this->out->hidden('notice_data-location_ns', empty($this->location_ns) ? null : $this->location_ns, 'location_ns'); + + Event::handle('StartShowNoticeFormData', array($this)); } - $this->out->hidden('notice_in-reply-to', $this->inreplyto, 'inreplyto'); } /** diff --git a/lib/noticelist.php b/lib/noticelist.php index d4cd3ff6e..5877827ff 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -178,9 +178,12 @@ class NoticeListItem extends Widget function show() { $this->showStart(); - $this->showNotice(); - $this->showNoticeInfo(); - $this->showNoticeOptions(); + if (Event::handle('StartShowNoticeItem', array($this))) { + $this->showNotice(); + $this->showNoticeInfo(); + $this->showNoticeOptions(); + Event::handle('EndShowNoticeItem', array($this)); + } $this->showEnd(); } @@ -197,6 +200,7 @@ class NoticeListItem extends Widget $this->out->elementStart('div', 'entry-content'); $this->showNoticeLink(); $this->showNoticeSource(); + $this->showNoticeLocation(); $this->showContext(); $this->out->elementEnd('div'); } @@ -367,6 +371,54 @@ class NoticeListItem extends Widget } /** + * show the notice location + * + * shows the notice location in the correct language. + * + * If an URL is available, makes a link. Otherwise, just a span. + * + * @return void + */ + + function showNoticeLocation() + { + $id = $this->notice->id; + + $location = $this->notice->getLocation(); + + if (empty($location)) { + return; + } + + $name = $location->getName(); + + if (empty($name)) { + // XXX: Could be a translation issue. Fall back to... something? + return; + } + + $lat = $this->notice->lat; + $lon = $this->notice->lon; + $latlon = (!empty($lat) && !empty($lon)) ? $lat.';'.$lon : ''; + + $url = $location->getUrl(); + + $this->out->elementStart('span', array('class' => 'location')); + $this->out->text(_('at')); + if (empty($url)) { + $this->out->element('span', array('class' => 'geo', + 'title' => $latlon), + $name); + } else { + $this->out->element('a', array('class' => 'geo', + 'title' => $latlon, + 'href' => $url), + $name); + } + $this->out->elementEnd('span'); + } + + /** * Show the source of the notice * * Either the name (and link) of the API client that posted the notice, @@ -469,7 +521,10 @@ class NoticeListItem extends Widget function showDeleteLink() { $user = common_current_user(); - if ($user && $this->notice->profile_id == $user->id) { + + if (!empty($user) && + ($this->notice->profile_id == $user->id || $user->hasRight(Right::DELETEOTHERSNOTICE))) { + $deleteurl = common_local_url('deletenotice', array('notice' => $this->notice->id)); $this->out->element('a', array('href' => $deleteurl, diff --git a/lib/noticesection.php b/lib/noticesection.php index b223932ef..24465f8ba 100644 --- a/lib/noticesection.php +++ b/lib/noticesection.php @@ -114,7 +114,7 @@ class NoticeSection extends Section $att_class = 'attachments'; } - $clip = theme_path('images/icons/clip.png', 'base'); + $clip = Theme::path('images/icons/clip.png', 'base'); $this->out->elementStart('a', array('class' => $att_class, 'style' => "font-style: italic;", 'href' => $href, 'title' => "# of attachments: $count")); $this->out->raw(" ($count "); $this->out->element('img', array('style' => 'display: inline', 'align' => 'top', 'width' => 20, 'height' => 20, 'src' => $clip, 'alt' => 'alt')); diff --git a/lib/oauthclient.php b/lib/oauthclient.php index f1827726e..1a86e2460 100644 --- a/lib/oauthclient.php +++ b/lib/oauthclient.php @@ -43,7 +43,7 @@ require_once 'OAuth.php'; * @link http://status.net/ * */ -class OAuthClientCurlException extends Exception +class OAuthClientException extends Exception { } @@ -97,9 +97,14 @@ class OAuthClient function getRequestToken($url) { $response = $this->oAuthGet($url); - parse_str($response); - $token = new OAuthToken($oauth_token, $oauth_token_secret); - return $token; + $arr = array(); + parse_str($response, $arr); + if (isset($arr['oauth_token']) && isset($arr['oauth_token_secret'])) { + $token = new OAuthToken($arr['oauth_token'], @$arr['oauth_token_secret']); + return $token; + } else { + throw new OAuthClientException(); + } } /** @@ -177,7 +182,7 @@ class OAuthClient } /** - * Make a HTTP request using cURL. + * Make a HTTP request. * * @param string $url Where to make the * @param array $params post parameters @@ -186,40 +191,32 @@ class OAuthClient */ function httpRequest($url, $params = null) { - $options = array( - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FAILONERROR => true, - CURLOPT_HEADER => false, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_USERAGENT => 'StatusNet', - CURLOPT_CONNECTTIMEOUT => 120, - CURLOPT_TIMEOUT => 120, - CURLOPT_HTTPAUTH => CURLAUTH_ANY, - CURLOPT_SSL_VERIFYPEER => false, - - // Twitter is strict about accepting invalid "Expect" headers - - CURLOPT_HTTPHEADER => array('Expect:') - ); + $request = new HTTPClient($url); + $request->setConfig(array( + 'connect_timeout' => 120, + 'timeout' => 120, + 'follow_redirects' => true, + 'ssl_verify_peer' => false, + )); + + // Twitter is strict about accepting invalid "Expect" headers + $request->setHeader('Expect', ''); if (isset($params)) { - $options[CURLOPT_POST] = true; - $options[CURLOPT_POSTFIELDS] = $params; + $request->setMethod(HTTP_Request2::METHOD_POST); + $request->setBody($params); } - $ch = curl_init($url); - curl_setopt_array($ch, $options); - $response = curl_exec($ch); - - if ($response === false) { - $msg = curl_error($ch); - $code = curl_errno($ch); - throw new OAuthClientCurlException($msg, $code); + try { + $response = $request->send(); + $code = $response->getStatus(); + if ($code < 200 || $code >= 400) { + throw new OAuthClientException($response->getBody(), $code); + } + return $response->getBody(); + } catch (Exception $e) { + throw new OAuthClientException($e->getMessage(), $e->getCode()); } - - curl_close($ch); - - return $response; } } diff --git a/lib/oauthstore.php b/lib/oauthstore.php index 6db07b20f..b04bcbb8b 100644 --- a/lib/oauthstore.php +++ b/lib/oauthstore.php @@ -19,13 +19,12 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once(INSTALLDIR.'/lib/omb.php'); +require_once 'libomb/datastore.php'; class StatusNetOAuthDataStore extends OAuthDataStore { // We keep a record of who's contacted us - function lookup_consumer($consumer_key) { $con = Consumer::staticGet('consumer_key', $consumer_key); @@ -44,7 +43,9 @@ class StatusNetOAuthDataStore extends OAuthDataStore function lookup_token($consumer, $token_type, $token_key) { $t = new Token(); - $t->consumer_key = $consumer->key; + if (!is_null($consumer)) { + $t->consumer_key = $consumer->key; + } $t->tok = $token_key; $t->type = ($token_type == 'access') ? 1 : 0; if ($t->find(true)) { @@ -154,4 +155,349 @@ class StatusNetOAuthDataStore extends OAuthDataStore { return $this->new_access_token($consumer); } + + /** + * Revoke specified OAuth token + * + * Revokes the authorization token specified by $token_key. + * Throws exceptions in case of error. + * + * @param string $token_key The token to be revoked + * + * @access public + **/ + public function revoke_token($token_key) { + $rt = new Token(); + $rt->tok = $token_key; + $rt->type = 0; + $rt->state = 0; + if (!$rt->find(true)) { + throw new Exception('Tried to revoke unknown token'); + } + if (!$rt->delete()) { + throw new Exception('Failed to delete revoked token'); + } + } + + /** + * Authorize specified OAuth token + * + * Authorizes the authorization token specified by $token_key. + * Throws exceptions in case of error. + * + * @param string $token_key The token to be authorized + * + * @access public + **/ + public function authorize_token($token_key) { + $rt = new Token(); + $rt->tok = $token_key; + $rt->type = 0; + $rt->state = 0; + if (!$rt->find(true)) { + throw new Exception('Tried to authorize unknown token'); + } + $orig_rt = clone($rt); + $rt->state = 1; # Authorized but not used + if (!$rt->update($orig_rt)) { + throw new Exception('Failed to authorize token'); + } + } + + /** + * Get profile by identifying URI + * + * Returns an OMB_Profile object representing the OMB profile identified by + * $identifier_uri. + * Returns null if there is no such OMB profile. + * Throws exceptions in case of other error. + * + * @param string $identifier_uri The OMB identifier URI specifying the + * requested profile + * + * @access public + * + * @return OMB_Profile The corresponding profile + **/ + public function getProfile($identifier_uri) { + /* getProfile is only used for remote profiles by libomb. + TODO: Make it work with local ones anyway. */ + $remote = Remote_profile::staticGet('uri', $identifier_uri); + if (!$remote) throw new Exception('No such remote profile'); + $profile = Profile::staticGet('id', $remote->id); + if (!$profile) throw new Exception('No profile for remote user'); + + require_once INSTALLDIR.'/lib/omb.php'; + return profile_to_omb_profile($identifier_uri, $profile); + } + + /** + * Save passed profile + * + * Stores the OMB profile $profile. Overwrites an existing entry. + * Throws exceptions in case of error. + * + * @param OMB_Profile $profile The OMB profile which should be saved + * + * @access public + **/ + public function saveProfile($omb_profile) { + if (common_profile_url($omb_profile->getNickname()) == + $omb_profile->getProfileURL()) { + throw new Exception('Not implemented'); + } else { + $remote = Remote_profile::staticGet('uri', $omb_profile->getIdentifierURI()); + + if ($remote) { + $exists = true; + $profile = Profile::staticGet($remote->id); + $orig_remote = clone($remote); + $orig_profile = clone($profile); + # XXX: compare current postNotice and updateProfile URLs to the ones + # stored in the DB to avoid (possibly...) above attack + } else { + $exists = false; + $remote = new Remote_profile(); + $remote->uri = $omb_profile->getIdentifierURI(); + $profile = new Profile(); + } + + $profile->nickname = $omb_profile->getNickname(); + $profile->profileurl = $omb_profile->getProfileURL(); + + $fullname = $omb_profile->getFullname(); + $profile->fullname = is_null($fullname) ? '' : $fullname; + $homepage = $omb_profile->getHomepage(); + $profile->homepage = is_null($homepage) ? '' : $homepage; + $bio = $omb_profile->getBio(); + $profile->bio = is_null($bio) ? '' : $bio; + $location = $omb_profile->getLocation(); + $profile->location = is_null($location) ? '' : $location; + + if ($exists) { + $profile->update($orig_profile); + } else { + $profile->created = DB_DataObject_Cast::dateTime(); # current time + $id = $profile->insert(); + if (!$id) { + throw new Exception(_('Error inserting new profile')); + } + $remote->id = $id; + } + + $avatar_url = $omb_profile->getAvatarURL(); + if ($avatar_url) { + if (!$this->add_avatar($profile, $avatar_url)) { + throw new Exception(_('Error inserting avatar')); + } + } else { + $avatar = $profile->getOriginalAvatar(); + if($avatar) $avatar->delete(); + $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); + if($avatar) $avatar->delete(); + $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE); + if($avatar) $avatar->delete(); + $avatar = $profile->getAvatar(AVATAR_MINI_SIZE); + if($avatar) $avatar->delete(); + } + + if ($exists) { + if (!$remote->update($orig_remote)) { + throw new Exception(_('Error updating remote profile')); + } + } else { + $remote->created = DB_DataObject_Cast::dateTime(); # current time + if (!$remote->insert()) { + throw new Exception(_('Error inserting remote profile')); + } + } + } + } + + function add_avatar($profile, $url) + { + $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); + copy($url, $temp_filename); + $imagefile = new ImageFile($profile->id, $temp_filename); + $filename = Avatar::filename($profile->id, + image_type_to_extension($imagefile->type), + null, + common_timestamp()); + rename($temp_filename, Avatar::path($filename)); + return $profile->setOriginal($filename); + } + + /** + * Save passed notice + * + * Stores the OMB notice $notice. The datastore may change the passed notice. + * This might by neccessary for URIs depending on a database key. Note that + * it is the user’s duty to present a mechanism for his OMB_Datastore to + * appropriately change his OMB_Notice. + * Throws exceptions in case of error. + * + * @param OMB_Notice $notice The OMB notice which should be saved + * + * @access public + **/ + public function saveNotice(&$omb_notice) { + if (Notice::staticGet('uri', $omb_notice->getIdentifierURI())) { + throw new Exception(_('Duplicate notice')); + } + $author_uri = $omb_notice->getAuthor()->getIdentifierURI(); + common_log(LOG_DEBUG, $author_uri, __FILE__); + $author = Remote_profile::staticGet('uri', $author_uri); + if (!$author) { + $author = User::staticGet('uri', $author_uri); + } + if (!$author) { + throw new Exception('No such user.'); + } + + common_log(LOG_DEBUG, print_r($author, true), __FILE__); + + $notice = Notice::saveNew($author->id, + $omb_notice->getContent(), + 'omb', + false, + null, + $omb_notice->getIdentifierURI()); + + common_broadcast_notice($notice, true); + } + + /** + * Get subscriptions of a given profile + * + * Returns an array containing subscription informations for the specified + * profile. Every array entry should in turn be an array with keys + * 'uri´: The identifier URI of the subscriber + * 'token´: The subscribe token + * 'secret´: The secret token + * Throws exceptions in case of error. + * + * @param string $subscribed_user_uri The OMB identifier URI specifying the + * subscribed profile + * + * @access public + * + * @return mixed An array containing the subscriptions or 0 if no + * subscription has been found. + **/ + public function getSubscriptions($subscribed_user_uri) { + $sub = new Subscription(); + + $user = $this->_getAnyProfile($subscribed_user_uri); + + $sub->subscribed = $user->id; + + if (!$sub->find(true)) { + return 0; + } + + /* Since we do not use OMB_Service_Provider’s action methods, there + is no need to actually return the subscriptions. */ + return 1; + } + + private function _getAnyProfile($uri) + { + $user = Remote_profile::staticGet('uri', $uri); + if (!$user) { + $user = User::staticGet('uri', $uri); + } + if (!$user) { + throw new Exception('No such user.'); + } + return $user; + } + + /** + * Delete a subscription + * + * Deletes the subscription from $subscriber_uri to $subscribed_user_uri. + * Throws exceptions in case of error. + * + * @param string $subscriber_uri The OMB identifier URI specifying the + * subscribing profile + * + * @param string $subscribed_user_uri The OMB identifier URI specifying the + * subscribed profile + * + * @access public + **/ + public function deleteSubscription($subscriber_uri, $subscribed_user_uri) + { + $sub = new Subscription(); + + $subscribed = $this->_getAnyProfile($subscribed_user_uri); + $subscriber = $this->_getAnyProfile($subscriber_uri); + + $sub->subscribed = $subscribed->id; + $sub->subscriber = $subscriber->id; + + $sub->delete(); + } + + /** + * Save a subscription + * + * Saves the subscription from $subscriber_uri to $subscribed_user_uri. + * Throws exceptions in case of error. + * + * @param string $subscriber_uri The OMB identifier URI specifying + * the subscribing profile + * + * @param string $subscribed_user_uri The OMB identifier URI specifying + * the subscribed profile + * @param OAuthToken $token The access token + * + * @access public + **/ + public function saveSubscription($subscriber_uri, $subscribed_user_uri, + $token) + { + $sub = new Subscription(); + + $subscribed = $this->_getAnyProfile($subscribed_user_uri); + $subscriber = $this->_getAnyProfile($subscriber_uri); + + if (!$subscriber->hasRight(Right::SUBSCRIBE)) { + return _('You have been banned from subscribing.'); + } + + $sub->subscribed = $subscribed->id; + $sub->subscriber = $subscriber->id; + + $sub_exists = $sub->find(true); + + if ($sub_exists) { + $orig_sub = clone($sub); + } else { + $sub->created = DB_DataObject_Cast::dateTime(); + } + + $sub->token = $token->key; + $sub->secret = $token->secret; + + if ($sub_exists) { + $result = $sub->update($orig_sub); + } else { + $result = $sub->insert(); + } + + if (!$result) { + common_log_db_error($sub, ($sub_exists) ? 'UPDATE' : 'INSERT', __FILE__); + throw new Exception(_('Couldn\'t insert new subscription.')); + return; + } + + /* Notify user, if necessary. */ + + if ($subscribed instanceof User) { + mail_subscribe_notify_profile($subscribed, + Profile::staticGet($subscriber->id)); + } + } } +?> diff --git a/lib/omb.php b/lib/omb.php index 7dca760c6..cd6d6e1b2 100644 --- a/lib/omb.php +++ b/lib/omb.php @@ -19,34 +19,18 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once('OAuth.php'); -require_once(INSTALLDIR.'/lib/oauthstore.php'); - -require_once(INSTALLDIR.'/classes/Consumer.php'); -require_once(INSTALLDIR.'/classes/Nonce.php'); -require_once(INSTALLDIR.'/classes/Token.php'); - -require_once('Auth/Yadis/Yadis.php'); - -define('OAUTH_NAMESPACE', 'http://oauth.net/core/1.0/'); -define('OMB_NAMESPACE', 'http://openmicroblogging.org/protocol/0.1'); -define('OMB_VERSION_01', 'http://openmicroblogging.org/protocol/0.1'); -define('OAUTH_DISCOVERY', 'http://oauth.net/discovery/1.0'); - -define('OMB_ENDPOINT_UPDATEPROFILE', OMB_NAMESPACE.'/updateProfile'); -define('OMB_ENDPOINT_POSTNOTICE', OMB_NAMESPACE.'/postNotice'); -define('OAUTH_ENDPOINT_REQUEST', OAUTH_NAMESPACE.'endpoint/request'); -define('OAUTH_ENDPOINT_AUTHORIZE', OAUTH_NAMESPACE.'endpoint/authorize'); -define('OAUTH_ENDPOINT_ACCESS', OAUTH_NAMESPACE.'endpoint/access'); -define('OAUTH_ENDPOINT_RESOURCE', OAUTH_NAMESPACE.'endpoint/resource'); -define('OAUTH_AUTH_HEADER', OAUTH_NAMESPACE.'parameters/auth-header'); -define('OAUTH_POST_BODY', OAUTH_NAMESPACE.'parameters/post-body'); -define('OAUTH_HMAC_SHA1', OAUTH_NAMESPACE.'signature/HMAC-SHA1'); +require_once INSTALLDIR.'/lib/oauthstore.php'; +require_once 'OAuth.php'; +require_once 'libomb/constants.php'; +require_once 'libomb/service_consumer.php'; +require_once 'libomb/notice.php'; +require_once 'libomb/profile.php'; +require_once 'Auth/Yadis/Yadis.php'; function omb_oauth_consumer() { static $con = null; - if (!$con) { + if (is_null($con)) { $con = new OAuthConsumer(common_root_url(), ''); } return $con; @@ -55,7 +39,7 @@ function omb_oauth_consumer() function omb_oauth_server() { static $server = null; - if (!$server) { + if (is_null($server)) { $server = new OAuthServer(omb_oauth_datastore()); $server->add_signature_method(omb_hmac_sha1()); } @@ -65,7 +49,7 @@ function omb_oauth_server() function omb_oauth_datastore() { static $store = null; - if (!$store) { + if (is_null($store)) { $store = new StatusNetOAuthDataStore(); } return $store; @@ -74,57 +58,18 @@ function omb_oauth_datastore() function omb_hmac_sha1() { static $hmac_method = null; - if (!$hmac_method) { + if (is_null($hmac_method)) { $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); } return $hmac_method; } -function omb_get_services($xrd, $type) +function omb_broadcast_notice($notice) { - return $xrd->services(array(omb_service_filter($type))); -} - -function omb_service_filter($type) -{ - return create_function('$s', - 'return omb_match_service($s, \''.$type.'\');'); -} - -function omb_match_service($service, $type) -{ - return in_array($type, $service->getTypes()); -} - -function omb_service_uri($service) -{ - if (!$service) { - return null; - } - $uris = $service->getURIs(); - if (!$uris) { - return null; - } - return $uris[0]; -} - -function omb_local_id($service) -{ - if (!$service) { - return null; - } - $els = $service->getElements('xrd:LocalID'); - if (!$els) { - return null; - } - $el = $els[0]; - return $service->parser->content($el); -} -function omb_broadcast_remote_subscribers($notice) -{ + $omb_notice = notice_to_omb_notice($notice); - # First, get remote users subscribed to this profile + /* Get remote users subscribed to this profile. */ $rp = new Remote_profile(); $rp->query('SELECT postnoticeurl, token, secret ' . @@ -135,170 +80,148 @@ function omb_broadcast_remote_subscribers($notice) $posted = array(); while ($rp->fetch()) { - if (!array_key_exists($rp->postnoticeurl, $posted)) { - common_log(LOG_DEBUG, 'Posting to ' . $rp->postnoticeurl); - if (omb_post_notice_keys($notice, $rp->postnoticeurl, $rp->token, $rp->secret)) { - common_log(LOG_DEBUG, 'Finished to ' . $rp->postnoticeurl); - $posted[$rp->postnoticeurl] = true; - } else { - common_log(LOG_DEBUG, 'Failed posting to ' . $rp->postnoticeurl); - } + if (isset($posted[$rp->postnoticeurl])) { + /* We already posted to this url. */ + continue; } - } - - $rp->free(); - unset($rp); + common_debug('Posting to ' . $rp->postnoticeurl, __FILE__); + + /* Post notice. */ + $service = new StatusNet_OMB_Service_Consumer( + array(OMB_ENDPOINT_POSTNOTICE => $rp->postnoticeurl)); + try { + $service->setToken($rp->token, $rp->secret); + $service->postNotice($omb_notice); + } catch (Exception $e) { + common_log(LOG_ERR, 'Failed posting to ' . $rp->postnoticeurl); + common_log(LOG_ERR, 'Error status '.$e); + continue; + } + $posted[$rp->postnoticeurl] = true; - return true; -} + common_debug('Finished to ' . $rp->postnoticeurl, __FILE__); + } -function omb_post_notice($notice, $remote_profile, $subscription) -{ - return omb_post_notice_keys($notice, $remote_profile->postnoticeurl, $subscription->token, $subscription->secret); + return; } -function omb_post_notice_keys($notice, $postnoticeurl, $tk, $secret) +function omb_broadcast_profile($profile) { - $user = User::staticGet('id', $notice->profile_id); + $user = User::staticGet('id', $profile->id); if (!$user) { return false; } - $con = omb_oauth_consumer(); + $profile = $user->getProfile(); - $token = new OAuthToken($tk, $secret); - - $url = $postnoticeurl; - $parsed = parse_url($url); - $params = array(); - parse_str($parsed['query'], $params); - - $req = OAuthRequest::from_consumer_and_token($con, $token, - 'POST', $url, $params); - - $req->set_parameter('omb_version', OMB_VERSION_01); - $req->set_parameter('omb_listenee', $user->uri); - $req->set_parameter('omb_notice', $notice->uri); - $req->set_parameter('omb_notice_content', $notice->content); - $req->set_parameter('omb_notice_url', common_local_url('shownotice', - array('notice' => - $notice->id))); - $req->set_parameter('omb_notice_license', common_config('license', 'url')); + $omb_profile = profile_to_omb_profile($user->uri, $profile, true); - $user->free(); - unset($user); + /* Get remote users subscribed to this profile. */ + $rp = new Remote_profile(); - $req->sign_request(omb_hmac_sha1(), $con, $token); + $rp->query('SELECT updateprofileurl, token, secret ' . + 'FROM subscription JOIN remote_profile ' . + 'ON subscription.subscriber = remote_profile.id ' . + 'WHERE subscription.subscribed = ' . $profile->id . ' '); - # We re-use this tool's fetcher, since it's pretty good + $posted = array(); - $fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); + while ($rp->fetch()) { + if (isset($posted[$rp->updateprofileurl])) { + /* We already posted to this url. */ + continue; + } + common_debug('Posting to ' . $rp->updateprofileurl, __FILE__); + + /* Update profile. */ + $service = new StatusNet_OMB_Service_Consumer( + array(OMB_ENDPOINT_UPDATEPROFILE => $rp->updateprofileurl)); + try { + $service->setToken($rp->token, $rp->secret); + $service->updateProfile($omb_profile); + } catch (Exception $e) { + common_log(LOG_ERR, 'Failed posting to ' . $rp->updateprofileurl); + common_log(LOG_ERR, 'Error status '.$e); + continue; + } + $posted[$rp->updateprofileurl] = true; - if (!$fetcher) { - common_log(LOG_WARNING, 'Failed to initialize Yadis fetcher.', __FILE__); - return false; + common_debug('Finished to ' . $rp->updateprofileurl, __FILE__); } - $result = $fetcher->post($req->get_normalized_http_url(), - $req->to_postdata(), - array('User-Agent: StatusNet/' . STATUSNET_VERSION)); - - if ($result->status == 403) { # not authorized, don't send again - common_debug('403 result, deleting subscription', __FILE__); - # FIXME: figure out how to delete this - # $subscription->delete(); - return false; - } else if ($result->status != 200) { - common_debug('Error status '.$result->status, __FILE__); - return false; - } else { # success! - parse_str($result->body, $return); - if ($return['omb_version'] == OMB_VERSION_01) { - return true; - } else { - return false; - } - } + return; } -function omb_broadcast_profile($profile) -{ - # First, get remote users subscribed to this profile - # XXX: use a join here rather than looping through results - $sub = new Subscription(); - $sub->subscribed = $profile->id; - if ($sub->find()) { - $updated = array(); - while ($sub->fetch()) { - $rp = Remote_profile::staticGet('id', $sub->subscriber); - if ($rp) { - if (!array_key_exists($rp->updateprofileurl, $updated)) { - if (omb_update_profile($profile, $rp, $sub)) { - $updated[$rp->updateprofileurl] = true; - } - } - } - } +class StatusNet_OMB_Service_Consumer extends OMB_Service_Consumer { + public function __construct($urls) + { + $this->services = $urls; + $this->datastore = omb_oauth_datastore(); + $this->oauth_consumer = omb_oauth_consumer(); + $this->fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); } + } -function omb_update_profile($profile, $remote_profile, $subscription) +function profile_to_omb_profile($uri, $profile, $force = false) { - $user = User::staticGet($profile->id); - $con = omb_oauth_consumer(); - $token = new OAuthToken($subscription->token, $subscription->secret); - $url = $remote_profile->updateprofileurl; - $parsed = parse_url($url); - $params = array(); - parse_str($parsed['query'], $params); - $req = OAuthRequest::from_consumer_and_token($con, $token, - "POST", $url, $params); - $req->set_parameter('omb_version', OMB_VERSION_01); - $req->set_parameter('omb_listenee', $user->uri); - $req->set_parameter('omb_listenee_profile', common_profile_url($profile->nickname)); - $req->set_parameter('omb_listenee_nickname', $profile->nickname); - - # We use blanks to force emptying any existing values in these optional fields - - $req->set_parameter('omb_listenee_fullname', - ($profile->fullname) ? $profile->fullname : ''); - $req->set_parameter('omb_listenee_homepage', - ($profile->homepage) ? $profile->homepage : ''); - $req->set_parameter('omb_listenee_bio', - ($profile->bio) ? $profile->bio : ''); - $req->set_parameter('omb_listenee_location', - ($profile->location) ? $profile->location : ''); + $omb_profile = new OMB_Profile($uri); + $omb_profile->setNickname($profile->nickname); + $omb_profile->setLicenseURL(common_config('license', 'url')); + if (!is_null($profile->fullname)) { + $omb_profile->setFullname($profile->fullname); + } elseif ($force) { + $omb_profile->setFullname(''); + } + if (!is_null($profile->homepage)) { + $omb_profile->setHomepage($profile->homepage); + } elseif ($force) { + $omb_profile->setHomepage(''); + } + if (!is_null($profile->bio)) { + $omb_profile->setBio($profile->bio); + } elseif ($force) { + $omb_profile->setBio(''); + } + if (!is_null($profile->location)) { + $omb_profile->setLocation($profile->location); + } elseif ($force) { + $omb_profile->setLocation(''); + } + if (!is_null($profile->profileurl)) { + $omb_profile->setProfileURL($profile->profileurl); + } elseif ($force) { + $omb_profile->setProfileURL(''); + } $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); - $req->set_parameter('omb_listenee_avatar', - ($avatar) ? $avatar->url : ''); + if ($avatar) { + $omb_profile->setAvatarURL($avatar->url); + } elseif ($force) { + $omb_profile->setAvatarURL(''); + } + return $omb_profile; +} - $req->sign_request(omb_hmac_sha1(), $con, $token); +function notice_to_omb_notice($notice) +{ + /* Create an OMB_Notice for $notice. */ + $user = User::staticGet('id', $notice->profile_id); - # We re-use this tool's fetcher, since it's pretty good + if (!$user) { + return null; + } - $fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); + $profile = $user->getProfile(); - $result = $fetcher->post($req->get_normalized_http_url(), - $req->to_postdata(), - array('User-Agent: StatusNet/' . STATUSNET_VERSION)); + $omb_notice = new OMB_Notice(profile_to_omb_profile($user->uri, $profile), + $notice->uri, + $notice->content); + $omb_notice->setURL(common_local_url('shownotice', array('notice' => + $notice->id))); + $omb_notice->setLicenseURL(common_config('license', 'url')); - if (empty($result) || !$result) { - common_debug("Unable to contact " . $req->get_normalized_http_url()); - } else if ($result->status == 403) { # not authorized, don't send again - common_debug('403 result, deleting subscription', __FILE__); - $subscription->delete(); - return false; - } else if ($result->status != 200) { - common_debug('Error status '.$result->status, __FILE__); - return false; - } else { # success! - parse_str($result->body, $return); - if (isset($return['omb_version']) && $return['omb_version'] === OMB_VERSION_01) { - return true; - } else { - return false; - } - } + return $omb_notice; } +?> diff --git a/lib/openid.php b/lib/openid.php deleted file mode 100644 index 7a2c46f00..000000000 --- a/lib/openid.php +++ /dev/null @@ -1,280 +0,0 @@ -<?php -/* - * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2008, 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/>. - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - -require_once(INSTALLDIR.'/classes/User_openid.php'); - -require_once('Auth/OpenID.php'); -require_once('Auth/OpenID/Consumer.php'); -require_once('Auth/OpenID/SReg.php'); -require_once('Auth/OpenID/MySQLStore.php'); - -# About one year cookie expiry - -define('OPENID_COOKIE_EXPIRY', round(365.25 * 24 * 60 * 60)); -define('OPENID_COOKIE_KEY', 'lastusedopenid'); - -function oid_store() -{ - static $store = null; - if (!$store) { - # Can't be called statically - $user = new User(); - $conn = $user->getDatabaseConnection(); - $store = new Auth_OpenID_MySQLStore($conn); - } - return $store; -} - -function oid_consumer() -{ - $store = oid_store(); - $consumer = new Auth_OpenID_Consumer($store); - return $consumer; -} - -function oid_clear_last() -{ - oid_set_last(''); -} - -function oid_set_last($openid_url) -{ - common_set_cookie(OPENID_COOKIE_KEY, - $openid_url, - time() + OPENID_COOKIE_EXPIRY); -} - -function oid_get_last() -{ - if (empty($_COOKIE[OPENID_COOKIE_KEY])) { - return null; - } - $openid_url = $_COOKIE[OPENID_COOKIE_KEY]; - if ($openid_url && strlen($openid_url) > 0) { - return $openid_url; - } else { - return null; - } -} - -function oid_link_user($id, $canonical, $display) -{ - - $oid = new User_openid(); - $oid->user_id = $id; - $oid->canonical = $canonical; - $oid->display = $display; - $oid->created = DB_DataObject_Cast::dateTime(); - - if (!$oid->insert()) { - $err = PEAR::getStaticProperty('DB_DataObject','lastError'); - common_debug('DB error ' . $err->code . ': ' . $err->message, __FILE__); - return false; - } - - return true; -} - -function oid_get_user($openid_url) -{ - $user = null; - $oid = User_openid::staticGet('canonical', $openid_url); - if ($oid) { - $user = User::staticGet('id', $oid->user_id); - } - return $user; -} - -function oid_check_immediate($openid_url, $backto=null) -{ - if (!$backto) { - $action = $_REQUEST['action']; - $args = common_copy_args($_GET); - unset($args['action']); - $backto = common_local_url($action, $args); - } - common_debug('going back to "' . $backto . '"', __FILE__); - - common_ensure_session(); - - $_SESSION['openid_immediate_backto'] = $backto; - common_debug('passed-in variable is "' . $backto . '"', __FILE__); - common_debug('session variable is "' . $_SESSION['openid_immediate_backto'] . '"', __FILE__); - - oid_authenticate($openid_url, - 'finishimmediate', - true); -} - -function oid_authenticate($openid_url, $returnto, $immediate=false) -{ - - $consumer = oid_consumer(); - - if (!$consumer) { - common_server_error(_('Cannot instantiate OpenID consumer object.')); - return false; - } - - common_ensure_session(); - - $auth_request = $consumer->begin($openid_url); - - // Handle failure status return values. - if (!$auth_request) { - return _('Not a valid OpenID.'); - } else if (Auth_OpenID::isFailure($auth_request)) { - return sprintf(_('OpenID failure: %s'), $auth_request->message); - } - - $sreg_request = Auth_OpenID_SRegRequest::build(// Required - array(), - // Optional - array('nickname', - 'email', - 'fullname', - 'language', - 'timezone', - 'postcode', - 'country')); - - if ($sreg_request) { - $auth_request->addExtension($sreg_request); - } - - $trust_root = common_root_url(true); - $process_url = common_local_url($returnto); - - if ($auth_request->shouldSendRedirect()) { - $redirect_url = $auth_request->redirectURL($trust_root, - $process_url, - $immediate); - if (!$redirect_url) { - } else if (Auth_OpenID::isFailure($redirect_url)) { - return sprintf(_('Could not redirect to server: %s'), $redirect_url->message); - } else { - common_redirect($redirect_url, 303); - } - } else { - // Generate form markup and render it. - $form_id = 'openid_message'; - $form_html = $auth_request->formMarkup($trust_root, $process_url, - $immediate, array('id' => $form_id)); - - # XXX: This is cheap, but things choke if we don't escape ampersands - # in the HTML attributes - - $form_html = preg_replace('/&/', '&', $form_html); - - // Display an error if the form markup couldn't be generated; - // otherwise, render the HTML. - if (Auth_OpenID::isFailure($form_html)) { - common_server_error(sprintf(_('Could not create OpenID form: %s'), $form_html->message)); - } else { - $action = new AutosubmitAction(); // see below - $action->form_html = $form_html; - $action->form_id = $form_id; - $action->prepare(array('action' => 'autosubmit')); - $action->handle(array('action' => 'autosubmit')); - } - } -} - -# Half-assed attempt at a module-private function - -function _oid_print_instructions() -{ - common_element('div', 'instructions', - _('This form should automatically submit itself. '. - 'If not, click the submit button to go to your '. - 'OpenID provider.')); -} - -# update a user from sreg parameters - -function oid_update_user(&$user, &$sreg) -{ - - $profile = $user->getProfile(); - - $orig_profile = clone($profile); - - if ($sreg['fullname'] && strlen($sreg['fullname']) <= 255) { - $profile->fullname = $sreg['fullname']; - } - - if ($sreg['country']) { - if ($sreg['postcode']) { - # XXX: use postcode to get city and region - # XXX: also, store postcode somewhere -- it's valuable! - $profile->location = $sreg['postcode'] . ', ' . $sreg['country']; - } else { - $profile->location = $sreg['country']; - } - } - - # XXX save language if it's passed - # XXX save timezone if it's passed - - if (!$profile->update($orig_profile)) { - common_server_error(_('Error saving the profile.')); - return false; - } - - $orig_user = clone($user); - - if ($sreg['email'] && Validate::email($sreg['email'], true)) { - $user->email = $sreg['email']; - } - - if (!$user->update($orig_user)) { - common_server_error(_('Error saving the user.')); - return false; - } - - return true; -} - -class AutosubmitAction extends Action -{ - var $form_html = null; - var $form_id = null; - - function handle($args) - { - parent::handle($args); - $this->showPage(); - } - - function title() - { - return _('OpenID Auto-Submit'); - } - - function showContent() - { - $this->raw($this->form_html); - $this->element('script', null, - '$(document).ready(function() { ' . - ' $(\'#'. $this->form_id .'\').submit(); '. - '});'); - } -} diff --git a/lib/ping.php b/lib/ping.php index 175bf8440..5698c4038 100644 --- a/lib/ping.php +++ b/lib/ping.php @@ -44,20 +44,16 @@ function ping_broadcast_notice($notice) { array('nickname' => $profile->nickname)), $tags)); - $context = stream_context_create(array('http' => array('method' => "POST", - 'header' => - "Content-Type: text/xml\r\n". - "User-Agent: StatusNet/".STATUSNET_VERSION."\r\n", - 'content' => $req))); - $file = file_get_contents($notify_url, false, $context); + $request = HTTPClient::start(); + $httpResponse = $request->post($notify_url, array('Content-Type: text/xml'), $req); - if ($file === false || mb_strlen($file) == 0) { + if (!$httpResponse || mb_strlen($httpResponse->getBody()) == 0) { common_log(LOG_WARNING, "XML-RPC empty results for ping ($notify_url, $notice->id) "); continue; } - $response = xmlrpc_decode($file); + $response = xmlrpc_decode($httpResponse->getBody()); if (is_array($response) && xmlrpc_is_fault($response)) { common_log(LOG_WARNING, diff --git a/lib/profileactionform.php b/lib/profileactionform.php new file mode 100644 index 000000000..24d4595c0 --- /dev/null +++ b/lib/profileactionform.php @@ -0,0 +1,187 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Superclass for forms that operate on a profile + * + * 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> + * @copyright 2009 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); +} + +/** + * Superclass for forms that operate on a profile + * + * Certain forms (block, silence, userflag, sandbox, delete) work on + * a single profile and work almost the same. So, this form extracts + * a lot of the common code to simplify those forms. + * + * @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/ + */ + +class ProfileActionForm extends Form +{ + /** + * Profile of user to act on + */ + + var $profile = null; + + /** + * Return-to args + */ + + var $args = null; + + /** + * Constructor + * + * @param HTMLOutputter $out output channel + * @param Profile $profile profile of user to act on + * @param array $args return-to args + */ + + function __construct($out=null, $profile=null, $args=null) + { + parent::__construct($out); + + $this->profile = $profile; + $this->args = $args; + } + + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return $this->target() . '-' . $this->profile->id; + } + + /** + * class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'form_user_'.$this->target(); + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url($this->target()); + } + + /** + * Legend of the Form + * + * @return void + */ + + function formLegend() + { + $this->out->element('legend', null, $this->description()); + } + + /** + * Data elements of the form + * + * @return void + */ + + function formData() + { + $action = $this->target(); + + $this->out->hidden($action.'to-' . $this->profile->id, + $this->profile->id, + 'profileid'); + + if ($this->args) { + foreach ($this->args as $k => $v) { + $this->out->hidden('returnto-' . $k, $v); + } + } + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit('submit', $this->title(), 'submit', + null, $this->description()); + } + + /** + * Action this form targets + * + * @return string Name of the action, lowercased. + */ + + function target() + { + return null; + } + + /** + * Title of the form + * + * @return string Title of the form, internationalized + */ + + function title() + { + return null; + } + + /** + * Description of the form + * + * @return string description of the form, internationalized + */ + + function description() + { + return null; + } +} diff --git a/lib/profileformaction.php b/lib/profileformaction.php new file mode 100644 index 000000000..8cb5f6a93 --- /dev/null +++ b/lib/profileformaction.php @@ -0,0 +1,139 @@ +<?php +/** + * Superclass for actions that operate on a user + * + * PHP version 5 + * + * 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 Action + * @package StatusNet + * @author Evan Prodromou <evan@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 operate on a user + * + * @category Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + */ + +class ProfileFormAction extends Action +{ + var $profile = null; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + + function prepare($args) + { + parent::prepare($args); + + $this->checkSessionToken(); + + if (!common_logged_in()) { + $this->clientError(_('Not logged in.')); + return false; + } + + $id = $this->trimmed('profileid'); + + if (!$id) { + $this->clientError(_('No profile specified.')); + return false; + } + + $this->profile = Profile::staticGet('id', $id); + + if (!$this->profile) { + $this->clientError(_('No profile with that ID.')); + return false; + } + + return true; + } + + /** + * Handle request + * + * Shows a page with list of favorite notices + * + * @param array $args $_REQUEST args; handled in prepare() + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + 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")); + } + } + + /** + * handle a POST request + * + * sub-classes should overload this request + * + * @return void + */ + + function handlePost() + { + $this->serverError(_("unimplemented method")); + } +} diff --git a/lib/profilelist.php b/lib/profilelist.php index 331430b3e..3412d41d1 100644 --- a/lib/profilelist.php +++ b/lib/profilelist.php @@ -62,15 +62,21 @@ class ProfileList extends Widget function show() { - $this->startList(); - $cnt = $this->showProfiles(); - $this->endList(); + $cnt = 0; + + if (Event::handle('StartProfileList', array($this))) { + $this->startList(); + $cnt = $this->showProfiles(); + $this->endList(); + Event::handle('EndProfileList', array($this)); + } + return $cnt; } function startList() { - $this->out->elementStart('ul', 'profiles'); + $this->out->elementStart('ul', 'profiles xoxo'); } function endList() @@ -117,39 +123,67 @@ class ProfileListItem extends Widget function show() { - $this->startItem(); - $this->showProfile(); - $this->showActions(); - $this->endItem(); + if (Event::handle('StartProfileListItem', array($this))) { + $this->startItem(); + if (Event::handle('StartProfileListItemProfile', array($this))) { + $this->showProfile(); + Event::handle('EndProfileListItemProfile', array($this)); + } + if (Event::handle('StartProfileListItemActions', array($this))) { + $this->showActions(); + Event::handle('EndProfileListItemActions', array($this)); + } + $this->endItem(); + Event::handle('EndProfileListItem', array($this)); + } } function startItem() { - $this->out->elementStart('li', array('class' => 'profile', + $this->out->elementStart('li', array('class' => 'profile hentry', 'id' => 'profile-' . $this->profile->id)); } function showProfile() { $this->startProfile(); - $this->showAvatar(); - $this->showFullName(); - $this->showLocation(); - $this->showHomepage(); - $this->showBio(); + if (Event::handle('StartProfileListItemProfileElements', array($this))) { + if (Event::handle('StartProfileListItemAvatar', array($this))) { + $this->showAvatar(); + Event::handle('EndProfileListItemAvatar', array($this)); + } + if (Event::handle('StartProfileListItemFullName', array($this))) { + $this->showFullName(); + Event::handle('EndProfileListItemFullName', array($this)); + } + if (Event::handle('StartProfileListItemLocation', array($this))) { + $this->showLocation(); + Event::handle('EndProfileListItemLocation', array($this)); + } + if (Event::handle('StartProfileListItemHomepage', array($this))) { + $this->showHomepage(); + Event::handle('EndProfileListItemHomepage', array($this)); + } + if (Event::handle('StartProfileListItemBio', array($this))) { + $this->showBio(); + Event::handle('EndProfileListItemBio', array($this)); + } + Event::handle('EndProfileListItemProfileElements', array($this)); + } $this->endProfile(); } function startProfile() { - $this->out->elementStart('div', 'entity_profile vcard'); + $this->out->elementStart('div', 'entity_profile vcard entry-content'); } function showAvatar() { $avatar = $this->profile->getAvatar(AVATAR_STREAM_SIZE); $this->out->elementStart('a', array('href' => $this->profile->profileurl, - 'class' => 'url')); + 'class' => 'url entry-title', + 'rel' => 'contact')); $this->out->element('img', array('src' => ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_STREAM_SIZE), 'class' => 'photo avatar', 'width' => AVATAR_STREAM_SIZE, @@ -157,7 +191,7 @@ class ProfileListItem extends Widget 'alt' => ($this->profile->fullname) ? $this->profile->fullname : $this->profile->nickname)); - $hasFN = ($this->profile->fullname !== '') ? 'nickname' : 'fn nickname'; + $hasFN = (!empty($this->profile->fullname)) ? 'nickname' : 'fn nickname'; $this->out->elementStart('span', $hasFN); $this->out->raw($this->highlight($this->profile->nickname)); $this->out->elementEnd('span'); @@ -167,53 +201,37 @@ class ProfileListItem extends Widget function showFullName() { if (!empty($this->profile->fullname)) { - $this->out->elementStart('dl', 'entity_fn'); - $this->out->element('dt', null, 'Full name'); - $this->out->elementStart('dd'); $this->out->elementStart('span', 'fn'); $this->out->raw($this->highlight($this->profile->fullname)); $this->out->elementEnd('span'); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); } } function showLocation() { if (!empty($this->profile->location)) { - $this->out->elementStart('dl', 'entity_location'); - $this->out->element('dt', null, _('Location')); - $this->out->elementStart('dd', 'label'); + $this->out->elementStart('span', 'location'); $this->out->raw($this->highlight($this->profile->location)); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); + $this->out->elementEnd('span'); } } function showHomepage() { if (!empty($this->profile->homepage)) { - $this->out->elementStart('dl', 'entity_url'); - $this->out->element('dt', null, _('URL')); - $this->out->elementStart('dd'); $this->out->elementStart('a', array('href' => $this->profile->homepage, 'class' => 'url')); $this->out->raw($this->highlight($this->profile->homepage)); $this->out->elementEnd('a'); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); } } function showBio() { if (!empty($this->profile->bio)) { - $this->out->elementStart('dl', 'entity_note'); - $this->out->element('dt', null, _('Note')); - $this->out->elementStart('dd', 'note'); + $this->out->elementStart('p', 'note'); $this->out->raw($this->highlight($this->profile->bio)); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); + $this->out->elementEnd('p'); } } @@ -225,7 +243,10 @@ class ProfileListItem extends Widget function showActions() { $this->startActions(); - $this->showSubscribeButton(); + if (Event::handle('StartProfileListItemActionElements', array($this))) { + $this->showSubscribeButton(); + Event::handle('EndProfileListItemActionElements', array($this)); + } $this->endActions(); } diff --git a/lib/queuehandler.php b/lib/queuehandler.php index 8c65a97c6..cd43b1e09 100644 --- a/lib/queuehandler.php +++ b/lib/queuehandler.php @@ -27,6 +27,22 @@ define('CLAIM_TIMEOUT', 1200); define('QUEUE_HANDLER_MISS_IDLE', 10); define('QUEUE_HANDLER_HIT_IDLE', 0); +/** + * Base class for queue handlers. + * + * As extensions of the Daemon class, each queue handler has the ability + * to launch itself in the background, at which point it'll pass control + * to the configured QueueManager class to poll for updates. + * + * Subclasses must override at least the following methods: + * - transport + * - start + * - finish + * - handle_notice + * + * Some subclasses will also want to override the idle handler: + * - idle + */ class QueueHandler extends Daemon { @@ -39,6 +55,14 @@ class QueueHandler extends Daemon } } + /** + * How many seconds a polling-based queue manager should wait between + * checks for new items to handle. + * + * Defaults to 60 seconds; override to speed up or slow down. + * + * @return int timeout in seconds + */ function timeout() { return 60; @@ -54,24 +78,69 @@ class QueueHandler extends Daemon return strtolower($this->class_name().'.'.$this->get_id()); } + /** + * Return transport keyword which identifies items this queue handler + * services; must be defined for all subclasses. + * + * Must be 8 characters or less to fit in the queue_item database. + * ex "email", "jabber", "sms", "irc", ... + * + * @return string + */ function transport() { return null; } + /** + * Initialization, run when the queue handler starts. + * If this function indicates failure, the handler run will be aborted. + * + * @fixme run() will abort if this doesn't return true, + * but some subclasses don't bother. + * @return boolean true on success, false on failure + */ function start() { } + /** + * Cleanup, run when the queue handler ends. + * If this function indicates failure, a warning will be logged. + * + * @fixme run() will throw warnings if this doesn't return true, + * but many subclasses don't bother. + * @return boolean true on success, false on failure + */ function finish() { } + /** + * Here's the meat of your queue handler -- you're handed a Notice + * object, which you may do as you will with. + * + * If this function indicates failure, a warning will be logged + * and the item is placed back in the queue to be re-run. + * + * @param Notice $notice + * @return boolean true on success, false on failure + */ function handle_notice($notice) { return true; } + /** + * Setup and start of run loop for this queue handler as a daemon. + * Most of the heavy lifting is passed on to the QueueManager's service() + * method, which passes control back to our handle_notice() method for + * each notice that comes in on the queue. + * + * Most of the time this won't need to be overridden in a subclass. + * + * @return boolean true on success, false on failure + */ function run() { if (!$this->start()) { @@ -100,6 +169,14 @@ class QueueHandler extends Daemon return true; } + /** + * Called by QueueHandler after each handled item or empty polling cycle. + * This is a good time to e.g. service your XMPP connection. + * + * Doesn't need to be overridden if there's no maintenance to do. + * + * @param int $timeout seconds to sleep if there's nothing to do + */ function idle($timeout=0) { if ($timeout > 0) { diff --git a/lib/right.php b/lib/right.php new file mode 100644 index 000000000..5e66eae0e --- /dev/null +++ b/lib/right.php @@ -0,0 +1,61 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Class for user rights + * + * 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 Authorization + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 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); +} + +/** + * class for rights + * + * Mostly for holding the rights constants + * + * @category Authorization + * @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/ + */ + +class Right +{ + const DELETEOTHERSNOTICE = 'deleteothersnotice'; + const CONFIGURESITE = 'configuresite'; + const DELETEUSER = 'deleteuser'; + const SILENCEUSER = 'silenceuser'; + const SANDBOXUSER = 'sandboxuser'; + const NEWNOTICE = 'newnotice'; + const PUBLICNOTICE = 'publicnotice'; + const NEWMESSAGE = 'newmessage'; + const SUBSCRIBE = 'subscribe'; + const EMAILONREPLY = 'emailonreply'; + const EMAILONSUBSCRIBE = 'emailonsubscribe'; + const EMAILONFAVE = 'emailonfave'; +} + diff --git a/lib/router.php b/lib/router.php index 5529e60ac..b22185126 100644 --- a/lib/router.php +++ b/lib/router.php @@ -50,8 +50,7 @@ class Router var $m = null; static $inst = null; static $bare = array('requesttoken', 'accesstoken', 'userauthorization', - 'postnotice', 'updateprofile', 'finishremotesubscribe', - 'finishopenidlogin', 'finishaddopenid'); + 'postnotice', 'updateprofile', 'finishremotesubscribe'); static function get() { @@ -72,447 +71,586 @@ class Router { $m = Net_URL_Mapper::getInstance(); - // In the "root" + if (Event::handle('StartInitializeRouter', array(&$m))) { - $m->connect('', array('action' => 'public')); - $m->connect('rss', array('action' => 'publicrss')); - $m->connect('xrds', array('action' => 'publicxrds')); - $m->connect('featuredrss', array('action' => 'featuredrss')); - $m->connect('favoritedrss', array('action' => 'favoritedrss')); - $m->connect('opensearch/people', array('action' => 'opensearch', - 'type' => 'people')); - $m->connect('opensearch/notice', array('action' => 'opensearch', - 'type' => 'notice')); + // In the "root" - // docs + $m->connect('', array('action' => 'public')); + $m->connect('rss', array('action' => 'publicrss')); + $m->connect('featuredrss', array('action' => 'featuredrss')); + $m->connect('favoritedrss', array('action' => 'favoritedrss')); + $m->connect('opensearch/people', array('action' => 'opensearch', + 'type' => 'people')); + $m->connect('opensearch/notice', array('action' => 'opensearch', + 'type' => 'notice')); - $m->connect('doc/:title', array('action' => 'doc')); + // docs - // Twitter + $m->connect('doc/:title', array('action' => 'doc')); - $m->connect('twitter/authorization', array('action' => 'twitterauthorization')); + $m->connect('main/login?user_id=:user_id&token=:token', array('action'=>'login'), array('user_id'=> '[0-9]+', 'token'=>'.+')); - // facebook + // main stuff is repetitive - $m->connect('facebook', array('action' => 'facebookhome')); - $m->connect('facebook/index.php', array('action' => 'facebookhome')); - $m->connect('facebook/settings.php', array('action' => 'facebooksettings')); - $m->connect('facebook/invite.php', array('action' => 'facebookinvite')); - $m->connect('facebook/remove', array('action' => 'facebookremove')); + $main = array('login', 'logout', 'register', 'subscribe', + 'unsubscribe', 'confirmaddress', 'recoverpassword', + 'invite', 'favor', 'disfavor', 'sup', + 'block', 'unblock', 'subedit', + 'groupblock', 'groupunblock', + 'sandbox', 'unsandbox', + 'silence', 'unsilence', + 'deleteuser'); - // main stuff is repetitive + foreach ($main as $a) { + $m->connect('main/'.$a, array('action' => $a)); + } - $main = array('login', 'logout', 'register', 'subscribe', - 'unsubscribe', 'confirmaddress', 'recoverpassword', - 'invite', 'favor', 'disfavor', 'sup', - 'block', 'unblock', 'subedit', - 'groupblock', 'groupunblock'); + $m->connect('main/sup/:seconds', array('action' => 'sup'), + array('seconds' => '[0-9]+')); - foreach ($main as $a) { - $m->connect('main/'.$a, array('action' => $a)); - } + $m->connect('main/tagother/:id', array('action' => 'tagother')); - $m->connect('main/sup/:seconds', array('action' => 'sup'), - array('seconds' => '[0-9]+')); + $m->connect('main/oembed', + array('action' => 'oembed')); - $m->connect('main/tagother/:id', array('action' => 'tagother')); + $m->connect('main/xrds', + array('action' => 'publicxrds')); - $m->connect('main/oembed', - array('action' => 'oembed')); + // these take a code - // these take a code + foreach (array('register', 'confirmaddress', 'recoverpassword') as $c) { + $m->connect('main/'.$c.'/:code', array('action' => $c)); + } - foreach (array('register', 'confirmaddress', 'recoverpassword') as $c) { - $m->connect('main/'.$c.'/:code', array('action' => $c)); - } + // exceptional - // exceptional + $m->connect('main/remote', array('action' => 'remotesubscribe')); + $m->connect('main/remote?nickname=:nickname', array('action' => 'remotesubscribe'), array('nickname' => '[A-Za-z0-9_-]+')); - $m->connect('main/openid', array('action' => 'openidlogin')); - $m->connect('main/remote', array('action' => 'remotesubscribe')); - $m->connect('main/remote?nickname=:nickname', array('action' => 'remotesubscribe'), array('nickname' => '[A-Za-z0-9_-]+')); + foreach (Router::$bare as $action) { + $m->connect('index.php?action=' . $action, array('action' => $action)); + } - foreach (Router::$bare as $action) { - $m->connect('index.php?action=' . $action, array('action' => $action)); - } + // settings - // settings + foreach (array('profile', 'avatar', 'password', 'im', + 'email', 'sms', 'userdesign', 'other') as $s) { + $m->connect('settings/'.$s, array('action' => $s.'settings')); + } - foreach (array('profile', 'avatar', 'password', 'openid', 'im', - 'email', 'sms', 'twitter', 'userdesign', 'other') as $s) { - $m->connect('settings/'.$s, array('action' => $s.'settings')); - } + // search - // search + foreach (array('group', 'people', 'notice') as $s) { + $m->connect('search/'.$s, array('action' => $s.'search')); + $m->connect('search/'.$s.'?q=:q', + array('action' => $s.'search'), + array('q' => '.+')); + } - foreach (array('group', 'people', 'notice') as $s) { - $m->connect('search/'.$s, array('action' => $s.'search')); - $m->connect('search/'.$s.'?q=:q', - array('action' => $s.'search'), + // The second of these is needed to make the link work correctly + // when inserted into the page. The first is needed to match the + // route on the way in. Seems to be another Net_URL_Mapper bug to me. + $m->connect('search/notice/rss', array('action' => 'noticesearchrss')); + $m->connect('search/notice/rss?q=:q', array('action' => 'noticesearchrss'), array('q' => '.+')); - } - // The second of these is needed to make the link work correctly - // when inserted into the page. The first is needed to match the - // route on the way in. Seems to be another Net_URL_Mapper bug to me. - $m->connect('search/notice/rss', array('action' => 'noticesearchrss')); - $m->connect('search/notice/rss?q=:q', array('action' => 'noticesearchrss'), - array('q' => '.+')); - - $m->connect('attachment/:attachment', - array('action' => 'attachment'), - array('attachment' => '[0-9]+')); - - $m->connect('attachment/:attachment/ajax', - array('action' => 'attachment_ajax'), - array('attachment' => '[0-9]+')); - - $m->connect('attachment/:attachment/thumbnail', - array('action' => 'attachment_thumbnail'), - array('attachment' => '[0-9]+')); - - $m->connect('notice/new', array('action' => 'newnotice')); - $m->connect('notice/new?replyto=:replyto', - array('action' => 'newnotice'), - array('replyto' => '[A-Za-z0-9_-]+')); - $m->connect('notice/new?replyto=:replyto&inreplyto=:inreplyto', - array('action' => 'newnotice'), - array('replyto' => '[A-Za-z0-9_-]+'), - array('inreplyto' => '[0-9]+')); - - $m->connect('notice/:notice/file', - array('action' => 'file'), - array('notice' => '[0-9]+')); - - $m->connect('notice/:notice', - array('action' => 'shownotice'), - array('notice' => '[0-9]+')); - $m->connect('notice/delete', array('action' => 'deletenotice')); - $m->connect('notice/delete/:notice', - array('action' => 'deletenotice'), - array('notice' => '[0-9]+')); - - // conversation - - $m->connect('conversation/:id', - array('action' => 'conversation'), - array('id' => '[0-9]+')); - - $m->connect('message/new', array('action' => 'newmessage')); - $m->connect('message/new?to=:to', array('action' => 'newmessage'), array('to' => '[A-Za-z0-9_-]+')); - $m->connect('message/:message', - array('action' => 'showmessage'), - array('message' => '[0-9]+')); - - $m->connect('user/:id', - array('action' => 'userbyid'), - array('id' => '[0-9]+')); - - $m->connect('tags/', array('action' => 'publictagcloud')); - $m->connect('tag/', array('action' => 'publictagcloud')); - $m->connect('tags', array('action' => 'publictagcloud')); - $m->connect('tag', array('action' => 'publictagcloud')); - $m->connect('tag/:tag/rss', - array('action' => 'tagrss'), - array('tag' => '[a-zA-Z0-9]+')); - $m->connect('tag/:tag', - array('action' => 'tag'), - array('tag' => '[\pL\pN_\-\.]{1,64}')); - - $m->connect('peopletag/:tag', - array('action' => 'peopletag'), - array('tag' => '[a-zA-Z0-9]+')); - - $m->connect('featured/', array('action' => 'featured')); - $m->connect('featured', array('action' => 'featured')); - $m->connect('favorited/', array('action' => 'favorited')); - $m->connect('favorited', array('action' => 'favorited')); - - // groups - - $m->connect('group/new', array('action' => 'newgroup')); - - foreach (array('edit', 'join', 'leave') as $v) { - $m->connect('group/:nickname/'.$v, - array('action' => $v.'group'), + $m->connect('attachment/:attachment', + array('action' => 'attachment'), + array('attachment' => '[0-9]+')); + + $m->connect('attachment/:attachment/ajax', + array('action' => 'attachment_ajax'), + array('attachment' => '[0-9]+')); + + $m->connect('attachment/:attachment/thumbnail', + array('action' => 'attachment_thumbnail'), + array('attachment' => '[0-9]+')); + + $m->connect('notice/new', array('action' => 'newnotice')); + $m->connect('notice/new?replyto=:replyto', + array('action' => 'newnotice'), + array('replyto' => '[A-Za-z0-9_-]+')); + $m->connect('notice/new?replyto=:replyto&inreplyto=:inreplyto', + array('action' => 'newnotice'), + array('replyto' => '[A-Za-z0-9_-]+'), + array('inreplyto' => '[0-9]+')); + + $m->connect('notice/:notice/file', + array('action' => 'file'), + array('notice' => '[0-9]+')); + + $m->connect('notice/:notice', + array('action' => 'shownotice'), + array('notice' => '[0-9]+')); + $m->connect('notice/delete', array('action' => 'deletenotice')); + $m->connect('notice/delete/:notice', + array('action' => 'deletenotice'), + array('notice' => '[0-9]+')); + + $m->connect('bookmarklet/new', array('action' => 'bookmarklet')); + + // conversation + + $m->connect('conversation/:id', + array('action' => 'conversation'), + array('id' => '[0-9]+')); + + $m->connect('message/new', array('action' => 'newmessage')); + $m->connect('message/new?to=:to', array('action' => 'newmessage'), array('to' => '[A-Za-z0-9_-]+')); + $m->connect('message/:message', + array('action' => 'showmessage'), + array('message' => '[0-9]+')); + + $m->connect('user/:id', + array('action' => 'userbyid'), + array('id' => '[0-9]+')); + + $m->connect('tags/', array('action' => 'publictagcloud')); + $m->connect('tag/', array('action' => 'publictagcloud')); + $m->connect('tags', array('action' => 'publictagcloud')); + $m->connect('tag', array('action' => 'publictagcloud')); + $m->connect('tag/:tag/rss', + array('action' => 'tagrss'), + array('tag' => '[a-zA-Z0-9]+')); + $m->connect('tag/:tag', + array('action' => 'tag'), + array('tag' => '[\pL\pN_\-\.]{1,64}')); + + $m->connect('peopletag/:tag', + array('action' => 'peopletag'), + array('tag' => '[a-zA-Z0-9]+')); + + $m->connect('featured/', array('action' => 'featured')); + $m->connect('featured', array('action' => 'featured')); + $m->connect('favorited/', array('action' => 'favorited')); + $m->connect('favorited', array('action' => 'favorited')); + + // groups + + $m->connect('group/new', array('action' => 'newgroup')); + + foreach (array('edit', 'join', 'leave') as $v) { + $m->connect('group/:nickname/'.$v, + array('action' => $v.'group'), + array('nickname' => '[a-zA-Z0-9]+')); + } + + foreach (array('members', 'logo', 'rss', 'designsettings') as $n) { + $m->connect('group/:nickname/'.$n, + array('action' => 'group'.$n), + array('nickname' => '[a-zA-Z0-9]+')); + } + + $m->connect('group/:nickname/foaf', + array('action' => 'foafgroup'), array('nickname' => '[a-zA-Z0-9]+')); - } - foreach (array('members', 'logo', 'rss', 'designsettings') as $n) { - $m->connect('group/:nickname/'.$n, - array('action' => 'group'.$n), + $m->connect('group/:nickname/blocked', + array('action' => 'blockedfromgroup'), array('nickname' => '[a-zA-Z0-9]+')); - } - $m->connect('group/:nickname/blocked', - array('action' => 'blockedfromgroup'), - array('nickname' => '[a-zA-Z0-9]+')); + $m->connect('group/:nickname/makeadmin', + array('action' => 'makeadmin'), + array('nickname' => '[a-zA-Z0-9]+')); - $m->connect('group/:nickname/makeadmin', - array('action' => 'makeadmin'), - array('nickname' => '[a-zA-Z0-9]+')); + $m->connect('group/:id/id', + array('action' => 'groupbyid'), + array('id' => '[0-9]+')); - $m->connect('group/:id/id', - array('action' => 'groupbyid'), - array('id' => '[0-9]+')); + $m->connect('group/:nickname', + array('action' => 'showgroup'), + array('nickname' => '[a-zA-Z0-9]+')); - $m->connect('group/:nickname', - array('action' => 'showgroup'), - array('nickname' => '[a-zA-Z0-9]+')); + $m->connect('group/', array('action' => 'groups')); + $m->connect('group', array('action' => 'groups')); + $m->connect('groups/', array('action' => 'groups')); + $m->connect('groups', array('action' => 'groups')); + + // Twitter-compatible API + + // statuses API + + $m->connect('api/statuses/public_timeline.:format', + array('action' => 'ApiTimelinePublic', + 'format' => '(xml|json|rss|atom)')); + + $m->connect('api/statuses/friends_timeline.:format', + array('action' => 'ApiTimelineFriends', + 'format' => '(xml|json|rss|atom)')); + + $m->connect('api/statuses/friends_timeline/:id.:format', + array('action' => 'ApiTimelineFriends', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json|rss|atom)')); + $m->connect('api/statuses/home_timeline.:format', + array('action' => 'ApiTimelineFriends', + 'format' => '(xml|json|rss|atom)')); - $m->connect('group/', array('action' => 'groups')); - $m->connect('group', array('action' => 'groups')); - $m->connect('groups/', array('action' => 'groups')); - $m->connect('groups', array('action' => 'groups')); + $m->connect('api/statuses/home_timeline/:id.:format', + array('action' => 'ApiTimelineFriends', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json|rss|atom)')); - // Twitter-compatible API + $m->connect('api/statuses/user_timeline.:format', + array('action' => 'ApiTimelineUser', + 'format' => '(xml|json|rss|atom)')); - // statuses API + $m->connect('api/statuses/user_timeline/:id.:format', + array('action' => 'ApiTimelineUser', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json|rss|atom)')); + + $m->connect('api/statuses/mentions.:format', + array('action' => 'ApiTimelineMentions', + 'format' => '(xml|json|rss|atom)')); + + $m->connect('api/statuses/mentions/:id.:format', + array('action' => 'ApiTimelineMentions', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json|rss|atom)')); + + $m->connect('api/statuses/replies.:format', + array('action' => 'ApiTimelineMentions', + 'format' => '(xml|json|rss|atom)')); + + $m->connect('api/statuses/replies/:id.:format', + array('action' => 'ApiTimelineMentions', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json|rss|atom)')); - $m->connect('api/statuses/:method', - array('action' => 'api', - 'apiaction' => 'statuses'), - array('method' => '(public_timeline|home_timeline|friends_timeline|user_timeline|update|replies|mentions|show|friends|followers|featured)(\.(atom|rss|xml|json))?')); + $m->connect('api/statuses/friends.:format', + array('action' => 'ApiUserFriends', + 'format' => '(xml|json)')); - $m->connect('api/statuses/:method/:argument', - array('action' => 'api', - 'apiaction' => 'statuses'), - array('method' => '(user_timeline|home_timeline|friends_timeline|replies|mentions|show|destroy|friends|followers)')); + $m->connect('api/statuses/friends/:id.:format', + array('action' => 'ApiUserFriends', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); - // users + $m->connect('api/statuses/followers.:format', + array('action' => 'ApiUserFollowers', + 'format' => '(xml|json)')); - $m->connect('api/users/:method/:argument', - array('action' => 'api', - 'apiaction' => 'users'), - array('method' => 'show(\.(xml|json))?')); + $m->connect('api/statuses/followers/:id.:format', + array('action' => 'ApiUserFollowers', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); - $m->connect('api/users/:method', - array('action' => 'api', - 'apiaction' => 'users'), - array('method' => 'show(\.(xml|json))?')); + $m->connect('api/statuses/show.:format', + array('action' => 'ApiStatusesShow', + 'format' => '(xml|json)')); - // direct messages + $m->connect('api/statuses/show/:id.:format', + array('action' => 'ApiStatusesShow', + 'id' => '[0-9]+', + 'format' => '(xml|json)')); - foreach (array('xml', 'json') as $e) { - $m->connect('api/direct_messages/new.'.$e, - array('action' => 'api', - 'apiaction' => 'direct_messages', - 'method' => 'create.'.$e)); - } + $m->connect('api/statuses/update.:format', + array('action' => 'ApiStatusesUpdate', + 'format' => '(xml|json)')); - foreach (array('xml', 'json', 'rss', 'atom') as $e) { - $m->connect('api/direct_messages.'.$e, - array('action' => 'api', - 'apiaction' => 'direct_messages', - 'method' => 'direct_messages.'.$e)); - } + $m->connect('api/statuses/destroy.:format', + array('action' => 'ApiStatusesDestroy', + 'format' => '(xml|json)')); - foreach (array('xml', 'json', 'rss', 'atom') as $e) { - $m->connect('api/direct_messages/sent.'.$e, - array('action' => 'api', - 'apiaction' => 'direct_messages', - 'method' => 'sent.'.$e)); - } + $m->connect('api/statuses/destroy/:id.:format', + array('action' => 'ApiStatusesDestroy', + 'id' => '[0-9]+', + 'format' => '(xml|json)')); - $m->connect('api/direct_messages/destroy/:argument', - array('action' => 'api', - 'apiaction' => 'direct_messages')); + // users - // friendships + $m->connect('api/users/show/:id.:format', + array('action' => 'ApiUserShow', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); - $m->connect('api/friendships/:method/:argument', - array('action' => 'api', - 'apiaction' => 'friendships'), - array('method' => '(create|destroy)')); + // direct messages - $m->connect('api/friendships/:method', - array('action' => 'api', - 'apiaction' => 'friendships'), - array('method' => '(show|exists)(\.(xml|json))')); + $m->connect('api/direct_messages.:format', + array('action' => 'ApiDirectMessage', + 'format' => '(xml|json|rss|atom)')); - // Social graph + $m->connect('api/direct_messages/sent.:format', + array('action' => 'ApiDirectMessage', + 'format' => '(xml|json|rss|atom)', + 'sent' => true)); - $m->connect('api/friends/ids/:argument', - array('action' => 'api', - 'apiaction' => 'statuses', - 'method' => 'friendsIDs')); + $m->connect('api/direct_messages/new.:format', + array('action' => 'ApiDirectMessageNew', + 'format' => '(xml|json)')); - foreach (array('xml', 'json') as $e) { - $m->connect('api/friends/ids.'.$e, - array('action' => 'api', - 'apiaction' => 'statuses', - 'method' => 'friendsIDs.'.$e)); - } + // friendships - $m->connect('api/followers/ids/:argument', - array('action' => 'api', - 'apiaction' => 'statuses', - 'method' => 'followersIDs')); + $m->connect('api/friendships/show.:format', + array('action' => 'ApiFriendshipsShow', + 'format' => '(xml|json)')); - foreach (array('xml', 'json') as $e) { - $m->connect('api/followers/ids.'.$e, - array('action' => 'api', - 'apiaction' => 'statuses', - 'method' => 'followersIDs.'.$e)); - } + $m->connect('api/friendships/exists.:format', + array('action' => 'ApiFriendshipsExists', + 'format' => '(xml|json)')); - // account + $m->connect('api/friendships/create.:format', + array('action' => 'ApiFriendshipsCreate', + 'format' => '(xml|json)')); - $m->connect('api/account/:method', - array('action' => 'api', - 'apiaction' => 'account')); + $m->connect('api/friendships/destroy.:format', + array('action' => 'ApiFriendshipsDestroy', + 'format' => '(xml|json)')); - // favorites + $m->connect('api/friendships/create/:id.:format', + array('action' => 'ApiFriendshipsCreate', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); - $m->connect('api/favorites/:method/:argument', - array('action' => 'api', - 'apiaction' => 'favorites', - array('method' => '(create|destroy)'))); + $m->connect('api/friendships/destroy/:id.:format', + array('action' => 'ApiFriendshipsDestroy', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); - $m->connect('api/favorites/:argument', - array('action' => 'api', - 'apiaction' => 'favorites', - 'method' => 'favorites')); + // Social graph - foreach (array('xml', 'json', 'rss', 'atom') as $e) { - $m->connect('api/favorites.'.$e, - array('action' => 'api', - 'apiaction' => 'favorites', - 'method' => 'favorites.'.$e)); - } + $m->connect('api/friends/ids/:id.:format', + array('action' => 'apiFriends', + 'ids_only' => true)); - // notifications + $m->connect('api/followers/ids/:id.:format', + array('action' => 'apiFollowers', + 'ids_only' => true)); - $m->connect('api/notifications/:method/:argument', - array('action' => 'api', - 'apiaction' => 'favorites')); + $m->connect('api/friends/ids.:format', + array('action' => 'apiFriends', + 'ids_only' => true)); - // blocks + $m->connect('api/followers/ids.:format', + array('action' => 'apiFollowers', + 'ids_only' => true)); - $m->connect('api/blocks/:method/:argument', - array('action' => 'api', - 'apiaction' => 'blocks')); + // account - // help + $m->connect('api/account/verify_credentials.:format', + array('action' => 'ApiAccountVerifyCredentials')); - $m->connect('api/help/:method', - array('action' => 'api', - 'apiaction' => 'help')); + $m->connect('api/account/update_profile.:format', + array('action' => 'ApiAccountUpdateProfile')); - // statusnet + $m->connect('api/account/update_profile_image.:format', + array('action' => 'ApiAccountUpdateProfileImage')); - $m->connect('api/statusnet/:method', - array('action' => 'api', - 'apiaction' => 'statusnet')); + $m->connect('api/account/update_profile_background_image.:format', + array('action' => 'ApiAccountUpdateProfileBackgroundImage')); - // For older methods, we provide "laconica" base action + $m->connect('api/account/update_profile_colors.:format', + array('action' => 'ApiAccountUpdateProfileColors')); - $m->connect('api/laconica/:method', - array('action' => 'api', - 'apiaction' => 'statusnet')); + $m->connect('api/account/update_delivery_device.:format', + array('action' => 'ApiAccountUpdateDeliveryDevice')); - // Groups and tags are newer than 0.8.1 so no backward-compatibility - // necessary + // special case where verify_credentials is called w/out a format - // Groups - //'list' has to be handled differently, as php will not allow a method to be named 'list' - $m->connect('api/statusnet/groups/list/:argument', - array('action' => 'api', - 'method' => 'list_groups', - 'apiaction' => 'groups')); + $m->connect('api/account/verify_credentials', + array('action' => 'ApiAccountVerifyCredentials')); - foreach (array('xml', 'json', 'rss', 'atom') as $e) { - $m->connect('api/statusnet/groups/list.' . $e, - array('action' => 'api', - 'method' => 'list_groups.' . $e, - 'apiaction' => 'groups')); - } + $m->connect('api/account/rate_limit_status.:format', + array('action' => 'ApiAccountRateLimitStatus')); - $m->connect('api/statusnet/groups/:method', - array('action' => 'api', - 'apiaction' => 'statuses'), - array('method' => '(list_all|)(\.(atom|rss|xml|json))?')); - - $m->connect('api/statuses/:method/:argument', - array('action' => 'api', - 'apiaction' => 'statuses'), - array('method' => '(user_timeline|home_timeline|friends_timeline|replies|mentions|show|destroy|friends|followers)')); - - $m->connect('api/statusnet/groups/:method/:argument', - array('action' => 'api', - 'apiaction' => 'groups')); - - $m->connect('api/statusnet/groups/:method', - array('action' => 'api', - 'apiaction' => 'groups')); - - // Tags - $m->connect('api/statusnet/tags/:method/:argument', - array('action' => 'api', - 'apiaction' => 'tags')); - - $m->connect('api/statusnet/tags/:method', - array('action' => 'api', - 'apiaction' => 'tags')); - - // 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')); - - // user stuff - - foreach (array('subscriptions', 'subscribers', - 'nudge', 'xrds', 'all', 'foaf', - 'replies', 'inbox', 'outbox', 'microsummary') as $a) { - $m->connect(':nickname/'.$a, - array('action' => $a), - array('nickname' => '[a-zA-Z0-9]{1,64}')); - } + // favorites - foreach (array('subscriptions', 'subscribers') as $a) { - $m->connect(':nickname/'.$a.'/:tag', - array('action' => $a), - array('tag' => '[a-zA-Z0-9]+', - 'nickname' => '[a-zA-Z0-9]{1,64}')); - } + $m->connect('api/favorites.:format', + array('action' => 'ApiTimelineFavorites', + 'format' => '(xml|json|rss|atom)')); - foreach (array('rss', 'groups') as $a) { - $m->connect(':nickname/'.$a, - array('action' => 'user'.$a), - array('nickname' => '[a-zA-Z0-9]{1,64}')); - } + $m->connect('api/favorites/:id.:format', + array('action' => 'ApiTimelineFavorites', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xmljson|rss|atom)')); - foreach (array('all', 'replies', 'favorites') as $a) { - $m->connect(':nickname/'.$a.'/rss', - array('action' => $a.'rss'), - array('nickname' => '[a-zA-Z0-9]{1,64}')); - } + $m->connect('api/favorites/create/:id.:format', + array('action' => 'ApiFavoriteCreate', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/favorites/destroy/:id.:format', + array('action' => 'ApiFavoriteDestroy', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + // blocks + + $m->connect('api/blocks/create/:id.:format', + array('action' => 'ApiBlockCreate', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/blocks/destroy/:id.:format', + array('action' => 'ApiBlockDestroy', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + // help + + $m->connect('api/help/test.:format', + array('action' => 'ApiHelpTest', + 'format' => '(xml|json)')); + + // statusnet - $m->connect(':nickname/favorites', - array('action' => 'showfavorites'), - array('nickname' => '[a-zA-Z0-9]{1,64}')); + $m->connect('api/statusnet/version.:format', + array('action' => 'ApiStatusnetVersion', + 'format' => '(xml|json)')); - $m->connect(':nickname/avatar/:size', - array('action' => 'avatarbynickname'), - array('size' => '(original|96|48|24)', - 'nickname' => '[a-zA-Z0-9]{1,64}')); + $m->connect('api/statusnet/config.:format', + array('action' => 'ApiStatusnetConfig', + 'format' => '(xml|json)')); - $m->connect(':nickname/tag/:tag/rss', - array('action' => 'userrss'), - array('nickname' => '[a-zA-Z0-9]{1,64}'), - array('tag' => '[a-zA-Z0-9]+')); + // For older methods, we provide "laconica" base action - $m->connect(':nickname/tag/:tag', - array('action' => 'showstream'), - array('nickname' => '[a-zA-Z0-9]{1,64}'), - array('tag' => '[a-zA-Z0-9]+')); + $m->connect('api/laconica/version.:format', + array('action' => 'ApiStatusnetVersion', + 'format' => '(xml|json)')); - $m->connect(':nickname', - array('action' => 'showstream'), - array('nickname' => '[a-zA-Z0-9]{1,64}')); + $m->connect('api/laconica/config.:format', + array('action' => 'ApiStatusnetConfig', + 'format' => '(xml|json)')); + + // Groups and tags are newer than 0.8.1 so no backward-compatibility + // necessary - Event::handle('RouterInitialized', array($m)); + // Groups + //'list' has to be handled differently, as php will not allow a method to be named 'list' + + $m->connect('api/statusnet/groups/timeline/:id.:format', + array('action' => 'ApiTimelineGroup', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xmljson|rss|atom)')); + + $m->connect('api/statusnet/groups/show.:format', + array('action' => 'ApiGroupShow', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/show/:id.:format', + array('action' => 'ApiGroupShow', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/join.:format', + array('action' => 'ApiGroupJoin', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/join/:id.:format', + array('action' => 'ApiGroupJoin', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/leave.:format', + array('action' => 'ApiGroupLeave', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/leave/:id.:format', + array('action' => 'ApiGroupLeave', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/is_member.:format', + array('action' => 'ApiGroupIsMember', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/list.:format', + array('action' => 'ApiGroupList', + 'format' => '(xml|json|rss|atom)')); + + $m->connect('api/statusnet/groups/list/:id.:format', + array('action' => 'ApiGroupList', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json|rss|atom)')); + + $m->connect('api/statusnet/groups/list_all.:format', + array('action' => 'ApiGroupListAll', + 'format' => '(xml|json|rss|atom)')); + + $m->connect('api/statusnet/groups/membership.:format', + array('action' => 'ApiGroupMembership', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/membership/:id.:format', + array('action' => 'ApiGroupMembership', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/create.:format', + array('action' => 'ApiGroupCreate', + 'format' => '(xml|json)')); + // Tags + $m->connect('api/statusnet/tags/timeline/:tag.:format', + array('action' => 'ApiTimelineTag', + 'format' => '(xmljson|rss|atom)')); + + // 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('admin/site', array('action' => 'siteadminpanel')); + $m->connect('admin/design', array('action' => 'designadminpanel')); + $m->connect('admin/user', array('action' => 'useradminpanel')); + $m->connect('admin/paths', array('action' => 'pathsadminpanel')); + + $m->connect('getfile/:filename', + array('action' => 'getfile'), + array('filename' => '[A-Za-z0-9._-]+')); + + // user stuff + + foreach (array('subscriptions', 'subscribers', + 'nudge', 'all', 'foaf', 'xrds', + 'replies', 'inbox', 'outbox', 'microsummary') as $a) { + $m->connect(':nickname/'.$a, + array('action' => $a), + array('nickname' => '[a-zA-Z0-9]{1,64}')); + } + + foreach (array('subscriptions', 'subscribers') as $a) { + $m->connect(':nickname/'.$a.'/:tag', + array('action' => $a), + array('tag' => '[a-zA-Z0-9]+', + 'nickname' => '[a-zA-Z0-9]{1,64}')); + } + + foreach (array('rss', 'groups') as $a) { + $m->connect(':nickname/'.$a, + array('action' => 'user'.$a), + array('nickname' => '[a-zA-Z0-9]{1,64}')); + } + + foreach (array('all', 'replies', 'favorites') as $a) { + $m->connect(':nickname/'.$a.'/rss', + array('action' => $a.'rss'), + array('nickname' => '[a-zA-Z0-9]{1,64}')); + } + + $m->connect(':nickname/favorites', + array('action' => 'showfavorites'), + array('nickname' => '[a-zA-Z0-9]{1,64}')); + + $m->connect(':nickname/avatar/:size', + array('action' => 'avatarbynickname'), + array('size' => '(original|96|48|24)', + 'nickname' => '[a-zA-Z0-9]{1,64}')); + + $m->connect(':nickname/tag/:tag/rss', + array('action' => 'userrss'), + array('nickname' => '[a-zA-Z0-9]{1,64}'), + array('tag' => '[a-zA-Z0-9]+')); + + $m->connect(':nickname/tag/:tag', + array('action' => 'showstream'), + array('nickname' => '[a-zA-Z0-9]{1,64}'), + array('tag' => '[a-zA-Z0-9]+')); + + $m->connect(':nickname', + array('action' => 'showstream'), + array('nickname' => '[a-zA-Z0-9]{1,64}')); + + Event::handle('RouterInitialized', array($m)); + } return $m; } diff --git a/lib/rssaction.php b/lib/rssaction.php index 60611e48d..3b303f73e 100644 --- a/lib/rssaction.php +++ b/lib/rssaction.php @@ -78,25 +78,12 @@ class Rss10Action extends Action function prepare($args) { parent::prepare($args); + $this->limit = (int) $this->trimmed('limit'); + if ($this->limit == 0) { $this->limit = DEFAULT_RSS_LIMIT; } - return true; - } - - /** - * Handle a request - * - * @param array $args Arguments from $_REQUEST - * - * @return void - */ - - function handle($args) - { - // Parent handling, including cache check - parent::handle($args); if (common_config('site', 'private')) { if (!isset($_SERVER['PHP_AUTH_USER'])) { @@ -122,8 +109,21 @@ class Rss10Action extends Action } } - // Get the list of notices - $this->notices = $this->getNotices($this->limit); + return true; + } + + /** + * Handle a request + * + * @param array $args Arguments from $_REQUEST + * + * @return void + */ + + function handle($args) + { + // Parent handling, including cache check + parent::handle($args); $this->showRss(); } @@ -140,7 +140,7 @@ class Rss10Action extends Action } /** - * Get the notices to output in this stream + * Get the notices to output in this stream. * * @return array an array of Notice objects sorted in reverse chron */ @@ -244,6 +244,16 @@ class Rss10Action extends Action $this->element('dc:creator', null, ($profile->fullname) ? $profile->fullname : $profile->nickname); $this->element('foaf:maker', array('rdf:resource' => $creator_uri)); $this->element('sioc:has_creator', array('rdf:resource' => $creator_uri.'#acct')); + $location = $notice->getLocation(); + if ($location && isset($location->lat) && isset($location->lon)) { + $location_uri = $location->getRdfURL(); + $attrs = array('geo:lat' => $location->lat, + 'geo:long' => $location->lon); + if (strlen($location_uri)) { + $attrs['rdf:resource'] = $location_uri; + } + $this->element('statusnet:origin', $attrs); + } $this->element('statusnet:postIcon', array('rdf:resource' => $profile->avatarUrl())); $this->element('cc:licence', array('rdf:resource' => common_config('license', 'url'))); if ($notice->reply_to) { @@ -258,28 +268,22 @@ class Rss10Action extends Action $attachments = $notice->attachments(); if($attachments){ foreach($attachments as $attachment){ - if ($attachment->isEnclosure()) { - // DO NOT move xmlns declaration to root element. Making it - // the default namespace here improves compatibility with - // real-world feed readers. - $attribs = array( - 'rdf:resource' => $attachment->url, - 'url' => $attachment->url, - 'xmlns' => 'http://purl.oclc.org/net/rss_2.0/enc#' - ); - if ($attachment->title) { - $attribs['dc:title'] = $attachment->title; + $enclosure=$attachment->getEnclosure(); + if ($enclosure) { + $attribs = array('rdf:resource' => $enclosure->url); + if ($enclosure->title) { + $attribs['dc:title'] = $enclosure->title; } - if ($attachment->modified) { - $attribs['dc:date'] = common_date_w3dtf($attachment->modified); + if ($enclosure->modified) { + $attribs['dc:date'] = common_date_w3dtf($enclosure->modified); } - if ($attachment->size) { - $attribs['length'] = $attachment->size; + if ($enclosure->size) { + $attribs['enc:length'] = $enclosure->size; } - if ($attachment->mimetype) { - $attribs['type'] = $attachment->mimetype; + if ($enclosure->mimetype) { + $attribs['enc:type'] = $enclosure->mimetype; } - $this->element('enclosure', $attribs); + $this->element('enc:enclosure', $attribs); } $this->element('sioc:links_to', array('rdf:resource'=>$attachment->url)); } @@ -347,12 +351,16 @@ class Rss10Action extends Action 'http://commontag.org/ns#', 'xmlns:foaf' => 'http://xmlns.com/foaf/0.1/', + 'xmlns:enc' => + 'http://purl.oclc.org/net/rss_2.0/enc#', 'xmlns:sioc' => 'http://rdfs.org/sioc/ns#', 'xmlns:sioct' => 'http://rdfs.org/sioc/types#', 'xmlns:rdfs' => 'http://www.w3.org/2000/01/rdf-schema#', + 'xmlns:geo' => + 'http://www.w3.org/2003/01/geo/wgs84_pos#', 'xmlns:statusnet' => 'http://status.net/ont/', 'xmlns' => 'http://purl.org/rss/1.0/')); diff --git a/lib/sandboxform.php b/lib/sandboxform.php new file mode 100644 index 000000000..7a98e0a5f --- /dev/null +++ b/lib/sandboxform.php @@ -0,0 +1,80 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Form for sandboxing a user + * + * 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> + * @copyright 2009 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 SandboxForm extends ProfileActionForm +{ + /** + * Action this form provides + * + * @return string Name of the action, lowercased. + */ + + function target() + { + return 'sandbox'; + } + + /** + * Title of the form + * + * @return string Title of the form, internationalized + */ + + function title() + { + return _('Sandbox'); + } + + /** + * Description of the form + * + * @return string description of the form, internationalized + */ + + function description() + { + return _('Sandbox this user'); + } +} diff --git a/lib/schema.php b/lib/schema.php new file mode 100644 index 000000000..560884d9f --- /dev/null +++ b/lib/schema.php @@ -0,0 +1,717 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Database schema utilities + * + * 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 Database + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 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 representing the database schema + * + * A class representing the database schema. Can be used to + * manipulate the schema -- especially for plugins and upgrade + * utilities. + * + * @category Database + * @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/ + */ + +class Schema +{ + static $_single = null; + protected $conn = null; + + /** + * Constructor. Only run once for singleton object. + */ + + protected function __construct() + { + // XXX: there should be an easier way to do this. + $user = new User(); + + $this->conn = $user->getDatabaseConnection(); + + $user->free(); + + unset($user); + } + + /** + * Main public entry point. Use this to get + * the singleton object. + * + * @return Schema the (single) Schema object + */ + + static function get() + { + if (empty(self::$_single)) { + self::$_single = new Schema(); + } + return self::$_single; + } + + /** + * Returns a TableDef object for the table + * in the schema with the given name. + * + * Throws an exception if the table is not found. + * + * @param string $name Name of the table to get + * + * @return TableDef tabledef for that table. + */ + + public function getTableDef($name) + { + $res =& $this->conn->query('DESCRIBE ' . $name); + + if (PEAR::isError($res)) { + throw new Exception($res->getMessage()); + } + + $td = new TableDef(); + + $td->name = $name; + $td->columns = array(); + + $row = array(); + + while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) { + + $cd = new ColumnDef(); + + $cd->name = $row['Field']; + + $packed = $row['Type']; + + if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) { + $cd->type = $match[1]; + $cd->size = $match[2]; + } else { + $cd->type = $packed; + } + + $cd->nullable = ($row['Null'] == 'YES') ? true : false; + $cd->key = $row['Key']; + $cd->default = $row['Default']; + $cd->extra = $row['Extra']; + + $td->columns[] = $cd; + } + + return $td; + } + + /** + * Gets a ColumnDef object for a single column. + * + * Throws an exception if the table is not found. + * + * @param string $table name of the table + * @param string $column name of the column + * + * @return ColumnDef definition of the column or null + * if not found. + */ + + public function getColumnDef($table, $column) + { + $td = $this->getTableDef($table); + + foreach ($td->columns as $cd) { + if ($cd->name == $column) { + return $cd; + } + } + + return null; + } + + /** + * Creates a table with the given names and columns. + * + * @param string $name Name of the table + * @param array $columns Array of ColumnDef objects + * for new table. + * + * @return boolean success flag + */ + + public function createTable($name, $columns) + { + $uniques = array(); + $primary = array(); + $indices = array(); + + $sql = "CREATE TABLE $name (\n"; + + for ($i = 0; $i < count($columns); $i++) { + + $cd =& $columns[$i]; + + if ($i > 0) { + $sql .= ",\n"; + } + + $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) . ")"; + } + + foreach ($uniques as $u) { + $sql .= ",\nunique index {$name}_{$u}_idx ($u)"; + } + + foreach ($indices as $i) { + $sql .= ",\nindex {$name}_{$i}_idx ($i)"; + } + + $sql .= "); "; + + $res =& $this->conn->query($sql); + + if (PEAR::isError($res)) { + throw new Exception($res->getMessage()); + } + + return true; + } + + /** + * Drops a table from the schema + * + * Throws an exception if the table is not found. + * + * @param string $name Name of the table to drop + * + * @return boolean success flag + */ + + public function dropTable($name) + { + $res =& $this->conn->query("DROP TABLE $name"); + + if (PEAR::isError($res)) { + throw new Exception($res->getMessage()); + } + + return true; + } + + /** + * Adds an index to a table. + * + * If no name is provided, a name will be made up based + * on the table name and column names. + * + * Throws an exception on database error, esp. if the table + * does not exist. + * + * @param string $table Name of the table + * @param array $columnNames Name of columns to index + * @param string $name (Optional) name of the index + * + * @return boolean success flag + */ + + public function createIndex($table, $columnNames, $name=null) + { + if (!is_array($columnNames)) { + $columnNames = array($columnNames); + } + + if (empty($name)) { + $name = "$table_".implode("_", $columnNames)."_idx"; + } + + $res =& $this->conn->query("ALTER TABLE $table ". + "ADD INDEX $name (". + implode(",", $columnNames).")"); + + if (PEAR::isError($res)) { + throw new Exception($res->getMessage()); + } + + return true; + } + + /** + * Drops a named index from a table. + * + * @param string $table name of the table the index is on. + * @param string $name name of the index + * + * @return boolean success flag + */ + + public function dropIndex($table, $name) + { + $res =& $this->conn->query("ALTER TABLE $table DROP INDEX $name"); + + if (PEAR::isError($res)) { + throw new Exception($res->getMessage()); + } + + return true; + } + + /** + * Adds a column to a table + * + * @param string $table name of the table + * @param ColumnDef $columndef Definition of the new + * column. + * + * @return boolean success flag + */ + + public function addColumn($table, $columndef) + { + $sql = "ALTER TABLE $table ADD COLUMN " . $this->_columnSql($columndef); + + $res =& $this->conn->query($sql); + + if (PEAR::isError($res)) { + throw new Exception($res->getMessage()); + } + + return true; + } + + /** + * Modifies a column in the schema. + * + * The name must match an existing column and table. + * + * @param string $table name of the table + * @param ColumnDef $columndef new definition of the column. + * + * @return boolean success flag + */ + + public function modifyColumn($table, $columndef) + { + $sql = "ALTER TABLE $table MODIFY COLUMN " . + $this->_columnSql($columndef); + + $res =& $this->conn->query($sql); + + if (PEAR::isError($res)) { + throw new Exception($res->getMessage()); + } + + return true; + } + + /** + * Drops a column from a table + * + * The name must match an existing column. + * + * @param string $table name of the table + * @param string $columnName name of the column to drop + * + * @return boolean success flag + */ + + public function dropColumn($table, $columnName) + { + $sql = "ALTER TABLE $table DROP COLUMN $columnName"; + + $res =& $this->conn->query($sql); + + if (PEAR::isError($res)) { + throw new Exception($res->getMessage()); + } + + return true; + } + + /** + * Ensures that the table that backs a given + * Plugin_DataObject class exists. + * + * If the table does not yet exist, it will + * create the table. If it does exist, it will + * alter the table to match the column definitions. + * + * @param Plugin_DataObject $dataObjectClass + * + * @return boolean success flag + */ + + public function ensureDataObject($dataObjectClass) + { + $obj = new $dataObjectClass(); + $tableDef = $obj->tableDef(); + return $this->ensureTable($tableDef->name,$tableDef->columns); + } + + /** + * Ensures that a table exists with the given + * name and the given column definitions. + * + * If the table does not yet exist, it will + * create the table. If it does exist, it will + * alter the table to match the column definitions. + * + * @param string $tableName name of the table + * @param array $columns array of ColumnDef + * objects for the table + * + * @return boolean success flag + */ + + public function ensureTable($tableName, $columns) + { + // XXX: DB engine portability -> toilet + + try { + $td = $this->getTableDef($tableName); + } catch (Exception $e) { + if (preg_match('/no such table/', $e->getMessage())) { + return $this->createTable($tableName, $columns); + } else { + throw $e; + } + } + + $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(); + + foreach ($same as $m) { + $curCol = $this->_byName($td->columns, $m); + $newCol = $this->_byName($columns, $m); + + if (!$newCol->equals($curCol)) { + $tomod[] = $newCol->name; + } + } + + if (count($toadd) + count($todrop) + count($tomod) == 0) { + // nothing to do + return true; + } + + // For efficiency, we want this all in one + // query, instead of using our methods. + + $phrase = array(); + + foreach ($toadd as $columnName) { + $cd = $this->_byName($columns, $columnName); + + $phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd); + } + + foreach ($todrop as $columnName) { + $phrase[] = 'DROP COLUMN ' . $columnName; + } + + foreach ($tomod as $columnName) { + $cd = $this->_byName($columns, $columnName); + + $phrase[] = 'MODIFY COLUMN ' . $this->_columnSql($cd); + } + + $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase); + + $res =& $this->conn->query($sql); + + if (PEAR::isError($res)) { + throw new Exception($res->getMessage()); + } + + return true; + } + + /** + * Returns the array of names from an array of + * ColumnDef objects. + * + * @param array $cds array of ColumnDef objects + * + * @return array strings for name values + */ + + private function _names($cds) + { + $names = array(); + + foreach ($cds as $cd) { + $names[] = $cd->name; + } + + return $names; + } + + /** + * Get a ColumnDef from an array matching + * name. + * + * @param array $cds Array of ColumnDef objects + * @param string $name Name of the column + * + * @return ColumnDef matching item or null if no match. + */ + + private function _byName($cds, $name) + { + foreach ($cds as $cd) { + if ($cd->name == $name) { + return $cd; + } + } + + return null; + } + + /** + * Return the proper SQL for creating or + * altering a column. + * + * Appropriate for use in CREATE TABLE or + * ALTER TABLE statements. + * + * @param ColumnDef $cd column to create + * + * @return string correct SQL for that column + */ + + private function _columnSql($cd) + { + $sql = "{$cd->name} "; + + if (!empty($cd->size)) { + $sql .= "{$cd->type}({$cd->size}) "; + } else { + $sql .= "{$cd->type} "; + } + + if (!empty($cd->default)) { + $sql .= "default {$cd->default} "; + } else { + $sql .= ($cd->nullable) ? "null " : "not null "; + } + + return $sql; + } +} + +/** + * A class encapsulating the structure of a table. + * + * @category Database + * @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/ + */ + +class TableDef +{ + /** name of the table */ + public $name; + /** array of ColumnDef objects for the columns. */ + public $columns; + + /** + * Constructor. + * + * @param string $name name of the table + * @param array $columns columns in the table + */ + + function __construct($name=null,$columns=null) + { + $this->name = $name; + $this->columns = $columns; + } +} + +/** + * A class encapsulating the structure of a column in a table. + * + * @category Database + * @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/ + */ + +class ColumnDef +{ + /** name of the column. */ + public $name; + /** type of column, e.g. 'int', 'varchar' */ + public $type; + /** size of the column. */ + public $size; + /** boolean flag; can it be null? */ + public $nullable; + /** + * type of key: null = no key; 'PRI' => primary; + * 'UNI' => unique key; 'MUL' => multiple values. + */ + public $key; + /** default value if any. */ + public $default; + /** 'extra' stuff. Returned by MySQL, largely + * unused. */ + public $extra; + /** auto increment this field if no value is specific for it during an insert **/ + public $auto_increment; + + /** + * Constructor. + * + * @param string $name name of the column + * @param string $type type of the column + * @param int $size size of the column + * @param boolean $nullable can this be null? + * @param string $key type of key + * @param value $default default value + * @param value $extra unused + */ + + function __construct($name=null, $type=null, $size=null, + $nullable=true, $key=null, $default=null, + $extra=null, $auto_increment=false) + { + $this->name = strtolower($name); + $this->type = strtolower($type); + $this->size = $size+0; + $this->nullable = $nullable; + $this->key = $key; + $this->default = $default; + $this->extra = $extra; + $this->auto_increment = $auto_increment; + } + + /** + * Compares this columndef with another to see + * if they're functionally equivalent. + * + * @param ColumnDef $other column to compare + * + * @return boolean true if equivalent, otherwise false. + */ + + function equals($other) + { + return ($this->name == $other->name && + $this->_typeMatch($other) && + $this->_defaultMatch($other) && + $this->_nullMatch($other) && + $this->key == $other->key && + $this->auto_increment == $other->auto_increment); + } + + /** + * Does the type of this column match the + * type of the other column? + * + * Checks the type and size of a column. Tries + * to ignore differences between synonymous + * data types, like 'integer' and 'int'. + * + * @param ColumnDef $other other column to check + * + * @return boolean true if they're about equivalent + */ + + private function _typeMatch($other) + { + switch ($this->type) { + case 'integer': + case 'int': + return ($other->type == 'integer' || + $other->type == 'int'); + break; + default: + return ($this->type == $other->type && + $this->size == $other->size); + } + } + + /** + * Does the default behaviour of this column match + * the other? + * + * @param ColumnDef $other other column to check + * + * @return boolean true if defaults are effectively the same. + */ + + private function _defaultMatch($other) + { + return ((is_null($this->default) && is_null($other->default)) || + ($this->default == $other->default)); + } + + /** + * Does the null behaviour of this column match + * the other? + * + * @param ColumnDef $other other column to check + * + * @return boolean true if these columns 'null' the same. + */ + + private function _nullMatch($other) + { + return ((!is_null($this->default) && !is_null($other->default) && + $this->default == $other->default) || + ($this->nullable == $other->nullable)); + } +} diff --git a/lib/search_engines.php b/lib/search_engines.php index 69f6ff468..332db3f89 100644 --- a/lib/search_engines.php +++ b/lib/search_engines.php @@ -46,70 +46,11 @@ class SearchEngine } } -class SphinxSearch extends SearchEngine -{ - private $sphinx; - private $connected; - - function __construct($target, $table) - { - $fp = @fsockopen(common_config('sphinx', 'server'), common_config('sphinx', 'port')); - if (!$fp) { - $this->connected = false; - return; - } - fclose($fp); - parent::__construct($target, $table); - $this->sphinx = new SphinxClient; - $this->sphinx->setServer(common_config('sphinx', 'server'), common_config('sphinx', 'port')); - $this->connected = true; - } - - function is_connected() - { - return $this->connected; - } - - function limit($offset, $count, $rss = false) - { - //FIXME without LARGEST_POSSIBLE, the most recent results aren't returned - // this probably has a large impact on performance - $LARGEST_POSSIBLE = 1e6; - - if ($rss) { - $this->sphinx->setLimits($offset, $count, $count, $LARGEST_POSSIBLE); - } - else { - // return at most 50 pages of results - $this->sphinx->setLimits($offset, $count, 50 * ($count - 1), $LARGEST_POSSIBLE); - } - - return $this->target->limit(0, $count); - } - - function query($q) - { - $result = $this->sphinx->query($q, $this->table); - if (!isset($result['matches'])) return false; - $id_set = join(', ', array_keys($result['matches'])); - $this->target->whereAdd("id in ($id_set)"); - return true; - } - - function set_sort_mode($mode) - { - if ('chron' === $mode) { - $this->sphinx->SetSortMode(SPH_SORT_ATTR_DESC, 'created_ts'); - return $this->target->orderBy('created desc'); - } - } -} - class MySQLSearch extends SearchEngine { function query($q) { - if ('identica_people' === $this->table) { + if ('profile' === $this->table) { $this->target->whereAdd('MATCH(nickname, fullname, location, bio, homepage) ' . 'AGAINST (\''.addslashes($q).'\' IN BOOLEAN MODE)'); if (strtolower($q) != $q) { @@ -117,7 +58,7 @@ class MySQLSearch extends SearchEngine 'AGAINST (\''.addslashes(strtolower($q)).'\' IN BOOLEAN MODE)', 'OR'); } return true; - } else if ('identica_notices' === $this->table) { + } else if ('notice' === $this->table) { // Don't show imported notices $this->target->whereAdd('notice.is_local != ' . Notice::GATEWAY); @@ -143,13 +84,13 @@ class MySQLLikeSearch extends SearchEngine { function query($q) { - if ('identica_people' === $this->table) { + if ('profile' === $this->table) { $qry = sprintf('(nickname LIKE "%%%1$s%%" OR '. ' fullname LIKE "%%%1$s%%" OR '. ' location LIKE "%%%1$s%%" OR '. ' bio LIKE "%%%1$s%%" OR '. ' homepage LIKE "%%%1$s%%")', addslashes($q)); - } else if ('identica_notices' === $this->table) { + } else if ('notice' === $this->table) { $qry = sprintf('content LIKE "%%%1$s%%"', addslashes($q)); } else { throw new ServerException('Unknown table: ' . $this->table); @@ -165,9 +106,9 @@ class PGSearch extends SearchEngine { function query($q) { - if ('identica_people' === $this->table) { + if ('profile' === $this->table) { return $this->target->whereAdd('textsearch @@ plainto_tsquery(\''.addslashes($q).'\')'); - } else if ('identica_notices' === $this->table) { + } else if ('notice' === $this->table) { // XXX: We need to filter out gateway notices (notice.is_local = -2) --Zach diff --git a/lib/settingsaction.php b/lib/settingsaction.php index a1f305f5b..c3669868d 100644 --- a/lib/settingsaction.php +++ b/lib/settingsaction.php @@ -77,9 +77,7 @@ class SettingsAction extends CurrentUserDesignAction // _all_ our settings are important common_set_returnto($this->selfUrl()); $user = common_current_user(); - if ($user->hasOpenID()) { - common_redirect(common_local_url('openidlogin'), 303); - } else { + if (Event::handle('RedirectToLogin', array($this, $user))) { common_redirect(common_local_url('login'), 303); } } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { diff --git a/lib/silenceform.php b/lib/silenceform.php new file mode 100644 index 000000000..9673fa120 --- /dev/null +++ b/lib/silenceform.php @@ -0,0 +1,80 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Form for silencing a user + * + * 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> + * @copyright 2009 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 silencing 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 UnSilenceForm + */ + +class SilenceForm extends ProfileActionForm +{ + /** + * Action this form provides + * + * @return string Name of the action, lowercased. + */ + + function target() + { + return 'silence'; + } + + /** + * Title of the form + * + * @return string Title of the form, internationalized + */ + + function title() + { + return _('Silence'); + } + + /** + * Description of the form + * + * @return string description of the form, internationalized + */ + + function description() + { + return _('Silence this user'); + } +} diff --git a/lib/snapshot.php b/lib/snapshot.php index ede846e5b..2a10c6b93 100644 --- a/lib/snapshot.php +++ b/lib/snapshot.php @@ -172,26 +172,9 @@ class Snapshot { // XXX: Use OICU2 and OAuth to make authorized requests - $postdata = http_build_query($this->stats); - - $opts = - array('http' => - array( - 'method' => 'POST', - 'header' => 'Content-type: '. - 'application/x-www-form-urlencoded', - 'content' => $postdata, - 'user_agent' => 'StatusNet/'.STATUSNET_VERSION - ) - ); - - $context = stream_context_create($opts); - $reporturl = common_config('snapshot', 'reporturl'); - - $result = @file_get_contents($reporturl, false, $context); - - return $result; + $request = HTTPClient::start(); + $request->post($reporturl, null, $this->stats); } /** diff --git a/lib/subs.php b/lib/subs.php index 68c89c842..2fc3160de 100644 --- a/lib/subs.php +++ b/lib/subs.php @@ -44,8 +44,12 @@ function subs_subscribe_user($user, $other_nickname) function subs_subscribe_to($user, $other) { + if (!$user->hasRight(Right::SUBSCRIBE)) { + return _('You have been banned from subscribing.'); + } + if ($user->isSubscribed($other)) { - return _('Already subscribed!.'); + return _('Already subscribed!'); } if ($other->hasBlocked($user)) { @@ -121,7 +125,7 @@ function subs_unsubscribe_user($user, $other_nickname) function subs_unsubscribe_to($user, $other) { if (!$user->isSubscribed($other)) - return _('Not subscribed!.'); + return _('Not subscribed!'); $sub = DB_DataObject::factory('subscription'); diff --git a/lib/theme.php b/lib/theme.php index 08e3e8538..020ce1ac4 100644 --- a/lib/theme.php +++ b/lib/theme.php @@ -23,7 +23,7 @@ * @package StatusNet * @author Evan Prodromou <evan@status.net> * @author Sarven Capadisli <csarven@status.net> - * @copyright 2008 StatusNet, Inc. + * @copyright 2008-2009 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/ */ @@ -33,62 +33,215 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { } /** - * Gets the full path of a file in a theme dir based on its relative name + * Class for querying and manipulating a theme * - * @param string $relative relative path within the theme directory - * @param string $theme name of the theme; defaults to current theme + * 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. * - * @return string File path to the theme file + * This used to be a couple of functions, but for various reasons it's nice + * to have a class instead. + * + * @category Output + * @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/ */ -function theme_file($relative, $theme=null) +class Theme { - if (empty($theme)) { - $theme = common_config('site', 'theme'); + var $dir = null; + var $path = null; + + /** + * Constructor + * + * Determines the proper directory and path for this theme. + * + * @param string $name Name of the theme; defaults to config value + */ + + function __construct($name=null) + { + if (empty($name)) { + $name = common_config('site', 'theme'); + } + + // Check to see if it's in the local dir + + $localroot = self::localRoot(); + + $fulldir = $localroot.'/'.$name; + + if (file_exists($fulldir) && is_dir($fulldir)) { + $this->dir = $fulldir; + $this->path = common_path('local/theme/'.$name.'/'); + return; + } + + // Check to see if it's in the distribution dir + + $instroot = self::installRoot(); + + $fulldir = $instroot.'/'.$name; + + if (file_exists($fulldir) && is_dir($fulldir)) { + + $this->dir = $fulldir; + + $path = common_config('theme', 'path'); + + if (empty($path)) { + $path = common_config('site', 'path') . '/theme/'; + } + + if ($path[strlen($path)-1] != '/') { + $path .= '/'; + } + + if ($path[0] != '/') { + $path = '/'.$path; + } + + $server = common_config('theme', 'server'); + + if (empty($server)) { + $server = common_config('site', 'server'); + } + + // XXX: protocol + + $this->path = 'http://'.$server.$path.$name; + } } - $dir = common_config('theme', 'dir'); - if (empty($dir)) { - $dir = INSTALLDIR.'/theme'; + + /** + * Gets the full local filename of a file in this theme. + * + * @param string $relative relative name, like 'logo.png' + * + * @return string full pathname, like /var/www/mublog/theme/default/logo.png + */ + + function getFile($relative) + { + return $this->dir.'/'.$relative; } - return $dir.'/'.$theme.'/'.$relative; -} -/** - * Gets the full URL of a file in a theme dir based on its relative name - * - * @param string $relative relative path within the theme directory - * @param string $theme name of the theme; defaults to current theme - * - * @return string URL of the file - */ + /** + * Gets the full HTTP url of a file in this theme + * + * @param string $relative relative name, like 'logo.png' + * + * @return string full URL, like 'http://example.com/theme/default/logo.png' + */ -function theme_path($relative, $theme=null) -{ - if (empty($theme)) { - $theme = common_config('site', 'theme'); + function getPath($relative) + { + return $this->path.'/'.$relative; + } + + /** + * Gets the full path of a file in a theme dir based on its relative name + * + * @param string $relative relative path within the theme directory + * @param string $name name of the theme; defaults to current theme + * + * @return string File path to the theme file + */ + + static function file($relative, $name=null) + { + $theme = new Theme($name); + return $theme->getFile($relative); } - $path = common_config('theme', 'path'); + /** + * Gets the full URL of a file in a theme dir based on its relative name + * + * @param string $relative relative path within the theme directory + * @param string $name name of the theme; defaults to current theme + * + * @return string URL of the file + */ - if (empty($path)) { - $path = common_config('site', 'path') . '/theme/'; + static function path($relative, $name=null) + { + $theme = new Theme($name); + return $theme->getPath($relative); } - if ($path[strlen($path)-1] != '/') { - $path .= '/'; + /** + * list available theme names + * + * @return array list of available theme names + */ + + static function listAvailable() + { + $local = self::subdirsOf(self::localRoot()); + $install = self::subdirsOf(self::installRoot()); + + $i = array_search('base', $install); + + unset($install[$i]); + + return array_merge($local, $install); } - if ($path[0] != '/') { - $path = '/'.$path; + /** + * Utility for getting subdirs of a directory + * + * @param string $dir full path to directory to check + * + * @return array relative filenames of subdirs, or empty array + */ + + protected static function subdirsOf($dir) + { + $subdirs = array(); + + if (is_dir($dir)) { + if ($dh = opendir($dir)) { + while (($filename = readdir($dh)) !== false) { + if ($filename != '..' && $filename !== '.' && + is_dir($dir.'/'.$filename)) { + $subdirs[] = $filename; + } + } + closedir($dh); + } + } + + return $subdirs; } - $server = common_config('theme', 'server'); + /** + * Local root dir for themes + * + * @return string local root dir for themes + */ - if (empty($server)) { - $server = common_config('site', 'server'); + protected static function localRoot() + { + return INSTALLDIR.'/local/theme'; } - // XXX: protocol + /** + * Root dir for themes that are shipped with StatusNet + * + * @return string root dir for StatusNet themes + */ + + protected static function installRoot() + { + $instroot = common_config('theme', 'dir'); - return 'http://'.$server.$path.$theme.'/'.$relative; + if (empty($instroot)) { + $instroot = INSTALLDIR.'/theme'; + } + + return $instroot; + } } diff --git a/lib/twitter.php b/lib/twitter.php deleted file mode 100644 index 676c9b20a..000000000 --- a/lib/twitter.php +++ /dev/null @@ -1,306 +0,0 @@ -<?php -/* - * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2008, 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/>. - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { - exit(1); -} - -define('TWITTER_SERVICE', 1); // Twitter is foreign_service ID 1 - -function update_twitter_user($twitter_id, $screen_name) -{ - $uri = 'http://twitter.com/' . $screen_name; - $fuser = new Foreign_user(); - - $fuser->query('BEGIN'); - - // Dropping down to SQL because regular DB_DataObject udpate stuff doesn't seem - // to work so good with tables that have multiple column primary keys - - // Any time we update the uri for a forein user we have to make sure there - // are no dupe entries first -- unique constraint on the uri column - - $qry = 'UPDATE foreign_user set uri = \'\' WHERE uri = '; - $qry .= '\'' . $uri . '\'' . ' AND service = ' . TWITTER_SERVICE; - - $fuser->query($qry); - - // Update the user - - $qry = 'UPDATE foreign_user SET nickname = '; - $qry .= '\'' . $screen_name . '\'' . ', uri = \'' . $uri . '\' '; - $qry .= 'WHERE id = ' . $twitter_id . ' AND service = ' . TWITTER_SERVICE; - - $fuser->query('COMMIT'); - - $fuser->free(); - unset($fuser); - - return true; -} - -function add_twitter_user($twitter_id, $screen_name) -{ - - $new_uri = 'http://twitter.com/' . $screen_name; - - // Clear out any bad old foreign_users with the new user's legit URL - // This can happen when users move around or fakester accounts get - // repoed, and things like that. - - $luser = new Foreign_user(); - $luser->uri = $new_uri; - $luser->service = TWITTER_SERVICE; - $result = $luser->delete(); - - if (empty($result)) { - common_log(LOG_WARNING, - "Twitter bridge - removed invalid Twitter user squatting on uri: $new_uri"); - } - - $luser->free(); - unset($luser); - - // Otherwise, create a new Twitter user - - $fuser = new Foreign_user(); - - $fuser->nickname = $screen_name; - $fuser->uri = 'http://twitter.com/' . $screen_name; - $fuser->id = $twitter_id; - $fuser->service = TWITTER_SERVICE; - $fuser->created = common_sql_now(); - $result = $fuser->insert(); - - if (empty($result)) { - common_log(LOG_WARNING, - "Twitter bridge - failed to add new Twitter user: $twitter_id - $screen_name."); - common_log_db_error($fuser, 'INSERT', __FILE__); - } else { - common_debug("Twitter bridge - Added new Twitter user: $screen_name ($twitter_id)."); - } - - return $result; -} - -// Creates or Updates a Twitter user -function save_twitter_user($twitter_id, $screen_name) -{ - - // Check to see whether the Twitter user is already in the system, - // and update its screen name and uri if so. - - $fuser = Foreign_user::getForeignUser($twitter_id, TWITTER_SERVICE); - - if (!empty($fuser)) { - - $result = true; - - // Only update if Twitter screen name has changed - - if ($fuser->nickname != $screen_name) { - $result = update_twitter_user($twitter_id, $screen_name); - - common_debug('Twitter bridge - Updated nickname (and URI) for Twitter user ' . - "$fuser->id to $screen_name, was $fuser->nickname"); - } - - return $result; - - } else { - return add_twitter_user($twitter_id, $screen_name); - } - - $fuser->free(); - unset($fuser); - - return true; -} - -function is_twitter_bound($notice, $flink) { - - // Check to see if notice should go to Twitter - if (!empty($flink) && ($flink->noticesync & FOREIGN_NOTICE_SEND)) { - - // If it's not a Twitter-style reply, or if the user WANTS to send replies. - if (!preg_match('/^@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) || - ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) { - return true; - } - } - - return false; -} - -function broadcast_twitter($notice) -{ - $flink = Foreign_link::getByUserID($notice->profile_id, - TWITTER_SERVICE); - - if (is_twitter_bound($notice, $flink)) { - if (TwitterOAuthClient::isPackedToken($flink->credentials)) { - return broadcast_oauth($notice, $flink); - } else { - return broadcast_basicauth($notice, $flink); - } - } - - return true; -} - -function broadcast_oauth($notice, $flink) { - - $user = $flink->getUser(); - $statustxt = format_status($notice); - $token = TwitterOAuthClient::unpackToken($flink->credentials); - $client = new TwitterOAuthClient($token->key, $token->secret); - $status = null; - - try { - $status = $client->statusesUpdate($statustxt); - } catch (OAuthClientCurlException $e) { - return process_error($e, $flink); - } - - if (empty($status)) { - - // This could represent a failure posting, - // or the Twitter API might just be behaving flakey. - - $errmsg = sprintf('Twitter bridge - No data returned by Twitter API when ' . - 'trying to send update for %1$s (user id %2$s).', - $user->nickname, $user->id); - common_log(LOG_WARNING, $errmsg); - - return false; - } - - // Notice crossed the great divide - - $msg = sprintf('Twitter bridge - posted notice %s to Twitter using OAuth.', - $notice->id); - common_log(LOG_INFO, $msg); - - return true; -} - -function broadcast_basicauth($notice, $flink) -{ - $user = $flink->getUser(); - - $statustxt = format_status($notice); - - $client = new TwitterBasicAuthClient($flink); - $status = null; - - try { - $status = $client->statusesUpdate($statustxt); - } catch (BasicAuthCurlException $e) { - return process_error($e, $flink); - } - - if (empty($status)) { - - $errmsg = sprintf('Twitter bridge - No data returned by Twitter API when ' . - 'trying to send update for %1$s (user id %2$s).', - $user->nickname, $user->id); - common_log(LOG_WARNING, $errmsg); - - return false; - } - - $msg = sprintf('Twitter bridge - posted notice %s to Twitter using basic auth.', - $notice->id); - common_log(LOG_INFO, $msg); - - return true; -} - -function process_error($e, $flink) -{ - $user = $flink->getUser(); - $errmsg = $e->getMessage(); - $delivered = false; - - switch($errmsg) { - case 'The requested URL returned error: 401': - $logmsg = sprintf('Twiter bridge - User %1$s (user id: %2$s) has an invalid ' . - 'Twitter screen_name/password combo or an invalid acesss token.', - $user->nickname, $user->id); - $delivered = true; - remove_twitter_link($flink); - break; - case 'The requested URL returned error: 403': - $logmsg = sprintf('Twitter bridge - User %1$s (user id: %2$s) has exceeded ' . - 'his/her Twitter request limit.', - $user->nickname, $user->id); - break; - default: - $logmsg = sprintf('Twitter bridge - cURL error trying to send notice to Twitter ' . - 'for user %1$s (user id: %2$s) - ' . - 'code: %3$s message: %4$s.', - $user->nickname, $user->id, - $e->getCode(), $e->getMessage()); - break; - } - - common_log(LOG_WARNING, $logmsg); - - return $delivered; -} - -function format_status($notice) -{ - // XXX: Hack to get around PHP cURL's use of @ being a a meta character - return preg_replace('/^@/', ' @', $notice->content); -} - -function remove_twitter_link($flink) -{ - $user = $flink->getUser(); - - common_log(LOG_INFO, 'Removing Twitter bridge Foreign link for ' . - "user $user->nickname (user id: $user->id)."); - - $result = $flink->delete(); - - if (empty($result)) { - common_log(LOG_ERR, 'Could not remove Twitter bridge ' . - "Foreign_link for $user->nickname (user id: $user->id)!"); - common_log_db_error($flink, 'DELETE', __FILE__); - } - - // Notify the user that her Twitter bridge is down - - if (isset($user->email)) { - - $result = mail_twitter_bridge_removed($user); - - if (!$result) { - - $msg = 'Unable to send email to notify ' . - "$user->nickname (user id: $user->id) " . - 'that their Twitter bridge link was ' . - 'removed!'; - - common_log(LOG_WARNING, $msg); - } - } - -} diff --git a/lib/twitterbasicauthclient.php b/lib/twitterbasicauthclient.php deleted file mode 100644 index fd331fbdc..000000000 --- a/lib/twitterbasicauthclient.php +++ /dev/null @@ -1,236 +0,0 @@ -<?php -/** - * StatusNet, the distributed open-source microblogging tool - * - * Class for doing OAuth calls against Twitter - * - * 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 Integration - * @package StatusNet - * @author Zach Copley <zach@status.net> - * @copyright 2009 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); -} - -/** - * Exception wrapper for cURL errors - * - * @category Integration - * @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 BasicAuthCurlException extends Exception -{ -} - -/** - * Class for talking to the Twitter API with HTTP Basic Auth. - * - * @category Integration - * @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 TwitterBasicAuthClient -{ - var $screen_name = null; - var $password = null; - - /** - * constructor - * - * @param Foreign_link $flink a Foreign_link storing the - * Twitter user's password, etc. - */ - function __construct($flink) - { - $fuser = $flink->getForeignUser(); - $this->screen_name = $fuser->nickname; - $this->password = $flink->credentials; - } - - /** - * Calls Twitter's /statuses/update API method - * - * @param string $status text of the status - * @param int $in_reply_to_status_id optional id of the status it's - * a reply to - * - * @return mixed the status - */ - function statusesUpdate($status, $in_reply_to_status_id = null) - { - $url = 'https://twitter.com/statuses/update.json'; - $params = array('status' => $status, - 'source' => common_config('integration', 'source'), - 'in_reply_to_status_id' => $in_reply_to_status_id); - $response = $this->httpRequest($url, $params); - $status = json_decode($response); - return $status; - } - - /** - * Calls Twitter's /statuses/friends_timeline API method - * - * @param int $since_id show statuses after this id - * @param int $max_id show statuses before this id - * @param int $cnt number of statuses to show - * @param int $page page number - * - * @return mixed an array of statuses - */ - function statusesFriendsTimeline($since_id = null, $max_id = null, - $cnt = null, $page = null) - { - $url = 'https://twitter.com/statuses/friends_timeline.json'; - $params = array('since_id' => $since_id, - 'max_id' => $max_id, - 'count' => $cnt, - 'page' => $page); - $qry = http_build_query($params); - - if (!empty($qry)) { - $url .= "?$qry"; - } - - $response = $this->httpRequest($url); - $statuses = json_decode($response); - return $statuses; - } - - /** - * Calls Twitter's /statuses/friends API method - * - * @param int $id id of the user whom you wish to see friends of - * @param int $user_id numerical user id - * @param int $screen_name screen name - * @param int $page page number - * - * @return mixed an array of twitter users and their latest status - */ - function statusesFriends($id = null, $user_id = null, $screen_name = null, - $page = null) - { - $url = "https://twitter.com/statuses/friends.json"; - - $params = array('id' => $id, - 'user_id' => $user_id, - 'screen_name' => $screen_name, - 'page' => $page); - $qry = http_build_query($params); - - if (!empty($qry)) { - $url .= "?$qry"; - } - - $response = $this->httpRequest($url); - $friends = json_decode($response); - return $friends; - } - - /** - * Calls Twitter's /statuses/friends/ids API method - * - * @param int $id id of the user whom you wish to see friends of - * @param int $user_id numerical user id - * @param int $screen_name screen name - * @param int $page page number - * - * @return mixed a list of ids, 100 per page - */ - function friendsIds($id = null, $user_id = null, $screen_name = null, - $page = null) - { - $url = "https://twitter.com/friends/ids.json"; - - $params = array('id' => $id, - 'user_id' => $user_id, - 'screen_name' => $screen_name, - 'page' => $page); - $qry = http_build_query($params); - - if (!empty($qry)) { - $url .= "?$qry"; - } - - $response = $this->httpRequest($url); - $ids = json_decode($response); - return $ids; - } - - /** - * Make a HTTP request using cURL. - * - * @param string $url Where to make the request - * @param array $params post parameters - * - * @return mixed the request - */ - function httpRequest($url, $params = null, $auth = true) - { - $options = array( - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FAILONERROR => true, - CURLOPT_HEADER => false, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_USERAGENT => 'StatusNet', - CURLOPT_CONNECTTIMEOUT => 120, - CURLOPT_TIMEOUT => 120, - CURLOPT_HTTPAUTH => CURLAUTH_ANY, - CURLOPT_SSL_VERIFYPEER => false, - - // Twitter is strict about accepting invalid "Expect" headers - - CURLOPT_HTTPHEADER => array('Expect:') - ); - - if (isset($params)) { - $options[CURLOPT_POST] = true; - $options[CURLOPT_POSTFIELDS] = $params; - } - - if ($auth) { - $options[CURLOPT_USERPWD] = $this->screen_name . - ':' . $this->password; - } - - $ch = curl_init($url); - curl_setopt_array($ch, $options); - $response = curl_exec($ch); - - if ($response === false) { - $msg = curl_error($ch); - $code = curl_errno($ch); - throw new BasicAuthCurlException($msg, $code); - } - - curl_close($ch); - - return $response; - } - -} diff --git a/lib/twitteroauthclient.php b/lib/twitteroauthclient.php deleted file mode 100644 index bad2b74ca..000000000 --- a/lib/twitteroauthclient.php +++ /dev/null @@ -1,229 +0,0 @@ -<?php -/** - * StatusNet, the distributed open-source microblogging tool - * - * Class for doing OAuth calls against Twitter - * - * 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 Integration - * @package StatusNet - * @author Zach Copley <zach@status.net> - * @copyright 2009 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); -} - -/** - * Class for talking to the Twitter API with OAuth. - * - * @category Integration - * @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 TwitterOAuthClient extends OAuthClient -{ - public static $requestTokenURL = 'https://twitter.com/oauth/request_token'; - public static $authorizeURL = 'https://twitter.com/oauth/authorize'; - public static $accessTokenURL = 'https://twitter.com/oauth/access_token'; - - /** - * Constructor - * - * @param string $oauth_token the user's token - * @param string $oauth_token_secret the user's token secret - * - * @return nothing - */ - function __construct($oauth_token = null, $oauth_token_secret = null) - { - $consumer_key = common_config('twitter', 'consumer_key'); - $consumer_secret = common_config('twitter', 'consumer_secret'); - - parent::__construct($consumer_key, $consumer_secret, - $oauth_token, $oauth_token_secret); - } - - // XXX: the following two functions are to support the horrible hack - // of using the credentils field in Foreign_link to store both - // the access token and token secret. This hack should go away with - // 0.9, in which we can make DB changes and add a new column for the - // token itself. - - static function packToken($token) - { - return implode(chr(0), array($token->key, $token->secret)); - } - - static function unpackToken($str) - { - $vals = explode(chr(0), $str); - return new OAuthToken($vals[0], $vals[1]); - } - - static function isPackedToken($str) - { - if (strpos($str, chr(0)) === false) { - return false; - } else { - return true; - } - } - - /** - * Builds a link to Twitter's endpoint for authorizing a request token - * - * @param OAuthToken $request_token token to authorize - * - * @return the link - */ - function getAuthorizeLink($request_token) - { - return parent::getAuthorizeLink(self::$authorizeURL, - $request_token, - common_local_url('twitterauthorization')); - } - - /** - * Calls Twitter's /account/verify_credentials API method - * - * @return mixed the Twitter user - */ - function verifyCredentials() - { - $url = 'https://twitter.com/account/verify_credentials.json'; - $response = $this->oAuthGet($url); - $twitter_user = json_decode($response); - return $twitter_user; - } - - /** - * Calls Twitter's /statuses/update API method - * - * @param string $status text of the status - * @param int $in_reply_to_status_id optional id of the status it's - * a reply to - * - * @return mixed the status - */ - function statusesUpdate($status, $in_reply_to_status_id = null) - { - $url = 'https://twitter.com/statuses/update.json'; - $params = array('status' => $status, - 'in_reply_to_status_id' => $in_reply_to_status_id); - $response = $this->oAuthPost($url, $params); - $status = json_decode($response); - return $status; - } - - /** - * Calls Twitter's /statuses/friends_timeline API method - * - * @param int $since_id show statuses after this id - * @param int $max_id show statuses before this id - * @param int $cnt number of statuses to show - * @param int $page page number - * - * @return mixed an array of statuses - */ - function statusesFriendsTimeline($since_id = null, $max_id = null, - $cnt = null, $page = null) - { - - $url = 'https://twitter.com/statuses/friends_timeline.json'; - $params = array('since_id' => $since_id, - 'max_id' => $max_id, - 'count' => $cnt, - 'page' => $page); - $qry = http_build_query($params); - - if (!empty($qry)) { - $url .= "?$qry"; - } - - $response = $this->oAuthGet($url); - $statuses = json_decode($response); - return $statuses; - } - - /** - * Calls Twitter's /statuses/friends API method - * - * @param int $id id of the user whom you wish to see friends of - * @param int $user_id numerical user id - * @param int $screen_name screen name - * @param int $page page number - * - * @return mixed an array of twitter users and their latest status - */ - function statusesFriends($id = null, $user_id = null, $screen_name = null, - $page = null) - { - $url = "https://twitter.com/statuses/friends.json"; - - $params = array('id' => $id, - 'user_id' => $user_id, - 'screen_name' => $screen_name, - 'page' => $page); - $qry = http_build_query($params); - - if (!empty($qry)) { - $url .= "?$qry"; - } - - $response = $this->oAuthGet($url); - $friends = json_decode($response); - return $friends; - } - - /** - * Calls Twitter's /statuses/friends/ids API method - * - * @param int $id id of the user whom you wish to see friends of - * @param int $user_id numerical user id - * @param int $screen_name screen name - * @param int $page page number - * - * @return mixed a list of ids, 100 per page - */ - function friendsIds($id = null, $user_id = null, $screen_name = null, - $page = null) - { - $url = "https://twitter.com/friends/ids.json"; - - $params = array('id' => $id, - 'user_id' => $user_id, - 'screen_name' => $screen_name, - 'page' => $page); - $qry = http_build_query($params); - - if (!empty($qry)) { - $url .= "?$qry"; - } - - $response = $this->oAuthGet($url); - $ids = json_decode($response); - return $ids; - } - -} diff --git a/lib/unblockform.php b/lib/unblockform.php index f1343757c..2a444f7cd 100644 --- a/lib/unblockform.php +++ b/lib/unblockform.php @@ -28,12 +28,10 @@ * @link http://status.net/ */ -if (!defined('STATUSNET') && !defined('LACONICA')) { +if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR.'/lib/form.php'; - /** * Form for unblocking a user * @@ -47,106 +45,38 @@ require_once INSTALLDIR.'/lib/form.php'; * @see BlockForm */ -class UnblockForm extends Form +class UnblockForm extends ProfileActionForm { /** - * Profile of user to unblock - */ - - var $profile = null; - - /** - * Return-to args - */ - - var $args = null; - - /** - * Constructor - * - * @param HTMLOutputter $out output channel - * @param Profile $profile profile of user to unblock - * @param array $args return-to args - */ - - function __construct($out=null, $profile=null, $args=null) - { - parent::__construct($out); - - $this->profile = $profile; - $this->args = $args; - } - - /** - * ID of the form - * - * @return int ID of the form - */ - - function id() - { - return 'unblock-' . $this->profile->id; - } - - /** - * class of the form + * Action this form provides * - * @return string class of the form + * @return string Name of the action, lowercased. */ - function formClass() + function target() { - return 'form_user_unblock'; + return 'unblock'; } /** - * Action of the form - * - * @return string URL of the action - */ - - function action() - { - return common_local_url('unblock'); - } - - /** - * Legend of the Form - * - * @return void - */ - function formLegend() - { - $this->out->element('legend', null, _('Unblock this user')); - } - - - /** - * Data elements of the form + * Title of the form * - * @return void + * @return string Title of the form, internationalized */ - function formData() + function title() { - $this->out->hidden('unblockto-' . $this->profile->id, - $this->profile->id, - 'unblockto'); - if ($this->args) { - foreach ($this->args as $k => $v) { - $this->out->hidden('returnto-' . $k, $v); - } - } + return _('Unblock'); } /** - * Action elements + * Description of the form * - * @return void + * @return string description of the form, internationalized */ - function formActions() + function description() { - $this->out->submit('submit', _('Unblock'), 'submit', null, _('Unblock this user')); + return _('Unblock this user'); } } diff --git a/lib/unqueuemanager.php b/lib/unqueuemanager.php index 3cdad0b54..72dbc4eed 100644 --- a/lib/unqueuemanager.php +++ b/lib/unqueuemanager.php @@ -39,7 +39,7 @@ class UnQueueManager case 'omb': if ($this->_isLocal($notice)) { require_once(INSTALLDIR.'/lib/omb.php'); - omb_broadcast_remote_subscribers($notice); + omb_broadcast_notice($notice); } break; case 'public': @@ -48,17 +48,6 @@ class UnQueueManager jabber_public_notice($notice); } break; - case 'twitter': - if ($this->_isLocal($notice)) { - broadcast_twitter($notice); - } - break; - case 'facebook': - if ($this->_isLocal($notice)) { - require_once INSTALLDIR . '/lib/facebookutil.php'; - return facebookBroadcastNotice($notice); - } - break; case 'ping': if ($this->_isLocal($notice)) { require_once INSTALLDIR . '/lib/ping.php'; @@ -72,8 +61,13 @@ class UnQueueManager require_once(INSTALLDIR.'/lib/jabber.php'); jabber_broadcast_notice($notice); break; + case 'plugin': + Event::handle('HandleQueuedNotice', array(&$notice)); + break; default: - throw ServerException("UnQueueManager: Unknown queue: $type"); + if (Event::handle('UnqueueHandleNotice', array(&$notice, $queue))) { + throw new ServerException("UnQueueManager: Unknown queue: $queue"); + } } } diff --git a/lib/unsandboxform.php b/lib/unsandboxform.php new file mode 100644 index 000000000..a77634244 --- /dev/null +++ b/lib/unsandboxform.php @@ -0,0 +1,82 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Form for unsandboxing a user + * + * 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> + * @copyright 2009 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 unsandboxing a user + * + * Removes the "sandboxed" role for 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 UnsandboxForm extends ProfileActionForm +{ + /** + * Action this form provides + * + * @return string Name of the action, lowercased. + */ + + function target() + { + return 'unsandbox'; + } + + /** + * Title of the form + * + * @return string Title of the form, internationalized + */ + + function title() + { + return _('Unsandbox'); + } + + /** + * Description of the form + * + * @return string description of the form, internationalized + */ + + function description() + { + return _('Unsandbox this user'); + } +} diff --git a/lib/unsilenceform.php b/lib/unsilenceform.php new file mode 100644 index 000000000..ac02b8b6c --- /dev/null +++ b/lib/unsilenceform.php @@ -0,0 +1,80 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Form for unsilencing a user + * + * 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> + * @copyright 2009 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 unsilencing 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 SilenceForm + */ + +class UnSilenceForm extends ProfileActionForm +{ + /** + * Action this form provides + * + * @return string Name of the action, lowercased. + */ + + function target() + { + return 'unsilence'; + } + + /** + * Title of the form + * + * @return string Title of the form, internationalized + */ + + function title() + { + return _('Unsilence'); + } + + /** + * Description of the form + * + * @return string description of the form, internationalized + */ + + function description() + { + return _('Unsilence this user'); + } +} diff --git a/lib/userprofile.php b/lib/userprofile.php new file mode 100644 index 000000000..ee205af85 --- /dev/null +++ b/lib/userprofile.php @@ -0,0 +1,358 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Profile for a particular user + * + * 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 Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @author Sarven Capadisli <csarven@status.net> + * @copyright 2008 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); +} + +require_once INSTALLDIR.'/lib/widget.php'; + +/** + * Profile of a user + * + * Shows profile information about a particular user + * + * @category Output + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @author Sarven Capadisli <csarven@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 HTMLOutputter + */ + +class UserProfile extends Widget +{ + var $user = null; + var $profile = null; + + function __construct($action=null, $user=null, $profile=null) + { + parent::__construct($action); + $this->user = $user; + $this->profile = $profile; + } + + function show() + { + $this->showProfileData(); + $this->showEntityActions(); + } + + function showProfileData() + { + if (Event::handle('StartProfilePageProfileSection', array(&$this->out, $this->profile))) { + + $this->out->elementStart('div', 'entity_profile vcard author'); + $this->out->element('h2', null, _('User profile')); + + if (Event::handle('StartProfilePageProfileElements', array(&$this->out, $this->profile))) { + + $this->showAvatar(); + $this->showNickname(); + $this->showFullName(); + $this->showLocation(); + $this->showHomepage(); + $this->showBio(); + $this->showProfileTags(); + + Event::handle('EndProfilePageProfileElements', array(&$this->out, $this->profile)); + } + + $this->out->elementEnd('div'); + Event::handle('EndProfilePageProfileSection', array(&$this->out, $this->profile)); + } + } + + function showAvatar() + { + if (Event::handle('StartProfilePageAvatar', array($this->out, $this->profile))) { + + $avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE); + + $this->out->elementStart('dl', 'entity_depiction'); + $this->out->element('dt', null, _('Photo')); + $this->out->elementStart('dd'); + $this->out->element('img', array('src' => ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE), + 'class' => 'photo avatar', + 'width' => AVATAR_PROFILE_SIZE, + 'height' => AVATAR_PROFILE_SIZE, + 'alt' => $this->profile->nickname)); + $this->out->elementEnd('dd'); + + $user = User::staticGet('id', $this->profile->id); + + $cur = common_current_user(); + if ($cur && $cur->id == $user->id) { + $this->out->elementStart('dd'); + $this->out->element('a', array('href' => common_local_url('avatarsettings')), _('Edit Avatar')); + $this->out->elementEnd('dd'); + } + + $this->out->elementEnd('dl'); + + Event::handle('EndProfilePageAvatar', array($this->out, $this->profile)); + } + } + + function showNickname() + { + if (Event::handle('StartProfilePageNickname', array($this->out, $this->profile))) { + + $this->out->elementStart('dl', 'entity_nickname'); + $this->out->element('dt', null, _('Nickname')); + $this->out->elementStart('dd'); + $hasFN = ($this->profile->fullname) ? 'nickname url uid' : 'fn nickname url uid'; + $this->out->element('a', array('href' => $this->profile->profileurl, + 'rel' => 'me', 'class' => $hasFN), + $this->profile->nickname); + $this->out->elementEnd('dd'); + $this->out->elementEnd('dl'); + + Event::handle('EndProfilePageNickname', array($this->out, $this->profile)); + } + } + + function showFullName() + { + if (Event::handle('StartProfilePageFullName', array($this->out, $this->profile))) { + if ($this->profile->fullname) { + $this->out->elementStart('dl', 'entity_fn'); + $this->out->element('dt', null, _('Full name')); + $this->out->elementStart('dd'); + $this->out->element('span', 'fn', $this->profile->fullname); + $this->out->elementEnd('dd'); + $this->out->elementEnd('dl'); + } + Event::handle('EndProfilePageFullName', array($this->out, $this->profile)); + } + } + + function showLocation() + { + if (Event::handle('StartProfilePageLocation', array($this->out, $this->profile))) { + if ($this->profile->location) { + $this->out->elementStart('dl', 'entity_location'); + $this->out->element('dt', null, _('Location')); + $this->out->element('dd', 'label', $this->profile->location); + $this->out->elementEnd('dl'); + } + Event::handle('EndProfilePageLocation', array($this->out, $this->profile)); + } + } + + function showHomepage() + { + if (Event::handle('StartProfilePageHomepage', array($this->out, $this->profile))) { + if ($this->profile->homepage) { + $this->out->elementStart('dl', 'entity_url'); + $this->out->element('dt', null, _('URL')); + $this->out->elementStart('dd'); + $this->out->element('a', array('href' => $this->profile->homepage, + 'rel' => 'me', 'class' => 'url'), + $this->profile->homepage); + $this->out->elementEnd('dd'); + $this->out->elementEnd('dl'); + } + Event::handle('EndProfilePageHomepage', array($this->out, $this->profile)); + } + } + + function showBio() + { + if (Event::handle('StartProfilePageBio', array($this->out, $this->profile))) { + if ($this->profile->bio) { + $this->out->elementStart('dl', 'entity_note'); + $this->out->element('dt', null, _('Note')); + $this->out->element('dd', 'note', $this->profile->bio); + $this->out->elementEnd('dl'); + } + Event::handle('EndProfilePageBio', array($this->out, $this->profile)); + } + } + + function showProfileTags() + { + if (Event::handle('StartProfilePageProfileTags', array($this->out, $this->profile))) { + $tags = Profile_tag::getTags($this->profile->id, $this->profile->id); + + if (count($tags) > 0) { + $this->out->elementStart('dl', 'entity_tags'); + $this->out->element('dt', null, _('Tags')); + $this->out->elementStart('dd'); + $this->out->elementStart('ul', 'tags xoxo'); + foreach ($tags as $tag) { + $this->out->elementStart('li'); + // Avoid space by using raw output. + $pt = '<span class="mark_hash">#</span><a rel="tag" href="' . + common_local_url('peopletag', array('tag' => $tag)) . + '">' . $tag . '</a>'; + $this->out->raw($pt); + $this->out->elementEnd('li'); + } + $this->out->elementEnd('ul'); + $this->out->elementEnd('dd'); + $this->out->elementEnd('dl'); + } + Event::handle('EndProfilePageProfileTags', array($this->out, $this->profile)); + } + } + + function showEntityActions() + { + if (Event::handle('StartProfilePageActionsSection', array(&$this->out, $this->profile))) { + + $cur = common_current_user(); + + $this->out->elementStart('div', 'entity_actions'); + $this->out->element('h2', null, _('User actions')); + $this->out->elementStart('ul'); + + if (Event::handle('StartProfilePageActionsElements', array(&$this->out, $this->profile))) { + if (empty($cur)) { // not logged in + $this->out->elementStart('li', 'entity_subscribe'); + $this->showRemoteSubscribeLink(); + $this->out->elementEnd('li'); + } else { + if ($cur->id == $this->profile->id) { // your own page + $this->out->elementStart('li', 'entity_edit'); + $this->out->element('a', array('href' => common_local_url('profilesettings'), + 'title' => _('Edit profile settings')), + _('Edit')); + $this->out->elementEnd('li'); + } else { // someone else's page + + // subscribe/unsubscribe button + + $this->out->elementStart('li', 'entity_subscribe'); + + if ($cur->isSubscribed($this->profile)) { + $usf = new UnsubscribeForm($this->out, $this->profile); + $usf->show(); + } else { + $sf = new SubscribeForm($this->out, $this->profile); + $sf->show(); + } + $this->out->elementEnd('li'); + + if ($cur->mutuallySubscribed($this->user)) { + + // message + + $this->out->elementStart('li', 'entity_send-a-message'); + $this->out->element('a', array('href' => common_local_url('newmessage', array('to' => $this->user->id)), + 'title' => _('Send a direct message to this user')), + _('Message')); + $this->out->elementEnd('li'); + + // nudge + + if ($this->user->email && $this->user->emailnotifynudge) { + $this->out->elementStart('li', 'entity_nudge'); + $nf = new NudgeForm($this->out, $this->user); + $nf->show(); + $this->out->elementEnd('li'); + } + } + + // return-to args, so we don't have to keep re-writing them + + list($action, $r2args) = $this->out->returnToArgs(); + + // push the action into the list + + $r2args['action'] = $action; + + // block/unblock + + $blocked = $cur->hasBlocked($this->profile); + $this->out->elementStart('li', 'entity_block'); + if ($blocked) { + $ubf = new UnblockForm($this->out, $this->profile, $r2args); + $ubf->show(); + } else { + $bf = new BlockForm($this->out, $this->profile, $r2args); + $bf->show(); + } + $this->out->elementEnd('li'); + + if ($cur->hasRight(Right::SANDBOXUSER)) { + $this->out->elementStart('li', 'entity_sandbox'); + if ($this->user->isSandboxed()) { + $usf = new UnSandboxForm($this->out, $this->profile, $r2args); + $usf->show(); + } else { + $sf = new SandboxForm($this->out, $this->profile, $r2args); + $sf->show(); + } + $this->out->elementEnd('li'); + } + + if ($cur->hasRight(Right::SILENCEUSER)) { + $this->out->elementStart('li', 'entity_silence'); + if ($this->user->isSilenced()) { + $usf = new UnSilenceForm($this->out, $this->profile, $r2args); + $usf->show(); + } else { + $sf = new SilenceForm($this->out, $this->profile, $r2args); + $sf->show(); + } + $this->out->elementEnd('li'); + } + + if ($cur->hasRight(Right::DELETEUSER)) { + $this->out->elementStart('li', 'entity_delete'); + $df = new DeleteUserForm($this->out, $this->profile, $r2args); + $df->show(); + $this->out->elementEnd('li'); + } + } + } + + Event::handle('EndProfilePageActionsElements', array(&$this->out, $this->profile)); + } + + $this->out->elementEnd('ul'); + $this->out->elementEnd('div'); + + Event::handle('EndProfilePageActionsSection', array(&$this->out, $this->profile)); + } + } + + function showRemoteSubscribeLink() + { + $url = common_local_url('remotesubscribe', + array('nickname' => $this->profile->nickname)); + $this->out->element('a', array('href' => $url, + 'class' => 'entity_remote_subscribe'), + _('Subscribe')); + } +} diff --git a/lib/util.php b/lib/util.php index 0052090f6..5bf4f6091 100644 --- a/lib/util.php +++ b/lib/util.php @@ -51,13 +51,22 @@ function common_init_locale($language=null) 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"); + $language = common_language(); - // So we don't have to make people install the gettext locales $locale_set = common_init_locale($language); - bindtextdomain("statusnet", common_config('site','locale_path')); + 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"); - setlocale(LC_CTYPE, 'C'); + if(!$locale_set) { common_log(LOG_INFO, 'Language requested:' . $language . ' - locale could not be set. Perhaps that system locale is not installed.', __FILE__); } @@ -107,23 +116,26 @@ function common_munge_password($password, $id) } // check if a username exists and has matching password + function common_check_user($nickname, $password) { - // NEVER allow blank passwords, even if they match the DB - if (mb_strlen($password) == 0) { - return false; - } - $user = User::staticGet('nickname', $nickname); - if (is_null($user) || $user === false) { - return false; - } else { - if (0 == strcmp(common_munge_password($password, $user->id), - $user->password)) { - return $user; - } else { - return false; + $authenticatedUser = false; + + if (Event::handle('StartCheckPassword', array($nickname, $password, &$authenticatedUser))) { + $user = User::staticGet('nickname', $nickname); + if (!empty($user)) { + if (!empty($password)) { // never allow login with blank password + if (0 == strcmp(common_munge_password($password, $user->id), + $user->password)) { + //internal checking passed + $authenticatedUser =& $user; + } + } } + Event::handle('EndCheckPassword', array($nickname, $password, $authenticatedUser)); } + + return $authenticatedUser; } // is the current user logged in? @@ -184,10 +196,15 @@ function common_set_user($user) } if ($user) { - common_ensure_session(); - $_SESSION['userid'] = $user->id; - $_cur = $user; - return $_cur; + if (Event::handle('StartSetUser', array(&$user))) { + if($user){ + common_ensure_session(); + $_SESSION['userid'] = $user->id; + $_cur = $user; + Event::handle('EndSetUser', array($user)); + return $_cur; + } + } } return false; } @@ -338,8 +355,11 @@ function common_current_user() common_ensure_session(); $id = isset($_SESSION['userid']) ? $_SESSION['userid'] : false; if ($id) { - $_cur = User::staticGet($id); - return $_cur; + $user = User::staticGet($id); + if ($user) { + $_cur = $user; + return $_cur; + } } } @@ -393,8 +413,8 @@ function common_render_content($text, $notice) $id = $notice->profile_id; $r = preg_replace('/(^|\s+)@(['.NICKNAME_FMT.']{1,64})/e', "'\\1@'.common_at_link($id, '\\2')", $r); $r = preg_replace('/^T ([A-Z0-9]{1,64}) /e', "'T '.common_at_link($id, '\\1').' '", $r); - $r = preg_replace('/(^|\s+)@#([A-Za-z0-9]{1,64})/e', "'\\1@#'.common_at_hash_link($id, '\\2')", $r); - $r = preg_replace('/(^|\s)!([A-Za-z0-9]{1,64})/e', "'\\1!'.common_group_link($id, '\\2')", $r); + $r = preg_replace('/(^|[\s\.\,\:\;]+)@#([A-Za-z0-9]{1,64})/e', "'\\1@#'.common_at_hash_link($id, '\\2')", $r); + $r = preg_replace('/(^|[\s\.\,\:\;]+)!([A-Za-z0-9]{1,64})/e', "'\\1!'.common_group_link($id, '\\2')", $r); return $r; } @@ -412,7 +432,7 @@ function common_render_text($text) function common_replace_urls_callback($text, $callback, $notice_id = null) { // Start off with a regex $regex = '#'. - '(?:^|[\s\(\)\[\]\{\}\\\'\\\";]+)(?![\@\!\#])'. + '(?:^|[\s\<\>\(\)\[\]\{\}\\\'\\\";]+)(?![\@\!\#])'. '('. '(?:'. '(?:'. //Known protocols @@ -442,9 +462,9 @@ function common_replace_urls_callback($text, $callback, $notice_id = null) { ')'. '(?:'. '(?:\:\d+)?'. //:port - '(?:/[\pN\pL$\[\]\,\!\(\)\.\:\-\_\+\/\=\&\;\%\~\*\$\+\'\"@]*)?'. // /path - '(?:\?[\pN\pL\$\[\]\,\!\(\)\.\:\-\_\+\/\=\&\;\%\~\*\$\+\'\"@\/]*)?'. // ?query string - '(?:\#[\pN\pL$\[\]\,\!\(\)\.\:\-\_\+\/\=\&\;\%\~\*\$\+\'\"\@/\?\#]*)?'. // #fragment + '(?:/[\pN\pL$\,\!\(\)\.\:\-\_\+\/\=\&\;\%\~\*\$\+\'@]*)?'. // /path + '(?:\?[\pN\pL\$\,\!\(\)\.\:\-\_\+\/\=\&\;\%\~\*\$\+\'@\/]*)?'. // ?query string + '(?:\#[\pN\pL$\,\!\(\)\.\:\-\_\+\/\=\&\;\%\~\*\$\+\'\@/\?\#]*)?'. // #fragment ')(?<![\?\.\,\#\,])'. ')'. '#ixu'; @@ -470,6 +490,10 @@ function callback_helper($matches, $callback, $notice_id) { array( 'left'=>'{', 'right'=>'}' + ), + array( + 'left'=>'<', + 'right'=>'>' ) ); $cannotEndWith=array('.','?',',','#'); @@ -493,7 +517,7 @@ function callback_helper($matches, $callback, $notice_id) { }while($original_url!=$url); if(empty($notice_id)){ - $result = call_user_func_array($callback,$url); + $result = call_user_func_array($callback, array($url)); }else{ $result = call_user_func_array($callback, array(array($url,$notice_id)) ); } @@ -522,21 +546,22 @@ function common_linkify($url) { if(strpos($url, '@') !== false && strpos($url, ':') === false) { //url is an email address without the mailto: protocol - return XMLStringer::estring('a', array('href' => "mailto:$url", 'rel' => 'external'), $url); - } + $canon = "mailto:$url"; + $longurl = "mailto:$url"; + }else{ - $canon = File_redirection::_canonUrl($url); + $canon = File_redirection::_canonUrl($url); - $longurl_data = File_redirection::where($url); - if (is_array($longurl_data)) { - $longurl = $longurl_data['url']; - } elseif (is_string($longurl_data)) { - $longurl = $longurl_data; - } else { - throw new ServerException("Can't linkify url '$url'"); + $longurl_data = File_redirection::where($canon); + if (is_array($longurl_data)) { + $longurl = $longurl_data['url']; + } elseif (is_string($longurl_data)) { + $longurl = $longurl_data; + } else { + throw new ServerException("Can't linkify url '$url'"); + } } - - $attrs = array('href' => $canon, 'rel' => 'external'); + $attrs = array('href' => $canon, 'title' => $longurl, 'rel' => 'external'); $is_attachment = false; $attachment_id = null; @@ -584,7 +609,8 @@ function common_linkify($url) { function common_shorten_links($text) { - if (mb_strlen($text) <= 140) return $text; + $maxLength = Notice::maxContent(); + if ($maxLength == 0 || mb_strlen($text) <= $maxLength) return $text; return common_replace_urls_callback($text, array('File_redirection', 'makeShort')); } @@ -729,14 +755,10 @@ function common_relative_profile($sender, $nickname, $dt=null) function common_local_url($action, $args=null, $params=null, $fragment=null) { - static $sensitive = array('login', 'register', 'passwordsettings', - 'twittersettings', 'finishopenidlogin', - 'finishaddopenid', 'api'); - $r = Router::get(); $path = $r->build($action, $args, $params, $fragment); - $ssl = in_array($action, $sensitive); + $ssl = common_is_sensitive($action); if (common_config('site','fancy')) { $url = common_path(mb_substr($path, 1), $ssl); @@ -750,6 +772,19 @@ function common_local_url($action, $args=null, $params=null, $fragment=null) return $url; } +function common_is_sensitive($action) +{ + static $sensitive = array('login', 'register', 'passwordsettings', + 'twittersettings', 'api'); + $ssl = null; + + if (Event::handle('SensitiveAction', array($action, &$ssl))) { + $ssl = in_array($action, $sensitive); + } + + return $ssl; +} + function common_path($relative, $ssl=false) { $pathpart = (common_config('site', 'path')) ? common_config('site', 'path')."/" : ''; @@ -891,10 +926,9 @@ function common_broadcast_notice($notice, $remote=false) function common_enqueue_notice($notice) { static $localTransports = array('omb', - 'twitter', - 'facebook', 'ping'); - static $allTransports = array('sms'); + + static $allTransports = array('sms', 'plugin'); $transports = $allTransports; @@ -912,11 +946,16 @@ function common_enqueue_notice($notice) } } - $qm = QueueManager::get(); + if (Event::handle('StartEnqueueNotice', array($notice, &$transports))) { + + $qm = QueueManager::get(); - foreach ($transports as $transport) - { - $qm->enqueue($notice, $transport); + foreach ($transports as $transport) + { + $qm->enqueue($notice, $transport); + } + + Event::handle('EndEnqueueNotice', array($notice, $transports)); } return true; @@ -1055,7 +1094,11 @@ function common_log_objstring(&$object) $arr = $object->toArray(); $fields = array(); foreach ($arr as $k => $v) { - $fields[] = "$k='$v'"; + if (is_object($v)) { + $fields[] = "$k='".get_class($v)."'"; + } else { + $fields[] = "$k='$v'"; + } } $objstring = $object->tableName() . '[' . implode(',', $fields) . ']'; return $objstring; @@ -1154,7 +1197,7 @@ function common_negotiate_type($cprefs, $sprefs) } if ('text/html' === $besttype) { - return "text/html"; + return "text/html; charset=utf-8"; } return $besttype; } @@ -1162,7 +1205,8 @@ function common_negotiate_type($cprefs, $sprefs) function common_config($main, $sub) { global $config; - return isset($config[$main][$sub]) ? $config[$main][$sub] : false; + return (array_key_exists($main, $config) && + array_key_exists($sub, $config[$main])) ? $config[$main][$sub] : false; } function common_copy_args($from) @@ -1340,9 +1384,28 @@ function common_memcache() } } +function common_license_terms($uri) +{ + if(preg_match('/creativecommons.org\/licenses\/([^\/]+)/', $uri, $matches)) { + return explode('-',$matches[1]); + } + return array($uri); +} + function common_compatible_license($from, $to) { + $from_terms = common_license_terms($from); + // public domain and cc-by are compatible with everything + if(count($from_terms) == 1 && ($from_terms[0] == 'publicdomain' || $from_terms[0] == 'by')) { + return true; + } + $to_terms = common_license_terms($to); + // sa is compatible across versions. IANAL + if(in_array('sa',$from_terms) || in_array('sa',$to_terms)) { + return count(array_diff($from_terms, $to_terms)) == 0; + } // XXX: better compatibility check needed here! + // Should at least normalise URIs return ($from == $to); } @@ -1365,63 +1428,18 @@ function common_shorten_url($long_url) if (empty($user)) { // common current user does not find a user when called from the XMPP daemon // therefore we'll set one here fix, so that XMPP given URLs may be shortened - $svc = 'ur1.ca'; + $shortenerName = 'ur1.ca'; } else { - $svc = $user->urlshorteningservice; + $shortenerName = $user->urlshorteningservice; } - $curlh = curl_init(); - curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 20); // # seconds to wait - curl_setopt($curlh, CURLOPT_USERAGENT, 'StatusNet'); - curl_setopt($curlh, CURLOPT_RETURNTRANSFER, true); - - switch($svc) { - case 'ur1.ca': - require_once INSTALLDIR.'/lib/Shorturl_api.php'; - $short_url_service = new LilUrl; - $short_url = $short_url_service->shorten($long_url); - break; - - case '2tu.us': - $short_url_service = new TightUrl; - require_once INSTALLDIR.'/lib/Shorturl_api.php'; - $short_url = $short_url_service->shorten($long_url); - break; - - case 'ptiturl.com': - require_once INSTALLDIR.'/lib/Shorturl_api.php'; - $short_url_service = new PtitUrl; - $short_url = $short_url_service->shorten($long_url); - break; - - case 'bit.ly': - curl_setopt($curlh, CURLOPT_URL, 'http://bit.ly/api?method=shorten&long_url='.urlencode($long_url)); - $short_url = current(json_decode(curl_exec($curlh))->results)->hashUrl; - break; - - case 'is.gd': - curl_setopt($curlh, CURLOPT_URL, 'http://is.gd/api.php?longurl='.urlencode($long_url)); - $short_url = curl_exec($curlh); - break; - case 'snipr.com': - curl_setopt($curlh, CURLOPT_URL, 'http://snipr.com/site/snip?r=simple&link='.urlencode($long_url)); - $short_url = curl_exec($curlh); - break; - case 'metamark.net': - curl_setopt($curlh, CURLOPT_URL, 'http://metamark.net/api/rest/simple?long_url='.urlencode($long_url)); - $short_url = curl_exec($curlh); - break; - case 'tinyurl.com': - curl_setopt($curlh, CURLOPT_URL, 'http://tinyurl.com/api-create.php?url='.urlencode($long_url)); - $short_url = curl_exec($curlh); - break; - default: - $short_url = false; + if(Event::handle('StartShortenUrl', array($long_url,$shortenerName,&$shortenedUrl))){ + //URL wasn't shortened, so return the long url + return $long_url; + }else{ + //URL was shortened, so return the result + return $shortenedUrl; } - - curl_close($curlh); - - return $short_url; } function common_client_ip() diff --git a/lib/xrdsoutputter.php b/lib/xrdsoutputter.php new file mode 100644 index 000000000..4b77ed5a3 --- /dev/null +++ b/lib/xrdsoutputter.php @@ -0,0 +1,96 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Low-level generator for HTML + * + * 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 Output + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2008 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); +} + +require_once INSTALLDIR.'/lib/xmloutputter.php'; + +/** + * Low-level generator for XRDS XML + * + * @category Output + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + * @see Action + * @see XMLOutputter + */ +class XRDSOutputter extends XMLOutputter +{ + public function startXRDS() + { + header('Content-Type: application/xrds+xml'); + $this->startXML(); + $this->elementStart('XRDS', array('xmlns' => 'xri://$xrds')); + } + + public function endXRDS() + { + $this->elementEnd('XRDS'); + $this->endXML(); + } + + /** + * Show service. + * + * @param string $type XRDS type + * @param string $uri URI + * @param array $params type parameters, null by default + * @param array $sigs type signatures, null by default + * @param string $localId local ID, null by default + * + * @return void + */ + function showXrdsService($type, $uri, $params=null, $sigs=null, $localId=null) + { + $this->elementStart('Service'); + if ($uri) { + $this->element('URI', null, $uri); + } + $this->element('Type', null, $type); + if ($params) { + foreach ($params as $param) { + $this->element('Type', null, $param); + } + } + if ($sigs) { + foreach ($sigs as $sig) { + $this->element('Type', null, $sig); + } + } + if ($localId) { + $this->element('LocalID', null, $localId); + } + $this->elementEnd('Service'); + } +} |