diff options
author | Brion Vibber <brion@status.net> | 2010-08-10 15:01:29 -0700 |
---|---|---|
committer | Brion Vibber <brion@status.net> | 2010-08-10 15:01:29 -0700 |
commit | 819d33210d298de74b64dc7ead79e9d9b223b12e (patch) | |
tree | 902d42087e633b96e12bef699f6c80e7342c9312 /plugins | |
parent | 8f071b2818e8321ea910df612016175f65093402 (diff) | |
parent | 08fc6053ec55e911b842fd05dafc5e0c99c4e992 (diff) |
Merge branch '0.9.x' into tinymce
Diffstat (limited to 'plugins')
179 files changed, 16372 insertions, 1830 deletions
diff --git a/plugins/Adsense/AdsensePlugin.php b/plugins/Adsense/AdsensePlugin.php index ab2b9a6fb..cd6fc3503 100644 --- a/plugins/Adsense/AdsensePlugin.php +++ b/plugins/Adsense/AdsensePlugin.php @@ -83,6 +83,21 @@ class AdsensePlugin extends UAPPlugin public $adScript = 'http://pagead2.googlesyndication.com/pagead/show_ads.js'; public $client = null; + function initialize() + { + parent::initialize(); + + // A little bit of chicanery so we avoid overwriting values that + // are passed in with the constructor + + foreach (array('mediumRectangle', 'rectangle', 'leaderboard', 'wideSkyscraper', 'adScript', 'client') as $setting) { + $value = common_config('adsense', strtolower($setting)); + if (!empty($value)) { // not found + $this->$setting = $value; + } + } + } + /** * Show a medium rectangle 'ad' * @@ -157,4 +172,37 @@ class AdsensePlugin extends UAPPlugin $action->script($this->adScript); } + + function onRouterInitialized($m) + { + $m->connect('admin/adsense', + array('action' => 'adsenseadminpanel')); + + return true; + } + + function onAutoload($cls) + { + $dir = dirname(__FILE__); + + switch ($cls) + { + case 'AdsenseadminpanelAction': + require_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; + return false; + default: + return true; + } + } + + function onEndAdminPanelNav($menu) { + if (AdminPanelAction::canAdmin('adsense')) { + // TRANS: Menu item title/tooltip + $menu_title = _('Adsense configuration'); + // TRANS: Menu item for site administration + $menu->out->menuItem(common_local_url('adsenseadminpanel'), _('Adsense'), + $menu_title, $action_name == 'adsenseadminpanel', 'nav_adsense_admin_panel'); + } + return true; + } }
\ No newline at end of file diff --git a/plugins/Adsense/adsenseadminpanel.php b/plugins/Adsense/adsenseadminpanel.php new file mode 100644 index 000000000..7b99cf805 --- /dev/null +++ b/plugins/Adsense/adsenseadminpanel.php @@ -0,0 +1,223 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Adsense administration panel + * + * 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 Adsense + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Administer adsense settings + * + * @category Adsense + * @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 AdsenseadminpanelAction extends AdminPanelAction +{ + /** + * Returns the page title + * + * @return string page title + */ + + function title() + { + return _('Adsense'); + } + + /** + * Instructions for using this form. + * + * @return string instructions + */ + + function getInstructions() + { + return _('Adsense settings for this StatusNet site'); + } + + /** + * Show the site admin panel form + * + * @return void + */ + + function showForm() + { + $form = new AdsenseAdminPanelForm($this); + $form->show(); + return; + } + + /** + * Save settings from the form + * + * @return void + */ + + function saveSettings() + { + static $settings = array('adsense' => array('adScript', 'client', 'mediumRectangle', 'rectangle', 'leaderboard', 'wideSkyscraper')); + + $values = array(); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + $values[$section][$setting] = $this->trimmed($setting); + } + } + + // This throws an exception on validation errors + + $this->validate($values); + + // assert(all values are valid); + + $config = new Config(); + + $config->query('BEGIN'); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + Config::save($section, $setting, $values[$section][$setting]); + } + } + + $config->query('COMMIT'); + + return; + } + + function validate(&$values) + { + } +} + +/** + * Form for the adsense admin panel + */ + +class AdsenseAdminPanelForm extends AdminForm +{ + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return 'form_adsense_admin_panel'; + } + + /** + * class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'form_adsense'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('adsenseadminpanel'); + } + + /** + * Data elements of the form + * + * @return void + */ + + function formData() + { + $this->out->elementStart('fieldset', array('id' => 'adsense_admin')); + $this->out->elementStart('ul', 'form_data'); + $this->li(); + $this->input('client', + _('Client ID'), + _('Google client ID'), + 'adsense'); + $this->unli(); + $this->li(); + $this->input('adScript', + _('Ad Script URL'), + _('Script URL (advanced)'), + 'adsense'); + $this->unli(); + $this->li(); + $this->input('mediumRectangle', + _('Medium rectangle'), + _('Medium rectangle slot code'), + 'adsense'); + $this->unli(); + $this->li(); + $this->input('rectangle', + _('Rectangle'), + _('Rectangle slot code'), + 'adsense'); + $this->unli(); + $this->li(); + $this->input('leaderboard', + _('Leaderboard'), + _('Leaderboard slot code'), + 'adsense'); + $this->unli(); + $this->li(); + $this->input('wideSkyscraper', + _('Skyscraper'), + _('Wide skyscraper slot code'), + 'adsense'); + $this->unli(); + $this->out->elementEnd('ul'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit('submit', _('Save'), 'submit', null, _('Save AdSense settings')); + } +} diff --git a/plugins/AutoSandbox/AutoSandboxPlugin.php b/plugins/AutoSandbox/AutoSandboxPlugin.php new file mode 100644 index 000000000..ffd8bf455 --- /dev/null +++ b/plugins/AutoSandbox/AutoSandboxPlugin.php @@ -0,0 +1,96 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Plugin to automatically sandbox newly registered users in an effort to beat + * spammers. If the user proves to be legitimate, moderators can un-sandbox them. + * + * 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 Plugin + * @package StatusNet + * @author Sean Carmody<seancarmody@gmail.com> + * @copyright 2010 + * @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); +} + +define('AUTOSANDBOX', '0.1'); + +//require_once(INSTALLDIR.'/plugins/AutoSandbox/autosandbox.php'); + +class AutoSandboxPlugin extends Plugin +{ + var $contact; + var $debug; + + function onInitializePlugin() + { + if(!isset($this->debug)) + { + $this->debug = 0; + } + + if(!isset($this->contact)) { + $default = common_config('newuser', 'default'); + if (!empty($default)) { + $this->contact = $default; + } + } + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'AutoSandbox', + 'version' => STATUSNET_VERSION, + 'author' => 'Sean Carmody', + 'homepage' => 'http://status.net/wiki/Plugin:AutoSandbox', + 'rawdescription' => + _m('Automatically sandboxes newly registered members.')); + return true; + } + + function onStartRegistrationFormData($action) + { + + $instr = 'Note you will initially be "sandboxed" so your posts will not appear in the public timeline.'; + + if (isset($this->contact)) { + $contactuser = User::staticGet('nickname', $this->contact); + if (!empty($contactuser)) { + $contactlink = "@<a href=\"$contactuser->uri\">$contactuser->nickname</a>"; + $instr = $instr . " Send a message to $contactlink to speed up the unsandboxing process."; + } + } + + $output = common_markup_to_html($instr); + $action->elementStart('div', 'instructions'); + $action->raw($output); + $action->elementEnd('div'); + } + + function onEndUserRegister(&$profile,&$user) + { + $profile->sandbox(); + if ($this->debug) { + common_log(LOG_WARNING, "AutoSandbox: sandboxed of $user->nickname"); + } + } +} diff --git a/plugins/AutoSandbox/LICENSE b/plugins/AutoSandbox/LICENSE new file mode 100644 index 000000000..011faa4e7 --- /dev/null +++ b/plugins/AutoSandbox/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2010 Stubborn Mule - http://www.stubbornmule.net +AUTHORS: + Sean Carmody + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/AutoSandbox/README b/plugins/AutoSandbox/README new file mode 100644 index 000000000..2f5d625f7 --- /dev/null +++ b/plugins/AutoSandbox/README @@ -0,0 +1,39 @@ +StatusNet AutoSandbox plugin 0.1 03/16/10 +========================================= +Automatically sandboxes newly registered users as a spam-management technique. +Only really suits small sites where all users can be hand-moderated. A moderator +will then have to unbox legimate users, using the following built-in script: + +./scripts/userrole.php -n username -r moderator + +(replace 'username' with the nickname of the user you wish to make a moderator). + +The following note will be added to the top of the Registration form: + +"Note you will initially be "sandboxed" so your posts will not appear in the +public timeline." + +This can be followed by the following extra information if a contact user (denoted +here by XXX) is specified: + +"Send a message to @XXX to speed up the unsandboxing process." + +If no contact user is specified, it will default to the "Default subscription" user +who automatically subscribes to new users (set in Admin -> User). + +Use: +1. Add plugin: + +Default usage: +addPlugin('AutoSandbox'); + +Specify a contact user (replace 'someuser' with appropriate username): +addPlugin('AutoSandbox', array('contact' => 'someuser')); + +Stop contact user from defaulting to the Defaul subscription: +addPlugin('AutoSandbox', array('contact' => '')); + +Changelog +========= +0.1 initial release + diff --git a/plugins/AutoSandbox/locale/AutoSandbox.pot b/plugins/AutoSandbox/locale/AutoSandbox.pot new file mode 100644 index 000000000..b01f9dc89 --- /dev/null +++ b/plugins/AutoSandbox/locale/AutoSandbox.pot @@ -0,0 +1,21 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: AutoSandboxPlugin.php:66 +msgid "Automatically sandboxes newly registered members." +msgstr "" diff --git a/plugins/Autocomplete/AutocompletePlugin.php b/plugins/Autocomplete/AutocompletePlugin.php index d586631a4..b2be365dd 100644 --- a/plugins/Autocomplete/AutocompletePlugin.php +++ b/plugins/Autocomplete/AutocompletePlugin.php @@ -22,7 +22,8 @@ * @category Plugin * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> - * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @copyright 2010 Free Software Foundation http://fsf.org + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ @@ -31,8 +32,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once(INSTALLDIR.'/plugins/Autocomplete/autocomplete.php'); - class AutocompletePlugin extends Plugin { function __construct() @@ -40,6 +39,16 @@ class AutocompletePlugin extends Plugin parent::__construct(); } + function onAutoload($cls) + { + switch ($cls) + { + case 'AutocompleteAction': + require_once(INSTALLDIR.'/plugins/Autocomplete/autocomplete.php'); + return false; + } + } + function onEndShowScripts($action){ if (common_logged_in()) { $action->script('plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.pack.js'); diff --git a/plugins/Autocomplete/autocomplete.php b/plugins/Autocomplete/autocomplete.php index 379390ffd..9a30ba01d 100644 --- a/plugins/Autocomplete/autocomplete.php +++ b/plugins/Autocomplete/autocomplete.php @@ -23,6 +23,7 @@ * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> * @copyright 2008-2009 StatusNet, Inc. + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/Autocomplete/locale/Autocomplete.pot b/plugins/Autocomplete/locale/Autocomplete.pot new file mode 100644 index 000000000..c0274af85 --- /dev/null +++ b/plugins/Autocomplete/locale/Autocomplete.pot @@ -0,0 +1,24 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: AutocompletePlugin.php:79 +msgid "" +"The autocomplete plugin allows users to autocomplete screen names in @ " +"replies. When an \"@\" is typed into the notice text area, an autocomplete " +"box is displayed populated with the user's friend' screen names." +msgstr "" diff --git a/plugins/BitlyUrl/BitlyUrlPlugin.php b/plugins/BitlyUrl/BitlyUrlPlugin.php index f7f28b4d6..11e3c0b84 100644 --- a/plugins/BitlyUrl/BitlyUrlPlugin.php +++ b/plugins/BitlyUrl/BitlyUrlPlugin.php @@ -22,7 +22,7 @@ * @category Plugin * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> - * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/BitlyUrl/locale/BitlyUrl.pot b/plugins/BitlyUrl/locale/BitlyUrl.pot new file mode 100644 index 000000000..28023759a --- /dev/null +++ b/plugins/BitlyUrl/locale/BitlyUrl.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: BitlyUrlPlugin.php:60 +#, php-format +msgid "Uses <a href=\"http://%1$s/\">%1$s</a> URL-shortener service." +msgstr "" diff --git a/plugins/Blacklist/BlacklistPlugin.php b/plugins/Blacklist/BlacklistPlugin.php index fb8f7306f..63bffe2c6 100644 --- a/plugins/Blacklist/BlacklistPlugin.php +++ b/plugins/Blacklist/BlacklistPlugin.php @@ -49,26 +49,63 @@ class BlacklistPlugin extends Plugin public $urls = array(); public $canAdmin = true; - private $_nicknamePatterns = array(); - private $_urlPatterns = array(); + function _getNicknamePatterns() + { + $confNicknames = $this->_configArray('blacklist', 'nicknames'); + + $dbNicknames = Nickname_blacklist::getPatterns(); + + return array_merge($this->nicknames, + $confNicknames, + $dbNicknames); + } + + function _getUrlPatterns() + { + $confURLs = $this->_configArray('blacklist', 'urls'); + + $dbURLs = Homepage_blacklist::getPatterns(); + + return array_merge($this->urls, + $confURLs, + $dbURLs); + } /** - * Initialize the plugin + * Database schema setup * - * @return void + * @return boolean hook value */ - function initialize() + function onCheckSchema() { - $confNicknames = $this->_configArray('blacklist', 'nicknames'); - - $this->_nicknamePatterns = array_merge($this->nicknames, - $confNicknames); - - $confURLs = $this->_configArray('blacklist', 'urls'); + $schema = Schema::get(); + + // For storing blacklist patterns for nicknames + + $schema->ensureTable('nickname_blacklist', + array(new ColumnDef('pattern', + 'varchar', + 255, + false, + 'PRI'), + new ColumnDef('created', + 'datetime', + null, + false))); + + $schema->ensureTable('homepage_blacklist', + array(new ColumnDef('pattern', + 'varchar', + 255, + false, + 'PRI'), + new ColumnDef('created', + 'datetime', + null, + false))); - $this->_urlPatterns = array_merge($this->urls, - $confURLs); + return true; } /** @@ -222,9 +259,10 @@ class BlacklistPlugin extends Plugin private function _checkUrl($url) { - foreach ($this->_urlPatterns as $pattern) { - common_debug("Checking $url against $pattern"); - if (preg_match("/$pattern/", $url)) { + $patterns = $this->_getUrlPatterns(); + + foreach ($patterns as $pattern) { + if ($pattern != '' && preg_match("/$pattern/", $url)) { return false; } } @@ -244,9 +282,10 @@ class BlacklistPlugin extends Plugin private function _checkNickname($nickname) { - foreach ($this->_nicknamePatterns as $pattern) { - common_debug("Checking $nickname against $pattern"); - if (preg_match("/$pattern/", $nickname)) { + $patterns = $this->_getNicknamePatterns(); + + foreach ($patterns as $pattern) { + if ($pattern != '' && preg_match("/$pattern/", $nickname)) { return false; } } @@ -280,6 +319,10 @@ class BlacklistPlugin extends Plugin { switch (strtolower($cls)) { + case 'nickname_blacklist': + case 'homepage_blacklist': + include_once INSTALLDIR.'/plugins/Blacklist/'.ucfirst($cls).'.php'; + return false; case 'blacklistadminpanelaction': $base = strtolower(mb_substr($cls, 0, -6)); include_once INSTALLDIR.'/plugins/Blacklist/'.$base.'.php'; @@ -391,20 +434,14 @@ class BlacklistPlugin extends Plugin function onEndDeleteUser($action, $user) { - common_debug("Action args: " . print_r($action->args, true)); - if ($action->boolean('blacklisthomepage')) { $pattern = $action->trimmed('blacklisthomepagepattern'); - $confURLs = $this->_configArray('blacklist', 'urls'); - $confURLs[] = $pattern; - Config::save('blacklist', 'urls', implode("\r\n", $confURLs)); + Homepage_blacklist::ensurePattern($pattern); } if ($action->boolean('blacklistnickname')) { $pattern = $action->trimmed('blacklistnicknamepattern'); - $confNicknames = $this->_configArray('blacklist', 'nicknames'); - $confNicknames[] = $pattern; - Config::save('blacklist', 'nicknames', implode("\r\n", $confNicknames)); + Nickname_blacklist::ensurePattern($pattern); } return true; diff --git a/plugins/Blacklist/Homepage_blacklist.php b/plugins/Blacklist/Homepage_blacklist.php new file mode 100644 index 000000000..ec89ee4bd --- /dev/null +++ b/plugins/Blacklist/Homepage_blacklist.php @@ -0,0 +1,189 @@ +<?php +/** + * Data class for homepage blacklisting + * + * PHP version 5 + * + * @category Data + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * 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/>. + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +/** + * Data class for Homepage blacklist + * + * @category Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class Homepage_blacklist extends Memcached_DataObject +{ + public $__table = 'homepage_blacklist'; // table name + public $pattern; // string pattern + public $created; // datetime + + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup (usually 'user_id' for this class) + * @param mixed $v Value to lookup + * + * @return Homepage_blacklist object found, or null for no hits + * + */ + + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('Homepage_blacklist', $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array('pattern' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has; this function + * defines them. + * + * @return array key definitions + */ + + function keys() + { + return array_keys($this->keyTypes()); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. + * + * @return array key definitions + */ + + function keyTypes() + { + return array('pattern' => 'K'); + } + + /** + * Return a list of patterns to check + * + * @return array string patterns to check + */ + + static function getPatterns() + { + $patterns = self::cacheGet('homepage_blacklist:patterns'); + + if ($patterns === false) { + + $patterns = array(); + + $nb = new Homepage_blacklist(); + + $nb->find(); + + while ($nb->fetch()) { + $patterns[] = $nb->pattern; + } + + self::cacheSet('homepage_blacklist:patterns', $patterns); + } + + return $patterns; + } + + /** + * Save new list of patterns + * + * @return array of patterns to check + */ + + static function saveNew($newPatterns) + { + $oldPatterns = self::getPatterns(); + + // Delete stuff that's old that not in new + + $toDelete = array_diff($oldPatterns, $newPatterns); + + // Insert stuff that's in new and not in old + + $toInsert = array_diff($newPatterns, $oldPatterns); + + foreach ($toDelete as $pattern) { + $nb = Homepage_blacklist::staticGet('pattern', $pattern); + if (!empty($nb)) { + $nb->delete(); + } + } + + foreach ($toInsert as $pattern) { + $nb = new Homepage_blacklist(); + $nb->pattern = $pattern; + $nb->created = common_sql_now(); + $nb->insert(); + } + + self::blow('homepage_blacklist:patterns'); + } + + static function ensurePattern($pattern) + { + $hb = Homepage_blacklist::staticGet('pattern', $pattern); + + if (empty($nb)) { + $hb = new Homepage_blacklist(); + $hb->pattern = $pattern; + $hb->created = common_sql_now(); + $hb->insert(); + self::blow('homepage_blacklist:patterns'); + } + } +} diff --git a/plugins/Blacklist/Nickname_blacklist.php b/plugins/Blacklist/Nickname_blacklist.php new file mode 100644 index 000000000..e8545292d --- /dev/null +++ b/plugins/Blacklist/Nickname_blacklist.php @@ -0,0 +1,180 @@ +<?php +/** + * Data class for nickname blacklisting + * + * PHP version 5 + * + * @category Data + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * 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/>. + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +/** + * Data class for Nickname blacklist + * + * @category Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class Nickname_blacklist extends Memcached_DataObject +{ + public $__table = 'nickname_blacklist'; // table name + public $pattern; // string pattern + public $created; // datetime + + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup + * @param mixed $v Value to lookup + * + * @return Nickname_blacklist object found, or null for no hits + * + */ + + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('Nickname_blacklist', $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * @return array array of column definitions + */ + + function table() + { + return array('pattern' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); + } + + /** + * return key definitions for DB_DataObject + * + * @return array key definitions + */ + + function keys() + { + return array_keys($this->keyTypes()); + } + + /** + * return key definitions for Memcached_DataObject + * + * @return array key definitions + */ + + function keyTypes() + { + return array('pattern' => 'K'); + } + + /** + * Return a list of patterns to check + * + * @return array string patterns to check + */ + + static function getPatterns() + { + $patterns = self::cacheGet('nickname_blacklist:patterns'); + + if ($patterns === false) { + + $patterns = array(); + + $nb = new Nickname_blacklist(); + + $nb->find(); + + while ($nb->fetch()) { + $patterns[] = $nb->pattern; + } + + self::cacheSet('nickname_blacklist:patterns', $patterns); + } + + return $patterns; + } + + /** + * Save new list of patterns + * + * @return array of patterns to check + */ + + static function saveNew($newPatterns) + { + $oldPatterns = self::getPatterns(); + + // Delete stuff that's old that not in new + + $toDelete = array_diff($oldPatterns, $newPatterns); + + // Insert stuff that's in new and not in old + + $toInsert = array_diff($newPatterns, $oldPatterns); + + foreach ($toDelete as $pattern) { + $nb = Nickname_blacklist::staticGet('pattern', $pattern); + if (!empty($nb)) { + $nb->delete(); + } + } + + foreach ($toInsert as $pattern) { + $nb = new Nickname_blacklist(); + $nb->pattern = $pattern; + $nb->created = common_sql_now(); + $nb->insert(); + } + + self::blow('nickname_blacklist:patterns'); + } + + static function ensurePattern($pattern) + { + $nb = Nickname_blacklist::staticGet('pattern', $pattern); + + if (empty($nb)) { + $nb = new Nickname_blacklist(); + $nb->pattern = $pattern; + $nb->created = common_sql_now(); + $nb->insert(); + self::blow('nickname_blacklist:patterns'); + } + } +} diff --git a/plugins/Blacklist/blacklistadminpanel.php b/plugins/Blacklist/blacklistadminpanel.php index 98d07080d..4289dec1b 100644 --- a/plugins/Blacklist/blacklistadminpanel.php +++ b/plugins/Blacklist/blacklistadminpanel.php @@ -88,37 +88,25 @@ class BlacklistadminpanelAction extends AdminPanelAction function saveSettings() { - static $settings = array( - 'blacklist' => array('nicknames', 'urls'), - ); + $nickPatterns = $this->splitPatterns($this->trimmed('blacklist-nicknames')); + Nickname_blacklist::saveNew($nickPatterns); - $values = array(); + $urlPatterns = $this->splitPatterns($this->trimmed('blacklist-urls')); + Homepage_blacklist::saveNew($urlPatterns); - foreach ($settings as $section => $parts) { - foreach ($parts as $setting) { - $values[$section][$setting] = $this->trimmed("$section-$setting"); - } - } - - // This throws an exception on validation errors - - $this->validate($values); - - // assert(all values are valid); - - $config = new Config(); - - $config->query('BEGIN'); + return; + } - foreach ($settings as $section => $parts) { - foreach ($parts as $setting) { - Config::save($section, $setting, $values[$section][$setting]); + protected function splitPatterns($text) + { + $patterns = array(); + foreach (explode("\n", $text) as $raw) { + $trimmed = trim($raw); + if ($trimmed != '') { + $patterns[] = $trimmed; } } - - $config->query('COMMIT'); - - return; + return $patterns; } /** @@ -191,14 +179,19 @@ class BlacklistAdminPanelForm extends Form $this->out->elementStart('ul', 'form_data'); $this->out->elementStart('li'); + + $nickPatterns = Nickname_blacklist::getPatterns(); + $this->out->textarea('blacklist-nicknames', _m('Nicknames'), - common_config('blacklist', 'nicknames'), + implode("\r\n", $nickPatterns), _('Patterns of nicknames to block, one per line')); $this->out->elementEnd('li'); + $urlPatterns = Homepage_blacklist::getPatterns(); + $this->out->elementStart('li'); $this->out->textarea('blacklist-urls', _m('URLs'), - common_config('blacklist', 'urls'), + implode("\r\n", $urlPatterns), _('Patterns of URLs to block, one per line')); $this->out->elementEnd('li'); diff --git a/plugins/Blacklist/locale/Blacklist.pot b/plugins/Blacklist/locale/Blacklist.pot new file mode 100644 index 000000000..90eda0941 --- /dev/null +++ b/plugins/Blacklist/locale/Blacklist.pot @@ -0,0 +1,54 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: BlacklistPlugin.php:153 +#, php-format +msgid "You may not register with homepage '%s'" +msgstr "" + +#: BlacklistPlugin.php:163 +#, php-format +msgid "You may not register with nickname '%s'" +msgstr "" + +#: BlacklistPlugin.php:188 +#, php-format +msgid "You may not use homepage '%s'" +msgstr "" + +#: BlacklistPlugin.php:198 +#, php-format +msgid "You may not use nickname '%s'" +msgstr "" + +#: BlacklistPlugin.php:242 +#, php-format +msgid "You may not use url '%s' in notices" +msgstr "" + +#: BlacklistPlugin.php:351 +msgid "Keep a blacklist of forbidden nickname and URL patterns." +msgstr "" + +#: blacklistadminpanel.php:185 +msgid "Nicknames" +msgstr "" + +#: blacklistadminpanel.php:193 +msgid "URLs" +msgstr "" diff --git a/plugins/CasAuthentication/CasAuthenticationPlugin.php b/plugins/CasAuthentication/CasAuthenticationPlugin.php index 203e5fe42..1662db3eb 100644 --- a/plugins/CasAuthentication/CasAuthenticationPlugin.php +++ b/plugins/CasAuthentication/CasAuthenticationPlugin.php @@ -22,7 +22,7 @@ * @category Plugin * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> - * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/CasAuthentication/locale/CasAuthentication.pot b/plugins/CasAuthentication/locale/CasAuthentication.pot new file mode 100644 index 000000000..20a2bf233 --- /dev/null +++ b/plugins/CasAuthentication/locale/CasAuthentication.pot @@ -0,0 +1,35 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: CasAuthenticationPlugin.php:82 +msgid "CAS" +msgstr "" + +#: CasAuthenticationPlugin.php:83 +msgid "Login or register with CAS" +msgstr "" + +#: CasAuthenticationPlugin.php:150 +msgid "" +"The CAS Authentication plugin allows for StatusNet to handle authentication " +"through CAS (Central Authentication Service)." +msgstr "" + +#: caslogin.php:28 +msgid "Already logged in." +msgstr "" diff --git a/plugins/ClientSideShorten/ClientSideShortenPlugin.php b/plugins/ClientSideShorten/ClientSideShortenPlugin.php new file mode 100644 index 000000000..57f5ad89e --- /dev/null +++ b/plugins/ClientSideShorten/ClientSideShortenPlugin.php @@ -0,0 +1,79 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Plugin to enable client side url shortening in the status box + * + * 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 Plugin + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once(INSTALLDIR.'/plugins/ClientSideShorten/shorten.php'); + +class ClientSideShortenPlugin extends Plugin +{ + function __construct() + { + parent::__construct(); + } + + function onAutoload($cls) + { + switch ($cls) + { + case 'ShortenAction': + require_once(INSTALLDIR.'/plugins/ClientSideShorten/shorten.php'); + return false; + } + } + + function onEndShowScripts($action){ + $action->inlineScript('var Notice_maxContent = ' . Notice::maxContent()); + if (common_logged_in()) { + $action->script('plugins/ClientSideShorten/shorten.js'); + } + } + + function onRouterInitialized($m) + { + if (common_logged_in()) { + $m->connect('plugins/ClientSideShorten/shorten', array('action'=>'shorten')); + } + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'Shorten', + 'version' => STATUSNET_VERSION, + 'author' => 'Craig Andrews', + 'homepage' => 'http://status.net/wiki/Plugin:ClientSideShorten', + 'rawdescription' => + _m('ClientSideShorten causes the web interface\'s notice form to automatically shorten urls as they entered, and before the notice is submitted.')); + return true; + } + +} + diff --git a/plugins/ClientSideShorten/README b/plugins/ClientSideShorten/README new file mode 100644 index 000000000..e6524c9c7 --- /dev/null +++ b/plugins/ClientSideShorten/README @@ -0,0 +1,6 @@ +ClientSideShorten causes the web interface's notice form to automatically shorten urls as they entered, and before the notice is submitted. + +Installation +============ +Add "addPlugin('ClientSideShorten');" to the bottom of your config.php +That's it! diff --git a/plugins/ClientSideShorten/locale/ClientSideShorten.pot b/plugins/ClientSideShorten/locale/ClientSideShorten.pot new file mode 100644 index 000000000..83caff322 --- /dev/null +++ b/plugins/ClientSideShorten/locale/ClientSideShorten.pot @@ -0,0 +1,27 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ClientSideShortenPlugin.php:74 +msgid "" +"ClientSideShorten causes the web interface's notice form to automatically " +"shorten urls as they entered, and before the notice is submitted." +msgstr "" + +#: shorten.php:55 +msgid "'text' argument must be specified." +msgstr "" diff --git a/plugins/ClientSideShorten/shorten.js b/plugins/ClientSideShorten/shorten.js new file mode 100644 index 000000000..856c7f05f --- /dev/null +++ b/plugins/ClientSideShorten/shorten.js @@ -0,0 +1,66 @@ +//wrap everything in a self-executing anonymous function to avoid conflicts +(function(){ + + // smart(x) from Paul Irish + // http://paulirish.com/2009/throttled-smartresize-jquery-event-handler/ + + (function($,sr){ + + // debouncing function from John Hann + // http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/ + var debounce = function (func, threshold, execAsap) { + var timeout; + + return function debounced () { + var obj = this, args = arguments; + function delayed () { + if (!execAsap) + func.apply(obj, args); + timeout = null; + }; + + if (timeout) + clearTimeout(timeout); + else if (execAsap) + func.apply(obj, args); + + timeout = setTimeout(delayed, threshold || 100); + }; + } + jQuery.fn[sr] = function(fn){ return fn ? this.bind('keypress', debounce(fn, 1000)) : this.trigger(sr); }; + + })(jQuery,'smartkeypress'); + + function shorten() + { + $noticeDataText = $('#'+SN.C.S.NoticeDataText); + if(Notice_maxContent > 0 && $noticeDataText.val().length > Notice_maxContent){ + var original = $noticeDataText.val(); + shortenAjax = $.ajax({ + url: $('address .url')[0].href+'/plugins/ClientSideShorten/shorten', + data: { text: $noticeDataText.val() }, + dataType: 'text', + success: function(data) { + if(original == $noticeDataText.val()) { + $noticeDataText.val(data).keyup(); + } + } + }); + } + } + + $(document).ready(function(){ + $noticeDataText = $('#'+SN.C.S.NoticeDataText); + $noticeDataText.smartkeypress(function(e){ + //if(typeof(shortenAjax) !== 'undefined') shortenAjax.abort(); + if(e.charCode == '32') { + shorten(); + } + }); + $noticeDataText.bind('paste', function() { + //if(typeof(shortenAjax) !== 'undefined') shortenAjax.abort(); + setTimeout(shorten,1); + }); + }); + +})(); diff --git a/plugins/ClientSideShorten/shorten.php b/plugins/ClientSideShorten/shorten.php new file mode 100644 index 000000000..f67cbf3b2 --- /dev/null +++ b/plugins/ClientSideShorten/shorten.php @@ -0,0 +1,69 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * List users for autocompletion + * + * 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 Plugin + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2008-2009 StatusNet, Inc. + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Shorten all URLs in a string + * + * @category Plugin + * @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/ + */ + +class ShortenAction extends Action +{ + private $text; + + function prepare($args) + { + parent::prepare($args); + $this->groups=array(); + $this->users=array(); + $this->text = $this->arg('text'); + if(is_null($this->text)){ + throw new ClientException(_m('\'text\' argument must be specified.')); + } + return true; + } + + function handle($args) + { + parent::handle($args); + header('Content-Type: text/plain'); + $shortened_text = common_shorten_links($this->text); + print $shortened_text; + } +} + diff --git a/plugins/DirectionDetector/DirectionDetectorPlugin.php b/plugins/DirectionDetector/DirectionDetectorPlugin.php new file mode 100644 index 000000000..b1362b166 --- /dev/null +++ b/plugins/DirectionDetector/DirectionDetectorPlugin.php @@ -0,0 +1,230 @@ +<?php +/** + * DirectionDetector plugin, detects notices with RTL content & sets RTL + * style for them. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Plugin + * @package StatusNet + * @author Behrooz shabani (everplays) - <behrooz@rock.com> + * @copyright 2009-2010 Behrooz shabani + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +define('DIRECTIONDETECTORPLUGIN_VERSION', '0.1.2'); + +class DirectionDetectorPlugin extends Plugin { + /** + * SN plugin API, here we will make changes on rendered column + * + * @param object $notice notice is going to be saved + */ + public function onStartNoticeSave(&$notice){ + if(!preg_match('/<span class="rtl">/', $notice->rendered) && self::isRTL($notice->content)) + $notice->rendered = '<span class="rtl">'.$notice->rendered.'</span>'; + return true; + } + + /** + * SN plugin API, here we will add css needed for modifiyed rendered + * + * @param + */ + public function onEndShowStatusNetStyles($xml){ + $xml->element('style', array('type' => 'text/css'), 'span.rtl {display:block;direction:rtl;text-align:right;float:right;width:490px;} .notice .author {float:left}'); + } + /** + * checks that passed string is a RTL language or not + * + * @param string $str String to be checked + */ + public static function isRTL($str){ + self::getClearText($str); + if( is_array($cc = self::utf8ToUnicode(mb_substr($str, 0, 1, 'utf-8'))) ) + $cc = $cc[0]; + else + return false; + if($cc>=1536 && $cc<=1791) // Arabic, Persian, Urdu, Kurdish, ... + return true; + if($cc>=65136 && $cc<=65279) // Arabic peresent 2 + return true; + if($cc>=64336 && $cc<=65023) // Arabic peresent 1 + return true; + if($cc>=1424 && $cc<=1535) // Hebrew + return true; + if($cc>=64256 && $cc<=64335) // Hebrew peresent + return true; + if($cc>=1792 && $cc<=1871) // Syriac + return true; + if($cc>=1920 && $cc<=1983) // Thaana + return true; + if($cc>=1984 && $cc<=2047) // NKo + return true; + if($cc>=11568 && $cc<=11647) // Tifinagh + return true; + return false; + } + + /** + * clears text from replies, tags, groups, repeats & whitespaces + * + * @param string &$str string to be cleared + */ + private static function getClearText(&$str){ + $str = preg_replace('/@[^ ]+|![^ ]+|#[^ ]+/u', '', $str); // reply, tag, group + $str = preg_replace('/^RT[: ]{1}| RT | RT: |^RD[: ]{1}| RD | RD: |[♺♻:]/u', '', $str); // redent, retweet + $str = preg_replace("/[ \r\t\n]+/", ' ', trim($str)); // remove spaces + } + + /** + * Takes a UTF-8 string and returns an array of ints representing the + * Unicode characters. Astral planes are supported i.e. the ints in the + * output can be > 0xFFFF. Occurrances of the BOM are ignored. Surrogates + * are not allowed. ### modified ### returns first character code + * + * Returns false if the input string isn't a valid UTF-8 octet sequence. + */ + private static function utf8ToUnicode($str){ + $mState = 0; // cached expected number of octets after the current octet + // until the beginning of the next UTF8 character sequence + $mUcs4 = 0; // cached Unicode character + $mBytes = 1; // cached expected number of octets in the current sequence + $out = array(); + $len = strlen($str); + + for($i = 0; $i < $len; $i++) { + $in = ord($str{$i}); + if (0 == $mState) { + // When mState is zero we expect either a US-ASCII character or a + // multi-octet sequence. + if (0 == (0x80 & ($in))) { + // US-ASCII, pass straight through. + $out[] = $in; + $mBytes = 1; + } elseif (0xC0 == (0xE0 & ($in))) { + // First octet of 2 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x1F) << 6; + $mState = 1; + $mBytes = 2; + } elseif (0xE0 == (0xF0 & ($in))) { + // First octet of 3 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x0F) << 12; + $mState = 2; + $mBytes = 3; + } elseif (0xF0 == (0xF8 & ($in))) { + // First octet of 4 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x07) << 18; + $mState = 3; + $mBytes = 4; + } elseif (0xF8 == (0xFC & ($in))) { + /* First octet of 5 octet sequence. + * + * This is illegal because the encoded codepoint must be either + * (a) not the shortest form or + * (b) outside the Unicode range of 0-0x10FFFF. + * Rather than trying to resynchronize, we will carry on until the end + * of the sequence and let the later error handling code catch it. + */ + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x03) << 24; + $mState = 4; + $mBytes = 5; + } elseif (0xFC == (0xFE & ($in))) { + // First octet of 6 octet sequence, see comments for 5 octet sequence. + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 1) << 30; + $mState = 5; + $mBytes = 6; + } else { + /* Current octet is neither in the US-ASCII range nor a legal first + * octet of a multi-octet sequence. + */ + return false; + } + } else { + // When mState is non-zero, we expect a continuation of the multi-octet + // sequence + if (0x80 == (0xC0 & ($in))) { + // Legal continuation. + $shift = ($mState - 1) * 6; + $tmp = $in; + $tmp = ($tmp & 0x0000003F) << $shift; + $mUcs4 |= $tmp; + if (0 == --$mState) { + /* End of the multi-octet sequence. mUcs4 now contains the final + * Unicode codepoint to be output + * + * Check for illegal sequences and codepoints. + */ + // From Unicode 3.1, non-shortest form is illegal + if ( + ((2 == $mBytes) && ($mUcs4 < 0x0080)) || + ((3 == $mBytes) && ($mUcs4 < 0x0800)) || + ((4 == $mBytes) && ($mUcs4 < 0x10000)) || + (4 < $mBytes) || + // From Unicode 3.2, surrogate characters are illegal + (($mUcs4 & 0xFFFFF800) == 0xD800) || + // Codepoints outside the Unicode range are illegal + ($mUcs4 > 0x10FFFF) + ){ + return false; + } + if (0xFEFF != $mUcs4) { + $out[] = $mUcs4; + } + //initialize UTF8 cache + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + } + } else { + /* ((0xC0 & (*in) != 0x80) && (mState != 0)) + * + * Incomplete multi-octet sequence. + */ + return false; + } + } + } + return $out; + } + + /** + * plugin details + */ + function onPluginVersion(&$versions){ + $versions[] = array( + 'name' => 'Direction detector', + 'version' => DIRECTIONDETECTORPLUGIN_VERSION, + 'author' => 'Behrooz Shabani', + // TRANS: Direction detector plugin description. + 'rawdescription' => _m('Shows notices with right-to-left content in correct direction.') + ); + return true; + } +} + +/* +// Example: +var_dump(DirectionDetectorPlugin::isRTL('RT @everplays ♺: دادگاه به دليل عدم Øضور وکلای متهمان بنا بر اصل ١٣٥ قانون اساسی غير قانونی است')); // true +*/ diff --git a/plugins/DirectionDetector/locale/DirectionDetector.pot b/plugins/DirectionDetector/locale/DirectionDetector.pot new file mode 100644 index 000000000..44bbcca4d --- /dev/null +++ b/plugins/DirectionDetector/locale/DirectionDetector.pot @@ -0,0 +1,21 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-05-08 22:32+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: DirectionDetectorPlugin.php:222 +msgid "Shows notices with right-to-left content in correct direction." +msgstr "" diff --git a/plugins/DirectionDetector/locale/nl/LC_MESSAGES/DirectionDetector.po b/plugins/DirectionDetector/locale/nl/LC_MESSAGES/DirectionDetector.po new file mode 100644 index 000000000..e8dae6ea8 --- /dev/null +++ b/plugins/DirectionDetector/locale/nl/LC_MESSAGES/DirectionDetector.po @@ -0,0 +1,22 @@ +# Translation of StatusNet plugin DirectionDetector to Dutch +# +# Author@translatewiki.net: Siebrand +# -- +# This file is distributed under the same license as the StatusNet package. +# +msgid "" +msgstr "" +"Project-Id-Version: StatusNet\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-05-08 22:32+0000\n" +"PO-Revision-Date: 2010-05-08 23:32+0100\n" +"Last-Translator: Siebrand Mazeland <s.mazeland@xs4all.nl>\n" +"Language-Team: Dutch\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: DirectionDetectorPlugin.php:222 +msgid "Geeft mededelingen met rechts-naar-linksinhoud weer in de juiste richting." +msgstr "" diff --git a/plugins/EmailAuthentication/EmailAuthenticationPlugin.php b/plugins/EmailAuthentication/EmailAuthenticationPlugin.php index 406c00073..4c018537b 100644 --- a/plugins/EmailAuthentication/EmailAuthenticationPlugin.php +++ b/plugins/EmailAuthentication/EmailAuthenticationPlugin.php @@ -22,7 +22,7 @@ * @category Plugin * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> - * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/EmailAuthentication/locale/EmailAuthentication.pot b/plugins/EmailAuthentication/locale/EmailAuthentication.pot new file mode 100644 index 000000000..d945e2537 --- /dev/null +++ b/plugins/EmailAuthentication/locale/EmailAuthentication.pot @@ -0,0 +1,23 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: EmailAuthenticationPlugin.php:61 +msgid "" +"The Email Authentication plugin allows users to login using their email " +"address." +msgstr "" diff --git a/plugins/Facebook/FBConnectAuth.php b/plugins/Facebook/FBConnectAuth.php index 51bfc3865..8eba7fc13 100644 --- a/plugins/Facebook/FBConnectAuth.php +++ b/plugins/Facebook/FBConnectAuth.php @@ -138,6 +138,11 @@ class FBConnectauthAction extends Action parent::showPage(); } + /** + * @fixme much of this duplicates core code, which is very fragile. + * Should probably be replaced with an extensible mini version of + * the core registration form. + */ function showContent() { if (!empty($this->message_text)) { @@ -159,10 +164,15 @@ class FBConnectauthAction extends Action 'name' => 'license', 'value' => 'true')); $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license')); - $this->text(_m('My text and files are available under ')); - $this->element('a', array('href' => common_config('license', 'url')), - common_config('license', 'title')); - $this->text(_m(' except this private data: password, email address, IM address, phone number.')); + $message = _('My text and files are available under %s ' . + 'except this private data: password, ' . + 'email address, IM address, and phone number.'); + $link = '<a href="' . + htmlspecialchars(common_config('license', 'url')) . + '">' . + htmlspecialchars(common_config('license', 'title')) . + '</a>'; + $this->raw(sprintf(htmlspecialchars($message), $link)); $this->elementEnd('label'); $this->elementEnd('li'); $this->elementEnd('ul'); diff --git a/plugins/Facebook/FacebookPlugin.php b/plugins/Facebook/FacebookPlugin.php index 5dba73a5d..19989a952 100644 --- a/plugins/Facebook/FacebookPlugin.php +++ b/plugins/Facebook/FacebookPlugin.php @@ -585,7 +585,7 @@ class FacebookPlugin extends Plugin function onStartEnqueueNotice($notice, &$transports) { - if (self::hasKeys()) { + if (self::hasKeys() && $notice->isLocal()) { array_push($transports, 'facebook'); } return true; diff --git a/plugins/Facebook/README b/plugins/Facebook/README index 14c1d3241..532f1d82e 100644 --- a/plugins/Facebook/README +++ b/plugins/Facebook/README @@ -38,11 +38,11 @@ editor or write them down. In Facebook's application editor, specify the following URLs for your app: -- Canvas Callback URL : http://example.net/mublog/facebook/app/ -- Post-Remove Callback URL: http://example.net/mublog/facebook/app/remove -- Post-Add Redirect URL : http://apps.facebook.com/yourapp/ -- Canvas Page URL : http://apps.facebook.com/yourapp/ -- Connect URL : http://example.net/mublog/ +- Canvas Callback URL : http://example.net/mublog/facebook/app/ +- Post-Remove Callback URL : http://example.net/mublog/facebook/app/remove +- Post-Authorize Redirect URL : http://apps.facebook.com/yourapp/ +- Canvas Page URL : http://apps.facebook.com/yourapp/ +- Connect URL : http://example.net/mublog/ *** ATTENTION *** These URLs have changed slightly since StatusNet version 0.8.1, diff --git a/plugins/Facebook/facebook/facebook.php b/plugins/Facebook/facebook/facebook.php index 440706cbc..76696c1d5 100644 --- a/plugins/Facebook/facebook/facebook.php +++ b/plugins/Facebook/facebook/facebook.php @@ -45,7 +45,9 @@ class Facebook { public $user; public $profile_user; public $canvas_user; + public $ext_perms = array(); protected $base_domain; + /* * Create a Facebook client like this: * @@ -104,17 +106,17 @@ class Facebook { * * For nitty-gritty details of when each of these is used, check out * http://wiki.developers.facebook.com/index.php/Verifying_The_Signature - * - * @param bool resolve_auth_token convert an auth token into a session */ - public function validate_fb_params($resolve_auth_token=true) { + public function validate_fb_params() { $this->fb_params = $this->get_valid_fb_params($_POST, 48 * 3600, 'fb_sig'); // note that with preload FQL, it's possible to receive POST params in // addition to GET, so use a different prefix to differentiate them if (!$this->fb_params) { $fb_params = $this->get_valid_fb_params($_GET, 48 * 3600, 'fb_sig'); - $fb_post_params = $this->get_valid_fb_params($_POST, 48 * 3600, 'fb_post_sig'); + $fb_post_params = $this->get_valid_fb_params($_POST, + 48 * 3600, // 48 hours + 'fb_post_sig'); $this->fb_params = array_merge($fb_params, $fb_post_params); } @@ -128,6 +130,9 @@ class Facebook { $this->fb_params['canvas_user'] : null; $this->base_domain = isset($this->fb_params['base_domain']) ? $this->fb_params['base_domain'] : null; + $this->ext_perms = isset($this->fb_params['ext_perms']) ? + explode(',', $this->fb_params['ext_perms']) + : array(); if (isset($this->fb_params['session_key'])) { $session_key = $this->fb_params['session_key']; @@ -141,13 +146,11 @@ class Facebook { $this->set_user($user, $session_key, $expires); - } - // if no Facebook parameters were found in the GET or POST variables, - // then fall back to cookies, which may have cached user information - // Cookies are also used to receive session data via the Javascript API - else if ($cookies = - $this->get_valid_fb_params($_COOKIE, null, $this->api_key)) { - + } else if ($cookies = + $this->get_valid_fb_params($_COOKIE, null, $this->api_key)) { + // if no Facebook parameters were found in the GET or POST variables, + // then fall back to cookies, which may have cached user information + // Cookies are also used to receive session data via the Javascript API $base_domain_cookie = 'base_domain_' . $this->api_key; if (isset($_COOKIE[$base_domain_cookie])) { $this->base_domain = $_COOKIE[$base_domain_cookie]; @@ -160,25 +163,6 @@ class Facebook { $cookies['session_key'], $expires); } - // finally, if we received no parameters, but the 'auth_token' GET var - // is present, then we are in the middle of auth handshake, - // so go ahead and create the session - else if ($resolve_auth_token && isset($_GET['auth_token']) && - $session = $this->do_get_session($_GET['auth_token'])) { - if ($this->generate_session_secret && - !empty($session['secret'])) { - $session_secret = $session['secret']; - } - - if (isset($session['base_domain'])) { - $this->base_domain = $session['base_domain']; - } - - $this->set_user($session['uid'], - $session['session_key'], - $session['expires'], - isset($session_secret) ? $session_secret : null); - } return !empty($this->fb_params); } @@ -309,11 +293,28 @@ class Facebook { // require_add and require_install have been removed. // see http://developer.facebook.com/news.php?blog=1&story=116 for more details - public function require_login() { - if ($user = $this->get_loggedin_user()) { + public function require_login($required_permissions = '') { + $user = $this->get_loggedin_user(); + $has_permissions = true; + + if ($required_permissions) { + $this->require_frame(); + $permissions = array_map('trim', explode(',', $required_permissions)); + foreach ($permissions as $permission) { + if (!in_array($permission, $this->ext_perms)) { + $has_permissions = false; + break; + } + } + } + + if ($user && $has_permissions) { return $user; } - $this->redirect($this->get_login_url(self::current_url(), $this->in_frame())); + + $this->redirect( + $this->get_login_url(self::current_url(), $this->in_frame(), + $required_permissions)); } public function require_frame() { @@ -342,10 +343,11 @@ class Facebook { return $page . '?' . http_build_query($params); } - public function get_login_url($next, $canvas) { + public function get_login_url($next, $canvas, $req_perms = '') { $page = self::get_facebook_url().'/login.php'; - $params = array('api_key' => $this->api_key, - 'v' => '1.0'); + $params = array('api_key' => $this->api_key, + 'v' => '1.0', + 'req_perms' => $req_perms); if ($next) { $params['next'] = $next; diff --git a/plugins/Facebook/facebook/facebookapi_php5_restlib.php b/plugins/Facebook/facebook/facebookapi_php5_restlib.php index fa1088cd0..e249a326b 100755 --- a/plugins/Facebook/facebook/facebookapi_php5_restlib.php +++ b/plugins/Facebook/facebook/facebookapi_php5_restlib.php @@ -569,7 +569,7 @@ function toggleDisplay(id, type) { return $this->call_method('facebook.events.invite', array('eid' => $eid, 'uids' => $uids, - 'personal_message', $personal_message)); + 'personal_message' => $personal_message)); } /** @@ -1350,53 +1350,6 @@ function toggleDisplay(id, type) { ); } - /** - * Dashboard API - */ - - /** - * Set the news for the specified user. - * - * @param int $uid The user for whom you are setting news for - * @param string $news Text of news to display - * - * @return bool Success - */ - public function dashboard_setNews($uid, $news) { - return $this->call_method('facebook.dashboard.setNews', - array('uid' => $uid, - 'news' => $news) - ); - } - - /** - * Get the current news of the specified user. - * - * @param int $uid The user to get the news of - * - * @return string The text of the current news for the user - */ - public function dashboard_getNews($uid) { - return json_decode( - $this->call_method('facebook.dashboard.getNews', - array('uid' => $uid) - ), true); - } - - /** - * Set the news for the specified user. - * - * @param int $uid The user you are clearing the news of - * - * @return bool Success - */ - public function dashboard_clearNews($uid) { - return $this->call_method('facebook.dashboard.clearNews', - array('uid' => $uid) - ); - } - - /** * Creates a note with the specified title and content. @@ -2005,7 +1958,7 @@ function toggleDisplay(id, type) { * @return array A list of strings describing any compile errors for the * submitted FBML */ - function profile_setFBML($markup, + public function profile_setFBML($markup, $uid=null, $profile='', $profile_action='', @@ -3267,9 +3220,8 @@ function toggleDisplay(id, type) { } else { $get['v'] = '1.0'; } - if (isset($this->use_ssl_resources) && - $this->use_ssl_resources) { - $post['return_ssl_resources'] = true; + if (isset($this->use_ssl_resources)) { + $post['return_ssl_resources'] = (bool) $this->use_ssl_resources; } return array($get, $post); } diff --git a/plugins/Facebook/facebooksettings.php b/plugins/Facebook/facebooksettings.php index 766d0e199..f94a346b5 100644 --- a/plugins/Facebook/facebooksettings.php +++ b/plugins/Facebook/facebooksettings.php @@ -54,22 +54,11 @@ class FacebooksettingsAction extends FacebookAction $noticesync = $this->boolean('noticesync'); $replysync = $this->boolean('replysync'); - $prefix = $this->trimmed('prefix'); $original = clone($this->flink); $this->flink->set_flags($noticesync, false, $replysync, false); $result = $this->flink->update($original); - if ($prefix == '' || $prefix == '0') { - // Facebook bug: saving empty strings to prefs now fails - // http://bugs.developers.facebook.com/show_bug.cgi?id=7110 - $trimmed = $prefix . ' '; - } else { - $trimmed = substr($prefix, 0, 128); - } - $this->facebook->api_client->data_setUserPreference(FACEBOOK_NOTICE_PREFIX, - $trimmed); - if ($result === false) { $this->showForm(_m('There was a problem saving your sync preferences!')); } else { @@ -110,16 +99,6 @@ class FacebooksettingsAction extends FacebookAction $this->elementStart('li'); - $prefix = trim($this->facebook->api_client->data_getUserPreference(FACEBOOK_NOTICE_PREFIX)); - - $this->input('prefix', _m('Prefix'), - ($prefix) ? $prefix : null, - _m('A string to prefix notices with.')); - - $this->elementEnd('li'); - - $this->elementStart('li'); - $this->submit('save', _m('Save')); $this->elementEnd('li'); diff --git a/plugins/Facebook/facebookutil.php b/plugins/Facebook/facebookutil.php index ac532e18b..1290fed55 100644 --- a/plugins/Facebook/facebookutil.php +++ b/plugins/Facebook/facebookutil.php @@ -81,97 +81,251 @@ function isFacebookBound($notice, $flink) { function facebookBroadcastNotice($notice) { $facebook = getFacebook(); - $flink = Foreign_link::getByUserID($notice->profile_id, FACEBOOK_SERVICE); + $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)."); + // Check permissions + + common_debug( + 'FacebookPlugin - checking for publish_stream permission for user ' + . "$user->nickname ($user->id), Facebook UID: $fbuid" + ); + + // NOTE: $facebook->api_client->users_hasAppPermission('publish_stream', $fbuid) + // has been returning bogus results, so we're using FQL to check for + // publish_stream permission now + + $fql = "SELECT publish_stream FROM permissions WHERE uid = $fbuid"; + $result = $facebook->api_client->fql_query($fql); + + $canPublish = 0; + + if (!empty($result)) { + $canPublish = $result[0]['publish_stream']; + } + + if ($canPublish == 1) { + common_debug( + "FacebookPlugin - $user->nickname ($user->id), Facebook UID: $fbuid " + . 'has publish_stream permission.' + ); + } else { + common_debug( + "FacebookPlugin - $user->nickname ($user->id), Facebook UID: $fbuid " + . 'does NOT have publish_stream permission. Facebook ' + . 'returned: ' . var_export($result, true) + ); + } + + common_debug( + 'FacebookPlugin - checking for status_update permission for user ' + . "$user->nickname ($user->id), Facebook UID: $fbuid. " + ); + + $canUpdate = $facebook->api_client->users_hasAppPermission( + 'status_update', + $fbuid + ); + + if ($canUpdate == 1) { + common_debug( + "FacebookPlugin - $user->nickname ($user->id), Facebook UID: $fbuid " + . 'has status_update permission.' + ); } else { - $msg = "Not sending notice $notice->id to Facebook " . - "because user $user->nickname hasn't given the " . + common_debug( + "FacebookPlugin - $user->nickname ($user->id), Facebook UID: $fbuid " + .'does NOT have status_update permission. Facebook ' + . 'returned: ' . var_export($canPublish, true) + ); + } + + // Post to Facebook + + if ($notice->hasAttachments() && $canPublish == 1) { + publishStream($notice, $user, $fbuid); + } elseif ($canUpdate == 1 || $canPublish == 1) { + statusUpdate($notice, $user, $fbuid); + } else { + $msg = "FacebookPlugin - Not sending notice $notice->id to Facebook " . + "because user $user->nickname has not 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); + if ($canPublish == 1 || $canUpdate == 1) { + updateProfileBox($facebook, $flink, $notice, $user); } } catch (FacebookRestClientException $e) { + return handleFacebookError($e, $notice, $flink); + } + } - $code = $e->getCode(); - - $msg = "Facebook returned error code $code: " . - $e->getMessage() . ' - ' . - "Unable to update Facebook status (notice $notice->id) " . - "for $user->nickname (user id: $user->id)!"; + return true; +} - common_log(LOG_WARNING, $msg); +function handleFacebookError($e, $notice, $flink) +{ + $fbuid = $flink->foreign_id; + $user = $flink->getUser(); + $code = $e->getCode(); + $errmsg = $e->getMessage(); + + // XXX: Check for any others? + switch($code) { + case 100: // Invalid parameter + $msg = "FacebookPlugin - Facebook claims notice %d was posted with an invalid parameter (error code 100):" + . "\"%s\" (Notice details: nickname=%s, user ID=%d, Facebook ID=%d, notice content=\"%s\"). " + . "Removing notice from the Facebook queue for safety."; + common_log( + LOG_ERR, sprintf( + $msg, + $notice->id, + $errmsg, + $user->nickname, + $user->id, + $fbuid, + $notice->content + ) + ); + return true; + break; + case 200: // Permissions error + case 250: // Updating status requires the extended permission status_update + remove_facebook_app($flink); + return true; // dequeue + break; + case 341: // Feed action request limit reached + $msg = "FacebookPlugin - User %s (User ID=%d, Facebook ID=%d) has exceeded " + . "his/her limit for posting notices to Facebook today. Dequeuing " + . "notice %d."; + common_log( + LOG_INFO, sprintf( + $msg, + $user->nickname, + $user->id, + $fbuid, + $notice->id + ) + ); + // @fixme: We want to rety at a later time when the throttling has expired + // instead of just giving up. + return true; + break; + default: + $msg = "FacebookPlugin - Facebook returned an error we don't know how to deal with while trying to " + . "post notice %d. Error code: %d, error message: \"%s\". (Notice details: " + . "nickname=%s, user ID=%d, Facebook ID=%d, notice content=\"%s\"). Removing notice " + . "from the Facebook queue for safety."; + common_log( + LOG_ERR, sprintf( + $msg, + $notice->id, + $code, + $errmsg, + $user->nickname, + $user->id, + $fbuid, + $notice->content + ) + ); + return true; // dequeue + break; + } +} - if ($code == 100 || $code == 200 || $code == 250) { +function statusUpdate($notice, $user, $fbuid) +{ + common_debug( + "FacebookPlugin - Attempting to post notice $notice->id " + . "as a status update for $user->nickname ($user->id), " + . "Facebook UID: $fbuid" + ); - // 100 The account is 'inactive' (probably - this is not well documented) - // 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 + $facebook = getFacebook(); + $result = $facebook->api_client->users_setStatus( + $notice->content, + $fbuid, + false, + true + ); + + common_debug('Facebook returned: ' . var_export($result, true)); + + common_log( + LOG_INFO, + "FacebookPlugin - Posted notice $notice->id as a status " + . "update for $user->nickname ($user->id), " + . "Facebook UID: $fbuid" + ); +} - remove_facebook_app($flink); +function publishStream($notice, $user, $fbuid) +{ + common_debug( + "FacebookPlugin - Attempting to post notice $notice->id " + . "as stream item with attachment for $user->nickname ($user->id), " + . "Facebook UID: $fbuid" + ); - } else { + $fbattachment = format_attachments($notice->attachments()); - // Try sending again later. + $facebook = getFacebook(); + $facebook->api_client->stream_publish( + $notice->content, + $fbattachment, + null, + null, + $fbuid + ); + + common_log( + LOG_INFO, + "FacebookPlugin - Posted notice $notice->id as a stream " + . "item with attachment for $user->nickname ($user->id), " + . "Facebook UID: $fbuid" + ); +} - return false; - } +function updateProfileBox($facebook, $flink, $notice, $user) { - } - } + $facebook = getFacebook(); + $fbaction = new FacebookAction( + $output = 'php://output', + $indent = null, + $facebook, + $flink + ); - return true; + $fbuid = $flink->foreign_id; -} + common_debug( + 'FacebookPlugin - Attempting to update profile box with ' + . "content from notice $notice->id for $user->nickname ($user->id), " + . "Facebook UID: $fbuid" + ); -function updateProfileBox($facebook, $flink, $notice) { - $fbaction = new FacebookAction($output = 'php://output', - $indent = null, $facebook, $flink); $fbaction->updateProfileBox($notice); + + common_debug( + 'FacebookPlugin - finished updating profile box for ' + . "$user->nickname ($user->id) Facebook UID: $fbuid" + ); + } function format_attachments($attachments) @@ -272,12 +426,12 @@ function remove_facebook_app($flink) function mail_facebook_app_removed($user) { - common_init_locale($user->language); - $profile = $user->getProfile(); $site_name = common_config('site', 'name'); + common_switch_locale($user->language); + $subject = sprintf( _m('Your %1$s Facebook application access has been disabled.', $site_name)); @@ -291,7 +445,7 @@ function mail_facebook_app_removed($user) "re-installing the %2\$s Facebook application.\n\nRegards,\n\n%2\$s"), $user->nickname, $site_name); - common_init_locale(); + common_switch_locale(); return mail_to_user($user, $subject, $body); } diff --git a/plugins/Facebook/locale/Facebook.po b/plugins/Facebook/locale/Facebook.pot index 4bc00248c..dce10d230 100644 --- a/plugins/Facebook/locale/Facebook.po +++ b/plugins/Facebook/locale/Facebook.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-03-01 14:58-0800\n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -16,72 +16,129 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: facebookaction.php:171 -msgid "Home" +#: facebookutil.php:285 +#, php-format +msgid "" +"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" +"\n" +"Regards,\n" +"\n" +"%2$s" msgstr "" -#: facebookaction.php:179 -msgid "Invite" +#: FBConnectAuth.php:51 +msgid "You must be logged into Facebook to use Facebook Connect." msgstr "" -#: facebookaction.php:188 -msgid "Settings" +#: FBConnectAuth.php:77 +msgid "There is already a local user linked with this Facebook." msgstr "" -#: facebookaction.php:228 +#: FBConnectAuth.php:90 FBConnectSettings.php:164 +msgid "There was a problem with your session token. Try again, please." +msgstr "" + +#: FBConnectAuth.php:95 +msgid "You can't register if you don't agree to the license." +msgstr "" + +#: FBConnectAuth.php:105 +msgid "Something weird happened." +msgstr "" + +#: FBConnectAuth.php:119 #, php-format msgid "" -"To use the %s Facebook Application you need to login with your username and " -"password. Don't have a username yet? " +"This is the first time you've logged into %s so we must connect your " +"Facebook to a local account. You can either create a new account, or connect " +"with your existing account, if you have one." msgstr "" -#: facebookaction.php:230 -msgid " a new account." +#: FBConnectAuth.php:125 +msgid "Facebook Account Setup" msgstr "" -#: facebookaction.php:236 -msgid "Register" +#: FBConnectAuth.php:158 +msgid "Connection options" msgstr "" -#: facebookaction.php:249 facebookaction.php:275 facebooklogin.php:91 -msgid "Login" +#: FBConnectAuth.php:183 +msgid "Create new account" msgstr "" -#: facebookaction.php:268 -msgid "Nickname" +#: FBConnectAuth.php:185 +msgid "Create a new user with this nickname." msgstr "" -#: facebookaction.php:271 FBConnectAuth.php:196 +#: FBConnectAuth.php:188 +msgid "New nickname" +msgstr "" + +#: FBConnectAuth.php:190 +msgid "1-64 lowercase letters or numbers, no punctuation or spaces" +msgstr "" + +#: FBConnectAuth.php:193 +msgid "Create" +msgstr "" + +#: FBConnectAuth.php:198 +msgid "Connect existing account" +msgstr "" + +#: FBConnectAuth.php:200 +msgid "" +"If you already have an account, login with your username and password to " +"connect it to your Facebook." +msgstr "" + +#: FBConnectAuth.php:203 +msgid "Existing nickname" +msgstr "" + +#: FBConnectAuth.php:206 facebookaction.php:271 msgid "Password" msgstr "" -#: facebookaction.php:281 -msgid "Lost or forgotten password?" +#: FBConnectAuth.php:209 +msgid "Connect" msgstr "" -#: facebookaction.php:330 facebookhome.php:248 -msgid "Pagination" +#: FBConnectAuth.php:225 FBConnectAuth.php:234 +msgid "Registration not allowed." msgstr "" -#: facebookaction.php:339 facebookhome.php:257 -msgid "After" +#: FBConnectAuth.php:241 +msgid "Not a valid invitation code." msgstr "" -#: facebookaction.php:347 facebookhome.php:265 -msgid "Before" +#: FBConnectAuth.php:251 +msgid "Nickname must have only lowercase letters and numbers and no spaces." msgstr "" -#: facebookaction.php:365 -msgid "No notice content!" +#: FBConnectAuth.php:256 +msgid "Nickname not allowed." msgstr "" -#: facebookaction.php:371 -#, php-format -msgid "That's too long. Max notice size is %d chars." +#: FBConnectAuth.php:261 +msgid "Nickname already in use. Try another one." msgstr "" -#: facebookaction.php:430 -msgid "Notices" +#: FBConnectAuth.php:279 FBConnectAuth.php:313 FBConnectAuth.php:333 +msgid "Error connecting user to Facebook." +msgstr "" + +#: FBConnectAuth.php:299 +msgid "Invalid username or password." +msgstr "" + +#: facebooklogin.php:91 facebookaction.php:249 facebookaction.php:275 +msgid "Login" msgstr "" #: facebookhome.php:111 @@ -117,6 +174,18 @@ msgstr "" msgid "Skip" msgstr "" +#: facebookhome.php:248 facebookaction.php:330 +msgid "Pagination" +msgstr "" + +#: facebookhome.php:257 facebookaction.php:339 +msgid "After" +msgstr "" + +#: facebookhome.php:265 facebookaction.php:347 +msgid "Before" +msgstr "" + #: facebookinvite.php:72 #, php-format msgid "Thanks for inviting your friends to use %s" @@ -145,208 +214,123 @@ msgstr "" msgid "Send invitations" msgstr "" -#: FacebookPlugin.php:413 FacebookPlugin.php:433 +#: FacebookPlugin.php:195 FacebookPlugin.php:488 FacebookPlugin.php:510 +#: facebookadminpanel.php:54 msgid "Facebook" msgstr "" -#: FacebookPlugin.php:414 +#: FacebookPlugin.php:196 +msgid "Facebook integration configuration" +msgstr "" + +#: FacebookPlugin.php:489 msgid "Login or register using Facebook" msgstr "" -#: FacebookPlugin.php:434 FBConnectSettings.php:56 +#: FacebookPlugin.php:511 FBConnectSettings.php:56 msgid "Facebook Connect Settings" msgstr "" -#: FacebookPlugin.php:533 +#: FacebookPlugin.php:617 msgid "" "The Facebook plugin allows you to integrate your StatusNet instance with <a " "href=\"http://facebook.com/\">Facebook</a> and Facebook Connect." msgstr "" -#: facebookremove.php:58 -msgid "Couldn't remove Facebook user." -msgstr "" - -#: facebooksettings.php:74 -msgid "There was a problem saving your sync preferences!" -msgstr "" - -#: facebooksettings.php:76 -msgid "Sync preferences saved." -msgstr "" - -#: facebooksettings.php:99 -msgid "Automatically update my Facebook status with my notices." -msgstr "" - -#: facebooksettings.php:106 -msgid "Send \"@\" replies to Facebook." +#: FBConnectLogin.php:33 +msgid "Already logged in." msgstr "" -#: facebooksettings.php:115 -msgid "Prefix" +#: FBConnectLogin.php:41 +msgid "Login with your Facebook Account" msgstr "" -#: facebooksettings.php:117 -msgid "A string to prefix notices with." +#: FBConnectLogin.php:55 +msgid "Facebook Login" msgstr "" -#: facebooksettings.php:123 -msgid "Save" +#: facebookremove.php:58 +msgid "Couldn't remove Facebook user." msgstr "" -#: facebooksettings.php:133 -#, php-format -msgid "" -"If you would like %s to automatically update your Facebook status with your " -"latest notice, you need to give it permission." +#: facebookaction.php:171 +msgid "Home" msgstr "" -#: facebooksettings.php:146 -#, php-format -msgid "Allow %s to update my Facebook status" +#: facebookaction.php:179 +msgid "Invite" msgstr "" -#: facebooksettings.php:156 -msgid "Sync preferences" +#: facebookaction.php:188 +msgid "Settings" msgstr "" -#: facebookutil.php:285 +#: facebookaction.php:228 #, php-format msgid "" -"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" -"\n" -"Regards,\n" -"\n" -"%2$s" +"To use the %s Facebook Application you need to login with your username and " +"password. Don't have a username yet? " msgstr "" -#: FBConnectAuth.php:51 -msgid "You must be logged into Facebook to use Facebook Connect." +#: facebookaction.php:230 +msgid " a new account." msgstr "" -#: FBConnectAuth.php:77 -msgid "There is already a local user linked with this Facebook." +#: facebookaction.php:236 +msgid "Register" msgstr "" -#: FBConnectAuth.php:90 FBConnectSettings.php:164 -msgid "There was a problem with your session token. Try again, please." +#: facebookaction.php:268 +msgid "Nickname" msgstr "" -#: FBConnectAuth.php:95 -msgid "You can't register if you don't agree to the license." +#: facebookaction.php:281 +msgid "Lost or forgotten password?" msgstr "" -#: FBConnectAuth.php:105 -msgid "Something weird happened." +#: facebookaction.php:365 +msgid "No notice content!" msgstr "" -#: FBConnectAuth.php:119 +#: facebookaction.php:371 #, php-format -msgid "" -"This is the first time you've logged into %s so we must connect your " -"Facebook to a local account. You can either create a new account, or connect " -"with your existing account, if you have one." -msgstr "" - -#: FBConnectAuth.php:125 -msgid "Facebook Account Setup" -msgstr "" - -#: FBConnectAuth.php:153 -msgid "Connection options" -msgstr "" - -#: FBConnectAuth.php:162 -msgid "My text and files are available under " -msgstr "" - -#: FBConnectAuth.php:165 -msgid "" -" except this private data: password, email address, IM address, phone number." -msgstr "" - -#: FBConnectAuth.php:173 -msgid "Create new account" -msgstr "" - -#: FBConnectAuth.php:175 -msgid "Create a new user with this nickname." -msgstr "" - -#: FBConnectAuth.php:178 -msgid "New nickname" -msgstr "" - -#: FBConnectAuth.php:180 -msgid "1-64 lowercase letters or numbers, no punctuation or spaces" -msgstr "" - -#: FBConnectAuth.php:183 -msgid "Create" -msgstr "" - -#: FBConnectAuth.php:188 -msgid "Connect existing account" -msgstr "" - -#: FBConnectAuth.php:190 -msgid "" -"If you already have an account, login with your username and password to " -"connect it to your Facebook." -msgstr "" - -#: FBConnectAuth.php:193 -msgid "Existing nickname" -msgstr "" - -#: FBConnectAuth.php:199 -msgid "Connect" -msgstr "" - -#: FBConnectAuth.php:215 FBConnectAuth.php:224 -msgid "Registration not allowed." +msgid "That's too long. Max notice size is %d chars." msgstr "" -#: FBConnectAuth.php:231 -msgid "Not a valid invitation code." +#: facebookaction.php:430 +msgid "Notices" msgstr "" -#: FBConnectAuth.php:241 -msgid "Nickname must have only lowercase letters and numbers and no spaces." +#: facebookadminpanel.php:65 +msgid "Facebook integration settings" msgstr "" -#: FBConnectAuth.php:246 -msgid "Nickname not allowed." +#: facebookadminpanel.php:129 +msgid "Invalid Facebook API key. Max length is 255 characters." msgstr "" -#: FBConnectAuth.php:251 -msgid "Nickname already in use. Try another one." +#: facebookadminpanel.php:135 +msgid "Invalid Facebook API secret. Max length is 255 characters." msgstr "" -#: FBConnectAuth.php:269 FBConnectAuth.php:303 FBConnectAuth.php:323 -msgid "Error connecting user to Facebook." +#: facebookadminpanel.php:188 +msgid "Facebook application settings" msgstr "" -#: FBConnectAuth.php:289 -msgid "Invalid username or password." +#: facebookadminpanel.php:194 +msgid "API key" msgstr "" -#: FBConnectLogin.php:33 -msgid "Already logged in." +#: facebookadminpanel.php:195 +msgid "API key provided by Facebook" msgstr "" -#: FBConnectLogin.php:41 -msgid "Login with your Facebook Account" +#: facebookadminpanel.php:203 +msgid "Secret" msgstr "" -#: FBConnectLogin.php:55 -msgid "Facebook Login" +#: facebookadminpanel.php:204 +msgid "API secret provided by Facebook" msgstr "" #: FBConnectSettings.php:67 @@ -393,3 +377,47 @@ msgstr "" #: FBConnectSettings.php:197 msgid "Not sure what you're trying to do." msgstr "" + +#: facebooksettings.php:74 +msgid "There was a problem saving your sync preferences!" +msgstr "" + +#: facebooksettings.php:76 +msgid "Sync preferences saved." +msgstr "" + +#: facebooksettings.php:99 +msgid "Automatically update my Facebook status with my notices." +msgstr "" + +#: facebooksettings.php:106 +msgid "Send \"@\" replies to Facebook." +msgstr "" + +#: facebooksettings.php:115 +msgid "Prefix" +msgstr "" + +#: facebooksettings.php:117 +msgid "A string to prefix notices with." +msgstr "" + +#: facebooksettings.php:123 +msgid "Save" +msgstr "" + +#: facebooksettings.php:133 +#, php-format +msgid "" +"If you would like %s to automatically update your Facebook status with your " +"latest notice, you need to give it permission." +msgstr "" + +#: facebooksettings.php:146 +#, php-format +msgid "Allow %s to update my Facebook status" +msgstr "" + +#: facebooksettings.php:156 +msgid "Sync preferences" +msgstr "" diff --git a/plugins/FirePHP/FirePHPPlugin.php b/plugins/FirePHP/FirePHPPlugin.php index 452f79024..d984ec1af 100644 --- a/plugins/FirePHP/FirePHPPlugin.php +++ b/plugins/FirePHP/FirePHPPlugin.php @@ -24,11 +24,13 @@ Author URI: http://candrews.integralblue.com/ * * 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 Plugin * @package MinifyPlugin * @maintainer Craig Andrews <candrews@integralblue.com> + * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ */ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } @@ -52,8 +54,8 @@ class FirePHPPlugin extends Plugin { static $firephp_priorities = array(FirePHP::ERROR, FirePHP::ERROR, FirePHP::ERROR, FirePHP::ERROR, FirePHP::WARN, FirePHP::LOG, FirePHP::LOG, FirePHP::INFO); - $priority = $firephp_priorities[$priority]; - $this->firephp->fb($msg, $priority); + $fp_priority = $firephp_priorities[$priority]; + $this->firephp->fb($msg, $fp_priority); } function onPluginVersion(&$versions) diff --git a/plugins/FirePHP/locale/FirePHP.pot b/plugins/FirePHP/locale/FirePHP.pot new file mode 100644 index 000000000..fa16f283e --- /dev/null +++ b/plugins/FirePHP/locale/FirePHP.pot @@ -0,0 +1,21 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: FirePHPPlugin.php:66 +msgid "The FirePHP plugin writes StatusNet's log output to FirePHP." +msgstr "" diff --git a/plugins/GeonamesPlugin.php b/plugins/GeonamesPlugin.php index 589462ed9..3815a31fa 100644 --- a/plugins/GeonamesPlugin.php +++ b/plugins/GeonamesPlugin.php @@ -55,6 +55,12 @@ class GeonamesPlugin extends Plugin public $username = null; public $token = null; public $expiry = 7776000; // 90-day expiry + public $timeout = 2; // Web service timeout in seconds. + public $timeoutWindow = 60; // Further lookups in this process will be disabled for N seconds after a timeout. + public $cachePrefix = null; // Optional shared memcache prefix override + // to share lookups between local instances. + + protected $lastTimeout = null; // timestamp of last web service timeout /** * convert a name into a Location object @@ -370,7 +376,7 @@ class GeonamesPlugin extends Plugin return true; } - $url = 'http://sw.geonames.org/' . $location->location_id . '/'; + $url = 'http://sws.geonames.org/' . $location->location_id . '/'; // it's been filled, so don't process further. return false; @@ -408,9 +414,14 @@ class GeonamesPlugin extends Plugin function cacheKey($attrs) { - return common_cache_key('geonames:'. - implode(',', array_keys($attrs)) . ':'. - common_keyize(implode(',', array_values($attrs)))); + $key = 'geonames:' . + implode(',', array_keys($attrs)) . ':'. + common_keyize(implode(',', array_values($attrs))); + if ($this->cachePrefix) { + return $this->cachePrefix . ':' . $key; + } else { + return common_cache_key($key); + } } function wsUrl($method, $params) @@ -430,12 +441,24 @@ class GeonamesPlugin extends Plugin function getGeonames($method, $params) { + if ($this->lastTimeout && (time() - $this->lastTimeout < $this->timeoutWindow)) { + throw new Exception("skipping due to recent web service timeout"); + } + $client = HTTPClient::start(); + $client->setConfig('connect_timeout', $this->timeout); + $client->setConfig('timeout', $this->timeout); - $result = $client->get($this->wsUrl($method, $params)); + try { + $result = $client->get($this->wsUrl($method, $params)); + } catch (Exception $e) { + common_log(LOG_ERR, __METHOD__ . ": " . $e->getMessage()); + $this->lastTimeout = time(); + throw $e; + } if (!$result->isOk()) { - throw new Exception("HTTP error code " . $result->code); + throw new Exception("HTTP error code " . $result->getStatus()); } $body = $result->getBody(); diff --git a/plugins/Gravatar/GravatarPlugin.php b/plugins/Gravatar/GravatarPlugin.php index 580852072..8a9721ea9 100644 --- a/plugins/Gravatar/GravatarPlugin.php +++ b/plugins/Gravatar/GravatarPlugin.php @@ -30,11 +30,13 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { class GravatarPlugin extends Plugin { - function onInitializePlugin() { + function onInitializePlugin() + { return true; } - function onStartAvatarFormData($action) { + function onStartAvatarFormData($action) + { $user = common_current_user(); $hasGravatar = $this->hasGravatar($user->id); @@ -43,7 +45,8 @@ class GravatarPlugin extends Plugin } } - function onEndAvatarFormData(&$action) { + function onEndAvatarFormData($action) + { $user = common_current_user(); $hasGravatar = $this->hasGravatar($user->id); @@ -89,7 +92,8 @@ class GravatarPlugin extends Plugin } } - function onStartAvatarSaveForm($action) { + function onStartAvatarSaveForm($action) + { if ($action->arg('add')) { $result = $this->gravatar_save(); @@ -178,7 +182,8 @@ class GravatarPlugin extends Plugin 'success' => true); } - function gravatar_url($email, $size) { + function gravatar_url($email, $size) + { $url = "http://www.gravatar.com/avatar.php?gravatar_id=". md5(strtolower($email)). "&default=".urlencode(Avatar::defaultImage($size)). @@ -197,4 +202,4 @@ class GravatarPlugin extends Plugin return true; } -} +}
\ No newline at end of file diff --git a/plugins/Gravatar/locale/Gravatar.po b/plugins/Gravatar/locale/Gravatar.pot index d7275b929..d3a4cd86b 100644 --- a/plugins/Gravatar/locale/Gravatar.po +++ b/plugins/Gravatar/locale/Gravatar.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-03-01 14:58-0800\n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" diff --git a/plugins/Imap/ImapPlugin.php b/plugins/Imap/ImapPlugin.php index d1e920b00..66be799d3 100644 --- a/plugins/Imap/ImapPlugin.php +++ b/plugins/Imap/ImapPlugin.php @@ -21,8 +21,9 @@ * * @category Plugin * @package StatusNet - * @author Zach Copley <zach@status.net> + * @author Craig Andrews <candrews@integralblue.com * @copyright 2009 StatusNet, Inc. + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ @@ -37,6 +38,7 @@ if (!defined('STATUSNET')) { * @category Plugin * @package StatusNet * @author Craig Andrews <candrews@integralblue.com + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/Imap/imapmanager.php b/plugins/Imap/imapmanager.php index e4fda5809..0bbd42e78 100644 --- a/plugins/Imap/imapmanager.php +++ b/plugins/Imap/imapmanager.php @@ -23,6 +23,8 @@ * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> * @copyright 2009-2010 StatusNet, Inc. + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @maintainer 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/ */ diff --git a/plugins/Imap/locale/Imap.pot b/plugins/Imap/locale/Imap.pot new file mode 100644 index 000000000..ee8452aaa --- /dev/null +++ b/plugins/Imap/locale/Imap.pot @@ -0,0 +1,27 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: imapmailhandler.php:28 +msgid "Error" +msgstr "" + +#: ImapPlugin.php:101 +msgid "" +"The IMAP plugin allows for StatusNet to check a POP or IMAP mailbox for " +"incoming mail containing user posts." +msgstr "" diff --git a/plugins/InfiniteScroll/InfiniteScrollPlugin.php b/plugins/InfiniteScroll/InfiniteScrollPlugin.php index a4d1a5d05..50c1b5a20 100644 --- a/plugins/InfiniteScroll/InfiniteScrollPlugin.php +++ b/plugins/InfiniteScroll/InfiniteScrollPlugin.php @@ -22,7 +22,7 @@ * @category Plugin * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> - * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/InfiniteScroll/locale/InfiniteScroll.pot b/plugins/InfiniteScroll/locale/InfiniteScroll.pot new file mode 100644 index 000000000..a0f466fcb --- /dev/null +++ b/plugins/InfiniteScroll/locale/InfiniteScroll.pot @@ -0,0 +1,25 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: InfiniteScrollPlugin.php:54 +msgid "" +"Infinite Scroll adds the following functionality to your StatusNet " +"installation: When a user scrolls towards the bottom of the page, the next " +"page of notices is automatically retrieved and appended. This means they " +"never need to click \"Next Page\", which dramatically increases stickiness." +msgstr "" diff --git a/plugins/LdapAuthentication/LdapAuthenticationPlugin.php b/plugins/LdapAuthentication/LdapAuthenticationPlugin.php index 483209676..52d326287 100644 --- a/plugins/LdapAuthentication/LdapAuthenticationPlugin.php +++ b/plugins/LdapAuthentication/LdapAuthenticationPlugin.php @@ -22,7 +22,7 @@ * @category Plugin * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> - * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ @@ -31,48 +31,25 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once 'Net/LDAP2.php'; - class LdapAuthenticationPlugin extends AuthenticationPlugin { - public $host=null; - public $port=null; - public $version=null; - public $starttls=null; - public $binddn=null; - public $bindpw=null; - public $basedn=null; - public $options=null; - public $filter=null; - public $scope=null; - public $password_encoding=null; - public $attributes=array(); - function onInitializePlugin(){ parent::onInitializePlugin(); - if(!isset($this->host)){ - throw new Exception("must specify a host"); - } - if(!isset($this->basedn)){ - throw new Exception("must specify a basedn"); - } if(!isset($this->attributes['nickname'])){ throw new Exception("must specify a nickname attribute"); } - if(!isset($this->attributes['username'])){ - throw new Exception("must specify a username attribute"); - } if($this->password_changeable && (! isset($this->attributes['password']) || !isset($this->password_encoding))){ throw new Exception("if password_changeable is set, the password attribute and password_encoding must also be specified"); } + $this->ldapCommon = new LdapCommon(get_object_vars($this)); } function onAutoload($cls) { switch ($cls) { - case 'MemcacheSchemaCache': - require_once(INSTALLDIR.'/plugins/LdapAuthentication/MemcacheSchemaCache.php'); + case 'LdapCommon': + require_once(INSTALLDIR.'/plugins/LdapCommon/LdapCommon.php'); return false; } } @@ -107,19 +84,7 @@ class LdapAuthenticationPlugin extends AuthenticationPlugin function checkPassword($username, $password) { - $entry = $this->ldap_get_user($username); - if(!$entry){ - return false; - }else{ - $config = $this->ldap_get_config(); - $config['binddn']=$entry->dn(); - $config['bindpw']=$password; - if($this->ldap_get_connection($config)){ - return true; - }else{ - return false; - } - } + return $this->ldapCommon->checkPassword($username,$password); } function autoRegister($username, $nickname) @@ -127,11 +92,16 @@ class LdapAuthenticationPlugin extends AuthenticationPlugin if(is_null($nickname)){ $nickname = $username; } - $entry = $this->ldap_get_user($username,$this->attributes); + $entry = $this->ldapCommon->get_user($username,$this->attributes); if($entry){ $registration_data = array(); foreach($this->attributes as $sn_attribute=>$ldap_attribute){ - $registration_data[$sn_attribute]=$entry->getValue($ldap_attribute,'single'); + //ldap won't let us read a user's password, + //and we're going to set the password to a random string later anyways, + //so don't bother trying to read it. + if($sn_attribute != 'password'){ + $registration_data[$sn_attribute]=$entry->getValue($ldap_attribute,'single'); + } } if(isset($registration_data['email']) && !empty($registration_data['email'])){ $registration_data['email_confirmed']=true; @@ -148,45 +118,12 @@ class LdapAuthenticationPlugin extends AuthenticationPlugin function changePassword($username,$oldpassword,$newpassword) { - if(! isset($this->attributes['password']) || !isset($this->password_encoding)){ - //throw new Exception(_('Sorry, changing LDAP passwords is not supported at this time')); - return false; - } - $entry = $this->ldap_get_user($username); - if(!$entry){ - return false; - }else{ - $config = $this->ldap_get_config(); - $config['binddn']=$entry->dn(); - $config['bindpw']=$oldpassword; - if($ldap = $this->ldap_get_connection($config)){ - $entry = $this->ldap_get_user($username,array(),$ldap); - - $newCryptedPassword = $this->hashPassword($newpassword, $this->password_encoding); - if ($newCryptedPassword===false) { - return false; - } - if($this->password_encoding=='ad') { - //TODO I believe this code will work once this bug is fixed: http://pear.php.net/bugs/bug.php?id=16796 - $oldCryptedPassword = $this->hashPassword($oldpassword, $this->password_encoding); - $entry->delete( array($this->attributes['password'] => $oldCryptedPassword )); - } - $entry->replace( array($this->attributes['password'] => $newCryptedPassword ), true); - if( Net_LDAP2::isError($entry->upate()) ) { - return false; - } - return true; - }else{ - return false; - } - } - - return false; + return $this->ldapCommon->changePassword($username,$oldpassword,$newpassword); } function suggestNicknameForUsername($username) { - $entry = $this->ldap_get_user($username, $this->attributes); + $entry = $this->ldapCommon->get_user($username, $this->attributes); if(!$entry){ //this really shouldn't happen $nickname = $username; @@ -198,203 +135,6 @@ class LdapAuthenticationPlugin extends AuthenticationPlugin } return common_nicknamize($nickname); } - - //---utility functions---// - function ldap_get_config(){ - $config = array(); - $keys = array('host','port','version','starttls','binddn','bindpw','basedn','options','filter','scope'); - foreach($keys as $key){ - $value = $this->$key; - if($value!==null){ - $config[$key]=$value; - } - } - return $config; - } - - function ldap_get_connection($config = null){ - if($config == null && isset($this->default_ldap)){ - return $this->default_ldap; - } - - //cannot use Net_LDAP2::connect() as StatusNet uses - //PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError'); - //PEAR handling can be overridden on instance objects, so we do that. - $ldap = new Net_LDAP2(isset($config)?$config:$this->ldap_get_config()); - $ldap->setErrorHandling(PEAR_ERROR_RETURN); - $err=$ldap->bind(); - if (Net_LDAP2::isError($err)) { - // if we were called with a config, assume caller will handle - // incorrect username/password (LDAP_INVALID_CREDENTIALS) - if (isset($config) && $err->getCode() == 0x31) { - return null; - } - throw new Exception('Could not connect to LDAP server: '.$err->getMessage()); - } - if($config == null) $this->default_ldap=$ldap; - - $c = common_memcache(); - if (!empty($c)) { - $cacheObj = new MemcacheSchemaCache( - array('c'=>$c, - 'cacheKey' => common_cache_key('ldap_schema:' . crc32(serialize($config))))); - $ldap->registerSchemaCache($cacheObj); - } - return $ldap; - } - - /** - * get an LDAP entry for a user with a given username - * - * @param string $username - * $param array $attributes LDAP attributes to retrieve - * @return string DN - */ - function ldap_get_user($username,$attributes=array(),$ldap=null){ - if($ldap==null) { - $ldap = $this->ldap_get_connection(); - } - $filter = Net_LDAP2_Filter::create($this->attributes['username'], 'equals', $username); - $options = array( - 'attributes' => $attributes - ); - $search = $ldap->search($this->basedn, $filter, $options); - - if (PEAR::isError($search)) { - common_log(LOG_WARNING, 'Error while getting DN for user: '.$search->getMessage()); - return false; - } - - $searchcount = $search->count(); - if($searchcount == 0) { - return false; - }else if($searchcount == 1) { - $entry = $search->shiftEntry(); - return $entry; - }else{ - common_log(LOG_WARNING, 'Found ' . $searchcount . ' ldap user with the username: ' . $username); - return false; - } - } - - /** - * Code originaly from the phpLDAPadmin development team - * http://phpldapadmin.sourceforge.net/ - * - * Hashes a password and returns the hash based on the specified enc_type. - * - * @param string $passwordClear The password to hash in clear text. - * @param string $encodageType Standard LDAP encryption type which must be one of - * crypt, ext_des, md5crypt, blowfish, md5, sha, smd5, ssha, or clear. - * @return string The hashed password. - * - */ - - function hashPassword( $passwordClear, $encodageType ) - { - $encodageType = strtolower( $encodageType ); - switch( $encodageType ) { - case 'crypt': - $cryptedPassword = '{CRYPT}' . crypt($passwordClear,$this->randomSalt(2)); - break; - - case 'ext_des': - // extended des crypt. see OpenBSD crypt man page. - if ( ! defined( 'CRYPT_EXT_DES' ) || CRYPT_EXT_DES == 0 ) {return FALSE;} //Your system crypt library does not support extended DES encryption. - $cryptedPassword = '{CRYPT}' . crypt( $passwordClear, '_' . $this->randomSalt(8) ); - break; - - case 'md5crypt': - if( ! defined( 'CRYPT_MD5' ) || CRYPT_MD5 == 0 ) {return FALSE;} //Your system crypt library does not support md5crypt encryption. - $cryptedPassword = '{CRYPT}' . crypt( $passwordClear , '$1$' . $this->randomSalt(9) ); - break; - - case 'blowfish': - if( ! defined( 'CRYPT_BLOWFISH' ) || CRYPT_BLOWFISH == 0 ) {return FALSE;} //Your system crypt library does not support blowfish encryption. - $cryptedPassword = '{CRYPT}' . crypt( $passwordClear , '$2a$12$' . $this->randomSalt(13) ); // hardcoded to second blowfish version and set number of rounds - break; - - case 'md5': - $cryptedPassword = '{MD5}' . base64_encode( pack( 'H*' , md5( $passwordClear) ) ); - break; - - case 'sha': - if( function_exists('sha1') ) { - // use php 4.3.0+ sha1 function, if it is available. - $cryptedPassword = '{SHA}' . base64_encode( pack( 'H*' , sha1( $passwordClear) ) ); - } elseif( function_exists( 'mhash' ) ) { - $cryptedPassword = '{SHA}' . base64_encode( mhash( MHASH_SHA1, $passwordClear) ); - } else { - return FALSE; //Your PHP install does not have the mhash() function. Cannot do SHA hashes. - } - break; - - case 'ssha': - if( function_exists( 'mhash' ) && function_exists( 'mhash_keygen_s2k' ) ) { - mt_srand( (double) microtime() * 1000000 ); - $salt = mhash_keygen_s2k( MHASH_SHA1, $passwordClear, substr( pack( "h*", md5( mt_rand() ) ), 0, 8 ), 4 ); - $cryptedPassword = "{SSHA}".base64_encode( mhash( MHASH_SHA1, $passwordClear.$salt ).$salt ); - } else { - return FALSE; //Your PHP install does not have the mhash() function. Cannot do SHA hashes. - } - break; - - case 'smd5': - if( function_exists( 'mhash' ) && function_exists( 'mhash_keygen_s2k' ) ) { - mt_srand( (double) microtime() * 1000000 ); - $salt = mhash_keygen_s2k( MHASH_MD5, $passwordClear, substr( pack( "h*", md5( mt_rand() ) ), 0, 8 ), 4 ); - $cryptedPassword = "{SMD5}".base64_encode( mhash( MHASH_MD5, $passwordClear.$salt ).$salt ); - } else { - return FALSE; //Your PHP install does not have the mhash() function. Cannot do SHA hashes. - } - break; - - case 'ad': - $cryptedPassword = ''; - $passwordClear = "\"" . $passwordClear . "\""; - $len = strlen($passwordClear); - for ($i = 0; $i < $len; $i++) { - $cryptedPassword .= "{$passwordClear{$i}}\000"; - } - - case 'clear': - default: - $cryptedPassword = $passwordClear; - } - - return $cryptedPassword; - } - - /** - * Code originaly from the phpLDAPadmin development team - * http://phpldapadmin.sourceforge.net/ - * - * Used to generate a random salt for crypt-style passwords. Salt strings are used - * to make pre-built hash cracking dictionaries difficult to use as the hash algorithm uses - * not only the user's password but also a randomly generated string. The string is - * stored as the first N characters of the hash for reference of hashing algorithms later. - * - * --- added 20021125 by bayu irawan <bayuir@divnet.telkom.co.id> --- - * --- ammended 20030625 by S C Rigler <srigler@houston.rr.com> --- - * - * @param int $length The length of the salt string to generate. - * @return string The generated salt string. - */ - - function randomSalt( $length ) - { - $possible = '0123456789'. - 'abcdefghijklmnopqrstuvwxyz'. - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'. - './'; - $str = ""; - mt_srand((double)microtime() * 1000000); - - while( strlen( $str ) < $length ) - $str .= substr( $possible, ( rand() % strlen( $possible ) ), 1 ); - - return $str; - } function onPluginVersion(&$versions) { diff --git a/plugins/LdapAuthentication/locale/LdapAuthentication.pot b/plugins/LdapAuthentication/locale/LdapAuthentication.pot new file mode 100644 index 000000000..8f09b1e51 --- /dev/null +++ b/plugins/LdapAuthentication/locale/LdapAuthentication.pot @@ -0,0 +1,23 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: LdapAuthenticationPlugin.php:146 +msgid "" +"The LDAP Authentication plugin allows for StatusNet to handle authentication " +"through LDAP." +msgstr "" diff --git a/plugins/LdapAuthorization/LdapAuthorizationPlugin.php b/plugins/LdapAuthorization/LdapAuthorizationPlugin.php index 042b2db8d..3842385cf 100644 --- a/plugins/LdapAuthorization/LdapAuthorizationPlugin.php +++ b/plugins/LdapAuthorization/LdapAuthorizationPlugin.php @@ -22,7 +22,7 @@ * @category Plugin * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> - * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ @@ -31,41 +31,28 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once 'Net/LDAP2.php'; - class LdapAuthorizationPlugin extends AuthorizationPlugin { - public $host=null; - public $port=null; - public $version=null; - public $starttls=null; - public $binddn=null; - public $bindpw=null; - public $basedn=null; - public $options=null; - public $filter=null; - public $scope=null; - public $provider_name = null; - public $uniqueMember_attribute = null; public $roles_to_groups = array(); public $login_group = null; - public $attributes = array(); function onInitializePlugin(){ - if(!isset($this->host)){ - throw new Exception("must specify a host"); - } - if(!isset($this->basedn)){ - throw new Exception("must specify a basedn"); - } if(!isset($this->provider_name)){ throw new Exception("provider_name must be set. Use the provider_name from the LDAP Authentication plugin."); } if(!isset($this->uniqueMember_attribute)){ throw new Exception("uniqueMember_attribute must be set."); } - if(!isset($this->attributes['username'])){ - throw new Exception("username attribute must be set."); + $this->ldapCommon = new LdapCommon(get_object_vars($this)); + } + + function onAutoload($cls) + { + switch ($cls) + { + case 'LdapCommon': + require_once(INSTALLDIR.'/plugins/LdapCommon/LdapCommon.php'); + return false; } } @@ -75,17 +62,17 @@ class LdapAuthorizationPlugin extends AuthorizationPlugin $user_username->user_id=$user->id; $user_username->provider_name=$this->provider_name; if($user_username->find() && $user_username->fetch()){ - $entry = $this->ldap_get_user($user_username->username); + $entry = $this->ldapCommon->get_user($user_username->username); if($entry){ if(isset($this->login_group)){ if(is_array($this->login_group)){ foreach($this->login_group as $group){ - if($this->ldap_is_dn_member_of_group($entry->dn(),$group)){ + if($this->ldapCommon->is_dn_member_of_group($entry->dn(),$group)){ return true; } } }else{ - if($this->ldap_is_dn_member_of_group($entry->dn(),$this->login_group)){ + if($this->ldapCommon->is_dn_member_of_group($entry->dn(),$this->login_group)){ return true; } } @@ -107,17 +94,17 @@ class LdapAuthorizationPlugin extends AuthorizationPlugin $user_username->user_id=$profile->id; $user_username->provider_name=$this->provider_name; if($user_username->find() && $user_username->fetch()){ - $entry = $this->ldap_get_user($user_username->username); + $entry = $this->ldapCommon->get_user($user_username->username); if($entry){ if(isset($this->roles_to_groups[$name])){ if(is_array($this->roles_to_groups[$name])){ foreach($this->roles_to_groups[$name] as $group){ - if($this->ldap_is_dn_member_of_group($entry->dn(),$group)){ + if($this->ldapCommon->is_dn_member_of_group($entry->dn(),$group)){ return true; } } }else{ - if($this->ldap_is_dn_member_of_group($entry->dn(),$this->roles_to_groups[$name])){ + if($this->ldapCommon->is_dn_member_of_group($entry->dn(),$this->roles_to_groups[$name])){ return true; } } @@ -127,94 +114,6 @@ class LdapAuthorizationPlugin extends AuthorizationPlugin return false; } - function ldap_is_dn_member_of_group($userDn, $groupDn) - { - $ldap = $this->ldap_get_connection(); - $link = $ldap->getLink(); - $r = @ldap_compare($link, $groupDn, $this->uniqueMember_attribute, $userDn); - if ($r === true){ - return true; - }else if($r === false){ - return false; - }else{ - common_log(LOG_ERR, "LDAP error determining if userDn=$userDn is a member of groupDn=groupDn using uniqueMember_attribute=$this->uniqueMember_attribute error: ".ldap_error($link)); - return false; - } - } - - function ldap_get_config(){ - $config = array(); - $keys = array('host','port','version','starttls','binddn','bindpw','basedn','options','filter','scope'); - foreach($keys as $key){ - $value = $this->$key; - if($value!==null){ - $config[$key]=$value; - } - } - return $config; - } - - //-----the below function were copied from LDAPAuthenticationPlugin. They will be moved to a utility class soon.----\\ - function ldap_get_connection($config = null){ - if($config == null && isset($this->default_ldap)){ - return $this->default_ldap; - } - - //cannot use Net_LDAP2::connect() as StatusNet uses - //PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError'); - //PEAR handling can be overridden on instance objects, so we do that. - $ldap = new Net_LDAP2(isset($config)?$config:$this->ldap_get_config()); - $ldap->setErrorHandling(PEAR_ERROR_RETURN); - $err=$ldap->bind(); - if (Net_LDAP2::isError($err)) { - // if we were called with a config, assume caller will handle - // incorrect username/password (LDAP_INVALID_CREDENTIALS) - if (isset($config) && $err->getCode() == 0x31) { - return null; - } - throw new Exception('Could not connect to LDAP server: '.$err->getMessage()); - return false; - } - if($config == null) $this->default_ldap=$ldap; - return $ldap; - } - - /** - * get an LDAP entry for a user with a given username - * - * @param string $username - * $param array $attributes LDAP attributes to retrieve - * @return string DN - */ - function ldap_get_user($username,$attributes=array(),$ldap=null){ - if($ldap==null) { - $ldap = $this->ldap_get_connection(); - } - if(! $ldap) { - throw new Exception("Could not connect to LDAP"); - } - $filter = Net_LDAP2_Filter::create($this->attributes['username'], 'equals', $username); - $options = array( - 'attributes' => $attributes - ); - $search = $ldap->search(null,$filter,$options); - - if (PEAR::isError($search)) { - common_log(LOG_WARNING, 'Error while getting DN for user: '.$search->getMessage()); - return false; - } - - if($search->count()==0){ - return false; - }else if($search->count()==1){ - $entry = $search->shiftEntry(); - return $entry; - }else{ - common_log(LOG_WARNING, 'Found ' . $search->count() . ' ldap user with the username: ' . $username); - return false; - } - } - function onPluginVersion(&$versions) { $versions[] = array('name' => 'LDAP Authorization', diff --git a/plugins/LdapAuthorization/locale/LdapAuthorization.pot b/plugins/LdapAuthorization/locale/LdapAuthorization.pot new file mode 100644 index 000000000..8156f6146 --- /dev/null +++ b/plugins/LdapAuthorization/locale/LdapAuthorization.pot @@ -0,0 +1,23 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: LdapAuthorizationPlugin.php:124 +msgid "" +"The LDAP Authorization plugin allows for StatusNet to handle authorization " +"through LDAP." +msgstr "" diff --git a/plugins/LdapCommon/LdapCommon.php b/plugins/LdapCommon/LdapCommon.php new file mode 100644 index 000000000..1f1647a75 --- /dev/null +++ b/plugins/LdapCommon/LdapCommon.php @@ -0,0 +1,369 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Utility class of LDAP functions + * + * 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 Plugin + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +// We bundle the Net/LDAP2 library... +set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib'); + +class LdapCommon +{ + protected static $ldap_connections = array(); + public $host=null; + public $port=null; + public $version=null; + public $starttls=null; + public $binddn=null; + public $bindpw=null; + public $basedn=null; + public $options=null; + public $filter=null; + public $scope=null; + public $uniqueMember_attribute = null; + public $attributes=array(); + public $password_encoding=null; + + public function __construct($config) + { + Event::addHandler('Autoload',array($this,'onAutoload')); + foreach($config as $key=>$value) { + $this->$key = $value; + } + $this->ldap_config = $this->get_ldap_config(); + + if(!isset($this->host)){ + throw new Exception("must specify a host"); + } + if(!isset($this->basedn)){ + throw new Exception("must specify a basedn"); + } + if(!isset($this->attributes['username'])){ + throw new Exception("username attribute must be set."); + } + } + + function onAutoload($cls) + { + switch ($cls) + { + case 'MemcacheSchemaCache': + require_once(INSTALLDIR.'/plugins/LdapCommon/MemcacheSchemaCache.php'); + return false; + case 'Net_LDAP2': + require_once 'Net/LDAP2.php'; + return false; + case 'Net_LDAP2_Filter': + require_once 'Net/LDAP2/Filter.php'; + return false; + case 'Net_LDAP2_Filter': + require_once 'Net/LDAP2/Filter.php'; + return false; + case 'Net_LDAP2_Entry': + require_once 'Net/LDAP2/Entry.php'; + return false; + } + } + + function get_ldap_config(){ + $config = array(); + $keys = array('host','port','version','starttls','binddn','bindpw','basedn','options','filter','scope'); + foreach($keys as $key){ + $value = $this->$key; + if($value!==null){ + $config[$key]=$value; + } + } + return $config; + } + + function get_ldap_connection($config = null){ + if($config == null) { + $config = $this->ldap_config; + } + $config_id = crc32(serialize($config)); + if(array_key_exists($config_id,self::$ldap_connections)) { + $ldap = self::$ldap_connections[$config_id]; + } else { + //cannot use Net_LDAP2::connect() as StatusNet uses + //PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError'); + //PEAR handling can be overridden on instance objects, so we do that. + $ldap = new Net_LDAP2($config); + $ldap->setErrorHandling(PEAR_ERROR_RETURN); + $err=$ldap->bind(); + if (Net_LDAP2::isError($err)) { + // if we were called with a config, assume caller will handle + // incorrect username/password (LDAP_INVALID_CREDENTIALS) + if (isset($config) && $err->getCode() == 0x31) { + throw new LdapInvalidCredentialsException('Could not connect to LDAP server: '.$err->getMessage()); + } + throw new Exception('Could not connect to LDAP server: '.$err->getMessage()); + } + $c = common_memcache(); + if (!empty($c)) { + $cacheObj = new MemcacheSchemaCache( + array('c'=>$c, + 'cacheKey' => common_cache_key('ldap_schema:' . $config_id))); + $ldap->registerSchemaCache($cacheObj); + } + self::$ldap_connections[$config_id] = $ldap; + } + return $ldap; + } + + function checkPassword($username, $password) + { + $entry = $this->get_user($username); + if(!$entry){ + return false; + }else{ + $config = $this->get_ldap_config(); + $config['binddn']=$entry->dn(); + $config['bindpw']=$password; + try { + $this->get_ldap_connection($config); + } catch (LdapInvalidCredentialsException $e) { + return false; + } + return true; + } + } + + function changePassword($username,$oldpassword,$newpassword) + { + if(! isset($this->attributes['password']) || !isset($this->password_encoding)){ + //throw new Exception(_('Sorry, changing LDAP passwords is not supported at this time')); + return false; + } + $entry = $this->get_user($username); + if(!$entry){ + return false; + }else{ + $config = $this->get_ldap_config(); + $config['binddn']=$entry->dn(); + $config['bindpw']=$oldpassword; + try { + $ldap = $this->get_ldap_connection($config); + + $entry = $this->get_user($username,array(),$ldap); + + $newCryptedPassword = $this->hashPassword($newpassword, $this->password_encoding); + if ($newCryptedPassword===false) { + return false; + } + if($this->password_encoding=='ad') { + //TODO I believe this code will work once this bug is fixed: http://pear.php.net/bugs/bug.php?id=16796 + $oldCryptedPassword = $this->hashPassword($oldpassword, $this->password_encoding); + $entry->delete( array($this->attributes['password'] => $oldCryptedPassword )); + } + $entry->replace( array($this->attributes['password'] => $newCryptedPassword ), true); + if( Net_LDAP2::isError($entry->upate()) ) { + return false; + } + return true; + } catch (LdapInvalidCredentialsException $e) { + return false; + } + } + + return false; + } + + function is_dn_member_of_group($userDn, $groupDn) + { + $ldap = $this->get_ldap_connection(); + $link = $ldap->getLink(); + $r = @ldap_compare($link, $groupDn, $this->uniqueMember_attribute, $userDn); + if ($r === true){ + return true; + }else if($r === false){ + return false; + }else{ + common_log(LOG_ERR, "LDAP error determining if userDn=$userDn is a member of groupDn=$groupDn using uniqueMember_attribute=$this->uniqueMember_attribute error: ".ldap_error($link)); + return false; + } + } + + /** + * get an LDAP entry for a user with a given username + * + * @param string $username + * $param array $attributes LDAP attributes to retrieve + * @return string DN + */ + function get_user($username,$attributes=array()){ + $ldap = $this->get_ldap_connection(); + $filter = Net_LDAP2_Filter::create($this->attributes['username'], 'equals', $username); + $options = array( + 'attributes' => $attributes + ); + $search = $ldap->search(null,$filter,$options); + + if (PEAR::isError($search)) { + common_log(LOG_WARNING, 'Error while getting DN for user: '.$search->getMessage()); + return false; + } + + if($search->count()==0){ + return false; + }else if($search->count()==1){ + $entry = $search->shiftEntry(); + return $entry; + }else{ + common_log(LOG_WARNING, 'Found ' . $search->count() . ' ldap user with the username: ' . $username); + return false; + } + } + + /** + * Code originaly from the phpLDAPadmin development team + * http://phpldapadmin.sourceforge.net/ + * + * Hashes a password and returns the hash based on the specified enc_type. + * + * @param string $passwordClear The password to hash in clear text. + * @param string $encodageType Standard LDAP encryption type which must be one of + * crypt, ext_des, md5crypt, blowfish, md5, sha, smd5, ssha, or clear. + * @return string The hashed password. + * + */ + + function hashPassword( $passwordClear, $encodageType ) + { + $encodageType = strtolower( $encodageType ); + switch( $encodageType ) { + case 'crypt': + $cryptedPassword = '{CRYPT}' . crypt($passwordClear,$this->randomSalt(2)); + break; + + case 'ext_des': + // extended des crypt. see OpenBSD crypt man page. + if ( ! defined( 'CRYPT_EXT_DES' ) || CRYPT_EXT_DES == 0 ) {return FALSE;} //Your system crypt library does not support extended DES encryption. + $cryptedPassword = '{CRYPT}' . crypt( $passwordClear, '_' . $this->randomSalt(8) ); + break; + + case 'md5crypt': + if( ! defined( 'CRYPT_MD5' ) || CRYPT_MD5 == 0 ) {return FALSE;} //Your system crypt library does not support md5crypt encryption. + $cryptedPassword = '{CRYPT}' . crypt( $passwordClear , '$1$' . $this->randomSalt(9) ); + break; + + case 'blowfish': + if( ! defined( 'CRYPT_BLOWFISH' ) || CRYPT_BLOWFISH == 0 ) {return FALSE;} //Your system crypt library does not support blowfish encryption. + $cryptedPassword = '{CRYPT}' . crypt( $passwordClear , '$2a$12$' . $this->randomSalt(13) ); // hardcoded to second blowfish version and set number of rounds + break; + + case 'md5': + $cryptedPassword = '{MD5}' . base64_encode( pack( 'H*' , md5( $passwordClear) ) ); + break; + + case 'sha': + if( function_exists('sha1') ) { + // use php 4.3.0+ sha1 function, if it is available. + $cryptedPassword = '{SHA}' . base64_encode( pack( 'H*' , sha1( $passwordClear) ) ); + } elseif( function_exists( 'mhash' ) ) { + $cryptedPassword = '{SHA}' . base64_encode( mhash( MHASH_SHA1, $passwordClear) ); + } else { + return FALSE; //Your PHP install does not have the mhash() function. Cannot do SHA hashes. + } + break; + + case 'ssha': + if( function_exists( 'mhash' ) && function_exists( 'mhash_keygen_s2k' ) ) { + mt_srand( (double) microtime() * 1000000 ); + $salt = mhash_keygen_s2k( MHASH_SHA1, $passwordClear, substr( pack( "h*", md5( mt_rand() ) ), 0, 8 ), 4 ); + $cryptedPassword = "{SSHA}".base64_encode( mhash( MHASH_SHA1, $passwordClear.$salt ).$salt ); + } else { + return FALSE; //Your PHP install does not have the mhash() function. Cannot do SHA hashes. + } + break; + + case 'smd5': + if( function_exists( 'mhash' ) && function_exists( 'mhash_keygen_s2k' ) ) { + mt_srand( (double) microtime() * 1000000 ); + $salt = mhash_keygen_s2k( MHASH_MD5, $passwordClear, substr( pack( "h*", md5( mt_rand() ) ), 0, 8 ), 4 ); + $cryptedPassword = "{SMD5}".base64_encode( mhash( MHASH_MD5, $passwordClear.$salt ).$salt ); + } else { + return FALSE; //Your PHP install does not have the mhash() function. Cannot do SHA hashes. + } + break; + + case 'ad': + $cryptedPassword = ''; + $passwordClear = "\"" . $passwordClear . "\""; + $len = strlen($passwordClear); + for ($i = 0; $i < $len; $i++) { + $cryptedPassword .= "{$passwordClear{$i}}\000"; + } + + case 'clear': + default: + $cryptedPassword = $passwordClear; + } + + return $cryptedPassword; + } + + /** + * Code originaly from the phpLDAPadmin development team + * http://phpldapadmin.sourceforge.net/ + * + * Used to generate a random salt for crypt-style passwords. Salt strings are used + * to make pre-built hash cracking dictionaries difficult to use as the hash algorithm uses + * not only the user's password but also a randomly generated string. The string is + * stored as the first N characters of the hash for reference of hashing algorithms later. + * + * --- added 20021125 by bayu irawan <bayuir@divnet.telkom.co.id> --- + * --- ammended 20030625 by S C Rigler <srigler@houston.rr.com> --- + * + * @param int $length The length of the salt string to generate. + * @return string The generated salt string. + */ + + function randomSalt( $length ) + { + $possible = '0123456789'. + 'abcdefghijklmnopqrstuvwxyz'. + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'. + './'; + $str = ""; + mt_srand((double)microtime() * 1000000); + + while( strlen( $str ) < $length ) + $str .= substr( $possible, ( rand() % strlen( $possible ) ), 1 ); + + return $str; + } + +} + +class LdapInvalidCredentialsException extends Exception +{ + +} diff --git a/plugins/LdapAuthentication/MemcacheSchemaCache.php b/plugins/LdapCommon/MemcacheSchemaCache.php index 6b91d17d6..4ee2e8e16 100644 --- a/plugins/LdapAuthentication/MemcacheSchemaCache.php +++ b/plugins/LdapCommon/MemcacheSchemaCache.php @@ -22,7 +22,7 @@ * @category Plugin * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> - * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/LdapCommon/extlib/Net/LDAP2.php b/plugins/LdapCommon/extlib/Net/LDAP2.php new file mode 100644 index 000000000..26f5e7560 --- /dev/null +++ b/plugins/LdapCommon/extlib/Net/LDAP2.php @@ -0,0 +1,1791 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +/** +* File containing the Net_LDAP2 interface class. +* +* PHP version 5 +* +* @category Net +* @package Net_LDAP2 +* @author Tarjej Huse <tarjei@bergfald.no> +* @author Jan Wagner <wagner@netsols.de> +* @author Del <del@babel.com.au> +* @author Benedikt Hallinger <beni@php.net> +* @copyright 2003-2007 Tarjej Huse, Jan Wagner, Del Elson, Benedikt Hallinger +* @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 +* @version SVN: $Id: LDAP2.php 286788 2009-08-04 06:05:49Z beni $ +* @link http://pear.php.net/package/Net_LDAP2/ +*/ + +/** +* Package includes. +*/ +require_once 'PEAR.php'; +require_once 'Net/LDAP2/RootDSE.php'; +require_once 'Net/LDAP2/Schema.php'; +require_once 'Net/LDAP2/Entry.php'; +require_once 'Net/LDAP2/Search.php'; +require_once 'Net/LDAP2/Util.php'; +require_once 'Net/LDAP2/Filter.php'; +require_once 'Net/LDAP2/LDIF.php'; +require_once 'Net/LDAP2/SchemaCache.interface.php'; +require_once 'Net/LDAP2/SimpleFileSchemaCache.php'; + +/** +* Error constants for errors that are not LDAP errors. +*/ +define('NET_LDAP2_ERROR', 1000); + +/** +* Net_LDAP2 Version +*/ +define('NET_LDAP2_VERSION', '2.0.7'); + +/** +* Net_LDAP2 - manipulate LDAP servers the right way! +* +* @category Net +* @package Net_LDAP2 +* @author Tarjej Huse <tarjei@bergfald.no> +* @author Jan Wagner <wagner@netsols.de> +* @author Del <del@babel.com.au> +* @author Benedikt Hallinger <beni@php.net> +* @copyright 2003-2007 Tarjej Huse, Jan Wagner, Del Elson, Benedikt Hallinger +* @license http://www.gnu.org/copyleft/lesser.html LGPL +* @link http://pear.php.net/package/Net_LDAP2/ +*/ +class Net_LDAP2 extends PEAR +{ + /** + * Class configuration array + * + * host = the ldap host to connect to + * (may be an array of several hosts to try) + * port = the server port + * version = ldap version (defaults to v 3) + * starttls = when set, ldap_start_tls() is run after connecting. + * bindpw = no explanation needed + * binddn = the DN to bind as. + * basedn = ldap base + * options = hash of ldap options to set (opt => val) + * filter = default search filter + * scope = default search scope + * + * Newly added in 2.0.0RC4, for auto-reconnect: + * auto_reconnect = if set to true then the class will automatically + * attempt to reconnect to the LDAP server in certain + * failure conditionswhen attempting a search, or other + * LDAP operation. Defaults to false. Note that if you + * set this to true, calls to search() may block + * indefinitely if there is a catastrophic server failure. + * min_backoff = minimum reconnection delay period (in seconds). + * current_backoff = initial reconnection delay period (in seconds). + * max_backoff = maximum reconnection delay period (in seconds). + * + * @access protected + * @var array + */ + protected $_config = array('host' => 'localhost', + 'port' => 389, + 'version' => 3, + 'starttls' => false, + 'binddn' => '', + 'bindpw' => '', + 'basedn' => '', + 'options' => array(), + 'filter' => '(objectClass=*)', + 'scope' => 'sub', + 'auto_reconnect' => false, + 'min_backoff' => 1, + 'current_backoff' => 1, + 'max_backoff' => 32); + + /** + * List of hosts we try to establish a connection to + * + * @access protected + * @var array + */ + protected $_host_list = array(); + + /** + * List of hosts that are known to be down. + * + * @access protected + * @var array + */ + protected $_down_host_list = array(); + + /** + * LDAP resource link. + * + * @access protected + * @var resource + */ + protected $_link = false; + + /** + * Net_LDAP2_Schema object + * + * This gets set and returned by {@link schema()} + * + * @access protected + * @var object Net_LDAP2_Schema + */ + protected $_schema = null; + + /** + * Schema cacher function callback + * + * @see registerSchemaCache() + * @var string + */ + protected $_schema_cache = null; + + /** + * Cache for attribute encoding checks + * + * @access protected + * @var array Hash with attribute names as key and boolean value + * to determine whether they should be utf8 encoded or not. + */ + protected $_schemaAttrs = array(); + + /** + * Cache for rootDSE objects + * + * Hash with requested rootDSE attr names as key and rootDSE object as value + * + * Since the RootDSE object itself may request a rootDSE object, + * {@link rootDse()} caches successful requests. + * Internally, Net_LDAP2 needs several lookups to this object, so + * caching increases performance significally. + * + * @access protected + * @var array + */ + protected $_rootDSE_cache = array(); + + /** + * Returns the Net_LDAP2 Release version, may be called statically + * + * @static + * @return string Net_LDAP2 version + */ + public static function getVersion() + { + return NET_LDAP2_VERSION; + } + + /** + * Configure Net_LDAP2, connect and bind + * + * Use this method as starting point of using Net_LDAP2 + * to establish a connection to your LDAP server. + * + * Static function that returns either an error object or the new Net_LDAP2 + * object. Something like a factory. Takes a config array with the needed + * parameters. + * + * @param array $config Configuration array + * + * @access public + * @return Net_LDAP2_Error|Net_LDAP2 Net_LDAP2_Error or Net_LDAP2 object + */ + public static function &connect($config = array()) + { + $ldap_check = self::checkLDAPExtension(); + if (self::iserror($ldap_check)) { + return $ldap_check; + } + + @$obj = new Net_LDAP2($config); + + // todo? better errorhandling for setConfig()? + + // connect and bind with credentials in config + $err = $obj->bind(); + if (self::isError($err)) { + return $err; + } + + return $obj; + } + + /** + * Net_LDAP2 constructor + * + * Sets the config array + * + * Please note that the usual way of getting Net_LDAP2 to work is + * to call something like: + * <code>$ldap = Net_LDAP2::connect($ldap_config);</code> + * + * @param array $config Configuration array + * + * @access protected + * @return void + * @see $_config + */ + public function __construct($config = array()) + { + $this->PEAR('Net_LDAP2_Error'); + $this->setConfig($config); + } + + /** + * Sets the internal configuration array + * + * @param array $config Configuration array + * + * @access protected + * @return void + */ + protected function setConfig($config) + { + // + // Parameter check -- probably should raise an error here if config + // is not an array. + // + if (! is_array($config)) { + return; + } + + foreach ($config as $k => $v) { + if (isset($this->_config[$k])) { + $this->_config[$k] = $v; + } else { + // map old (Net_LDAP2) parms to new ones + switch($k) { + case "dn": + $this->_config["binddn"] = $v; + break; + case "password": + $this->_config["bindpw"] = $v; + break; + case "tls": + $this->_config["starttls"] = $v; + break; + case "base": + $this->_config["basedn"] = $v; + break; + } + } + } + + // + // Ensure the host list is an array. + // + if (is_array($this->_config['host'])) { + $this->_host_list = $this->_config['host']; + } else { + if (strlen($this->_config['host']) > 0) { + $this->_host_list = array($this->_config['host']); + } else { + $this->_host_list = array(); + // ^ this will cause an error in performConnect(), + // so the user is notified about the failure + } + } + + // + // Reset the down host list, which seems like a sensible thing to do + // if the config is being reset for some reason. + // + $this->_down_host_list = array(); + } + + /** + * Bind or rebind to the ldap-server + * + * This function binds with the given dn and password to the server. In case + * no connection has been made yet, it will be started and startTLS issued + * if appropiate. + * + * The internal bind configuration is not being updated, so if you call + * bind() without parameters, you can rebind with the credentials + * provided at first connecting to the server. + * + * @param string $dn Distinguished name for binding + * @param string $password Password for binding + * + * @access public + * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true + */ + public function bind($dn = null, $password = null) + { + // fetch current bind credentials + if (is_null($dn)) { + $dn = $this->_config["binddn"]; + } + if (is_null($password)) { + $password = $this->_config["bindpw"]; + } + + // Connect first, if we haven't so far. + // This will also bind us to the server. + if ($this->_link === false) { + // store old credentials so we can revert them later + // then overwrite config with new bind credentials + $olddn = $this->_config["binddn"]; + $oldpw = $this->_config["bindpw"]; + + // overwrite bind credentials in config + // so performConnect() knows about them + $this->_config["binddn"] = $dn; + $this->_config["bindpw"] = $password; + + // try to connect with provided credentials + $msg = $this->performConnect(); + + // reset to previous config + $this->_config["binddn"] = $olddn; + $this->_config["bindpw"] = $oldpw; + + // see if bind worked + if (self::isError($msg)) { + return $msg; + } + } else { + // do the requested bind as we are + // asked to bind manually + if (is_null($dn)) { + // anonymous bind + $msg = @ldap_bind($this->_link); + } else { + // privileged bind + $msg = @ldap_bind($this->_link, $dn, $password); + } + if (false === $msg) { + return PEAR::raiseError("Bind failed: " . + @ldap_error($this->_link), + @ldap_errno($this->_link)); + } + } + return true; + } + + /** + * Connect to the ldap-server + * + * This function connects to the LDAP server specified in + * the configuration, binds and set up the LDAP protocol as needed. + * + * @access protected + * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true + */ + protected function performConnect() + { + // Note: Connecting is briefly described in RFC1777. + // Basicly it works like this: + // 1. set up TCP connection + // 2. secure that connection if neccessary + // 3a. setLDAPVersion to tell server which version we want to speak + // 3b. perform bind + // 3c. setLDAPVersion to tell server which version we want to speak + // together with a test for supported versions + // 4. set additional protocol options + + // Return true if we are already connected. + if ($this->_link !== false) { + return true; + } + + // Connnect to the LDAP server if we are not connected. Note that + // with some LDAP clients, ldapperformConnect returns a link value even + // if no connection is made. We need to do at least one anonymous + // bind to ensure that a connection is actually valid. + // + // Ref: http://www.php.net/manual/en/function.ldap-connect.php + + // Default error message in case all connection attempts + // fail but no message is set + $current_error = new PEAR_Error('Unknown connection error'); + + // Catch empty $_host_list arrays. + if (!is_array($this->_host_list) || count($this->_host_list) == 0) { + $current_error = PEAR::raiseError('No Servers configured! Please '. + 'pass in an array of servers to Net_LDAP2'); + return $current_error; + } + + // Cycle through the host list. + foreach ($this->_host_list as $host) { + + // Ensure we have a valid string for host name + if (is_array($host)) { + $current_error = PEAR::raiseError('No Servers configured! '. + 'Please pass in an one dimensional array of servers to '. + 'Net_LDAP2! (multidimensional array detected!)'); + continue; + } + + // Skip this host if it is known to be down. + if (in_array($host, $this->_down_host_list)) { + continue; + } + + // Record the host that we are actually connecting to in case + // we need it later. + $this->_config['host'] = $host; + + // Attempt a connection. + $this->_link = @ldap_connect($host, $this->_config['port']); + if (false === $this->_link) { + $current_error = PEAR::raiseError('Could not connect to ' . + $host . ':' . $this->_config['port']); + $this->_down_host_list[] = $host; + continue; + } + + // If we're supposed to use TLS, do so before we try to bind, + // as some strict servers only allow binding via secure connections + if ($this->_config["starttls"] === true) { + if (self::isError($msg = $this->startTLS())) { + $current_error = $msg; + $this->_link = false; + $this->_down_host_list[] = $host; + continue; + } + } + + // Try to set the configured LDAP version on the connection if LDAP + // server needs that before binding (eg OpenLDAP). + // This could be necessary since rfc-1777 states that the protocol version + // has to be set at the bind request. + // We use force here which means that the test in the rootDSE is skipped; + // this is neccessary, because some strict LDAP servers only allow to + // read the LDAP rootDSE (which tells us the supported protocol versions) + // with authenticated clients. + // This may fail in which case we try again after binding. + // In this case, most probably the bind() or setLDAPVersion()-call + // below will also fail, providing error messages. + $version_set = false; + $ignored_err = $this->setLDAPVersion(0, true); + if (!self::isError($ignored_err)) { + $version_set = true; + } + + // Attempt to bind to the server. If we have credentials configured, + // we try to use them, otherwise its an anonymous bind. + // As stated by RFC-1777, the bind request should be the first + // operation to be performed after the connection is established. + // This may give an protocol error if the server does not support + // V2 binds and the above call to setLDAPVersion() failed. + // In case the above call failed, we try an V2 bind here and set the + // version afterwards (with checking to the rootDSE). + $msg = $this->bind(); + if (self::isError($msg)) { + // The bind failed, discard link and save error msg. + // Then record the host as down and try next one + if ($msg->getCode() == 0x02 && !$version_set) { + // provide a finer grained error message + // if protocol error arieses because of invalid version + $msg = new Net_LDAP2_Error($msg->getMessage(). + " (could not set LDAP protocol version to ". + $this->_config['version'].")", + $msg->getCode()); + } + $this->_link = false; + $current_error = $msg; + $this->_down_host_list[] = $host; + continue; + } + + // Set desired LDAP version if not successfully set before. + // Here, a check against the rootDSE is performed, so we get a + // error message if the server does not support the version. + // The rootDSE entry should tell us which LDAP versions are + // supported. However, some strict LDAP servers only allow + // bound suers to read the rootDSE. + if (!$version_set) { + if (self::isError($msg = $this->setLDAPVersion())) { + $current_error = $msg; + $this->_link = false; + $this->_down_host_list[] = $host; + continue; + } + } + + // Set LDAP parameters, now we know we have a valid connection. + if (isset($this->_config['options']) && + is_array($this->_config['options']) && + count($this->_config['options'])) { + foreach ($this->_config['options'] as $opt => $val) { + $err = $this->setOption($opt, $val); + if (self::isError($err)) { + $current_error = $err; + $this->_link = false; + $this->_down_host_list[] = $host; + continue 2; + } + } + } + + // At this stage we have connected, bound, and set up options, + // so we have a known good LDAP server. Time to go home. + return true; + } + + + // All connection attempts have failed, return the last error. + return $current_error; + } + + /** + * Reconnect to the ldap-server. + * + * In case the connection to the LDAP + * service has dropped out for some reason, this function will reconnect, + * and re-bind if a bind has been attempted in the past. It is probably + * most useful when the server list provided to the new() or connect() + * function is an array rather than a single host name, because in that + * case it will be able to connect to a failover or secondary server in + * case the primary server goes down. + * + * This doesn't return anything, it just tries to re-establish + * the current connection. It will sleep for the current backoff + * period (seconds) before attempting the connect, and if the + * connection fails it will double the backoff period, but not + * try again. If you want to ensure a reconnection during a + * transient period of server downtime then you need to call this + * function in a loop. + * + * @access protected + * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true + */ + protected function performReconnect() + { + + // Return true if we are already connected. + if ($this->_link !== false) { + return true; + } + + // Default error message in case all connection attempts + // fail but no message is set + $current_error = new PEAR_Error('Unknown connection error'); + + // Sleep for a backoff period in seconds. + sleep($this->_config['current_backoff']); + + // Retry all available connections. + $this->_down_host_list = array(); + $msg = $this->performConnect(); + + // Bail out if that fails. + if (self::isError($msg)) { + $this->_config['current_backoff'] = + $this->_config['current_backoff'] * 2; + if ($this->_config['current_backoff'] > $this->_config['max_backoff']) { + $this->_config['current_backoff'] = $this->_config['max_backoff']; + } + return $msg; + } + + // Now we should be able to safely (re-)bind. + $msg = $this->bind(); + if (self::isError($msg)) { + $this->_config['current_backoff'] = $this->_config['current_backoff'] * 2; + if ($this->_config['current_backoff'] > $this->_config['max_backoff']) { + $this->_config['current_backoff'] = $this->_config['max_backoff']; + } + + // _config['host'] should have had the last connected host stored in it + // by performConnect(). Since we are unable to bind to that host we can safely + // assume that it is down or has some other problem. + $this->_down_host_list[] = $this->_config['host']; + return $msg; + } + + // At this stage we have connected, bound, and set up options, + // so we have a known good LDAP server. Time to go home. + $this->_config['current_backoff'] = $this->_config['min_backoff']; + return true; + } + + /** + * Starts an encrypted session + * + * @access public + * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true + */ + public function startTLS() + { + // Test to see if the server supports TLS first. + // This is done via testing the extensions offered by the server. + // The OID 1.3.6.1.4.1.1466.20037 tells us, if TLS is supported. + $rootDSE = $this->rootDse(); + if (self::isError($rootDSE)) { + return $this->raiseError("Unable to fetch rootDSE entry ". + "to see if TLS is supoported: ".$rootDSE->getMessage(), $rootDSE->getCode()); + } + + $supported_extensions = $rootDSE->getValue('supportedExtension'); + if (self::isError($supported_extensions)) { + return $this->raiseError("Unable to fetch rootDSE attribute 'supportedExtension' ". + "to see if TLS is supoported: ".$supported_extensions->getMessage(), $supported_extensions->getCode()); + } + + if (in_array('1.3.6.1.4.1.1466.20037', $supported_extensions)) { + if (false === @ldap_start_tls($this->_link)) { + return $this->raiseError("TLS not started: " . + @ldap_error($this->_link), + @ldap_errno($this->_link)); + } + return true; + } else { + return $this->raiseError("Server reports that it does not support TLS"); + } + } + + /** + * alias function of startTLS() for perl-ldap interface + * + * @return void + * @see startTLS() + */ + public function start_tls() + { + $args = func_get_args(); + return call_user_func_array(array( &$this, 'startTLS' ), $args); + } + + /** + * Close LDAP connection. + * + * Closes the connection. Use this when the session is over. + * + * @return void + */ + public function done() + { + $this->_Net_LDAP2(); + } + + /** + * Alias for {@link done()} + * + * @return void + * @see done() + */ + public function disconnect() + { + $this->done(); + } + + /** + * Destructor + * + * @access protected + */ + public function _Net_LDAP2() + { + @ldap_close($this->_link); + } + + /** + * Add a new entryobject to a directory. + * + * Use add to add a new Net_LDAP2_Entry object to the directory. + * This also links the entry to the connection used for the add, + * if it was a fresh entry ({@link Net_LDAP2_Entry::createFresh()}) + * + * @param Net_LDAP2_Entry &$entry Net_LDAP2_Entry + * + * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true + */ + public function add(&$entry) + { + if (!$entry instanceof Net_LDAP2_Entry) { + return PEAR::raiseError('Parameter to Net_LDAP2::add() must be a Net_LDAP2_Entry object.'); + } + + // Continue attempting the add operation in a loop until we + // get a success, a definitive failure, or the world ends. + $foo = 0; + while (true) { + $link = $this->getLink(); + + if ($link === false) { + // We do not have a successful connection yet. The call to + // getLink() would have kept trying if we wanted one. Go + // home now. + return PEAR::raiseError("Could not add entry " . $entry->dn() . + " no valid LDAP connection could be found."); + } + + if (@ldap_add($link, $entry->dn(), $entry->getValues())) { + // entry successfully added, we should update its $ldap reference + // in case it is not set so far (fresh entry) + if (!$entry->getLDAP() instanceof Net_LDAP2) { + $entry->setLDAP($this); + } + // store, that the entry is present inside the directory + $entry->markAsNew(false); + return true; + } else { + // We have a failure. What type? We may be able to reconnect + // and try again. + $error_code = @ldap_errno($link); + $error_name = $this->errorMessage($error_code); + + if (($error_name === 'LDAP_OPERATIONS_ERROR') && + ($this->_config['auto_reconnect'])) { + + // The server has become disconnected before trying the + // operation. We should try again, possibly with a different + // server. + $this->_link = false; + $this->performReconnect(); + } else { + // Errors other than the above catched are just passed + // back to the user so he may react upon them. + return PEAR::raiseError("Could not add entry " . $entry->dn() . " " . + $error_name, + $error_code); + } + } + } + } + + /** + * Delete an entry from the directory + * + * The object may either be a string representing the dn or a Net_LDAP2_Entry + * object. When the boolean paramter recursive is set, all subentries of the + * entry will be deleted as well. + * + * @param string|Net_LDAP2_Entry $dn DN-string or Net_LDAP2_Entry + * @param boolean $recursive Should we delete all children recursive as well? + * + * @access public + * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true + */ + public function delete($dn, $recursive = false) + { + if ($dn instanceof Net_LDAP2_Entry) { + $dn = $dn->dn(); + } + if (false === is_string($dn)) { + return PEAR::raiseError("Parameter is not a string nor an entry object!"); + } + // Recursive delete searches for children and calls delete for them + if ($recursive) { + $result = @ldap_list($this->_link, $dn, '(objectClass=*)', array(null), 0, 0); + if (@ldap_count_entries($this->_link, $result)) { + $subentry = @ldap_first_entry($this->_link, $result); + $this->delete(@ldap_get_dn($this->_link, $subentry), true); + while ($subentry = @ldap_next_entry($this->_link, $subentry)) { + $this->delete(@ldap_get_dn($this->_link, $subentry), true); + } + } + } + + // Continue attempting the delete operation in a loop until we + // get a success, a definitive failure, or the world ends. + while (true) { + $link = $this->getLink(); + + if ($link === false) { + // We do not have a successful connection yet. The call to + // getLink() would have kept trying if we wanted one. Go + // home now. + return PEAR::raiseError("Could not add entry " . $dn . + " no valid LDAP connection could be found."); + } + + if (@ldap_delete($link, $dn)) { + // entry successfully deleted. + return true; + } else { + // We have a failure. What type? + // We may be able to reconnect and try again. + $error_code = @ldap_errno($link); + $error_name = $this->errorMessage($error_code); + + if (($this->errorMessage($error_code) === 'LDAP_OPERATIONS_ERROR') && + ($this->_config['auto_reconnect'])) { + // The server has become disconnected before trying the + // operation. We should try again, possibly with a + // different server. + $this->_link = false; + $this->performReconnect(); + + } elseif ($error_code == 66) { + // Subentries present, server refused to delete. + // Deleting subentries is the clients responsibility, but + // since the user may not know of the subentries, we do not + // force that here but instead notify the developer so he + // may take actions himself. + return PEAR::raiseError("Could not delete entry $dn because of subentries. Use the recursive parameter to delete them."); + + } else { + // Errors other than the above catched are just passed + // back to the user so he may react upon them. + return PEAR::raiseError("Could not delete entry " . $dn . " " . + $error_name, + $error_code); + } + } + } + } + + /** + * Modify an ldapentry directly on the server + * + * This one takes the DN or a Net_LDAP2_Entry object and an array of actions. + * This array should be something like this: + * + * array('add' => array('attribute1' => array('val1', 'val2'), + * 'attribute2' => array('val1')), + * 'delete' => array('attribute1'), + * 'replace' => array('attribute1' => array('val1')), + * 'changes' => array('add' => ..., + * 'replace' => ..., + * 'delete' => array('attribute1', 'attribute2' => array('val1'))) + * + * The changes array is there so the order of operations can be influenced + * (the operations are done in order of appearance). + * The order of execution is as following: + * 1. adds from 'add' array + * 2. deletes from 'delete' array + * 3. replaces from 'replace' array + * 4. changes (add, replace, delete) in order of appearance + * All subarrays (add, replace, delete, changes) may be given at the same time. + * + * The function calls the corresponding functions of an Net_LDAP2_Entry + * object. A detailed description of array structures can be found there. + * + * Unlike the modification methods provided by the Net_LDAP2_Entry object, + * this method will instantly carry out an update() after each operation, + * thus modifying "directly" on the server. + * + * @param string|Net_LDAP2_Entry $entry DN-string or Net_LDAP2_Entry + * @param array $parms Array of changes + * + * @access public + * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true + */ + public function modify($entry, $parms = array()) + { + if (is_string($entry)) { + $entry = $this->getEntry($entry); + if (self::isError($entry)) { + return $entry; + } + } + if (!$entry instanceof Net_LDAP2_Entry) { + return PEAR::raiseError("Parameter is not a string nor an entry object!"); + } + + // Perform changes mentioned separately + foreach (array('add', 'delete', 'replace') as $action) { + if (isset($parms[$action])) { + $msg = $entry->$action($parms[$action]); + if (self::isError($msg)) { + return $msg; + } + $entry->setLDAP($this); + + // Because the @ldap functions are called inside Net_LDAP2_Entry::update(), + // we have to trap the error codes issued from that if we want to support + // reconnection. + while (true) { + $msg = $entry->update(); + + if (self::isError($msg)) { + // We have a failure. What type? We may be able to reconnect + // and try again. + $error_code = $msg->getCode(); + $error_name = $this->errorMessage($error_code); + + if (($this->errorMessage($error_code) === 'LDAP_OPERATIONS_ERROR') && + ($this->_config['auto_reconnect'])) { + + // The server has become disconnected before trying the + // operation. We should try again, possibly with a different + // server. + $this->_link = false; + $this->performReconnect(); + + } else { + + // Errors other than the above catched are just passed + // back to the user so he may react upon them. + return PEAR::raiseError("Could not modify entry: ".$msg->getMessage()); + } + } else { + // modification succeedet, evaluate next change + break; + } + } + } + } + + // perform combined changes in 'changes' array + if (isset($parms['changes']) && is_array($parms['changes'])) { + foreach ($parms['changes'] as $action => $value) { + + // Because the @ldap functions are called inside Net_LDAP2_Entry::update, + // we have to trap the error codes issued from that if we want to support + // reconnection. + while (true) { + $msg = $this->modify($entry, array($action => $value)); + + if (self::isError($msg)) { + // We have a failure. What type? We may be able to reconnect + // and try again. + $error_code = $msg->getCode(); + $error_name = $this->errorMessage($error_code); + + if (($this->errorMessage($error_code) === 'LDAP_OPERATIONS_ERROR') && + ($this->_config['auto_reconnect'])) { + + // The server has become disconnected before trying the + // operation. We should try again, possibly with a different + // server. + $this->_link = false; + $this->performReconnect(); + + } else { + // Errors other than the above catched are just passed + // back to the user so he may react upon them. + return $msg; + } + } else { + // modification succeedet, evaluate next change + break; + } + } + } + } + + return true; + } + + /** + * Run a ldap search query + * + * Search is used to query the ldap-database. + * $base and $filter may be ommitted. The one from config will + * then be used. $base is either a DN-string or an Net_LDAP2_Entry + * object in which case its DN willb e used. + * + * Params may contain: + * + * scope: The scope which will be used for searching + * base - Just one entry + * sub - The whole tree + * one - Immediately below $base + * sizelimit: Limit the number of entries returned (default: 0 = unlimited), + * timelimit: Limit the time spent for searching (default: 0 = unlimited), + * attrsonly: If true, the search will only return the attribute names, + * attributes: Array of attribute names, which the entry should contain. + * It is good practice to limit this to just the ones you need. + * [NOT IMPLEMENTED] + * deref: By default aliases are dereferenced to locate the base object for the search, but not when + * searching subordinates of the base object. This may be changed by specifying one of the + * following values: + * + * never - Do not dereference aliases in searching or in locating the base object of the search. + * search - Dereference aliases in subordinates of the base object in searching, but not in + * locating the base object of the search. + * find + * always + * + * Please note, that you cannot override server side limitations to sizelimit + * and timelimit: You can always only lower a given limit. + * + * @param string|Net_LDAP2_Entry $base LDAP searchbase + * @param string|Net_LDAP2_Filter $filter LDAP search filter or a Net_LDAP2_Filter object + * @param array $params Array of options + * + * @access public + * @return Net_LDAP2_Search|Net_LDAP2_Error Net_LDAP2_Search object or Net_LDAP2_Error object + * @todo implement search controls (sorting etc) + */ + public function search($base = null, $filter = null, $params = array()) + { + if (is_null($base)) { + $base = $this->_config['basedn']; + } + if ($base instanceof Net_LDAP2_Entry) { + $base = $base->dn(); // fetch DN of entry, making searchbase relative to the entry + } + if (is_null($filter)) { + $filter = $this->_config['filter']; + } + if ($filter instanceof Net_LDAP2_Filter) { + $filter = $filter->asString(); // convert Net_LDAP2_Filter to string representation + } + if (PEAR::isError($filter)) { + return $filter; + } + if (PEAR::isError($base)) { + return $base; + } + + /* setting searchparameters */ + (isset($params['sizelimit'])) ? $sizelimit = $params['sizelimit'] : $sizelimit = 0; + (isset($params['timelimit'])) ? $timelimit = $params['timelimit'] : $timelimit = 0; + (isset($params['attrsonly'])) ? $attrsonly = $params['attrsonly'] : $attrsonly = 0; + (isset($params['attributes'])) ? $attributes = $params['attributes'] : $attributes = array(); + + // Ensure $attributes to be an array in case only one + // attribute name was given as string + if (!is_array($attributes)) { + $attributes = array($attributes); + } + + // reorganize the $attributes array index keys + // sometimes there are problems with not consecutive indexes + $attributes = array_values($attributes); + + // scoping makes searches faster! + $scope = (isset($params['scope']) ? $params['scope'] : $this->_config['scope']); + + switch ($scope) { + case 'one': + $search_function = 'ldap_list'; + break; + case 'base': + $search_function = 'ldap_read'; + break; + default: + $search_function = 'ldap_search'; + } + + // Continue attempting the search operation until we get a success + // or a definitive failure. + while (true) { + $link = $this->getLink(); + $search = @call_user_func($search_function, + $link, + $base, + $filter, + $attributes, + $attrsonly, + $sizelimit, + $timelimit); + + if ($err = @ldap_errno($link)) { + if ($err == 32) { + // Errorcode 32 = no such object, i.e. a nullresult. + return $obj = new Net_LDAP2_Search ($search, $this, $attributes); + } elseif ($err == 4) { + // Errorcode 4 = sizelimit exeeded. + return $obj = new Net_LDAP2_Search ($search, $this, $attributes); + } elseif ($err == 87) { + // bad search filter + return $this->raiseError($this->errorMessage($err) . "($filter)", $err); + } elseif (($err == 1) && ($this->_config['auto_reconnect'])) { + // Errorcode 1 = LDAP_OPERATIONS_ERROR but we can try a reconnect. + $this->_link = false; + $this->performReconnect(); + } else { + $msg = "\nParameters:\nBase: $base\nFilter: $filter\nScope: $scope"; + return $this->raiseError($this->errorMessage($err) . $msg, $err); + } + } else { + return $obj = new Net_LDAP2_Search($search, $this, $attributes); + } + } + } + + /** + * Set an LDAP option + * + * @param string $option Option to set + * @param mixed $value Value to set Option to + * + * @access public + * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true + */ + public function setOption($option, $value) + { + if ($this->_link) { + if (defined($option)) { + if (@ldap_set_option($this->_link, constant($option), $value)) { + return true; + } else { + $err = @ldap_errno($this->_link); + if ($err) { + $msg = @ldap_err2str($err); + } else { + $err = NET_LDAP2_ERROR; + $msg = $this->errorMessage($err); + } + return $this->raiseError($msg, $err); + } + } else { + return $this->raiseError("Unkown Option requested"); + } + } else { + return $this->raiseError("Could not set LDAP option: No LDAP connection"); + } + } + + /** + * Get an LDAP option value + * + * @param string $option Option to get + * + * @access public + * @return Net_LDAP2_Error|string Net_LDAP2_Error or option value + */ + public function getOption($option) + { + if ($this->_link) { + if (defined($option)) { + if (@ldap_get_option($this->_link, constant($option), $value)) { + return $value; + } else { + $err = @ldap_errno($this->_link); + if ($err) { + $msg = @ldap_err2str($err); + } else { + $err = NET_LDAP2_ERROR; + $msg = $this->errorMessage($err); + } + return $this->raiseError($msg, $err); + } + } else { + $this->raiseError("Unkown Option requested"); + } + } else { + $this->raiseError("No LDAP connection"); + } + } + + /** + * Get the LDAP_PROTOCOL_VERSION that is used on the connection. + * + * A lot of ldap functionality is defined by what protocol version the ldap server speaks. + * This might be 2 or 3. + * + * @return int + */ + public function getLDAPVersion() + { + if ($this->_link) { + $version = $this->getOption("LDAP_OPT_PROTOCOL_VERSION"); + } else { + $version = $this->_config['version']; + } + return $version; + } + + /** + * Set the LDAP_PROTOCOL_VERSION that is used on the connection. + * + * @param int $version LDAP-version that should be used + * @param boolean $force If set to true, the check against the rootDSE will be skipped + * + * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true + * @todo Checking via the rootDSE takes much time - why? fetching and instanciation is quick! + */ + public function setLDAPVersion($version = 0, $force = false) + { + if (!$version) { + $version = $this->_config['version']; + } + + // + // Check to see if the server supports this version first. + // + // Todo: Why is this so horribly slow? + // $this->rootDse() is very fast, as well as Net_LDAP2_RootDSE::fetch() + // seems like a problem at copiyng the object inside PHP?? + // Additionally, this is not always reproducable... + // + if (!$force) { + $rootDSE = $this->rootDse(); + if ($rootDSE instanceof Net_LDAP2_Error) { + return $rootDSE; + } else { + $supported_versions = $rootDSE->getValue('supportedLDAPVersion'); + if (is_string($supported_versions)) { + $supported_versions = array($supported_versions); + } + $check_ok = in_array($version, $supported_versions); + } + } + + if ($force || $check_ok) { + return $this->setOption("LDAP_OPT_PROTOCOL_VERSION", $version); + } else { + return $this->raiseError("LDAP Server does not support protocol version " . $version); + } + } + + + /** + * Tells if a DN does exist in the directory + * + * @param string|Net_LDAP2_Entry $dn The DN of the object to test + * + * @return boolean|Net_LDAP2_Error + */ + public function dnExists($dn) + { + if (PEAR::isError($dn)) { + return $dn; + } + if ($dn instanceof Net_LDAP2_Entry) { + $dn = $dn->dn(); + } + if (false === is_string($dn)) { + return PEAR::raiseError('Parameter $dn is not a string nor an entry object!'); + } + + // make dn relative to parent + $base = Net_LDAP2_Util::ldap_explode_dn($dn, array('casefold' => 'none', 'reverse' => false, 'onlyvalues' => false)); + if (self::isError($base)) { + return $base; + } + $entry_rdn = array_shift($base); + if (is_array($entry_rdn)) { + // maybe the dn consist of a multivalued RDN, we must build the dn in this case + // because the $entry_rdn is an array! + $filter_dn = Net_LDAP2_Util::canonical_dn($entry_rdn); + } + $base = Net_LDAP2_Util::canonical_dn($base); + + $result = @ldap_list($this->_link, $base, $entry_rdn, array(), 1, 1); + if (@ldap_count_entries($this->_link, $result)) { + return true; + } + if (ldap_errno($this->_link) == 32) { + return false; + } + if (ldap_errno($this->_link) != 0) { + return PEAR::raiseError(ldap_error($this->_link), ldap_errno($this->_link)); + } + return false; + } + + + /** + * Get a specific entry based on the DN + * + * @param string $dn DN of the entry that should be fetched + * @param array $attr Array of Attributes to select. If ommitted, all attributes are fetched. + * + * @return Net_LDAP2_Entry|Net_LDAP2_Error Reference to a Net_LDAP2_Entry object or Net_LDAP2_Error object + * @todo Maybe check against the shema should be done to be sure the attribute type exists + */ + public function &getEntry($dn, $attr = array()) + { + if (!is_array($attr)) { + $attr = array($attr); + } + $result = $this->search($dn, '(objectClass=*)', + array('scope' => 'base', 'attributes' => $attr)); + if (self::isError($result)) { + return $result; + } elseif ($result->count() == 0) { + return PEAR::raiseError('Could not fetch entry '.$dn.': no entry found'); + } + $entry = $result->shiftEntry(); + if (false == $entry) { + return PEAR::raiseError('Could not fetch entry (error retrieving entry from search result)'); + } + return $entry; + } + + /** + * Rename or move an entry + * + * This method will instantly carry out an update() after the move, + * so the entry is moved instantly. + * You can pass an optional Net_LDAP2 object. In this case, a cross directory + * move will be performed which deletes the entry in the source (THIS) directory + * and adds it in the directory $target_ldap. + * A cross directory move will switch the Entrys internal LDAP reference so + * updates to the entry will go to the new directory. + * + * Note that if you want to do a cross directory move, you need to + * pass an Net_LDAP2_Entry object, otherwise the attributes will be empty. + * + * @param string|Net_LDAP2_Entry $entry Entry DN or Entry object + * @param string $newdn New location + * @param Net_LDAP2 $target_ldap (optional) Target directory for cross server move; should be passed via reference + * + * @return Net_LDAP2_Error|true + */ + public function move($entry, $newdn, $target_ldap = null) + { + if (is_string($entry)) { + $entry_o = $this->getEntry($entry); + } else { + $entry_o =& $entry; + } + if (!$entry_o instanceof Net_LDAP2_Entry) { + return PEAR::raiseError('Parameter $entry is expected to be a Net_LDAP2_Entry object! (If DN was passed, conversion failed)'); + } + if (null !== $target_ldap && !$target_ldap instanceof Net_LDAP2) { + return PEAR::raiseError('Parameter $target_ldap is expected to be a Net_LDAP2 object!'); + } + + if ($target_ldap && $target_ldap !== $this) { + // cross directory move + if (is_string($entry)) { + return PEAR::raiseError('Unable to perform cross directory move: operation requires a Net_LDAP2_Entry object'); + } + if ($target_ldap->dnExists($newdn)) { + return PEAR::raiseError('Unable to perform cross directory move: entry does exist in target directory'); + } + $entry_o->dn($newdn); + $res = $target_ldap->add($entry_o); + if (self::isError($res)) { + return PEAR::raiseError('Unable to perform cross directory move: '.$res->getMessage().' in target directory'); + } + $res = $this->delete($entry_o->currentDN()); + if (self::isError($res)) { + $res2 = $target_ldap->delete($entry_o); // undo add + if (self::isError($res2)) { + $add_error_string = 'Additionally, the deletion (undo add) of $entry in target directory failed.'; + } + return PEAR::raiseError('Unable to perform cross directory move: '.$res->getMessage().' in source directory. '.$add_error_string); + } + $entry_o->setLDAP($target_ldap); + return true; + } else { + // local move + $entry_o->dn($newdn); + $entry_o->setLDAP($this); + return $entry_o->update(); + } + } + + /** + * Copy an entry to a new location + * + * The entry will be immediately copied. + * Please note that only attributes you have + * selected will be copied. + * + * @param Net_LDAP2_Entry &$entry Entry object + * @param string $newdn New FQF-DN of the entry + * + * @return Net_LDAP2_Error|Net_LDAP2_Entry Error Message or reference to the copied entry + */ + public function ©(&$entry, $newdn) + { + if (!$entry instanceof Net_LDAP2_Entry) { + return PEAR::raiseError('Parameter $entry is expected to be a Net_LDAP2_Entry object!'); + } + + $newentry = Net_LDAP2_Entry::createFresh($newdn, $entry->getValues()); + $result = $this->add($newentry); + + if ($result instanceof Net_LDAP2_Error) { + return $result; + } else { + return $newentry; + } + } + + + /** + * Returns the string for an ldap errorcode. + * + * Made to be able to make better errorhandling + * Function based on DB::errorMessage() + * Tip: The best description of the errorcodes is found here: + * http://www.directory-info.com/LDAP2/LDAPErrorCodes.html + * + * @param int $errorcode Error code + * + * @return string The errorstring for the error. + */ + public function errorMessage($errorcode) + { + $errorMessages = array( + 0x00 => "LDAP_SUCCESS", + 0x01 => "LDAP_OPERATIONS_ERROR", + 0x02 => "LDAP_PROTOCOL_ERROR", + 0x03 => "LDAP_TIMELIMIT_EXCEEDED", + 0x04 => "LDAP_SIZELIMIT_EXCEEDED", + 0x05 => "LDAP_COMPARE_FALSE", + 0x06 => "LDAP_COMPARE_TRUE", + 0x07 => "LDAP_AUTH_METHOD_NOT_SUPPORTED", + 0x08 => "LDAP_STRONG_AUTH_REQUIRED", + 0x09 => "LDAP_PARTIAL_RESULTS", + 0x0a => "LDAP_REFERRAL", + 0x0b => "LDAP_ADMINLIMIT_EXCEEDED", + 0x0c => "LDAP_UNAVAILABLE_CRITICAL_EXTENSION", + 0x0d => "LDAP_CONFIDENTIALITY_REQUIRED", + 0x0e => "LDAP_SASL_BIND_INPROGRESS", + 0x10 => "LDAP_NO_SUCH_ATTRIBUTE", + 0x11 => "LDAP_UNDEFINED_TYPE", + 0x12 => "LDAP_INAPPROPRIATE_MATCHING", + 0x13 => "LDAP_CONSTRAINT_VIOLATION", + 0x14 => "LDAP_TYPE_OR_VALUE_EXISTS", + 0x15 => "LDAP_INVALID_SYNTAX", + 0x20 => "LDAP_NO_SUCH_OBJECT", + 0x21 => "LDAP_ALIAS_PROBLEM", + 0x22 => "LDAP_INVALID_DN_SYNTAX", + 0x23 => "LDAP_IS_LEAF", + 0x24 => "LDAP_ALIAS_DEREF_PROBLEM", + 0x30 => "LDAP_INAPPROPRIATE_AUTH", + 0x31 => "LDAP_INVALID_CREDENTIALS", + 0x32 => "LDAP_INSUFFICIENT_ACCESS", + 0x33 => "LDAP_BUSY", + 0x34 => "LDAP_UNAVAILABLE", + 0x35 => "LDAP_UNWILLING_TO_PERFORM", + 0x36 => "LDAP_LOOP_DETECT", + 0x3C => "LDAP_SORT_CONTROL_MISSING", + 0x3D => "LDAP_INDEX_RANGE_ERROR", + 0x40 => "LDAP_NAMING_VIOLATION", + 0x41 => "LDAP_OBJECT_CLASS_VIOLATION", + 0x42 => "LDAP_NOT_ALLOWED_ON_NONLEAF", + 0x43 => "LDAP_NOT_ALLOWED_ON_RDN", + 0x44 => "LDAP_ALREADY_EXISTS", + 0x45 => "LDAP_NO_OBJECT_CLASS_MODS", + 0x46 => "LDAP_RESULTS_TOO_LARGE", + 0x47 => "LDAP_AFFECTS_MULTIPLE_DSAS", + 0x50 => "LDAP_OTHER", + 0x51 => "LDAP_SERVER_DOWN", + 0x52 => "LDAP_LOCAL_ERROR", + 0x53 => "LDAP_ENCODING_ERROR", + 0x54 => "LDAP_DECODING_ERROR", + 0x55 => "LDAP_TIMEOUT", + 0x56 => "LDAP_AUTH_UNKNOWN", + 0x57 => "LDAP_FILTER_ERROR", + 0x58 => "LDAP_USER_CANCELLED", + 0x59 => "LDAP_PARAM_ERROR", + 0x5a => "LDAP_NO_MEMORY", + 0x5b => "LDAP_CONNECT_ERROR", + 0x5c => "LDAP_NOT_SUPPORTED", + 0x5d => "LDAP_CONTROL_NOT_FOUND", + 0x5e => "LDAP_NO_RESULTS_RETURNED", + 0x5f => "LDAP_MORE_RESULTS_TO_RETURN", + 0x60 => "LDAP_CLIENT_LOOP", + 0x61 => "LDAP_REFERRAL_LIMIT_EXCEEDED", + 1000 => "Unknown Net_LDAP2 Error" + ); + + return isset($errorMessages[$errorcode]) ? + $errorMessages[$errorcode] : + $errorMessages[NET_LDAP2_ERROR] . ' (' . $errorcode . ')'; + } + + /** + * Gets a rootDSE object + * + * This either fetches a fresh rootDSE object or returns it from + * the internal cache for performance reasons, if possible. + * + * @param array $attrs Array of attributes to search for + * + * @access public + * @return Net_LDAP2_Error|Net_LDAP2_RootDSE Net_LDAP2_Error or Net_LDAP2_RootDSE object + */ + public function &rootDse($attrs = null) + { + if ($attrs !== null && !is_array($attrs)) { + return PEAR::raiseError('Parameter $attr is expected to be an array!'); + } + + $attrs_signature = serialize($attrs); + + // see if we need to fetch a fresh object, or if we already + // requested this object with the same attributes + if (true || !array_key_exists($attrs_signature, $this->_rootDSE_cache)) { + $rootdse =& Net_LDAP2_RootDSE::fetch($this, $attrs); + if ($rootdse instanceof Net_LDAP2_Error) { + return $rootdse; + } + + // search was ok, store rootDSE in cache + $this->_rootDSE_cache[$attrs_signature] = $rootdse; + } + return $this->_rootDSE_cache[$attrs_signature]; + } + + /** + * Alias function of rootDse() for perl-ldap interface + * + * @access public + * @see rootDse() + * @return Net_LDAP2_Error|Net_LDAP2_RootDSE + */ + public function &root_dse() + { + $args = func_get_args(); + return call_user_func_array(array(&$this, 'rootDse'), $args); + } + + /** + * Get a schema object + * + * @param string $dn (optional) Subschema entry dn + * + * @access public + * @return Net_LDAP2_Schema|Net_LDAP2_Error Net_LDAP2_Schema or Net_LDAP2_Error object + */ + public function &schema($dn = null) + { + // Schema caching by Knut-Olav Hoven + // If a schema caching object is registered, we use that to fetch + // a schema object. + // See registerSchemaCache() for more info on this. + if ($this->_schema === null) { + if ($this->_schema_cache) { + $cached_schema = $this->_schema_cache->loadSchema(); + if ($cached_schema instanceof Net_LDAP2_Error) { + return $cached_schema; // route error to client + } else { + if ($cached_schema instanceof Net_LDAP2_Schema) { + $this->_schema = $cached_schema; + } + } + } + } + + // Fetch schema, if not tried before and no cached version available. + // If we are already fetching the schema, we will skip fetching. + if ($this->_schema === null) { + // store a temporary error message so subsequent calls to schema() can + // detect, that we are fetching the schema already. + // Otherwise we will get an infinite loop at Net_LDAP2_Schema::fetch() + $this->_schema = new Net_LDAP2_Error('Schema not initialized'); + $this->_schema = Net_LDAP2_Schema::fetch($this, $dn); + + // If schema caching is active, advise the cache to store the schema + if ($this->_schema_cache) { + $caching_result = $this->_schema_cache->storeSchema($this->_schema); + if ($caching_result instanceof Net_LDAP2_Error) { + return $caching_result; // route error to client + } + } + } + return $this->_schema; + } + + /** + * Enable/disable persistent schema caching + * + * Sometimes it might be useful to allow your scripts to cache + * the schema information on disk, so the schema is not fetched + * every time the script runs which could make your scripts run + * faster. + * + * This method allows you to register a custom object that + * implements your schema cache. Please see the SchemaCache interface + * (SchemaCache.interface.php) for informations on how to implement this. + * To unregister the cache, pass null as $cache parameter. + * + * For ease of use, Net_LDAP2 provides a simple file based cache + * which is used in the example below. You may use this, for example, + * to store the schema in a linux tmpfs which results in the schema + * beeing cached inside the RAM which allows nearly instant access. + * <code> + * // Create the simple file cache object that comes along with Net_LDAP2 + * $mySchemaCache_cfg = array( + * 'path' => '/tmp/Net_LDAP2_Schema.cache', + * 'max_age' => 86400 // max age is 24 hours (in seconds) + * ); + * $mySchemaCache = new Net_LDAP2_SimpleFileSchemaCache($mySchemaCache_cfg); + * $ldap = new Net_LDAP2::connect(...); + * $ldap->registerSchemaCache($mySchemaCache); // enable caching + * // now each call to $ldap->schema() will get the schema from disk! + * </code> + * + * @param Net_LDAP2_SchemaCache|null $cache Object implementing the Net_LDAP2_SchemaCache interface + * + * @return true|Net_LDAP2_Error + */ + public function registerSchemaCache($cache) { + if (is_null($cache) + || (is_object($cache) && in_array('Net_LDAP2_SchemaCache', class_implements($cache))) ) { + $this->_schema_cache = $cache; + return true; + } else { + return new Net_LDAP2_Error('Custom schema caching object is either no '. + 'valid object or does not implement the Net_LDAP2_SchemaCache interface!'); + } + } + + + /** + * Checks if phps ldap-extension is loaded + * + * If it is not loaded, it tries to load it manually using PHPs dl(). + * It knows both windows-dll and *nix-so. + * + * @static + * @return Net_LDAP2_Error|true + */ + public static function checkLDAPExtension() + { + if (!extension_loaded('ldap') && !@dl('ldap.' . PHP_SHLIB_SUFFIX)) { + return new Net_LDAP2_Error("It seems that you do not have the ldap-extension installed. Please install it before using the Net_LDAP2 package."); + } else { + return true; + } + } + + /** + * Encodes given attributes to UTF8 if needed by schema + * + * This function takes attributes in an array and then checks against the schema if they need + * UTF8 encoding. If that is so, they will be encoded. An encoded array will be returned and + * can be used for adding or modifying. + * + * $attributes is expected to be an array with keys describing + * the attribute names and the values as the value of this attribute: + * <code>$attributes = array('cn' => 'foo', 'attr2' => array('mv1', 'mv2'));</code> + * + * @param array $attributes Array of attributes + * + * @access public + * @return array|Net_LDAP2_Error Array of UTF8 encoded attributes or Error + */ + public function utf8Encode($attributes) + { + return $this->utf8($attributes, 'utf8_encode'); + } + + /** + * Decodes the given attribute values if needed by schema + * + * $attributes is expected to be an array with keys describing + * the attribute names and the values as the value of this attribute: + * <code>$attributes = array('cn' => 'foo', 'attr2' => array('mv1', 'mv2'));</code> + * + * @param array $attributes Array of attributes + * + * @access public + * @see utf8Encode() + * @return array|Net_LDAP2_Error Array with decoded attribute values or Error + */ + public function utf8Decode($attributes) + { + return $this->utf8($attributes, 'utf8_decode'); + } + + /** + * Encodes or decodes attribute values if needed + * + * @param array $attributes Array of attributes + * @param array $function Function to apply to attribute values + * + * @access protected + * @return array|Net_LDAP2_Error Array of attributes with function applied to values or Error + */ + protected function utf8($attributes, $function) + { + if (!is_array($attributes) || array_key_exists(0, $attributes)) { + return PEAR::raiseError('Parameter $attributes is expected to be an associative array'); + } + + if (!$this->_schema) { + $this->_schema = $this->schema(); + } + + if (!$this->_link || self::isError($this->_schema) || !function_exists($function)) { + return $attributes; + } + + if (is_array($attributes) && count($attributes) > 0) { + + foreach ($attributes as $k => $v) { + + if (!isset($this->_schemaAttrs[$k])) { + + $attr = $this->_schema->get('attribute', $k); + if (self::isError($attr)) { + continue; + } + + if (false !== strpos($attr['syntax'], '1.3.6.1.4.1.1466.115.121.1.15')) { + $encode = true; + } else { + $encode = false; + } + $this->_schemaAttrs[$k] = $encode; + + } else { + $encode = $this->_schemaAttrs[$k]; + } + + if ($encode) { + if (is_array($v)) { + foreach ($v as $ak => $av) { + $v[$ak] = call_user_func($function, $av); + } + } else { + $v = call_user_func($function, $v); + } + } + $attributes[$k] = $v; + } + } + return $attributes; + } + + /** + * Get the LDAP link resource. It will loop attempting to + * re-establish the connection if the connection attempt fails and + * auto_reconnect has been turned on (see the _config array documentation). + * + * @access public + * @return resource LDAP link + */ + public function &getLink() + { + if ($this->_config['auto_reconnect']) { + while (true) { + // + // Return the link handle if we are already connected. Otherwise + // try to reconnect. + // + if ($this->_link !== false) { + return $this->_link; + } else { + $this->performReconnect(); + } + } + } + return $this->_link; + } +} + +/** +* Net_LDAP2_Error implements a class for reporting portable LDAP error messages. +* +* @category Net +* @package Net_LDAP2 +* @author Tarjej Huse <tarjei@bergfald.no> +* @license http://www.gnu.org/copyleft/lesser.html LGPL +* @link http://pear.php.net/package/Net_LDAP22/ +*/ +class Net_LDAP2_Error extends PEAR_Error +{ + /** + * Net_LDAP2_Error constructor. + * + * @param string $message String with error message. + * @param integer $code Net_LDAP2 error code + * @param integer $mode what "error mode" to operate in + * @param mixed $level what error level to use for $mode & PEAR_ERROR_TRIGGER + * @param mixed $debuginfo additional debug info, such as the last query + * + * @access public + * @see PEAR_Error + */ + public function __construct($message = 'Net_LDAP2_Error', $code = NET_LDAP2_ERROR, $mode = PEAR_ERROR_RETURN, + $level = E_USER_NOTICE, $debuginfo = null) + { + if (is_int($code)) { + $this->PEAR_Error($message . ': ' . Net_LDAP2::errorMessage($code), $code, $mode, $level, $debuginfo); + } else { + $this->PEAR_Error("$message: $code", NET_LDAP2_ERROR, $mode, $level, $debuginfo); + } + } +} + +?> diff --git a/plugins/LdapCommon/extlib/Net/LDAP2/Entry.php b/plugins/LdapCommon/extlib/Net/LDAP2/Entry.php new file mode 100644 index 000000000..66de96678 --- /dev/null +++ b/plugins/LdapCommon/extlib/Net/LDAP2/Entry.php @@ -0,0 +1,1055 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +/** +* File containing the Net_LDAP2_Entry interface class. +* +* PHP version 5 +* +* @category Net +* @package Net_LDAP2 +* @author Jan Wagner <wagner@netsols.de> +* @author Tarjej Huse <tarjei@bergfald.no> +* @author Benedikt Hallinger <beni@php.net> +* @copyright 2009 Tarjej Huse, Jan Wagner, Benedikt Hallinger +* @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 +* @version SVN: $Id: Entry.php 286787 2009-08-04 06:03:12Z beni $ +* @link http://pear.php.net/package/Net_LDAP2/ +*/ + +/** +* Includes +*/ +require_once 'PEAR.php'; +require_once 'Util.php'; + +/** +* Object representation of a directory entry +* +* This class represents a directory entry. You can add, delete, replace +* attributes and their values, rename the entry, delete the entry. +* +* @category Net +* @package Net_LDAP2 +* @author Jan Wagner <wagner@netsols.de> +* @author Tarjej Huse <tarjei@bergfald.no> +* @author Benedikt Hallinger <beni@php.net> +* @license http://www.gnu.org/copyleft/lesser.html LGPL +* @link http://pear.php.net/package/Net_LDAP2/ +*/ +class Net_LDAP2_Entry extends PEAR +{ + /** + * Entry ressource identifier + * + * @access protected + * @var ressource + */ + protected $_entry = null; + + /** + * LDAP ressource identifier + * + * @access protected + * @var ressource + */ + protected $_link = null; + + /** + * Net_LDAP2 object + * + * This object will be used for updating and schema checking + * + * @access protected + * @var object Net_LDAP2 + */ + protected $_ldap = null; + + /** + * Distinguished name of the entry + * + * @access protected + * @var string + */ + protected $_dn = null; + + /** + * Attributes + * + * @access protected + * @var array + */ + protected $_attributes = array(); + + /** + * Original attributes before any modification + * + * @access protected + * @var array + */ + protected $_original = array(); + + + /** + * Map of attribute names + * + * @access protected + * @var array + */ + protected $_map = array(); + + + /** + * Is this a new entry? + * + * @access protected + * @var boolean + */ + protected $_new = true; + + /** + * New distinguished name + * + * @access protected + * @var string + */ + protected $_newdn = null; + + /** + * Shall the entry be deleted? + * + * @access protected + * @var boolean + */ + protected $_delete = false; + + /** + * Map with changes to the entry + * + * @access protected + * @var array + */ + protected $_changes = array("add" => array(), + "delete" => array(), + "replace" => array() + ); + /** + * Internal Constructor + * + * Constructor of the entry. Sets up the distinguished name and the entries + * attributes. + * You should not call this method manually! Use {@link Net_LDAP2_Entry::createFresh()} + * or {@link Net_LDAP2_Entry::createConnected()} instead! + * + * @param Net_LDAP2|ressource|array &$ldap Net_LDAP2 object, ldap-link ressource or array of attributes + * @param string|ressource $entry Either a DN or a LDAP-Entry ressource + * + * @access protected + * @return none + */ + protected function __construct(&$ldap, $entry = null) + { + $this->PEAR('Net_LDAP2_Error'); + + // set up entry resource or DN + if (is_resource($entry)) { + $this->_entry = &$entry; + } else { + $this->_dn = $entry; + } + + // set up LDAP link + if ($ldap instanceof Net_LDAP2) { + $this->_ldap = &$ldap; + $this->_link = $ldap->getLink(); + } elseif (is_resource($ldap)) { + $this->_link = $ldap; + } elseif (is_array($ldap)) { + // Special case: here $ldap is an array of attributes, + // this means, we have no link. This is a "virtual" entry. + // We just set up the attributes so one can work with the object + // as expected, but an update() fails unless setLDAP() is called. + $this->setAttributes($ldap); + } + + // if this is an entry existing in the directory, + // then set up as old and fetch attrs + if (is_resource($this->_entry) && is_resource($this->_link)) { + $this->_new = false; + $this->_dn = @ldap_get_dn($this->_link, $this->_entry); + $this->setAttributes(); // fetch attributes from server + } + } + + /** + * Creates a fresh entry that may be added to the directory later on + * + * Use this method, if you want to initialize a fresh entry. + * + * The method should be called statically: $entry = Net_LDAP2_Entry::createFresh(); + * You should put a 'objectClass' attribute into the $attrs so the directory server + * knows which object you want to create. However, you may omit this in case you + * don't want to add this entry to a directory server. + * + * The attributes parameter is as following: + * <code> + * $attrs = array( 'attribute1' => array('value1', 'value2'), + * 'attribute2' => 'single value' + * ); + * </code> + * + * @param string $dn DN of the Entry + * @param array $attrs Attributes of the entry + * + * @static + * @return Net_LDAP2_Entry|Net_LDAP2_Error + */ + public static function createFresh($dn, $attrs = array()) + { + if (!is_array($attrs)) { + return PEAR::raiseError("Unable to create fresh entry: Parameter \$attrs needs to be an array!"); + } + + $entry = new Net_LDAP2_Entry($attrs, $dn); + return $entry; + } + + /** + * Creates a Net_LDAP2_Entry object out of an ldap entry resource + * + * Use this method, if you want to initialize an entry object that is + * already present in some directory and that you have read manually. + * + * Please note, that if you want to create an entry object that represents + * some already existing entry, you should use {@link createExisting()}. + * + * The method should be called statically: $entry = Net_LDAP2_Entry::createConnected(); + * + * @param Net_LDAP2 $ldap Net_LDA2 object + * @param resource $entry PHP LDAP entry resource + * + * @static + * @return Net_LDAP2_Entry|Net_LDAP2_Error + */ + public static function createConnected($ldap, $entry) + { + if (!$ldap instanceof Net_LDAP2) { + return PEAR::raiseError("Unable to create connected entry: Parameter \$ldap needs to be a Net_LDAP2 object!"); + } + if (!is_resource($entry)) { + return PEAR::raiseError("Unable to create connected entry: Parameter \$entry needs to be a ldap entry resource!"); + } + + $entry = new Net_LDAP2_Entry($ldap, $entry); + return $entry; + } + + /** + * Creates an Net_LDAP2_Entry object that is considered already existing + * + * Use this method, if you want to modify an already existing entry + * without fetching it first. + * In most cases however, it is better to fetch the entry via Net_LDAP2->getEntry()! + * + * Please note that you should take care if you construct entries manually with this + * because you may get weird synchronisation problems. + * The attributes and values as well as the entry itself are considered existent + * which may produce errors if you try to modify an entry which doesn't really exist + * or if you try to overwrite some attribute with an value already present. + * + * This method is equal to calling createFresh() and after that markAsNew(FALSE). + * + * The method should be called statically: $entry = Net_LDAP2_Entry::createExisting(); + * + * The attributes parameter is as following: + * <code> + * $attrs = array( 'attribute1' => array('value1', 'value2'), + * 'attribute2' => 'single value' + * ); + * </code> + * + * @param string $dn DN of the Entry + * @param array $attrs Attributes of the entry + * + * @static + * @return Net_LDAP2_Entry|Net_LDAP2_Error + */ + public static function createExisting($dn, $attrs = array()) + { + if (!is_array($attrs)) { + return PEAR::raiseError("Unable to create entry object: Parameter \$attrs needs to be an array!"); + } + + $entry = Net_LDAP2_Entry::createFresh($dn, $attrs); + if ($entry instanceof Net_LDAP2_Error) { + return $entry; + } else { + $entry->markAsNew(false); + return $entry; + } + } + + /** + * Get or set the distinguished name of the entry + * + * If called without an argument the current (or the new DN if set) DN gets returned. + * If you provide an DN, this entry is moved to the new location specified if a DN existed. + * If the DN was not set, the DN gets initialized. Call {@link update()} to actually create + * the new Entry in the directory. + * To fetch the current active DN after setting a new DN but before an update(), you can use + * {@link currentDN()} to retrieve the DN that is currently active. + * + * Please note that special characters (eg german umlauts) should be encoded using utf8_encode(). + * You may use {@link Net_LDAP2_Util::canonical_dn()} for properly encoding of the DN. + * + * @param string $dn New distinguished name + * + * @access public + * @return string|true Distinguished name (or true if a new DN was provided) + */ + public function dn($dn = null) + { + if (false == is_null($dn)) { + if (is_null($this->_dn)) { + $this->_dn = $dn; + } else { + $this->_newdn = $dn; + } + return true; + } + return (isset($this->_newdn) ? $this->_newdn : $this->currentDN()); + } + + /** + * Renames or moves the entry + * + * This is just a convinience alias to {@link dn()} + * to make your code more meaningful. + * + * @param string $newdn The new DN + * + * @return true + */ + public function move($newdn) + { + return $this->dn($newdn); + } + + /** + * Sets the internal attributes array + * + * This fetches the values for the attributes from the server. + * The attribute Syntax will be checked so binary attributes will be returned + * as binary values. + * + * Attributes may be passed directly via the $attributes parameter to setup this + * entry manually. This overrides attribute fetching from the server. + * + * @param array $attributes Attributes to set for this entry + * + * @access protected + * @return void + */ + protected function setAttributes($attributes = null) + { + /* + * fetch attributes from the server + */ + if (is_null($attributes) && is_resource($this->_entry) && is_resource($this->_link)) { + // fetch schema + if ($this->_ldap instanceof Net_LDAP2) { + $schema =& $this->_ldap->schema(); + } + // fetch attributes + $attributes = array(); + do { + if (empty($attr)) { + $ber = null; + $attr = @ldap_first_attribute($this->_link, $this->_entry, $ber); + } else { + $attr = @ldap_next_attribute($this->_link, $this->_entry, $ber); + } + if ($attr) { + $func = 'ldap_get_values'; // standard function to fetch value + + // Try to get binary values as binary data + if ($schema instanceof Net_LDAP2_Schema) { + if ($schema->isBinary($attr)) { + $func = 'ldap_get_values_len'; + } + } + // fetch attribute value (needs error checking?) + $attributes[$attr] = $func($this->_link, $this->_entry, $attr); + } + } while ($attr); + } + + /* + * set attribute data directly, if passed + */ + if (is_array($attributes) && count($attributes) > 0) { + if (isset($attributes["count"]) && is_numeric($attributes["count"])) { + unset($attributes["count"]); + } + foreach ($attributes as $k => $v) { + // attribute names should not be numeric + if (is_numeric($k)) { + continue; + } + // map generic attribute name to real one + $this->_map[strtolower($k)] = $k; + // attribute values should be in an array + if (false == is_array($v)) { + $v = array($v); + } + // remove the value count (comes from ldap server) + if (isset($v["count"])) { + unset($v["count"]); + } + $this->_attributes[$k] = $v; + } + } + + // save a copy for later use + $this->_original = $this->_attributes; + } + + /** + * Get the values of all attributes in a hash + * + * The returned hash has the form + * <code>array('attributename' => 'single value', + * 'attributename' => array('value1', value2', value3'))</code> + * + * @access public + * @return array Hash of all attributes with their values + */ + public function getValues() + { + $attrs = array(); + foreach ($this->_attributes as $attr => $value) { + $attrs[$attr] = $this->getValue($attr); + } + return $attrs; + } + + /** + * Get the value of a specific attribute + * + * The first parameter is the name of the attribute + * The second parameter influences the way the value is returned: + * 'single': only the first value is returned as string + * 'all': all values including the value count are returned in an + * array + * 'default': in all other cases an attribute value with a single value is + * returned as string, if it has multiple values it is returned + * as an array (without value count) + * + * @param string $attr Attribute name + * @param string $option Option + * + * @access public + * @return string|array|PEAR_Error string, array or PEAR_Error + */ + public function getValue($attr, $option = null) + { + $attr = $this->getAttrName($attr); + + if (false == array_key_exists($attr, $this->_attributes)) { + return PEAR::raiseError("Unknown attribute ($attr) requested"); + } + + $value = $this->_attributes[$attr]; + + if ($option == "single" || (count($value) == 1 && $option != 'all')) { + $value = array_shift($value); + } + + return $value; + } + + /** + * Alias function of getValue for perl-ldap interface + * + * @see getValue() + * @return string|array|PEAR_Error + */ + public function get_value() + { + $args = func_get_args(); + return call_user_func_array(array( &$this, 'getValue' ), $args); + } + + /** + * Returns an array of attributes names + * + * @access public + * @return array Array of attribute names + */ + public function attributes() + { + return array_keys($this->_attributes); + } + + /** + * Returns whether an attribute exists or not + * + * @param string $attr Attribute name + * + * @access public + * @return boolean + */ + public function exists($attr) + { + $attr = $this->getAttrName($attr); + return array_key_exists($attr, $this->_attributes); + } + + /** + * Adds a new attribute or a new value to an existing attribute + * + * The paramter has to be an array of the form: + * array('attributename' => 'single value', + * 'attributename' => array('value1', 'value2)) + * When the attribute already exists the values will be added, else the + * attribute will be created. These changes are local to the entry and do + * not affect the entry on the server until update() is called. + * + * Note, that you can add values of attributes that you haven't selected, but if + * you do so, {@link getValue()} and {@link getValues()} will only return the + * values you added, _NOT_ all values present on the server. To avoid this, just refetch + * the entry after calling {@link update()} or select the attribute. + * + * @param array $attr Attributes to add + * + * @access public + * @return true|Net_LDAP2_Error + */ + public function add($attr = array()) + { + if (false == is_array($attr)) { + return PEAR::raiseError("Parameter must be an array"); + } + foreach ($attr as $k => $v) { + $k = $this->getAttrName($k); + if (false == is_array($v)) { + // Do not add empty values + if ($v == null) { + continue; + } else { + $v = array($v); + } + } + // add new values to existing attribute or add new attribute + if ($this->exists($k)) { + $this->_attributes[$k] = array_unique(array_merge($this->_attributes[$k], $v)); + } else { + $this->_map[strtolower($k)] = $k; + $this->_attributes[$k] = $v; + } + // save changes for update() + if (empty($this->_changes["add"][$k])) { + $this->_changes["add"][$k] = array(); + } + $this->_changes["add"][$k] = array_unique(array_merge($this->_changes["add"][$k], $v)); + } + $return = true; + return $return; + } + + /** + * Deletes an whole attribute or a value or the whole entry + * + * The parameter can be one of the following: + * + * "attributename" - The attribute as a whole will be deleted + * array("attributename1", "attributename2) - All given attributes will be + * deleted + * array("attributename" => "value") - The value will be deleted + * array("attributename" => array("value1", "value2") - The given values + * will be deleted + * If $attr is null or omitted , then the whole Entry will be deleted! + * + * These changes are local to the entry and do + * not affect the entry on the server until {@link update()} is called. + * + * Please note that you must select the attribute (at $ldap->search() for example) + * to be able to delete values of it, Otherwise {@link update()} will silently fail + * and remove nothing. + * + * @param string|array $attr Attributes to delete (NULL or missing to delete whole entry) + * + * @access public + * @return true + */ + public function delete($attr = null) + { + if (is_null($attr)) { + $this->_delete = true; + return true; + } + if (is_string($attr)) { + $attr = array($attr); + } + // Make the assumption that attribute names cannot be numeric, + // therefore this has to be a simple list of attribute names to delete + if (is_numeric(key($attr))) { + foreach ($attr as $name) { + if (is_array($name)) { + // someone mixed modes (list mode but specific values given!) + $del_attr_name = array_search($name, $attr); + $this->delete(array($del_attr_name => $name)); + } else { + // mark for update() if this attr was not marked before + $name = $this->getAttrName($name); + if ($this->exists($name)) { + $this->_changes["delete"][$name] = null; + unset($this->_attributes[$name]); + } + } + } + } else { + // Here we have a hash with "attributename" => "value to delete" + foreach ($attr as $name => $values) { + if (is_int($name)) { + // someone mixed modes and gave us just an attribute name + $this->delete($values); + } else { + // mark for update() if this attr was not marked before; + // this time it must consider the selected values also + $name = $this->getAttrName($name); + if ($this->exists($name)) { + if (false == is_array($values)) { + $values = array($values); + } + // save values to be deleted + if (empty($this->_changes["delete"][$name])) { + $this->_changes["delete"][$name] = array(); + } + $this->_changes["delete"][$name] = + array_unique(array_merge($this->_changes["delete"][$name], $values)); + foreach ($values as $value) { + // find the key for the value that should be deleted + $key = array_search($value, $this->_attributes[$name]); + if (false !== $key) { + // delete the value + unset($this->_attributes[$name][$key]); + } + } + } + } + } + } + $return = true; + return $return; + } + + /** + * Replaces attributes or its values + * + * The parameter has to an array of the following form: + * array("attributename" => "single value", + * "attribute2name" => array("value1", "value2"), + * "deleteme1" => null, + * "deleteme2" => "") + * If the attribute does not yet exist it will be added instead (see also $force). + * If the attribue value is null, the attribute will de deleted. + * + * These changes are local to the entry and do + * not affect the entry on the server until {@link update()} is called. + * + * In some cases you are not allowed to read the attributes value (for + * example the ActiveDirectory attribute unicodePwd) but are allowed to + * replace the value. In this case replace() would assume that the attribute + * is not in the directory yet and tries to add it which will result in an + * LDAP_TYPE_OR_VALUE_EXISTS error. + * To force replace mode instead of add, you can set $force to true. + * + * @param array $attr Attributes to replace + * @param bool $force Force replacing mode in case we can't read the attr value but are allowed to replace it + * + * @access public + * @return true|Net_LDAP2_Error + */ + public function replace($attr = array(), $force = false) + { + if (false == is_array($attr)) { + return PEAR::raiseError("Parameter must be an array"); + } + foreach ($attr as $k => $v) { + $k = $this->getAttrName($k); + if (false == is_array($v)) { + // delete attributes with empty values; treat ints as string + if (is_int($v)) { + $v = "$v"; + } + if ($v == null) { + $this->delete($k); + continue; + } else { + $v = array($v); + } + } + // existing attributes will get replaced + if ($this->exists($k) || $force) { + $this->_changes["replace"][$k] = $v; + $this->_attributes[$k] = $v; + } else { + // new ones just get added + $this->add(array($k => $v)); + } + } + $return = true; + return $return; + } + + /** + * Update the entry on the directory server + * + * This will evaluate all changes made so far and send them + * to the directory server. + * Please note, that if you make changes to objectclasses wich + * have mandatory attributes set, update() will currently fail. + * Remove the entry from the server and readd it as new in such cases. + * This also will deal with problems with setting structural object classes. + * + * @param Net_LDAP2 $ldap If passed, a call to setLDAP() is issued prior update, thus switching the LDAP-server. This is for perl-ldap interface compliance + * + * @access public + * @return true|Net_LDAP2_Error + * @todo Entry rename with a DN containing special characters needs testing! + */ + public function update($ldap = null) + { + if ($ldap) { + $msg = $this->setLDAP($ldap); + if (Net_LDAP2::isError($msg)) { + return PEAR::raiseError('You passed an invalid $ldap variable to update()'); + } + } + + // ensure we have a valid LDAP object + $ldap =& $this->getLDAP(); + if (!$ldap instanceof Net_LDAP2) { + return PEAR::raiseError("The entries LDAP object is not valid"); + } + + // Get and check link + $link = $ldap->getLink(); + if (!is_resource($link)) { + return PEAR::raiseError("Could not update entry: internal LDAP link is invalid"); + } + + /* + * Delete the entry + */ + if (true === $this->_delete) { + return $ldap->delete($this); + } + + /* + * New entry + */ + if (true === $this->_new) { + $msg = $ldap->add($this); + if (Net_LDAP2::isError($msg)) { + return $msg; + } + $this->_new = false; + $this->_changes['add'] = array(); + $this->_changes['delete'] = array(); + $this->_changes['replace'] = array(); + $this->_original = $this->_attributes; + + $return = true; + return $return; + } + + /* + * Rename/move entry + */ + if (false == is_null($this->_newdn)) { + if ($ldap->getLDAPVersion() !== 3) { + return PEAR::raiseError("Renaming/Moving an entry is only supported in LDAPv3"); + } + // make dn relative to parent (needed for ldap rename) + $parent = Net_LDAP2_Util::ldap_explode_dn($this->_newdn, array('casefolding' => 'none', 'reverse' => false, 'onlyvalues' => false)); + if (Net_LDAP2::isError($parent)) { + return $parent; + } + $child = array_shift($parent); + // maybe the dn consist of a multivalued RDN, we must build the dn in this case + // because the $child-RDN is an array! + if (is_array($child)) { + $child = Net_LDAP2_Util::canonical_dn($child); + } + $parent = Net_LDAP2_Util::canonical_dn($parent); + + // rename/move + if (false == @ldap_rename($link, $this->_dn, $child, $parent, true)) { + return PEAR::raiseError("Entry not renamed: " . + @ldap_error($link), @ldap_errno($link)); + } + // reflect changes to local copy + $this->_dn = $this->_newdn; + $this->_newdn = null; + } + + /* + * Carry out modifications to the entry + */ + // ADD + foreach ($this->_changes["add"] as $attr => $value) { + // if attribute exists, add new values + if ($this->exists($attr)) { + if (false === @ldap_mod_add($link, $this->dn(), array($attr => $value))) { + return PEAR::raiseError("Could not add new values to attribute $attr: " . + @ldap_error($link), @ldap_errno($link)); + } + } else { + // new attribute + if (false === @ldap_modify($link, $this->dn(), array($attr => $value))) { + return PEAR::raiseError("Could not add new attribute $attr: " . + @ldap_error($link), @ldap_errno($link)); + } + } + // all went well here, I guess + unset($this->_changes["add"][$attr]); + } + + // DELETE + foreach ($this->_changes["delete"] as $attr => $value) { + // In LDAPv3 you need to specify the old values for deleting + if (is_null($value) && $ldap->getLDAPVersion() === 3) { + $value = $this->_original[$attr]; + } + if (false === @ldap_mod_del($link, $this->dn(), array($attr => $value))) { + return PEAR::raiseError("Could not delete attribute $attr: " . + @ldap_error($link), @ldap_errno($link)); + } + unset($this->_changes["delete"][$attr]); + } + + // REPLACE + foreach ($this->_changes["replace"] as $attr => $value) { + if (false === @ldap_modify($link, $this->dn(), array($attr => $value))) { + return PEAR::raiseError("Could not replace attribute $attr values: " . + @ldap_error($link), @ldap_errno($link)); + } + unset($this->_changes["replace"][$attr]); + } + + // all went well, so _original (server) becomes _attributes (local copy) + $this->_original = $this->_attributes; + + $return = true; + return $return; + } + + /** + * Returns the right attribute name + * + * @param string $attr Name of attribute + * + * @access protected + * @return string The right name of the attribute + */ + protected function getAttrName($attr) + { + $name = strtolower($attr); + if (array_key_exists($name, $this->_map)) { + $attr = $this->_map[$name]; + } + return $attr; + } + + /** + * Returns a reference to the LDAP-Object of this entry + * + * @access public + * @return Net_LDAP2|Net_LDAP2_Error Reference to the Net_LDAP2 Object (the connection) or Net_LDAP2_Error + */ + public function &getLDAP() + { + if (!$this->_ldap instanceof Net_LDAP2) { + $err = new PEAR_Error('LDAP is not a valid Net_LDAP2 object'); + return $err; + } else { + return $this->_ldap; + } + } + + /** + * Sets a reference to the LDAP-Object of this entry + * + * After setting a Net_LDAP2 object, calling update() will use that object for + * updating directory contents. Use this to dynamicly switch directorys. + * + * @param Net_LDAP2 &$ldap Net_LDAP2 object that this entry should be connected to + * + * @access public + * @return true|Net_LDAP2_Error + */ + public function setLDAP(&$ldap) + { + if (!$ldap instanceof Net_LDAP2) { + return PEAR::raiseError("LDAP is not a valid Net_LDAP2 object"); + } else { + $this->_ldap =& $ldap; + return true; + } + } + + /** + * Marks the entry as new/existing. + * + * If an Entry is marked as new, it will be added to the directory + * when calling {@link update()}. + * If the entry is marked as old ($mark = false), then the entry is + * assumed to be present in the directory server wich results in + * modification when calling {@link update()}. + * + * @param boolean $mark Value to set, defaults to "true" + * + * @return void + */ + public function markAsNew($mark = true) + { + $this->_new = ($mark)? true : false; + } + + /** + * Applies a regular expression onto a single- or multivalued attribute (like preg_match()) + * + * This method behaves like PHPs preg_match() but with some exceptions. + * If you want to retrieve match information, then you MUST pass the + * $matches parameter via reference! otherwise you will get no matches. + * Since it is possible to have multi valued attributes the $matches + * array will have a additionally numerical dimension (one for each value): + * <code> + * $matches = array( + * 0 => array (usual preg_match() returnarray), + * 1 => array (usual preg_match() returnarray) + * ) + * </code> + * Please note, that $matches will be initialized to an empty array inside. + * + * Usage example: + * <code> + * $result = $entry->preg_match('/089(\d+)/', 'telephoneNumber', &$matches); + * if ( $result === true ){ + * echo "First match: ".$matches[0][1]; // Match of value 1, content of first bracket + * } else { + * if ( Net_LDAP2::isError($result) ) { + * echo "Error: ".$result->getMessage(); + * } else { + * echo "No match found."; + * } + * } + * </code> + * + * Please note that it is important to test for an Net_LDAP2_Error, because objects are + * evaluating to true by default, thus if an error occured, and you only check using "==" then + * you get misleading results. Use the "identical" (===) operator to test for matches to + * avoid this as shown above. + * + * @param string $regex The regular expression + * @param string $attr_name The attribute to search in + * @param array $matches (optional, PASS BY REFERENCE!) Array to store matches in + * + * @return boolean|Net_LDAP2_Error TRUE, if we had a match in one of the values, otherwise false. Net_LDAP2_Error in case something went wrong + */ + public function pregMatch($regex, $attr_name, $matches = array()) + { + $matches = array(); + + // fetch attribute values + $attr = $this->getValue($attr_name, 'all'); + if (Net_LDAP2::isError($attr)) { + return $attr; + } else { + unset($attr['count']); + } + + // perform preg_match() on all values + $match = false; + foreach ($attr as $thisvalue) { + $matches_int = array(); + if (preg_match($regex, $thisvalue, $matches_int)) { + $match = true; + array_push($matches, $matches_int); // store matches in reference + } + } + return $match; + } + + /** + * Alias of {@link pregMatch()} for compatibility to Net_LDAP 1 + * + * @see pregMatch() + * @return boolean|Net_LDAP2_Error + */ + public function preg_match() + { + $args = func_get_args(); + return call_user_func_array(array( &$this, 'pregMatch' ), $args); + } + + /** + * Tells if the entry is consiedered as new (not present in the server) + * + * Please note, that this doesn't tell you if the entry is present on the server. + * Use {@link Net_LDAP2::dnExists()} to see if an entry is already there. + * + * @return boolean + */ + public function isNew() + { + return $this->_new; + } + + + /** + * Is this entry going to be deleted once update() is called? + * + * @return boolean + */ + public function willBeDeleted() + { + return $this->_delete; + } + + /** + * Is this entry going to be moved once update() is called? + * + * @return boolean + */ + public function willBeMoved() + { + return ($this->dn() !== $this->currentDN()); + } + + /** + * Returns always the original DN + * + * If an entry will be moved but {@link update()} was not called, + * {@link dn()} will return the new DN. This method however, returns + * always the current active DN. + * + * @return string + */ + public function currentDN() + { + return $this->_dn; + } + + /** + * Returns the attribute changes to be carried out once update() is called + * + * @return array + */ + public function getChanges() + { + return $this->_changes; + } +} +?> diff --git a/plugins/LdapCommon/extlib/Net/LDAP2/Filter.php b/plugins/LdapCommon/extlib/Net/LDAP2/Filter.php new file mode 100644 index 000000000..0723edab2 --- /dev/null +++ b/plugins/LdapCommon/extlib/Net/LDAP2/Filter.php @@ -0,0 +1,514 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +/** +* File containing the Net_LDAP2_Filter interface class. +* +* PHP version 5 +* +* @category Net +* @package Net_LDAP2 +* @author Benedikt Hallinger <beni@php.net> +* @copyright 2009 Benedikt Hallinger +* @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 +* @version SVN: $Id: Filter.php 289978 2009-10-27 09:56:41Z beni $ +* @link http://pear.php.net/package/Net_LDAP2/ +*/ + +/** +* Includes +*/ +require_once 'PEAR.php'; +require_once 'Util.php'; + +/** +* Object representation of a part of a LDAP filter. +* +* This Class is not completely compatible to the PERL interface! +* +* The purpose of this class is, that users can easily build LDAP filters +* without having to worry about right escaping etc. +* A Filter is built using several independent filter objects +* which are combined afterwards. This object works in two +* modes, depending how the object is created. +* If the object is created using the {@link create()} method, then this is a leaf-object. +* If the object is created using the {@link combine()} method, then this is a container object. +* +* LDAP filters are defined in RFC-2254 and can be found under +* {@link http://www.ietf.org/rfc/rfc2254.txt} +* +* Here a quick copy&paste example: +* <code> +* $filter0 = Net_LDAP2_Filter::create('stars', 'equals', '***'); +* $filter_not0 = Net_LDAP2_Filter::combine('not', $filter0); +* +* $filter1 = Net_LDAP2_Filter::create('gn', 'begins', 'bar'); +* $filter2 = Net_LDAP2_Filter::create('gn', 'ends', 'baz'); +* $filter_comp = Net_LDAP2_Filter::combine('or',array($filter_not0, $filter1, $filter2)); +* +* echo $filter_comp->asString(); +* // This will output: (|(!(stars=\0x5c0x2a\0x5c0x2a\0x5c0x2a))(gn=bar*)(gn=*baz)) +* // The stars in $filter0 are treaten as real stars unless you disable escaping. +* </code> +* +* @category Net +* @package Net_LDAP2 +* @author Benedikt Hallinger <beni@php.net> +* @license http://www.gnu.org/copyleft/lesser.html LGPL +* @link http://pear.php.net/package/Net_LDAP2/ +*/ +class Net_LDAP2_Filter extends PEAR +{ + /** + * Storage for combination of filters + * + * This variable holds a array of filter objects + * that should be combined by this filter object. + * + * @access protected + * @var array + */ + protected $_subfilters = array(); + + /** + * Match of this filter + * + * If this is a leaf filter, then a matching rule is stored, + * if it is a container, then it is a logical operator + * + * @access protected + * @var string + */ + protected $_match; + + /** + * Single filter + * + * If we operate in leaf filter mode, + * then the constructing method stores + * the filter representation here + * + * @acces private + * @var string + */ + protected $_filter; + + /** + * Create a new Net_LDAP2_Filter object and parse $filter. + * + * This is for PERL Net::LDAP interface. + * Construction of Net_LDAP2_Filter objects should happen through either + * {@link create()} or {@link combine()} which give you more control. + * However, you may use the perl iterface if you already have generated filters. + * + * @param string $filter LDAP filter string + * + * @see parse() + */ + public function __construct($filter = false) + { + // The optional parameter must remain here, because otherwise create() crashes + if (false !== $filter) { + $filter_o = self::parse($filter); + if (PEAR::isError($filter_o)) { + $this->_filter = $filter_o; // assign error, so asString() can report it + } else { + $this->_filter = $filter_o->asString(); + } + } + } + + /** + * Constructor of a new part of a LDAP filter. + * + * The following matching rules exists: + * - equals: One of the attributes values is exactly $value + * Please note that case sensitiviness is depends on the + * attributes syntax configured in the server. + * - begins: One of the attributes values must begin with $value + * - ends: One of the attributes values must end with $value + * - contains: One of the attributes values must contain $value + * - present | any: The attribute can contain any value but must be existent + * - greater: The attributes value is greater than $value + * - less: The attributes value is less than $value + * - greaterOrEqual: The attributes value is greater or equal than $value + * - lessOrEqual: The attributes value is less or equal than $value + * - approx: One of the attributes values is similar to $value + * + * If $escape is set to true (default) then $value will be escaped + * properly. If it is set to false then $value will be treaten as raw filter value string. + * You should escape yourself using {@link Net_LDAP2_Util::escape_filter_value()}! + * + * Examples: + * <code> + * // This will find entries that contain an attribute "sn" that ends with "foobar": + * $filter = new Net_LDAP2_Filter('sn', 'ends', 'foobar'); + * + * // This will find entries that contain an attribute "sn" that has any value set: + * $filter = new Net_LDAP2_Filter('sn', 'any'); + * </code> + * + * @param string $attr_name Name of the attribute the filter should apply to + * @param string $match Matching rule (equals, begins, ends, contains, greater, less, greaterOrEqual, lessOrEqual, approx, any) + * @param string $value (optional) if given, then this is used as a filter + * @param boolean $escape Should $value be escaped? (default: yes, see {@link Net_LDAP2_Util::escape_filter_value()} for detailed information) + * + * @return Net_LDAP2_Filter|Net_LDAP2_Error + */ + public static function &create($attr_name, $match, $value = '', $escape = true) + { + $leaf_filter = new Net_LDAP2_Filter(); + if ($escape) { + $array = Net_LDAP2_Util::escape_filter_value(array($value)); + $value = $array[0]; + } + switch (strtolower($match)) { + case 'equals': + $leaf_filter->_filter = '(' . $attr_name . '=' . $value . ')'; + break; + case 'begins': + $leaf_filter->_filter = '(' . $attr_name . '=' . $value . '*)'; + break; + case 'ends': + $leaf_filter->_filter = '(' . $attr_name . '=*' . $value . ')'; + break; + case 'contains': + $leaf_filter->_filter = '(' . $attr_name . '=*' . $value . '*)'; + break; + case 'greater': + $leaf_filter->_filter = '(' . $attr_name . '>' . $value . ')'; + break; + case 'less': + $leaf_filter->_filter = '(' . $attr_name . '<' . $value . ')'; + break; + case 'greaterorequal': + case '>=': + $leaf_filter->_filter = '(' . $attr_name . '>=' . $value . ')'; + break; + case 'lessorequal': + case '<=': + $leaf_filter->_filter = '(' . $attr_name . '<=' . $value . ')'; + break; + case 'approx': + case '~=': + $leaf_filter->_filter = '(' . $attr_name . '~=' . $value . ')'; + break; + case 'any': + case 'present': // alias that may improve user code readability + $leaf_filter->_filter = '(' . $attr_name . '=*)'; + break; + default: + return PEAR::raiseError('Net_LDAP2_Filter create error: matching rule "' . $match . '" not known!'); + } + return $leaf_filter; + } + + /** + * Combine two or more filter objects using a logical operator + * + * This static method combines two or more filter objects and returns one single + * filter object that contains all the others. + * Call this method statically: $filter = Net_LDAP2_Filter('or', array($filter1, $filter2)) + * If the array contains filter strings instead of filter objects, we will try to parse them. + * + * @param string $log_op The locicall operator. May be "and", "or", "not" or the subsequent logical equivalents "&", "|", "!" + * @param array|Net_LDAP2_Filter $filters array with Net_LDAP2_Filter objects + * + * @return Net_LDAP2_Filter|Net_LDAP2_Error + * @static + */ + public static function &combine($log_op, $filters) + { + if (PEAR::isError($filters)) { + return $filters; + } + + // substitude named operators to logical operators + if ($log_op == 'and') $log_op = '&'; + if ($log_op == 'or') $log_op = '|'; + if ($log_op == 'not') $log_op = '!'; + + // tests for sane operation + if ($log_op == '!') { + // Not-combination, here we only accept one filter object or filter string + if ($filters instanceof Net_LDAP2_Filter) { + $filters = array($filters); // force array + } elseif (is_string($filters)) { + $filter_o = self::parse($filters); + if (PEAR::isError($filter_o)) { + $err = PEAR::raiseError('Net_LDAP2_Filter combine error: '.$filter_o->getMessage()); + return $err; + } else { + $filters = array($filter_o); + } + } elseif (is_array($filters)) { + $err = PEAR::raiseError('Net_LDAP2_Filter combine error: operator is "not" but $filter is an array!'); + return $err; + } else { + $err = PEAR::raiseError('Net_LDAP2_Filter combine error: operator is "not" but $filter is not a valid Net_LDAP2_Filter nor a filter string!'); + return $err; + } + } elseif ($log_op == '&' || $log_op == '|') { + if (!is_array($filters) || count($filters) < 2) { + $err = PEAR::raiseError('Net_LDAP2_Filter combine error: parameter $filters is not an array or contains less than two Net_LDAP2_Filter objects!'); + return $err; + } + } else { + $err = PEAR::raiseError('Net_LDAP2_Filter combine error: logical operator is not known!'); + return $err; + } + + $combined_filter = new Net_LDAP2_Filter(); + foreach ($filters as $key => $testfilter) { // check for errors + if (PEAR::isError($testfilter)) { + return $testfilter; + } elseif (is_string($testfilter)) { + // string found, try to parse into an filter object + $filter_o = self::parse($testfilter); + if (PEAR::isError($filter_o)) { + return $filter_o; + } else { + $filters[$key] = $filter_o; + } + } elseif (!$testfilter instanceof Net_LDAP2_Filter) { + $err = PEAR::raiseError('Net_LDAP2_Filter combine error: invalid object passed in array $filters!'); + return $err; + } + } + + $combined_filter->_subfilters = $filters; + $combined_filter->_match = $log_op; + return $combined_filter; + } + + /** + * Parse FILTER into a Net_LDAP2_Filter object + * + * This parses an filter string into Net_LDAP2_Filter objects. + * + * @param string $FILTER The filter string + * + * @access static + * @return Net_LDAP2_Filter|Net_LDAP2_Error + * @todo Leaf-mode: Do we need to escape at all? what about *-chars?check for the need of encoding values, tackle problems (see code comments) + */ + public static function parse($FILTER) + { + if (preg_match('/^\((.+?)\)$/', $FILTER, $matches)) { + if (in_array(substr($matches[1], 0, 1), array('!', '|', '&'))) { + // Subfilter processing: pass subfilters to parse() and combine + // the objects using the logical operator detected + // we have now something like "&(...)(...)(...)" but at least one part ("!(...)"). + // Each subfilter could be an arbitary complex subfilter. + + // extract logical operator and filter arguments + $log_op = substr($matches[1], 0, 1); + $remaining_component = substr($matches[1], 1); + + // split $remaining_component into individual subfilters + // we cannot use split() for this, because we do not know the + // complexiness of the subfilter. Thus, we look trough the filter + // string and just recognize ending filters at the first level. + // We record the index number of the char and use that information + // later to split the string. + $sub_index_pos = array(); + $prev_char = ''; // previous character looked at + $level = 0; // denotes the current bracket level we are, + // >1 is too deep, 1 is ok, 0 is outside any + // subcomponent + for ($curpos = 0; $curpos < strlen($remaining_component); $curpos++) { + $cur_char = substr($remaining_component, $curpos, 1); + + // rise/lower bracket level + if ($cur_char == '(' && $prev_char != '\\') { + $level++; + } elseif ($cur_char == ')' && $prev_char != '\\') { + $level--; + } + + if ($cur_char == '(' && $prev_char == ')' && $level == 1) { + array_push($sub_index_pos, $curpos); // mark the position for splitting + } + $prev_char = $cur_char; + } + + // now perform the splits. To get also the last part, we + // need to add the "END" index to the split array + array_push($sub_index_pos, strlen($remaining_component)); + $subfilters = array(); + $oldpos = 0; + foreach ($sub_index_pos as $s_pos) { + $str_part = substr($remaining_component, $oldpos, $s_pos - $oldpos); + array_push($subfilters, $str_part); + $oldpos = $s_pos; + } + + // some error checking... + if (count($subfilters) == 1) { + // only one subfilter found + } elseif (count($subfilters) > 1) { + // several subfilters found + if ($log_op == "!") { + return PEAR::raiseError("Filter parsing error: invalid filter syntax - NOT operator detected but several arguments given!"); + } + } else { + // this should not happen unless the user specified a wrong filter + return PEAR::raiseError("Filter parsing error: invalid filter syntax - got operator '$log_op' but no argument!"); + } + + // Now parse the subfilters into objects and combine them using the operator + $subfilters_o = array(); + foreach ($subfilters as $s_s) { + $o = self::parse($s_s); + if (PEAR::isError($o)) { + return $o; + } else { + array_push($subfilters_o, self::parse($s_s)); + } + } + + $filter_o = self::combine($log_op, $subfilters_o); + return $filter_o; + + } else { + // This is one leaf filter component, do some syntax checks, then escape and build filter_o + // $matches[1] should be now something like "foo=bar" + + // detect multiple leaf components + // [TODO] Maybe this will make problems with filters containing brackets inside the value + if (stristr($matches[1], ')(')) { + return PEAR::raiseError("Filter parsing error: invalid filter syntax - multiple leaf components detected!"); + } else { + $filter_parts = preg_split('/(?<!\\\\)(=|=~|>|<|>=|<=)/', $matches[1], 2, PREG_SPLIT_DELIM_CAPTURE); + if (count($filter_parts) != 3) { + return PEAR::raiseError("Filter parsing error: invalid filter syntax - unknown matching rule used"); + } else { + $filter_o = new Net_LDAP2_Filter(); + // [TODO]: Do we need to escape at all? what about *-chars user provide and that should remain special? + // I think, those prevent escaping! We need to check against PERL Net::LDAP! + // $value_arr = Net_LDAP2_Util::escape_filter_value(array($filter_parts[2])); + // $value = $value_arr[0]; + $value = $filter_parts[2]; + $filter_o->_filter = '('.$filter_parts[0].$filter_parts[1].$value.')'; + return $filter_o; + } + } + } + } else { + // ERROR: Filter components must be enclosed in round brackets + return PEAR::raiseError("Filter parsing error: invalid filter syntax - filter components must be enclosed in round brackets"); + } + } + + /** + * Get the string representation of this filter + * + * This method runs through all filter objects and creates + * the string representation of the filter. If this + * filter object is a leaf filter, then it will return + * the string representation of this filter. + * + * @return string|Net_LDAP2_Error + */ + public function asString() + { + if ($this->isLeaf()) { + $return = $this->_filter; + } else { + $return = ''; + foreach ($this->_subfilters as $filter) { + $return = $return.$filter->asString(); + } + $return = '(' . $this->_match . $return . ')'; + } + return $return; + } + + /** + * Alias for perl interface as_string() + * + * @see asString() + * @return string|Net_LDAP2_Error + */ + public function as_string() + { + return $this->asString(); + } + + /** + * Print the text representation of the filter to FH, or the currently selected output handle if FH is not given + * + * This method is only for compatibility to the perl interface. + * However, the original method was called "print" but due to PHP language restrictions, + * we can't have a print() method. + * + * @param resource $FH (optional) A filehandle resource + * + * @return true|Net_LDAP2_Error + */ + public function printMe($FH = false) + { + if (!is_resource($FH)) { + if (PEAR::isError($FH)) { + return $FH; + } + $filter_str = $this->asString(); + if (PEAR::isError($filter_str)) { + return $filter_str; + } else { + print($filter_str); + } + } else { + $filter_str = $this->asString(); + if (PEAR::isError($filter_str)) { + return $filter_str; + } else { + $res = @fwrite($FH, $this->asString()); + if ($res == false) { + return PEAR::raiseError("Unable to write filter string to filehandle \$FH!"); + } + } + } + return true; + } + + /** + * This can be used to escape a string to provide a valid LDAP-Filter. + * + * LDAP will only recognise certain characters as the + * character istself if they are properly escaped. This is + * what this method does. + * The method can be called statically, so you can use it outside + * for your own purposes (eg for escaping only parts of strings) + * + * In fact, this is just a shorthand to {@link Net_LDAP2_Util::escape_filter_value()}. + * For upward compatibiliy reasons you are strongly encouraged to use the escape + * methods provided by the Net_LDAP2_Util class. + * + * @param string $value Any string who should be escaped + * + * @static + * @return string The string $string, but escaped + * @deprecated Do not use this method anymore, instead use Net_LDAP2_Util::escape_filter_value() directly + */ + public static function escape($value) + { + $return = Net_LDAP2_Util::escape_filter_value(array($value)); + return $return[0]; + } + + /** + * Is this a container or a leaf filter object? + * + * @access protected + * @return boolean + */ + protected function isLeaf() + { + if (count($this->_subfilters) > 0) { + return false; // Container! + } else { + return true; // Leaf! + } + } +} +?> diff --git a/plugins/LdapCommon/extlib/Net/LDAP2/LDIF.php b/plugins/LdapCommon/extlib/Net/LDAP2/LDIF.php new file mode 100644 index 000000000..34f3e75dd --- /dev/null +++ b/plugins/LdapCommon/extlib/Net/LDAP2/LDIF.php @@ -0,0 +1,922 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +/** +* File containing the Net_LDAP2_LDIF interface class. +* +* PHP version 5 +* +* @category Net +* @package Net_LDAP2 +* @author Benedikt Hallinger <beni@php.net> +* @copyright 2009 Benedikt Hallinger +* @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 +* @version SVN: $Id: LDIF.php 286718 2009-08-03 07:30:49Z beni $ +* @link http://pear.php.net/package/Net_LDAP2/ +*/ + +/** +* Includes +*/ +require_once 'PEAR.php'; +require_once 'Net/LDAP2.php'; +require_once 'Net/LDAP2/Entry.php'; +require_once 'Net/LDAP2/Util.php'; + +/** +* LDIF capabilitys for Net_LDAP2, closely taken from PERLs Net::LDAP +* +* It provides a means to convert between Net_LDAP2_Entry objects and LDAP entries +* represented in LDIF format files. Reading and writing are supported and may +* manipulate single entries or lists of entries. +* +* Usage example: +* <code> +* // Read and parse an ldif-file into Net_LDAP2_Entry objects +* // and print out the DNs. Store the entries for later use. +* require 'Net/LDAP2/LDIF.php'; +* $options = array( +* 'onerror' => 'die' +* ); +* $entries = array(); +* $ldif = new Net_LDAP2_LDIF('test.ldif', 'r', $options); +* do { +* $entry = $ldif->read_entry(); +* $dn = $entry->dn(); +* echo " done building entry: $dn\n"; +* array_push($entries, $entry); +* } while (!$ldif->eof()); +* $ldif->done(); +* +* +* // write those entries to another file +* $ldif = new Net_LDAP2_LDIF('test.out.ldif', 'w', $options); +* $ldif->write_entry($entries); +* $ldif->done(); +* </code> +* +* @category Net +* @package Net_LDAP2 +* @author Benedikt Hallinger <beni@php.net> +* @license http://www.gnu.org/copyleft/lesser.html LGPL +* @link http://pear.php.net/package/Net_LDAP22/ +* @see http://www.ietf.org/rfc/rfc2849.txt +* @todo Error handling should be PEARified +* @todo LDAPv3 controls are not implemented yet +*/ +class Net_LDAP2_LDIF extends PEAR +{ + /** + * Options + * + * @access protected + * @var array + */ + protected $_options = array('encode' => 'base64', + 'onerror' => null, + 'change' => 0, + 'lowercase' => 0, + 'sort' => 0, + 'version' => null, + 'wrap' => 78, + 'raw' => '' + ); + + /** + * Errorcache + * + * @access protected + * @var array + */ + protected $_error = array('error' => null, + 'line' => 0 + ); + + /** + * Filehandle for read/write + * + * @access protected + * @var array + */ + protected $_FH = null; + + /** + * Says, if we opened the filehandle ourselves + * + * @access protected + * @var array + */ + protected $_FH_opened = false; + + /** + * Linecounter for input file handle + * + * @access protected + * @var array + */ + protected $_input_line = 0; + + /** + * counter for processed entries + * + * @access protected + * @var int + */ + protected $_entrynum = 0; + + /** + * Mode we are working in + * + * Either 'r', 'a' or 'w' + * + * @access protected + * @var string + */ + protected $_mode = false; + + /** + * Tells, if the LDIF version string was already written + * + * @access protected + * @var boolean + */ + protected $_version_written = false; + + /** + * Cache for lines that have build the current entry + * + * @access protected + * @var boolean + */ + protected $_lines_cur = array(); + + /** + * Cache for lines that will build the next entry + * + * @access protected + * @var boolean + */ + protected $_lines_next = array(); + + /** + * Open LDIF file for reading or for writing + * + * new (FILE): + * Open the file read-only. FILE may be the name of a file + * or an already open filehandle. + * If the file doesn't exist, it will be created if in write mode. + * + * new (FILE, MODE, OPTIONS): + * Open the file with the given MODE (see PHPs fopen()), eg "w" or "a". + * FILE may be the name of a file or an already open filehandle. + * PERLs Net_LDAP2 "FILE|" mode does not work curently. + * + * OPTIONS is an associative array and may contain: + * encode => 'none' | 'canonical' | 'base64' + * Some DN values in LDIF cannot be written verbatim and have to be encoded in some way: + * 'none' No encoding. + * 'canonical' See "canonical_dn()" in Net::LDAP::Util. + * 'base64' Use base64. (default, this differs from the Perl interface. + * The perl default is "none"!) + * + * onerror => 'die' | 'warn' | NULL + * Specify what happens when an error is detected. + * 'die' Net_LDAP2_LDIF will croak with an appropriate message. + * 'warn' Net_LDAP2_LDIF will warn (echo) with an appropriate message. + * NULL Net_LDAP2_LDIF will not warn (default), use error(). + * + * change => 1 + * Write entry changes to the LDIF file instead of the entries itself. I.e. write LDAP + * operations acting on the entries to the file instead of the entries contents. + * This writes the changes usually carried out by an update() to the LDIF file. + * + * lowercase => 1 + * Convert attribute names to lowercase when writing. + * + * sort => 1 + * Sort attribute names when writing entries according to the rule: + * objectclass first then all other attributes alphabetically sorted by attribute name + * + * version => '1' + * Set the LDIF version to write to the resulting LDIF file. + * According to RFC 2849 currently the only legal value for this option is 1. + * When this option is set Net_LDAP2_LDIF tries to adhere more strictly to + * the LDIF specification in RFC2489 in a few places. + * The default is NULL meaning no version information is written to the LDIF file. + * + * wrap => 78 + * Number of columns where output line wrapping shall occur. + * Default is 78. Setting it to 40 or lower inhibits wrapping. + * + * raw => REGEX + * Use REGEX to denote the names of attributes that are to be + * considered binary in search results if writing entries. + * Example: raw => "/(?i:^jpegPhoto|;binary)/i" + * + * @param string|ressource $file Filename or filehandle + * @param string $mode Mode to open filename + * @param array $options Options like described above + */ + public function __construct($file, $mode = 'r', $options = array()) + { + $this->PEAR('Net_LDAP2_Error'); // default error class + + // First, parse options + // todo: maybe implement further checks on possible values + foreach ($options as $option => $value) { + if (!array_key_exists($option, $this->_options)) { + $this->dropError('Net_LDAP2_LDIF error: option '.$option.' not known!'); + return; + } else { + $this->_options[$option] = strtolower($value); + } + } + + // setup LDIF class + $this->version($this->_options['version']); + + // setup file mode + if (!preg_match('/^[rwa]\+?$/', $mode)) { + $this->dropError('Net_LDAP2_LDIF error: file mode '.$mode.' not supported!'); + } else { + $this->_mode = $mode; + + // setup filehandle + if (is_resource($file)) { + // TODO: checks on mode possible? + $this->_FH =& $file; + } else { + $imode = substr($this->_mode, 0, 1); + if ($imode == 'r') { + if (!file_exists($file)) { + $this->dropError('Unable to open '.$file.' for read: file not found'); + $this->_mode = false; + } + if (!is_readable($file)) { + $this->dropError('Unable to open '.$file.' for read: permission denied'); + $this->_mode = false; + } + } + + if (($imode == 'w' || $imode == 'a')) { + if (file_exists($file)) { + if (!is_writable($file)) { + $this->dropError('Unable to open '.$file.' for write: permission denied'); + $this->_mode = false; + } + } else { + if (!@touch($file)) { + $this->dropError('Unable to create '.$file.' for write: permission denied'); + $this->_mode = false; + } + } + } + + if ($this->_mode) { + $this->_FH = @fopen($file, $this->_mode); + if (false === $this->_FH) { + // Fallback; should never be reached if tests above are good enough! + $this->dropError('Net_LDAP2_LDIF error: Could not open file '.$file); + } else { + $this->_FH_opened = true; + } + } + } + } + } + + /** + * Read one entry from the file and return it as a Net::LDAP::Entry object. + * + * @return Net_LDAP2_Entry + */ + public function read_entry() + { + // read fresh lines, set them as current lines and create the entry + $attrs = $this->next_lines(true); + if (count($attrs) > 0) { + $this->_lines_cur = $attrs; + } + return $this->current_entry(); + } + + /** + * Returns true when the end of the file is reached. + * + * @return boolean + */ + public function eof() + { + return feof($this->_FH); + } + + /** + * Write the entry or entries to the LDIF file. + * + * If you want to build an LDIF file containing several entries AND + * you want to call write_entry() several times, you must open the filehandle + * in append mode ("a"), otherwise you will always get the last entry only. + * + * @param Net_LDAP2_Entry|array $entries Entry or array of entries + * + * @return void + * @todo implement operations on whole entries (adding a whole entry) + */ + public function write_entry($entries) + { + if (!is_array($entries)) { + $entries = array($entries); + } + + foreach ($entries as $entry) { + $this->_entrynum++; + if (!$entry instanceof Net_LDAP2_Entry) { + $this->dropError('Net_LDAP2_LDIF error: entry '.$this->_entrynum.' is not an Net_LDAP2_Entry object'); + } else { + if ($this->_options['change']) { + // LDIF change mode + // fetch change information from entry + $entry_attrs_changes = $entry->getChanges(); + $num_of_changes = count($entry_attrs_changes['add']) + + count($entry_attrs_changes['replace']) + + count($entry_attrs_changes['delete']); + + $is_changed = ($num_of_changes > 0 || $entry->willBeDeleted() || $entry->willBeMoved()); + + // write version if not done yet + // also write DN of entry + if ($is_changed) { + if (!$this->_version_written) { + $this->write_version(); + } + $this->writeDN($entry->currentDN()); + } + + // process changes + // TODO: consider DN add! + if ($entry->willBeDeleted()) { + $this->writeLine("changetype: delete".PHP_EOL); + } elseif ($entry->willBeMoved()) { + $this->writeLine("changetype: modrdn".PHP_EOL); + $olddn = Net_LDAP2_Util::ldap_explode_dn($entry->currentDN(), array('casefold' => 'none')); // maybe gives a bug if using multivalued RDNs + $oldrdn = array_shift($olddn); + $oldparent = implode(',', $olddn); + $newdn = Net_LDAP2_Util::ldap_explode_dn($entry->dn(), array('casefold' => 'none')); // maybe gives a bug if using multivalued RDNs + $rdn = array_shift($newdn); + $parent = implode(',', $newdn); + $this->writeLine("newrdn: ".$rdn.PHP_EOL); + $this->writeLine("deleteoldrdn: 1".PHP_EOL); + if ($parent !== $oldparent) { + $this->writeLine("newsuperior: ".$parent.PHP_EOL); + } + // TODO: What if the entry has attribute changes as well? + // I think we should check for that and make a dummy + // entry with the changes that is written to the LDIF file + } elseif ($num_of_changes > 0) { + // write attribute change data + $this->writeLine("changetype: modify".PHP_EOL); + foreach ($entry_attrs_changes as $changetype => $entry_attrs) { + foreach ($entry_attrs as $attr_name => $attr_values) { + $this->writeLine("$changetype: $attr_name".PHP_EOL); + if ($attr_values !== null) $this->writeAttribute($attr_name, $attr_values, $changetype); + $this->writeLine("-".PHP_EOL); + } + } + } + + // finish this entrys data if we had changes + if ($is_changed) { + $this->finishEntry(); + } + } else { + // LDIF-content mode + // fetch attributes for further processing + $entry_attrs = $entry->getValues(); + + // sort and put objectclass-attrs to first position + if ($this->_options['sort']) { + ksort($entry_attrs); + if (array_key_exists('objectclass', $entry_attrs)) { + $oc = $entry_attrs['objectclass']; + unset($entry_attrs['objectclass']); + $entry_attrs = array_merge(array('objectclass' => $oc), $entry_attrs); + } + } + + // write data + if (!$this->_version_written) { + $this->write_version(); + } + $this->writeDN($entry->dn()); + foreach ($entry_attrs as $attr_name => $attr_values) { + $this->writeAttribute($attr_name, $attr_values); + } + $this->finishEntry(); + } + } + } + } + + /** + * Write version to LDIF + * + * If the object's version is defined, this method allows to explicitely write the version before an entry is written. + * If not called explicitely, it gets called automatically when writing the first entry. + * + * @return void + */ + public function write_version() + { + $this->_version_written = true; + if (!is_null($this->version())) { + return $this->writeLine('version: '.$this->version().PHP_EOL, 'Net_LDAP2_LDIF error: unable to write version'); + } + } + + /** + * Get or set LDIF version + * + * If called without arguments it returns the version of the LDIF file or NULL if no version has been set. + * If called with an argument it sets the LDIF version to VERSION. + * According to RFC 2849 currently the only legal value for VERSION is 1. + * + * @param int $version (optional) LDIF version to set + * + * @return int + */ + public function version($version = null) + { + if ($version !== null) { + if ($version != 1) { + $this->dropError('Net_LDAP2_LDIF error: illegal LDIF version set'); + } else { + $this->_options['version'] = $version; + } + } + return $this->_options['version']; + } + + /** + * Returns the file handle the Net_LDAP2_LDIF object reads from or writes to. + * + * You can, for example, use this to fetch the content of the LDIF file yourself + * + * @return null|resource + */ + public function &handle() + { + if (!is_resource($this->_FH)) { + $this->dropError('Net_LDAP2_LDIF error: invalid file resource'); + $null = null; + return $null; + } else { + return $this->_FH; + } + } + + /** + * Clean up + * + * This method signals that the LDIF object is no longer needed. + * You can use this to free up some memory and close the file handle. + * The file handle is only closed, if it was opened from Net_LDAP2_LDIF. + * + * @return void + */ + public function done() + { + // close FH if we opened it + if ($this->_FH_opened) { + fclose($this->handle()); + } + + // free variables + foreach (get_object_vars($this) as $name => $value) { + unset($this->$name); + } + } + + /** + * Returns last error message if error was found. + * + * Example: + * <code> + * $ldif->someAction(); + * if ($ldif->error()) { + * echo "Error: ".$ldif->error()." at input line: ".$ldif->error_lines(); + * } + * </code> + * + * @param boolean $as_string If set to true, only the message is returned + * + * @return false|Net_LDAP2_Error + */ + public function error($as_string = false) + { + if (Net_LDAP2::isError($this->_error['error'])) { + return ($as_string)? $this->_error['error']->getMessage() : $this->_error['error']; + } else { + return false; + } + } + + /** + * Returns lines that resulted in error. + * + * Perl returns an array of faulty lines in list context, + * but we always just return an int because of PHPs language. + * + * @return int + */ + public function error_lines() + { + return $this->_error['line']; + } + + /** + * Returns the current Net::LDAP::Entry object. + * + * @return Net_LDAP2_Entry|false + */ + public function current_entry() + { + return $this->parseLines($this->current_lines()); + } + + /** + * Parse LDIF lines of one entry into an Net_LDAP2_Entry object + * + * @param array $lines LDIF lines for one entry + * + * @return Net_LDAP2_Entry|false Net_LDAP2_Entry object for those lines + * @todo what about file inclusions and urls? "jpegphoto:< file:///usr/local/directory/photos/fiona.jpg" + */ + public function parseLines($lines) + { + // parse lines into an array of attributes and build the entry + $attributes = array(); + $dn = false; + foreach ($lines as $line) { + if (preg_match('/^(\w+)(:|::|:<)\s(.+)$/', $line, $matches)) { + $attr =& $matches[1]; + $delim =& $matches[2]; + $data =& $matches[3]; + + if ($delim == ':') { + // normal data + $attributes[$attr][] = $data; + } elseif ($delim == '::') { + // base64 data + $attributes[$attr][] = base64_decode($data); + } elseif ($delim == ':<') { + // file inclusion + // TODO: Is this the job of the LDAP-client or the server? + $this->dropError('File inclusions are currently not supported'); + //$attributes[$attr][] = ...; + } else { + // since the pattern above, the delimeter cannot be something else. + $this->dropError('Net_LDAP2_LDIF parsing error: invalid syntax at parsing entry line: '.$line); + continue; + } + + if (strtolower($attr) == 'dn') { + // DN line detected + $dn = $attributes[$attr][0]; // save possibly decoded DN + unset($attributes[$attr]); // remove wrongly added "dn: " attribute + } + } else { + // line not in "attr: value" format -> ignore + // maybe we should rise an error here, but this should be covered by + // next_lines() already. A problem arises, if users try to feed data of + // several entries to this method - the resulting entry will + // get wrong attributes. However, this is already mentioned in the + // methods documentation above. + } + } + + if (false === $dn) { + $this->dropError('Net_LDAP2_LDIF parsing error: unable to detect DN for entry'); + return false; + } else { + $newentry = Net_LDAP2_Entry::createFresh($dn, $attributes); + return $newentry; + } + } + + /** + * Returns the lines that generated the current Net::LDAP::Entry object. + * + * Note that this returns an empty array if no lines have been read so far. + * + * @return array Array of lines + */ + public function current_lines() + { + return $this->_lines_cur; + } + + /** + * Returns the lines that will generate the next Net::LDAP::Entry object. + * + * If you set $force to TRUE then you can iterate over the lines that build + * up entries manually. Otherwise, iterating is done using {@link read_entry()}. + * Force will move the file pointer forward, thus returning the next entries lines. + * + * Wrapped lines will be unwrapped. Comments are stripped. + * + * @param boolean $force Set this to true if you want to iterate over the lines manually + * + * @return array + */ + public function next_lines($force = false) + { + // if we already have those lines, just return them, otherwise read + if (count($this->_lines_next) == 0 || $force) { + $this->_lines_next = array(); // empty in case something was left (if used $force) + $entry_done = false; + $fh = &$this->handle(); + $commentmode = false; // if we are in an comment, for wrapping purposes + $datalines_read = 0; // how many lines with data we have read + + while (!$entry_done && !$this->eof()) { + $this->_input_line++; + // Read line. Remove line endings, we want only data; + // this is okay since ending spaces should be encoded + $data = rtrim(fgets($fh)); + if ($data === false) { + // error only, if EOF not reached after fgets() call + if (!$this->eof()) { + $this->dropError('Net_LDAP2_LDIF error: error reading from file at input line '.$this->_input_line, $this->_input_line); + } + break; + } else { + if (count($this->_lines_next) > 0 && preg_match('/^$/', $data)) { + // Entry is finished if we have an empty line after we had data + $entry_done = true; + + // Look ahead if the next EOF is nearby. Comments and empty + // lines at the file end may cause problems otherwise + $current_pos = ftell($fh); + $data = fgets($fh); + while (!feof($fh)) { + if (preg_match('/^\s*$/', $data) || preg_match('/^#/', $data)) { + // only empty lines or comments, continue to seek + // TODO: Known bug: Wrappings for comments are okay but are treaten as + // error, since we do not honor comment mode here. + // This should be a very theoretically case, however + // i am willing to fix this if really necessary. + $this->_input_line++; + $current_pos = ftell($fh); + $data = fgets($fh); + } else { + // Data found if non emtpy line and not a comment!! + // Rewind to position prior last read and stop lookahead + fseek($fh, $current_pos); + break; + } + } + // now we have either the file pointer at the beginning of + // a new data position or at the end of file causing feof() to return true + + } else { + // build lines + if (preg_match('/^version:\s(.+)$/', $data, $match)) { + // version statement, set version + $this->version($match[1]); + } elseif (preg_match('/^\w+::?\s.+$/', $data)) { + // normal attribute: add line + $commentmode = false; + $this->_lines_next[] = trim($data); + $datalines_read++; + } elseif (preg_match('/^\s(.+)$/', $data, $matches)) { + // wrapped data: unwrap if not in comment mode + if (!$commentmode) { + if ($datalines_read == 0) { + // first line of entry: wrapped data is illegal + $this->dropError('Net_LDAP2_LDIF error: illegal wrapping at input line '.$this->_input_line, $this->_input_line); + } else { + $last = array_pop($this->_lines_next); + $last = $last.trim($matches[1]); + $this->_lines_next[] = $last; + $datalines_read++; + } + } + } elseif (preg_match('/^#/', $data)) { + // LDIF comments + $commentmode = true; + } elseif (preg_match('/^\s*$/', $data)) { + // empty line but we had no data for this + // entry, so just ignore this line + $commentmode = false; + } else { + $this->dropError('Net_LDAP2_LDIF error: invalid syntax at input line '.$this->_input_line, $this->_input_line); + continue; + } + + } + } + } + } + return $this->_lines_next; + } + + /** + * Convert an attribute and value to LDIF string representation + * + * It honors correct encoding of values according to RFC 2849. + * Line wrapping will occur at the configured maximum but only if + * the value is greater than 40 chars. + * + * @param string $attr_name Name of the attribute + * @param string $attr_value Value of the attribute + * + * @access protected + * @return string LDIF string for that attribute and value + */ + protected function convertAttribute($attr_name, $attr_value) + { + // Handle empty attribute or process + if (strlen($attr_value) == 0) { + $attr_value = " "; + } else { + $base64 = false; + // ASCII-chars that are NOT safe for the + // start and for being inside the value. + // These are the int values of those chars. + $unsafe_init = array(0, 10, 13, 32, 58, 60); + $unsafe = array(0, 10, 13); + + // Test for illegal init char + $init_ord = ord(substr($attr_value, 0, 1)); + if ($init_ord > 127 || in_array($init_ord, $unsafe_init)) { + $base64 = true; + } + + // Test for illegal content char + for ($i = 0; $i < strlen($attr_value); $i++) { + $char_ord = ord(substr($attr_value, $i, 1)); + if ($char_ord > 127 || in_array($char_ord, $unsafe)) { + $base64 = true; + } + } + + // Test for ending space + if (substr($attr_value, -1) == ' ') { + $base64 = true; + } + + // If converting is needed, do it + // Either we have some special chars or a matching "raw" regex + if ($base64 || ($this->_options['raw'] && preg_match($this->_options['raw'], $attr_name))) { + $attr_name .= ':'; + $attr_value = base64_encode($attr_value); + } + + // Lowercase attr names if requested + if ($this->_options['lowercase']) $attr_name = strtolower($attr_name); + + // Handle line wrapping + if ($this->_options['wrap'] > 40 && strlen($attr_value) > $this->_options['wrap']) { + $attr_value = wordwrap($attr_value, $this->_options['wrap'], PHP_EOL." ", true); + } + } + + return $attr_name.': '.$attr_value; + } + + /** + * Convert an entries DN to LDIF string representation + * + * It honors correct encoding of values according to RFC 2849. + * + * @param string $dn UTF8-Encoded DN + * + * @access protected + * @return string LDIF string for that DN + * @todo I am not sure, if the UTF8 stuff is correctly handled right now + */ + protected function convertDN($dn) + { + $base64 = false; + // ASCII-chars that are NOT safe for the + // start and for being inside the dn. + // These are the int values of those chars. + $unsafe_init = array(0, 10, 13, 32, 58, 60); + $unsafe = array(0, 10, 13); + + // Test for illegal init char + $init_ord = ord(substr($dn, 0, 1)); + if ($init_ord >= 127 || in_array($init_ord, $unsafe_init)) { + $base64 = true; + } + + // Test for illegal content char + for ($i = 0; $i < strlen($dn); $i++) { + $char = substr($dn, $i, 1); + if (ord($char) >= 127 || in_array($init_ord, $unsafe)) { + $base64 = true; + } + } + + // Test for ending space + if (substr($dn, -1) == ' ') { + $base64 = true; + } + + // if converting is needed, do it + return ($base64)? 'dn:: '.base64_encode($dn) : 'dn: '.$dn; + } + + /** + * Writes an attribute to the filehandle + * + * @param string $attr_name Name of the attribute + * @param string|array $attr_values Single attribute value or array with attribute values + * + * @access protected + * @return void + */ + protected function writeAttribute($attr_name, $attr_values) + { + // write out attribute content + if (!is_array($attr_values)) { + $attr_values = array($attr_values); + } + foreach ($attr_values as $attr_val) { + $line = $this->convertAttribute($attr_name, $attr_val).PHP_EOL; + $this->writeLine($line, 'Net_LDAP2_LDIF error: unable to write attribute '.$attr_name.' of entry '.$this->_entrynum); + } + } + + /** + * Writes a DN to the filehandle + * + * @param string $dn DN to write + * + * @access protected + * @return void + */ + protected function writeDN($dn) + { + // prepare DN + if ($this->_options['encode'] == 'base64') { + $dn = $this->convertDN($dn).PHP_EOL; + } elseif ($this->_options['encode'] == 'canonical') { + $dn = Net_LDAP2_Util::canonical_dn($dn, array('casefold' => 'none')).PHP_EOL; + } else { + $dn = $dn.PHP_EOL; + } + $this->writeLine($dn, 'Net_LDAP2_LDIF error: unable to write DN of entry '.$this->_entrynum); + } + + /** + * Finishes an LDIF entry + * + * @access protected + * @return void + */ + protected function finishEntry() + { + $this->writeLine(PHP_EOL, 'Net_LDAP2_LDIF error: unable to close entry '.$this->_entrynum); + } + + /** + * Just write an arbitary line to the filehandle + * + * @param string $line Content to write + * @param string $error If error occurs, drop this message + * + * @access protected + * @return true|false + */ + protected function writeLine($line, $error = 'Net_LDAP2_LDIF error: unable to write to filehandle') + { + if (is_resource($this->handle()) && fwrite($this->handle(), $line, strlen($line)) === false) { + $this->dropError($error); + return false; + } else { + return true; + } + } + + /** + * Optionally raises an error and pushes the error on the error cache + * + * @param string $msg Errortext + * @param int $line Line in the LDIF that caused the error + * + * @access protected + * @return void + */ + protected function dropError($msg, $line = null) + { + $this->_error['error'] = new Net_LDAP2_Error($msg); + if ($line !== null) $this->_error['line'] = $line; + + if ($this->_options['onerror'] == 'die') { + die($msg.PHP_EOL); + } elseif ($this->_options['onerror'] == 'warn') { + echo $msg.PHP_EOL; + } + } +} +?> diff --git a/plugins/LdapCommon/extlib/Net/LDAP2/RootDSE.php b/plugins/LdapCommon/extlib/Net/LDAP2/RootDSE.php new file mode 100644 index 000000000..8dc81fd4f --- /dev/null +++ b/plugins/LdapCommon/extlib/Net/LDAP2/RootDSE.php @@ -0,0 +1,240 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +/** +* File containing the Net_LDAP2_RootDSE interface class. +* +* PHP version 5 +* +* @category Net +* @package Net_LDAP2 +* @author Jan Wagner <wagner@netsols.de> +* @copyright 2009 Jan Wagner +* @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 +* @version SVN: $Id: RootDSE.php 286718 2009-08-03 07:30:49Z beni $ +* @link http://pear.php.net/package/Net_LDAP2/ +*/ + +/** +* Includes +*/ +require_once 'PEAR.php'; + +/** +* Getting the rootDSE entry of a LDAP server +* +* @category Net +* @package Net_LDAP2 +* @author Jan Wagner <wagner@netsols.de> +* @license http://www.gnu.org/copyleft/lesser.html LGPL +* @link http://pear.php.net/package/Net_LDAP22/ +*/ +class Net_LDAP2_RootDSE extends PEAR +{ + /** + * @access protected + * @var object Net_LDAP2_Entry + **/ + protected $_entry; + + /** + * Class constructor + * + * @param Net_LDAP2_Entry &$entry Net_LDAP2_Entry object of the RootDSE + */ + protected function __construct(&$entry) + { + $this->_entry = $entry; + } + + /** + * Fetches a RootDSE object from an LDAP connection + * + * @param Net_LDAP2 $ldap Directory from which the RootDSE should be fetched + * @param array $attrs Array of attributes to search for + * + * @access static + * @return Net_LDAP2_RootDSE|Net_LDAP2_Error + */ + public static function fetch($ldap, $attrs = null) + { + if (!$ldap instanceof Net_LDAP2) { + return PEAR::raiseError("Unable to fetch Schema: Parameter \$ldap must be a Net_LDAP2 object!"); + } + + if (is_array($attrs) && count($attrs) > 0 ) { + $attributes = $attrs; + } else { + $attributes = array('vendorName', + 'vendorVersion', + 'namingContexts', + 'altServer', + 'supportedExtension', + 'supportedControl', + 'supportedSASLMechanisms', + 'supportedLDAPVersion', + 'subschemaSubentry' ); + } + $result = $ldap->search('', '(objectClass=*)', array('attributes' => $attributes, 'scope' => 'base')); + if (self::isError($result)) { + return $result; + } + $entry = $result->shiftEntry(); + if (false === $entry) { + return PEAR::raiseError('Could not fetch RootDSE entry'); + } + $ret = new Net_LDAP2_RootDSE($entry); + return $ret; + } + + /** + * Gets the requested attribute value + * + * Same usuage as {@link Net_LDAP2_Entry::getValue()} + * + * @param string $attr Attribute name + * @param array $options Array of options + * + * @access public + * @return mixed Net_LDAP2_Error object or attribute values + * @see Net_LDAP2_Entry::get_value() + */ + public function getValue($attr = '', $options = '') + { + return $this->_entry->get_value($attr, $options); + } + + /** + * Alias function of getValue() for perl-ldap interface + * + * @see getValue() + * @return mixed + */ + public function get_value() + { + $args = func_get_args(); + return call_user_func_array(array( &$this, 'getValue' ), $args); + } + + /** + * Determines if the extension is supported + * + * @param array $oids Array of oids to check + * + * @access public + * @return boolean + */ + public function supportedExtension($oids) + { + return $this->checkAttr($oids, 'supportedExtension'); + } + + /** + * Alias function of supportedExtension() for perl-ldap interface + * + * @see supportedExtension() + * @return boolean + */ + public function supported_extension() + { + $args = func_get_args(); + return call_user_func_array(array( &$this, 'supportedExtension'), $args); + } + + /** + * Determines if the version is supported + * + * @param array $versions Versions to check + * + * @access public + * @return boolean + */ + public function supportedVersion($versions) + { + return $this->checkAttr($versions, 'supportedLDAPVersion'); + } + + /** + * Alias function of supportedVersion() for perl-ldap interface + * + * @see supportedVersion() + * @return boolean + */ + public function supported_version() + { + $args = func_get_args(); + return call_user_func_array(array(&$this, 'supportedVersion'), $args); + } + + /** + * Determines if the control is supported + * + * @param array $oids Control oids to check + * + * @access public + * @return boolean + */ + public function supportedControl($oids) + { + return $this->checkAttr($oids, 'supportedControl'); + } + + /** + * Alias function of supportedControl() for perl-ldap interface + * + * @see supportedControl() + * @return boolean + */ + public function supported_control() + { + $args = func_get_args(); + return call_user_func_array(array(&$this, 'supportedControl' ), $args); + } + + /** + * Determines if the sasl mechanism is supported + * + * @param array $mechlist SASL mechanisms to check + * + * @access public + * @return boolean + */ + public function supportedSASLMechanism($mechlist) + { + return $this->checkAttr($mechlist, 'supportedSASLMechanisms'); + } + + /** + * Alias function of supportedSASLMechanism() for perl-ldap interface + * + * @see supportedSASLMechanism() + * @return boolean + */ + public function supported_sasl_mechanism() + { + $args = func_get_args(); + return call_user_func_array(array(&$this, 'supportedSASLMechanism'), $args); + } + + /** + * Checks for existance of value in attribute + * + * @param array $values values to check + * @param string $attr attribute name + * + * @access protected + * @return boolean + */ + protected function checkAttr($values, $attr) + { + if (!is_array($values)) $values = array($values); + + foreach ($values as $value) { + if (!@in_array($value, $this->get_value($attr, 'all'))) { + return false; + } + } + return true; + } +} + +?> diff --git a/plugins/LdapCommon/extlib/Net/LDAP2/Schema.php b/plugins/LdapCommon/extlib/Net/LDAP2/Schema.php new file mode 100644 index 000000000..b590eabc5 --- /dev/null +++ b/plugins/LdapCommon/extlib/Net/LDAP2/Schema.php @@ -0,0 +1,516 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +/** +* File containing the Net_LDAP2_Schema interface class. +* +* PHP version 5 +* +* @category Net +* @package Net_LDAP2 +* @author Jan Wagner <wagner@netsols.de> +* @author Benedikt Hallinger <beni@php.net> +* @copyright 2009 Jan Wagner, Benedikt Hallinger +* @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 +* @version SVN: $Id: Schema.php 286718 2009-08-03 07:30:49Z beni $ +* @link http://pear.php.net/package/Net_LDAP2/ +* @todo see the comment at the end of the file +*/ + +/** +* Includes +*/ +require_once 'PEAR.php'; + +/** +* Syntax definitions +* +* Please don't forget to add binary attributes to isBinary() below +* to support proper value fetching from Net_LDAP2_Entry +*/ +define('NET_LDAP2_SYNTAX_BOOLEAN', '1.3.6.1.4.1.1466.115.121.1.7'); +define('NET_LDAP2_SYNTAX_DIRECTORY_STRING', '1.3.6.1.4.1.1466.115.121.1.15'); +define('NET_LDAP2_SYNTAX_DISTINGUISHED_NAME', '1.3.6.1.4.1.1466.115.121.1.12'); +define('NET_LDAP2_SYNTAX_INTEGER', '1.3.6.1.4.1.1466.115.121.1.27'); +define('NET_LDAP2_SYNTAX_JPEG', '1.3.6.1.4.1.1466.115.121.1.28'); +define('NET_LDAP2_SYNTAX_NUMERIC_STRING', '1.3.6.1.4.1.1466.115.121.1.36'); +define('NET_LDAP2_SYNTAX_OID', '1.3.6.1.4.1.1466.115.121.1.38'); +define('NET_LDAP2_SYNTAX_OCTET_STRING', '1.3.6.1.4.1.1466.115.121.1.40'); + +/** +* Load an LDAP Schema and provide information +* +* This class takes a Subschema entry, parses this information +* and makes it available in an array. Most of the code has been +* inspired by perl-ldap( http://perl-ldap.sourceforge.net). +* You will find portions of their implementation in here. +* +* @category Net +* @package Net_LDAP2 +* @author Jan Wagner <wagner@netsols.de> +* @author Benedikt Hallinger <beni@php.net> +* @license http://www.gnu.org/copyleft/lesser.html LGPL +* @link http://pear.php.net/package/Net_LDAP22/ +*/ +class Net_LDAP2_Schema extends PEAR +{ + /** + * Map of entry types to ldap attributes of subschema entry + * + * @access public + * @var array + */ + public $types = array( + 'attribute' => 'attributeTypes', + 'ditcontentrule' => 'dITContentRules', + 'ditstructurerule' => 'dITStructureRules', + 'matchingrule' => 'matchingRules', + 'matchingruleuse' => 'matchingRuleUse', + 'nameform' => 'nameForms', + 'objectclass' => 'objectClasses', + 'syntax' => 'ldapSyntaxes' + ); + + /** + * Array of entries belonging to this type + * + * @access protected + * @var array + */ + protected $_attributeTypes = array(); + protected $_matchingRules = array(); + protected $_matchingRuleUse = array(); + protected $_ldapSyntaxes = array(); + protected $_objectClasses = array(); + protected $_dITContentRules = array(); + protected $_dITStructureRules = array(); + protected $_nameForms = array(); + + + /** + * hash of all fetched oids + * + * @access protected + * @var array + */ + protected $_oids = array(); + + /** + * Tells if the schema is initialized + * + * @access protected + * @var boolean + * @see parse(), get() + */ + protected $_initialized = false; + + + /** + * Constructor of the class + * + * @access protected + */ + protected function __construct() + { + $this->PEAR('Net_LDAP2_Error'); // default error class + } + + /** + * Fetch the Schema from an LDAP connection + * + * @param Net_LDAP2 $ldap LDAP connection + * @param string $dn (optional) Subschema entry dn + * + * @access public + * @return Net_LDAP2_Schema|NET_LDAP2_Error + */ + public function fetch($ldap, $dn = null) + { + if (!$ldap instanceof Net_LDAP2) { + return PEAR::raiseError("Unable to fetch Schema: Parameter \$ldap must be a Net_LDAP2 object!"); + } + + $schema_o = new Net_LDAP2_Schema(); + + if (is_null($dn)) { + // get the subschema entry via root dse + $dse = $ldap->rootDSE(array('subschemaSubentry')); + if (false == Net_LDAP2::isError($dse)) { + $base = $dse->getValue('subschemaSubentry', 'single'); + if (!Net_LDAP2::isError($base)) { + $dn = $base; + } + } + } + + // Support for buggy LDAP servers (e.g. Siemens DirX 6.x) that incorrectly + // call this entry subSchemaSubentry instead of subschemaSubentry. + // Note the correct case/spelling as per RFC 2251. + if (is_null($dn)) { + // get the subschema entry via root dse + $dse = $ldap->rootDSE(array('subSchemaSubentry')); + if (false == Net_LDAP2::isError($dse)) { + $base = $dse->getValue('subSchemaSubentry', 'single'); + if (!Net_LDAP2::isError($base)) { + $dn = $base; + } + } + } + + // Final fallback case where there is no subschemaSubentry attribute + // in the root DSE (this is a bug for an LDAP v3 server so report this + // to your LDAP vendor if you get this far). + if (is_null($dn)) { + $dn = 'cn=Subschema'; + } + + // fetch the subschema entry + $result = $ldap->search($dn, '(objectClass=*)', + array('attributes' => array_values($schema_o->types), + 'scope' => 'base')); + if (Net_LDAP2::isError($result)) { + return $result; + } + + $entry = $result->shiftEntry(); + if (!$entry instanceof Net_LDAP2_Entry) { + return PEAR::raiseError('Could not fetch Subschema entry'); + } + + $schema_o->parse($entry); + return $schema_o; + } + + /** + * Return a hash of entries for the given type + * + * Returns a hash of entry for th givene type. Types may be: + * objectclasses, attributes, ditcontentrules, ditstructurerules, matchingrules, + * matchingruleuses, nameforms, syntaxes + * + * @param string $type Type to fetch + * + * @access public + * @return array|Net_LDAP2_Error Array or Net_LDAP2_Error + */ + public function &getAll($type) + { + $map = array('objectclasses' => &$this->_objectClasses, + 'attributes' => &$this->_attributeTypes, + 'ditcontentrules' => &$this->_dITContentRules, + 'ditstructurerules' => &$this->_dITStructureRules, + 'matchingrules' => &$this->_matchingRules, + 'matchingruleuses' => &$this->_matchingRuleUse, + 'nameforms' => &$this->_nameForms, + 'syntaxes' => &$this->_ldapSyntaxes ); + + $key = strtolower($type); + $ret = ((key_exists($key, $map)) ? $map[$key] : PEAR::raiseError("Unknown type $type")); + return $ret; + } + + /** + * Return a specific entry + * + * @param string $type Type of name + * @param string $name Name or OID to fetch + * + * @access public + * @return mixed Entry or Net_LDAP2_Error + */ + public function &get($type, $name) + { + if ($this->_initialized) { + $type = strtolower($type); + if (false == key_exists($type, $this->types)) { + return PEAR::raiseError("No such type $type"); + } + + $name = strtolower($name); + $type_var = &$this->{'_' . $this->types[$type]}; + + if (key_exists($name, $type_var)) { + return $type_var[$name]; + } elseif (key_exists($name, $this->_oids) && $this->_oids[$name]['type'] == $type) { + return $this->_oids[$name]; + } else { + return PEAR::raiseError("Could not find $type $name"); + } + } else { + $return = null; + return $return; + } + } + + + /** + * Fetches attributes that MAY be present in the given objectclass + * + * @param string $oc Name or OID of objectclass + * + * @access public + * @return array|Net_LDAP2_Error Array with attributes or Net_LDAP2_Error + */ + public function may($oc) + { + return $this->_getAttr($oc, 'may'); + } + + /** + * Fetches attributes that MUST be present in the given objectclass + * + * @param string $oc Name or OID of objectclass + * + * @access public + * @return array|Net_LDAP2_Error Array with attributes or Net_LDAP2_Error + */ + public function must($oc) + { + return $this->_getAttr($oc, 'must'); + } + + /** + * Fetches the given attribute from the given objectclass + * + * @param string $oc Name or OID of objectclass + * @param string $attr Name of attribute to fetch + * + * @access protected + * @return array|Net_LDAP2_Error The attribute or Net_LDAP2_Error + */ + protected function _getAttr($oc, $attr) + { + $oc = strtolower($oc); + if (key_exists($oc, $this->_objectClasses) && key_exists($attr, $this->_objectClasses[$oc])) { + return $this->_objectClasses[$oc][$attr]; + } elseif (key_exists($oc, $this->_oids) && + $this->_oids[$oc]['type'] == 'objectclass' && + key_exists($attr, $this->_oids[$oc])) { + return $this->_oids[$oc][$attr]; + } else { + return PEAR::raiseError("Could not find $attr attributes for $oc "); + } + } + + /** + * Returns the name(s) of the immediate superclass(es) + * + * @param string $oc Name or OID of objectclass + * + * @access public + * @return array|Net_LDAP2_Error Array of names or Net_LDAP2_Error + */ + public function superclass($oc) + { + $o = $this->get('objectclass', $oc); + if (Net_LDAP2::isError($o)) { + return $o; + } + return (key_exists('sup', $o) ? $o['sup'] : array()); + } + + /** + * Parses the schema of the given Subschema entry + * + * @param Net_LDAP2_Entry &$entry Subschema entry + * + * @access public + * @return void + */ + public function parse(&$entry) + { + foreach ($this->types as $type => $attr) { + // initialize map type to entry + $type_var = '_' . $attr; + $this->{$type_var} = array(); + + // get values for this type + if ($entry->exists($attr)) { + $values = $entry->getValue($attr); + if (is_array($values)) { + foreach ($values as $value) { + + unset($schema_entry); // this was a real mess without it + + // get the schema entry + $schema_entry = $this->_parse_entry($value); + + // set the type + $schema_entry['type'] = $type; + + // save a ref in $_oids + $this->_oids[$schema_entry['oid']] = &$schema_entry; + + // save refs for all names in type map + $names = $schema_entry['aliases']; + array_push($names, $schema_entry['name']); + foreach ($names as $name) { + $this->{$type_var}[strtolower($name)] = &$schema_entry; + } + } + } + } + } + $this->_initialized = true; + } + + /** + * Parses an attribute value into a schema entry + * + * @param string $value Attribute value + * + * @access protected + * @return array|false Schema entry array or false + */ + protected function &_parse_entry($value) + { + // tokens that have no value associated + $noValue = array('single-value', + 'obsolete', + 'collective', + 'no-user-modification', + 'abstract', + 'structural', + 'auxiliary'); + + // tokens that can have multiple values + $multiValue = array('must', 'may', 'sup'); + + $schema_entry = array('aliases' => array()); // initilization + + $tokens = $this->_tokenize($value); // get an array of tokens + + // remove surrounding brackets + if ($tokens[0] == '(') array_shift($tokens); + if ($tokens[count($tokens) - 1] == ')') array_pop($tokens); // -1 doesnt work on arrays :-( + + $schema_entry['oid'] = array_shift($tokens); // first token is the oid + + // cycle over the tokens until none are left + while (count($tokens) > 0) { + $token = strtolower(array_shift($tokens)); + if (in_array($token, $noValue)) { + $schema_entry[$token] = 1; // single value token + } else { + // this one follows a string or a list if it is multivalued + if (($schema_entry[$token] = array_shift($tokens)) == '(') { + // this creates the list of values and cycles through the tokens + // until the end of the list is reached ')' + $schema_entry[$token] = array(); + while ($tmp = array_shift($tokens)) { + if ($tmp == ')') break; + if ($tmp != '$') array_push($schema_entry[$token], $tmp); + } + } + // create a array if the value should be multivalued but was not + if (in_array($token, $multiValue) && !is_array($schema_entry[$token])) { + $schema_entry[$token] = array($schema_entry[$token]); + } + } + } + // get max length from syntax + if (key_exists('syntax', $schema_entry)) { + if (preg_match('/{(\d+)}/', $schema_entry['syntax'], $matches)) { + $schema_entry['max_length'] = $matches[1]; + } + } + // force a name + if (empty($schema_entry['name'])) { + $schema_entry['name'] = $schema_entry['oid']; + } + // make one name the default and put the other ones into aliases + if (is_array($schema_entry['name'])) { + $aliases = $schema_entry['name']; + $schema_entry['name'] = array_shift($aliases); + $schema_entry['aliases'] = $aliases; + } + return $schema_entry; + } + + /** + * Tokenizes the given value into an array of tokens + * + * @param string $value String to parse + * + * @access protected + * @return array Array of tokens + */ + protected function _tokenize($value) + { + $tokens = array(); // array of tokens + $matches = array(); // matches[0] full pattern match, [1,2,3] subpatterns + + // this one is taken from perl-ldap, modified for php + $pattern = "/\s* (?:([()]) | ([^'\s()]+) | '((?:[^']+|'[^\s)])*)') \s*/x"; + + /** + * This one matches one big pattern wherin only one of the three subpatterns matched + * We are interested in the subpatterns that matched. If it matched its value will be + * non-empty and so it is a token. Tokens may be round brackets, a string, or a string + * enclosed by ' + */ + preg_match_all($pattern, $value, $matches); + + for ($i = 0; $i < count($matches[0]); $i++) { // number of tokens (full pattern match) + for ($j = 1; $j < 4; $j++) { // each subpattern + if (null != trim($matches[$j][$i])) { // pattern match in this subpattern + $tokens[$i] = trim($matches[$j][$i]); // this is the token + } + } + } + return $tokens; + } + + /** + * Returns wether a attribute syntax is binary or not + * + * This method gets used by Net_LDAP2_Entry to decide which + * PHP function needs to be used to fetch the value in the + * proper format (e.g. binary or string) + * + * @param string $attribute The name of the attribute (eg.: 'sn') + * + * @access public + * @return boolean + */ + public function isBinary($attribute) + { + $return = false; // default to false + + // This list contains all syntax that should be treaten as + // containing binary values + // The Syntax Definitons go into constants at the top of this page + $syntax_binary = array( + NET_LDAP2_SYNTAX_OCTET_STRING, + NET_LDAP2_SYNTAX_JPEG + ); + + // Check Syntax + $attr_s = $this->get('attribute', $attribute); + if (Net_LDAP2::isError($attr_s)) { + // Attribute not found in schema + $return = false; // consider attr not binary + } elseif (isset($attr_s['syntax']) && in_array($attr_s['syntax'], $syntax_binary)) { + // Syntax is defined as binary in schema + $return = true; + } else { + // Syntax not defined as binary, or not found + // if attribute is a subtype, check superior attribute syntaxes + if (isset($attr_s['sup'])) { + foreach ($attr_s['sup'] as $superattr) { + $return = $this->isBinary($superattr); + if ($return) { + break; // stop checking parents since we are binary + } + } + } + } + + return $return; + } + + // [TODO] add method that allows us to see to which objectclasses a certain attribute belongs to + // it should return the result structured, e.g. sorted in "may" and "must". Optionally it should + // be able to return it just "flat", e.g. array_merge()d. + // We could use get_all() to achieve this easily, i think +} +?> diff --git a/plugins/LdapCommon/extlib/Net/LDAP2/SchemaCache.interface.php b/plugins/LdapCommon/extlib/Net/LDAP2/SchemaCache.interface.php new file mode 100644 index 000000000..e0c3094c4 --- /dev/null +++ b/plugins/LdapCommon/extlib/Net/LDAP2/SchemaCache.interface.php @@ -0,0 +1,59 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +/** +* File containing the Net_LDAP2_SchemaCache interface class. +* +* PHP version 5 +* +* @category Net +* @package Net_LDAP2 +* @author Benedikt Hallinger <beni@php.net> +* @copyright 2009 Benedikt Hallinger +* @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 +* @version SVN: $Id: SchemaCache.interface.php 286718 2009-08-03 07:30:49Z beni $ +* @link http://pear.php.net/package/Net_LDAP2/ +*/ + +/** +* Interface describing a custom schema cache object +* +* To implement a custom schema cache, one must implement this interface and +* pass the instanciated object to Net_LDAP2s registerSchemaCache() method. +*/ +interface Net_LDAP2_SchemaCache +{ + /** + * Return the schema object from the cache + * + * Net_LDAP2 will consider anything returned invalid, except + * a valid Net_LDAP2_Schema object. + * In case you return a Net_LDAP2_Error, this error will be routed + * to the return of the $ldap->schema() call. + * If you return something else, Net_LDAP2 will + * fetch a fresh Schema object from the LDAP server. + * + * You may want to implement a cache aging mechanism here too. + * + * @return Net_LDAP2_Schema|Net_LDAP2_Error|false + */ + public function loadSchema(); + + /** + * Store a schema object in the cache + * + * This method will be called, if Net_LDAP2 has fetched a fresh + * schema object from LDAP and wants to init or refresh the cache. + * + * In case of errors you may return a Net_LDAP2_Error which will + * be routet to the client. + * Note that doing this prevents, that the schema object fetched from LDAP + * will be given back to the client, so only return errors if storing + * of the cache is something crucial (e.g. for doing something else with it). + * Normaly you dont want to give back errors in which case Net_LDAP2 needs to + * fetch the schema once per script run and instead use the error + * returned from loadSchema(). + * + * @return true|Net_LDAP2_Error + */ + public function storeSchema($schema); +} diff --git a/plugins/LdapCommon/extlib/Net/LDAP2/Search.php b/plugins/LdapCommon/extlib/Net/LDAP2/Search.php new file mode 100644 index 000000000..de4fde122 --- /dev/null +++ b/plugins/LdapCommon/extlib/Net/LDAP2/Search.php @@ -0,0 +1,614 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +/** +* File containing the Net_LDAP2_Search interface class. +* +* PHP version 5 +* +* @category Net +* @package Net_LDAP2 +* @author Tarjej Huse <tarjei@bergfald.no> +* @author Benedikt Hallinger <beni@php.net> +* @copyright 2009 Tarjej Huse, Benedikt Hallinger +* @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 +* @version SVN: $Id: Search.php 286718 2009-08-03 07:30:49Z beni $ +* @link http://pear.php.net/package/Net_LDAP2/ +*/ + +/** +* Includes +*/ +require_once 'PEAR.php'; + +/** +* Result set of an LDAP search +* +* @category Net +* @package Net_LDAP2 +* @author Tarjej Huse <tarjei@bergfald.no> +* @author Benedikt Hallinger <beni@php.net> +* @license http://www.gnu.org/copyleft/lesser.html LGPL +* @link http://pear.php.net/package/Net_LDAP22/ +*/ +class Net_LDAP2_Search extends PEAR implements Iterator +{ + /** + * Search result identifier + * + * @access protected + * @var resource + */ + protected $_search; + + /** + * LDAP resource link + * + * @access protected + * @var resource + */ + protected $_link; + + /** + * Net_LDAP2 object + * + * A reference of the Net_LDAP2 object for passing to Net_LDAP2_Entry + * + * @access protected + * @var object Net_LDAP2 + */ + protected $_ldap; + + /** + * Result entry identifier + * + * @access protected + * @var resource + */ + protected $_entry = null; + + /** + * The errorcode the search got + * + * Some errorcodes might be of interest, but might not be best handled as errors. + * examples: 4 - LDAP_SIZELIMIT_EXCEEDED - indicates a huge search. + * Incomplete results are returned. If you just want to check if there's anything in the search. + * than this is a point to handle. + * 32 - no such object - search here returns a count of 0. + * + * @access protected + * @var int + */ + protected $_errorCode = 0; // if not set - sucess! + + /** + * Cache for all entries already fetched from iterator interface + * + * @access protected + * @var array + */ + protected $_iteratorCache = array(); + + /** + * What attributes we searched for + * + * The $attributes array contains the names of the searched attributes and gets + * passed from $Net_LDAP2->search() so the Net_LDAP2_Search object can tell + * what attributes was searched for ({@link searchedAttrs()) + * + * This variable gets set from the constructor and returned + * from {@link searchedAttrs()} + * + * @access protected + * @var array + */ + protected $_searchedAttrs = array(); + + /** + * Cache variable for storing entries fetched internally + * + * This currently is only used by {@link pop_entry()} + * + * @access protected + * @var array + */ + protected $_entry_cache = false; + + /** + * Constructor + * + * @param resource &$search Search result identifier + * @param Net_LDAP2|resource &$ldap Net_LDAP2 object or just a LDAP-Link resource + * @param array $attributes (optional) Array with searched attribute names. (see {@link $_searchedAttrs}) + * + * @access public + */ + public function __construct(&$search, &$ldap, $attributes = array()) + { + $this->PEAR('Net_LDAP2_Error'); + + $this->setSearch($search); + + if ($ldap instanceof Net_LDAP2) { + $this->_ldap =& $ldap; + $this->setLink($this->_ldap->getLink()); + } else { + $this->setLink($ldap); + } + + $this->_errorCode = @ldap_errno($this->_link); + + if (is_array($attributes) && !empty($attributes)) { + $this->_searchedAttrs = $attributes; + } + } + + /** + * Returns an array of entry objects + * + * @return array Array of entry objects. + */ + public function entries() + { + $entries = array(); + + while ($entry = $this->shiftEntry()) { + $entries[] = $entry; + } + + return $entries; + } + + /** + * Get the next entry in the searchresult. + * + * This will return a valid Net_LDAP2_Entry object or false, so + * you can use this method to easily iterate over the entries inside + * a while loop. + * + * @return Net_LDAP2_Entry|false Reference to Net_LDAP2_Entry object or false + */ + public function &shiftEntry() + { + if ($this->count() == 0 ) { + $false = false; + return $false; + } + + if (is_null($this->_entry)) { + $this->_entry = @ldap_first_entry($this->_link, $this->_search); + $entry = Net_LDAP2_Entry::createConnected($this->_ldap, $this->_entry); + if ($entry instanceof Net_LDAP2_Error) $entry = false; + } else { + if (!$this->_entry = @ldap_next_entry($this->_link, $this->_entry)) { + $false = false; + return $false; + } + $entry = Net_LDAP2_Entry::createConnected($this->_ldap, $this->_entry); + if ($entry instanceof Net_LDAP2_Error) $entry = false; + } + return $entry; + } + + /** + * Alias function of shiftEntry() for perl-ldap interface + * + * @see shiftEntry() + * @return Net_LDAP2_Entry|false + */ + public function shift_entry() + { + $args = func_get_args(); + return call_user_func_array(array( &$this, 'shiftEntry' ), $args); + } + + /** + * Retrieve the next entry in the searchresult, but starting from last entry + * + * This is the opposite to {@link shiftEntry()} and is also very useful + * to be used inside a while loop. + * + * @return Net_LDAP2_Entry|false + */ + public function popEntry() + { + if (false === $this->_entry_cache) { + // fetch entries into cache if not done so far + $this->_entry_cache = $this->entries(); + } + + $return = array_pop($this->_entry_cache); + return (null === $return)? false : $return; + } + + /** + * Alias function of popEntry() for perl-ldap interface + * + * @see popEntry() + * @return Net_LDAP2_Entry|false + */ + public function pop_entry() + { + $args = func_get_args(); + return call_user_func_array(array( &$this, 'popEntry' ), $args); + } + + /** + * Return entries sorted as array + * + * This returns a array with sorted entries and the values. + * Sorting is done with PHPs {@link array_multisort()}. + * This method relies on {@link as_struct()} to fetch the raw data of the entries. + * + * Please note that attribute names are case sensitive! + * + * Usage example: + * <code> + * // to sort entries first by location, then by surename, but descending: + * $entries = $search->sorted_as_struct(array('locality','sn'), SORT_DESC); + * </code> + * + * @param array $attrs Array of attribute names to sort; order from left to right. + * @param int $order Ordering direction, either constant SORT_ASC or SORT_DESC + * + * @return array|Net_LDAP2_Error Array with sorted entries or error + * @todo what about server side sorting as specified in http://www.ietf.org/rfc/rfc2891.txt? + */ + public function sorted_as_struct($attrs = array('cn'), $order = SORT_ASC) + { + /* + * Old Code, suitable and fast for single valued sorting + * This code should be used if we know that single valued sorting is desired, + * but we need some method to get that knowledge... + */ + /* + $attrs = array_reverse($attrs); + foreach ($attrs as $attribute) { + if (!ldap_sort($this->_link, $this->_search, $attribute)){ + $this->raiseError("Sorting failed for Attribute " . $attribute); + } + } + + $results = ldap_get_entries($this->_link, $this->_search); + + unset($results['count']); //for tidier output + if ($order) { + return array_reverse($results); + } else { + return $results; + }*/ + + /* + * New code: complete "client side" sorting + */ + // first some parameterchecks + if (!is_array($attrs)) { + return PEAR::raiseError("Sorting failed: Parameterlist must be an array!"); + } + if ($order != SORT_ASC && $order != SORT_DESC) { + return PEAR::raiseError("Sorting failed: sorting direction not understood! (neither constant SORT_ASC nor SORT_DESC)"); + } + + // fetch the entries data + $entries = $this->as_struct(); + + // now sort each entries attribute values + // this is neccessary because later we can only sort by one value, + // so we need the highest or lowest attribute now, depending on the + // selected ordering for that specific attribute + foreach ($entries as $dn => $entry) { + foreach ($entry as $attr_name => $attr_values) { + sort($entries[$dn][$attr_name]); + if ($order == SORT_DESC) { + array_reverse($entries[$dn][$attr_name]); + } + } + } + + // reformat entrys array for later use with array_multisort() + $to_sort = array(); // <- will be a numeric array similar to ldap_get_entries + foreach ($entries as $dn => $entry_attr) { + $row = array(); + $row['dn'] = $dn; + foreach ($entry_attr as $attr_name => $attr_values) { + $row[$attr_name] = $attr_values; + } + $to_sort[] = $row; + } + + // Build columns for array_multisort() + // each requested attribute is one row + $columns = array(); + foreach ($attrs as $attr_name) { + foreach ($to_sort as $key => $row) { + $columns[$attr_name][$key] =& $to_sort[$key][$attr_name][0]; + } + } + + // sort the colums with array_multisort, if there is something + // to sort and if we have requested sort columns + if (!empty($to_sort) && !empty($columns)) { + $sort_params = ''; + foreach ($attrs as $attr_name) { + $sort_params .= '$columns[\''.$attr_name.'\'], '.$order.', '; + } + eval("array_multisort($sort_params \$to_sort);"); // perform sorting + } + + return $to_sort; + } + + /** + * Return entries sorted as objects + * + * This returns a array with sorted Net_LDAP2_Entry objects. + * The sorting is actually done with {@link sorted_as_struct()}. + * + * Please note that attribute names are case sensitive! + * Also note, that it is (depending on server capabilitys) possible to let + * the server sort your results. This happens through search controls + * and is described in detail at {@link http://www.ietf.org/rfc/rfc2891.txt} + * + * Usage example: + * <code> + * // to sort entries first by location, then by surename, but descending: + * $entries = $search->sorted(array('locality','sn'), SORT_DESC); + * </code> + * + * @param array $attrs Array of sort attributes to sort; order from left to right. + * @param int $order Ordering direction, either constant SORT_ASC or SORT_DESC + * + * @return array|Net_LDAP2_Error Array with sorted Net_LDAP2_Entries or error + * @todo Entry object construction could be faster. Maybe we could use one of the factorys instead of fetching the entry again + */ + public function sorted($attrs = array('cn'), $order = SORT_ASC) + { + $return = array(); + $sorted = $this->sorted_as_struct($attrs, $order); + if (PEAR::isError($sorted)) { + return $sorted; + } + foreach ($sorted as $key => $row) { + $entry = $this->_ldap->getEntry($row['dn'], $this->searchedAttrs()); + if (!PEAR::isError($entry)) { + array_push($return, $entry); + } else { + return $entry; + } + } + return $return; + } + + /** + * Return entries as array + * + * This method returns the entries and the selected attributes values as + * array. + * The first array level contains all found entries where the keys are the + * DNs of the entries. The second level arrays contian the entries attributes + * such that the keys is the lowercased name of the attribute and the values + * are stored in another indexed array. Note that the attribute values are stored + * in an array even if there is no or just one value. + * + * The array has the following structure: + * <code> + * $return = array( + * 'cn=foo,dc=example,dc=com' => array( + * 'sn' => array('foo'), + * 'multival' => array('val1', 'val2', 'valN') + * ) + * 'cn=bar,dc=example,dc=com' => array( + * 'sn' => array('bar'), + * 'multival' => array('val1', 'valN') + * ) + * ) + * </code> + * + * @return array associative result array as described above + */ + public function as_struct() + { + $return = array(); + $entries = $this->entries(); + foreach ($entries as $entry) { + $attrs = array(); + $entry_attributes = $entry->attributes(); + foreach ($entry_attributes as $attr_name) { + $attr_values = $entry->getValue($attr_name, 'all'); + if (!is_array($attr_values)) { + $attr_values = array($attr_values); + } + $attrs[$attr_name] = $attr_values; + } + $return[$entry->dn()] = $attrs; + } + return $return; + } + + /** + * Set the search objects resource link + * + * @param resource &$search Search result identifier + * + * @access public + * @return void + */ + public function setSearch(&$search) + { + $this->_search = $search; + } + + /** + * Set the ldap ressource link + * + * @param resource &$link Link identifier + * + * @access public + * @return void + */ + public function setLink(&$link) + { + $this->_link = $link; + } + + /** + * Returns the number of entries in the searchresult + * + * @return int Number of entries in search. + */ + public function count() + { + // this catches the situation where OL returned errno 32 = no such object! + if (!$this->_search) { + return 0; + } + return @ldap_count_entries($this->_link, $this->_search); + } + + /** + * Get the errorcode the object got in its search. + * + * @return int The ldap error number. + */ + public function getErrorCode() + { + return $this->_errorCode; + } + + /** + * Destructor + * + * @access protected + */ + public function _Net_LDAP2_Search() + { + @ldap_free_result($this->_search); + } + + /** + * Closes search result + * + * @return void + */ + public function done() + { + $this->_Net_LDAP2_Search(); + } + + /** + * Return the attribute names this search selected + * + * @return array + * @see $_searchedAttrs + * @access protected + */ + protected function searchedAttrs() + { + return $this->_searchedAttrs; + } + + /** + * Tells if this search exceeds a sizelimit + * + * @return boolean + */ + public function sizeLimitExceeded() + { + return ($this->getErrorCode() == 4); + } + + + /* + * SPL Iterator interface methods. + * This interface allows to use Net_LDAP2_Search + * objects directly inside a foreach loop! + */ + /** + * SPL Iterator interface: Return the current element. + * + * The SPL Iterator interface allows you to fetch entries inside + * a foreach() loop: <code>foreach ($search as $dn => $entry) { ...</code> + * + * Of course, you may call {@link current()}, {@link key()}, {@link next()}, + * {@link rewind()} and {@link valid()} yourself. + * + * If the search throwed an error, it returns false. + * False is also returned, if the end is reached + * In case no call to next() was made, we will issue one, + * thus returning the first entry. + * + * @return Net_LDAP2_Entry|false + */ + public function current() + { + if (count($this->_iteratorCache) == 0) { + $this->next(); + reset($this->_iteratorCache); + } + $entry = current($this->_iteratorCache); + return ($entry instanceof Net_LDAP2_Entry)? $entry : false; + } + + /** + * SPL Iterator interface: Return the identifying key (DN) of the current entry. + * + * @see current() + * @return string|false DN of the current entry; false in case no entry is returned by current() + */ + public function key() + { + $entry = $this->current(); + return ($entry instanceof Net_LDAP2_Entry)? $entry->dn() :false; + } + + /** + * SPL Iterator interface: Move forward to next entry. + * + * After a call to {@link next()}, {@link current()} will return + * the next entry in the result set. + * + * @see current() + * @return void + */ + public function next() + { + // fetch next entry. + // if we have no entrys anymore, we add false (which is + // returned by shiftEntry()) so current() will complain. + if (count($this->_iteratorCache) - 1 <= $this->count()) { + $this->_iteratorCache[] = $this->shiftEntry(); + } + + // move on array pointer to current element. + // even if we have added all entries, this will + // ensure proper operation in case we rewind() + next($this->_iteratorCache); + } + + /** + * SPL Iterator interface: Check if there is a current element after calls to {@link rewind()} or {@link next()}. + * + * Used to check if we've iterated to the end of the collection. + * + * @see current() + * @return boolean FALSE if there's nothing more to iterate over + */ + public function valid() + { + return ($this->current() instanceof Net_LDAP2_Entry); + } + + /** + * SPL Iterator interface: Rewind the Iterator to the first element. + * + * After rewinding, {@link current()} will return the first entry in the result set. + * + * @see current() + * @return void + */ + public function rewind() + { + reset($this->_iteratorCache); + } +} + +?> diff --git a/plugins/LdapCommon/extlib/Net/LDAP2/SimpleFileSchemaCache.php b/plugins/LdapCommon/extlib/Net/LDAP2/SimpleFileSchemaCache.php new file mode 100644 index 000000000..8019654ac --- /dev/null +++ b/plugins/LdapCommon/extlib/Net/LDAP2/SimpleFileSchemaCache.php @@ -0,0 +1,97 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +/** +* File containing the example simple file based Schema Caching class. +* +* PHP version 5 +* +* @category Net +* @package Net_LDAP2 +* @author Benedikt Hallinger <beni@php.net> +* @copyright 2009 Benedikt Hallinger +* @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 +* @version SVN: $Id: SimpleFileSchemaCache.php 286718 2009-08-03 07:30:49Z beni $ +* @link http://pear.php.net/package/Net_LDAP2/ +*/ + +/** +* A simple file based schema cacher with cache aging. +* +* Once the cache is too old, the loadSchema() method will return false, so +* Net_LDAP2 will fetch a fresh object from the LDAP server that will +* overwrite the current (outdated) old cache. +*/ +class Net_LDAP2_SimpleFileSchemaCache implements Net_LDAP2_SchemaCache +{ + /** + * Internal config of this cache + * + * @see Net_LDAP2_SimpleFileSchemaCache() + * @var array + */ + protected $config = array( + 'path' => '/tmp/Net_LDAP_Schema.cache', + 'max_age' => 1200 + ); + + /** + * Initialize the simple cache + * + * Config is as following: + * path Complete path to the cache file. + * max_age Maximum age of cache in seconds, 0 means "endlessly". + * + * @param array $cfg Config array + */ + public function Net_LDAP2_SimpleFileSchemaCache($cfg) + { + foreach ($cfg as $key => $value) { + if (array_key_exists($key, $this->config)) { + if (gettype($this->config[$key]) != gettype($value)) { + $this->getCore()->dropFatalError(__CLASS__.": Could not set config! Key $key does not match type ".gettype($this->config[$key])."!"); + } + $this->config[$key] = $value; + } else { + $this->getCore()->dropFatalError(__CLASS__.": Could not set config! Key $key is not defined!"); + } + } + } + + /** + * Return the schema object from the cache + * + * If file is existent and cache has not expired yet, + * then the cache is deserialized and returned. + * + * @return Net_LDAP2_Schema|Net_LDAP2_Error|false + */ + public function loadSchema() + { + $return = false; // Net_LDAP2 will load schema from LDAP + if (file_exists($this->config['path'])) { + $cache_maxage = filemtime($this->config['path']) + $this->config['max_age']; + if (time() <= $cache_maxage || $this->config['max_age'] == 0) { + $return = unserialize(file_get_contents($this->config['path'])); + } + } + return $return; + } + + /** + * Store a schema object in the cache + * + * This method will be called, if Net_LDAP2 has fetched a fresh + * schema object from LDAP and wants to init or refresh the cache. + * + * To invalidate the cache and cause Net_LDAP2 to refresh the cache, + * you can call this method with null or false as value. + * The next call to $ldap->schema() will then refresh the caches object. + * + * @param mixed $schema The object that should be cached + * @return true|Net_LDAP2_Error|false + */ + public function storeSchema($schema) { + file_put_contents($this->config['path'], serialize($schema)); + return true; + } +} diff --git a/plugins/LdapCommon/extlib/Net/LDAP2/Util.php b/plugins/LdapCommon/extlib/Net/LDAP2/Util.php new file mode 100644 index 000000000..48b03f9f9 --- /dev/null +++ b/plugins/LdapCommon/extlib/Net/LDAP2/Util.php @@ -0,0 +1,572 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +/** +* File containing the Net_LDAP2_Util interface class. +* +* PHP version 5 +* +* @category Net +* @package Net_LDAP2 +* @author Benedikt Hallinger <beni@php.net> +* @copyright 2009 Benedikt Hallinger +* @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 +* @version SVN: $Id: Util.php 286718 2009-08-03 07:30:49Z beni $ +* @link http://pear.php.net/package/Net_LDAP2/ +*/ + +/** +* Includes +*/ +require_once 'PEAR.php'; + +/** +* Utility Class for Net_LDAP2 +* +* This class servers some functionality to the other classes of Net_LDAP2 but most of +* the methods can be used separately as well. +* +* @category Net +* @package Net_LDAP2 +* @author Benedikt Hallinger <beni@php.net> +* @license http://www.gnu.org/copyleft/lesser.html LGPL +* @link http://pear.php.net/package/Net_LDAP22/ +*/ +class Net_LDAP2_Util extends PEAR +{ + /** + * Constructor + * + * @access public + */ + public function __construct() + { + // We do nothing here, since all methods can be called statically. + // In Net_LDAP <= 0.7, we needed a instance of Util, because + // it was possible to do utf8 encoding and decoding, but this + // has been moved to the LDAP class. The constructor remains only + // here to document the downward compatibility of creating an instance. + } + + /** + * Explodes the given DN into its elements + * + * {@link http://www.ietf.org/rfc/rfc2253.txt RFC 2253} says, a Distinguished Name is a sequence + * of Relative Distinguished Names (RDNs), which themselves + * are sets of Attributes. For each RDN a array is constructed where the RDN part is stored. + * + * For example, the DN 'OU=Sales+CN=J. Smith,DC=example,DC=net' is exploded to: + * <kbd>array( [0] => array([0] => 'OU=Sales', [1] => 'CN=J. Smith'), [2] => 'DC=example', [3] => 'DC=net' )</kbd> + * + * [NOT IMPLEMENTED] DNs might also contain values, which are the bytes of the BER encoding of + * the X.500 AttributeValue rather than some LDAP string syntax. These values are hex-encoded + * and prefixed with a #. To distinguish such BER values, ldap_explode_dn uses references to + * the actual values, e.g. '1.3.6.1.4.1.1466.0=#04024869,DC=example,DC=com' is exploded to: + * [ { '1.3.6.1.4.1.1466.0' => "\004\002Hi" }, { 'DC' => 'example' }, { 'DC' => 'com' } ]; + * See {@link http://www.vijaymukhi.com/vmis/berldap.htm} for more information on BER. + * + * It also performs the following operations on the given DN: + * - Unescape "\" followed by ",", "+", """, "\", "<", ">", ";", "#", "=", " ", or a hexpair + * and strings beginning with "#". + * - Removes the leading 'OID.' characters if the type is an OID instead of a name. + * - If an RDN contains multiple parts, the parts are re-ordered so that the attribute type names are in alphabetical order. + * + * OPTIONS is a list of name/value pairs, valid options are: + * casefold Controls case folding of attribute types names. + * Attribute values are not affected by this option. + * The default is to uppercase. Valid values are: + * lower Lowercase attribute types names. + * upper Uppercase attribute type names. This is the default. + * none Do not change attribute type names. + * reverse If TRUE, the RDN sequence is reversed. + * onlyvalues If TRUE, then only attributes values are returned ('foo' instead of 'cn=foo') + * + + * @param string $dn The DN that should be exploded + * @param array $options Options to use + * + * @static + * @return array Parts of the exploded DN + * @todo implement BER + */ + public static function ldap_explode_dn($dn, $options = array('casefold' => 'upper')) + { + if (!isset($options['onlyvalues'])) $options['onlyvalues'] = false; + if (!isset($options['reverse'])) $options['reverse'] = false; + if (!isset($options['casefold'])) $options['casefold'] = 'upper'; + + // Escaping of DN and stripping of "OID." + $dn = self::canonical_dn($dn, array('casefold' => $options['casefold'])); + + // splitting the DN + $dn_array = preg_split('/(?<=[^\\\\]),/', $dn); + + // clear wrong splitting (possibly we have split too much) + // /!\ Not clear, if this is neccessary here + //$dn_array = self::correct_dn_splitting($dn_array, ','); + + // construct subarrays for multivalued RDNs and unescape DN value + // also convert to output format and apply casefolding + foreach ($dn_array as $key => $value) { + $value_u = self::unescape_dn_value($value); + $rdns = self::split_rdn_multival($value_u[0]); + if (count($rdns) > 1) { + // MV RDN! + foreach ($rdns as $subrdn_k => $subrdn_v) { + // Casefolding + if ($options['casefold'] == 'upper') $subrdn_v = preg_replace("/^(\w+=)/e", "''.strtoupper('\\1').''", $subrdn_v); + if ($options['casefold'] == 'lower') $subrdn_v = preg_replace("/^(\w+=)/e", "''.strtolower('\\1').''", $subrdn_v); + + if ($options['onlyvalues']) { + preg_match('/(.+?)(?<!\\\\)=(.+)/', $subrdn_v, $matches); + $rdn_ocl = $matches[1]; + $rdn_val = $matches[2]; + $unescaped = self::unescape_dn_value($rdn_val); + $rdns[$subrdn_k] = $unescaped[0]; + } else { + $unescaped = self::unescape_dn_value($subrdn_v); + $rdns[$subrdn_k] = $unescaped[0]; + } + } + + $dn_array[$key] = $rdns; + } else { + // normal RDN + + // Casefolding + if ($options['casefold'] == 'upper') $value = preg_replace("/^(\w+=)/e", "''.strtoupper('\\1').''", $value); + if ($options['casefold'] == 'lower') $value = preg_replace("/^(\w+=)/e", "''.strtolower('\\1').''", $value); + + if ($options['onlyvalues']) { + preg_match('/(.+?)(?<!\\\\)=(.+)/', $value, $matches); + $dn_ocl = $matches[1]; + $dn_val = $matches[2]; + $unescaped = self::unescape_dn_value($dn_val); + $dn_array[$key] = $unescaped[0]; + } else { + $unescaped = self::unescape_dn_value($value); + $dn_array[$key] = $unescaped[0]; + } + } + } + + if ($options['reverse']) { + return array_reverse($dn_array); + } else { + return $dn_array; + } + } + + /** + * Escapes a DN value according to RFC 2253 + * + * Escapes the given VALUES according to RFC 2253 so that they can be safely used in LDAP DNs. + * The characters ",", "+", """, "\", "<", ">", ";", "#", "=" with a special meaning in RFC 2252 + * are preceeded by ba backslash. Control characters with an ASCII code < 32 are represented as \hexpair. + * Finally all leading and trailing spaces are converted to sequences of \20. + * + * @param array $values An array containing the DN values that should be escaped + * + * @static + * @return array The array $values, but escaped + */ + public static function escape_dn_value($values = array()) + { + // Parameter validation + if (!is_array($values)) { + $values = array($values); + } + + foreach ($values as $key => $val) { + // Escaping of filter meta characters + $val = str_replace('\\', '\\\\', $val); + $val = str_replace(',', '\,', $val); + $val = str_replace('+', '\+', $val); + $val = str_replace('"', '\"', $val); + $val = str_replace('<', '\<', $val); + $val = str_replace('>', '\>', $val); + $val = str_replace(';', '\;', $val); + $val = str_replace('#', '\#', $val); + $val = str_replace('=', '\=', $val); + + // ASCII < 32 escaping + $val = self::asc2hex32($val); + + // Convert all leading and trailing spaces to sequences of \20. + if (preg_match('/^(\s*)(.+?)(\s*)$/', $val, $matches)) { + $val = $matches[2]; + for ($i = 0; $i < strlen($matches[1]); $i++) { + $val = '\20'.$val; + } + for ($i = 0; $i < strlen($matches[3]); $i++) { + $val = $val.'\20'; + } + } + + if (null === $val) $val = '\0'; // apply escaped "null" if string is empty + + $values[$key] = $val; + } + + return $values; + } + + /** + * Undoes the conversion done by escape_dn_value(). + * + * Any escape sequence starting with a baskslash - hexpair or special character - + * will be transformed back to the corresponding character. + * + * @param array $values Array of DN Values + * + * @return array Same as $values, but unescaped + * @static + */ + public static function unescape_dn_value($values = array()) + { + // Parameter validation + if (!is_array($values)) { + $values = array($values); + } + + foreach ($values as $key => $val) { + // strip slashes from special chars + $val = str_replace('\\\\', '\\', $val); + $val = str_replace('\,', ',', $val); + $val = str_replace('\+', '+', $val); + $val = str_replace('\"', '"', $val); + $val = str_replace('\<', '<', $val); + $val = str_replace('\>', '>', $val); + $val = str_replace('\;', ';', $val); + $val = str_replace('\#', '#', $val); + $val = str_replace('\=', '=', $val); + + // Translate hex code into ascii + $values[$key] = self::hex2asc($val); + } + + return $values; + } + + /** + * Returns the given DN in a canonical form + * + * Returns false if DN is not a valid Distinguished Name. + * DN can either be a string or an array + * as returned by ldap_explode_dn, which is useful when constructing a DN. + * The DN array may have be indexed (each array value is a OCL=VALUE pair) + * or associative (array key is OCL and value is VALUE). + * + * It performs the following operations on the given DN: + * - Removes the leading 'OID.' characters if the type is an OID instead of a name. + * - Escapes all RFC 2253 special characters (",", "+", """, "\", "<", ">", ";", "#", "="), slashes ("/"), and any other character where the ASCII code is < 32 as \hexpair. + * - Converts all leading and trailing spaces in values to be \20. + * - If an RDN contains multiple parts, the parts are re-ordered so that the attribute type names are in alphabetical order. + * + * OPTIONS is a list of name/value pairs, valid options are: + * casefold Controls case folding of attribute type names. + * Attribute values are not affected by this option. The default is to uppercase. + * Valid values are: + * lower Lowercase attribute type names. + * upper Uppercase attribute type names. This is the default. + * none Do not change attribute type names. + * [NOT IMPLEMENTED] mbcescape If TRUE, characters that are encoded as a multi-octet UTF-8 sequence will be escaped as \(hexpair){2,*}. + * reverse If TRUE, the RDN sequence is reversed. + * separator Separator to use between RDNs. Defaults to comma (','). + * + * Note: The empty string "" is a valid DN, so be sure not to do a "$can_dn == false" test, + * because an empty string evaluates to false. Use the "===" operator instead. + * + * @param array|string $dn The DN + * @param array $options Options to use + * + * @static + * @return false|string The canonical DN or FALSE + * @todo implement option mbcescape + */ + public static function canonical_dn($dn, $options = array('casefold' => 'upper', 'separator' => ',')) + { + if ($dn === '') return $dn; // empty DN is valid! + + // options check + if (!isset($options['reverse'])) { + $options['reverse'] = false; + } else { + $options['reverse'] = true; + } + if (!isset($options['casefold'])) $options['casefold'] = 'upper'; + if (!isset($options['separator'])) $options['separator'] = ','; + + + if (!is_array($dn)) { + // It is not clear to me if the perl implementation splits by the user defined + // separator or if it just uses this separator to construct the new DN + $dn = preg_split('/(?<=[^\\\\])'.$options['separator'].'/', $dn); + + // clear wrong splitting (possibly we have split too much) + $dn = self::correct_dn_splitting($dn, $options['separator']); + } else { + // Is array, check, if the array is indexed or associative + $assoc = false; + foreach ($dn as $dn_key => $dn_part) { + if (!is_int($dn_key)) { + $assoc = true; + } + } + // convert to indexed, if associative array detected + if ($assoc) { + $newdn = array(); + foreach ($dn as $dn_key => $dn_part) { + if (is_array($dn_part)) { + ksort($dn_part, SORT_STRING); // we assume here, that the rdn parts are also associative + $newdn[] = $dn_part; // copy array as-is, so we can resolve it later + } else { + $newdn[] = $dn_key.'='.$dn_part; + } + } + $dn =& $newdn; + } + } + + // Escaping and casefolding + foreach ($dn as $pos => $dnval) { + if (is_array($dnval)) { + // subarray detected, this means very surely, that we had + // a multivalued dn part, which must be resolved + $dnval_new = ''; + foreach ($dnval as $subkey => $subval) { + // build RDN part + if (!is_int($subkey)) { + $subval = $subkey.'='.$subval; + } + $subval_processed = self::canonical_dn($subval); + if (false === $subval_processed) return false; + $dnval_new .= $subval_processed.'+'; + } + $dn[$pos] = substr($dnval_new, 0, -1); // store RDN part, strip last plus + } else { + // try to split multivalued RDNS into array + $rdns = self::split_rdn_multival($dnval); + if (count($rdns) > 1) { + // Multivalued RDN was detected! + // The RDN value is expected to be correctly split by split_rdn_multival(). + // It's time to sort the RDN and build the DN! + $rdn_string = ''; + sort($rdns, SORT_STRING); // Sort RDN keys alphabetically + foreach ($rdns as $rdn) { + $subval_processed = self::canonical_dn($rdn); + if (false === $subval_processed) return false; + $rdn_string .= $subval_processed.'+'; + } + + $dn[$pos] = substr($rdn_string, 0, -1); // store RDN part, strip last plus + + } else { + // no multivalued RDN! + // split at first unescaped "=" + $dn_comp = preg_split('/(?<=[^\\\\])=/', $rdns[0], 2); + $ocl = ltrim($dn_comp[0]); // trim left whitespaces 'cause of "cn=foo, l=bar" syntax (whitespace after comma) + $val = $dn_comp[1]; + + // strip 'OID.', otherwise apply casefolding and escaping + if (substr(strtolower($ocl), 0, 4) == 'oid.') { + $ocl = substr($ocl, 4); + } else { + if ($options['casefold'] == 'upper') $ocl = strtoupper($ocl); + if ($options['casefold'] == 'lower') $ocl = strtolower($ocl); + $ocl = self::escape_dn_value(array($ocl)); + $ocl = $ocl[0]; + } + + // escaping of dn-value + $val = self::escape_dn_value(array($val)); + $val = str_replace('/', '\/', $val[0]); + + $dn[$pos] = $ocl.'='.$val; + } + } + } + + if ($options['reverse']) $dn = array_reverse($dn); + return implode($options['separator'], $dn); + } + + /** + * Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters. + * + * Any control characters with an ACII code < 32 as well as the characters with special meaning in + * LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a + * backslash followed by two hex digits representing the hexadecimal value of the character. + * + * @param array $values Array of values to escape + * + * @static + * @return array Array $values, but escaped + */ + public static function escape_filter_value($values = array()) + { + // Parameter validation + if (!is_array($values)) { + $values = array($values); + } + + foreach ($values as $key => $val) { + // Escaping of filter meta characters + $val = str_replace('\\', '\5c', $val); + $val = str_replace('*', '\2a', $val); + $val = str_replace('(', '\28', $val); + $val = str_replace(')', '\29', $val); + + // ASCII < 32 escaping + $val = self::asc2hex32($val); + + if (null === $val) $val = '\0'; // apply escaped "null" if string is empty + + $values[$key] = $val; + } + + return $values; + } + + /** + * Undoes the conversion done by {@link escape_filter_value()}. + * + * Converts any sequences of a backslash followed by two hex digits into the corresponding character. + * + * @param array $values Array of values to escape + * + * @static + * @return array Array $values, but unescaped + */ + public static function unescape_filter_value($values = array()) + { + // Parameter validation + if (!is_array($values)) { + $values = array($values); + } + + foreach ($values as $key => $value) { + // Translate hex code into ascii + $values[$key] = self::hex2asc($value); + } + + return $values; + } + + /** + * Converts all ASCII chars < 32 to "\HEX" + * + * @param string $string String to convert + * + * @static + * @return string + */ + public static function asc2hex32($string) + { + for ($i = 0; $i < strlen($string); $i++) { + $char = substr($string, $i, 1); + if (ord($char) < 32) { + $hex = dechex(ord($char)); + if (strlen($hex) == 1) $hex = '0'.$hex; + $string = str_replace($char, '\\'.$hex, $string); + } + } + return $string; + } + + /** + * Converts all Hex expressions ("\HEX") to their original ASCII characters + * + * @param string $string String to convert + * + * @static + * @author beni@php.net, heavily based on work from DavidSmith@byu.net + * @return string + */ + public static function hex2asc($string) + { + $string = preg_replace("/\\\([0-9A-Fa-f]{2})/e", "''.chr(hexdec('\\1')).''", $string); + return $string; + } + + /** + * Split an multivalued RDN value into an Array + * + * A RDN can contain multiple values, spearated by a plus sign. + * This function returns each separate ocl=value pair of the RDN part. + * + * If no multivalued RDN is detected, an array containing only + * the original rdn part is returned. + * + * For example, the multivalued RDN 'OU=Sales+CN=J. Smith' is exploded to: + * <kbd>array([0] => 'OU=Sales', [1] => 'CN=J. Smith')</kbd> + * + * The method trys to be smart if it encounters unescaped "+" characters, but may fail, + * so ensure escaped "+"es in attr names and attr values. + * + * [BUG] If you have a multivalued RDN with unescaped plus characters + * and there is a unescaped plus sign at the end of an value followed by an + * attribute name containing an unescaped plus, then you will get wrong splitting: + * $rdn = 'OU=Sales+C+N=J. Smith'; + * returns: + * array('OU=Sales+C', 'N=J. Smith'); + * The "C+" is treaten as value of the first pair instead as attr name of the second pair. + * To prevent this, escape correctly. + * + * @param string $rdn Part of an (multivalued) escaped RDN (eg. ou=foo OR ou=foo+cn=bar) + * + * @static + * @return array Array with the components of the multivalued RDN or Error + */ + public static function split_rdn_multival($rdn) + { + $rdns = preg_split('/(?<!\\\\)\+/', $rdn); + $rdns = self::correct_dn_splitting($rdns, '+'); + return array_values($rdns); + } + + /** + * Splits a attribute=value syntax into an array + * + * The split will occur at the first unescaped '=' character. + * + * @param string $attr Attribute and Value Syntax + * + * @return array Indexed array: 0=attribute name, 1=attribute value + */ + public static function split_attribute_string($attr) + { + return preg_split('/(?<!\\\\)=/', $attr, 2); + } + + /** + * Corrects splitting of dn parts + * + * @param array $dn Raw DN array + * @param array $separator Separator that was used when splitting + * + * @return array Corrected array + * @access protected + */ + protected static function correct_dn_splitting($dn = array(), $separator = ',') + { + foreach ($dn as $key => $dn_value) { + $dn_value = $dn[$key]; // refresh value (foreach caches!) + // if the dn_value is not in attr=value format, then we had an + // unescaped separator character inside the attr name or the value. + // We assume, that it was the attribute value. + // [TODO] To solve this, we might ask the schema. Keep in mind, that UTIL class + // must remain independent from the other classes or connections. + if (!preg_match('/.+(?<!\\\\)=.+/', $dn_value)) { + unset($dn[$key]); + if (array_key_exists($key-1, $dn)) { + $dn[$key-1] = $dn[$key-1].$separator.$dn_value; // append to previous attr value + } else { + $dn[$key+1] = $dn_value.$separator.$dn[$key+1]; // first element: prepend to next attr name + } + } + } + return array_values($dn); + } +} + +?> diff --git a/plugins/LilUrl/LilUrlPlugin.php b/plugins/LilUrl/LilUrlPlugin.php index c3e37c0c0..1c3d6f84b 100644 --- a/plugins/LilUrl/LilUrlPlugin.php +++ b/plugins/LilUrl/LilUrlPlugin.php @@ -22,7 +22,7 @@ * @category Plugin * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> - * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/LilUrl/locale/LilUrl.pot b/plugins/LilUrl/locale/LilUrl.pot new file mode 100644 index 000000000..47ed36727 --- /dev/null +++ b/plugins/LilUrl/locale/LilUrl.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: LilUrlPlugin.php:68 +#, php-format +msgid "Uses <a href=\"http://%1$s/\">%1$s</a> URL-shortener service." +msgstr "" diff --git a/plugins/Mapstraction/MapstractionPlugin.php b/plugins/Mapstraction/MapstractionPlugin.php index 868933fd4..e7240a644 100644 --- a/plugins/Mapstraction/MapstractionPlugin.php +++ b/plugins/Mapstraction/MapstractionPlugin.php @@ -125,8 +125,8 @@ class MapstractionPlugin extends Plugin $action->script('http://tile.cloudmade.com/wml/0.2/web-maps-lite.js'); break; case 'google': - $action->script(sprintf('http://maps.google.com/maps?file=api&v=2&sensor=false&key=%s', - $this->apikey)); + $action->script(sprintf('http://maps.google.com/maps?file=api&v=2&sensor=false&key=%s', + urlencode($this->apikey))); break; case 'microsoft': $action->script('http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6'); @@ -137,7 +137,7 @@ class MapstractionPlugin extends Plugin break; case 'yahoo': $action->script(sprintf('http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=%s', - $this->apikey)); + urlencode($this->apikey))); break; case 'geocommons': // don't support this yet default: diff --git a/plugins/Mapstraction/allmap.php b/plugins/Mapstraction/allmap.php index e73aa76e8..5dab670e2 100644 --- a/plugins/Mapstraction/allmap.php +++ b/plugins/Mapstraction/allmap.php @@ -38,6 +38,7 @@ if (!defined('STATUSNET')) { * @package StatusNet * @author Evan Prodromou <evan@status.net> * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/Mapstraction/locale/Mapstraction.po b/plugins/Mapstraction/locale/Mapstraction.pot index 1dd5dbbcc..764bf7b29 100644 --- a/plugins/Mapstraction/locale/Mapstraction.po +++ b/plugins/Mapstraction/locale/Mapstraction.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-03-01 14:58-0800\n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -16,14 +16,18 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: allmap.php:71 -#, php-format -msgid "%s friends map" +#: MapstractionPlugin.php:182 +msgid "Map" msgstr "" -#: allmap.php:74 -#, php-format -msgid "%s friends map, page %d" +#: MapstractionPlugin.php:193 +msgid "Full size" +msgstr "" + +#: MapstractionPlugin.php:205 +msgid "" +"Show maps of users' and friends' notices with <a href=\"http://www." +"mapstraction.com/\">Mapstraction</a> JavaScript library." msgstr "" #: map.php:72 @@ -34,18 +38,14 @@ msgstr "" msgid "User has no profile." msgstr "" -#: MapstractionPlugin.php:182 -msgid "Map" -msgstr "" - -#: MapstractionPlugin.php:193 -msgid "Full size" +#: allmap.php:71 +#, php-format +msgid "%s friends map" msgstr "" -#: MapstractionPlugin.php:205 -msgid "" -"Show maps of users' and friends' notices with <a href=\"http://www." -"mapstraction.com/\">Mapstraction</a> JavaScript library." +#: allmap.php:74 +#, php-format +msgid "%s friends map, page %d" msgstr "" #: usermap.php:71 diff --git a/plugins/Mapstraction/map.php b/plugins/Mapstraction/map.php index b809c1b8e..7dab8e10a 100644 --- a/plugins/Mapstraction/map.php +++ b/plugins/Mapstraction/map.php @@ -38,6 +38,7 @@ if (!defined('STATUSNET')) { * @package StatusNet * @author Evan Prodromou <evan@status.net> * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/Mapstraction/usermap.js b/plugins/Mapstraction/usermap.js index 4b7a6c26b..53cfe6bb0 100644 --- a/plugins/Mapstraction/usermap.js +++ b/plugins/Mapstraction/usermap.js @@ -104,7 +104,7 @@ function showMapstraction(element, notices) { pt = new mxn.LatLonPoint(lat, lon); mkr = new mxn.Marker(pt); - mkr.setIcon(n['user']['profile_image_url']); + mkr.setIcon(n['user']['profile_image_url'], [24, 24]); mkr.setInfoBubble('<a href="'+ n['user']['profile_url'] + '">' + n['user']['screen_name'] + '</a>' + ' ' + n['html'] + '<br/><a href="'+ n['url'] + '">'+ n['created_at'] + '</a>'); diff --git a/plugins/Mapstraction/usermap.php b/plugins/Mapstraction/usermap.php index ff47b6ada..094334f60 100644 --- a/plugins/Mapstraction/usermap.php +++ b/plugins/Mapstraction/usermap.php @@ -38,6 +38,7 @@ if (!defined('STATUSNET')) { * @package StatusNet * @author Evan Prodromou <evan@status.net> * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/MemcachedPlugin.php b/plugins/MemcachedPlugin.php index 707e6db9a..77b989b95 100644 --- a/plugins/MemcachedPlugin.php +++ b/plugins/MemcachedPlugin.php @@ -22,8 +22,10 @@ * * @category Cache * @package StatusNet - * @author Evan Prodromou <evan@status.net>, Craig Andrews <candrews@integralblue.com> + * @author Evan Prodromou <evan@status.net> + * @author Craig Andrews <candrews@integralblue.com> * @copyright 2009 StatusNet, Inc. + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ @@ -43,8 +45,10 @@ if (!defined('STATUSNET')) { * * @category Cache * @package StatusNet - * @author Evan Prodromou <evan@status.net>, Craig Andrews <candrews@integralblue.com> + * @author Evan Prodromou <evan@status.net> + * @author Craig Andrews <candrews@integralblue.com> * @copyright 2009 StatusNet, Inc. + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/Meteor/MeteorPlugin.php b/plugins/Meteor/MeteorPlugin.php index 5b345d7c2..ec8c9e217 100644 --- a/plugins/Meteor/MeteorPlugin.php +++ b/plugins/Meteor/MeteorPlugin.php @@ -50,6 +50,7 @@ class MeteorPlugin extends RealtimePlugin public $controlport = null; public $controlserver = null; public $channelbase = null; + public $persistent = true; protected $_socket = null; function __construct($webserver=null, $webport=4670, $controlport=4671, $controlserver=null, $channelbase='') @@ -65,6 +66,26 @@ class MeteorPlugin extends RealtimePlugin parent::__construct(); } + /** + * Pull settings from config file/database if set. + */ + function initialize() + { + $settings = array('webserver', + 'webport', + 'controlport', + 'controlserver', + 'channelbase'); + foreach ($settings as $name) { + $val = common_config('meteor', $name); + if ($val !== false) { + $this->$name = $val; + } + } + + return parent::initialize(); + } + function _getScripts() { $scripts = parent::_getScripts(); @@ -82,8 +103,14 @@ class MeteorPlugin extends RealtimePlugin function _connect() { $controlserver = (empty($this->controlserver)) ? $this->webserver : $this->controlserver; + + $errno = $errstr = null; + $timeout = 5; + $flags = STREAM_CLIENT_CONNECT; + if ($this->persistent) $flags |= STREAM_CLIENT_PERSISTENT; + // May throw an exception. - $this->_socket = stream_socket_client("tcp://{$controlserver}:{$this->controlport}"); + $this->_socket = stream_socket_client("tcp://{$controlserver}:{$this->controlport}", $errno, $errstr, $timeout, $flags); if (!$this->_socket) { throw new Exception("Couldn't connect to {$controlserver} on {$this->controlport}"); } @@ -104,8 +131,10 @@ class MeteorPlugin extends RealtimePlugin function _disconnect() { - $cnt = fwrite($this->_socket, "QUIT\n"); - @fclose($this->_socket); + if (!$this->persistent) { + $cnt = fwrite($this->_socket, "QUIT\n"); + @fclose($this->_socket); + } } // Meteord flips out with default '/' separator diff --git a/plugins/Minify/MinifyPlugin.php b/plugins/Minify/MinifyPlugin.php index 69def6064..13010e75a 100644 --- a/plugins/Minify/MinifyPlugin.php +++ b/plugins/Minify/MinifyPlugin.php @@ -29,6 +29,7 @@ Author URI: http://candrews.integralblue.com/ /** * @package MinifyPlugin * @maintainer Craig Andrews <candrews@integralblue.com> + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org */ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } diff --git a/plugins/Minify/locale/Minify.pot b/plugins/Minify/locale/Minify.pot new file mode 100644 index 000000000..6f7372d40 --- /dev/null +++ b/plugins/Minify/locale/Minify.pot @@ -0,0 +1,23 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: MinifyPlugin.php:179 +msgid "" +"The Minify plugin minifies your CSS and Javascript, removing whitespace and " +"comments." +msgstr "" diff --git a/plugins/MobileProfile/MobileProfilePlugin.php b/plugins/MobileProfile/MobileProfilePlugin.php index 0b37734b7..6076bbde0 100644 --- a/plugins/MobileProfile/MobileProfilePlugin.php +++ b/plugins/MobileProfile/MobileProfilePlugin.php @@ -73,9 +73,11 @@ class MobileProfilePlugin extends WAP20Plugin $this->serveMobile = true; } else { // If they like the WAP 2.0 mimetype, serve them MP - if (strstr('application/vnd.wap.xhtml+xml', $type) !== false) { - $this->serveMobile = true; - } else { + // @fixme $type is undefined, making this if case useless and spewing errors. + // What's the intent? + //if (strstr('application/vnd.wap.xhtml+xml', $type) !== false) { + // $this->serveMobile = true; + //} else { // If they are a mobile device that supports WAP 2.0, // serve them MP @@ -136,11 +138,23 @@ class MobileProfilePlugin extends WAP20Plugin 'vodafone', 'wap1', 'wap2', + 'webos', 'windows ce' ); + $blacklist = array( + 'ipad', // Larger screen handles the full theme fairly well. + ); + $httpuseragent = strtolower($_SERVER['HTTP_USER_AGENT']); + foreach ($blacklist as $md) { + if (strstr($httpuseragent, $md) !== false) { + $this->serveMobile = false; + return true; + } + } + foreach ($this->mobiledevices as $md) { if (strstr($httpuseragent, $md) !== false) { $this->setMobileFeatures($httpuseragent); @@ -149,7 +163,7 @@ class MobileProfilePlugin extends WAP20Plugin break; } } - } + //} // If they are okay with MP, and the site has a mobile server, // redirect there @@ -167,7 +181,9 @@ class MobileProfilePlugin extends WAP20Plugin return true; } - if (!$type) { + // @fixme $type is undefined, making this if case useless and spewing errors. + // What's the intent? + //if (!$type) { $httpaccept = isset($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : null; @@ -180,7 +196,7 @@ class MobileProfilePlugin extends WAP20Plugin throw new ClientException(_('This page is not available in a '. 'media type you accept'), 406); } - } + //} header('Content-Type: '.$type); @@ -219,21 +235,6 @@ class MobileProfilePlugin extends WAP20Plugin } - function onStartShowHeadElements($action) - { - if (!$action->serveMobile) { - return true; - } - - $action->showTitle(); - $action->showShortcutIcon(); - $action->showStylesheets(); - $action->showFeeds(); - $action->showDescription(); - $action->extraHead(); - } - - function onStartShowStatusNetStyles($action) { if (!$this->serveMobile) { @@ -254,6 +255,10 @@ class MobileProfilePlugin extends WAP20Plugin $action->cssLink('plugins/MobileProfile/mp-handheld.css',null,'handheld'); } + // Allow other plugins to load their styles. + Event::handle('EndShowStatusNetStyles', array($action)); + Event::handle('EndShowLaconicaStyles', array($action)); + return false; } diff --git a/plugins/MobileProfile/locale/MobileProfile.pot b/plugins/MobileProfile/locale/MobileProfile.pot new file mode 100644 index 000000000..9495e975b --- /dev/null +++ b/plugins/MobileProfile/locale/MobileProfile.pot @@ -0,0 +1,21 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: MobileProfilePlugin.php:424 +msgid "XHTML MobileProfile output for supporting user agents." +msgstr "" diff --git a/plugins/MobileProfile/mp-screen.css b/plugins/MobileProfile/mp-screen.css index 0fc801612..1f70b5612 100644 --- a/plugins/MobileProfile/mp-screen.css +++ b/plugins/MobileProfile/mp-screen.css @@ -2,7 +2,7 @@ * * @package StatusNet * @author Sarven Capadisli <csarven@status.net> - * @copyright 2009 StatusNet, Inc. + * @copyright 2009-2010 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ @@ -195,10 +195,6 @@ width:43px; margin-right:1%; } -.notice-options form { -width:16px; -height:16px; -} .notice-options form.processing { background-image:none; } diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 58f373e45..6fef20d6f 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -28,6 +28,15 @@ set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/ext class FeedSubException extends Exception { + function __construct($msg=null) + { + $type = get_class($this); + if ($msg) { + parent::__construct("$type: $msg"); + } else { + parent::__construct($type); + } + } } class OStatusPlugin extends Plugin @@ -87,6 +96,8 @@ class OStatusPlugin extends Plugin // Outgoing from our internal PuSH hub $qm->connect('hubconf', 'HubConfQueueHandler'); + $qm->connect('hubprep', 'HubPrepQueueHandler'); + $qm->connect('hubout', 'HubOutQueueHandler'); // Outgoing Salmon replies (when we don't need a return value) @@ -102,7 +113,10 @@ class OStatusPlugin extends Plugin */ function onStartEnqueueNotice($notice, &$transports) { - $transports[] = 'ostatus'; + if ($notice->isLocal()) { + // put our transport first, in case there's any conflict (like OMB) + array_unshift($transports, 'ostatus'); + } return true; } @@ -153,6 +167,9 @@ class OStatusPlugin extends Plugin // Also, we'll add in the salmon link $salmon = common_local_url($salmonAction, array('id' => $id)); + $feed->addLink($salmon, array('rel' => Salmon::REL_SALMON)); + + // XXX: these are deprecated $feed->addLink($salmon, array('rel' => Salmon::NS_REPLIES)); $feed->addLink($salmon, array('rel' => Salmon::NS_MENTIONS)); } @@ -256,7 +273,7 @@ class OStatusPlugin extends Plugin $matches = array(); // Webfinger matches: @user@example.com - if (preg_match_all('!(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)!', + if (preg_match_all('!(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\-?\w+\.)*\w+(?:\w+\-\w+)*\.\w+)!', $text, $wmatches, PREG_OFFSET_CAPTURE)) { @@ -451,6 +468,7 @@ class OStatusPlugin extends Plugin return false; } } + return true; } /** @@ -471,6 +489,24 @@ class OStatusPlugin extends Plugin } /** + * Tell the FeedSub infrastructure whether we have any active OStatus + * usage for the feed; if not it'll be able to garbage-collect the + * feed subscription. + * + * @param FeedSub $feedsub + * @param integer $count in/out + * @return mixed hook return code + */ + function onFeedSubSubscriberCount($feedsub, &$count) + { + $oprofile = Ostatus_profile::staticGet('feeduri', $feedsub->uri); + if ($oprofile) { + $count += $oprofile->subscriberCount(); + } + return true; + } + + /** * When about to subscribe to a remote user, start a server-to-server * PuSH subscription if needed. If we can't establish that, abort. * @@ -948,22 +984,15 @@ class OStatusPlugin extends Plugin return false; } - /** - * Utility function to check if the given URL is a canonical user profile - * page, and if so return the ID number. - * - * @param string $url - * @return mixed int or false - */ - public static function localProfileFromUrl($url) + public function onStartProfileGetAtomFeed($profile, &$feed) { - $template = common_local_url('userbyid', array('id' => '31337')); - $template = preg_quote($template, '/'); - $template = str_replace('31337', '(\d+)', $template); - if (preg_match("/$template/", $url, $matches)) { - return intval($matches[1]); + $oprofile = Ostatus_profile::staticGet('profile_id', $profile->id); + + if (empty($oprofile)) { + return true; } + + $feed = $oprofile->feeduri; return false; } - } diff --git a/plugins/OStatus/README b/plugins/OStatus/README index 3a98b7b25..ea5dfc055 100644 --- a/plugins/OStatus/README +++ b/plugins/OStatus/README @@ -1,18 +1,42 @@ -Plugin to support importing updates from external RSS and Atom feeds into your timeline. +Plugin to support importing and exporting notices through Atom and RSS feeds. +The OStatus plugin concentrates on user-to-user cases for federating StatusNet +and similar social networking / microblogging / blogging sites, but includes +low-level feed subscription systems which are used by some other plugins. + +Uses PubSubHubbub for push feed updates; currently non-PuSH feeds cannot be +subscribed unless an external PuSH hub proxy is used. -Uses PubSubHubbub for push feed updates; currently non-PuSH feeds cannot be subscribed. Configuration options available: $config['ostatus']['hub'] (default internal hub) - Set to URL of an external PuSH hub to use it instead of our internal hub. + Set to URL of an external PuSH hub to use it instead of our internal hub + for sending outgoing updates in user and group feeds. $config['ostatus']['hub_retries'] (default 0) Number of times to retry a PuSH send to consumers if using internal hub +Settings controlling incoming feed subscription: + +$config['feedsub']['fallback_hub'] + To subscribe to feeds that don't have a hub, an external PuSH proxy hub + such as Superfeedr may be used. Any feed without a hub of its own will + be subscribed through the specified hub URL instead. If the external hub + has usage charges, be aware that there is no restriction placed to how + many feeds may be subscribed! + + $config['feedsub']['fallback_hub'] = 'https://superfeedr.com/hubbub'; + +$config['feedsub']['hub_user'] +$config['feedsub']['hub_password'] + If using the fallback hub mode, these settings may be used to provide + HTTP authentication credentials for contacting the hub. Default hubs + specified from feeds are assumed to not require + + For testing, shouldn't be used in production: $config['ostatus']['skip_signatures'] @@ -23,12 +47,11 @@ $config['feedsub']['nohub'] (default require hub) Allow low-level feed subscription setup for feeds without hubs. Not actually usable at this stage, OStatus will check for hubs too - and we have no polling backend. + and we have no polling backend. (The fallback hub option can be used + with a 3rd-party service to provide such polling.) Todo: -* fully functional l10n -* redo non-OStatus feed support -** rssCloud support? -** possibly a polling daemon to support non-PuSH feeds? +* better support for feeds that aren't natively oriented at social networking * make use of tags/categories from feeds +* better repeat handling diff --git a/plugins/OStatus/actions/groupsalmon.php b/plugins/OStatus/actions/groupsalmon.php index 29377b5fa..d60725a71 100644 --- a/plugins/OStatus/actions/groupsalmon.php +++ b/plugins/OStatus/actions/groupsalmon.php @@ -60,7 +60,8 @@ class GroupsalmonAction extends SalmonAction function handlePost() { - switch ($this->act->object->type) { + // @fixme process all objects? + switch ($this->act->objects[0]->type) { case ActivityObject::ARTICLE: case ActivityObject::BLOGENTRY: case ActivityObject::NOTE: diff --git a/plugins/OStatus/actions/hostmeta.php b/plugins/OStatus/actions/hostmeta.php index 6d35ada6c..8ca07f916 100644 --- a/plugins/OStatus/actions/hostmeta.php +++ b/plugins/OStatus/actions/hostmeta.php @@ -36,13 +36,12 @@ class HostMetaAction extends Action $url.= '?uri={uri}'; $xrd = new XRD(); - - $xrd = new XRD(); $xrd->host = $domain; $xrd->links[] = array('rel' => Discovery::LRDD_REL, 'template' => $url, 'title' => array('Resource Descriptor')); + header('Content-type: application/xrd+xml'); print $xrd->toXML(); } } diff --git a/plugins/OStatus/actions/ostatusgroup.php b/plugins/OStatus/actions/ostatusgroup.php index f325ba053..1b368de63 100644 --- a/plugins/OStatus/actions/ostatusgroup.php +++ b/plugins/OStatus/actions/ostatusgroup.php @@ -104,7 +104,7 @@ class OStatusGroupAction extends OStatusSubAction } $this->showEntity($group, - $group->getProfileUrl(), + $group->homeUrl(), $group->homepage_logo, $group->description); return $ok; diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php index 994af6e95..28714514f 100644 --- a/plugins/OStatus/actions/ostatussub.php +++ b/plugins/OStatus/actions/ostatussub.php @@ -446,4 +446,18 @@ class OStatusSubAction extends Action { return common_local_url('ostatussub'); } + + /** + * Disable the send-notice form at the top of the page. + * This is really just a hack for the broken CSS in the Cloudy theme, + * I think; copying from other non-notice-navigation pages that do this + * as well. There will be plenty of others also broken. + * + * @fixme fix the cloudy theme + * @fixme do this in a more general way + */ + function showNoticeForm() { + // nop + } + } diff --git a/plugins/OStatus/actions/usersalmon.php b/plugins/OStatus/actions/usersalmon.php index 15e8c1869..6c360c49f 100644 --- a/plugins/OStatus/actions/usersalmon.php +++ b/plugins/OStatus/actions/usersalmon.php @@ -55,9 +55,10 @@ class UsersalmonAction extends SalmonAction */ function handlePost() { - common_log(LOG_INFO, "Received post of '{$this->act->object->id}' from '{$this->act->actor->id}'"); + common_log(LOG_INFO, "Received post of '{$this->act->objects[0]->id}' from '{$this->act->actor->id}'"); - switch ($this->act->object->type) { + // @fixme: process all activity objects? + switch ($this->act->objects[0]->type) { case ActivityObject::ARTICLE: case ActivityObject::BLOGENTRY: case ActivityObject::NOTE: @@ -91,7 +92,7 @@ class UsersalmonAction extends SalmonAction throw new ClientException("Not to anyone in reply to anything!"); } - $existing = Notice::staticGet('uri', $this->act->object->id); + $existing = Notice::staticGet('uri', $this->act->objects[0]->id); if (!empty($existing)) { common_log(LOG_ERR, "Not saving notice '{$existing->uri}'; already exists."); @@ -142,7 +143,7 @@ class UsersalmonAction extends SalmonAction function handleFavorite() { - $notice = $this->getNotice($this->act->object); + $notice = $this->getNotice($this->act->objects[0]); $profile = $this->ensureProfile()->localProfile(); $old = Fave::pkeyGet(array('user_id' => $profile->id, @@ -163,7 +164,7 @@ class UsersalmonAction extends SalmonAction */ function handleUnfavorite() { - $notice = $this->getNotice($this->act->object); + $notice = $this->getNotice($this->act->objects[0]); $profile = $this->ensureProfile()->localProfile(); $fave = Fave::pkeyGet(array('user_id' => $profile->id, diff --git a/plugins/OStatus/actions/userxrd.php b/plugins/OStatus/actions/userxrd.php index eb80a5ad4..6a6886eb8 100644 --- a/plugins/OStatus/actions/userxrd.php +++ b/plugins/OStatus/actions/userxrd.php @@ -35,9 +35,13 @@ class UserxrdAction extends XrdAction $this->uri = Discovery::normalize($this->uri); if (Discovery::isWebfinger($this->uri)) { - list($nick, $domain) = explode('@', substr(urldecode($this->uri), 5)); - $nick = common_canonical_nickname($nick); - $this->user = User::staticGet('nickname', $nick); + $parts = explode('@', substr(urldecode($this->uri), 5)); + if (count($parts) == 2) { + list($nick, $domain) = $parts; + // @fixme confirm the domain too + $nick = common_canonical_nickname($nick); + $this->user = User::staticGet('nickname', $nick); + } } else { $this->user = User::staticGet('uri', $this->uri); } diff --git a/plugins/OStatus/classes/FeedSub.php b/plugins/OStatus/classes/FeedSub.php index b10509dae..dd1968db1 100644 --- a/plugins/OStatus/classes/FeedSub.php +++ b/plugins/OStatus/classes/FeedSub.php @@ -207,8 +207,8 @@ class FeedSub extends Memcached_DataObject $discover = new FeedDiscovery(); $discover->discoverFromFeedURL($feeduri); - $huburi = $discover->getAtomLink('hub'); - if (!$huburi) { + $huburi = $discover->getHubLink(); + if (!$huburi && !common_config('feedsub', 'fallback_hub')) { throw new FeedSubNoHubException(); } @@ -241,8 +241,12 @@ class FeedSub extends Memcached_DataObject common_log(LOG_WARNING, "Attempting to (re)start PuSH subscription to $this->uri in unexpected state $this->sub_state"); } if (empty($this->huburi)) { - if (common_config('feedsub', 'nohub')) { + if (common_config('feedsub', 'fallback_hub')) { + // No native hub on this feed? + // Use our fallback hub, which handles polling on our behalf. + } else if (common_config('feedsub', 'nohub')) { // Fake it! We're just testing remote feeds w/o hubs. + // We'll never actually get updates in this mode. return true; } else { throw new ServerException("Attempting to start PuSH subscription for feed with no hub"); @@ -255,6 +259,9 @@ class FeedSub extends Memcached_DataObject /** * Send a PuSH unsubscription request to the hub for this feed. * The hub will later send us a confirmation POST to /main/push/callback. + * Warning: this will cancel the subscription even if someone else in + * the system is using it. Most callers will want garbageCollect() instead, + * which confirms there's no uses left. * * @return bool true on success, false on failure * @throws ServerException if feed state is not valid @@ -264,8 +271,12 @@ class FeedSub extends Memcached_DataObject common_log(LOG_WARNING, "Attempting to (re)end PuSH subscription to $this->uri in unexpected state $this->sub_state"); } if (empty($this->huburi)) { - if (common_config('feedsub', 'nohub')) { + if (common_config('feedsub', 'fallback_hub')) { + // No native hub on this feed? + // Use our fallback hub, which handles polling on our behalf. + } else if (common_config('feedsub', 'nohub')) { // Fake it! We're just testing remote feeds w/o hubs. + // We'll never actually get updates in this mode. return true; } else { throw new ServerException("Attempting to end PuSH subscription for feed with no hub"); @@ -275,6 +286,33 @@ class FeedSub extends Memcached_DataObject return $this->doSubscribe('unsubscribe'); } + /** + * Check if there are any active local uses of this feed, and if not then + * make sure it's inactive, unsubscribing if necessary. + * + * @return boolean true if the subscription is now inactive, false if still active. + */ + public function garbageCollect() + { + if ($this->sub_state == '' || $this->sub_state == 'inactive') { + // No active PuSH subscription, we can just leave it be. + return true; + } else { + // PuSH subscription is either active or in an indeterminate state. + // Check if we're out of subscribers, and if so send an unsubscribe. + $count = 0; + Event::handle('FeedSubSubscriberCount', array($this, &$count)); + + if ($count) { + common_log(LOG_INFO, __METHOD__ . ': ok, ' . $count . ' user(s) left for ' . $this->uri); + return false; + } else { + common_log(LOG_INFO, __METHOD__ . ': unsubscribing, no users left for ' . $this->uri); + return $this->unsubscribe(); + } + } + } + protected function doSubscribe($mode) { $orig = clone($this); @@ -296,7 +334,21 @@ class FeedSub extends Memcached_DataObject 'hub.secret' => $this->secret, 'hub.topic' => $this->uri); $client = new HTTPClient(); - $response = $client->post($this->huburi, $headers, $post); + if ($this->huburi) { + $hub = $this->huburi; + } else { + if (common_config('feedsub', 'fallback_hub')) { + $hub = common_config('feedsub', 'fallback_hub'); + if (common_config('feedsub', 'hub_user')) { + $u = common_config('feedsub', 'hub_user'); + $p = common_config('feedsub', 'hub_pass'); + $client->setAuth($u, $p); + } + } else { + throw new FeedSubException('WTF?'); + } + } + $response = $client->post($hub, $headers, $post); $status = $response->getStatus(); if ($status == 202) { common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback'); diff --git a/plugins/OStatus/classes/HubSub.php b/plugins/OStatus/classes/HubSub.php index cdace3c1f..7db528a4e 100644 --- a/plugins/OStatus/classes/HubSub.php +++ b/plugins/OStatus/classes/HubSub.php @@ -260,6 +260,37 @@ class HubSub extends Memcached_DataObject $retries = intval(common_config('ostatus', 'hub_retries')); } + if (common_config('ostatus', 'local_push_bypass')) { + // If target is a local site, bypass the web server and drop the + // item directly into the target's input queue. + $url = parse_url($this->callback); + $wildcard = common_config('ostatus', 'local_wildcard'); + $site = Status_network::getFromHostname($url['host'], $wildcard); + + if ($site) { + if ($this->secret) { + $hmac = 'sha1=' . hash_hmac('sha1', $atom, $this->secret); + } else { + $hmac = ''; + } + + // Hack: at the moment we stick the subscription ID in the callback + // URL so we don't have to look inside the Atom to route the subscription. + // For now this means we need to extract that from the target URL + // so we can include it in the data. + $parts = explode('/', $url['path']); + $subId = intval(array_pop($parts)); + + $data = array('feedsub_id' => $subId, + 'post' => $atom, + 'hmac' => $hmac); + common_log(LOG_DEBUG, "Cross-site PuSH bypass enqueueing straight to $site->nickname feed $subId"); + $qm = QueueManager::get(); + $qm->enqueue($data, 'pushin', $site->nickname); + return; + } + } + // We dare not clone() as when the clone is discarded it'll // destroy the result data for the parent query. // @fixme use clone() again when it's safe to copy an @@ -274,6 +305,26 @@ class HubSub extends Memcached_DataObject } /** + * Queue up a large batch of pushes to multiple subscribers + * for this same topic update. + * + * If queues are disabled, this will run immediately. + * + * @param string $atom well-formed Atom feed + * @param array $pushCallbacks list of callback URLs + */ + function bulkDistribute($atom, $pushCallbacks) + { + $data = array('atom' => $atom, + 'topic' => $this->topic, + 'pushCallbacks' => $pushCallbacks); + common_log(LOG_INFO, "Queuing PuSH batch: $this->topic to " . + count($pushCallbacks) . " sites"); + $qm = QueueManager::get(); + $qm->enqueue($data, 'hubprep'); + } + + /** * Send a 'fat ping' to the subscriber's callback endpoint * containing the given Atom feed chunk. * diff --git a/plugins/OStatus/classes/Magicsig.php b/plugins/OStatus/classes/Magicsig.php index 5705ecc11..f8c56a05f 100644 --- a/plugins/OStatus/classes/Magicsig.php +++ b/plugins/OStatus/classes/Magicsig.php @@ -52,7 +52,15 @@ class Magicsig extends Memcached_DataObject { $obj = parent::staticGet(__CLASS__, $k, $v); if (!empty($obj)) { - return Magicsig::fromString($obj->keypair); + $obj = Magicsig::fromString($obj->keypair); + + // Double check keys: Crypt_RSA did not + // consistently generate good keypairs. + // We've also moved to 1024 bit keys. + if (strlen($obj->publicKey->modulus->toBits()) != 1024) { + $obj->delete(); + return false; + } } return $obj; @@ -121,11 +129,11 @@ class Magicsig extends Memcached_DataObject public function toString($full_pair = true) { - $mod = base64_url_encode($this->publicKey->modulus->toBytes()); - $exp = base64_url_encode($this->publicKey->exponent->toBytes()); + $mod = Magicsig::base64_url_encode($this->publicKey->modulus->toBytes()); + $exp = Magicsig::base64_url_encode($this->publicKey->exponent->toBytes()); $private_exp = ''; if ($full_pair && $this->privateKey->exponent->toBytes()) { - $private_exp = '.' . base64_url_encode($this->privateKey->exponent->toBytes()); + $private_exp = '.' . Magicsig::base64_url_encode($this->privateKey->exponent->toBytes()); } return 'RSA.' . $mod . '.' . $exp . $private_exp; @@ -166,9 +174,9 @@ class Magicsig extends Memcached_DataObject $rsa = new Crypt_RSA(); $rsa->signatureMode = CRYPT_RSA_SIGNATURE_PKCS1; $rsa->setHash('sha256'); - $rsa->modulus = new Math_BigInteger(base64_url_decode($mod), 256); + $rsa->modulus = new Math_BigInteger(Magicsig::base64_url_decode($mod), 256); $rsa->k = strlen($rsa->modulus->toBytes()); - $rsa->exponent = new Math_BigInteger(base64_url_decode($exp), 256); + $rsa->exponent = new Math_BigInteger(Magicsig::base64_url_decode($exp), 256); if ($type == 'private') { $this->privateKey = $rsa; @@ -195,23 +203,25 @@ class Magicsig extends Memcached_DataObject public function sign($bytes) { $sig = $this->privateKey->sign($bytes); - return base64_url_encode($sig); + return Magicsig::base64_url_encode($sig); } public function verify($signed_bytes, $signature) { - $signature = base64_url_decode($signature); + $signature = Magicsig::base64_url_decode($signature); return $this->publicKey->verify($signed_bytes, $signature); } - -} -function base64_url_encode($input) -{ - return strtr(base64_encode($input), '+/', '-_'); -} -function base64_url_decode($input) -{ - return base64_decode(strtr($input, '-_', '+/')); + public static function base64_url_encode($input) + { + return strtr(base64_encode($input), '+/', '-_'); + } + + public static function base64_url_decode($input) + { + return base64_decode(strtr($input, '-_', '+/')); + } } + + diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index e48ed6ee8..8f8eb773f 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -215,22 +215,13 @@ class Ostatus_profile extends Memcached_DataObject } /** - * Send a PuSH unsubscription request to the hub for this feed. - * The hub will later send us a confirmation POST to /main/push/callback. + * Check if this remote profile has any active local subscriptions, and + * if not drop the PuSH subscription feed. * * @return bool true on success, false on failure - * @throws ServerException if feed state is not valid */ public function unsubscribe() { - $feedsub = FeedSub::staticGet('uri', $this->feeduri); - if (!$feedsub || $feedsub->sub_state == '' || $feedsub->sub_state == 'inactive') { - // No active PuSH subscription, we can just leave it be. - return true; - } else { - // PuSH subscription is either active or in an indeterminate state. - // Send an unsubscribe. - return $feedsub->unsubscribe(); - } + $this->garbageCollect(); } /** @@ -241,19 +232,35 @@ class Ostatus_profile extends Memcached_DataObject */ public function garbageCollect() { + $feedsub = FeedSub::staticGet('uri', $this->feeduri); + return $feedsub->garbageCollect(); + } + + /** + * Check if this remote profile has any active local subscriptions, so the + * PuSH subscription layer can decide if it can drop the feed. + * + * This gets called via the FeedSubSubscriberCount event when running + * FeedSub::garbageCollect(). + * + * @return int + */ + public function subscriberCount() + { if ($this->isGroup()) { $members = $this->localGroup()->getMembers(0, 1); $count = $members->N; } else { $count = $this->localProfile()->subscriberCount(); } - if ($count == 0) { - common_log(LOG_INFO, "Unsubscribing from now-unused remote feed $this->feeduri"); - $this->unsubscribe(); - return true; - } else { - return false; - } + common_log(LOG_INFO, __METHOD__ . " SUB COUNT BEFORE: $count"); + + // Other plugins may be piggybacking on OStatus without having + // an active group or user-to-user subscription we know about. + Event::handle('Ostatus_profileSubscriberCount', array($this, &$count)); + common_log(LOG_INFO, __METHOD__ . " SUB COUNT AFTER: $count"); + + return $count; } /** @@ -442,6 +449,18 @@ class Ostatus_profile extends Memcached_DataObject { $activity = new Activity($entry, $feed); + // @todo process all activity objects + switch ($activity->objects[0]->type) { + case ActivityObject::ARTICLE: + case ActivityObject::BLOGENTRY: + case ActivityObject::NOTE: + case ActivityObject::STATUS: + case ActivityObject::COMMENT: + break; + default: + throw new ClientException("Can't handle that kind of post."); + } + if ($activity->verb == ActivityVerb::POST) { $this->processPost($activity, $source); } else { @@ -474,8 +493,14 @@ class Ostatus_profile extends Memcached_DataObject // OK here! assume the default } else if ($actor->id == $this->uri || $actor->link == $this->uri) { $this->updateFromActivityObject($actor); - } else { + } else if ($actor->id) { + // We have an ActivityStreams actor with an explicit ID that doesn't match the feed owner. + // This isn't what we expect from mainline OStatus person feeds! + // Group feeds go down another path, with different validation. throw new Exception("Got an actor '{$actor->title}' ({$actor->id}) on single-user feed for {$this->uri}"); + } else { + // Plain <author> without ActivityStreams actor info. + // We'll just ignore this info for now and save the update under the feed's identity. } $oprofile = $this; @@ -483,7 +508,7 @@ class Ostatus_profile extends Memcached_DataObject // It's not always an ActivityObject::NOTE, but... let's just say it is. - $note = $activity->object; + $note = $activity->objects[0]; // The id URI will be used as a unique identifier for for the notice, // protecting against duplicate saves. It isn't required to be a URL; @@ -538,14 +563,22 @@ class Ostatus_profile extends Memcached_DataObject } $shortSummary = common_shorten_links($summary); if (Notice::contentTooLong($shortSummary)) { - $url = common_shorten_url(common_local_url('attachment', - array('attachment' => $attachment->id))); + $url = common_shorten_url($sourceUrl); $shortSummary = substr($shortSummary, 0, Notice::maxContent() - (mb_strlen($url) + 2)); - $shortSummary .= '… ' . $url; - $content = $shortSummary; - $rendered = common_render_text($content); + $content = $shortSummary . ' ' . $url; + + // We mark up the attachment link specially for the HTML output + // so we can fold-out the full version inline. + $attachUrl = common_local_url('attachment', + array('attachment' => $attachment->id)); + $rendered = common_render_text($shortSummary) . + '<a href="' . htmlspecialchars($attachUrl) .'"'. + ' class="attachment more"' . + ' title="'. htmlspecialchars(_m('Show more')) . '">' . + '…' . + '</a>'; } } @@ -648,7 +681,7 @@ class Ostatus_profile extends Memcached_DataObject common_log(LOG_DEBUG, "Original reply recipients: " . implode(', ', $attention_uris)); $groups = array(); $replies = array(); - foreach ($attention_uris as $recipient) { + foreach (array_unique($attention_uris) as $recipient) { // Is the recipient a local user? $user = User::staticGet('uri', $recipient); if ($user) { @@ -700,9 +733,14 @@ class Ostatus_profile extends Memcached_DataObject } /** + * Look up and if necessary create an Ostatus_profile for the remote entity + * with the given profile page URL. This should never return null -- you + * will either get an object or an exception will be thrown. + * * @param string $profile_url * @return Ostatus_profile - * @throws FeedSubException + * @throws Exception on various error conditions + * @throws OStatusShadowException if this reference would obscure a local user/group */ public static function ensureProfileURL($profile_url, $hints=array()) @@ -723,7 +761,7 @@ class Ostatus_profile extends Memcached_DataObject $response = $client->get($profile_url); if (!$response->isOk()) { - return null; + throw new Exception("Could not reach profile page: " . $profile_url); } // Check if we have a non-canonical URL @@ -777,11 +815,20 @@ class Ostatus_profile extends Memcached_DataObject if (!empty($feedurl)) { $hints['feedurl'] = $feedurl; - return self::ensureFeedURL($feedurl, $hints); } + + throw new Exception("Could not find a feed URL for profile page " . $finalUrl); } + /** + * Look up the Ostatus_profile, if present, for a remote entity with the + * given profile page URL. Will return null for both unknown and invalid + * remote profiles. + * + * @return mixed Ostatus_profile or null + * @throws OStatusShadowException for local profiles + */ static function getFromProfileURL($profile_url) { $profile = Profile::staticGet('profileurl', $profile_url); @@ -803,7 +850,7 @@ class Ostatus_profile extends Memcached_DataObject $user = User::staticGet('id', $profile->id); if (!empty($user)) { - throw new Exception("'$profile_url' is the profile for local user '{$user->nickname}'."); + throw new OStatusShadowException($profile, "'$profile_url' is the profile for local user '{$user->nickname}'."); } // Continue discovery; it's a remote profile @@ -813,6 +860,14 @@ class Ostatus_profile extends Memcached_DataObject return null; } + /** + * Look up and if necessary create an Ostatus_profile for remote entity + * with the given update feed. This should never return null -- you will + * either get an object or an exception will be thrown. + * + * @return Ostatus_profile + * @throws Exception + */ public static function ensureFeedURL($feed_url, $hints=array()) { $discover = new FeedDiscovery(); @@ -820,12 +875,12 @@ class Ostatus_profile extends Memcached_DataObject $feeduri = $discover->discoverFromFeedURL($feed_url); $hints['feedurl'] = $feeduri; - $huburi = $discover->getAtomLink('hub'); + $huburi = $discover->getHubLink(); $hints['hub'] = $huburi; $salmonuri = $discover->getAtomLink(Salmon::NS_REPLIES); $hints['salmon'] = $salmonuri; - if (!$huburi) { + if (!$huburi && !common_config('feedsub', 'fallback_hub')) { // We can only deal with folks with a PuSH hub throw new FeedSubNoHubException(); } @@ -841,6 +896,18 @@ class Ostatus_profile extends Memcached_DataObject } } + /** + * Look up and, if necessary, create an Ostatus_profile for the remote + * profile with the given Atom feed - actually loaded from the feed. + * This should never return null -- you will either get an object or + * an exception will be thrown. + * + * @param DOMElement $feedEl root element of a loaded Atom feed + * @param array $hints additional discovery information passed from higher levels + * @fixme should this be marked public? + * @return Ostatus_profile + * @throws Exception + */ public static function ensureAtomFeed($feedEl, $hints) { // Try to get a profile from the feed activity:subject @@ -891,6 +958,18 @@ class Ostatus_profile extends Memcached_DataObject throw new FeedSubException("Can't find enough profile information to make a feed."); } + /** + * Look up and, if necessary, create an Ostatus_profile for the remote + * profile with the given RSS feed - actually loaded from the feed. + * This should never return null -- you will either get an object or + * an exception will be thrown. + * + * @param DOMElement $feedEl root element of a loaded RSS feed + * @param array $hints additional discovery information passed from higher levels + * @fixme should this be marked public? + * @return Ostatus_profile + * @throws Exception + */ public static function ensureRssChannel($feedEl, $hints) { // Special-case for Posterous. They have some nice metadata in their @@ -935,7 +1014,7 @@ class Ostatus_profile extends Memcached_DataObject return; } if (!common_valid_http_url($url)) { - throw new ServerException(_m("Invalid avatar URL %s"), $url); + throw new ServerException(sprintf(_m("Invalid avatar URL %s"), $url)); } if ($this->isGroup()) { @@ -1054,11 +1133,14 @@ class Ostatus_profile extends Memcached_DataObject /** * Fetch, or build if necessary, an Ostatus_profile for the actor * in a given Activity Streams activity. + * This should never return null -- you will either get an object or + * an exception will be thrown. * * @param Activity $activity * @param string $feeduri if we already know the canonical feed URI! * @param string $salmonuri if we already know the salmon return channel URI * @return Ostatus_profile + * @throws Exception */ public static function ensureActorProfile($activity, $hints=array()) @@ -1066,6 +1148,18 @@ class Ostatus_profile extends Memcached_DataObject return self::ensureActivityObjectProfile($activity->actor, $hints); } + /** + * Fetch, or build if necessary, an Ostatus_profile for the profile + * in a given Activity Streams object (can be subject, actor, or object). + * This should never return null -- you will either get an object or + * an exception will be thrown. + * + * @param ActivityObject $object + * @param array $hints additional discovery information passed from higher levels + * @return Ostatus_profile + * @throws Exception + */ + public static function ensureActivityObjectProfile($object, $hints=array()) { $profile = self::getActivityObjectProfile($object); @@ -1080,35 +1174,45 @@ class Ostatus_profile extends Memcached_DataObject /** * @param Activity $activity * @return mixed matching Ostatus_profile or false if none known + * @throws ServerException if feed info invalid */ public static function getActorProfile($activity) { return self::getActivityObjectProfile($activity->actor); } + /** + * @param ActivityObject $activity + * @return mixed matching Ostatus_profile or false if none known + * @throws ServerException if feed info invalid + */ protected static function getActivityObjectProfile($object) { $uri = self::getActivityObjectProfileURI($object); return Ostatus_profile::staticGet('uri', $uri); } - protected static function getActorProfileURI($activity) - { - return self::getActivityObjectProfileURI($activity->actor); - } - /** - * @param Activity $activity + * Get the identifier URI for the remote entity described + * by this ActivityObject. This URI is *not* guaranteed to be + * a resolvable HTTP/HTTPS URL. + * + * @param ActivityObject $object * @return string - * @throws ServerException + * @throws ServerException if feed info invalid */ protected static function getActivityObjectProfileURI($object) { - $opts = array('allowed_schemes' => array('http', 'https')); - if ($object->id && Validate::uri($object->id, $opts)) { - return $object->id; + if ($object->id) { + if (ActivityUtils::validateUri($object->id)) { + return $object->id; + } } - if ($object->link && Validate::uri($object->link, $opts)) { + + // If the id is missing or invalid (we've seen feeds mistakenly listing + // things like local usernames in that field) then we'll use the profile + // page link, if valid. + if ($object->link && common_valid_http_url($object->link)) { return $object->link; } throw new ServerException("No author ID URI found"); @@ -1121,6 +1225,8 @@ class Ostatus_profile extends Memcached_DataObject /** * Create local ostatus_profile and profile/user_group entries for * the provided remote user or group. + * This should never return null -- you will either get an object or + * an exception will be thrown. * * @param ActivityObject $object * @param array $hints @@ -1137,7 +1243,8 @@ class Ostatus_profile extends Memcached_DataObject throw new Exception("No profile URI"); } - if (OStatusPlugin::localProfileFromUrl($homeuri)) { + $user = User::staticGet('uri', $homeuri); + if ($user) { throw new Exception("Local user can't be referenced as remote."); } @@ -1169,10 +1276,10 @@ class Ostatus_profile extends Memcached_DataObject $discover = new FeedDiscovery(); $discover->discoverFromFeedURL($hints['feedurl']); } - $huburi = $discover->getAtomLink('hub'); + $huburi = $discover->getHubLink(); } - if (!$huburi) { + if (!$huburi && !common_config('feedsub', 'fallback_hub')) { // We can only deal with folks with a PuSH hub throw new FeedSubNoHubException(); } @@ -1209,15 +1316,23 @@ class Ostatus_profile extends Memcached_DataObject $ok = $oprofile->insert(); - if ($ok) { - $avatar = self::getActivityObjectAvatar($object, $hints); - if ($avatar) { + if (!$ok) { + throw new ServerException("Can't save OStatus profile"); + } + + $avatar = self::getActivityObjectAvatar($object, $hints); + + if ($avatar) { + try { $oprofile->updateAvatar($avatar); + } catch (Exception $ex) { + // Profile is saved, but Avatar is messed up. We're + // just going to continue. + common_log(LOG_WARNING, "Exception saving OStatus profile avatar: ". $ex->getMessage()); } - return $oprofile; - } else { - throw new ServerException("Can't save OStatus profile"); } + + return $oprofile; } /** @@ -1236,7 +1351,11 @@ class Ostatus_profile extends Memcached_DataObject } $avatar = self::getActivityObjectAvatar($object, $hints); if ($avatar) { - $this->updateAvatar($avatar); + try { + $this->updateAvatar($avatar); + } catch (Exception $ex) { + common_log(LOG_WARNING, "Exception saving OStatus profile avatar: " . $ex->getMessage()); + } } } @@ -1437,9 +1556,15 @@ class Ostatus_profile extends Memcached_DataObject } /** + * Look up, and if necessary create, an Ostatus_profile for the remote + * entity with the given webfinger address. + * This should never return null -- you will either get an object or + * an exception will be thrown. + * * @param string $addr webfinger address * @return Ostatus_profile * @throws Exception on error conditions + * @throws OStatusShadowException if this reference would obscure a local user/group */ public static function ensureWebfinger($addr) { @@ -1518,9 +1643,18 @@ class Ostatus_profile extends Memcached_DataObject $oprofile = self::ensureProfileURL($hints['profileurl'], $hints); self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri); return $oprofile; + } catch (OStatusShadowException $e) { + // We've ended up with a remote reference to a local user or group. + // @fixme ideally we should be able to say who it was so we can + // go back and refer to it the regular way + throw $e; } catch (Exception $e) { common_log(LOG_WARNING, "Failed creating profile from profile URL '$profileUrl': " . $e->getMessage()); // keep looking + // + // @fixme this means an error discovering from profile page + // may give us a corrupt entry using the webfinger URI, which + // will obscure the correct page-keyed profile later on. } } @@ -1577,10 +1711,22 @@ class Ostatus_profile extends Memcached_DataObject throw new Exception("Couldn't find a valid profile for '$addr'"); } + /** + * Store the full-length scrubbed HTML of a remote notice to an attachment + * file on our server. We'll link to this at the end of the cropped version. + * + * @param string $title plaintext for HTML page's title + * @param string $rendered HTML fragment for HTML page's body + * @return File + */ function saveHTMLFile($title, $rendered) { - $final = sprintf("<!DOCTYPE html>\n<html><head><title>%s</title></head>". - '<body><div>%s</div></body></html>', + $final = sprintf("<!DOCTYPE html>\n" . + '<html><head>' . + '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">' . + '<title>%s</title>' . + '</head>' . + '<body>%s</body></html>', htmlspecialchars($title), $rendered); @@ -1610,3 +1756,24 @@ class Ostatus_profile extends Memcached_DataObject return $file; } } + +/** + * Exception indicating we've got a remote reference to a local user, + * not a remote user! + * + * If we can ue a local profile after all, it's available as $e->profile. + */ +class OStatusShadowException extends Exception +{ + public $profile; + + /** + * @param Profile $profile + * @param string $message + */ + function __construct($profile, $message) { + $this->profile = $profile; + parent::__construct($message); + } +} + diff --git a/plugins/OStatus/extlib/Math/BigInteger.php b/plugins/OStatus/extlib/Math/BigInteger.php index 9733351d4..4373805f9 100644 --- a/plugins/OStatus/extlib/Math/BigInteger.php +++ b/plugins/OStatus/extlib/Math/BigInteger.php @@ -67,7 +67,7 @@ * @author Jim Wigginton <terrafrost@php.net> * @copyright MMVI Jim Wigginton * @license http://www.gnu.org/licenses/lgpl.txt - * @version $Id: BigInteger.php,v 1.31 2010/03/01 17:28:19 terrafrost Exp $ + * @version $Id: BigInteger.php,v 1.33 2010/03/22 22:32:03 terrafrost Exp $ * @link http://pear.php.net/package/Math_BigInteger */ @@ -294,7 +294,7 @@ class Math_BigInteger { $this->value = array(); } - if ($x === 0) { + if (empty($x)) { return; } @@ -718,7 +718,7 @@ class Math_BigInteger { * * Will be called, automatically, when serialize() is called on a Math_BigInteger object. * - * @see __wakeup + * @see __wakeup() * @access public */ function __sleep() @@ -740,7 +740,7 @@ class Math_BigInteger { * * Will be called, automatically, when unserialize() is called on a Math_BigInteger object. * - * @see __sleep + * @see __sleep() * @access public */ function __wakeup() diff --git a/plugins/OStatus/lib/discovery.php b/plugins/OStatus/lib/discovery.php index 44fad62fb..7187c1f3e 100644 --- a/plugins/OStatus/lib/discovery.php +++ b/plugins/OStatus/lib/discovery.php @@ -195,7 +195,7 @@ class Discovery_LRDD_Link_Header implements Discovery_LRDD // return false; } - return Discovery_LRDD_Link_Header::parseHeader($link_header); + return array(Discovery_LRDD_Link_Header::parseHeader($link_header)); } protected static function parseHeader($header) diff --git a/plugins/OStatus/lib/discoveryhints.php b/plugins/OStatus/lib/discoveryhints.php index 80cfbbf15..34c9be277 100644 --- a/plugins/OStatus/lib/discoveryhints.php +++ b/plugins/OStatus/lib/discoveryhints.php @@ -30,6 +30,7 @@ class DiscoveryHints { case Discovery::PROFILEPAGE: $hints['profileurl'] = $link['href']; break; + case Salmon::NS_MENTIONS: case Salmon::NS_REPLIES: $hints['salmon'] = $link['href']; break; @@ -83,7 +84,7 @@ class DiscoveryHints { $hints['fullname'] = implode(' ', $hcard['n']); } - if (array_key_exists('photo', $hcard)) { + if (array_key_exists('photo', $hcard) && count($hcard['photo'])) { $hints['avatar'] = $hcard['photo'][0]; } diff --git a/plugins/OStatus/lib/feeddiscovery.php b/plugins/OStatus/lib/feeddiscovery.php index 4809f9d35..a55399d7c 100644 --- a/plugins/OStatus/lib/feeddiscovery.php +++ b/plugins/OStatus/lib/feeddiscovery.php @@ -88,6 +88,16 @@ class FeedDiscovery } /** + * Get the referenced PuSH hub link from an Atom feed. + * + * @return mixed string or false + */ + public function getHubLink() + { + return $this->getAtomLink('hub'); + } + + /** * @param string $url * @param bool $htmlOk pass false here if you don't want to follow web pages. * @return string with validated URL @@ -104,7 +114,7 @@ class FeedDiscovery $response = $client->get($url); } catch (HTTP_Request2_Exception $e) { common_log(LOG_ERR, __METHOD__ . " Failure for $url - " . $e->getMessage()); - throw new FeedSubBadURLException($e); + throw new FeedSubBadURLException($e->getMessage()); } if ($htmlOk) { diff --git a/plugins/OStatus/lib/hubprepqueuehandler.php b/plugins/OStatus/lib/hubprepqueuehandler.php new file mode 100644 index 000000000..0d585938f --- /dev/null +++ b/plugins/OStatus/lib/hubprepqueuehandler.php @@ -0,0 +1,87 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * When we have a large batch of PuSH consumers, we break the data set + * into smaller chunks. Enqueue final destinations... + * + * @package Hub + * @author Brion Vibber <brion@status.net> + */ +class HubPrepQueueHandler extends QueueHandler +{ + // Enqueue this many low-level distributions before re-queueing the rest + // of the batch to be processed later. Helps to keep latency down for other + // things happening during a particularly long OStatus delivery session. + // + // [Could probably ditch this if we had working message delivery priorities + // for queueing, but this isn't supported in ActiveMQ 5.3.] + const ROLLING_BATCH = 20; + + function transport() + { + return 'hubprep'; + } + + function handle($data) + { + $topic = $data['topic']; + $atom = $data['atom']; + $pushCallbacks = $data['pushCallbacks']; + + assert(is_string($atom)); + assert(is_string($topic)); + assert(is_array($pushCallbacks)); + + // Set up distribution for the first n subscribing sites... + // If we encounter an uncatchable error, queue handling should + // automatically re-run the batch, which could lead to some dupe + // distributions. + // + // Worst case is if one of these hubprep entries dies too many + // times and gets dropped; the rest of the batch won't get processed. + try { + $n = 0; + while (count($pushCallbacks) && $n < self::ROLLING_BATCH) { + $n++; + $callback = array_shift($pushCallbacks); + $sub = HubSub::staticGet($topic, $callback); + if (!$sub) { + common_log(LOG_ERR, "Skipping PuSH delivery for deleted(?) consumer $callback on $topic"); + continue; + } + + $sub->distribute($atom); + } + } catch (Exception $e) { + common_log(LOG_ERR, "Exception during PuSH batch out: " . + $e->getMessage() . + " prepping $topic to $callback"); + } + + // And re-queue the rest of the batch! + if (count($pushCallbacks) > 0) { + $sub = new HubSub(); + $sub->topic = $topic; + $sub->bulkDistribute($atom, $pushCallbacks); + } + + return true; + } +} diff --git a/plugins/OStatus/lib/linkheader.php b/plugins/OStatus/lib/linkheader.php index afcd66d26..cd78d31ce 100644 --- a/plugins/OStatus/lib/linkheader.php +++ b/plugins/OStatus/lib/linkheader.php @@ -11,7 +11,7 @@ class LinkHeader preg_match('/^<[^>]+>/', $str, $uri_reference); //if (empty($uri_reference)) return; - $this->uri = trim($uri_reference[0], '<>'); + $this->href = trim($uri_reference[0], '<>'); $this->rel = array(); $this->type = null; diff --git a/plugins/OStatus/lib/magicenvelope.php b/plugins/OStatus/lib/magicenvelope.php index 9266cab5c..967e5f6d1 100644 --- a/plugins/OStatus/lib/magicenvelope.php +++ b/plugins/OStatus/lib/magicenvelope.php @@ -59,12 +59,21 @@ class MagicEnvelope } if ($xrd->links) { if ($link = Discovery::getService($xrd->links, Magicsig::PUBLICKEYREL)) { - list($type, $keypair) = explode(',', $link['href']); - if (empty($keypair)) { + $keypair = false; + $parts = explode(',', $link['href']); + if (count($parts) == 2) { + $keypair = $parts[1]; + } else { // Backwards compatibility check for separator bug in 0.9.0 - list($type, $keypair) = explode(';', $link['href']); + $parts = explode(';', $link['href']); + if (count($parts) == 2) { + $keypair = $parts[1]; + } + } + + if ($keypair) { + return $keypair; } - return $keypair; } } throw new Exception('Unable to locate signer public key'); @@ -74,7 +83,7 @@ class MagicEnvelope public function signMessage($text, $mimetype, $keypair) { $signature_alg = Magicsig::fromString($keypair); - $armored_text = base64_url_encode($text); + $armored_text = Magicsig::base64_url_encode($text); return array( 'data' => $armored_text, @@ -88,31 +97,25 @@ class MagicEnvelope } public function toXML($env) { - $dom = new DOMDocument(); - - $envelope = $dom->createElementNS(MagicEnvelope::NS, 'me:env'); - $envelope->setAttribute('xmlns:me', MagicEnvelope::NS); - $data = $dom->createElementNS(MagicEnvelope::NS, 'me:data', $env['data']); - $data->setAttribute('type', $env['data_type']); - $envelope->appendChild($data); - $enc = $dom->createElementNS(MagicEnvelope::NS, 'me:encoding', $env['encoding']); - $envelope->appendChild($enc); - $alg = $dom->createElementNS(MagicEnvelope::NS, 'me:alg', $env['alg']); - $envelope->appendChild($alg); - $sig = $dom->createElementNS(MagicEnvelope::NS, 'me:sig', $env['sig']); - $envelope->appendChild($sig); - - $dom->appendChild($envelope); + $xs = new XMLStringer(); + $xs->startXML(); + $xs->elementStart('me:env', array('xmlns:me' => MagicEnvelope::NS)); + $xs->element('me:data', array('type' => $env['data_type']), $env['data']); + $xs->element('me:encoding', null, $env['encoding']); + $xs->element('me:alg', null, $env['alg']); + $xs->element('me:sig', null, $env['sig']); + $xs->elementEnd('me:env'); - - return $dom->saveXML(); + $string = $xs->getString(); + common_debug($string); + return $string; } public function unfold($env) { $dom = new DOMDocument(); - $dom->loadXML(base64_url_decode($env['data'])); + $dom->loadXML(Magicsig::base64_url_decode($env['data'])); if ($dom->documentElement->tagName != 'entry') { return false; @@ -169,7 +172,7 @@ class MagicEnvelope return false; } - $text = base64_url_decode($env['data']); + $text = Magicsig::base64_url_decode($env['data']); $signer_uri = $this->getAuthor($text); try { @@ -207,13 +210,13 @@ class MagicEnvelope } $data_element = $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'data')->item(0); - + $sig_element = $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'sig')->item(0); return array( - 'data' => trim($data_element->nodeValue), + 'data' => preg_replace('/\s/', '', $data_element->nodeValue), 'data_type' => $data_element->getAttribute('type'), 'encoding' => $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'encoding')->item(0)->nodeValue, 'alg' => $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'alg')->item(0)->nodeValue, - 'sig' => $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'sig')->item(0)->nodeValue, + 'sig' => preg_replace('/\s/', '', $sig_element->nodeValue), ); } diff --git a/plugins/OStatus/lib/ostatusqueuehandler.php b/plugins/OStatus/lib/ostatusqueuehandler.php index d1e58f1d6..8905d2e21 100644 --- a/plugins/OStatus/lib/ostatusqueuehandler.php +++ b/plugins/OStatus/lib/ostatusqueuehandler.php @@ -25,6 +25,18 @@ */ class OStatusQueueHandler extends QueueHandler { + // If we have more than this many subscribing sites on a single feed, + // break up the PuSH distribution into smaller batches which will be + // rolled into the queue progressively. This reduces disruption to + // other, shorter activities being enqueued while we work. + const MAX_UNBATCHED = 50; + + // Each batch (a 'hubprep' entry) will have this many items. + // Selected to provide a balance between queue packet size + // and number of batches that will end up getting processed. + // For 20,000 target sites, 1000 should work acceptably. + const BATCH_SIZE = 1000; + function transport() { return 'ostatus'; @@ -147,14 +159,31 @@ class OStatusQueueHandler extends QueueHandler /** * Queue up direct feed update pushes to subscribers on our internal hub. + * If there are a large number of subscriber sites, intermediate bulk + * distribution triggers may be queued. + * * @param string $atom update feed, containing only new/changed items * @param HubSub $sub open query of subscribers */ function pushFeedInternal($atom, $sub) { common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $sub->topic"); + $n = 0; + $batch = array(); while ($sub->fetch()) { - $sub->distribute($atom); + $n++; + if ($n < self::MAX_UNBATCHED) { + $sub->distribute($atom); + } else { + $batch[] = $sub->callback; + if (count($batch) >= self::BATCH_SIZE) { + $sub->bulkDistribute($atom, $batch); + $batch = array(); + } + } + } + if (count($batch) >= 0) { + $sub->bulkDistribute($atom, $batch); } } diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php index 3d3341bc6..ef7719a40 100644 --- a/plugins/OStatus/lib/salmon.php +++ b/plugins/OStatus/lib/salmon.php @@ -28,9 +28,11 @@ */ class Salmon { + const REL_SALMON = 'salmon'; + const REL_MENTIONED = 'mentioned'; + // XXX: these are deprecated const NS_REPLIES = "http://salmon-protocol.org/ns/salmon-replies"; - const NS_MENTIONS = "http://salmon-protocol.org/ns/salmon-mention"; /** diff --git a/plugins/OStatus/lib/xrd.php b/plugins/OStatus/lib/xrd.php index aa13ef024..a10b9f427 100644 --- a/plugins/OStatus/lib/xrd.php +++ b/plugins/OStatus/lib/xrd.php @@ -53,7 +53,14 @@ class XRD $xrd = new XRD(); $dom = new DOMDocument(); - if (!$dom->loadXML($xml)) { + + // Don't spew XML warnings to output + $old = error_reporting(); + error_reporting($old & ~E_WARNING); + $ok = $dom->loadXML($xml); + error_reporting($old); + + if (!$ok) { throw new Exception("Invalid XML"); } $xrd_element = $dom->getElementsByTagName('XRD')->item(0); @@ -99,44 +106,43 @@ class XRD public function toXML() { - $dom = new DOMDocument('1.0', 'UTF-8'); - $dom->formatOutput = true; - - $xrd_dom = $dom->createElementNS(XRD::XRD_NS, 'XRD'); - $dom->appendChild($xrd_dom); + $xs = new XMLStringer(); + + $xs->startXML(); + $xs->elementStart('XRD', array('xmlns' => XRD::XRD_NS)); if ($this->host) { - $host_dom = $dom->createElement('hm:Host', $this->host); - $xrd_dom->setAttributeNS(XRD::XML_NS, 'xmlns:hm', XRD::HOST_META_NS); - $xrd_dom->appendChild($host_dom); + $xs->element('hm:Host', array('xmlns:hm' => XRD::HOST_META_NS), $this->host); + } + + if ($this->expires) { + $xs->element('Expires', null, $this->expires); + } + + if ($this->subject) { + $xs->element('Subject', null, $this->subject); + } + + foreach ($this->alias as $alias) { + $xs->element('Alias', null, $alias); + } + + foreach ($this->links as $link) { + $titles = array(); + if (isset($link['title'])) { + $titles = $link['title']; + unset($link['title']); + } + $xs->elementStart('Link', $link); + foreach ($titles as $title) { + $xs->element('Title', null, $title); + } + $xs->elementEnd('Link'); } - if ($this->expires) { - $expires_dom = $dom->createElement('Expires', $this->expires); - $xrd_dom->appendChild($expires_dom); - } - - if ($this->subject) { - $subject_dom = $dom->createElement('Subject', $this->subject); - $xrd_dom->appendChild($subject_dom); - } - - foreach ($this->alias as $alias) { - $alias_dom = $dom->createElement('Alias', $alias); - $xrd_dom->appendChild($alias_dom); - } - - foreach ($this->types as $type) { - $type_dom = $dom->createElement('Type', $type); - $xrd_dom->appendChild($type_dom); - } - - foreach ($this->links as $link) { - $link_dom = $this->saveLink($dom, $link); - $xrd_dom->appendChild($link_dom); - } - - return $dom->saveXML(); + $xs->elementEnd('XRD'); + + return $xs->getString(); } function parseType($element) @@ -162,32 +168,5 @@ class XRD return $link; } - - function saveLink($doc, $link) - { - $link_element = $doc->createElement('Link'); - if (!empty($link['rel'])) { - $link_element->setAttribute('rel', $link['rel']); - } - if (!empty($link['type'])) { - $link_element->setAttribute('type', $link['type']); - } - if (!empty($link['href'])) { - $link_element->setAttribute('href', $link['href']); - } - if (!empty($link['template'])) { - $link_element->setAttribute('template', $link['template']); - } - - if (!empty($link['title']) && is_array($link['title'])) { - foreach($link['title'] as $title) { - $title = $doc->createElement('Title', $title); - $link_element->appendChild($title); - } - } - - - return $link_element; - } } diff --git a/plugins/OStatus/lib/xrdaction.php b/plugins/OStatus/lib/xrdaction.php index f1a56e0a8..d8cf648d6 100644 --- a/plugins/OStatus/lib/xrdaction.php +++ b/plugins/OStatus/lib/xrdaction.php @@ -76,6 +76,9 @@ class XrdAction extends Action $salmon_url = common_local_url('usersalmon', array('id' => $this->user->id)); + $xrd->links[] = array('rel' => Salmon::REL_SALMON, + 'href' => $salmon_url); + // XXX : Deprecated - to be removed. $xrd->links[] = array('rel' => Salmon::NS_REPLIES, 'href' => $salmon_url); @@ -98,7 +101,7 @@ class XrdAction extends Action $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe', 'template' => $url ); - header('Content-type: text/xml'); + header('Content-type: application/xrd+xml'); print $xrd->toXML(); } diff --git a/plugins/OStatus/locale/OStatus.po b/plugins/OStatus/locale/OStatus.pot index 7e33a0eed..97d593ead 100644 --- a/plugins/OStatus/locale/OStatus.po +++ b/plugins/OStatus/locale/OStatus.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-03-01 14:58-0800\n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -16,297 +16,316 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: actions/groupsalmon.php:51 -msgid "Can't accept remote posts for a remote group." -msgstr "" - -#: actions/groupsalmon.php:123 -msgid "Can't read profile to set up group membership." +#: OStatusPlugin.php:210 OStatusPlugin.php:913 actions/ostatusinit.php:99 +msgid "Subscribe" msgstr "" -#: actions/groupsalmon.php:126 actions/groupsalmon.php:169 -msgid "Groups can't join groups." +#: OStatusPlugin.php:228 OStatusPlugin.php:635 actions/ostatussub.php:105 +#: actions/ostatusinit.php:96 +msgid "Join" msgstr "" -#: actions/groupsalmon.php:153 +#: OStatusPlugin.php:451 #, php-format -msgid "Could not join remote user %1$s to group %2$s." +msgid "Sent from %s via OStatus" msgstr "" -#: actions/groupsalmon.php:166 -msgid "Can't read profile to cancel group membership." +#: OStatusPlugin.php:503 +msgid "Could not set up remote subscription." msgstr "" -#: actions/groupsalmon.php:182 -#, php-format -msgid "Could not remove remote user %1$s from group %2$s." +#: OStatusPlugin.php:619 +msgid "Could not set up remote group membership." msgstr "" -#: actions/ostatusinit.php:40 -msgid "You can use the local subscription!" +#: OStatusPlugin.php:636 +#, php-format +msgid "%s has joined group %s." msgstr "" -#: actions/ostatusinit.php:61 -msgid "There was a problem with your session token. Try again, please." +#: OStatusPlugin.php:644 +msgid "Failed joining remote group." msgstr "" -#: actions/ostatusinit.php:79 actions/ostatussub.php:439 -msgid "Subscribe to user" +#: OStatusPlugin.php:684 +msgid "Leave" msgstr "" -#: actions/ostatusinit.php:97 +#: OStatusPlugin.php:685 #, php-format -msgid "Subscribe to %s" +msgid "%s has left group %s." msgstr "" -#: actions/ostatusinit.php:102 -msgid "User nickname" +#: OStatusPlugin.php:844 +msgid "Remote" msgstr "" -#: actions/ostatusinit.php:103 -msgid "Nickname of the user you want to follow" +#: OStatusPlugin.php:883 +msgid "Profile update" msgstr "" -#: actions/ostatusinit.php:106 -msgid "Profile Account" +#: OStatusPlugin.php:884 +#, php-format +msgid "%s has updated their profile page." msgstr "" -#: actions/ostatusinit.php:107 -msgid "Your account id (i.e. user@identi.ca)" +#: OStatusPlugin.php:928 +msgid "" +"Follow people across social networks that implement <a href=\"http://ostatus." +"org/\">OStatus</a>." msgstr "" -#: actions/ostatusinit.php:110 actions/ostatussub.php:115 -#: OStatusPlugin.php:205 -msgid "Subscribe" +#: classes/Ostatus_profile.php:566 +msgid "Show more" msgstr "" -#: actions/ostatusinit.php:128 -msgid "Must provide a remote profile." +#: classes/Ostatus_profile.php:1004 +#, php-format +msgid "Invalid avatar URL %s" msgstr "" -#: actions/ostatusinit.php:138 -msgid "Couldn't look up OStatus account profile." +#: classes/Ostatus_profile.php:1014 +#, php-format +msgid "Tried to update avatar for unsaved remote profile %s" msgstr "" -#: actions/ostatusinit.php:153 -msgid "Couldn't confirm remote profile address." +#: classes/Ostatus_profile.php:1022 +#, php-format +msgid "Unable to fetch avatar from %s" msgstr "" -#: actions/ostatusinit.php:171 -msgid "OStatus Connect" +#: lib/salmonaction.php:41 +msgid "This method requires a POST." msgstr "" -#: actions/ostatussub.php:68 -msgid "Address or profile URL" +#: lib/salmonaction.php:45 +msgid "Salmon requires application/magic-envelope+xml" msgstr "" -#: actions/ostatussub.php:70 -msgid "Enter the profile URL of a PubSubHubbub-enabled feed" +#: lib/salmonaction.php:55 +msgid "Salmon signature verification failed." msgstr "" -#: actions/ostatussub.php:74 -msgid "Continue" +#: lib/salmonaction.php:67 +msgid "Salmon post must be an Atom entry." msgstr "" -#: actions/ostatussub.php:112 OStatusPlugin.php:503 -msgid "Join" +#: lib/salmonaction.php:115 +msgid "Unrecognized activity type." msgstr "" -#: actions/ostatussub.php:113 -msgid "Join this group" +#: lib/salmonaction.php:123 +msgid "This target doesn't understand posts." msgstr "" -#: actions/ostatussub.php:116 -msgid "Subscribe to this user" +#: lib/salmonaction.php:128 +msgid "This target doesn't understand follows." msgstr "" -#: actions/ostatussub.php:137 -msgid "You are already subscribed to this user." +#: lib/salmonaction.php:133 +msgid "This target doesn't understand unfollows." msgstr "" -#: actions/ostatussub.php:165 -msgid "You are already a member of this group." +#: lib/salmonaction.php:138 +msgid "This target doesn't understand favorites." msgstr "" -#: actions/ostatussub.php:286 -msgid "Empty remote profile URL!" +#: lib/salmonaction.php:143 +msgid "This target doesn't understand unfavorites." msgstr "" -#: actions/ostatussub.php:297 -msgid "Invalid address format." +#: lib/salmonaction.php:148 +msgid "This target doesn't understand share events." msgstr "" -#: actions/ostatussub.php:302 -msgid "Invalid URL or could not reach server." +#: lib/salmonaction.php:153 +msgid "This target doesn't understand joins." msgstr "" -#: actions/ostatussub.php:304 -msgid "Cannot read feed; server returned error." +#: lib/salmonaction.php:158 +msgid "This target doesn't understand leave events." msgstr "" -#: actions/ostatussub.php:306 -msgid "Cannot read feed; server returned an empty page." +#: tests/gettext-speedtest.php:57 +msgid "Feeds" msgstr "" -#: actions/ostatussub.php:308 -msgid "Bad HTML, could not find feed link." +#: actions/ostatusgroup.php:75 +msgid "Join group" msgstr "" -#: actions/ostatussub.php:310 -msgid "Could not find a feed linked from this URL." +#: actions/ostatusgroup.php:77 +msgid "OStatus group's address, like http://example.net/group/nickname" msgstr "" -#: actions/ostatussub.php:312 -msgid "Not a recognized feed type." +#: actions/ostatusgroup.php:81 actions/ostatussub.php:71 +msgid "Continue" msgstr "" -#: actions/ostatussub.php:315 -#, php-format -msgid "Bad feed URL: %s %s" +#: actions/ostatusgroup.php:100 +msgid "You are already a member of this group." msgstr "" #. TRANS: OStatus remote group subscription dialog error. -#: actions/ostatussub.php:336 +#: actions/ostatusgroup.php:135 msgid "Already a member!" msgstr "" #. TRANS: OStatus remote group subscription dialog error. -#: actions/ostatussub.php:346 +#: actions/ostatusgroup.php:146 msgid "Remote group join failed!" msgstr "" #. TRANS: OStatus remote group subscription dialog error. -#: actions/ostatussub.php:350 +#: actions/ostatusgroup.php:150 msgid "Remote group join aborted!" msgstr "" -#. TRANS: OStatus remote subscription dialog error. -#: actions/ostatussub.php:356 -msgid "Already subscribed!" +#. TRANS: Page title for OStatus remote group join form +#: actions/ostatusgroup.php:163 +msgid "Confirm joining remote group" msgstr "" -#. TRANS: OStatus remote subscription dialog error. -#: actions/ostatussub.php:361 -msgid "Remote subscription failed!" +#: actions/ostatusgroup.php:174 +msgid "" +"You can subscribe to groups from other supported sites. Paste the group's " +"profile URI below:" msgstr "" -#. TRANS: Page title for OStatus remote subscription form -#: actions/ostatussub.php:459 -msgid "Authorize subscription" +#: actions/groupsalmon.php:51 +msgid "Can't accept remote posts for a remote group." msgstr "" -#: actions/ostatussub.php:470 -msgid "" -"You can subscribe to users from other supported sites. Paste their address " -"or profile URI below:" +#: actions/groupsalmon.php:124 +msgid "Can't read profile to set up group membership." msgstr "" -#: classes/Ostatus_profile.php:789 -#, php-format -msgid "Tried to update avatar for unsaved remote profile %s" +#: actions/groupsalmon.php:127 actions/groupsalmon.php:170 +msgid "Groups can't join groups." msgstr "" -#: classes/Ostatus_profile.php:797 +#: actions/groupsalmon.php:154 #, php-format -msgid "Unable to fetch avatar from %s" +msgid "Could not join remote user %1$s to group %2$s." msgstr "" -#: lib/salmonaction.php:41 -msgid "This method requires a POST." +#: actions/groupsalmon.php:167 +msgid "Can't read profile to cancel group membership." msgstr "" -#: lib/salmonaction.php:45 -msgid "Salmon requires application/magic-envelope+xml" +#: actions/groupsalmon.php:183 +#, php-format +msgid "Could not remove remote user %1$s from group %2$s." msgstr "" -#: lib/salmonaction.php:55 -msgid "Salmon signature verification failed." +#: actions/ostatussub.php:65 +msgid "Subscribe to" msgstr "" -#: lib/salmonaction.php:67 -msgid "Salmon post must be an Atom entry." +#: actions/ostatussub.php:67 +msgid "" +"OStatus user's address, like nickname@example.com or http://example.net/" +"nickname" msgstr "" -#: lib/salmonaction.php:115 -msgid "Unrecognized activity type." +#: actions/ostatussub.php:106 +msgid "Join this group" msgstr "" -#: lib/salmonaction.php:123 -msgid "This target doesn't understand posts." +#. TRANS: Page title for OStatus remote subscription form +#: actions/ostatussub.php:108 actions/ostatussub.php:400 +msgid "Confirm" msgstr "" -#: lib/salmonaction.php:128 -msgid "This target doesn't understand follows." +#: actions/ostatussub.php:109 +msgid "Subscribe to this user" msgstr "" -#: lib/salmonaction.php:133 -msgid "This target doesn't understand unfollows." +#: actions/ostatussub.php:130 +msgid "You are already subscribed to this user." msgstr "" -#: lib/salmonaction.php:138 -msgid "This target doesn't understand favorites." +#: actions/ostatussub.php:247 actions/ostatussub.php:253 +#: actions/ostatussub.php:272 +msgid "" +"Sorry, we could not reach that address. Please make sure that the OStatus " +"address is like nickname@example.com or http://example.net/nickname" msgstr "" -#: lib/salmonaction.php:143 -msgid "This target doesn't understand unfavorites." +#: actions/ostatussub.php:256 actions/ostatussub.php:259 +#: actions/ostatussub.php:262 actions/ostatussub.php:265 +#: actions/ostatussub.php:268 +msgid "" +"Sorry, we could not reach that feed. Please try that OStatus address again " +"later." msgstr "" -#: lib/salmonaction.php:148 -msgid "This target doesn't understand share events." +#. TRANS: OStatus remote subscription dialog error. +#: actions/ostatussub.php:301 +msgid "Already subscribed!" msgstr "" -#: lib/salmonaction.php:153 -msgid "This target doesn't understand joins." +#. TRANS: OStatus remote subscription dialog error. +#: actions/ostatussub.php:306 +msgid "Remote subscription failed!" msgstr "" -#: lib/salmonaction.php:158 -msgid "This target doesn't understand leave events." +#: actions/ostatussub.php:380 actions/ostatusinit.php:81 +msgid "Subscribe to user" msgstr "" -#: OStatusPlugin.php:319 -#, php-format -msgid "Sent from %s via OStatus" +#: actions/ostatussub.php:411 +msgid "" +"You can subscribe to users from other supported sites. Paste their address " +"or profile URI below:" msgstr "" -#: OStatusPlugin.php:371 -msgid "Could not set up remote subscription." +#: actions/ostatusinit.php:41 +msgid "You can use the local subscription!" msgstr "" -#: OStatusPlugin.php:487 -msgid "Could not set up remote group membership." +#: actions/ostatusinit.php:63 +msgid "There was a problem with your session token. Try again, please." msgstr "" -#: OStatusPlugin.php:504 +#: actions/ostatusinit.php:95 #, php-format -msgid "%s has joined group %s." +msgid "Join group %s" msgstr "" -#: OStatusPlugin.php:512 -msgid "Failed joining remote group." +#: actions/ostatusinit.php:98 +#, php-format +msgid "Subscribe to %s" msgstr "" -#: OStatusPlugin.php:553 -msgid "Leave" +#: actions/ostatusinit.php:111 +msgid "User nickname" msgstr "" -#: OStatusPlugin.php:554 -#, php-format -msgid "%s has left group %s." +#: actions/ostatusinit.php:112 +msgid "Nickname of the user you want to follow" msgstr "" -#: OStatusPlugin.php:685 -msgid "Subscribe to remote user" +#: actions/ostatusinit.php:116 +msgid "Profile Account" msgstr "" -#: OStatusPlugin.php:726 -msgid "Profile update" +#: actions/ostatusinit.php:117 +msgid "Your account id (i.e. user@identi.ca)" msgstr "" -#: OStatusPlugin.php:727 -#, php-format -msgid "%s has updated their profile page." +#: actions/ostatusinit.php:138 +msgid "Must provide a remote profile." msgstr "" -#: tests/gettext-speedtest.php:57 -msgid "Feeds" +#: actions/ostatusinit.php:149 +msgid "Couldn't look up OStatus account profile." +msgstr "" + +#: actions/ostatusinit.php:161 +msgid "Couldn't confirm remote profile address." +msgstr "" + +#: actions/ostatusinit.php:202 +msgid "OStatus Connect" msgstr "" diff --git a/plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po b/plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po deleted file mode 100644 index f17dfa50a..000000000 --- a/plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po +++ /dev/null @@ -1,106 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-12-07 14:14-0800\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" -"Language-Team: LANGUAGE <LL@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: FeedSubPlugin.php:77 -msgid "Feeds" -msgstr "Flux" - -#: FeedSubPlugin.php:78 -msgid "Feed subscription options" -msgstr "Préférences pour abonnement flux" - -#: feedmunger.php:215 -#, php-format -msgid "New post: \"%1$s\" %2$s" -msgstr "Nouveau: \"%1$s\" %2$s" - -#: actions/feedsubsettings.php:41 -msgid "Feed subscriptions" -msgstr "Abonnements aux fluxes" - -#: actions/feedsubsettings.php:52 -msgid "" -"You can subscribe to feeds from other sites; updates will appear in your " -"personal timeline." -msgstr "" -"Abonner aux fluxes RSS ou Atom des autres sites web; les temps se trouverair" -"en votre flux personnel." - -#: actions/feedsubsettings.php:96 -msgid "Subscribe" -msgstr "Abonner" - -#: actions/feedsubsettings.php:98 -msgid "Continue" -msgstr "Prochaine" - -#: actions/feedsubsettings.php:151 -msgid "Empty feed URL!" -msgstr "" - -#: actions/feedsubsettings.php:161 -msgid "Invalid URL or could not reach server." -msgstr "" - -#: actions/feedsubsettings.php:164 -msgid "Cannot read feed; server returned error." -msgstr "" - -#: actions/feedsubsettings.php:167 -msgid "Cannot read feed; server returned an empty page." -msgstr "" - -#: actions/feedsubsettings.php:170 -msgid "Bad HTML, could not find feed link." -msgstr "" - -#: actions/feedsubsettings.php:173 -msgid "Could not find a feed linked from this URL." -msgstr "" - -#: actions/feedsubsettings.php:176 -msgid "Not a recognized feed type." -msgstr "" - -#: actions/feedsubsettings.php:180 -msgid "Bad feed URL." -msgstr "" - -#: actions/feedsubsettings.php:188 -msgid "Feed is not PuSH-enabled; cannot subscribe." -msgstr "" - -#: actions/feedsubsettings.php:208 -msgid "Feed subscription failed! Bad response from hub." -msgstr "" - -#: actions/feedsubsettings.php:218 -msgid "Already subscribed!" -msgstr "" - -#: actions/feedsubsettings.php:220 -msgid "Feed subscribed!" -msgstr "" - -#: actions/feedsubsettings.php:222 -msgid "Feed subscription failed!" -msgstr "" - -#: actions/feedsubsettings.php:231 -msgid "Previewing feed:" -msgstr "" diff --git a/plugins/OStatus/scripts/fixup-shadow.php b/plugins/OStatus/scripts/fixup-shadow.php index ec014c787..6522ca240 100644 --- a/plugins/OStatus/scripts/fixup-shadow.php +++ b/plugins/OStatus/scripts/fixup-shadow.php @@ -50,20 +50,47 @@ $encGroup = str_replace($marker, '%', $encGroup); $sql = "SELECT * FROM ostatus_profile WHERE uri LIKE '%s' OR uri LIKE '%s'"; $oprofile->query(sprintf($sql, $encProfile, $encGroup)); -echo "Found $oprofile->N bogus ostatus_profile entries for local users and groups:\n"; +$count = $oprofile->N; +echo "Found $count bogus ostatus_profile entries shadowing local users and groups:\n"; while ($oprofile->fetch()) { - echo "$oprofile->uri"; - + $uri = $oprofile->uri; + if (preg_match('!/group/(\d+)/id!', $oprofile->uri, $matches)) { + $id = intval($matches[1]); + $group = Local_group::staticGet('group_id', $id); + if ($group) { + $nick = $group->nickname; + } else { + $nick = '<deleted>'; + } + echo "group $id ($nick) hidden by $uri"; + } else if (preg_match('!/user/(\d+)!', $uri, $matches)) { + $id = intval($matches[1]); + $user = User::staticGet('id', $id); + if ($user) { + $nick = $user->nickname; + } else { + $nick = '<deleted>'; + } + echo "user $id ($nick) hidden by $uri"; + } else { + echo "$uri matched query, but we don't recognize it.\n"; + continue; + } + if ($dry) { - echo " (unchanged)\n"; + echo " - skipping\n"; } else { - echo " removing bogus ostatus_profile entry..."; + echo " - removing bogus ostatus_profile entry..."; $evil = clone($oprofile); $evil->delete(); echo " ok\n"; } } -echo "done.\n"; +if ($count && $dry) { + echo "NO CHANGES MADE -- To delete the bogus entries, run again without --dry-run option.\n"; +} else { + echo "done.\n"; +} diff --git a/plugins/OStatus/scripts/resub-feed.php b/plugins/OStatus/scripts/resub-feed.php new file mode 100644 index 000000000..121d12109 --- /dev/null +++ b/plugins/OStatus/scripts/resub-feed.php @@ -0,0 +1,74 @@ +#!/usr/bin/env php +<?php +/* + * StatusNet - a distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..')); + +$helptext = <<<END_OF_HELP +resub-feed.php [options] http://example.com/atom-feed-url +Reinitialize the PuSH subscription for the given feed. This may help get +things restarted if we and the hub have gotten our states out of sync. + + +END_OF_HELP; + +require_once INSTALLDIR.'/scripts/commandline.inc'; + +if (empty($args[0]) || !Validate::uri($args[0])) { + print "$helptext"; + exit(1); +} + +$feedurl = $args[0]; + + +$sub = FeedSub::staticGet('topic', $feedurl); +if (!$sub) { + print "Feed $feedurl is not subscribed.\n"; + exit(1); +} + +print "Old state:\n"; +showSub($sub); + +print "\n"; +print "Pinging hub $sub->huburi with new subscription for $sub->uri\n"; +$ok = $sub->subscribe(); + +if ($ok) { + print "ok\n"; +} else { + print "Could not confirm.\n"; +} + +$sub2 = FeedSub::staticGet('topic', $feedurl); + +print "\n"; +print "New state:\n"; +showSub($sub2); + +function showSub($sub) +{ + print " Subscription state: $sub->sub_state\n"; + print " Verify token: $sub->verify_token\n"; + print " Signature secret: $sub->secret\n"; + print " Sub start date: $sub->sub_start\n"; + print " Record created: $sub->created\n"; + print " Record modified: $sub->modified\n"; +} diff --git a/plugins/OStatus/scripts/update-profile.php b/plugins/OStatus/scripts/update-profile.php new file mode 100644 index 000000000..64afa0f35 --- /dev/null +++ b/plugins/OStatus/scripts/update-profile.php @@ -0,0 +1,147 @@ +#!/usr/bin/env php +<?php +/* + * StatusNet - a distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..')); + +$helptext = <<<END_OF_HELP +update-profile.php [options] http://example.com/profile/url + +Rerun profile and feed info discovery for the given OStatus remote profile, +and reinitialize its PuSH subscription for the given feed. This may help get +things restarted if the hub or feed URLs have changed for the profile. + + +END_OF_HELP; + +require_once INSTALLDIR.'/scripts/commandline.inc'; + +if (empty($args[0]) || !Validate::uri($args[0])) { + print "$helptext"; + exit(1); +} + +$uri = $args[0]; + + +$oprofile = Ostatus_profile::staticGet('uri', $uri); + +if (!$oprofile) { + print "No OStatus remote profile known for URI $uri\n"; + exit(1); +} + +print "Old profile state for $oprofile->uri\n"; +showProfile($oprofile); + +print "\n"; +print "Re-running feed discovery for profile URL $oprofile->uri\n"; +// @fixme will bork where the URI isn't the profile URL for now +$discover = new FeedDiscovery(); +$feedurl = $discover->discoverFromURL($oprofile->uri); +$huburi = $discover->getHubLink(); +$salmonuri = $discover->getAtomLink(Salmon::NS_REPLIES); + +print " Feed URL: $feedurl\n"; +print " Hub URL: $huburi\n"; +print " Salmon URL: $salmonuri\n"; + +if ($feedurl != $oprofile->feeduri || $salmonuri != $oprofile->salmonuri) { + print "\n"; + print "Updating...\n"; + // @fixme update keys :P + #$orig = clone($oprofile); + #$oprofile->feeduri = $feedurl; + #$oprofile->salmonuri = $salmonuri; + #$ok = $oprofile->update($orig); + $ok = $oprofile->query('UPDATE ostatus_profile SET ' . + 'feeduri=\'' . $oprofile->escape($feedurl) . '\',' . + 'salmonuri=\'' . $oprofile->escape($salmonuri) . '\' ' . + 'WHERE uri=\'' . $oprofile->escape($uri) . '\''); + + if (!$ok) { + print "Failed to update profile record...\n"; + exit(1); + } + + $oprofile->decache(); +} else { + print "\n"; + print "Ok, ostatus_profile record unchanged.\n\n"; +} + +$sub = FeedSub::ensureFeed($feedurl); + +if ($huburi != $sub->huburi) { + print "\n"; + print "Updating hub record for feed; was $sub->huburi\n"; + $orig = clone($sub); + $sub->huburi = $huburi; + $ok = $sub->update($orig); + + if (!$ok) { + print "Failed to update sub record...\n"; + exit(1); + } +} else { + print "\n"; + print "Feed record ok, not changing.\n\n"; +} + +print "\n"; +print "Pinging hub $sub->huburi with new subscription for $sub->uri\n"; +$ok = $sub->subscribe(); + +if ($ok) { + print "ok\n"; +} else { + print "Could not confirm.\n"; +} + +$o2 = Ostatus_profile::staticGet('uri', $uri); + +print "\n"; +print "New profile state:\n"; +showProfile($o2); + +print "\n"; +print "New feed state:\n"; +$sub2 = FeedSub::ensureFeed($feedurl); +showSub($sub2); + +function showProfile($oprofile) +{ + print " Feed URL: $oprofile->feeduri\n"; + print " Salmon URL: $oprofile->salmonuri\n"; + print " Avatar URL: $oprofile->avatar\n"; + print " Profile ID: $oprofile->profile_id\n"; + print " Group ID: $oprofile->group_id\n"; + print " Record created: $oprofile->created\n"; + print " Record modified: $oprofile->modified\n"; +} + +function showSub($sub) +{ + print " Subscription state: $sub->sub_state\n"; + print " Verify token: $sub->verify_token\n"; + print " Signature secret: $sub->secret\n"; + print " Sub start date: $sub->sub_start\n"; + print " Record created: $sub->created\n"; + print " Record modified: $sub->modified\n"; +} diff --git a/plugins/OStatus/tests/FeedDiscoveryTest.php b/plugins/OStatus/tests/FeedDiscoveryTest.php index 1c5249701..0e6354a86 100644 --- a/plugins/OStatus/tests/FeedDiscoveryTest.php +++ b/plugins/OStatus/tests/FeedDiscoveryTest.php @@ -10,7 +10,7 @@ define('STATUSNET', true); define('LACONICA', true); require_once INSTALLDIR . '/lib/common.php'; -require_once INSTALLDIR . '/plugins/FeedSub/feedsub.php'; +require_once INSTALLDIR . '/plugins/OStatus/lib/feeddiscovery.php'; class FeedDiscoveryTest extends PHPUnit_Framework_TestCase { diff --git a/plugins/OStatus/tests/remote-tests.php b/plugins/OStatus/tests/remote-tests.php new file mode 100644 index 000000000..24b4b1660 --- /dev/null +++ b/plugins/OStatus/tests/remote-tests.php @@ -0,0 +1,555 @@ +<?php + +if (php_sapi_name() != 'cli') { + die('not for web'); +} + +define('INSTALLDIR', dirname(dirname(dirname(dirname(__FILE__))))); +set_include_path(INSTALLDIR . '/extlib' . PATH_SEPARATOR . get_include_path()); + +require_once 'PEAR.php'; +require_once 'Net/URL2.php'; +require_once 'HTTP/Request2.php'; + + +// ostatus test script, client-side :) + +class TestBase +{ + function log($str) + { + $args = func_get_args(); + array_shift($args); + + $msg = vsprintf($str, $args); + print $msg . "\n"; + } + + function assertEqual($a, $b) + { + if ($a != $b) { + throw new Exception("Failed to assert equality: expected $a, got $b"); + } + return true; + } + + function assertNotEqual($a, $b) + { + if ($a == $b) { + throw new Exception("Failed to assert inequality: expected not $a, got $b"); + } + return true; + } + + function assertTrue($a) + { + if (!$a) { + throw new Exception("Failed to assert true: got false"); + } + } + + function assertFalse($a) + { + if ($a) { + throw new Exception("Failed to assert false: got true"); + } + } +} + +class OStatusTester extends TestBase +{ + /** + * @param string $a base URL of test site A (eg http://localhost/mublog) + * @param string $b base URL of test site B (eg http://localhost/mublog2) + */ + function __construct($a, $b) { + $this->a = $a; + $this->b = $b; + + $base = 'test' . mt_rand(1, 1000000); + $this->pub = new SNTestClient($this->a, 'pub' . $base, 'pw-' . mt_rand(1, 1000000)); + $this->sub = new SNTestClient($this->b, 'sub' . $base, 'pw-' . mt_rand(1, 1000000)); + } + + function run() + { + $this->setup(); + + $methods = get_class_methods($this); + foreach ($methods as $method) { + if (strtolower(substr($method, 0, 4)) == 'test') { + print "\n"; + print "== $method ==\n"; + call_user_func(array($this, $method)); + } + } + + print "\n"; + $this->log("DONE!"); + } + + function setup() + { + $this->pub->register(); + $this->pub->assertRegistered(); + + $this->sub->register(); + $this->sub->assertRegistered(); + } + + function testLocalPost() + { + $post = $this->pub->post("Local post, no subscribers yet."); + $this->assertNotEqual('', $post); + + $post = $this->sub->post("Local post, no subscriptions yet."); + $this->assertNotEqual('', $post); + } + + /** + * pub posts: @b/sub + */ + function testMentionUrl() + { + $bits = parse_url($this->b); + $base = $bits['host']; + if (isset($bits['path'])) { + $base .= $bits['path']; + } + $name = $this->sub->username; + + $post = $this->pub->post("@$base/$name should have this in home and replies"); + $this->sub->assertReceived($post); + } + + function testSubscribe() + { + $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri())); + $this->sub->subscribe($this->pub->getProfileLink()); + $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri())); + } + + function testPush() + { + $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri())); + + $name = $this->sub->username; + $post = $this->pub->post("Regular post, which $name should get via PuSH"); + $this->sub->assertReceived($post); + } + + function testMentionSubscribee() + { + $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertFalse($this->pub->hasSubscription($this->sub->getProfileUri())); + + $name = $this->pub->username; + $post = $this->sub->post("Just a quick note back to my remote subscribee @$name"); + $this->pub->assertReceived($post); + } + + function testUnsubscribe() + { + $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri())); + $this->sub->unsubscribe($this->pub->getProfileLink()); + $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri())); + } + +} + +class SNTestClient extends TestBase +{ + function __construct($base, $username, $password) + { + $this->basepath = $base; + $this->username = $username; + $this->password = $password; + + $this->fullname = ucfirst($username) . ' Smith'; + $this->homepage = 'http://example.org/' . $username; + $this->bio = 'Stub account for OStatus tests.'; + $this->location = 'Montreal, QC'; + } + + /** + * Make a low-level web hit to this site, with authentication. + * @param string $path URL fragment for something under the base path + * @param array $params POST parameters to send + * @param boolean $auth whether to include auth data + * @return string + * @throws Exception on low-level error conditions + */ + protected function hit($path, $params=array(), $auth=false, $cookies=array()) + { + $url = $this->basepath . '/' . $path; + + $http = new HTTP_Request2($url, 'POST'); + if ($auth) { + $http->setAuth($this->username, $this->password, HTTP_Request2::AUTH_BASIC); + } + foreach ($cookies as $name => $val) { + $http->addCookie($name, $val); + } + $http->addPostParameter($params); + $response = $http->send(); + + $code = $response->getStatus(); + if ($code < '200' || $code >= '400') { + throw new Exception("Failed API hit to $url: $code\n" . $response->getBody()); + } + + return $response; + } + + /** + * Make a hit to a web form, without authentication but with a session. + * @param string $path URL fragment relative to site base + * @param string $form id of web form to pull initial parameters from + * @param array $params POST parameters, will be merged with defaults in form + */ + protected function web($path, $form, $params=array()) + { + $url = $this->basepath . '/' . $path; + $http = new HTTP_Request2($url, 'GET'); + $response = $http->send(); + + $dom = $this->checkWeb($url, 'GET', $response); + $cookies = array(); + foreach ($response->getCookies() as $cookie) { + // @fixme check for expirations etc + $cookies[$cookie['name']] = $cookie['value']; + } + + $form = $dom->getElementById($form); + if (!$form) { + throw new Exception("Form $form not found on $url"); + } + $inputs = $form->getElementsByTagName('input'); + foreach ($inputs as $item) { + $type = $item->getAttribute('type'); + if ($type != 'check') { + $name = $item->getAttribute('name'); + $val = $item->getAttribute('value'); + if ($name && $val && !isset($params[$name])) { + $params[$name] = $val; + } + } + } + + $response = $this->hit($path, $params, false, $cookies); + $dom = $this->checkWeb($url, 'POST', $response); + + return $dom; + } + + protected function checkWeb($url, $method, $response) + { + $dom = new DOMDocument(); + if (!$dom->loadHTML($response->getBody())) { + throw new Exception("Invalid HTML from $method to $url"); + } + + $xpath = new DOMXPath($dom); + $error = $xpath->query('//p[@class="error"]'); + if ($error && $error->length) { + throw new Exception("Error on $method to $url: " . + $error->item(0)->textContent); + } + + return $dom; + } + + protected function parseXml($path, $body) + { + $dom = new DOMDocument(); + if ($dom->loadXML($body)) { + return $dom; + } else { + throw new Exception("Bogus XML data from $path:\n$body"); + } + } + + /** + * Make a hit to a REST-y XML page on the site, without authentication. + * @param string $path URL fragment for something relative to base + * @param array $params POST parameters to send + * @return DOMDocument + * @throws Exception on low-level error conditions + */ + protected function xml($path, $params=array()) + { + $response = $this->hit($path, $params, true); + $body = $response->getBody(); + return $this->parseXml($path, $body); + } + + protected function parseJson($path, $body) + { + $data = json_decode($body, true); + if ($data !== null) { + if (!empty($data['error'])) { + throw new Exception("JSON API returned error: " . $data['error']); + } + return $data; + } else { + throw new Exception("Bogus JSON data from $path:\n$body"); + } + } + + /** + * Make an API hit to this site, with authentication. + * @param string $path URL fragment for something under 'api' folder + * @param string $style one of 'json', 'xml', or 'atom' + * @param array $params POST parameters to send + * @return mixed associative array for JSON, DOMDocument for XML/Atom + * @throws Exception on low-level error conditions + */ + protected function api($path, $style, $params=array()) + { + $response = $this->hit("api/$path.$style", $params, true); + $body = $response->getBody(); + if ($style == 'json') { + return $this->parseJson($path, $body); + } else if ($style == 'xml' || $style == 'atom') { + return $this->parseXml($path, $body); + } else { + throw new Exception("API needs to be JSON, XML, or Atom"); + } + } + + /** + * Register the account. + * + * Unfortunately there's not an API method for registering, so we fake it. + */ + function register() + { + $this->log("Registering user %s on %s", + $this->username, + $this->basepath); + $ret = $this->web('main/register', 'form_register', + array('nickname' => $this->username, + 'password' => $this->password, + 'confirm' => $this->password, + 'fullname' => $this->fullname, + 'homepage' => $this->homepage, + 'bio' => $this->bio, + 'license' => 1, + 'submit' => 'Register')); + } + + /** + * @return string canonical URI/URL to profile page + */ + function getProfileUri() + { + $data = $this->api('account/verify_credentials', 'json'); + $id = $data['id']; + return $this->basepath . '/user/' . $id; + } + + /** + * @return string human-friendly URL to profile page + */ + function getProfileLink() + { + return $this->basepath . '/' . $this->username; + } + + /** + * Check that the account has been registered and can be used. + * On failure, throws a test failure exception. + */ + function assertRegistered() + { + $this->log("Confirming %s is registered on %s", + $this->username, + $this->basepath); + $data = $this->api('account/verify_credentials', 'json'); + $this->assertEqual($this->username, $data['screen_name']); + $this->assertEqual($this->fullname, $data['name']); + $this->assertEqual($this->homepage, $data['url']); + $this->assertEqual($this->bio, $data['description']); + $this->log(" looks good!"); + } + + /** + * Post a given message from this account + * @param string $message + * @return string URL/URI of notice + * @todo reply, location options + */ + function post($message) + { + $this->log("Posting notice as %s on %s: %s", + $this->username, + $this->basepath, + $message); + $data = $this->api('statuses/update', 'json', + array('status' => $message)); + + $url = $this->basepath . '/notice/' . $data['id']; + return $url; + } + + /** + * Check that this account has received the notice. + * @param string $notice_uri URI for the notice to check for + */ + function assertReceived($notice_uri) + { + $timeout = 5; + $tries = 6; + while ($tries) { + $ok = $this->checkReceived($notice_uri); + if ($ok) { + return true; + } + $tries--; + if ($tries) { + $this->log(" didn't see it yet, waiting $timeout seconds"); + sleep($timeout); + } + } + throw new Exception(" message $notice_uri not received by $this->username"); + } + + /** + * Pull the user's home timeline to check if a notice with the given + * source URL has been received recently. + * If we don't see it, we'll try a couple more times up to 10 seconds. + * + * @param string $notice_uri + */ + function checkReceived($notice_uri) + { + $this->log("Checking if %s on %s received notice %s", + $this->username, + $this->basepath, + $notice_uri); + $params = array(); + $dom = $this->api('statuses/home_timeline', 'atom', $params); + + $xml = simplexml_import_dom($dom); + if (!$xml->entry) { + return false; + } + if (is_array($xml->entry)) { + $entries = $xml->entry; + } else { + $entries = array($xml->entry); + } + foreach ($entries as $entry) { + if ($entry->id == $notice_uri) { + $this->log(" found it $notice_uri"); + return true; + } + } + return false; + } + + /** + * @param string $profile user page link or webfinger + */ + function subscribe($profile) + { + // This uses the command interface, since there's not currently + // a friendly Twit-API way to do a fresh remote subscription and + // the web form's a pain to use. + $this->post('follow ' . $profile); + } + + /** + * @param string $profile user page link or webfinger + */ + function unsubscribe($profile) + { + // This uses the command interface, since there's not currently + // a friendly Twit-API way to do a fresh remote subscription and + // the web form's a pain to use. + $this->post('leave ' . $profile); + } + + /** + * Check that this account is subscribed to the given profile. + * @param string $profile_uri URI for the profile to check for + * @return boolean + */ + function hasSubscription($profile_uri) + { + $this->log("Checking if $this->username has a subscription to $profile_uri"); + + $me = $this->getProfileUri(); + return $this->checkSubscription($me, $profile_uri); + } + + /** + * Check that this account is subscribed to by the given profile. + * @param string $profile_uri URI for the profile to check for + * @return boolean + */ + function hasSubscriber($profile_uri) + { + $this->log("Checking if $this->username is subscribed to by $profile_uri"); + + $me = $this->getProfileUri(); + return $this->checkSubscription($profile_uri, $me); + } + + protected function checkSubscription($subscriber, $subscribed) + { + // Using FOAF as the API methods for checking the social graph + // currently are unfriendly to remote profiles + $ns_foaf = 'http://xmlns.com/foaf/0.1/'; + $ns_sioc = 'http://rdfs.org/sioc/ns#'; + $ns_rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; + + $dom = $this->xml($this->username . '/foaf'); + $agents = $dom->getElementsByTagNameNS($ns_foaf, 'Agent'); + foreach ($agents as $agent) { + $agent_uri = $agent->getAttributeNS($ns_rdf, 'about'); + if ($agent_uri == $subscriber) { + $follows = $agent->getElementsByTagNameNS($ns_sioc, 'follows'); + foreach ($follows as $follow) { + $target = $follow->getAttributeNS($ns_rdf, 'resource'); + if ($target == ($subscribed . '#acct')) { + $this->log(" confirmed $subscriber subscribed to $subscribed"); + return true; + } + } + $this->log(" we found $subscriber but they don't follow $subscribed"); + return false; + } + } + $this->log(" can't find $subscriber in {$this->username}'s social graph."); + return false; + } + +} + +$args = array_slice($_SERVER['argv'], 1); +if (count($args) < 2) { + print <<<END_HELP +remote-tests.php <url1> <url2> + url1: base URL of a StatusNet instance + url2: base URL of another StatusNet instance + +This will register user accounts on the two given StatusNet instances +and run some tests to confirm that OStatus subscription and posting +between the two sites works correctly. + +END_HELP; +exit(1); +} + +$a = $args[0]; +$b = $args[1]; + +$tester = new OStatusTester($a, $b); +$tester->run(); + diff --git a/plugins/OpenExternalLinkTarget/OpenExternalLinkTargetPlugin.php b/plugins/OpenExternalLinkTarget/OpenExternalLinkTargetPlugin.php index ebb0189e0..6756f1993 100644 --- a/plugins/OpenExternalLinkTarget/OpenExternalLinkTargetPlugin.php +++ b/plugins/OpenExternalLinkTarget/OpenExternalLinkTargetPlugin.php @@ -45,7 +45,7 @@ class OpenExternalLinkTargetPlugin extends Plugin { function onEndShowScripts($action) { - $action->inlineScript('$("a[rel~=external]").click(function(){ window.open(this.href); return false; });'); + $action->inlineScript('$("a[rel~=external]:not([class~=attachment])").live("click", function(){ window.open(this.href); return false; });'); return true; } diff --git a/plugins/OpenExternalLinkTarget/locale/OpenExternalLinkTarget.pot b/plugins/OpenExternalLinkTarget/locale/OpenExternalLinkTarget.pot new file mode 100644 index 000000000..f9bd4af10 --- /dev/null +++ b/plugins/OpenExternalLinkTarget/locale/OpenExternalLinkTarget.pot @@ -0,0 +1,21 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: OpenExternalLinkTargetPlugin.php:60 +msgid "Opens external links (i.e., with rel=external) on a new window or tab" +msgstr "" diff --git a/plugins/OpenID/OpenIDPlugin.php b/plugins/OpenID/OpenIDPlugin.php index 6b35ec3e1..7d6a5dc00 100644 --- a/plugins/OpenID/OpenIDPlugin.php +++ b/plugins/OpenID/OpenIDPlugin.php @@ -20,7 +20,9 @@ * @category Plugin * @package StatusNet * @author Evan Prodromou <evan@status.net> - * @copyright 2009 StatusNet, Inc. + * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2009-2010 StatusNet, Inc. + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ @@ -38,6 +40,8 @@ if (!defined('STATUSNET')) { * @category Plugin * @package StatusNet * @author Evan Prodromou <evan@status.net> + * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ * @link http://openid.net/ @@ -45,13 +49,18 @@ if (!defined('STATUSNET')) { class OpenIDPlugin extends Plugin { - /** - * Initializer for the plugin. - */ + // Plugin parameter: set true to disallow non-OpenID logins + // If set, overrides the setting in database or $config['site']['openidonly'] + public $openidOnly = null; - function __construct() + function initialize() { - parent::__construct(); + parent::initialize(); + if ($this->openidOnly !== null) { + global $config; + $config['site']['openidonly'] = (bool)$this->openidOnly; + } + } /** @@ -59,6 +68,8 @@ class OpenIDPlugin extends Plugin * * Hook for RouterInitialized event. * + * @param Net_URL_Mapper $m URL mapper + * * @return boolean hook return */ @@ -67,140 +78,374 @@ class OpenIDPlugin extends Plugin $m->connect('main/openid', array('action' => 'openidlogin')); $m->connect('main/openidtrust', array('action' => 'openidtrust')); $m->connect('settings/openid', array('action' => 'openidsettings')); - $m->connect('index.php?action=finishopenidlogin', array('action' => 'finishopenidlogin')); - $m->connect('index.php?action=finishaddopenid', array('action' => 'finishaddopenid')); + $m->connect('index.php?action=finishopenidlogin', + array('action' => 'finishopenidlogin')); + $m->connect('index.php?action=finishaddopenid', + array('action' => 'finishaddopenid')); $m->connect('main/openidserver', array('action' => 'openidserver')); + $m->connect('admin/openid', array('action' => 'openidadminpanel')); + + return true; + } + + /** + * In OpenID-only mode, disable paths for password stuff + * + * @param string $path path to connect + * @param array $defaults path defaults + * @param array $rules path rules + * @param array $result unused + * + * @return boolean hook return + */ + + function onStartConnectPath(&$path, &$defaults, &$rules, &$result) + { + if (common_config('site', 'openidonly')) { + static $block = array('main/login', + 'main/register', + 'main/recoverpassword', + 'settings/password'); + + if (in_array($path, $block)) { + return false; + } + } + + return true; + } + + /** + * If we've been hit with password-login args, redirect + * + * @param array $args args (URL, Get, post) + * + * @return boolean hook return + */ + function onArgsInitialize($args) + { + if (common_config('site', 'openidonly')) { + if (array_key_exists('action', $args)) { + $action = trim($args['action']); + if (in_array($action, array('login', 'register'))) { + common_redirect(common_local_url('openidlogin')); + exit(0); + } else if ($action == 'passwordsettings') { + common_redirect(common_local_url('openidsettings')); + exit(0); + } else if ($action == 'recoverpassword') { + throw new ClientException('Unavailable action'); + } + } + } return true; } + /** + * Public XRDS output hook + * + * Puts the bits of code needed by some OpenID providers to show + * we're good citizens. + * + * @param Action $action Action being executed + * @param XMLOutputter &$xrdsOutputter Output channel + * + * @return boolean hook return + */ + function onEndPublicXRDS($action, &$xrdsOutputter) { $xrdsOutputter->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)', - 'xmlns:simple' => 'http://xrds-simple.net/core/1.0', - 'version' => '2.0')); + 'xmlns:simple' => 'http://xrds-simple.net/core/1.0', + 'version' => '2.0')); $xrdsOutputter->element('Type', null, 'xri://$xrds*simple'); //consumer foreach (array('finishopenidlogin', 'finishaddopenid') as $finish) { $xrdsOutputter->showXrdsService(Auth_OpenID_RP_RETURN_TO_URL_TYPE, - common_local_url($finish)); + common_local_url($finish)); } //provider $xrdsOutputter->showXrdsService('http://specs.openid.net/auth/2.0/server', - common_local_url('openidserver'), - null, - null, - 'http://specs.openid.net/auth/2.0/identifier_select'); + common_local_url('openidserver'), + null, + null, + 'http://specs.openid.net/auth/2.0/identifier_select'); $xrdsOutputter->elementEnd('XRD'); } + /** + * User XRDS output hook + * + * Puts the bits of code needed to discover OpenID endpoints. + * + * @param Action $action Action being executed + * @param XMLOutputter &$xrdsOutputter Output channel + * + * @return boolean hook return + */ + function onEndUserXRDS($action, &$xrdsOutputter) { $xrdsOutputter->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)', - 'xml:id' => 'openid', - 'xmlns:simple' => 'http://xrds-simple.net/core/1.0', - 'version' => '2.0')); + 'xml:id' => 'openid', + 'xmlns:simple' => 'http://xrds-simple.net/core/1.0', + 'version' => '2.0')); $xrdsOutputter->element('Type', null, 'xri://$xrds*simple'); //consumer $xrdsOutputter->showXrdsService('http://specs.openid.net/auth/2.0/return_to', - common_local_url('finishopenidlogin')); + common_local_url('finishopenidlogin')); //provider $xrdsOutputter->showXrdsService('http://specs.openid.net/auth/2.0/signon', - common_local_url('openidserver'), - null, - null, - common_profile_url($action->user->nickname)); + common_local_url('openidserver'), + null, + null, + common_profile_url($action->user->nickname)); $xrdsOutputter->elementEnd('XRD'); } + /** + * If we're in OpenID-only mode, hide all the main menu except OpenID login. + * + * @param Action $action Action being run + * + * @return boolean hook return + */ + + function onStartPrimaryNav($action) + { + if (common_config('site', 'openidonly') && !common_logged_in()) { + // TRANS: Tooltip for main menu option "Login" + $tooltip = _m('TOOLTIP', 'Login to the site'); + $action->menuItem(common_local_url('openidlogin'), + // TRANS: Main menu option when not logged in to log in + _m('MENU', 'Login'), + $tooltip, + false, + 'nav_login'); + // TRANS: Tooltip for main menu option "Help" + $tooltip = _m('TOOLTIP', 'Help me!'); + $action->menuItem(common_local_url('doc', array('title' => 'help')), + // TRANS: Main menu option for help on the StatusNet site + _m('MENU', 'Help'), + $tooltip, + false, + 'nav_help'); + if (!common_config('site', 'private')) { + // TRANS: Tooltip for main menu option "Search" + $tooltip = _m('TOOLTIP', 'Search for people or text'); + $action->menuItem(common_local_url('peoplesearch'), + // TRANS: Main menu option when logged in or when the StatusNet instance is not private + _m('MENU', 'Search'), $tooltip, false, 'nav_search'); + } + Event::handle('EndPrimaryNav', array($action)); + return false; + } + return true; + } + + /** + * Menu for login + * + * If we're in openidOnly mode, we disable the menu for all other login. + * + * @param Action &$action Action being executed + * + * @return boolean hook return + */ + + function onStartLoginGroupNav(&$action) + { + if (common_config('site', 'openidonly')) { + $this->showOpenIDLoginTab($action); + // Even though we replace this code, we + // DON'T run the End* hook, to keep others from + // adding tabs. Not nice, but. + return false; + } + + return true; + } + + /** + * Menu item for login + * + * @param Action &$action Action being executed + * + * @return boolean hook return + */ + function onEndLoginGroupNav(&$action) { + $this->showOpenIDLoginTab($action); + + return true; + } + + /** + * Show menu item for login + * + * @param Action $action Action being executed + * + * @return void + */ + + function showOpenIDLoginTab($action) + { $action_name = $action->trimmed('action'); $action->menuItem(common_local_url('openidlogin'), - _m('OpenID'), + // TRANS: OpenID plugin menu item on site logon page. + _m('MENU', 'OpenID'), + // TRANS: OpenID plugin tooltip for logon menu item. _m('Login or register with OpenID'), $action_name === 'openidlogin'); + } + /** + * Show menu item for password + * + * We hide it in openID-only mode + * + * @param Action $menu Widget for menu + * @param void &$unused Unused value + * + * @return void + */ + + function onStartAccountSettingsPasswordMenuItem($menu, &$unused) { + if (common_config('site', 'openidonly')) { + return false; + } return true; } + /** + * Menu item for OpenID settings + * + * @param Action &$action Action being executed + * + * @return boolean hook return + */ + function onEndAccountSettingsNav(&$action) { $action_name = $action->trimmed('action'); $action->menuItem(common_local_url('openidsettings'), - _m('OpenID'), + // TRANS: OpenID plugin menu item on user settings page. + _m('MENU', 'OpenID'), + // TRANS: OpenID plugin tooltip for user settings menu item. _m('Add or remove OpenIDs'), $action_name === 'openidsettings'); return true; } + /** + * Autoloader + * + * Loads our classes if they're requested. + * + * @param string $cls Class requested + * + * @return boolean hook return + */ + function onAutoload($cls) { switch ($cls) { - case 'OpenidloginAction': - case 'FinishopenidloginAction': - case 'FinishaddopenidAction': - case 'XrdsAction': - case 'PublicxrdsAction': - case 'OpenidsettingsAction': - case 'OpenidserverAction': - case 'OpenidtrustAction': - require_once(INSTALLDIR.'/plugins/OpenID/' . strtolower(mb_substr($cls, 0, -6)) . '.php'); + case 'OpenidloginAction': + case 'FinishopenidloginAction': + case 'FinishaddopenidAction': + case 'XrdsAction': + case 'PublicxrdsAction': + case 'OpenidsettingsAction': + case 'OpenidserverAction': + case 'OpenidtrustAction': + case 'OpenidadminpanelAction': + require_once dirname(__FILE__) . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; + return false; + case 'User_openid': + require_once dirname(__FILE__) . '/User_openid.php'; return false; - case 'User_openid': - require_once(INSTALLDIR.'/plugins/OpenID/User_openid.php'); + case 'User_openid_trustroot': + require_once dirname(__FILE__) . '/User_openid_trustroot.php'; return false; - case 'User_openid_trustroot': - require_once(INSTALLDIR.'/plugins/OpenID/User_openid_trustroot.php'); + case 'Auth_OpenID_TeamsExtension': + case 'Auth_OpenID_TeamsRequest': + case 'Auth_OpenID_TeamsResponse': + require_once dirname(__FILE__) . '/extlib/teams-extension.php'; return false; - default: + default: return true; } } + /** + * Sensitive actions + * + * These actions should use https when SSL support is 'sometimes' + * + * @param Action $action Action to form an URL for + * @param boolean &$ssl Whether to mark it for SSL + * + * @return boolean hook return + */ + function onSensitiveAction($action, &$ssl) { switch ($action) { - case 'finishopenidlogin': - case 'finishaddopenid': + case 'finishopenidlogin': + case 'finishaddopenid': $ssl = true; return false; - default: + default: return true; } } + /** + * Login actions + * + * These actions should be visible even when the site is marked private + * + * @param Action $action Action to show + * @param boolean &$login Whether it's a login action + * + * @return boolean hook return + */ + function onLoginAction($action, &$login) { switch ($action) { - case 'openidlogin': - case 'finishopenidlogin': - case 'openidserver': + case 'openidlogin': + case 'finishopenidlogin': + case 'openidserver': $login = true; return false; - default: + default: return true; } } /** - * We include a <meta> element linking to the publicxrds page, for OpenID + * We include a <meta> element linking to the userxrds page, for OpenID * client-side authentication. * + * @param Action $action Action being shown + * * @return void */ function onEndShowHeadElements($action) { - if($action instanceof ShowstreamAction){ + if ($action instanceof ShowstreamAction) { $action->element('link', array('rel' => 'openid2.provider', 'href' => common_local_url('openidserver'))); $action->element('link', array('rel' => 'openid2.local_id', @@ -216,25 +461,36 @@ class OpenIDPlugin extends Plugin /** * Redirect to OpenID login if they have an OpenID * + * @param Action $action Action being executed + * @param User $user User doing the action + * * @return boolean whether to continue */ function onRedirectToLogin($action, $user) { - if (!empty($user) && User_openid::hasOpenID($user->id)) { + if (common_config('site', 'openid_only') || (!empty($user) && User_openid::hasOpenID($user->id))) { common_redirect(common_local_url('openidlogin'), 303); return false; } return true; } + /** + * Show some extra instructions for using OpenID + * + * @param Action $action Action being executed + * + * @return boolean hook value + */ + function onEndShowPageNotice($action) { $name = $action->trimmed('action'); switch ($name) { - case 'register': + case 'register': if (common_logged_in()) { $instr = '(Have an [OpenID](http://openid.net/)? ' . '[Add an OpenID to your account](%%action.openidsettings%%)!'; @@ -244,12 +500,12 @@ class OpenIDPlugin extends Plugin '(%%action.openidlogin%%)!)'; } break; - case 'login': + case 'login': $instr = '(Have an [OpenID](http://openid.net/)? ' . 'Try our [OpenID login]'. '(%%action.openidlogin%%)!)'; break; - default: + default: return true; } @@ -258,13 +514,21 @@ class OpenIDPlugin extends Plugin return true; } + /** + * Load our document if requested + * + * @param string &$title Title to fetch + * @param string &$output HTML to output + * + * @return boolean hook value + */ + function onStartLoadDoc(&$title, &$output) { - if ($title == 'openid') - { + if ($title == 'openid') { $filename = INSTALLDIR.'/plugins/OpenID/doc-src/openid'; - $c = file_get_contents($filename); + $c = file_get_contents($filename); $output = common_markup_to_html($c); return false; // success! } @@ -272,10 +536,18 @@ class OpenIDPlugin extends Plugin return true; } + /** + * Add our document to the global menu + * + * @param string $title Title being fetched + * @param string &$output HTML being output + * + * @return boolean hook value + */ + function onEndLoadDoc($title, &$output) { - if ($title == 'help') - { + if ($title == 'help') { $menuitem = '* [OpenID](%%doc.openid%%) - what OpenID is and how to use it with this service'; $output .= common_markup_to_html($menuitem); @@ -284,7 +556,16 @@ class OpenIDPlugin extends Plugin return true; } - function onCheckSchema() { + /** + * Data definitions + * + * Assure that our data objects are available in the DB + * + * @return boolean hook value + */ + + function onCheckSchema() + { $schema = Schema::get(); $schema->ensureTable('user_openid', array(new ColumnDef('canonical', 'varchar', @@ -307,6 +588,15 @@ class OpenIDPlugin extends Plugin return true; } + /** + * Add our tables to be deleted when a user is deleted + * + * @param User $user User being deleted + * @param array &$tables Array of table names + * + * @return boolean hook value + */ + function onUserDeleteRelated($user, &$tables) { $tables[] = 'User_openid'; @@ -314,6 +604,40 @@ class OpenIDPlugin extends Plugin return true; } + /** + * Add an OpenID tab to the admin panel + * + * @param Widget $nav Admin panel nav + * + * @return boolean hook value + */ + + function onEndAdminPanelNav($nav) + { + if (AdminPanelAction::canAdmin('openid')) { + + $action_name = $nav->action->trimmed('action'); + + $nav->out->menuItem( + common_local_url('openidadminpanel'), + _m('OpenID'), + _m('OpenID configuration'), + $action_name == 'openidadminpanel', + 'nav_openid_admin_panel' + ); + } + + return true; + } + + /** + * Add our version information to output + * + * @param array &$versions Array of version-data arrays + * + * @return boolean hook value + */ + function onPluginVersion(&$versions) { $versions[] = array('name' => 'OpenID', @@ -321,6 +645,7 @@ class OpenIDPlugin extends Plugin 'author' => 'Evan Prodromou, Craig Andrews', 'homepage' => 'http://status.net/wiki/Plugin:OpenID', 'rawdescription' => + // TRANS: OpenID plugin description. _m('Use <a href="http://openid.net/">OpenID</a> to login to the site.')); return true; } diff --git a/plugins/OpenID/extlib/README b/plugins/OpenID/extlib/README new file mode 100644 index 000000000..1fe80d79b --- /dev/null +++ b/plugins/OpenID/extlib/README @@ -0,0 +1,6 @@ +team-extension.php + Support for Launchpad's OpenID Teams extension + Maintainer: Canonical + Source: https://code.edge.launchpad.net/wordpress-teams-integration + r27 2010-04-27 + License: AGPLv3 diff --git a/plugins/OpenID/extlib/teams-extension.php b/plugins/OpenID/extlib/teams-extension.php new file mode 100644 index 000000000..451f2fb19 --- /dev/null +++ b/plugins/OpenID/extlib/teams-extension.php @@ -0,0 +1,175 @@ +<?php +/* + * Wordpress Teams plugin + * Copyright (C) 2009-2010 Canonical Ltd. + * + * 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/>. + */ + +/** + * Provides an example OpenID extension to query user team/group membership + * + * This code is based on code supplied with the openid library for simple + * registration data. + */ + +/** + * Require the Message implementation. + */ +require_once 'Auth/OpenID/Message.php'; +require_once 'Auth/OpenID/Extension.php'; + +/** + * The team/group extension base class + */ +class Auth_OpenID_TeamsExtension extends Auth_OpenID_Extension { + var $ns_uri = 'http://ns.launchpad.net/2007/openid-teams'; + var $ns_alias = 'lp'; + var $request_field = 'query_membership'; + var $response_field = 'is_member'; + + /** + * Get the string arguments that should be added to an OpenID + * message for this extension. + */ + function getExtensionArgs() { + $args = array(); + + if ($this->_teams) { + $args[$this->request_field] = implode(',', $this->_teams); + } + + return $args; + } + + /** + * Add the arguments from this extension to the provided message. + * + * Returns the message with the extension arguments added. + */ + function toMessage(&$message) { + if ($message->namespaces->addAlias($this->ns_uri, $this->ns_alias) === null) { + if ($message->namespaces->getAlias($this->ns_uri) != $this->ns_alias) { + return null; + } + } + + $message->updateArgs($this->ns_uri, $this->getExtensionArgs()); + return $message; + } + + /** + * Extract the team/group namespace URI from the given OpenID message. + * Handles OpenID 1 and 2. + * + * $message: The OpenID message from which to parse team/group data. + * This may be a request or response message. + * + * Returns the sreg namespace URI for the supplied message. + * + * @access private + */ + function _getExtensionNS(&$message) { + $alias = null; + $found_ns_uri = null; + + // See if there exists an alias for the namespace + $alias = $message->namespaces->getAlias($this->ns_uri); + + if ($alias !== null) { + $found_ns_uri = $this->ns_uri; + } + + if ($alias === null) { + // There is no alias for this extension, so try to add one. + $found_ns_uri = Auth_OpenID_TYPE_1_0; + + if ($message->namespaces->addAlias($this->ns_uri, $this->ns_alias) === null) { + // An alias for the string 'lp' already exists, but + // it's defined for something other than team/group membership + return null; + } + } + + return $found_ns_uri; + } +} + +/** + * The team/group extension request class + */ +class Auth_OpenID_TeamsRequest extends Auth_OpenID_TeamsExtension { + function __init($teams) { + if (!is_array($teams)) { + if (!empty($teams)) { + $teams = explode(',', $teams); + } else { + $teams = Array(); + } + } + + $this->_teams = $teams; + } + + function Auth_OpenID_TeamsRequest($teams) { + $this->__init($teams); + } +} + +/** + * The team/group extension response class + */ +class Auth_OpenID_TeamsResponse extends Auth_OpenID_TeamsExtension { + var $_teams = array(); + + function __init(&$resp, $signed_only=true) { + $this->ns_uri = $this->_getExtensionNS($resp->message); + + if ($signed_only) { + $args = $resp->getSignedNS($this->ns_uri); + } else { + $args = $resp->message->getArgs($this->ns_uri); + } + + if ($args === null) { + return null; + } + + // An OpenID 2.0 response will handle the namespaces + if (in_array($this->response_field, array_keys($args)) && !empty($args[$this->response_field])) { + $this->_teams = explode(',', $args[$this->response_field]); + } + + // Piggybacking on a 1.x request, however, won't so the field name will + // be different + elseif (in_array($this->ns_alias.'.'.$this->response_field, array_keys($args)) && !empty($args[$this->ns_alias.'.'.$this->response_field])) { + $this->_teams = explode(',', $args[$this->ns_alias.'.'.$this->response_field]); + } + } + + function Auth_OpenID_TeamsResponse(&$resp, $signed_only=true) { + $this->__init($resp, $signed_only); + } + + /** + * Get the array of teams the user is a member of + * + * @return array + */ + function getTeams() { + return $this->_teams; + } +} + +?> diff --git a/plugins/OpenID/finishaddopenid.php b/plugins/OpenID/finishaddopenid.php index 991e6584e..47b3f7fb1 100644 --- a/plugins/OpenID/finishaddopenid.php +++ b/plugins/OpenID/finishaddopenid.php @@ -64,6 +64,7 @@ class FinishaddopenidAction extends Action { parent::handle($args); if (!common_logged_in()) { + // TRANS: Client error message $this->clientError(_m('Not logged in.')); } else { $this->tryLogin(); @@ -85,10 +86,12 @@ class FinishaddopenidAction extends Action $response = $consumer->complete(common_local_url('finishaddopenid')); if ($response->status == Auth_OpenID_CANCEL) { + // TRANS: Status message in case the response from the OpenID provider is that the logon attempt was cancelled. $this->message(_m('OpenID authentication cancelled.')); return; } else if ($response->status == Auth_OpenID_FAILURE) { - // Authentication failed; display the error message. + // TRANS: OpenID authentication failed; display the error message. + // TRANS: %s is the error message. $this->message(sprintf(_m('OpenID authentication failed: %s'), $response->message)); } else if ($response->status == Auth_OpenID_SUCCESS) { @@ -103,14 +106,22 @@ class FinishaddopenidAction extends Action $sreg = $sreg_resp->contents(); } + // Launchpad teams extension + if (!oid_check_teams($response)) { + $this->message(_m('OpenID authentication aborted: you are not allowed to login to this site.')); + return; + } + $cur = common_current_user(); $other = oid_get_user($canonical); if ($other) { if ($other->id == $cur->id) { + // TRANS: message in case a user tries to add an OpenID that is already connected to them. $this->message(_m('You already have this OpenID!')); } else { + // TRANS: message in case a user tries to add an OpenID that is already used by another user. $this->message(_m('Someone else already has this OpenID.')); } return; @@ -123,15 +134,20 @@ class FinishaddopenidAction extends Action $result = oid_link_user($cur->id, $canonical, $display); if (!$result) { + // TRANS: message in case the OpenID object cannot be connected to the user. $this->message(_m('Error connecting user.')); return; } - if ($sreg) { - if (!oid_update_user($cur, $sreg)) { - $this->message(_m('Error updating profile')); - return; + if (Event::handle('StartOpenIDUpdateUser', array($cur, $canonical, &$sreg))) { + if ($sreg) { + if (!oid_update_user($cur, $sreg)) { + // TRANS: message in case the user or the user profile cannot be saved in StatusNet. + $this->message(_m('Error updating profile')); + return; + } } } + Event::handle('EndOpenIDUpdateUser', array($cur, $canonical, $sreg)); // success! @@ -167,6 +183,7 @@ class FinishaddopenidAction extends Action function title() { + // TRANS: Title after getting the status of the OpenID authorisation request. return _m('OpenID Login'); } diff --git a/plugins/OpenID/finishopenidlogin.php b/plugins/OpenID/finishopenidlogin.php index 438a728d8..0c03b5c4d 100644 --- a/plugins/OpenID/finishopenidlogin.php +++ b/plugins/OpenID/finishopenidlogin.php @@ -31,15 +31,18 @@ class FinishopenidloginAction extends Action { parent::handle($args); if (common_is_real_login()) { + // TRANS: Client error message trying to log on with OpenID while already logged on. $this->clientError(_m('Already logged in.')); } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Message given when there is a problem with the user's session token. $this->showForm(_m('There was a problem with your session token. Try again, please.')); return; } if ($this->arg('create')) { if (!$this->boolean('license')) { + // TRANS: Message given if user does not agree with the site's license. $this->showForm(_m('You can\'t register if you don\'t agree to the license.'), $this->trimmed('newname')); return; @@ -48,8 +51,8 @@ class FinishopenidloginAction extends Action } else if ($this->arg('connect')) { $this->connectUser(); } else { - common_debug(print_r($this->args, true), __FILE__); - $this->showForm(_m('Something weird happened.'), + // TRANS: Messag given on an unknown error. + $this->showForm(_m('An unknown error has occured.'), $this->trimmed('newname')); } } else { @@ -63,12 +66,15 @@ class FinishopenidloginAction extends Action $this->element('div', array('class' => 'error'), $this->error); } else { $this->element('div', 'instructions', + // TRANS: Instructions given after a first successful logon using OpenID. + // TRANS: %s is the site name. sprintf(_m('This is the first time you\'ve logged into %s so we must connect your OpenID to a local account. You can either create a new account, or connect with your existing account, if you have one.'), common_config('site', 'name'))); } } function title() { + // TRANS: Title return _m('OpenID Account Setup'); } @@ -80,6 +86,11 @@ class FinishopenidloginAction extends Action $this->showPage(); } + /** + * @fixme much of this duplicates core code, which is very fragile. + * Should probably be replaced with an extensible mini version of + * the core registration form. + */ function showContent() { if (!empty($this->message_text)) { @@ -111,30 +122,43 @@ class FinishopenidloginAction extends Action 'value' => 'true')); $this->elementStart('label', array('for' => 'license', 'class' => 'checkbox')); - $this->text(_m('My text and files are available under ')); - $this->element('a', array('href' => common_config('license', 'url')), - common_config('license', 'title')); - $this->text(_m(' except this private data: password, email address, IM address, phone number.')); + // TRANS: OpenID plugin link text. + // TRANS: %s is a link to a licese with the license name as link text. + $message = _('My text and files are available under %s ' . + 'except this private data: password, ' . + 'email address, IM address, and phone number.'); + $link = '<a href="' . + htmlspecialchars(common_config('license', 'url')) . + '">' . + htmlspecialchars(common_config('license', 'title')) . + '</a>'; + $this->raw(sprintf(htmlspecialchars($message), $link)); $this->elementEnd('label'); $this->elementEnd('li'); $this->elementEnd('ul'); - $this->submit('create', _m('Create')); + // TRANS: Button label in form in which to create a new user on the site for an OpenID. + $this->submit('create', _m('BUTTON', 'Create')); $this->elementEnd('fieldset'); $this->elementStart('fieldset', array('id' => 'form_openid_createaccount')); $this->element('legend', null, + // TRANS: Used as form legend for form in which to connect an OpenID to an existing user on the site. _m('Connect existing account')); $this->element('p', null, + // TRANS: User instructions for form in which to connect an OpenID to an existing user on the site. _m('If you already have an account, login with your username and password to connect it to your OpenID.')); $this->elementStart('ul', 'form_data'); $this->elementStart('li'); + // TRANS: Field label in form in which to connect an OpenID to an existing user on the site. $this->input('nickname', _m('Existing nickname')); $this->elementEnd('li'); $this->elementStart('li'); + // TRANS: Field label in form in which to connect an OpenID to an existing user on the site. $this->password('password', _m('Password')); $this->elementEnd('li'); $this->elementEnd('ul'); - $this->submit('connect', _m('Connect')); + // TRANS: Button label in form in which to connect an OpenID to an existing user on the site. + $this->submit('connect', _m('BUTTON', 'Connect')); $this->elementEnd('fieldset'); $this->elementEnd('form'); } @@ -146,10 +170,11 @@ class FinishopenidloginAction extends Action $response = $consumer->complete(common_local_url('finishopenidlogin')); if ($response->status == Auth_OpenID_CANCEL) { + // TRANS: Status message in case the response from the OpenID provider is that the logon attempt was cancelled. $this->message(_m('OpenID authentication cancelled.')); return; } else if ($response->status == Auth_OpenID_FAILURE) { - // Authentication failed; display the error message. + // TRANS: OpenID authentication failed; display the error message. %s is the error message. $this->message(sprintf(_m('OpenID authentication failed: %s'), $response->message)); } else if ($response->status == Auth_OpenID_SUCCESS) { // This means the authentication succeeded; extract the @@ -159,12 +184,21 @@ class FinishopenidloginAction extends Action $canonical = ($response->endpoint->canonicalID) ? $response->endpoint->canonicalID : $response->getDisplayIdentifier(); + oid_assert_allowed($display); + oid_assert_allowed($canonical); + $sreg_resp = Auth_OpenID_SRegResponse::fromSuccessResponse($response); if ($sreg_resp) { $sreg = $sreg_resp->contents(); } + // Launchpad teams extension + if (!oid_check_teams($response)) { + $this->message(_m('OpenID authentication aborted: you are not allowed to login to this site.')); + return; + } + $user = oid_get_user($canonical); if ($user) { @@ -212,6 +246,7 @@ class FinishopenidloginAction extends Action # FIXME: save invite code before redirect, and check here if (common_config('site', 'closed')) { + // TRANS: OpenID plugin message. No new user registration is allowed on the site. $this->clientError(_m('Registration not allowed.')); return; } @@ -221,6 +256,7 @@ class FinishopenidloginAction extends Action if (common_config('site', 'inviteonly')) { $code = $_SESSION['invitecode']; if (empty($code)) { + // TRANS: OpenID plugin message. No new user registration is allowed on the site without an invitation code, and none was provided. $this->clientError(_m('Registration not allowed.')); return; } @@ -228,6 +264,7 @@ class FinishopenidloginAction extends Action $invite = Invitation::staticGet($code); if (empty($invite)) { + // TRANS: OpenID plugin message. No new user registration is allowed on the site without an invitation code, and the one provided was not valid. $this->clientError(_m('Not a valid invitation code.')); return; } @@ -238,16 +275,19 @@ class FinishopenidloginAction extends Action if (!Validate::string($nickname, array('min_length' => 1, 'max_length' => 64, 'format' => NICKNAME_FMT))) { + // TRANS: OpenID plugin message. The entered new user name did not conform to the requirements. $this->showForm(_m('Nickname must have only lowercase letters and numbers and no spaces.')); return; } if (!User::allowed_nickname($nickname)) { + // TRANS: OpenID plugin message. The entered new user name is blacklisted. $this->showForm(_m('Nickname not allowed.')); return; } if (User::staticGet('nickname', $nickname)) { + // TRANS: OpenID plugin message. The entered new user name is already used. $this->showForm(_m('Nickname already in use. Try another one.')); return; } @@ -255,6 +295,7 @@ class FinishopenidloginAction extends Action list($display, $canonical, $sreg) = $this->getSavedValues(); if (!$display || !$canonical) { + // TRANS: OpenID plugin server error. A stored OpenID cannot be retrieved. $this->serverError(_m('Stored OpenID not found.')); return; } @@ -264,10 +305,13 @@ class FinishopenidloginAction extends Action $other = oid_get_user($canonical); if ($other) { + // TRANS: OpenID plugin server error. $this->serverError(_m('Creating new account for OpenID that already has a user.')); return; } + Event::handle('StartOpenIDCreateNewUser', array($canonical, &$sreg)); + $location = ''; if (!empty($sreg['country'])) { if ($sreg['postcode']) { @@ -307,6 +351,8 @@ class FinishopenidloginAction extends Action $result = oid_link_user($user->id, $canonical, $display); + Event::handle('EndOpenIDCreateNewUser', array($user, $canonical, $sreg)); + oid_set_last($display); common_set_user($user); common_real_login(true); @@ -324,6 +370,7 @@ class FinishopenidloginAction extends Action $password = $this->trimmed('password'); if (!common_check_user($nickname, $password)) { + // TRANS: OpenID plugin message. $this->showForm(_m('Invalid username or password.')); return; } @@ -335,6 +382,7 @@ class FinishopenidloginAction extends Action list($display, $canonical, $sreg) = $this->getSavedValues(); if (!$display || !$canonical) { + // TRANS: OpenID plugin server error. A stored OpenID cannot be found. $this->serverError(_m('Stored OpenID not found.')); return; } @@ -342,11 +390,16 @@ class FinishopenidloginAction extends Action $result = oid_link_user($user->id, $canonical, $display); if (!$result) { + // TRANS: OpenID plugin server error. The user or user profile could not be saved. $this->serverError(_m('Error connecting user to OpenID.')); return; } - oid_update_user($user, $sreg); + if (Event::handle('StartOpenIDUpdateUser', array($user, $canonical, &$sreg))) { + oid_update_user($user, $sreg); + } + Event::handle('EndOpenIDUpdateUser', array($user, $canonical, $sreg)); + oid_set_last($display); common_set_user($user); common_real_login(true); diff --git a/plugins/OpenID/locale/OpenID.po b/plugins/OpenID/locale/OpenID.pot index 7ed879835..70908422e 100644 --- a/plugins/OpenID/locale/OpenID.po +++ b/plugins/OpenID/locale/OpenID.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-03-01 14:58-0800\n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -16,311 +16,347 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: finishaddopenid.php:67 -msgid "Not logged in." +#: openidsettings.php:59 +msgid "OpenID settings" msgstr "" -#: finishaddopenid.php:88 finishopenidlogin.php:149 -msgid "OpenID authentication cancelled." +#: openidsettings.php:70 +#, php-format +msgid "" +"[OpenID](%%doc.openid%%) lets you log into many sites with the same user " +"account. Manage your associated OpenIDs from here." msgstr "" -#: finishaddopenid.php:92 finishopenidlogin.php:153 -#, php-format -msgid "OpenID authentication failed: %s" +#: openidsettings.php:99 +msgid "Add OpenID" msgstr "" -#: finishaddopenid.php:112 -msgid "You already have this OpenID!" +#: openidsettings.php:102 +msgid "" +"If you want to add an OpenID to your account, enter it in the box below and " +"click \"Add\"." msgstr "" -#: finishaddopenid.php:114 -msgid "Someone else already has this OpenID." +#: openidsettings.php:107 openidlogin.php:119 +msgid "OpenID URL" msgstr "" -#: finishaddopenid.php:126 -msgid "Error connecting user." +#: openidsettings.php:117 +msgid "Add" msgstr "" -#: finishaddopenid.php:131 -msgid "Error updating profile" +#: openidsettings.php:129 +msgid "Remove OpenID" msgstr "" -#: finishaddopenid.php:170 openidlogin.php:95 -msgid "OpenID Login" +#: openidsettings.php:134 +msgid "" +"Removing your only OpenID would make it impossible to log in! If you need to " +"remove it, add another OpenID first." msgstr "" -#: finishopenidlogin.php:34 openidlogin.php:30 -msgid "Already logged in." +#: openidsettings.php:149 +msgid "" +"You can remove an OpenID from your account by clicking the button marked " +"\"Remove\"." msgstr "" -#: finishopenidlogin.php:38 openidlogin.php:37 openidsettings.php:194 -msgid "There was a problem with your session token. Try again, please." +#: openidsettings.php:172 openidsettings.php:213 +msgid "Remove" msgstr "" -#: finishopenidlogin.php:43 -msgid "You can't register if you don't agree to the license." +#: openidsettings.php:186 +msgid "OpenID Trusted Sites" +msgstr "" + +#: openidsettings.php:189 +msgid "" +"The following sites are allowed to access your identity and log you in. You " +"can remove a site from this list to deny it access to your OpenID." msgstr "" -#: finishopenidlogin.php:52 openidsettings.php:208 +#: openidsettings.php:231 finishopenidlogin.php:38 openidlogin.php:39 +msgid "There was a problem with your session token. Try again, please." +msgstr "" + +#: openidsettings.php:247 finishopenidlogin.php:51 msgid "Something weird happened." msgstr "" -#: finishopenidlogin.php:66 -#, php-format -msgid "" -"This is the first time you've logged into %s so we must connect your OpenID " -"to a local account. You can either create a new account, or connect with " -"your existing account, if you have one." +#: openidsettings.php:271 +msgid "No such OpenID trustroot." msgstr "" -#: finishopenidlogin.php:72 -msgid "OpenID Account Setup" +#: openidsettings.php:275 +msgid "Trustroots removed" msgstr "" -#: finishopenidlogin.php:97 -msgid "Create new account" +#: openidsettings.php:298 +msgid "No such OpenID." msgstr "" -#: finishopenidlogin.php:99 -msgid "Create a new user with this nickname." +#: openidsettings.php:303 +msgid "That OpenID does not belong to you." msgstr "" -#: finishopenidlogin.php:102 -msgid "New nickname" +#: openidsettings.php:307 +msgid "OpenID removed." msgstr "" -#: finishopenidlogin.php:104 -msgid "1-64 lowercase letters or numbers, no punctuation or spaces" +#: openid.php:137 +msgid "Cannot instantiate OpenID consumer object." msgstr "" -#: finishopenidlogin.php:114 -msgid "My text and files are available under " +#: openid.php:147 +msgid "Not a valid OpenID." msgstr "" -#: finishopenidlogin.php:117 -msgid "" -" except this private data: password, email address, IM address, phone number." +#: openid.php:149 +#, php-format +msgid "OpenID failure: %s" msgstr "" -#: finishopenidlogin.php:121 -msgid "Create" +#: openid.php:176 +#, php-format +msgid "Could not redirect to server: %s" msgstr "" -#: finishopenidlogin.php:126 -msgid "Connect existing account" +#: openid.php:194 +#, php-format +msgid "Could not create OpenID form: %s" msgstr "" -#: finishopenidlogin.php:128 +#: openid.php:210 msgid "" -"If you already have an account, login with your username and password to " -"connect it to your OpenID." +"This form should automatically submit itself. If not, click the submit " +"button to go to your OpenID provider." msgstr "" -#: finishopenidlogin.php:131 -msgid "Existing nickname" +#: openid.php:242 +msgid "Error saving the profile." msgstr "" -#: finishopenidlogin.php:134 -msgid "Password" +#: openid.php:253 +msgid "Error saving the user." msgstr "" -#: finishopenidlogin.php:137 -msgid "Connect" +#: openid.php:282 +msgid "Unauthorized URL used for OpenID login." msgstr "" -#: finishopenidlogin.php:215 finishopenidlogin.php:224 -msgid "Registration not allowed." +#: openid.php:302 +msgid "OpenID Login Submission" msgstr "" -#: finishopenidlogin.php:231 -msgid "Not a valid invitation code." +#: openid.php:312 +msgid "Requesting authorization from your login provider..." msgstr "" -#: finishopenidlogin.php:241 -msgid "Nickname must have only lowercase letters and numbers and no spaces." +#: openid.php:315 +msgid "" +"If you are not redirected to your login provider in a few seconds, try " +"pushing the button below." msgstr "" -#: finishopenidlogin.php:246 -msgid "Nickname not allowed." +#. TRANS: Tooltip for main menu option "Login" +#: OpenIDPlugin.php:204 +msgctxt "TOOLTIP" +msgid "Login to the site" msgstr "" -#: finishopenidlogin.php:251 -msgid "Nickname already in use. Try another one." +#: OpenIDPlugin.php:207 +msgctxt "MENU" +msgid "Login" msgstr "" -#: finishopenidlogin.php:258 finishopenidlogin.php:338 -msgid "Stored OpenID not found." +#. TRANS: Tooltip for main menu option "Help" +#: OpenIDPlugin.php:212 +msgctxt "TOOLTIP" +msgid "Help me!" msgstr "" -#: finishopenidlogin.php:267 -msgid "Creating new account for OpenID that already has a user." +#: OpenIDPlugin.php:215 +msgctxt "MENU" +msgid "Help" msgstr "" -#: finishopenidlogin.php:327 -msgid "Invalid username or password." +#. TRANS: Tooltip for main menu option "Search" +#: OpenIDPlugin.php:221 +msgctxt "TOOLTIP" +msgid "Search for people or text" msgstr "" -#: finishopenidlogin.php:345 -msgid "Error connecting user to OpenID." +#: OpenIDPlugin.php:224 +msgctxt "MENU" +msgid "Search" msgstr "" -#: openid.php:141 -msgid "Cannot instantiate OpenID consumer object." +#: OpenIDPlugin.php:283 OpenIDPlugin.php:319 +msgid "OpenID" msgstr "" -#: openid.php:151 -msgid "Not a valid OpenID." +#: OpenIDPlugin.php:284 +msgid "Login or register with OpenID" msgstr "" -#: openid.php:153 -#, php-format -msgid "OpenID failure: %s" +#: OpenIDPlugin.php:320 +msgid "Add or remove OpenIDs" msgstr "" -#: openid.php:180 -#, php-format -msgid "Could not redirect to server: %s" +#: OpenIDPlugin.php:595 +msgid "Use <a href=\"http://openid.net/\">OpenID</a> to login to the site." msgstr "" -#: openid.php:198 +#: openidserver.php:106 #, php-format -msgid "Could not create OpenID form: %s" -msgstr "" - -#: openid.php:214 -msgid "" -"This form should automatically submit itself. If not, click the submit " -"button to go to your OpenID provider." +msgid "You are not authorized to use the identity %s." msgstr "" -#: openid.php:246 -msgid "Error saving the profile." +#: openidserver.php:126 +msgid "Just an OpenID provider. Nothing to see here, move along..." msgstr "" -#: openid.php:257 -msgid "Error saving the user." +#: finishopenidlogin.php:34 openidlogin.php:30 +msgid "Already logged in." msgstr "" -#: openid.php:277 -msgid "OpenID Auto-Submit" +#: finishopenidlogin.php:43 +msgid "You can't register if you don't agree to the license." msgstr "" -#: openidlogin.php:66 +#: finishopenidlogin.php:65 #, php-format msgid "" -"For security reasons, please re-login with your [OpenID](%%doc.openid%%) " -"before changing your settings." +"This is the first time you've logged into %s so we must connect your OpenID " +"to a local account. You can either create a new account, or connect with " +"your existing account, if you have one." msgstr "" -#: openidlogin.php:70 -#, php-format -msgid "Login with an [OpenID](%%doc.openid%%) account." +#: finishopenidlogin.php:71 +msgid "OpenID Account Setup" msgstr "" -#: openidlogin.php:112 -msgid "OpenID login" +#: finishopenidlogin.php:101 +msgid "Create new account" msgstr "" -#: openidlogin.php:117 openidsettings.php:107 -msgid "OpenID URL" +#: finishopenidlogin.php:103 +msgid "Create a new user with this nickname." msgstr "" -#: openidlogin.php:119 -msgid "Your OpenID URL" +#: finishopenidlogin.php:106 +msgid "New nickname" msgstr "" -#: openidlogin.php:122 -msgid "Remember me" +#: finishopenidlogin.php:108 +msgid "1-64 lowercase letters or numbers, no punctuation or spaces" msgstr "" -#: openidlogin.php:123 -msgid "Automatically login in the future; not for shared computers!" +#: finishopenidlogin.php:130 +msgid "Create" msgstr "" -#: openidlogin.php:127 -msgid "Login" +#: finishopenidlogin.php:135 +msgid "Connect existing account" msgstr "" -#: OpenIDPlugin.php:123 OpenIDPlugin.php:135 -msgid "OpenID" +#: finishopenidlogin.php:137 +msgid "" +"If you already have an account, login with your username and password to " +"connect it to your OpenID." msgstr "" -#: OpenIDPlugin.php:124 -msgid "Login or register with OpenID" +#: finishopenidlogin.php:140 +msgid "Existing nickname" msgstr "" -#: OpenIDPlugin.php:136 -msgid "Add or remove OpenIDs" +#: finishopenidlogin.php:143 +msgid "Password" msgstr "" -#: OpenIDPlugin.php:324 -msgid "Use <a href=\"http://openid.net/\">OpenID</a> to login to the site." +#: finishopenidlogin.php:146 +msgid "Connect" msgstr "" -#: openidserver.php:106 +#: finishopenidlogin.php:158 finishaddopenid.php:88 +msgid "OpenID authentication cancelled." +msgstr "" + +#: finishopenidlogin.php:162 finishaddopenid.php:92 #, php-format -msgid "You are not authorized to use the identity %s." +msgid "OpenID authentication failed: %s" msgstr "" -#: openidserver.php:126 -msgid "Just an OpenID provider. Nothing to see here, move along..." +#: finishopenidlogin.php:227 finishopenidlogin.php:236 +msgid "Registration not allowed." msgstr "" -#: openidsettings.php:59 -msgid "OpenID settings" +#: finishopenidlogin.php:243 +msgid "Not a valid invitation code." msgstr "" -#: openidsettings.php:70 -#, php-format -msgid "" -"[OpenID](%%doc.openid%%) lets you log into many sites with the same user " -"account. Manage your associated OpenIDs from here." +#: finishopenidlogin.php:253 +msgid "Nickname must have only lowercase letters and numbers and no spaces." msgstr "" -#: openidsettings.php:99 -msgid "Add OpenID" +#: finishopenidlogin.php:258 +msgid "Nickname not allowed." msgstr "" -#: openidsettings.php:102 -msgid "" -"If you want to add an OpenID to your account, enter it in the box below and " -"click \"Add\"." +#: finishopenidlogin.php:263 +msgid "Nickname already in use. Try another one." msgstr "" -#: openidsettings.php:117 -msgid "Add" +#: finishopenidlogin.php:270 finishopenidlogin.php:350 +msgid "Stored OpenID not found." msgstr "" -#: openidsettings.php:129 -msgid "Remove OpenID" +#: finishopenidlogin.php:279 +msgid "Creating new account for OpenID that already has a user." msgstr "" -#: openidsettings.php:134 -msgid "" -"Removing your only OpenID would make it impossible to log in! If you need to " -"remove it, add another OpenID first." +#: finishopenidlogin.php:339 +msgid "Invalid username or password." msgstr "" -#: openidsettings.php:149 +#: finishopenidlogin.php:357 +msgid "Error connecting user to OpenID." +msgstr "" + +#: openidlogin.php:68 +#, php-format msgid "" -"You can remove an OpenID from your account by clicking the button marked " -"\"Remove\"." +"For security reasons, please re-login with your [OpenID](%%doc.openid%%) " +"before changing your settings." msgstr "" -#: openidsettings.php:172 -msgid "Remove" +#: openidlogin.php:72 +#, php-format +msgid "Login with an [OpenID](%%doc.openid%%) account." msgstr "" -#: openidsettings.php:228 -msgid "No such OpenID." +#: openidlogin.php:97 finishaddopenid.php:170 +msgid "OpenID Login" msgstr "" -#: openidsettings.php:233 -msgid "That OpenID does not belong to you." +#: openidlogin.php:114 +msgid "OpenID login" msgstr "" -#: openidsettings.php:237 -msgid "OpenID removed." +#: openidlogin.php:121 +msgid "Your OpenID URL" +msgstr "" + +#: openidlogin.php:124 +msgid "Remember me" +msgstr "" + +#: openidlogin.php:125 +msgid "Automatically login in the future; not for shared computers!" +msgstr "" + +#: openidlogin.php:129 +msgid "Login" msgstr "" #: openidtrust.php:51 @@ -332,17 +368,37 @@ msgid "" "This page should only be reached during OpenID processing, not directly." msgstr "" -#: openidtrust.php:118 +#: openidtrust.php:117 #, php-format msgid "" "%s has asked to verify your identity. Click Continue to verify your " "identity and login without creating a new password." msgstr "" -#: openidtrust.php:136 +#: openidtrust.php:135 msgid "Continue" msgstr "" -#: openidtrust.php:137 +#: openidtrust.php:136 msgid "Cancel" msgstr "" + +#: finishaddopenid.php:67 +msgid "Not logged in." +msgstr "" + +#: finishaddopenid.php:112 +msgid "You already have this OpenID!" +msgstr "" + +#: finishaddopenid.php:114 +msgid "Someone else already has this OpenID." +msgstr "" + +#: finishaddopenid.php:126 +msgid "Error connecting user." +msgstr "" + +#: finishaddopenid.php:131 +msgid "Error updating profile" +msgstr "" diff --git a/plugins/OpenID/locale/nl/LC_MESSAGES/OpenID.po b/plugins/OpenID/locale/nl/LC_MESSAGES/OpenID.po new file mode 100644 index 000000000..5cda9b129 --- /dev/null +++ b/plugins/OpenID/locale/nl/LC_MESSAGES/OpenID.po @@ -0,0 +1,395 @@ +# Translation of StatusNet plugin OpenID to Dutch +# +# Author@translatewiki.net: Siebrand +# -- +# This file is distributed under the same license as the StatusNet package. +# +msgid "" +msgstr "" +"Project-Id-Version: StatusNet\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: 2010-04-30 02:16+0100\n" +"Last-Translator: Siebrand Mazeland <s.mazeland@xs4all.nl>\n" +"Language-Team: Dutch\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: openidsettings.php:59 +msgid "OpenID settings" +msgstr "OpenID-instellingen" + +#: openidsettings.php:70 +#, php-format +msgid "[OpenID](%%doc.openid%%) lets you log into many sites with the same user account. Manage your associated OpenIDs from here." +msgstr "Met [OpenID](%%doc.openid%%) kunt u aanmelden bij veel websites met dezelfde gebruiker. U kunt hier uw gekoppelde OpenID's beheren." + +#: openidsettings.php:99 +msgid "Add OpenID" +msgstr "OpenID toevoegen" + +#: openidsettings.php:102 +msgid "If you want to add an OpenID to your account, enter it in the box below and click \"Add\"." +msgstr "Als u een OpenID aan uw gebruiker wilt toevoegen, voer deze dan hieronder in en klik op \"Toevoegen\"." + +#: openidsettings.php:107 +#: openidlogin.php:119 +msgid "OpenID URL" +msgstr "OpenID-URL" + +#: openidsettings.php:117 +msgid "Add" +msgstr "Toevoegen" + +#: openidsettings.php:129 +msgid "Remove OpenID" +msgstr "OpenID verwijderen" + +#: openidsettings.php:134 +msgid "Removing your only OpenID would make it impossible to log in! If you need to remove it, add another OpenID first." +msgstr "Door uw enige OpenID te verwijderen zou het niet meer mogelijk zijn om aan te melden. Als u het wilt verwijderen, voeg dan eerst een andere OpenID toe." + +#: openidsettings.php:149 +msgid "You can remove an OpenID from your account by clicking the button marked \"Remove\"." +msgstr "U kunt een OpenID van uw gebruiker verwijderen door te klikken op de knop \"Verwijderen\"." + +#: openidsettings.php:172 +#: openidsettings.php:213 +msgid "Remove" +msgstr "Verwijderen" + +#: openidsettings.php:186 +msgid "OpenID Trusted Sites" +msgstr "Vertrouwde OpenID-sites" + +#: openidsettings.php:189 +msgid "The following sites are allowed to access your identity and log you in. You can remove a site from this list to deny it access to your OpenID." +msgstr "De volgende sites hebben toegang tot uw indentiteit en kunnen u aanmelden. U kunt een site verwijderen uit deze lijst zodat deze niet langer toegang heeft tot uw OpenID." + +#: openidsettings.php:231 +#: finishopenidlogin.php:38 +#: openidlogin.php:39 +msgid "There was a problem with your session token. Try again, please." +msgstr "Er was een probleem met uw sessietoken. Probeer het opnieuw." + +#: openidsettings.php:247 +#: finishopenidlogin.php:51 +msgid "Something weird happened." +msgstr "Er is iets vreemds gebeurd." + +#: openidsettings.php:271 +msgid "No such OpenID trustroot." +msgstr "Die OpenID trustroot bestaat niet." + +#: openidsettings.php:275 +msgid "Trustroots removed" +msgstr "De trustroots zijn verwijderd" + +#: openidsettings.php:298 +msgid "No such OpenID." +msgstr "De OpenID bestaat niet." + +#: openidsettings.php:303 +msgid "That OpenID does not belong to you." +msgstr "Die OpenID is niet van u." + +#: openidsettings.php:307 +msgid "OpenID removed." +msgstr "OpenID verwijderd." + +#: openid.php:137 +msgid "Cannot instantiate OpenID consumer object." +msgstr "Het was niet mogelijk een OpenID-object aan te maken." + +#: openid.php:147 +msgid "Not a valid OpenID." +msgstr "Geen geldige OpenID." + +#: openid.php:149 +#, php-format +msgid "OpenID failure: %s" +msgstr "OpenID-fout: %s" + +#: openid.php:176 +#, php-format +msgid "Could not redirect to server: %s" +msgstr "Het was niet mogelijk door te verwijzen naar de server: %s" + +#: openid.php:194 +#, php-format +msgid "Could not create OpenID form: %s" +msgstr "Het was niet mogelijk het OpenID-formulier aan te maken: %s" + +#: openid.php:210 +msgid "This form should automatically submit itself. If not, click the submit button to go to your OpenID provider." +msgstr "Dit formulier hoort zichzelf automatisch op te slaan. Als dat niet gebeurt, klik dan op de knop \"Aanmelden\" om naar uw OpenID-provider te gaan." + +#: openid.php:242 +msgid "Error saving the profile." +msgstr "Fout bij het opslaan van het profiel." + +#: openid.php:253 +msgid "Error saving the user." +msgstr "Fout bij het opslaan van de gebruiker." + +#: openid.php:282 +msgid "Unauthorized URL used for OpenID login." +msgstr "Ongeautoriseerde URL gebruikt voor aanmelden via OpenID" + +#: openid.php:302 +#, fuzzy +msgid "OpenID Login Submission" +msgstr "Aanmelden via OpenID" + +#: openid.php:312 +msgid "Requesting authorization from your login provider..." +msgstr "Bezig met het vragen van autorisatie van uw aanmeldprovider..." + +#: openid.php:315 +msgid "If you are not redirected to your login provider in a few seconds, try pushing the button below." +msgstr "Als u binnen een aantal seconden niet wordt doorverwezen naar uw aanmeldprovider, klik dan op de onderstaande knop." + +#. TRANS: Tooltip for main menu option "Login" +#: OpenIDPlugin.php:204 +msgctxt "TOOLTIP" +msgid "Login to the site" +msgstr "Aanmelden bij de site" + +#: OpenIDPlugin.php:207 +#, fuzzy +msgctxt "MENU" +msgid "Login" +msgstr "Aanmelden" + +#. TRANS: Tooltip for main menu option "Help" +#: OpenIDPlugin.php:212 +msgctxt "TOOLTIP" +msgid "Help me!" +msgstr "Help me" + +#: OpenIDPlugin.php:215 +msgctxt "MENU" +msgid "Help" +msgstr "Hulp" + +#. TRANS: Tooltip for main menu option "Search" +#: OpenIDPlugin.php:221 +msgctxt "TOOLTIP" +msgid "Search for people or text" +msgstr "Zoeken naar mensen of tekst" + +#: OpenIDPlugin.php:224 +msgctxt "MENU" +msgid "Search" +msgstr "Zoeken" + +#: OpenIDPlugin.php:283 +#: OpenIDPlugin.php:319 +msgid "OpenID" +msgstr "OpenID" + +#: OpenIDPlugin.php:284 +msgid "Login or register with OpenID" +msgstr "Aanmelden of registreren met OpenID" + +#: OpenIDPlugin.php:320 +msgid "Add or remove OpenIDs" +msgstr "OpenID's toevoegen of verwijderen" + +#: OpenIDPlugin.php:595 +msgid "Use <a href=\"http://openid.net/\">OpenID</a> to login to the site." +msgstr "Gebruik <a href=\"http://openid.net/\">OpenID</a> om aan te melden bij de site." + +#: openidserver.php:106 +#, php-format +msgid "You are not authorized to use the identity %s." +msgstr "U mag de identiteit %s niet gebruiken." + +#: openidserver.php:126 +msgid "Just an OpenID provider. Nothing to see here, move along..." +msgstr "Gewoon een OpenID-provider. Niets te zien hier..." + +#: finishopenidlogin.php:34 +#: openidlogin.php:30 +msgid "Already logged in." +msgstr "U bent al aangemeld." + +#: finishopenidlogin.php:43 +msgid "You can't register if you don't agree to the license." +msgstr "U kunt niet registreren als u niet akkoord gaat met de licentie." + +#: finishopenidlogin.php:65 +#, php-format +msgid "This is the first time you've logged into %s so we must connect your OpenID to a local account. You can either create a new account, or connect with your existing account, if you have one." +msgstr "Dit is de eerste keer dat u aameldt bij %s en uw OpenID moet gekoppeld worden aan uw lokale gebruiker. U kunt een nieuwe gebruiker aanmaken of koppelen met uw bestaande gebruiker als u die al hebt." + +#: finishopenidlogin.php:71 +msgid "OpenID Account Setup" +msgstr "Instellingen OpenID" + +#: finishopenidlogin.php:101 +msgid "Create new account" +msgstr "Nieuwe gebruiker aanmaken" + +#: finishopenidlogin.php:103 +msgid "Create a new user with this nickname." +msgstr "Nieuwe gebruiker met deze naam aanmaken." + +#: finishopenidlogin.php:106 +msgid "New nickname" +msgstr "Nieuwe gebruiker" + +#: finishopenidlogin.php:108 +msgid "1-64 lowercase letters or numbers, no punctuation or spaces" +msgstr "1-64 kleine letters of getallen; geen leestekens of spaties" + +#: finishopenidlogin.php:130 +msgid "Create" +msgstr "Aanmaken" + +#: finishopenidlogin.php:135 +msgid "Connect existing account" +msgstr "Koppelen met bestaande gebruiker" + +#: finishopenidlogin.php:137 +msgid "If you already have an account, login with your username and password to connect it to your OpenID." +msgstr "Als u al een gebruiker hebt, meld u dan aan met uw gebruikersnaam en wachtwoord om de gebruiker te koppelen met uw OpenID." + +#: finishopenidlogin.php:140 +msgid "Existing nickname" +msgstr "Bestaande gebruiker" + +#: finishopenidlogin.php:143 +msgid "Password" +msgstr "Wachtwoord" + +#: finishopenidlogin.php:146 +msgid "Connect" +msgstr "Koppelen" + +#: finishopenidlogin.php:158 +#: finishaddopenid.php:88 +msgid "OpenID authentication cancelled." +msgstr "De authenticatie via OpenID is afgebroken." + +#: finishopenidlogin.php:162 +#: finishaddopenid.php:92 +#, php-format +msgid "OpenID authentication failed: %s" +msgstr "De authenticatie via OpenID is mislukt: %s" + +#: finishopenidlogin.php:227 +#: finishopenidlogin.php:236 +msgid "Registration not allowed." +msgstr "Registreren is niet mogelijk." + +#: finishopenidlogin.php:243 +msgid "Not a valid invitation code." +msgstr "De uitnodigingscode is niet geldig." + +#: finishopenidlogin.php:253 +msgid "Nickname must have only lowercase letters and numbers and no spaces." +msgstr "De gebruikersnaam mag alleen uit kleine letters en cijfers bestaan, en geen spaties bevatten." + +#: finishopenidlogin.php:258 +msgid "Nickname not allowed." +msgstr "Deze gebruikersnaam is niet toegestaan." + +#: finishopenidlogin.php:263 +msgid "Nickname already in use. Try another one." +msgstr "Deze gebruikersnaam wordt al gebruikt. Kies een andere." + +#: finishopenidlogin.php:270 +#: finishopenidlogin.php:350 +msgid "Stored OpenID not found." +msgstr "Het opgeslagen OpenID is niet aangetroffen." + +#: finishopenidlogin.php:279 +msgid "Creating new account for OpenID that already has a user." +msgstr "Bezig met het aanmaken van een gebruiker voor OpenID die al een gebruiker heeft." + +#: finishopenidlogin.php:339 +msgid "Invalid username or password." +msgstr "Ongeldige gebruikersnaam of wachtwoord." + +#: finishopenidlogin.php:357 +msgid "Error connecting user to OpenID." +msgstr "Fout bij het koppelen met OpenID." + +#: openidlogin.php:68 +#, php-format +msgid "For security reasons, please re-login with your [OpenID](%%doc.openid%%) before changing your settings." +msgstr "Om veiligheidsreden moet u opnieuw aanmelden met uw [OpenID](%%doc.openid%%) voordat u uw instellingen kunt wijzigen." + +#: openidlogin.php:72 +#, php-format +msgid "Login with an [OpenID](%%doc.openid%%) account." +msgstr "Aanmelden met een [OpenID](%%doc.openid%%)-gebruiker." + +#: openidlogin.php:97 +#: finishaddopenid.php:170 +msgid "OpenID Login" +msgstr "Aanmelden via OpenID" + +#: openidlogin.php:114 +msgid "OpenID login" +msgstr "Aanmelden via OpenID" + +#: openidlogin.php:121 +msgid "Your OpenID URL" +msgstr "Uw OpenID-URL" + +#: openidlogin.php:124 +msgid "Remember me" +msgstr "Aanmeldgegevens onthouden" + +#: openidlogin.php:125 +msgid "Automatically login in the future; not for shared computers!" +msgstr "In het vervolg automatisch aanmelden. Niet gebruiken op gedeelde computers!" + +#: openidlogin.php:129 +msgid "Login" +msgstr "Aanmelden" + +#: openidtrust.php:51 +msgid "OpenID Identity Verification" +msgstr "OpenID-identiteitscontrole" + +#: openidtrust.php:69 +msgid "This page should only be reached during OpenID processing, not directly." +msgstr "Deze pagina hoort alleen bezocht te worden tijdens het verwerken van een OpenID, en niet direct." + +#: openidtrust.php:117 +#, php-format +msgid "%s has asked to verify your identity. Click Continue to verify your identity and login without creating a new password." +msgstr "%s heeft gevraagd uw identiteit te bevestigen. Klik op \"Doorgaan\" om uw indentiteit te controleren en aan te melden zonder een wachtwoord te hoeven invoeren." + +#: openidtrust.php:135 +msgid "Continue" +msgstr "Doorgaan" + +#: openidtrust.php:136 +msgid "Cancel" +msgstr "Annuleren" + +#: finishaddopenid.php:67 +msgid "Not logged in." +msgstr "Niet aangemeld." + +#: finishaddopenid.php:112 +msgid "You already have this OpenID!" +msgstr "U hebt deze OpenID al!" + +#: finishaddopenid.php:114 +msgid "Someone else already has this OpenID." +msgstr "Iemand anders gebruikt deze OpenID al." + +#: finishaddopenid.php:126 +msgid "Error connecting user." +msgstr "Fout bij het verbinden met de gebruiker." + +#: finishaddopenid.php:131 +msgid "Error updating profile" +msgstr "Fout bij het bijwerken van het profiel." diff --git a/plugins/OpenID/openid.php b/plugins/OpenID/openid.php index 9e02c7a88..4ce350f77 100644 --- a/plugins/OpenID/openid.php +++ b/plugins/OpenID/openid.php @@ -94,7 +94,6 @@ function oid_link_user($id, $canonical, $display) if (!$oid->insert()) { $err = PEAR::getStaticProperty('DB_DataObject','lastError'); - common_debug('DB error ' . $err->code . ': ' . $err->message, __FILE__); return false; } @@ -119,13 +118,10 @@ function oid_check_immediate($openid_url, $backto=null) 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', @@ -138,6 +134,7 @@ function oid_authenticate($openid_url, $returnto, $immediate=false) $consumer = oid_consumer(); if (!$consumer) { + // TRANS: OpenID plugin server error. common_server_error(_m('Cannot instantiate OpenID consumer object.')); return false; } @@ -148,8 +145,13 @@ function oid_authenticate($openid_url, $returnto, $immediate=false) // Handle failure status return values. if (!$auth_request) { + common_log(LOG_ERR, __METHOD__ . ": mystery fail contacting $openid_url"); + // TRANS: OpenID plugin message. Given when an OpenID is not valid. return _m('Not a valid OpenID.'); } else if (Auth_OpenID::isFailure($auth_request)) { + common_log(LOG_ERR, __METHOD__ . ": OpenID fail to $openid_url: $auth_request->message"); + // TRANS: OpenID plugin server error. Given when the OpenID authentication request fails. + // TRANS: %s is the failure message. return sprintf(_m('OpenID failure: %s'), $auth_request->message); } @@ -168,6 +170,15 @@ function oid_authenticate($openid_url, $returnto, $immediate=false) $auth_request->addExtension($sreg_request); } + $requiredTeam = common_config('openid', 'required_team'); + if ($requiredTeam) { + // LaunchPad OpenID extension + $team_request = new Auth_OpenID_TeamsRequest(array($requiredTeam)); + if ($team_request) { + $auth_request->addExtension($team_request); + } + } + $trust_root = common_root_url(true); $process_url = common_local_url($returnto); @@ -177,6 +188,8 @@ function oid_authenticate($openid_url, $returnto, $immediate=false) $immediate); if (!$redirect_url) { } else if (Auth_OpenID::isFailure($redirect_url)) { + // TRANS: OpenID plugin server error. Given when the OpenID authentication request cannot be redirected. + // TRANS: %s is the failure message. return sprintf(_m('Could not redirect to server: %s'), $redirect_url->message); } else { common_redirect($redirect_url, 303); @@ -195,6 +208,8 @@ function oid_authenticate($openid_url, $returnto, $immediate=false) // Display an error if the form markup couldn't be generated; // otherwise, render the HTML. if (Auth_OpenID::isFailure($form_html)) { + // TRANS: OpenID plugin server error if the form markup could not be generated. + // TRANS: %s is the failure message. common_server_error(sprintf(_m('Could not create OpenID form: %s'), $form_html->message)); } else { $action = new AutosubmitAction(); // see below @@ -211,16 +226,20 @@ function oid_authenticate($openid_url, $returnto, $immediate=false) function _oid_print_instructions() { common_element('div', 'instructions', + // TRANS: OpenID plugin user instructions. _m('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) +/** + * Update a user from sreg parameters + * @param User $user + * @param array $sreg fields from OpenID sreg response + * @access private + */ +function oid_update_user($user, $sreg) { - $profile = $user->getProfile(); $orig_profile = clone($profile); @@ -243,6 +262,7 @@ function oid_update_user(&$user, &$sreg) # XXX save timezone if it's passed if (!$profile->update($orig_profile)) { + // TRANS: OpenID plugin server error. common_server_error(_m('Error saving the profile.')); return false; } @@ -254,6 +274,7 @@ function oid_update_user(&$user, &$sreg) } if (!$user->update($orig_user)) { + // TRANS: OpenID plugin server error. common_server_error(_m('Error saving the user.')); return false; } @@ -261,6 +282,63 @@ function oid_update_user(&$user, &$sreg) return true; } +function oid_assert_allowed($url) +{ + $blacklist = common_config('openid', 'blacklist'); + $whitelist = common_config('openid', 'whitelist'); + + if (empty($blacklist)) { + $blacklist = array(); + } + + if (empty($whitelist)) { + $whitelist = array(); + } + + foreach ($blacklist as $pattern) { + if (preg_match("/$pattern/", $url)) { + common_log(LOG_INFO, "Matched OpenID blacklist pattern {$pattern} with {$url}"); + foreach ($whitelist as $exception) { + if (preg_match("/$exception/", $url)) { + common_log(LOG_INFO, "Matched OpenID whitelist pattern {$exception} with {$url}"); + return; + } + } + // TRANS: OpenID plugin client exception (403). + throw new ClientException(_m("Unauthorized URL used for OpenID login."), 403); + } + } + + return; +} + +/** + * Check the teams available in the given OpenID response + * Using Launchpad's OpenID teams extension + * + * @return boolean whether this user is acceptable + */ +function oid_check_teams($response) +{ + $requiredTeam = common_config('openid', 'required_team'); + if ($requiredTeam) { + $team_resp = new Auth_OpenID_TeamsResponse($response); + if ($team_resp) { + $teams = $team_resp->getTeams(); + } else { + $teams = array(); + } + + $match = in_array($requiredTeam, $teams); + $is = $match ? 'is' : 'is not'; + common_log(LOG_DEBUG, "Remote user $is in required team $requiredTeam: [" . implode(', ', $teams) . "]"); + + return $match; + } + + return true; +} + class AutosubmitAction extends Action { var $form_html = null; @@ -274,20 +352,31 @@ class AutosubmitAction extends Action function title() { - return _m('OpenID Auto-Submit'); + // TRANS: Title + return _m('OpenID Login Submission'); } function showContent() { + $this->raw('<p style="margin: 20px 80px">'); + // @fixme this would be better using standard CSS class, but the present theme's a bit scary. + $this->element('img', array('src' => Theme::path('images/icons/icon_processing.gif', 'base'), + // for some reason the base CSS sets <img>s as block display?! + 'style' => 'display: inline')); + // TRANS: OpenID plugin message used while requesting authorization user's OpenID login provider. + $this->text(_m('Requesting authorization from your login provider...')); + $this->raw('</p>'); + $this->raw('<p style="margin-top: 60px; font-style: italic">'); + // TRANS: OpenID plugin message. User instruction while requesting authorization user's OpenID login provider. + $this->text(_m('If you are not redirected to your login provider in a few seconds, try pushing the button below.')); + $this->raw('</p>'); $this->raw($this->form_html); } - + function showScripts() { parent::showScripts(); $this->element('script', null, - '$(document).ready(function() { ' . - ' $(\'#'. $this->form_id .'\').submit(); '. - '});'); + 'document.getElementById(\'' . $this->form_id . '\').submit();'); } } diff --git a/plugins/OpenID/openidadminpanel.php b/plugins/OpenID/openidadminpanel.php new file mode 100644 index 000000000..ce4806cc8 --- /dev/null +++ b/plugins/OpenID/openidadminpanel.php @@ -0,0 +1,280 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * OpenID bridge administration panel + * + * 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 Settings + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Administer global OpenID settings + * + * @category Admin + * @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 OpenidadminpanelAction extends AdminPanelAction +{ + /** + * Returns the page title + * + * @return string page title + */ + + function title() + { + return _m('OpenID'); + } + + /** + * Instructions for using this form. + * + * @return string instructions + */ + + function getInstructions() + { + return _m('OpenID settings'); + } + + /** + * Show the OpenID admin panel form + * + * @return void + */ + + function showForm() + { + $form = new OpenIDAdminPanelForm($this); + $form->show(); + return; + } + + /** + * Save settings from the form + * + * @return void + */ + + function saveSettings() + { + static $settings = array( + 'openid' => array('trusted_provider', 'required_team') + ); + + static $booleans = array( + 'openid' => array('append_username'), + 'site' => array('openidonly') + ); + + $values = array(); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + $values[$section][$setting] + = $this->trimmed($setting); + } + } + + foreach ($booleans as $section => $parts) { + foreach ($parts as $setting) { + $values[$section][$setting] + = ($this->boolean($setting)) ? 1 : 0; + } + } + + // This throws an exception on validation errors + + $this->validate($values); + + // assert(all values are valid); + + $config = new Config(); + + $config->query('BEGIN'); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + Config::save($section, $setting, $values[$section][$setting]); + } + } + + foreach ($booleans as $section => $parts) { + foreach ($parts as $setting) { + Config::save($section, $setting, $values[$section][$setting]); + } + } + + $config->query('COMMIT'); + + return; + } + + function validate(&$values) + { + // Validate consumer key and secret (can't be too long) + + if (mb_strlen($values['openid']['trusted_provider']) > 255) { + $this->clientError( + _m("Invalid provider URL. Max length is 255 characters.") + ); + } + + if (mb_strlen($values['openid']['required_team']) > 255) { + $this->clientError( + _m("Invalid team name. Max length is 255 characters.") + ); + } + } +} + +class OpenIDAdminPanelForm extends AdminForm +{ + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return 'openidadminpanel'; + } + + /** + * class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'form_settings'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('openidadminpanel'); + } + + /** + * Data elements of the form + * + * @return void + * + * @todo Some of the options could prevent users from logging in again. + * Make sure that the acting administrator has a valid OpenID matching, + * or more carefully warn folks. + */ + + function formData() + { + $this->out->elementStart( + 'fieldset', + array('id' => 'settings_openid') + ); + $this->out->element('legend', null, _m('Trusted provider')); + $this->out->element('p', 'form_guide', + _m('By default, users are allowed to authenticate with any OpenID provider. ' . + 'If you are using your own OpenID service for shared sign-in, ' . + 'you can restrict access to only your own users here.')); + $this->out->elementStart('ul', 'form_data'); + + $this->li(); + $this->input( + 'trusted_provider', + _m('Provider URL'), + _m('All OpenID logins will be sent to this URL; other providers may not be used.'), + 'openid' + ); + $this->unli(); + + $this->li(); + $this->out->checkbox( + 'append_username', _m('Append a username to base URL'), + (bool) $this->value('append_username', 'openid'), + _m('Login form will show the base URL and prompt for a username to add at the end. Use when OpenID provider URL should be the profile page for individual users.'), + 'true' + ); + $this->unli(); + + $this->li(); + $this->input( + 'required_team', + _m('Required team'), + _m('Only allow logins from users in the given team (Launchpad extension).'), + 'openid' + ); + $this->unli(); + + $this->out->elementEnd('ul'); + $this->out->elementEnd('fieldset'); + + $this->out->elementStart( + 'fieldset', + array('id' => 'settings_openid-options') + ); + $this->out->element('legend', null, _m('Options')); + + $this->out->elementStart('ul', 'form_data'); + + $this->li(); + + $this->out->checkbox( + 'openidonly', _m('Enable OpenID-only mode'), + (bool) $this->value('openidonly', 'site'), + _m('Require all users to login via OpenID. WARNING: disables password authentication for all users!'), + 'true' + ); + $this->unli(); + + $this->out->elementEnd('ul'); + + $this->out->elementEnd('fieldset'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit('submit', _('Save'), 'submit', null, _m('Save OpenID settings')); + } +} diff --git a/plugins/OpenID/openidlogin.php b/plugins/OpenID/openidlogin.php index 9ba55911c..20d6e070c 100644 --- a/plugins/OpenID/openidlogin.php +++ b/plugins/OpenID/openidlogin.php @@ -27,13 +27,25 @@ class OpenidloginAction extends Action { parent::handle($args); if (common_is_real_login()) { + // TRANS: Client error message trying to log on with OpenID while already logged on. $this->clientError(_m('Already logged in.')); } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $openid_url = $this->trimmed('openid_url'); + $provider = common_config('openid', 'trusted_provider'); + if ($provider) { + $openid_url = $provider; + if (common_config('openid', 'append_username')) { + $openid_url .= $this->trimmed('openid_username'); + } + } else { + $openid_url = $this->trimmed('openid_url'); + } + + oid_assert_allowed($openid_url); # CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Message given when there is a problem with the user's session token. $this->showForm(_m('There was a problem with your session token. Try again, please.'), $openid_url); return; } @@ -63,10 +75,14 @@ class OpenidloginAction extends Action common_get_returnto()) { // rememberme logins have to reauthenticate before // changing any profile settings (cookie-stealing protection) + // TRANS: OpenID plugin message. Rememberme logins have to reauthenticate before changing any profile settings. + // TRANS: "OpenID" is the display text for a link with URL "(%%doc.openid%%)". return _m('For security reasons, please re-login with your ' . '[OpenID](%%doc.openid%%) ' . 'before changing your settings.'); } else { + // TRANS: OpenID plugin message. + // TRANS: "OpenID" is the display text for a link with URL "(%%doc.openid%%)". return _m('Login with an [OpenID](%%doc.openid%%) account.'); } } @@ -87,11 +103,20 @@ class OpenidloginAction extends Action function showScripts() { parent::showScripts(); - $this->autofocus('openid_url'); + if (common_config('openid', 'trusted_provider')) { + if (common_config('openid', 'append_username')) { + $this->autofocus('openid_username'); + } else { + $this->autofocus('rememberme'); + } + } else { + $this->autofocus('openid_url'); + } } function title() { + // TRANS: OpenID plugin message. Title. return _m('OpenID Login'); } @@ -109,22 +134,44 @@ class OpenidloginAction extends Action 'class' => 'form_settings', 'action' => $formaction)); $this->elementStart('fieldset'); + // TRANS: OpenID plugin logon form legend. $this->element('legend', null, _m('OpenID login')); $this->hidden('token', common_session_token()); $this->elementStart('ul', 'form_data'); $this->elementStart('li'); - $this->input('openid_url', _m('OpenID URL'), - $this->openid_url, - _m('Your OpenID URL')); + $provider = common_config('openid', 'trusted_provider'); + $appendUsername = common_config('openid', 'append_username'); + if ($provider) { + $this->element('label', array(), _m('OpenID provider')); + $this->element('span', array(), $provider); + if ($appendUsername) { + $this->element('input', array('id' => 'openid_username', + 'name' => 'openid_username', + 'style' => 'float: none')); + } + $this->element('p', 'form_guide', + ($appendUsername ? _m('Enter your username.') . ' ' : '') . + _m('You will be sent to the provider\'s site for authentication.')); + $this->hidden('openid_url', $provider); + } else { + // TRANS: OpenID plugin logon form field label. + $this->input('openid_url', _m('OpenID URL'), + $this->openid_url, + // TRANS: OpenID plugin logon form field instructions. + _m('Your OpenID URL')); + } $this->elementEnd('li'); $this->elementStart('li', array('id' => 'settings_rememberme')); + // TRANS: OpenID plugin logon form checkbox label for setting to put the OpenID information in a cookie. $this->checkbox('rememberme', _m('Remember me'), false, + // TRANS: OpenID plugin logon form field instructions. _m('Automatically login in the future; ' . 'not for shared computers!')); $this->elementEnd('li'); $this->elementEnd('ul'); - $this->submit('submit', _m('Login')); + // TRANS: OpenID plugin logon form button label to start logon with the data provided in the logon form. + $this->submit('submit', _m('BUTTON', 'Login')); $this->elementEnd('fieldset'); $this->elementEnd('form'); } diff --git a/plugins/OpenID/openidserver.php b/plugins/OpenID/openidserver.php index afbca553f..b2cf1f8ac 100644 --- a/plugins/OpenID/openidserver.php +++ b/plugins/OpenID/openidserver.php @@ -23,6 +23,7 @@ * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> * @copyright 2008-2009 StatusNet, Inc. + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ @@ -43,6 +44,7 @@ require_once(INSTALLDIR.'/plugins/OpenID/User_openid_trustroot.php'); * @category Settings * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ @@ -69,9 +71,13 @@ class OpenidserverAction extends Action //cannot prompt the user to login in immediate mode, so answer false $response = $this->generateDenyResponse($request); }else{ - /* Go log in, and then come back. */ + // Go log in, and then come back. + // + // Note: 303 redirect rather than 307 to avoid + // prompting user for form resubmission if we + // were POSTed here. common_set_returnto($_SERVER['REQUEST_URI']); - common_redirect(common_local_url('login')); + common_redirect(common_local_url('login'), 303); return; } }else if(common_profile_url($user->nickname) == $request->identity || $request->idSelect()){ @@ -90,8 +96,13 @@ class OpenidserverAction extends Action $this->oserver->encodeResponse($denyResponse); //sign the response $_SESSION['openid_allow_url'] = $allowResponse->encodeToUrl(); $_SESSION['openid_deny_url'] = $denyResponse->encodeToUrl(); - //ask the user to trust this trust root - common_redirect(common_local_url('openidtrust')); + + // Ask the user to trust this trust root... + // + // Note: 303 redirect rather than 307 to avoid + // prompting user for form resubmission if we + // were POSTed here. + common_redirect(common_local_url('openidtrust'), 303); return; } }else{ @@ -103,6 +114,7 @@ class OpenidserverAction extends Action $response = $this->generateDenyResponse($request); } else { //invalid + // TRANS: OpenID plugin client error given trying to add an unauthorised OpenID to a user (403). $this->clientError(sprintf(_m('You are not authorized to use the identity %s.'),$request->identity),$code=403); } } else { @@ -123,6 +135,7 @@ class OpenidserverAction extends Action } $this->raw($response->body); }else{ + // TRANS: OpenID plugin client error given when not getting a response for a given OpenID provider (500). $this->clientError(_m('Just an OpenID provider. Nothing to see here, move along...'),$code=500); } } diff --git a/plugins/OpenID/openidsettings.php b/plugins/OpenID/openidsettings.php index 16142cf48..505e7d0ee 100644 --- a/plugins/OpenID/openidsettings.php +++ b/plugins/OpenID/openidsettings.php @@ -90,34 +90,36 @@ class OpenidsettingsAction extends AccountSettingsAction { $user = common_current_user(); - $this->elementStart('form', array('method' => 'post', - 'id' => 'form_settings_openid_add', - 'class' => 'form_settings', - 'action' => - common_local_url('openidsettings'))); - $this->elementStart('fieldset', array('id' => 'settings_openid_add')); - $this->element('legend', null, _m('Add OpenID')); - $this->hidden('token', common_session_token()); - $this->element('p', 'form_guide', - _m('If you want to add an OpenID to your account, ' . - 'enter it in the box below and click "Add".')); - $this->elementStart('ul', 'form_data'); - $this->elementStart('li'); - $this->element('label', array('for' => 'openid_url'), - _m('OpenID URL')); - $this->element('input', array('name' => 'openid_url', - 'type' => 'text', - 'id' => 'openid_url')); - $this->elementEnd('li'); - $this->elementEnd('ul'); - $this->element('input', array('type' => 'submit', - 'id' => 'settings_openid_add_action-submit', - 'name' => 'add', - 'class' => 'submit', - 'value' => _m('Add'))); - $this->elementEnd('fieldset'); - $this->elementEnd('form'); - + if (!common_config('openid', 'trusted_provider')) { + $this->elementStart('form', array('method' => 'post', + 'id' => 'form_settings_openid_add', + 'class' => 'form_settings', + 'action' => + common_local_url('openidsettings'))); + $this->elementStart('fieldset', array('id' => 'settings_openid_add')); + + $this->element('legend', null, _m('Add OpenID')); + $this->hidden('token', common_session_token()); + $this->element('p', 'form_guide', + _m('If you want to add an OpenID to your account, ' . + 'enter it in the box below and click "Add".')); + $this->elementStart('ul', 'form_data'); + $this->elementStart('li'); + $this->element('label', array('for' => 'openid_url'), + _m('OpenID URL')); + $this->element('input', array('name' => 'openid_url', + 'type' => 'text', + 'id' => 'openid_url')); + $this->elementEnd('li'); + $this->elementEnd('ul'); + $this->element('input', array('type' => 'submit', + 'id' => 'settings_openid_add_action-submit', + 'name' => 'add', + 'class' => 'submit', + 'value' => _m('Add'))); + $this->elementEnd('fieldset'); + $this->elementEnd('form'); + } $oid = new User_openid(); $oid->user_id = $user->id; @@ -234,10 +236,14 @@ class OpenidsettingsAction extends AccountSettingsAction } if ($this->arg('add')) { - $result = oid_authenticate($this->trimmed('openid_url'), - 'finishaddopenid'); - if (is_string($result)) { // error message - $this->showForm($result); + if (common_config('openid', 'trusted_provider')) { + $this->showForm(_m("Can't add new providers.")); + } else { + $result = oid_authenticate($this->trimmed('openid_url'), + 'finishaddopenid'); + if (is_string($result)) { // error message + $this->showForm($result); + } } } else if ($this->arg('remove')) { $this->removeOpenid(); diff --git a/plugins/OpenID/openidtrust.php b/plugins/OpenID/openidtrust.php index fa7ea36e2..ed6ca73a4 100644 --- a/plugins/OpenID/openidtrust.php +++ b/plugins/OpenID/openidtrust.php @@ -71,7 +71,7 @@ class OpenidtrustAction extends Action } return true; } - + function handle($args) { parent::handle($args); @@ -96,7 +96,6 @@ class OpenidtrustAction extends Action $user_openid_trustroot->created = DB_DataObject_Cast::dateTime(); if (!$user_openid_trustroot->insert()) { $err = PEAR::getStaticProperty('DB_DataObject','lastError'); - common_debug('DB error ' . $err->code . ': ' . $err->message, __FILE__); } common_redirect($this->allowUrl, $code=302); }else{ @@ -135,7 +134,7 @@ class OpenidtrustAction extends Action $this->elementStart('fieldset'); $this->submit('allow', _m('Continue')); $this->submit('deny', _m('Cancel')); - + $this->elementEnd('fieldset'); $this->elementEnd('form'); } diff --git a/plugins/PostDebug/locale/PostDebug.pot b/plugins/PostDebug/locale/PostDebug.pot new file mode 100644 index 000000000..b7107d4c1 --- /dev/null +++ b/plugins/PostDebug/locale/PostDebug.pot @@ -0,0 +1,21 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: PostDebugPlugin.php:58 +msgid "Debugging tool to record request details on POST." +msgstr "" diff --git a/plugins/PoweredByStatusNet/locale/PoweredByStatusNet.po b/plugins/PoweredByStatusNet/locale/PoweredByStatusNet.pot index 8f8434a85..bc0e814f2 100644 --- a/plugins/PoweredByStatusNet/locale/PoweredByStatusNet.po +++ b/plugins/PoweredByStatusNet/locale/PoweredByStatusNet.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-03-01 14:58-0800\n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" diff --git a/plugins/PtitUrl/PtitUrlPlugin.php b/plugins/PtitUrl/PtitUrlPlugin.php index ddba942e6..2963e8997 100644 --- a/plugins/PtitUrl/PtitUrlPlugin.php +++ b/plugins/PtitUrl/PtitUrlPlugin.php @@ -22,7 +22,7 @@ * @category Plugin * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> - * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/PtitUrl/locale/PtitUrl.pot b/plugins/PtitUrl/locale/PtitUrl.pot new file mode 100644 index 000000000..a888f80e4 --- /dev/null +++ b/plugins/PtitUrl/locale/PtitUrl.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: PtitUrlPlugin.php:67 +#, php-format +msgid "Uses <a href=\"http://%1$s/\">%1$s</a> URL-shortener service." +msgstr "" diff --git a/plugins/RSSCloud/RSSCloudPlugin.php b/plugins/RSSCloud/RSSCloudPlugin.php index 9f444c8bb..c1951cdbf 100644 --- a/plugins/RSSCloud/RSSCloudPlugin.php +++ b/plugins/RSSCloud/RSSCloudPlugin.php @@ -100,12 +100,12 @@ class RSSCloudPlugin extends Plugin * * Hook for RouterInitialized event. * - * @param Mapper &$m URL parser and mapper + * @param Mapper $m URL parser and mapper * * @return boolean hook return */ - function onRouterInitialized(&$m) + function onRouterInitialized($m) { $m->connect('/main/rsscloud/request_notify', array('action' => 'RSSCloudRequestNotify')); @@ -192,25 +192,13 @@ class RSSCloudPlugin extends Plugin function onStartEnqueueNotice($notice, &$transports) { - array_push($transports, 'rsscloud'); + if ($notice->isLocal()) { + array_push($transports, 'rsscloud'); + } return true; } /** - * Determine whether the notice was locally created - * - * @param Notice $notice the notice in question - * - * @return boolean locality - */ - - function _isLocal($notice) - { - return ($notice->is_local == Notice::LOCAL_PUBLIC || - $notice->is_local == Notice::LOCAL_NONPUBLIC); - } - - /** * Create the rsscloud_subscription table if it's not * already in the DB * diff --git a/plugins/RSSCloud/locale/RSSCloud.pot b/plugins/RSSCloud/locale/RSSCloud.pot new file mode 100644 index 000000000..4078cc749 --- /dev/null +++ b/plugins/RSSCloud/locale/RSSCloud.pot @@ -0,0 +1,24 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: RSSCloudPlugin.php:260 +msgid "" +"The RSSCloud plugin enables your StatusNet instance to publish real-time " +"updates for profile RSS feeds using the <a href=\"http://rsscloud.org/" +"\">RSSCloud protocol</a>\"." +msgstr "" diff --git a/plugins/Realtime/README b/plugins/Realtime/README index 524382696..99c79cfab 100644 --- a/plugins/Realtime/README +++ b/plugins/Realtime/README @@ -1,6 +1,5 @@ == TODO == * i18n -* Change in context URL to conversation (try not to construct the URL in JS) * Update mark behaviour (on notice send) * Pause, Send a notice ~ should not update counter * Pause ~ retain up to 50-100 most recent notices diff --git a/plugins/Realtime/RealtimePlugin.php b/plugins/Realtime/RealtimePlugin.php index b559d80c6..352afcf78 100644 --- a/plugins/Realtime/RealtimePlugin.php +++ b/plugins/Realtime/RealtimePlugin.php @@ -250,14 +250,7 @@ class RealtimePlugin extends Plugin $arr['url'] = $notice->bestUrl(); $arr['html'] = htmlspecialchars($notice->rendered); $arr['source'] = htmlspecialchars($arr['source']); - - if (!empty($notice->reply_to)) { - $reply_to = Notice::staticGet('id', $notice->reply_to); - if (!empty($reply_to)) { - $arr['in_reply_to_status_url'] = $reply_to->bestUrl(); - } - $reply_to = null; - } + $arr['conversation_url'] = $this->getConversationUrl($notice); $profile = $notice->getProfile(); $arr['user']['profile_url'] = $profile->profileurl; @@ -272,10 +265,7 @@ class RealtimePlugin extends Plugin $arr['retweeted_status']['source'] = htmlspecialchars($original->source); $originalProfile = $original->getProfile(); $arr['retweeted_status']['user']['profile_url'] = $originalProfile->profileurl; - if (!empty($original->reply_to)) { - $originalReply = Notice::staticGet('id', $original->reply_to); - $arr['retweeted_status']['in_reply_to_status_url'] = $originalReply->bestUrl(); - } + $arr['retweeted_status']['conversation_url'] = $this->getConversationUrl($original); } $original = null; } @@ -303,6 +293,34 @@ class RealtimePlugin extends Plugin return $tags; } + function getConversationUrl($notice) + { + $convurl = null; + + if ($notice->hasConversation()) { + $conv = Conversation::staticGet( + 'id', + $notice->conversation + ); + $convurl = $conv->uri; + + if(empty($convurl)) { + $msg = sprintf( + "Couldn't find Conversation ID %d to make 'in context'" + . "link for Notice ID %d", + $notice->conversation, + $notice->id + ); + + common_log(LOG_WARNING, $msg); + } else { + $convurl .= '#notice-' . $notice->id; + } + } + + return $convurl; + } + function _getScripts() { return array('plugins/Realtime/realtimeupdate.js'); diff --git a/plugins/Realtime/realtimeupdate.js b/plugins/Realtime/realtimeupdate.js index 0f7a680d7..25dc12d58 100644 --- a/plugins/Realtime/realtimeupdate.js +++ b/plugins/Realtime/realtimeupdate.js @@ -130,7 +130,7 @@ RealtimeUpdate = { user = data['user']; html = data['html'].replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/&/g,'&'); source = data['source'].replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/&/g,'&'); -console.log(data); + ni = "<li class=\"hentry notice\" id=\"notice-"+unique+"\">"+ "<div class=\"entry-title\">"+ "<span class=\"vcard author\">"+ @@ -149,8 +149,8 @@ console.log(data); "from "+ "<span class=\"device\">"+source+"</span>"+ // may have a link "</span>"; - if (data['in_reply_to_status_id']) { - ni = ni+" <a class=\"response\" href=\""+data['in_reply_to_status_url']+"\">in context</a>"; + if (data['conversation_url']) { + ni = ni+" <a class=\"response\" href=\""+data['conversation_url']+"\">in context</a>"; } if (repeat) { diff --git a/plugins/Recaptcha/RecaptchaPlugin.php b/plugins/Recaptcha/RecaptchaPlugin.php index c585da43c..7cc34c568 100644 --- a/plugins/Recaptcha/RecaptchaPlugin.php +++ b/plugins/Recaptcha/RecaptchaPlugin.php @@ -62,12 +62,32 @@ class RecaptchaPlugin extends Plugin { $action->elementStart('li'); $action->raw('<label for="recaptcha">Captcha</label>'); - if($this->checkssl() === true) { - $action->raw(recaptcha_get_html($this->public_key), null, true); - } else { - $action->raw(recaptcha_get_html($this->public_key)); - } + + // AJAX API will fill this div out. + // We're calling that instead of the regular one so we stay compatible + // with application/xml+xhtml output as for mobile. + $action->element('div', array('id' => 'recaptcha')); $action->elementEnd('li'); + + $action->recaptchaPluginNeedsOutput = true; + return true; + } + + function onEndShowScripts($action) + { + if (isset($action->recaptchaPluginNeedsOutput) && $action->recaptchaPluginNeedsOutput) { + // Load the AJAX API + if ($this->checkssl()) { + $url = "https://api-secure.recaptcha.net/js/recaptcha_ajax.js"; + } else { + $url = "http://api.recaptcha.net/js/recaptcha_ajax.js"; + } + $action->script($url); + + // And when we're ready, fill out the captcha! + $key = json_encode($this->public_key); + $action->inlinescript("\$(function(){Recaptcha.create($key, 'recaptcha');});"); + } return true; } diff --git a/plugins/Recaptcha/locale/Recaptcha.pot b/plugins/Recaptcha/locale/Recaptcha.pot new file mode 100644 index 000000000..6611ff604 --- /dev/null +++ b/plugins/Recaptcha/locale/Recaptcha.pot @@ -0,0 +1,23 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: RecaptchaPlugin.php:97 +msgid "" +"Uses <a href=\"http://recaptcha.org/\">Recaptcha</a> service to add a " +"captcha to the registration page." +msgstr "" diff --git a/plugins/RegisterThrottle/locale/RegisterThrottle.pot b/plugins/RegisterThrottle/locale/RegisterThrottle.pot new file mode 100644 index 000000000..834f5fd4a --- /dev/null +++ b/plugins/RegisterThrottle/locale/RegisterThrottle.pot @@ -0,0 +1,29 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: RegisterThrottlePlugin.php:122 RegisterThrottlePlugin.php:161 +msgid "Cannot find IP address." +msgstr "" + +#: RegisterThrottlePlugin.php:167 +msgid "Cannot find user after successful registration." +msgstr "" + +#: RegisterThrottlePlugin.php:200 +msgid "Throttles excessive registration from a single IP." +msgstr "" diff --git a/plugins/RequireValidatedEmail/README b/plugins/RequireValidatedEmail/README index 46ee24d5f..84b1485b2 100644 --- a/plugins/RequireValidatedEmail/README +++ b/plugins/RequireValidatedEmail/README @@ -12,6 +12,20 @@ registered prior to that timestamp. addPlugin('RequireValidatedEmail', array('grandfatherCutoff' => 'Dec 7, 2009'); +You can also exclude the validation checks from OpenID accounts +connected to a trusted provider, by providing a list of regular +expressions to match their provider URLs. + +For example, to trust WikiHow and Wikipedia users: + + addPlugin('RequireValidatedEmailPlugin', array( + 'trustedOpenIDs' => array( + '!^http://\w+\.wikihow\.com/!', + '!^http://\w+\.wikipedia\.org/!', + ), + )); + + Todo: * add a more visible indicator that validation is still outstanding diff --git a/plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php b/plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php index ccefa14f6..af75b96e0 100644 --- a/plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php +++ b/plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php @@ -21,8 +21,9 @@ * * @category Plugin * @package StatusNet - * @author Craig Andrews <candrews@integralblue.com>, Brion Vibber <brion@status.net> - * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @author Craig Andrews <candrews@integralblue.com> + * @author Brion Vibber <brion@status.net> + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ @@ -37,6 +38,20 @@ class RequireValidatedEmailPlugin extends Plugin // without the validation requirement. public $grandfatherCutoff=null; + // If OpenID plugin is installed, users with a verified OpenID + // association whose provider URL matches one of these regexes + // will be considered to be sufficiently valid for our needs. + // + // For example, to trust WikiHow and Wikipedia OpenID users: + // + // addPlugin('RequireValidatedEmailPlugin', array( + // 'trustedOpenIDs' => array( + // '!^http://\w+\.wikihow\.com/!', + // '!^http://\w+\.wikipedia\.org/!', + // ), + // )); + public $trustedOpenIDs=array(); + function __construct() { parent::__construct(); @@ -90,13 +105,17 @@ class RequireValidatedEmailPlugin extends Plugin */ protected function validated($user) { - if ($this->grandfathered($user)) { - return true; - } - // The email field is only stored after validation... // Until then you'll find them in confirm_address. - return !empty($user->email); + $knownGood = !empty($user->email) || + $this->grandfathered($user) || + $this->hasTrustedOpenID($user); + + // Give other plugins a chance to override, if they can validate + // that somebody's ok despite a non-validated email. + Event::handle('RequireValidatedEmailPlugin_Override', array($user, &$knownGood)); + + return $knownGood; } /** @@ -118,6 +137,28 @@ class RequireValidatedEmailPlugin extends Plugin return false; } + /** + * Override for RequireValidatedEmail plugin. If we have a user who's + * not validated an e-mail, but did come from a trusted provider, + * we'll consider them ok. + */ + function hasTrustedOpenID($user) + { + if ($this->trustedOpenIDs && class_exists('User_openid')) { + foreach ($this->trustedOpenIDs as $regex) { + $oid = new User_openid(); + $oid->user_id = $user->id; + $oid->find(); + while ($oid->fetch()) { + if (preg_match($regex, $oid->canonical)) { + return true; + } + } + } + } + return false; + } + function onPluginVersion(&$versions) { $versions[] = array('name' => 'Require Validated Email', diff --git a/plugins/RequireValidatedEmail/locale/RequireValidatedEmail.po b/plugins/RequireValidatedEmail/locale/RequireValidatedEmail.pot index 49ac4f6f4..c8953a1fa 100644 --- a/plugins/RequireValidatedEmail/locale/RequireValidatedEmail.po +++ b/plugins/RequireValidatedEmail/locale/RequireValidatedEmail.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-03-10 10:05-0800\n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" diff --git a/plugins/ReverseUsernameAuthentication/ReverseUsernameAuthenticationPlugin.php b/plugins/ReverseUsernameAuthentication/ReverseUsernameAuthenticationPlugin.php index dac5a1588..8a05a7734 100644 --- a/plugins/ReverseUsernameAuthentication/ReverseUsernameAuthenticationPlugin.php +++ b/plugins/ReverseUsernameAuthentication/ReverseUsernameAuthenticationPlugin.php @@ -22,7 +22,7 @@ * @category Plugin * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> - * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/ReverseUsernameAuthentication/locale/ReverseUsernameAuthentication.pot b/plugins/ReverseUsernameAuthentication/locale/ReverseUsernameAuthentication.pot new file mode 100644 index 000000000..6fa18c464 --- /dev/null +++ b/plugins/ReverseUsernameAuthentication/locale/ReverseUsernameAuthentication.pot @@ -0,0 +1,24 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ReverseUsernameAuthenticationPlugin.php:67 +msgid "" +"The Reverse Username Authentication plugin allows for StatusNet to handle " +"authentication by checking if the provided password is the same as the " +"reverse of the username." +msgstr "" diff --git a/plugins/Sample/User_greeting_count.php b/plugins/Sample/User_greeting_count.php index d9a59770d..fc0cbd28f 100644 --- a/plugins/Sample/User_greeting_count.php +++ b/plugins/Sample/User_greeting_count.php @@ -94,29 +94,34 @@ class User_greeting_count extends Memcached_DataObject /** * return key definitions for DB_DataObject * - * DB_DataObject needs to know about keys that the table has; this function - * defines them. + * DB_DataObject needs to know about keys that the table has, since it + * won't appear in StatusNet's own keys list. In most cases, this will + * simply reference your keyTypes() function. * - * @return array key definitions + * @return array list of key field names */ function keys() { - return array('user_id' => 'K'); + return array_keys($this->keyTypes()); } /** * return key definitions for Memcached_DataObject * * Our caching system uses the same key definitions, but uses a different - * method to get them. + * method to get them. This key information is used to store and clear + * cached data, so be sure to list any key that will be used for static + * lookups. * - * @return array key definitions + * @return array associative array of key definitions, field name to type: + * 'K' for primary key: for compound keys, add an entry for each component; + * 'U' for unique keys: compound keys are not well supported here. */ function keyTypes() { - return $this->keys(); + return array('user_id' => 'K'); } /** diff --git a/plugins/Sample/locale/Sample.po b/plugins/Sample/locale/Sample.pot index a52c4ec01..bd21dd3c4 100644 --- a/plugins/Sample/locale/Sample.po +++ b/plugins/Sample/locale/Sample.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-03-01 14:58-0800\n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -17,26 +17,20 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: hello.php:115 SamplePlugin.php:266 -msgid "Hello" +#: User_greeting_count.php:163 +#, php-format +msgid "Could not save new greeting count for %d" msgstr "" -#: hello.php:117 hello.php:141 +#: User_greeting_count.php:176 #, php-format -msgid "Hello, %s" +msgid "Could not increment greeting count for %d" msgstr "" -#: hello.php:138 -msgid "Hello, stranger!" +#: SamplePlugin.php:266 hello.php:115 +msgid "Hello" msgstr "" -#: hello.php:143 -#, php-format -msgid "I have greeted you %d time." -msgid_plural "I have greeted you %d times." -msgstr[0] "" -msgstr[1] "" - #: SamplePlugin.php:266 msgid "A warm greeting" msgstr "" @@ -45,12 +39,18 @@ msgstr "" msgid "A sample plugin to show basics of development for new hackers." msgstr "" -#: User_greeting_count.php:163 +#: hello.php:117 hello.php:141 #, php-format -msgid "Could not save new greeting count for %d" +msgid "Hello, %s" msgstr "" -#: User_greeting_count.php:176 -#, php-format -msgid "Could not increment greeting count for %d" +#: hello.php:138 +msgid "Hello, stranger!" msgstr "" + +#: hello.php:143 +#, php-format +msgid "I have greeted you %d time." +msgid_plural "I have greeted you %d times." +msgstr[0] "" +msgstr[1] "" diff --git a/plugins/SimpleUrl/SimpleUrlPlugin.php b/plugins/SimpleUrl/SimpleUrlPlugin.php index 6eac7dbb1..5e2e85878 100644 --- a/plugins/SimpleUrl/SimpleUrlPlugin.php +++ b/plugins/SimpleUrl/SimpleUrlPlugin.php @@ -22,7 +22,7 @@ * @category Plugin * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> - * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/SimpleUrl/locale/SimpleUrl.pot b/plugins/SimpleUrl/locale/SimpleUrl.pot new file mode 100644 index 000000000..e3c241d53 --- /dev/null +++ b/plugins/SimpleUrl/locale/SimpleUrl.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: SimpleUrlPlugin.php:58 +#, php-format +msgid "Uses <a href=\"http://%1$s/\">%1$s</a> URL-shortener service." +msgstr "" diff --git a/plugins/Sitemap/SitemapPlugin.php b/plugins/Sitemap/SitemapPlugin.php new file mode 100644 index 000000000..b6d3b1ad3 --- /dev/null +++ b/plugins/Sitemap/SitemapPlugin.php @@ -0,0 +1,224 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * Creates a dynamic sitemap for a StatusNet site + * + * PHP version 5 + * + * 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 Sample + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Sitemap plugin + * + * @category Sample + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class SitemapPlugin extends Plugin +{ + const USERS_PER_MAP = 50000; + const NOTICES_PER_MAP = 50000; + + /** + * Load related modules when needed + * + * @param string $cls Name of the class to be loaded + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function onAutoload($cls) + { + $dir = dirname(__FILE__); + + switch ($cls) + { + case 'Sitemap_user_count': + case 'Sitemap_notice_count': + require_once $dir . '/' . $cls . '.php'; + return false; + case 'SitemapindexAction': + case 'NoticesitemapAction': + case 'UsersitemapAction': + case 'SitemapadminpanelAction': + require_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; + return false; + case 'SitemapAction': + require_once $dir . '/' . strtolower($cls) . '.php'; + return false; + default: + return true; + } + } + + /** + * Add sitemap-related information at the end of robots.txt + * + * @param Action $action Action being run + * + * @return boolean hook value. + */ + + function onEndRobotsTxt($action) + { + $url = common_local_url('sitemapindex'); + + print "\nSitemap: $url\n"; + + return true; + } + + /** + * Map URLs to actions + * + * @param Net_URL_Mapper $m path-to-action mapper + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function onRouterInitialized($m) + { + $m->connect('sitemapindex.xml', + array('action' => 'sitemapindex')); + + $m->connect('/notice-sitemap-:year-:month-:day-:index.xml', + array('action' => 'noticesitemap'), + array('year' => '[0-9]{4}', + 'month' => '[01][0-9]', + 'day' => '[0123][0-9]', + 'index' => '[1-9][0-9]*')); + + $m->connect('/user-sitemap-:year-:month-:day-:index.xml', + array('action' => 'usersitemap'), + array('year' => '[0-9]{4}', + 'month' => '[01][0-9]', + 'day' => '[0123][0-9]', + 'index' => '[1-9][0-9]*')); + + $m->connect('admin/sitemap', + array('action' => 'sitemapadminpanel')); + + return true; + } + + /** + * Meta tags for "claiming" a site + * + * We add extra meta tags that search engines like Yahoo!, Google, and Bing + * require to let you claim your site. + * + * @param Action $action Action being executed + * + * @return boolean hook value. + */ + + function onStartShowHeadElements($action) + { + $actionName = $action->trimmed('action'); + + $singleUser = common_config('singleuser', 'enabled'); + + // Different "top" pages if it's single user or not + + if (($singleUser && $actionName == 'showstream') || + (!$singleUser && $actionName == 'public')) { + + $keys = array('googlekey' => 'google-site-verification', + 'yahookey' => 'y_key', + 'bingkey' => 'msvalidate.01'); // XXX: is this the same for all sites? + + foreach ($keys as $config => $metaname) { + $content = common_config('sitemap', $config); + + if (!empty($content)) { + $action->element('meta', array('name' => $metaname, + 'content' => $content)); + } + } + } + + return true; + } + + /** + * Database schema setup + * + * We cache some data persistently to avoid overlong queries. + * + * @see Sitemap_user_count + * @see Sitemap_notice_count + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function onCheckSchema() + { + $schema = Schema::get(); + + $schema->ensureTable('sitemap_user_count', + array(new ColumnDef('registration_date', 'date', null, + true, 'PRI'), + new ColumnDef('user_count', 'integer'), + new ColumnDef('created', 'datetime', + null, false), + new ColumnDef('modified', 'timestamp'))); + + $schema->ensureTable('sitemap_notice_count', + array(new ColumnDef('notice_date', 'date', null, + true, 'PRI'), + new ColumnDef('notice_count', 'integer'), + new ColumnDef('created', 'datetime', + null, false), + new ColumnDef('modified', 'timestamp'))); + + $userCreated = $schema->getColumnDef('user', 'created'); + + if (empty($userCreated) || $userCreated->key != 'MUL') { + $schema->createIndex('user', 'created'); + } + + return true; + } + + function onEndAdminPanelNav($menu) { + if (AdminPanelAction::canAdmin('sitemap')) { + // TRANS: Menu item title/tooltip + $menu_title = _('Sitemap configuration'); + // TRANS: Menu item for site administration + $menu->out->menuItem(common_local_url('sitemapadminpanel'), _('Sitemap'), + $menu_title, $action_name == 'sitemapadminpanel', 'nav_sitemap_admin_panel'); + } + return true; + } +} diff --git a/plugins/Sitemap/Sitemap_notice_count.php b/plugins/Sitemap/Sitemap_notice_count.php new file mode 100644 index 000000000..6e0061e97 --- /dev/null +++ b/plugins/Sitemap/Sitemap_notice_count.php @@ -0,0 +1,290 @@ +<?php +/** + * Data class for counting notice postings by date + * + * PHP version 5 + * + * @category Data + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +/** + * Data class for counting notices by date + * + * We make a separate sitemap for each notice posted by date. + * To save ourselves some (not inconsiderable) processing effort, + * we cache this data in the sitemap_notice_count table. Each + * row represents a day since the site has been started, with a count + * of notices posted on that day. Since, after the end of the day, + * this number doesn't change, it's a good candidate for persistent caching. + * + * @category Data + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class Sitemap_notice_count extends Memcached_DataObject +{ + public $__table = 'sitemap_notice_count'; // table name + + public $notice_date; // date primary_key not_null + public $notice_count; // int(4) + public $created; + public $modified; + + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup (usually 'notice_id' for this class) + * @param mixed $v Value to lookup + * + * @return Sitemap_notice_count object found, or null for no hits + * + */ + + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('Sitemap_notice_count', $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array('notice_date' => DB_DATAOBJECT_DATE + DB_DATAOBJECT_NOTNULL, + 'notice_count' => DB_DATAOBJECT_INT, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, + 'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has; this function + * defines them. + * + * @return array key definitions + */ + + function keys() + { + return array('notice_date' => 'K'); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. + * + * @return array key definitions + */ + + function keyTypes() + { + return $this->keys(); + } + + static function getAll() + { + $noticeCounts = self::cacheGet('sitemap:notice:counts'); + + if ($noticeCounts === false) { + + $snc = new Sitemap_notice_count(); + $snc->orderBy('notice_date DESC'); + + // Fetch the first one to check up-to-date-itude + + $n = $snc->find(true); + + $today = self::today(); + $noticeCounts = array(); + + if (!$n) { // No counts saved yet + $noticeCounts = self::initializeCounts(); + } else if ($snc->notice_date < $today) { // There are counts but not up to today + $noticeCounts = self::fillInCounts($snc->notice_date); + } else if ($snc->notice_date == $today) { // Refresh today's + $noticeCounts[$today] = self::updateToday(); + } + + // starts with second-to-last date + + while ($snc->fetch()) { + $noticeCounts[$snc->notice_date] = $snc->notice_count; + } + + // Cache notice counts for 4 hours. + + self::cacheSet('sitemap:notice:counts', $noticeCounts, null, time() + 4 * 60 * 60); + } + + return $noticeCounts; + } + + static function initializeCounts() + { + $firstDate = self::getFirstDate(); // awww + $today = self::today(); + + $counts = array(); + + for ($d = $firstDate; $d <= $today; $d = self::incrementDay($d)) { + $n = self::getCount($d); + self::insertCount($d, $n); + $counts[$d] = $n; + } + + return $counts; + } + + static function fillInCounts($lastDate) + { + $today = self::today(); + + $counts = array(); + + $n = self::getCount($lastDate); + self::updateCount($lastDate, $n); + + $counts[$lastDate] = $n; + + for ($d = self::incrementDay($lastDate); $d <= $today; $d = self::incrementDay($d)) { + $n = self::getCount($d); + self::insertCount($d, $n); + } + + return $counts; + } + + static function updateToday() + { + $today = self::today(); + + $n = self::getCount($today); + self::updateCount($today, $n); + + return $n; + } + + static function getCount($d) + { + $notice = new Notice(); + $notice->whereAdd('created BETWEEN "'.$d.' 00:00:00" AND "'.self::incrementDay($d).' 00:00:00"'); + $notice->whereAdd('is_local = ' . Notice::LOCAL_PUBLIC); + $n = $notice->count(); + + return $n; + } + + static function insertCount($d, $n) + { + $snc = new Sitemap_notice_count(); + + $snc->notice_date = DB_DataObject_Cast::date($d); + + $snc->notice_count = $n; + $snc->created = common_sql_now(); + $snc->modified = $snc->created; + + if (!$snc->insert()) { + common_log(LOG_WARNING, "Could not save user counts for '$d'"); + } + } + + static function updateCount($d, $n) + { + $snc = Sitemap_notice_count::staticGet('notice_date', DB_DataObject_Cast::date($d)); + + if (empty($snc)) { + throw new Exception("No such registration date: $d"); + } + + $orig = clone($snc); + + $snc->notice_date = DB_DataObject_Cast::date($d); + + $snc->notice_count = $n; + $snc->created = common_sql_now(); + $snc->modified = $snc->created; + + if (!$snc->update($orig)) { + common_log(LOG_WARNING, "Could not save user counts for '$d'"); + } + } + + static function incrementDay($d) + { + $dt = self::dateStrToInt($d); + return self::dateIntToStr($dt + 24 * 60 * 60); + } + + static function dateStrToInt($d) + { + return strtotime($d.' 00:00:00'); + } + + static function dateIntToStr($dt) + { + return date('Y-m-d', $dt); + } + + static function getFirstDate() + { + $n = new Notice(); + + $n->selectAdd(); + $n->selectAdd('date(min(created)) as first_date'); + + if ($n->find(true)) { + return $n->first_date; + } else { + // Is this right? + return self::dateIntToStr(time()); + } + } + + static function today() + { + return self::dateIntToStr(time()); + } +} diff --git a/plugins/Sitemap/Sitemap_user_count.php b/plugins/Sitemap/Sitemap_user_count.php new file mode 100644 index 000000000..98dd05bfe --- /dev/null +++ b/plugins/Sitemap/Sitemap_user_count.php @@ -0,0 +1,286 @@ +<?php +/** + * Data class for counting user registrations by date + * + * PHP version 5 + * + * @category Data + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +/** + * Data class for counting users by date + * + * We make a separate sitemap for each user registered by date. + * To save ourselves some processing effort, we cache this data + * + * @category Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class Sitemap_user_count extends Memcached_DataObject +{ + public $__table = 'sitemap_user_count'; // table name + + public $registration_date; // date primary_key not_null + public $user_count; // int(4) + public $created; + public $modified; + + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup (usually 'user_id' for this class) + * @param mixed $v Value to lookup + * + * @return Sitemap_user_count object found, or null for no hits + * + */ + + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('Sitemap_user_count', $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array('registration_date' => DB_DATAOBJECT_DATE + DB_DATAOBJECT_NOTNULL, + 'user_count' => DB_DATAOBJECT_INT, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, + 'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has; this function + * defines them. + * + * @return array key definitions + */ + + function keys() + { + return array('registration_date' => 'K'); + } + + function sequenceKey() + { + return array(false, false, false); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. + * + * @return array key definitions + */ + + function keyTypes() + { + return $this->keys(); + } + + static function getAll() + { + $userCounts = self::cacheGet('sitemap:user:counts'); + + if ($userCounts === false) { + + $suc = new Sitemap_user_count(); + $suc->orderBy('registration_date DESC'); + + // Fetch the first one to check up-to-date-itude + + $n = $suc->find(true); + + $today = self::today(); + $userCounts = array(); + + if (!$n) { // No counts saved yet + $userCounts = self::initializeCounts(); + } else if ($suc->registration_date < $today) { // There are counts but not up to today + $userCounts = self::fillInCounts($suc->registration_date); + } else if ($suc->registration_date == $today) { // Refresh today's + $userCounts[$today] = self::updateToday(); + } + + // starts with second-to-last date + + while ($suc->fetch()) { + $userCounts[$suc->registration_date] = $suc->user_count; + } + + // Cache user counts for 4 hours. + + self::cacheSet('sitemap:user:counts', $userCounts, null, time() + 4 * 60 * 60); + } + + return $userCounts; + } + + static function initializeCounts() + { + $firstDate = self::getFirstDate(); // awww + $today = self::today(); + + $counts = array(); + + for ($d = $firstDate; $d <= $today; $d = self::incrementDay($d)) { + $n = self::getCount($d); + self::insertCount($d, $n); + $counts[$d] = $n; + } + + return $counts; + } + + static function fillInCounts($lastDate) + { + $today = self::today(); + + $counts = array(); + + $n = self::getCount($lastDate); + self::updateCount($lastDate, $n); + + $counts[$lastDate] = $n; + + for ($d = self::incrementDay($lastDate); $d <= $today; $d = self::incrementDay($d)) { + $n = self::getCount($d); + self::insertCount($d, $n); + } + + return $counts; + } + + static function updateToday() + { + $today = self::today(); + + $n = self::getCount($today); + self::updateCount($today, $n); + + return $n; + } + + static function getCount($d) + { + $user = new User(); + $user->whereAdd('created BETWEEN "'.$d.' 00:00:00" AND "'.self::incrementDay($d).' 00:00:00"'); + $n = $user->count(); + + return $n; + } + + static function insertCount($d, $n) + { + $suc = new Sitemap_user_count(); + + $suc->registration_date = DB_DataObject_Cast::date($d); + $suc->user_count = $n; + $suc->created = common_sql_now(); + $suc->modified = $suc->created; + + if (!$suc->insert()) { + common_log(LOG_WARNING, "Could not save user counts for '$d'"); + } + } + + static function updateCount($d, $n) + { + $suc = Sitemap_user_count::staticGet('registration_date', DB_DataObject_Cast::date($d)); + + if (empty($suc)) { + throw new Exception("No such registration date: $d"); + } + + $orig = clone($suc); + + $suc->registration_date = DB_DataObject_Cast::date($d); + $suc->user_count = $n; + $suc->created = common_sql_now(); + $suc->modified = $suc->created; + + if (!$suc->update($orig)) { + common_log(LOG_WARNING, "Could not save user counts for '$d'"); + } + } + + static function incrementDay($d) + { + $dt = self::dateStrToInt($d); + return self::dateIntToStr($dt + 24 * 60 * 60); + } + + static function dateStrToInt($d) + { + return strtotime($d.' 00:00:00'); + } + + static function dateIntToStr($dt) + { + return date('Y-m-d', $dt); + } + + static function getFirstDate() + { + $u = new User(); + $u->selectAdd(); + $u->selectAdd('date(min(created)) as first_date'); + if ($u->find(true)) { + return $u->first_date; + } else { + // Is this right? + return self::dateIntToStr(time()); + } + } + + static function today() + { + return self::dateIntToStr(time()); + } +} diff --git a/plugins/Sitemap/noticesitemap.php b/plugins/Sitemap/noticesitemap.php new file mode 100644 index 000000000..7d9d2e5d6 --- /dev/null +++ b/plugins/Sitemap/noticesitemap.php @@ -0,0 +1,137 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Show list of user pages + * + * 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 Sitemap + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * sitemap for users + * + * @category Sitemap + * @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 NoticesitemapAction extends SitemapAction +{ + var $notices = null; + var $j = 0; + + function prepare($args) + { + parent::prepare($args); + + $y = $this->trimmed('year'); + + $m = $this->trimmed('month'); + $d = $this->trimmed('day'); + + $i = $this->trimmed('index'); + + $y += 0; + $m += 0; + $d += 0; + $i += 0; + + $this->notices = $this->getNotices($y, $m, $d, $i); + $this->j = 0; + + return true; + } + + function nextUrl() + { + if ($this->j < count($this->notices)) { + $n = $this->notices[$this->j]; + $this->j++; + return array(common_local_url('shownotice', array('notice' => $n[0])), + common_date_w3dtf($n[1]), + 'never', + null); + } else { + return null; + } + } + + function getNotices($y, $m, $d, $i) + { + $n = Notice::cacheGet("sitemap:notice:$y:$m:$d:$i"); + + if ($n === false) { + + $notice = new Notice(); + + $begindt = sprintf('%04d-%02d-%02d 00:00:00', $y, $m, $d); + + // XXX: estimates 1d == 24h, which screws up days + // with leap seconds (1d == 24h + 1s). Thankfully they're + // few and far between. + + $theend = strtotime($begindt) + (24 * 60 * 60); + $enddt = common_sql_date($theend); + + $notice->selectAdd(); + $notice->selectAdd('id, created'); + + $notice->whereAdd("created >= '$begindt'"); + $notice->whereAdd("created < '$enddt'"); + + $notice->whereAdd('is_local = ' . Notice::LOCAL_PUBLIC); + + $notice->orderBy('created'); + + $offset = ($i-1) * SitemapPlugin::NOTICES_PER_MAP; + $limit = SitemapPlugin::NOTICES_PER_MAP; + + $notice->limit($offset, $limit); + + $notice->find(); + + $n = array(); + + while ($notice->fetch()) { + $n[] = array($notice->id, $notice->created); + } + + $c = Cache::instance(); + + if (!empty($c)) { + $c->set(Cache::key("sitemap:notice:$y:$m:$d:$i"), + $n, + Cache::COMPRESSED, + ((time() > $theend) ? (time() + 90 * 24 * 60 * 60) : (time() + 5 * 60))); + } + } + + return $n; + } +} diff --git a/plugins/Sitemap/scripts/updatecounts.php b/plugins/Sitemap/scripts/updatecounts.php new file mode 100644 index 000000000..91bc0ac4e --- /dev/null +++ b/plugins/Sitemap/scripts/updatecounts.php @@ -0,0 +1,36 @@ +#!/usr/bin/env php +<?php +/* + * StatusNet - a distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..')); + +$helptext = <<<END_OF_UPDATECOUNTS_HELP +updatecounts.php [options] +Update the notice and user counts cached in the database. + +END_OF_UPDATECOUNTS_HELP; + +require_once INSTALLDIR.'/scripts/commandline.inc'; + +// Will fill the cache + +$userCounts = Sitemap_user_count::getAll(); +$noticeCounts = Sitemap_notice_count::getAll(); + +echo "Done.\n"; diff --git a/plugins/Sitemap/sitemapaction.php b/plugins/Sitemap/sitemapaction.php new file mode 100644 index 000000000..45edfccc5 --- /dev/null +++ b/plugins/Sitemap/sitemapaction.php @@ -0,0 +1,95 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Superclass for sitemap-generating 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 Sitemap + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * superclass for sitemap actions + * + * @category Sitemap + * @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 SitemapAction extends Action +{ + /** + * handle the action + * + * @param array $args unused. + * + * @return void + */ + + function handle($args) + { + header('Content-Type: text/xml; charset=UTF-8'); + $this->startXML(); + + $this->elementStart('urlset', array('xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9')); + + while (list($url, $lm, $cf, $p) = $this->nextUrl()) { + $this->showUrl($url, $lm, $cf, $p); + } + + $this->elementEnd('urlset'); + + $this->endXML(); + } + + function showUrl($url, $lastMod=null, $changeFreq=null, $priority=null) + { + $this->elementStart('url'); + $this->element('loc', null, $url); + if (!is_null($lastMod)) { + $this->element('lastmod', null, $lastMod); + } + if (!is_null($changeFreq)) { + $this->element('changefreq', null, $changeFreq); + } + if (!is_null($priority)) { + $this->element('priority', null, $priority); + } + $this->elementEnd('url'); + } + + function nextUrl() + { + return null; + } + + function isReadOnly() + { + return true; + } +} diff --git a/plugins/Sitemap/sitemapadminpanel.php b/plugins/Sitemap/sitemapadminpanel.php new file mode 100644 index 000000000..3c295b08e --- /dev/null +++ b/plugins/Sitemap/sitemapadminpanel.php @@ -0,0 +1,205 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Sitemap administration panel + * + * 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 Sitemap + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Administer sitemap settings + * + * @category Sitemap + * @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 SitemapadminpanelAction extends AdminPanelAction +{ + /** + * Returns the page title + * + * @return string page title + */ + + function title() + { + return _('Sitemap'); + } + + /** + * Instructions for using this form. + * + * @return string instructions + */ + + function getInstructions() + { + return _('Sitemap settings for this StatusNet site'); + } + + /** + * Show the site admin panel form + * + * @return void + */ + + function showForm() + { + $form = new SitemapAdminPanelForm($this); + $form->show(); + return; + } + + /** + * Save settings from the form + * + * @return void + */ + + function saveSettings() + { + static $settings = array('sitemap' => array('googlekey', 'yahookey', 'bingkey')); + + $values = array(); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + $values[$section][$setting] = $this->trimmed($setting); + } + } + + // This throws an exception on validation errors + + $this->validate($values); + + // assert(all values are valid); + + $config = new Config(); + + $config->query('BEGIN'); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + Config::save($section, $setting, $values[$section][$setting]); + } + } + + $config->query('COMMIT'); + + return; + } + + function validate(&$values) + { + } +} + +/** + * Form for the sitemap admin panel + */ + +class SitemapAdminPanelForm extends AdminForm +{ + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return 'form_sitemap_admin_panel'; + } + + /** + * class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'form_sitemap'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('sitemapadminpanel'); + } + + /** + * Data elements of the form + * + * @return void + */ + + function formData() + { + $this->out->elementStart('fieldset', array('id' => 'sitemap_admin')); + $this->out->elementStart('ul', 'form_data'); + $this->li(); + $this->input('googlekey', + _('Google key'), + _('Google Webmaster Tools verification key'), + 'sitemap'); + $this->unli(); + $this->li(); + $this->input('yahookey', + _('Yahoo key'), + _('Yahoo! Site Explorer verification key'), + 'sitemap'); + $this->unli(); + $this->li(); + $this->input('bingkey', + _('Bing key'), + _('Bing Webmaster Tools verification key'), + 'sitemap'); + $this->unli(); + $this->out->elementEnd('ul'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit('submit', _('Save'), 'submit', null, _('Save sitemap settings')); + } +} diff --git a/plugins/Sitemap/sitemapindex.php b/plugins/Sitemap/sitemapindex.php new file mode 100644 index 000000000..169e3031c --- /dev/null +++ b/plugins/Sitemap/sitemapindex.php @@ -0,0 +1,128 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Generate sitemap index + * + * 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 Sitemap + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Show the sitemap index + * + * @category Sitemap + * @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 SitemapindexAction extends Action +{ + /** + * handle the action + * + * @param array $args unused. + * + * @return void + */ + + function handle($args) + { + header('Content-Type: text/xml; charset=UTF-8'); + $this->startXML(); + + $this->elementStart('sitemapindex', array('xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9')); + + $this->showNoticeSitemaps(); + $this->showUserSitemaps(); + + $this->elementEnd('sitemapindex'); + + $this->endXML(); + } + + function showUserSitemaps() + { + $userCounts = Sitemap_user_count::getAll(); + + foreach ($userCounts as $dt => $cnt) { + $cnt = $cnt+0; + + if ($cnt == 0) { + continue; + } + + $n = (int)$cnt / (int)SitemapPlugin::USERS_PER_MAP; + if (($cnt % SitemapPlugin::USERS_PER_MAP) != 0) { + $n++; + } + for ($i = 1; $i <= $n; $i++) { + $this->showSitemap('user', $dt, $i); + } + } + } + + function showNoticeSitemaps() + { + $noticeCounts = Sitemap_notice_count::getAll(); + + foreach ($noticeCounts as $dt => $cnt) { + if ($cnt == 0) { + continue; + } + $n = $cnt / SitemapPlugin::NOTICES_PER_MAP; + if ($cnt % SitemapPlugin::NOTICES_PER_MAP) { + $n++; + } + for ($i = 1; $i <= $n; $i++) { + $this->showSitemap('notice', $dt, $i); + } + } + } + + function showSitemap($prefix, $dt, $i) + { + list($y, $m, $d) = explode('-', $dt); + + $this->elementStart('sitemap'); + $this->element('loc', null, common_local_url($prefix.'sitemap', + array('year' => $y, + 'month' => $m, + 'day' => $d, + 'index' => $i))); + + $begdate = strtotime("$y-$m-$d 00:00:00"); + $enddate = $begdate + (24 * 60 * 60); + + if ($enddate < time()) { + $this->element('lastmod', null, date(DATE_W3C, $enddate)); + } + + $this->elementEnd('sitemap'); + } +} diff --git a/plugins/Sitemap/usersitemap.php b/plugins/Sitemap/usersitemap.php new file mode 100644 index 000000000..de1200715 --- /dev/null +++ b/plugins/Sitemap/usersitemap.php @@ -0,0 +1,128 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Show list of user pages + * + * 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 Sitemap + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * sitemap for users + * + * @category Sitemap + * @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 UsersitemapAction extends SitemapAction +{ + var $users = null; + var $j = 0; + + function prepare($args) + { + parent::prepare($args); + + $y = $this->trimmed('year'); + + $m = $this->trimmed('month'); + $d = $this->trimmed('day'); + + $i = $this->trimmed('index'); + + $y += 0; + $m += 0; + $d += 0; + $i += 0; + + $this->users = $this->getUsers($y, $m, $d, $i); + $this->j = 0; + return true; + } + + function nextUrl() + { + if ($this->j < count($this->users)) { + $nickname = $this->users[$this->j]; + $this->j++; + return array(common_profile_url($nickname), null, null, '1.0'); + } else { + return null; + } + } + + function getUsers($y, $m, $d, $i) + { + $u = User::cacheGet("sitemap:user:$y:$m:$d:$i"); + + if ($u === false) { + + $user = new User(); + + $begindt = sprintf('%04d-%02d-%02d 00:00:00', $y, $m, $d); + + // XXX: estimates 1d == 24h, which screws up days + // with leap seconds (1d == 24h + 1s). Thankfully they're + // few and far between. + + $theend = strtotime($begindt) + (24 * 60 * 60); + $enddt = common_sql_date($theend); + + $user->selectAdd(); + $user->selectAdd('nickname'); + $user->whereAdd("created >= '$begindt'"); + $user->whereAdd("created < '$enddt'"); + + $user->orderBy('created'); + + $offset = ($i-1) * SitemapPlugin::USERS_PER_MAP; + $limit = SitemapPlugin::USERS_PER_MAP; + + $user->limit($offset, $limit); + + $user->find(); + + while ($user->fetch()) { + $u[] = $user->nickname; + } + + $c = Cache::instance(); + + if (!empty($c)) { + $c->set(Cache::key("sitemap:user:$y:$m:$d:$i"), + $u, + Cache::COMPRESSED, + ((time() > $theend) ? (time() + 90 * 24 * 60 * 60) : (time() + 5 * 60))); + } + } + + return $u; + } +} diff --git a/plugins/SpotifyPlugin.php b/plugins/SpotifyPlugin.php new file mode 100644 index 000000000..e7a5a5382 --- /dev/null +++ b/plugins/SpotifyPlugin.php @@ -0,0 +1,113 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Plugin to create pretty Spotify URLs + * + * 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 Plugin + * @package StatusNet + * @author Nick Holliday <n.g.holliday@gmail.com> + * @copyright Nick Holliday + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + * @see Event + */ +if (!defined('STATUSNET')) { + exit(1); +} +define('SPOTIFYPLUGIN_VERSION', '0.1'); + +/** + * Plugin to create pretty Spotify URLs + * + * The Spotify API is called before the notice is saved to gather artist and track information. + * + * @category Plugin + * @package StatusNet + * @author Nick Holliday <n.g.holliday@gmail.com> + * @copyright Nick Holliday + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + * @see Event + */ + +class SpotifyPlugin extends Plugin +{ + + function __construct() + { + parent::__construct(); + } + + function onStartNoticeSave($notice) + { + $notice->rendered = preg_replace_callback('/spotify:[a-z]{5,6}:[a-z0-9]{22}/i', + "renderSpotifyURILink", + $notice->rendered); + + $notice->rendered = preg_replace_callback('/<a href="http:\/\/open.spotify.com\/[a-z]{5,6}\/[a-z0-9]{22}" title="http:\/\/open.spotify.com\/[a-z]{5,6}\/[a-z0-9]{22}" rel="external">http:\/\/open.spotify.com\/[a-z]{5,6}\/[a-z0-9]{22}<\/a>/i', + "renderSpotifyHTTPLink", + $notice->rendered); + + return true; + } + + function userAgent() + { + return 'SpotifyPlugin/'.SPOTIFYPLUGIN_VERSION . + ' StatusNet/' . STATUSNET_VERSION; + } +} + +function doSpotifyLookup($uri, $isArtist) +{ + $request = HTTPClient::start(); + $response = $request->get('http://ws.spotify.com/lookup/1/?uri=' . $uri); + if ($response->isOk()) { + $xml = simplexml_load_string($response->getBody()); + + if($isArtist) + return $xml->name; + else + return $xml->artist->name . ' - ' . $xml->name; + } +} + +function renderSpotifyURILink($match) +{ + $isArtist = false; + if(preg_match('/artist/', $match[0]) > 0) $isArtist = true; + + $name = doSpotifyLookup($match[0], $isArtist); + return "<a href=\"{$match[0]}\">" . $name . "</a>"; +} + +function renderSpotifyHTTPLink($match) +{ + $match[0] = preg_replace('/<a href="http:\/\/open.spotify.com\/[a-z]{5,6}\/[a-z0-9]{22}" title="http:\/\/open.spotify.com\/[a-z]{5,6}\/[a-z0-9]{22}" rel="external">http:\/\/open.spotify.com\//i', 'spotify:', $match[0]); + $match[0] = preg_replace('/<\/a>/', '', $match[0]); + $match[0] = preg_replace('/\//', ':', $match[0]); + + $isArtist = false; + if(preg_match('/artist/', $match[0]) > 0) $isArtist = true; + + $name = doSpotifyLookup($match[0], $isArtist); + return "<a href=\"{$match[0]}\">" . $name . "</a>"; +} diff --git a/plugins/SubMirror/SubMirrorPlugin.php b/plugins/SubMirror/SubMirrorPlugin.php new file mode 100644 index 000000000..80c6c5a88 --- /dev/null +++ b/plugins/SubMirror/SubMirrorPlugin.php @@ -0,0 +1,168 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009-2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * @package SubMirrorPlugin + * @maintainer Brion Vibber <brion@status.net> + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + + +class SubMirrorPlugin extends Plugin +{ + /** + * Hook for RouterInitialized event. + * + * @param Net_URL_Mapper $m path-to-action mapper + * @return boolean hook return + */ + function onRouterInitialized($m) + { + $m->connect('settings/mirror', + array('action' => 'mirrorsettings')); + $m->connect('settings/mirror/add', + array('action' => 'addmirror')); + $m->connect('settings/mirror/edit', + array('action' => 'editmirror')); + return true; + } + + /** + * Automatically load the actions and libraries used by the plugin + * + * @param Class $cls the class + * + * @return boolean hook return + * + */ + function onAutoload($cls) + { + $base = dirname(__FILE__); + $lower = strtolower($cls); + $files = array("$base/lib/$lower.php", + "$base/classes/$cls.php"); + if (substr($lower, -6) == 'action') { + $files[] = "$base/actions/" . substr($lower, 0, -6) . ".php"; + } + foreach ($files as $file) { + if (file_exists($file)) { + include_once $file; + return false; + } + } + return true; + } + + function handle($notice) + { + // Is anybody mirroring? + $mirror = new SubMirror(); + $mirror->subscribed = $notice->profile_id; + if ($mirror->find()) { + while ($mirror->fetch()) { + $mirror->repeat($notice); + } + } + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'SubMirror', + 'version' => STATUSNET_VERSION, + 'author' => 'Brion Vibber', + 'homepage' => 'http://status.net/wiki/Plugin:SubMirror', + 'rawdescription' => + _m('Pull feeds into your timeline!')); + + return true; + } + + /** + * Menu item for settings + * + * @param Action &$action Action being executed + * + * @return boolean hook return + */ + + function onEndAccountSettingsNav(&$action) + { + $action_name = $action->trimmed('action'); + + $action->menuItem(common_local_url('mirrorsettings'), + // TRANS: SubMirror plugin menu item on user settings page. + _m('MENU', 'Mirroring'), + // TRANS: SubMirror plugin tooltip for user settings menu item. + _m('Configure mirroring of posts from other feeds'), + $action_name === 'mirrorsettings'); + + return true; + } + + function onCheckSchema() + { + $schema = Schema::get(); + $schema->ensureTable('submirror', SubMirror::schemaDef()); + + // @hack until key definition support is merged + SubMirror::fixIndexes($schema); + return true; + } + + /** + * Set up queue handlers for outgoing hub pushes + * @param QueueManager $qm + * @return boolean hook return + */ + function onEndInitializeQueueManager(QueueManager $qm) + { + // After each notice save, check if there's any repeat mirrors. + $qm->connect('mirror', 'MirrorQueueHandler'); + return true; + } + + function onStartEnqueueNotice($notice, &$transports) + { + $transports[] = 'mirror'; + } + + /** + * Let the OStatus subscription garbage collection know if we're + * making use of a remote feed, so it doesn't get dropped out + * from under us. + * + * @param Ostatus_profile $oprofile + * @param int $count in/out + * @return mixed hook return value + */ + function onOstatus_profileSubscriberCount($oprofile, &$count) + { + if ($oprofile->profile_id) { + $mirror = new SubMirror(); + $mirror->subscribed = $oprofile->profile_id; + if ($mirror->find()) { + while ($mirror->fetch()) { + $count++; + } + } + } + return true; + } +} diff --git a/plugins/SubMirror/actions/addmirror.php b/plugins/SubMirror/actions/addmirror.php new file mode 100644 index 000000000..5acdf1dfe --- /dev/null +++ b/plugins/SubMirror/actions/addmirror.php @@ -0,0 +1,77 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * PHP version 5 + * + * @category Action + * @package StatusNet + * @author Brion Vibber <brion@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Takes parameters: + * + * - feed: a profile ID + * - token: session token to prevent CSRF attacks + * - ajax: boolean; whether to return Ajax or full-browser results + * + * Only works if the current user is logged in. + * + * @category Action + * @package StatusNet + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class AddMirrorAction extends BaseMirrorAction +{ + var $feedurl; + + /** + * Check pre-requisites and instantiate attributes + * + * @param Array $args array of arguments (URL, GET, POST) + * + * @return boolean success flag + */ + + function prepare($args) + { + parent::prepare($args); + $this->feedurl = $this->validateFeedUrl($this->trimmed('feedurl')); + $this->profile = $this->profileForFeed($this->feedurl); + return true; + } + + function saveMirror() + { + if ($this->oprofile->subscribe()) { + SubMirror::saveMirror($this->user, $this->profile); + } else { + $this->serverError(_m("Could not subscribe to feed.")); + } + } +} diff --git a/plugins/SubMirror/actions/basemirror.php b/plugins/SubMirror/actions/basemirror.php new file mode 100644 index 000000000..5be0699f0 --- /dev/null +++ b/plugins/SubMirror/actions/basemirror.php @@ -0,0 +1,169 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * PHP version 5 + * + * @category Action + * @package StatusNet + * @author Brion Vibber <brion@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Takes parameters: + * + * - feed: a profile ID + * - token: session token to prevent CSRF attacks + * - ajax: boolean; whether to return Ajax or full-browser results + * + * Only works if the current user is logged in. + * + * @category Action + * @package StatusNet + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +abstract class BaseMirrorAction extends Action +{ + var $user; + var $profile; + + /** + * Check pre-requisites and instantiate attributes + * + * @param Array $args array of arguments (URL, GET, POST) + * + * @return boolean success flag + */ + + function prepare($args) + { + parent::prepare($args); + return $this->sharedBoilerplate(); + } + + protected function validateFeedUrl($url) + { + if (common_valid_http_url($url)) { + return $url; + } else { + $this->clientError(_m("Invalid feed URL.")); + } + } + + protected function validateProfile($id) + { + $id = intval($id); + $profile = Profile::staticGet('id', $id); + if ($profile && $profile->id != $this->user->id) { + return $profile; + } + // TRANS: Error message returned to user when setting up feed mirroring, but we were unable to resolve the given URL to a working feed. + $this->clientError(_m("Invalid profile for mirroring.")); + } + + /** + * + * @param string $url + * @return Profile + */ + protected function profileForFeed($url) + { + $oprofile = Ostatus_profile::ensureProfileURL($url); + if ($oprofile->isGroup()) { + $this->clientError(_m("Can't mirror a StatusNet group at this time.")); + } + $this->oprofile = $oprofile; // @fixme ugly side effect :D + return $oprofile->localProfile(); + } + + /** + * @fixme none of this belongs in end classes + * this stuff belongs in shared code! + */ + function sharedBoilerplate() + { + // Only allow POST requests + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + $this->clientError(_('This action only accepts POST requests.')); + return false; + } + + // CSRF protection + + $token = $this->trimmed('token'); + + if (!$token || $token != common_session_token()) { + $this->clientError(_('There was a problem with your session token.'. + ' Try again, please.')); + return false; + } + + // Only for logged-in users + + $this->user = common_current_user(); + + if (empty($this->user)) { + $this->clientError(_('Not logged in.')); + return false; + } + return true; + } + + /** + * Handle request + * + * Does the subscription and returns results. + * + * @param Array $args unused. + * + * @return void + */ + + function handle($args) + { + // Throws exception on error + $this->saveMirror(); + + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + $this->element('title', null, _('Subscribed')); + $this->elementEnd('head'); + $this->elementStart('body'); + $unsubscribe = new EditMirrorForm($this, $this->profile); + $unsubscribe->show(); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + $url = common_local_url('mirrorsettings'); + common_redirect($url, 303); + } + } + + abstract function saveMirror(); +} diff --git a/plugins/SubMirror/actions/editmirror.php b/plugins/SubMirror/actions/editmirror.php new file mode 100644 index 000000000..c7fdab0d6 --- /dev/null +++ b/plugins/SubMirror/actions/editmirror.php @@ -0,0 +1,112 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * PHP version 5 + * + * @category Action + * @package StatusNet + * @author Brion Vibber <brion@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Takes parameters: + * + * - feed: a profile ID + * - token: session token to prevent CSRF attacks + * - ajax: boolean; whether to return Ajax or full-browser results + * + * Only works if the current user is logged in. + * + * @category Action + * @package StatusNet + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class EditMirrorAction extends BaseMirrorAction +{ + + /** + * Check pre-requisites and instantiate attributes + * + * @param Array $args array of arguments (URL, GET, POST) + * + * @return boolean success flag + */ + + function prepare($args) + { + parent::prepare($args); + + $this->profile = $this->validateProfile($this->trimmed('profile')); + + $this->mirror = SubMirror::pkeyGet(array('subscriber' => $this->user->id, + 'subscribed' => $this->profile->id)); + + if (!$this->mirror) { + $this->clientError(_m("Requested invalid profile to edit.")); + } + + $this->style = $this->validateStyle($this->trimmed('style')); + + // DO NOT change to $this->boolean(), it will be wrong. + // We're checking for the presence of the setting, not its value. + $this->delete = (bool)$this->arg('delete'); + + return true; + } + + protected function validateStyle($style) + { + $allowed = array('repeat', 'copy'); + if (in_array($style, $allowed)) { + return $style; + } else { + $this->clientError(_m("Bad form data.")); + } + } + + function saveMirror() + { + $mirror = SubMirror::getMirror($this->user, $this->profile); + if (!$mirror) { + $this->clientError(_m('Requested edit of missing mirror')); + } + + if ($this->delete) { + $mirror->delete(); + $oprofile = Ostatus_profile::staticGet('profile_id', $this->profile->id); + if ($oprofile) { + $oprofile->garbageCollect(); + } + } else if ($this->style != $mirror->style) { + $orig = clone($mirror); + $mirror->style = $this->style; + $mirror->modified = common_sql_now(); + $mirror->update($orig); + } + } +} diff --git a/plugins/SubMirror/actions/mirrorsettings.php b/plugins/SubMirror/actions/mirrorsettings.php new file mode 100644 index 000000000..5463a8dc0 --- /dev/null +++ b/plugins/SubMirror/actions/mirrorsettings.php @@ -0,0 +1,106 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Plugins + * @package StatusNet + * @author Brion Vibber <brion@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +class MirrorSettingsAction extends AccountSettingsAction +{ + /** + * Title of the page + * + * @return string Page title + */ + + function title() + { + return _m('Feed mirror settings'); + } + + /** + * Instructions for use + * + * @return string Instructions for use + */ + + function getInstructions() + { + return _m('You can mirror updates from many RSS and Atom feeds ' . + 'into your StatusNet timeline!'); + } + + /** + * Show the form for OpenID management + * + * We have one form with a few different submit buttons to do different things. + * + * @return void + */ + + function showContent() + { + $user = common_current_user(); + + $this->showAddFeedForm(); + + $mirror = new SubMirror(); + $mirror->subscriber = $user->id; + if ($mirror->find()) { + while ($mirror->fetch()) { + $this->showFeedForm($mirror); + } + } + } + + function showFeedForm($mirror) + { + $profile = Profile::staticGet('id', $mirror->subscribed); + if ($profile) { + $form = new EditMirrorForm($this, $profile); + $form->show(); + } + } + + function showAddFeedForm() + { + $form = new AddMirrorForm($this); + $form->show(); + } + + /** + * Handle a POST request + * + * Muxes to different sub-functions based on which button was pushed + * + * @return void + */ + + function handlePost() + { + } +} diff --git a/plugins/SubMirror/classes/SubMirror.php b/plugins/SubMirror/classes/SubMirror.php new file mode 100644 index 000000000..bd8fc80a5 --- /dev/null +++ b/plugins/SubMirror/classes/SubMirror.php @@ -0,0 +1,229 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * @package SubMirrorPlugin + * @maintainer Brion Vibber <brion@status.net> + */ + +class SubMirror extends Memcached_DataObject +{ + public $__table = 'submirror'; + + public $subscriber; + public $subscribed; + + public $style; + + public $created; + public $modified; + + public /*static*/ function staticGet($k, $v=null) + { + return parent::staticGet(__CLASS__, $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array('subscriber' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'subscribed' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + + 'style' => DB_DATAOBJECT_STR, + + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, + 'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); + } + + static function schemaDef() + { + // @fixme need a reverse key on (subscribed, subscriber) as well + return array(new ColumnDef('subscriber', 'integer', + null, false, 'PRI'), + new ColumnDef('subscribed', 'integer', + null, false, 'PRI'), + + new ColumnDef('style', 'varchar', + 16, true), + + new ColumnDef('created', 'datetime', + null, false), + new ColumnDef('modified', 'datetime', + null, false)); + } + + /** + * Temporary hack to set up the compound index, since we can't do + * it yet through regular Schema interface. (Coming for 1.0...) + * + * @param Schema $schema + * @return void + */ + static function fixIndexes($schema) + { + try { + $schema->createIndex('submirror', array('subscribed', 'subscriber')); + } catch (Exception $e) { + common_log(LOG_ERR, __METHOD__ . ': ' . $e->getMessage()); + } + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has; this function + * defines them. + * + * @return array key definitions + */ + + function keys() + { + return array_keys($this->keyTypes()); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. + * + * @return array key definitions + */ + + function keyTypes() + { + // @fixme keys + // need a sane key for reverse lookup too + return array('subscriber' => 'K', 'subscribed' => 'K'); + } + + function sequenceKey() + { + return array(false, false, false); + } + + /** + * @param Profile $subscribed + * @param Profile $subscribed + * @return SubMirror + * @throws ServerException + */ + public static function saveMirror($subscriber, $subscribed, $style='repeat') + { + // @fixme make sure they're subscribed! + $mirror = new SubMirror(); + + $mirror->subscriber = $subscriber->id; + $mirror->subscribed = $subscribed->id; + $mirror->style = $style; + + $mirror->created = common_sql_now(); + $mirror->modified = common_sql_now(); + $mirror->insert(); + + return $mirror; + } + + /** + * @param Notice $notice + * @return mixed Notice on successful mirroring, boolean if not + */ + public function mirrorNotice($notice) + { + $profile = Profile::staticGet('id', $this->subscriber); + if (!$profile) { + common_log(LOG_ERROR, "SubMirror plugin skipping auto-repeat of notice $notice->id for missing user $profile->id"); + return false; + } + + if ($this->style == 'copy') { + return $this->copyNotice($profile, $notice); + } else { // default to repeat mode + return $this->repeatNotice($profile, $notice); + } + } + + /** + * Mirror a notice using StatusNet's repeat functionality. + * This retains attribution within the site, and other nice things, + * but currently ends up looking like 'RT @foobar bla bla' when + * bridged out over OStatus or TwitterBridge. + * + * @param Notice $notice + * @return mixed Notice on successful repeat, true if already repeated, false on failure + */ + protected function repeatNotice($profile, $notice) + { + if($profile->hasRepeated($notice->id)) { + common_log(LOG_INFO, "SubMirror plugin skipping auto-repeat of notice $notice->id for user $profile->id; already repeated."); + return true; + } else { + common_log(LOG_INFO, "SubMirror plugin auto-repeating notice $notice->id for $profile->id"); + return $notice->repeat($profile->id, 'mirror'); + } + } + + /** + * Mirror a notice by emitting a new notice with the same contents. + * Kind of dirty, but if pulling an external data feed into an account + * that may be what you want. + * + * @param Notice $notice + * @return mixed Notice on successful repeat, true if already repeated, false on failure + */ + protected function copyNotice($profile, $notice) + { + $options = array('is_local' => Notice::LOCAL_PUBLIC, + 'url' => $notice->bestUrl(), // pass through the foreign link... + 'rendered' => $notice->rendered); + + $saved = Notice::saveNew($profile->id, + $notice->content, + 'feed', + $options); + return $saved; + } + + public /*static*/ function pkeyGet($v) + { + return parent::pkeyGet(__CLASS__, $v); + } + + /** + * Get the mirroring setting for a pair of profiles, if existing. + * + * @param Profile $subscriber + * @param Profile $subscribed + * @return mixed Profile or empty + */ + public static function getMirror($subscriber, $subscribed) + { + return self::pkeyGet(array('subscriber' => $subscriber->id, + 'subscribed' => $subscribed->id)); + } +} diff --git a/plugins/SubMirror/lib/addmirrorform.php b/plugins/SubMirror/lib/addmirrorform.php new file mode 100644 index 000000000..0a798c9ea --- /dev/null +++ b/plugins/SubMirror/lib/addmirrorform.php @@ -0,0 +1,141 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @package StatusNet + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +class AddMirrorForm extends Form +{ + + /** + * Name of the form + * + * Sub-classes should overload this with the name of their form. + * + * @return void + */ + + function formLegend() + { + } + + /** + * Visible or invisible data elements + * + * Display the form fields that make up the data of the form. + * Sub-classes should overload this to show their data. + * + * @return void + */ + + function formData() + { + $this->out->elementStart('fieldset'); + + $this->out->elementStart('ul'); + + $this->li(); + $this->doInput('addmirror-feedurl', + 'feedurl', + _m('Web page or feed URL:'), + $this->out->trimmed('feedurl')); + $this->unli(); + + $this->li(); + $this->out->submit('addmirror-save', _m('Add feed')); + $this->unli(); + $this->out->elementEnd('ul'); + $this->out->elementEnd('fieldset'); + } + + private function doInput($id, $name, $label, $value=null, $instructions=null) + { + $this->out->element('label', array('for' => $id), $label); + $attrs = array('name' => $name, + 'type' => 'text', + 'id' => $id, + 'style' => 'width: 80%'); + if ($value) { + $attrs['value'] = $value; + } + $this->out->element('input', $attrs); + if ($instructions) { + $this->out->element('p', 'form_guide', $instructions); + } + } + + /** + * Buttons for form actions + * + * Submit and cancel buttons (or whatever) + * Sub-classes should overload this to show their own buttons. + * + * @return void + */ + + function formActions() + { + } + + /** + * ID of the form + * + * Should be unique on the page. Sub-classes should overload this + * to show their own IDs. + * + * @return string ID of the form + */ + + function id() + { + return 'add-mirror-form'; + } + + /** + * Action of the form. + * + * URL to post to. Should be overloaded by subclasses to give + * somewhere to post to. + * + * @return string URL to post to + */ + + function action() + { + return common_local_url('addmirror'); + } + + /** + * Class of the form. + * + * @return string the form's class + */ + + function formClass() + { + return 'form_settings'; + } + +} diff --git a/plugins/SubMirror/lib/editmirrorform.php b/plugins/SubMirror/lib/editmirrorform.php new file mode 100644 index 000000000..8236da389 --- /dev/null +++ b/plugins/SubMirror/lib/editmirrorform.php @@ -0,0 +1,189 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @package StatusNet + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +class EditMirrorForm extends Form +{ + function __construct($action, $profile) + { + parent::__construct($action); + + $this->profile = clone($profile); + $this->user = common_current_user(); + $this->mirror = SubMirror::pkeyGet(array('subscriber' => $this->user->id, + 'subscribed' => $this->profile->id)); + } + + /** + * Name of the form + * + * Sub-classes should overload this with the name of their form. + * + * @return void + */ + + function formLegend() + { + } + + /** + * Visible or invisible data elements + * + * Display the form fields that make up the data of the form. + * Sub-classes should overload this to show their data. + * + * @return void + */ + + function formData() + { + $this->out->elementStart('fieldset'); + + $this->out->hidden('profile', $this->profile->id); + + $this->out->elementStart('div', array('style' => 'float: left; width: 80px;')); + $img = $this->getAvatar($this->profile); + $feed = $this->getFeed($this->profile); + $this->out->elementStart('a', array('href' => $this->profile->profileurl)); + $this->out->element('img', array('src' => $img, 'style' => 'float: left')); + $this->out->elementEnd('a'); + $this->out->elementEnd('div'); + + + $this->out->elementStart('div', array('style' => 'margin-left: 80px; margin-right: 20px')); + $this->out->elementStart('p'); + $this->out->elementStart('div'); + $this->out->element('a', array('href' => $this->profile->profileurl), $this->profile->getBestName()); + $this->out->elementEnd('div'); + $this->out->elementStart('div'); + if ($feed) { + $this->out->text(_m('LABEL', 'Remote feed:') . ' '); + //$this->out->element('a', array('href' => $feed), $feed); + $this->out->element('input', array('value' => $feed, 'readonly' => 'readonly', 'style' => 'width: 100%')); + } else { + $this->out->text(_m('LABEL', 'Local user')); + } + $this->out->elementEnd('div'); + $this->out->elementEnd('p'); + + $this->out->elementStart('fieldset', array('style' => 'margin-top: 20px')); + $this->out->element('legend', false, _m("Mirroring style")); + + $styles = array('repeat' => _m("Repeat: reference the original user's post (sometimes shows as 'RT @blah')"), + 'copy' => _m("Repost the content under my account")); + foreach ($styles as $key => $label) { + $this->out->elementStart('div'); + $attribs = array('type' => 'radio', + 'value' => $key, + 'name' => 'style', + 'id' => $this->id() . '-style'); + if ($key == $this->mirror->style || ($key == 'repeat' && empty($this->mirror->style))) { + $attribs['checked'] = 'checked'; + } + $this->out->element('input', $attribs); + $this->out->element('span', false, $label); // @fixme should be label, but the styles muck it up for now + $this->out->elementEnd('div'); + + } + $this->out->elementEnd('fieldset'); + + + $this->out->elementStart('div'); + $this->out->submit($this->id() . '-save', _m('Save')); + $this->out->element('input', array('type' => 'submit', + 'value' => _m('Stop mirroring'), + 'name' => 'delete', + 'class' => 'submit')); + $this->out->elementEnd('div'); + + $this->out->elementEnd('div'); + $this->out->elementEnd('fieldset'); + } + + private function getAvatar($profile) + { + $avatar = $this->profile->getAvatar(48); + if ($avatar) { + return $avatar->displayUrl(); + } else { + return Avatar::defaultImage(48); + } + } + + private function getFeed($profile) + { + // Ok this is a bit of a hack. ;) + if (class_exists('Ostatus_profile')) { + $oprofile = Ostatus_profile::staticGet('profile_id', $profile->id); + if ($oprofile) { + return $oprofile->feeduri; + } + } + var_dump('wtf'); + return false; + } + + /** + * ID of the form + * + * Should be unique on the page. Sub-classes should overload this + * to show their own IDs. + * + * @return string ID of the form + */ + + function id() + { + return 'edit-mirror-form-' . $this->profile->id; + } + + /** + * Action of the form. + * + * URL to post to. Should be overloaded by subclasses to give + * somewhere to post to. + * + * @return string URL to post to + */ + + function action() + { + return common_local_url('editmirror'); + } + + /** + * Class of the form. + * + * @return string the form's class + */ + + function formClass() + { + return 'form_settings'; + } + +} diff --git a/plugins/SubMirror/lib/mirrorqueuehandler.php b/plugins/SubMirror/lib/mirrorqueuehandler.php new file mode 100644 index 000000000..92b36b5eb --- /dev/null +++ b/plugins/SubMirror/lib/mirrorqueuehandler.php @@ -0,0 +1,45 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * Check for subscription mirroring options on each newly seen post! + * + * @package SubMirror + * @author Brion Vibber <brion@status.net> + */ + +class MirrorQueueHandler extends QueueHandler +{ + function transport() + { + return 'mirror'; + } + + function handle($notice) + { + $mirror = new SubMirror(); + $mirror->subscribed = $notice->profile_id; + if ($mirror->find()) { + while ($mirror->fetch()) { + $mirror->mirrorNotice($notice); + } + } + return true; + } +} diff --git a/plugins/TabFocus/TabFocusPlugin.php b/plugins/TabFocus/TabFocusPlugin.php index bf89c478c..46e329d8a 100644 --- a/plugins/TabFocus/TabFocusPlugin.php +++ b/plugins/TabFocus/TabFocusPlugin.php @@ -23,7 +23,7 @@ * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> * @author Paul Irish <paul.irish@isobar.net> - * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/TabFocus/locale/TabFocus.pot b/plugins/TabFocus/locale/TabFocus.pot new file mode 100644 index 000000000..3b0e3c261 --- /dev/null +++ b/plugins/TabFocus/locale/TabFocus.pot @@ -0,0 +1,24 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: TabFocusPlugin.php:54 +msgid "" +"TabFocus changes the notice form behavior so that, while in the text area, " +"pressing the tab key focuses the \"Send\" button, matching the behavor of " +"Twitter." +msgstr "" diff --git a/plugins/TightUrl/TightUrlPlugin.php b/plugins/TightUrl/TightUrlPlugin.php index e2d494a7b..b8e5addb1 100644 --- a/plugins/TightUrl/TightUrlPlugin.php +++ b/plugins/TightUrl/TightUrlPlugin.php @@ -22,7 +22,7 @@ * @category Plugin * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> - * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/TightUrl/locale/TightUrl.pot b/plugins/TightUrl/locale/TightUrl.pot new file mode 100644 index 000000000..10f59a1e8 --- /dev/null +++ b/plugins/TightUrl/locale/TightUrl.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: TightUrlPlugin.php:68 +#, php-format +msgid "Uses <a href=\"http://%1$s/\">%1$s</a> URL-shortener service." +msgstr "" diff --git a/plugins/TwitterBridge/TwitterBridgePlugin.php b/plugins/TwitterBridge/TwitterBridgePlugin.php index 1a0a69682..0505a328f 100644 --- a/plugins/TwitterBridge/TwitterBridgePlugin.php +++ b/plugins/TwitterBridge/TwitterBridgePlugin.php @@ -50,6 +50,7 @@ class TwitterBridgePlugin extends Plugin { const VERSION = STATUSNET_VERSION; + public $adminImportControl = false; // Should the 'import' checkbox be exposed in the admin panel? /** * Initializer for the plugin. @@ -221,7 +222,7 @@ class TwitterBridgePlugin extends Plugin */ function onStartEnqueueNotice($notice, &$transports) { - if (self::hasKeys()) { + if (self::hasKeys() && $notice->isLocal()) { // Avoid a possible loop if ($notice->source != 'twitter') { array_push($transports, 'twitter'); @@ -322,5 +323,17 @@ class TwitterBridgePlugin extends Plugin return true; } + /** + * Expose the adminImportControl setting to the administration panel code. + * This allows us to disable the import bridge enabling checkbox for administrators, + * since on a bulk farm site we can't yet automate the import daemon setup. + * + * @return boolean hook value; + */ + function onTwitterBridgeAdminImportControl() + { + return (bool)$this->adminImportControl; + } + } diff --git a/plugins/TwitterBridge/daemons/twitterstatusfetcher.php b/plugins/TwitterBridge/daemons/twitterstatusfetcher.php index bff657eb6..7c624fdb3 100755 --- a/plugins/TwitterBridge/daemons/twitterstatusfetcher.php +++ b/plugins/TwitterBridge/daemons/twitterstatusfetcher.php @@ -44,10 +44,17 @@ require_once INSTALLDIR . '/plugins/TwitterBridge/twitterbasicauthclient.php'; require_once INSTALLDIR . '/plugins/TwitterBridge/twitteroauthclient.php'; /** - * Fetcher for statuses from Twitter + * Fetch statuses from Twitter * - * Fetches statuses from Twitter and inserts them as notices in local - * system. + * Fetches statuses from Twitter and inserts them as notices + * + * NOTE: an Avatar path MUST be set in config.php for this + * script to work, e.g.: + * $config['avatar']['path'] = $config['site']['path'] . '/avatar/'; + * + * @todo @fixme @gar Fix the above. For some reason $_path is always empty when + * this script is run, so the default avatar path is always set wrong in + * default.php. Therefore it must be set explicitly in config.php. --Z * * @category Twitter * @package StatusNet @@ -57,9 +64,6 @@ require_once INSTALLDIR . '/plugins/TwitterBridge/twitteroauthclient.php'; * @link http://status.net/ */ -// NOTE: an Avatar path MUST be set in config.php for this -// script to work: e.g.: $config['avatar']['path'] = '/statusnet/avatar'; - class TwitterStatusFetcher extends ParallelizingDaemon { /** @@ -195,6 +199,8 @@ class TwitterStatusFetcher extends ParallelizingDaemon return; } + common_debug(LOG_INFO, $this->name() . ' - Retrieved ' . sizeof($timeline) . ' statuses from Twitter.'); + // Reverse to preserve order foreach (array_reverse($timeline) as $status) { @@ -209,13 +215,7 @@ class TwitterStatusFetcher extends ParallelizingDaemon continue; } - $notice = null; - - $notice = $this->saveStatus($status, $flink); - - if (!empty($notice)) { - common_broadcast_notice($notice); - } + $this->saveStatus($status, $flink); } // Okay, record the time we synced with Twitter for posterity @@ -226,50 +226,77 @@ class TwitterStatusFetcher extends ParallelizingDaemon function saveStatus($status, $flink) { - $id = $this->ensureProfile($status->user); - - $profile = Profile::staticGet($id); + $profile = $this->ensureProfile($status->user); if (empty($profile)) { common_log(LOG_ERR, $this->name() . ' - Problem saving notice. No associated Profile.'); - return null; + return; } - // XXX: change of screen name? - - $uri = 'http://twitter.com/' . $status->user->screen_name . - '/status/' . $status->id; + $statusUri = 'http://twitter.com/' + . $status->user->screen_name + . '/status/' + . $status->id; // check to see if we've already imported the status - $notice = Notice::staticGet('uri', $uri); + $dupe = $this->checkDupe($profile, $statusUri); + + if (!empty($dupe)) { + common_log( + LOG_INFO, + $this->name() . + " - Ignoring duplicate import: $statusUri" + ); + return; + } + + $notice = new Notice(); - if (empty($notice)) { + $notice->profile_id = $profile->id; + $notice->uri = $statusUri; + $notice->url = $statusUri; + $notice->created = strftime( + '%Y-%m-%d %H:%M:%S', + strtotime($status->created_at) + ); - // XXX: transaction here? + $notice->source = 'twitter'; + $notice->reply_to = null; + $notice->is_local = Notice::GATEWAY; - $notice = new Notice(); + $notice->content = common_shorten_links($status->text); + $notice->rendered = common_render_content( + $notice->content, + $notice + ); - $notice->profile_id = $id; - $notice->uri = $uri; - $notice->created = strftime('%Y-%m-%d %H:%M:%S', - strtotime($status->created_at)); - $notice->content = common_shorten_links($status->text); // XXX - $notice->rendered = common_render_content($notice->content, $notice); - $notice->source = 'twitter'; - $notice->reply_to = null; // XXX: lookup reply - $notice->is_local = Notice::GATEWAY; + if (Event::handle('StartNoticeSave', array(&$notice))) { - if (Event::handle('StartNoticeSave', array(&$notice))) { - $notice->insert(); - Event::handle('EndNoticeSave', array($notice)); + $id = $notice->insert(); + + if (!$id) { + common_log_db_error($notice, 'INSERT', __FILE__); + common_log(LOG_ERR, $this->name() . + ' - Problem saving notice.'); } + Event::handle('EndNoticeSave', array($notice)); } - Inbox::insertNotice($flink->user_id, $notice->id); + $orig = clone($notice); + $conv = Conversation::create(); + + $notice->conversation = $conv->id; + + if (!$notice->update($orig)) { + common_log_db_error($notice, 'UPDATE', __FILE__); + common_log(LOG_ERR, $this->name() . + ' - Problem saving notice.'); + } + Inbox::insertNotice($flink->user_id, $notice->id); $notice->blowOnInsert(); return $notice; @@ -279,9 +306,10 @@ class TwitterStatusFetcher extends ParallelizingDaemon * Look up a Profile by profileurl field. Profile::staticGet() was * not working consistently. * - * @param string $url the profile url + * @param string $nickname local nickname of the Twitter user + * @param string $profileurl the profile url * - * @return mixed the first profile with that url, or null + * @return mixed value the first Profile with that url, or null */ function getProfileByUrl($nickname, $profileurl) @@ -299,6 +327,30 @@ class TwitterStatusFetcher extends ParallelizingDaemon return null; } + /** + * Check to see if this Twitter status has already been imported + * + * @param Profile $profile Twitter user's local profile + * @param string $statusUri URI of the status on Twitter + * + * @return mixed value a matching Notice or null + */ + + function checkDupe($profile, $statusUri) + { + $notice = new Notice(); + $notice->uri = $statusUri; + $notice->profile_id = $profile->id; + $notice->limit(1); + + if ($notice->find()) { + $notice->fetch(); + return $notice; + } + + return null; + } + function ensureProfile($user) { // check to see if there's already a profile for this user @@ -313,7 +365,7 @@ class TwitterStatusFetcher extends ParallelizingDaemon // Check to see if the user's Avatar has changed $this->checkAvatar($user, $profile); - return $profile->id; + return $profile; } else { @@ -372,7 +424,7 @@ class TwitterStatusFetcher extends ParallelizingDaemon $this->saveAvatars($user, $id); - return $id; + return $profile; } } @@ -403,7 +455,6 @@ class TwitterStatusFetcher extends ParallelizingDaemon $this->updateAvatars($twitter_user, $profile); } - } function updateAvatars($twitter_user, $profile) { @@ -428,17 +479,13 @@ class TwitterStatusFetcher extends ParallelizingDaemon } function missingAvatarFile($profile) { - foreach (array(24, 48, 73) as $size) { - $filename = $profile->getAvatar($size)->filename; $avatarpath = Avatar::path($filename); - if (file_exists($avatarpath) == FALSE) { return true; } } - return false; } diff --git a/plugins/TwitterBridge/locale/TwitterBridge.po b/plugins/TwitterBridge/locale/TwitterBridge.pot index eff125579..c7ac8053c 100644 --- a/plugins/TwitterBridge/locale/TwitterBridge.po +++ b/plugins/TwitterBridge/locale/TwitterBridge.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-03-01 14:58-0800\n" +"POT-Creation-Date: 2010-04-29 23:39+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -16,11 +16,11 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: twitter.php:320 +#: twitter.php:342 msgid "Your Twitter bridge has been disabled." msgstr "" -#: twitter.php:324 +#: twitter.php:346 #, php-format msgid "" "Hi, %1$s. We're sorry to inform you that your link to Twitter has been " @@ -36,28 +36,97 @@ msgid "" "%3$s\n" msgstr "" -#: twitterauthorization.php:181 twitterauthorization.php:229 -msgid "Couldn't link your Twitter account." +#: TwitterBridgePlugin.php:155 TwitterBridgePlugin.php:178 +#: TwitterBridgePlugin.php:291 twitteradminpanel.php:54 +msgid "Twitter" msgstr "" -#: twitterauthorization.php:201 -msgid "Couldn't link your Twitter account: oauth_token mismatch." +#: TwitterBridgePlugin.php:156 +msgid "Login or register using Twitter" msgstr "" -#: TwitterBridgePlugin.php:114 -msgid "Twitter" +#: TwitterBridgePlugin.php:179 +msgid "Twitter integration options" msgstr "" -#: TwitterBridgePlugin.php:115 -msgid "Twitter integration options" +#: TwitterBridgePlugin.php:292 +msgid "Twitter bridge configuration" msgstr "" -#: TwitterBridgePlugin.php:207 +#: TwitterBridgePlugin.php:317 msgid "" "The Twitter \"bridge\" plugin allows you to integrate your StatusNet " "instance with <a href=\"http://twitter.com/\">Twitter</a>." msgstr "" +#: twitteradminpanel.php:65 +msgid "Twitter bridge settings" +msgstr "" + +#: twitteradminpanel.php:148 +msgid "Invalid consumer key. Max length is 255 characters." +msgstr "" + +#: twitteradminpanel.php:154 +msgid "Invalid consumer secret. Max length is 255 characters." +msgstr "" + +#: twitteradminpanel.php:207 +msgid "Twitter application settings" +msgstr "" + +#: twitteradminpanel.php:213 +msgid "Consumer key" +msgstr "" + +#: twitteradminpanel.php:214 +msgid "Consumer key assigned by Twitter" +msgstr "" + +#: twitteradminpanel.php:222 +msgid "Consumer secret" +msgstr "" + +#: twitteradminpanel.php:223 +msgid "Consumer secret assigned by Twitter" +msgstr "" + +#: twitteradminpanel.php:240 +msgid "Integration source" +msgstr "" + +#: twitteradminpanel.php:241 +msgid "Name of your Twitter application" +msgstr "" + +#: twitteradminpanel.php:253 +msgid "Options" +msgstr "" + +#: twitteradminpanel.php:260 +msgid "Enable \"Sign-in with Twitter\"" +msgstr "" + +#: twitteradminpanel.php:262 +msgid "Allow users to login with their Twitter credentials" +msgstr "" + +#: twitteradminpanel.php:268 +msgid "Enable Twitter import" +msgstr "" + +#: twitteradminpanel.php:270 +msgid "Allow users to import their Twitter friends' timelines" +msgstr "" + +#: twitterauthorization.php:181 twitterauthorization.php:229 +msgid "Couldn't link your Twitter account." +msgstr "" + +#: twitterauthorization.php:201 +msgid "Couldn't link your Twitter account: oauth_token mismatch." +msgstr "" + #: twittersettings.php:59 msgid "Twitter settings" msgstr "" diff --git a/plugins/TwitterBridge/twitter.php b/plugins/TwitterBridge/twitter.php index 2805b3ab5..306ba2442 100644 --- a/plugins/TwitterBridge/twitter.php +++ b/plugins/TwitterBridge/twitter.php @@ -75,8 +75,6 @@ function save_twitter_user($twitter_id, $screen_name) if (!empty($fuser)) { - $result = true; - // Delete old record if Twitter user changed screen name if ($fuser->nickname != $screen_name) { @@ -88,6 +86,25 @@ function save_twitter_user($twitter_id, $screen_name) $screen_name, $oldname)); } + + } else { + + // Kill any old, invalid records for this screen name + + $fuser = Foreign_user::getByNickname($screen_name, TWITTER_SERVICE); + + if (!empty($fuser)) { + $fuser->delete(); + common_log( + LOG_INFO, + sprintf( + 'Twitter bridge - deteted old record for Twitter ' . + 'screen name "%s" belonging to Twitter ID %d.', + $screen_name, + $fuser->id + ) + ); + } } return add_twitter_user($twitter_id, $screen_name); @@ -124,15 +141,36 @@ function broadcast_twitter($notice) return true; } +/** + * Pull any extra information from a notice that we should transfer over + * to Twitter beyond the notice text itself. + * + * @param Notice $notice + * @return array of key-value pairs for Twitter update submission + * @access private + */ +function twitter_update_params($notice) +{ + $params = array(); + if ($notice->lat || $notice->lon) { + $params['lat'] = $notice->lat; + $params['long'] = $notice->lon; + } + return $params; +} + + function broadcast_oauth($notice, $flink) { $user = $flink->getUser(); $statustxt = format_status($notice); + $params = twitter_update_params($notice); + $token = TwitterOAuthClient::unpackToken($flink->credentials); $client = new TwitterOAuthClient($token->key, $token->secret); $status = null; try { - $status = $client->statusesUpdate($statustxt); + $status = $client->statusesUpdate($statustxt, $params); } catch (OAuthClientException $e) { return process_error($e, $flink, $notice); } @@ -171,12 +209,13 @@ function broadcast_basicauth($notice, $flink) $user = $flink->getUser(); $statustxt = format_status($notice); + $params = twitter_update_params($notice); $client = new TwitterBasicAuthClient($flink); $status = null; try { - $status = $client->statusesUpdate($statustxt); + $status = $client->statusesUpdate($statustxt, $params); } catch (BasicAuthException $e) { return process_error($e, $flink, $notice); } @@ -313,10 +352,10 @@ function remove_twitter_link($flink) function mail_twitter_bridge_removed($user) { - common_init_locale($user->language); - $profile = $user->getProfile(); + common_switch_locale($user->language); + $subject = sprintf(_m('Your Twitter bridge has been disabled.')); $site_name = common_config('site', 'name'); @@ -332,7 +371,7 @@ function mail_twitter_bridge_removed($user) common_local_url('twittersettings'), common_config('site', 'name')); - common_init_locale(); + common_switch_locale(); return mail_to_user($user, $subject, $body); } diff --git a/plugins/TwitterBridge/twitteradminpanel.php b/plugins/TwitterBridge/twitteradminpanel.php index a78a92c66..69f8da078 100644 --- a/plugins/TwitterBridge/twitteradminpanel.php +++ b/plugins/TwitterBridge/twitteradminpanel.php @@ -92,9 +92,11 @@ class TwitteradminpanelAction extends AdminPanelAction ); static $booleans = array( - 'twitter' => array('signin'), - 'twitterimport' => array('enabled') + 'twitter' => array('signin') ); + if (Event::handle('TwitterBridgeAdminImportControl')) { + $booleans['twitterimport'] = array('enabled'); + } $values = array(); @@ -155,6 +157,13 @@ class TwitteradminpanelAction extends AdminPanelAction ); } } + + function isImportEnabled() + { + // Since daemon setup isn't automated yet... + // @todo: if merged into main queues, detect presence of daemon config + return true; + } } class TwitterAdminPanelForm extends AdminForm @@ -263,13 +272,15 @@ class TwitterAdminPanelForm extends AdminForm ); $this->unli(); - $this->li(); - $this->out->checkbox( - 'enabled', _m('Enable Twitter import'), - (bool) $this->value('enabled', 'twitterimport'), - _m('Allow users to import their Twitter friends\' timelines') - ); - $this->unli(); + if (Event::handle('TwitterBridgeAdminImportControl')) { + $this->li(); + $this->out->checkbox( + 'enabled', _m('Enable Twitter import'), + (bool) $this->value('enabled', 'twitterimport'), + _m('Allow users to import their Twitter friends\' timelines. Requires daemons to be manually configured.') + ); + $this->unli(); + } $this->out->elementEnd('ul'); diff --git a/plugins/TwitterBridge/twitterauthorization.php b/plugins/TwitterBridge/twitterauthorization.php index bc004cb95..7a896e168 100644 --- a/plugins/TwitterBridge/twitterauthorization.php +++ b/plugins/TwitterBridge/twitterauthorization.php @@ -332,6 +332,11 @@ class TwitterauthorizationAction extends Action parent::showPage(); } + /** + * @fixme much of this duplicates core code, which is very fragile. + * Should probably be replaced with an extensible mini version of + * the core registration form. + */ function showContent() { if (!empty($this->message_text)) { @@ -353,10 +358,15 @@ class TwitterauthorizationAction extends Action 'name' => 'license', 'value' => 'true')); $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license')); - $this->text(_('My text and files are available under ')); - $this->element('a', array('href' => common_config('license', 'url')), - common_config('license', 'title')); - $this->text(_(' except this private data: password, email address, IM address, phone number.')); + $message = _('My text and files are available under %s ' . + 'except this private data: password, ' . + 'email address, IM address, and phone number.'); + $link = '<a href="' . + htmlspecialchars(common_config('license', 'url')) . + '">' . + htmlspecialchars(common_config('license', 'title')) . + '</a>'; + $this->raw(sprintf(htmlspecialchars($message), $link)); $this->elementEnd('label'); $this->elementEnd('li'); $this->elementEnd('ul'); diff --git a/plugins/TwitterBridge/twitterbasicauthclient.php b/plugins/TwitterBridge/twitterbasicauthclient.php index fd26293f9..2c18c9469 100644 --- a/plugins/TwitterBridge/twitterbasicauthclient.php +++ b/plugins/TwitterBridge/twitterbasicauthclient.php @@ -76,18 +76,21 @@ class TwitterBasicAuthClient /** * 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 + * @param string $status text of the status + * @param mixed $params optional other parameters to pass to Twitter, + * as defined. For back-compatibility, if an int + * is passed we'll consider it a reply-to ID. * * @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); + if (is_numeric($params)) { + $params = array('in_reply_to_status_id' => intval($params)); + } + $params['status'] = $status; + $params['source'] = common_config('integration', 'source'); $response = $this->httpRequest($url, $params); $status = json_decode($response); return $status; diff --git a/plugins/TwitterBridge/twitteroauthclient.php b/plugins/TwitterBridge/twitteroauthclient.php index 93f6aadd1..d895d8c73 100644 --- a/plugins/TwitterBridge/twitteroauthclient.php +++ b/plugins/TwitterBridge/twitteroauthclient.php @@ -166,17 +166,22 @@ class TwitterOAuthClient extends OAuthClient /** * 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 + * @param string $status text of the status + * @param mixed $params optional other parameters to pass to Twitter, + * as defined. For back-compatibility, if an int + * is passed we'll consider it a reply-to ID. * * @return mixed the status */ - function statusesUpdate($status, $in_reply_to_status_id = null) + function statusesUpdate($status, $params=array()) { $url = 'https://twitter.com/statuses/update.json'; - $params = array('status' => $status, - 'in_reply_to_status_id' => $in_reply_to_status_id); + if (is_numeric($params)) { + $params = array('in_reply_to_status_id' => intval($params)); + } + $params['status'] = $status; + // We don't have to pass 'source' as the oauth key is tied to an app. + $response = $this->oAuthPost($url, $params); $status = json_decode($response); return $status; diff --git a/plugins/UrlShortener/UrlShortenerPlugin.php b/plugins/UrlShortener/UrlShortenerPlugin.php index 027624b7a..41f64bb26 100644 --- a/plugins/UrlShortener/UrlShortenerPlugin.php +++ b/plugins/UrlShortener/UrlShortenerPlugin.php @@ -22,6 +22,7 @@ * @category Plugin * @package StatusNet * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/plugins/UserFlag/clearflag.php b/plugins/UserFlag/clearflag.php index bd6732e2d..f032527ed 100644 --- a/plugins/UserFlag/clearflag.php +++ b/plugins/UserFlag/clearflag.php @@ -81,7 +81,7 @@ class ClearflagAction extends ProfileFormAction if ($_SERVER['REQUEST_METHOD'] == 'POST') { $this->handlePost(); if (!$this->boolean('ajax')) { - $this->returnToArgs(); + $this->returnToPrevious(); } } } diff --git a/plugins/UserFlag/flagprofile.php b/plugins/UserFlag/flagprofile.php index 2d0f0abb9..018c1e8ac 100644 --- a/plugins/UserFlag/flagprofile.php +++ b/plugins/UserFlag/flagprofile.php @@ -87,7 +87,7 @@ class FlagprofileAction extends ProfileFormAction if ($_SERVER['REQUEST_METHOD'] == 'POST') { $this->handlePost(); if (!$this->boolean('ajax')) { - $this->returnToArgs(); + $this->returnToPrevious(); } } } diff --git a/plugins/WikiHowProfile/README b/plugins/WikiHowProfile/README new file mode 100644 index 000000000..ee6096c9f --- /dev/null +++ b/plugins/WikiHowProfile/README @@ -0,0 +1,6 @@ +This is an additional plugin which piggybacks on OpenID authentication to pull +profile information from WikiHow user pages when creating or updating accounts. + +WikiHow runs a customized MediaWiki setup, with locally-built extensions to add +profile features such as an avatar. As this additional info isn't yet exposed +through OpenID, we need to pull it separately. diff --git a/plugins/WikiHowProfile/WikiHowProfilePlugin.php b/plugins/WikiHowProfile/WikiHowProfilePlugin.php new file mode 100644 index 000000000..b72bd55d6 --- /dev/null +++ b/plugins/WikiHowProfile/WikiHowProfilePlugin.php @@ -0,0 +1,196 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * Plugin to pull WikiHow-style user avatars at OpenID setup time. + * These are not currently exposed via OpenID. + * + * PHP version 5 + * + * 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 Plugins + * @package StatusNet + * @author Brion Vibber <brion@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Sample plugin main class + * + * Each plugin requires a main class to interact with the StatusNet system. + * + * @category Plugins + * @package WikiHowProfilePlugin + * @author Brion Vibber <brion@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class WikiHowProfilePlugin extends Plugin +{ + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'WikiHow avatar fetcher', + 'version' => STATUSNET_VERSION, + 'author' => 'Brion Vibber', + 'homepage' => 'http://status.net/wiki/Plugin:Sample', + 'rawdescription' => + _m('Fetches avatar and other profile info for WikiHow users when setting up an account via OpenID.')); + return true; + } + + /** + * Hook for OpenID user creation; we'll pull the avatar. + * + * @param User $user + * @param string $canonical OpenID provider URL + * @param array $sreg query data from provider + */ + function onEndOpenIDCreateNewUser($user, $canonical, $sreg) + { + $this->updateProfile($user, $canonical); + return true; + } + + /** + * Hook for OpenID profile updating; we'll pull the avatar. + * + * @param User $user + * @param string $canonical OpenID provider URL (wiki profile page) + * @param array $sreg query data from provider + */ + function onEndOpenIDUpdateUser($user, $canonical, $sreg) + { + $this->updateProfile($user, $canonical); + return true; + } + + /** + * @param User $user + * @param string $canonical OpenID provider URL (wiki profile page) + */ + private function updateProfile($user, $canonical) + { + $prefix = 'http://www.wikihow.com/User:'; + + if (substr($canonical, 0, strlen($prefix)) == $prefix) { + // Yes, it's a WikiHow user! + $profile = $this->fetchProfile($canonical); + + if (!empty($profile['avatar'])) { + $this->saveAvatar($user, $profile['avatar']); + } + } + } + + /** + * Given a user's WikiHow profile URL, find their avatar. + * + * @param string $profileUrl user page on the wiki + * + * @return array of data; possible members: + * 'avatar' => full URL to avatar image + * + * @throws Exception on various low-level failures + * + * @todo pull location, web site, and about sections -- they aren't currently marked up cleanly. + */ + private function fetchProfile($profileUrl) + { + $client = HTTPClient::start(); + $response = $client->get($profileUrl); + if (!$response->isOk()) { + throw new Exception("WikiHow profile page fetch failed."); + // HTTP error response already logged. + return false; + } + + // Suppress warnings during HTML parsing; non-well-formed bits will + // spew horrible warning everywhere even though it works fine. + $old = error_reporting(); + error_reporting($old & ~E_WARNING); + + $dom = new DOMDocument(); + $ok = $dom->loadHTML($response->getBody()); + + error_reporting($old); + + if (!$ok) { + throw new Exception("HTML parse failure during check for WikiHow avatar."); + return false; + } + + $data = array(); + + $avatar = $dom->getElementById('avatarULimg'); + if ($avatar) { + $src = $avatar->getAttribute('src'); + + $base = new Net_URL2($profileUrl); + $absolute = $base->resolve($src); + $avatarUrl = strval($absolute); + + common_log(LOG_DEBUG, "WikiHow avatar found for $profileUrl - $avatarUrl"); + $data['avatar'] = $avatarUrl; + } + + return $data; + } + + /** + * Actually save the avatar we found locally. + * + * @param User $user + * @param string $url to avatar URL + * @todo merge wrapper funcs for this into common place for 1.0 core + */ + private function saveAvatar($user, $url) + { + if (!common_valid_http_url($url)) { + throw new ServerException(sprintf(_m("Invalid avatar URL %s"), $url)); + } + + // @fixme this should be better encapsulated + // ripped from OStatus via oauthstore.php (for old OMB client) + $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); + if (!copy($url, $temp_filename)) { + throw new ServerException(sprintf(_m("Unable to fetch avatar from %s"), $url)); + } + + $profile = $user->getProfile(); + $id = $profile->id; + // @fixme should we be using different ids? + + $imagefile = new ImageFile($id, $temp_filename); + $filename = Avatar::filename($id, + image_type_to_extension($imagefile->type), + null, + common_timestamp()); + rename($temp_filename, Avatar::path($filename)); + $profile->setOriginal($filename); + } + +} + |