summaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
authorBrion Vibber <brion@status.net>2010-08-10 15:01:29 -0700
committerBrion Vibber <brion@status.net>2010-08-10 15:01:29 -0700
commit819d33210d298de74b64dc7ead79e9d9b223b12e (patch)
tree902d42087e633b96e12bef699f6c80e7342c9312 /plugins
parent8f071b2818e8321ea910df612016175f65093402 (diff)
parent08fc6053ec55e911b842fd05dafc5e0c99c4e992 (diff)
Merge branch '0.9.x' into tinymce
Diffstat (limited to 'plugins')
-rw-r--r--plugins/Adsense/AdsensePlugin.php48
-rw-r--r--plugins/Adsense/adsenseadminpanel.php223
-rw-r--r--plugins/AutoSandbox/AutoSandboxPlugin.php96
-rw-r--r--plugins/AutoSandbox/LICENSE21
-rw-r--r--plugins/AutoSandbox/README39
-rw-r--r--plugins/AutoSandbox/locale/AutoSandbox.pot21
-rw-r--r--plugins/Autocomplete/AutocompletePlugin.php15
-rw-r--r--plugins/Autocomplete/autocomplete.php1
-rw-r--r--plugins/Autocomplete/locale/Autocomplete.pot24
-rw-r--r--plugins/BitlyUrl/BitlyUrlPlugin.php2
-rw-r--r--plugins/BitlyUrl/locale/BitlyUrl.pot22
-rw-r--r--plugins/Blacklist/BlacklistPlugin.php91
-rw-r--r--plugins/Blacklist/Homepage_blacklist.php189
-rw-r--r--plugins/Blacklist/Nickname_blacklist.php180
-rw-r--r--plugins/Blacklist/blacklistadminpanel.php49
-rw-r--r--plugins/Blacklist/locale/Blacklist.pot54
-rw-r--r--plugins/CasAuthentication/CasAuthenticationPlugin.php2
-rw-r--r--plugins/CasAuthentication/locale/CasAuthentication.pot35
-rw-r--r--plugins/ClientSideShorten/ClientSideShortenPlugin.php79
-rw-r--r--plugins/ClientSideShorten/README6
-rw-r--r--plugins/ClientSideShorten/locale/ClientSideShorten.pot27
-rw-r--r--plugins/ClientSideShorten/shorten.js66
-rw-r--r--plugins/ClientSideShorten/shorten.php69
-rw-r--r--plugins/DirectionDetector/DirectionDetectorPlugin.php230
-rw-r--r--plugins/DirectionDetector/locale/DirectionDetector.pot21
-rw-r--r--plugins/DirectionDetector/locale/nl/LC_MESSAGES/DirectionDetector.po22
-rw-r--r--plugins/EmailAuthentication/EmailAuthenticationPlugin.php2
-rw-r--r--plugins/EmailAuthentication/locale/EmailAuthentication.pot23
-rw-r--r--plugins/Facebook/FBConnectAuth.php18
-rw-r--r--plugins/Facebook/FacebookPlugin.php2
-rw-r--r--plugins/Facebook/README10
-rw-r--r--plugins/Facebook/facebook/facebook.php74
-rwxr-xr-xplugins/Facebook/facebook/facebookapi_php5_restlib.php56
-rw-r--r--plugins/Facebook/facebooksettings.php21
-rw-r--r--plugins/Facebook/facebookutil.php276
-rw-r--r--plugins/Facebook/locale/Facebook.pot (renamed from plugins/Facebook/locale/Facebook.po)378
-rw-r--r--plugins/FirePHP/FirePHPPlugin.php12
-rw-r--r--plugins/FirePHP/locale/FirePHP.pot21
-rw-r--r--plugins/GeonamesPlugin.php35
-rw-r--r--plugins/Gravatar/GravatarPlugin.php17
-rw-r--r--plugins/Gravatar/locale/Gravatar.pot (renamed from plugins/Gravatar/locale/Gravatar.po)2
-rw-r--r--plugins/Imap/ImapPlugin.php4
-rw-r--r--plugins/Imap/imapmanager.php2
-rw-r--r--plugins/Imap/locale/Imap.pot27
-rw-r--r--plugins/InfiniteScroll/InfiniteScrollPlugin.php2
-rw-r--r--plugins/InfiniteScroll/locale/InfiniteScroll.pot25
-rw-r--r--plugins/LdapAuthentication/LdapAuthenticationPlugin.php288
-rw-r--r--plugins/LdapAuthentication/locale/LdapAuthentication.pot23
-rw-r--r--plugins/LdapAuthorization/LdapAuthorizationPlugin.php135
-rw-r--r--plugins/LdapAuthorization/locale/LdapAuthorization.pot23
-rw-r--r--plugins/LdapCommon/LdapCommon.php369
-rw-r--r--plugins/LdapCommon/MemcacheSchemaCache.php (renamed from plugins/LdapAuthentication/MemcacheSchemaCache.php)2
-rw-r--r--plugins/LdapCommon/extlib/Net/LDAP2.php1791
-rw-r--r--plugins/LdapCommon/extlib/Net/LDAP2/Entry.php1055
-rw-r--r--plugins/LdapCommon/extlib/Net/LDAP2/Filter.php514
-rw-r--r--plugins/LdapCommon/extlib/Net/LDAP2/LDIF.php922
-rw-r--r--plugins/LdapCommon/extlib/Net/LDAP2/RootDSE.php240
-rw-r--r--plugins/LdapCommon/extlib/Net/LDAP2/Schema.php516
-rw-r--r--plugins/LdapCommon/extlib/Net/LDAP2/SchemaCache.interface.php59
-rw-r--r--plugins/LdapCommon/extlib/Net/LDAP2/Search.php614
-rw-r--r--plugins/LdapCommon/extlib/Net/LDAP2/SimpleFileSchemaCache.php97
-rw-r--r--plugins/LdapCommon/extlib/Net/LDAP2/Util.php572
-rw-r--r--plugins/LilUrl/LilUrlPlugin.php2
-rw-r--r--plugins/LilUrl/locale/LilUrl.pot22
-rw-r--r--plugins/Mapstraction/MapstractionPlugin.php6
-rw-r--r--plugins/Mapstraction/allmap.php1
-rw-r--r--plugins/Mapstraction/locale/Mapstraction.pot (renamed from plugins/Mapstraction/locale/Mapstraction.po)34
-rw-r--r--plugins/Mapstraction/map.php1
-rw-r--r--plugins/Mapstraction/usermap.js2
-rw-r--r--plugins/Mapstraction/usermap.php1
-rw-r--r--plugins/MemcachedPlugin.php8
-rw-r--r--plugins/Meteor/MeteorPlugin.php35
-rw-r--r--plugins/Minify/MinifyPlugin.php1
-rw-r--r--plugins/Minify/locale/Minify.pot23
-rw-r--r--plugins/MobileProfile/MobileProfilePlugin.php47
-rw-r--r--plugins/MobileProfile/locale/MobileProfile.pot21
-rw-r--r--plugins/MobileProfile/mp-screen.css6
-rw-r--r--plugins/OStatus/OStatusPlugin.php61
-rw-r--r--plugins/OStatus/README39
-rw-r--r--plugins/OStatus/actions/groupsalmon.php3
-rw-r--r--plugins/OStatus/actions/hostmeta.php3
-rw-r--r--plugins/OStatus/actions/ostatusgroup.php2
-rw-r--r--plugins/OStatus/actions/ostatussub.php14
-rw-r--r--plugins/OStatus/actions/usersalmon.php11
-rw-r--r--plugins/OStatus/actions/userxrd.php10
-rw-r--r--plugins/OStatus/classes/FeedSub.php62
-rw-r--r--plugins/OStatus/classes/HubSub.php51
-rw-r--r--plugins/OStatus/classes/Magicsig.php44
-rw-r--r--plugins/OStatus/classes/Ostatus_profile.php281
-rw-r--r--plugins/OStatus/extlib/Math/BigInteger.php8
-rw-r--r--plugins/OStatus/lib/discovery.php2
-rw-r--r--plugins/OStatus/lib/discoveryhints.php3
-rw-r--r--plugins/OStatus/lib/feeddiscovery.php12
-rw-r--r--plugins/OStatus/lib/hubprepqueuehandler.php87
-rw-r--r--plugins/OStatus/lib/linkheader.php2
-rw-r--r--plugins/OStatus/lib/magicenvelope.php57
-rw-r--r--plugins/OStatus/lib/ostatusqueuehandler.php31
-rw-r--r--plugins/OStatus/lib/salmon.php4
-rw-r--r--plugins/OStatus/lib/xrd.php103
-rw-r--r--plugins/OStatus/lib/xrdaction.php5
-rw-r--r--plugins/OStatus/locale/OStatus.pot (renamed from plugins/OStatus/locale/OStatus.po)319
-rw-r--r--plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po106
-rw-r--r--plugins/OStatus/scripts/fixup-shadow.php39
-rw-r--r--plugins/OStatus/scripts/resub-feed.php74
-rw-r--r--plugins/OStatus/scripts/update-profile.php147
-rw-r--r--plugins/OStatus/tests/FeedDiscoveryTest.php2
-rw-r--r--plugins/OStatus/tests/remote-tests.php555
-rw-r--r--plugins/OpenExternalLinkTarget/OpenExternalLinkTargetPlugin.php2
-rw-r--r--plugins/OpenExternalLinkTarget/locale/OpenExternalLinkTarget.pot21
-rw-r--r--plugins/OpenID/OpenIDPlugin.php441
-rw-r--r--plugins/OpenID/extlib/README6
-rw-r--r--plugins/OpenID/extlib/teams-extension.php175
-rw-r--r--plugins/OpenID/finishaddopenid.php27
-rw-r--r--plugins/OpenID/finishopenidlogin.php73
-rw-r--r--plugins/OpenID/locale/OpenID.pot (renamed from plugins/OpenID/locale/OpenID.po)388
-rw-r--r--plugins/OpenID/locale/nl/LC_MESSAGES/OpenID.po395
-rw-r--r--plugins/OpenID/openid.php115
-rw-r--r--plugins/OpenID/openidadminpanel.php280
-rw-r--r--plugins/OpenID/openidlogin.php59
-rw-r--r--plugins/OpenID/openidserver.php21
-rw-r--r--plugins/OpenID/openidsettings.php70
-rw-r--r--plugins/OpenID/openidtrust.php5
-rw-r--r--plugins/PostDebug/locale/PostDebug.pot21
-rw-r--r--plugins/PoweredByStatusNet/locale/PoweredByStatusNet.pot (renamed from plugins/PoweredByStatusNet/locale/PoweredByStatusNet.po)2
-rw-r--r--plugins/PtitUrl/PtitUrlPlugin.php2
-rw-r--r--plugins/PtitUrl/locale/PtitUrl.pot22
-rw-r--r--plugins/RSSCloud/RSSCloudPlugin.php22
-rw-r--r--plugins/RSSCloud/locale/RSSCloud.pot24
-rw-r--r--plugins/Realtime/README1
-rw-r--r--plugins/Realtime/RealtimePlugin.php42
-rw-r--r--plugins/Realtime/realtimeupdate.js6
-rw-r--r--plugins/Recaptcha/RecaptchaPlugin.php30
-rw-r--r--plugins/Recaptcha/locale/Recaptcha.pot23
-rw-r--r--plugins/RegisterThrottle/locale/RegisterThrottle.pot29
-rw-r--r--plugins/RequireValidatedEmail/README14
-rw-r--r--plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php55
-rw-r--r--plugins/RequireValidatedEmail/locale/RequireValidatedEmail.pot (renamed from plugins/RequireValidatedEmail/locale/RequireValidatedEmail.po)2
-rw-r--r--plugins/ReverseUsernameAuthentication/ReverseUsernameAuthenticationPlugin.php2
-rw-r--r--plugins/ReverseUsernameAuthentication/locale/ReverseUsernameAuthentication.pot24
-rw-r--r--plugins/Sample/User_greeting_count.php19
-rw-r--r--plugins/Sample/locale/Sample.pot (renamed from plugins/Sample/locale/Sample.po)38
-rw-r--r--plugins/SimpleUrl/SimpleUrlPlugin.php2
-rw-r--r--plugins/SimpleUrl/locale/SimpleUrl.pot22
-rw-r--r--plugins/Sitemap/SitemapPlugin.php224
-rw-r--r--plugins/Sitemap/Sitemap_notice_count.php290
-rw-r--r--plugins/Sitemap/Sitemap_user_count.php286
-rw-r--r--plugins/Sitemap/noticesitemap.php137
-rw-r--r--plugins/Sitemap/scripts/updatecounts.php36
-rw-r--r--plugins/Sitemap/sitemapaction.php95
-rw-r--r--plugins/Sitemap/sitemapadminpanel.php205
-rw-r--r--plugins/Sitemap/sitemapindex.php128
-rw-r--r--plugins/Sitemap/usersitemap.php128
-rw-r--r--plugins/SpotifyPlugin.php113
-rw-r--r--plugins/SubMirror/SubMirrorPlugin.php168
-rw-r--r--plugins/SubMirror/actions/addmirror.php77
-rw-r--r--plugins/SubMirror/actions/basemirror.php169
-rw-r--r--plugins/SubMirror/actions/editmirror.php112
-rw-r--r--plugins/SubMirror/actions/mirrorsettings.php106
-rw-r--r--plugins/SubMirror/classes/SubMirror.php229
-rw-r--r--plugins/SubMirror/lib/addmirrorform.php141
-rw-r--r--plugins/SubMirror/lib/editmirrorform.php189
-rw-r--r--plugins/SubMirror/lib/mirrorqueuehandler.php45
-rw-r--r--plugins/TabFocus/TabFocusPlugin.php2
-rw-r--r--plugins/TabFocus/locale/TabFocus.pot24
-rw-r--r--plugins/TightUrl/TightUrlPlugin.php2
-rw-r--r--plugins/TightUrl/locale/TightUrl.pot22
-rw-r--r--plugins/TwitterBridge/TwitterBridgePlugin.php15
-rwxr-xr-xplugins/TwitterBridge/daemons/twitterstatusfetcher.php141
-rw-r--r--plugins/TwitterBridge/locale/TwitterBridge.pot (renamed from plugins/TwitterBridge/locale/TwitterBridge.po)93
-rw-r--r--plugins/TwitterBridge/twitter.php53
-rw-r--r--plugins/TwitterBridge/twitteradminpanel.php29
-rw-r--r--plugins/TwitterBridge/twitterauthorization.php18
-rw-r--r--plugins/TwitterBridge/twitterbasicauthclient.php15
-rw-r--r--plugins/TwitterBridge/twitteroauthclient.php17
-rw-r--r--plugins/UrlShortener/UrlShortenerPlugin.php1
-rw-r--r--plugins/UserFlag/clearflag.php2
-rw-r--r--plugins/UserFlag/flagprofile.php2
-rw-r--r--plugins/WikiHowProfile/README6
-rw-r--r--plugins/WikiHowProfile/WikiHowProfilePlugin.php196
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 &copy(&$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&amp;v=2&amp;sensor=false&amp;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')) . '">' .
+ '&#8230;' .
+ '</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(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"').replace(/&amp;/g,'&');
source = data['source'].replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"').replace(/&amp;/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);
+ }
+
+}
+