]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch 'testing' of git@gitorious.org:statusnet/mainline into testing
authorZach Copley <zach@status.net>
Tue, 2 Mar 2010 07:33:18 +0000 (07:33 +0000)
committerZach Copley <zach@status.net>
Tue, 2 Mar 2010 07:33:18 +0000 (07:33 +0000)
35 files changed:
actions/newnotice.php
actions/postnotice.php
actions/updateprofile.php
classes/Notice.php
classes/Subscription.php
db/08to09.sql
db/statusnet.sql
lib/action.php
lib/common.php
lib/default.php
lib/oauthstore.php
lib/omb.php
lib/util.php
locale/statusnet.po
plugins/Facebook/FacebookPlugin.php
plugins/Facebook/facebookadminpanel.php [new file with mode: 0644]
plugins/MobileProfile/MobileProfilePlugin.php
plugins/OStatus/OStatusPlugin.php
plugins/OStatus/actions/ostatussub.php
plugins/OStatus/actions/pushhub.php
plugins/OStatus/classes/HubSub.php
plugins/OStatus/classes/Magicsig.php
plugins/OStatus/classes/Ostatus_profile.php
plugins/OStatus/lib/discovery.php
plugins/OStatus/lib/xrd.php
plugins/OStatus/locale/OStatus.po
plugins/OStatus/scripts/updateostatus.php [new file with mode: 0644]
plugins/RegisterThrottle/RegisterThrottlePlugin.php [new file with mode: 0644]
plugins/RegisterThrottle/Registration_ip.php [new file with mode: 0644]
plugins/TwitterBridge/README
plugins/TwitterBridge/TwitterBridgePlugin.php
plugins/TwitterBridge/twitteradminpanel.php [new file with mode: 0644]
plugins/TwitterBridge/twitterauthorization.php
scripts/update_po_templates.php
theme/base/css/display.css

index 78480ababb069849508550141ee4c5df9862b78c..ed0fa1b2b5768026e486196d0112a2c46af3f54e 100644 (file)
@@ -294,6 +294,9 @@ class NewnoticeAction extends Action
             if ($profile) {
                 $content = '@' . $profile->nickname . ' ';
             }
+        } else {
+            // @fixme most of these bits above aren't being passed on above
+            $inreplyto = null;
         }
 
         $notice_form = new NoticeForm($this, '', $content, null, $inreplyto);
index fb0670376651ed145b0ea8ea7673eb3f7e07d30a..b2f6f1bb95debe9a57e441bbdc07a96c8a37447e 100644 (file)
@@ -54,7 +54,10 @@ class PostnoticeAction extends Action
      */
     function prepare($argarray)
     {
+        StatusNet::setApi(true); // Send smaller error pages
+
         parent::prepare($argarray);
+
         try {
             $this->checkNotice();
         } catch (Exception $e) {
@@ -71,6 +74,14 @@ class PostnoticeAction extends Action
             $srv = new OMB_Service_Provider(null, omb_oauth_datastore(),
                                             omb_oauth_server());
             $srv->handlePostNotice();
+        } catch (OMB_RemoteServiceException $rse) {
+            $msg = $rse->getMessage();
+            if (preg_match('/Revoked accesstoken/', $msg) ||
+                preg_match('/No subscriber/', $msg)) {
+                $this->clientError($msg, 403);
+            } else {
+                $this->clientError($msg);
+            }
         } catch (Exception $e) {
             $this->serverError($e->getMessage());
             return;
index e416a6fa93dbd881ab152249d1abed50dfd2ff52..bae6108cced4e0647982e4b579705ff48ba938c1 100644 (file)
@@ -55,6 +55,8 @@ class UpdateprofileAction extends Action
      */
     function prepare($argarray)
     {
+        StatusNet::setApi(true); // Send smaller error pages
+
         parent::prepare($argarray);
         $license      = $_POST['omb_listenee_license'];
         $site_license = common_config('license', 'url');
@@ -75,6 +77,14 @@ class UpdateprofileAction extends Action
             $srv = new OMB_Service_Provider(null, omb_oauth_datastore(),
                                             omb_oauth_server());
             $srv->handleUpdateProfile();
+        } catch (OMB_RemoteServiceException $rse) {
+            $msg = $rse->getMessage();
+            if (preg_match('/Revoked accesstoken/', $msg) ||
+                preg_match('/No subscriber/', $msg)) {
+                $this->clientError($msg, 403);
+            } else {
+                $this->clientError($msg);
+            }
         } catch (Exception $e) {
             $this->serverError($e->getMessage());
             return;
index 2d02a9a19f1c747ce02134bdd97e71b0713f936e..3702dbcfa8e59010b1ee07aba9983eb8b1f1e3a8 100644 (file)
@@ -282,12 +282,6 @@ class Notice extends Memcached_DataObject
 
         $notice->content = $final;
 
-        if (!empty($rendered)) {
-            $notice->rendered = $rendered;
-        } else {
-            $notice->rendered = common_render_content($final, $notice);
-        }
-
         $notice->source = $source;
         $notice->uri = $uri;
         $notice->url = $url;
@@ -315,6 +309,12 @@ class Notice extends Memcached_DataObject
             $notice->location_ns = $location_ns;
         }
 
+        if (!empty($rendered)) {
+            $notice->rendered = $rendered;
+        } else {
+            $notice->rendered = common_render_content($final, $notice);
+        }
+
         if (Event::handle('StartNoticeSave', array(&$notice))) {
 
             // XXX: some of these functions write to the DB
@@ -973,7 +973,10 @@ class Notice extends Memcached_DataObject
 
         $sender = Profile::staticGet($this->profile_id);
 
-        $mentions = common_find_mentions($this->profile_id, $this->content);
+        // @todo ideally this parser information would only
+        // be calculated once.
+
+        $mentions = common_find_mentions($this->content, $this);
 
         $replied = array();
 
index d6fb3fcbdda82f17d70dcce6692868dfed6f1840..9cef2df1ad7c222d407fc1c3177fd4fdc1f1fa44 100644 (file)
@@ -172,6 +172,28 @@ class Subscription extends Memcached_DataObject
 
             assert(!empty($sub));
 
+            // @todo: move this block to EndSubscribe handler for
+            // OMB plugin when it exists.
+
+            if (!empty($sub->token)) {
+
+                $token = new Token();
+
+                $token->tok    = $sub->token;
+
+                if ($token->find(true)) {
+
+                    $result = $token->delete();
+
+                    if (!$result) {
+                        common_log_db_error($token, 'DELETE', __FILE__);
+                        throw new Exception(_('Couldn\'t delete subscription OMB token.'));
+                    }
+                } else {
+                    common_log(LOG_ERR, "Couldn't find credentials with token {$token->tok}");
+                }
+            }
+
             $result = $sub->delete();
 
             if (!$result) {
index b10e47dbcb67f769f1577c046c9222e0e772c600..f30572154197c96d922c545af69a6c256d495eaa 100644 (file)
@@ -110,3 +110,8 @@ insert into queue_item_new (frame,transport,created,claimed)
 alter table queue_item rename to queue_item_old;
 alter table queue_item_new rename to queue_item;
 
+alter table file_to_post
+    add index post_id_idx (post_id);
+
+alter table group_inbox
+    add index group_inbox_notice_id_idx (notice_id);
index 4158f0167db2a9f9def64f29ac8fff1fef574382..3f95948e1ed5ede5d9cde05511f78fb49f657021 100644 (file)
@@ -458,7 +458,8 @@ create table group_inbox (
     created datetime not null comment 'date the notice was created',
 
     constraint primary key (group_id, notice_id),
-    index group_inbox_created_idx (created)
+    index group_inbox_created_idx (created),
+    index group_inbox_notice_id_idx (notice_id)
 
 ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
 
@@ -523,7 +524,8 @@ create table file_to_post (
     post_id integer comment 'id of the notice it belongs to' references notice (id),
     modified timestamp comment 'date this record was modified',
 
-    constraint primary key (file_id, post_id)
+    constraint primary key (file_id, post_id),
+    index post_id_idx (post_id)
 
 ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
 
index a7e0eb33baa62dcf5594bcc8264b725fc9419733..0918c68582b31cc2c2ff7e9b2d07c6f2c0420a34 100644 (file)
@@ -425,8 +425,6 @@ class Action extends HTMLOutputter // lawsuit
             $connect = 'imsettings';
         } else if (common_config('sms', 'enabled')) {
             $connect = 'smssettings';
-        } else if (common_config('twitter', 'enabled')) {
-            $connect = 'twittersettings';
         }
 
         $this->elementStart('dl', array('id' => 'site_nav_global_primary'));
index 2dbe3b3c531ba481060854e7a7ad0f0d66b8ac65..546f6bbe4beecc327f25b3b6cf407fd7d1bb48fd 100644 (file)
@@ -22,7 +22,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
 //exit with 200 response, if this is checking fancy from the installer
 if (isset($_REQUEST['p']) && $_REQUEST['p'] == 'check-fancy') {  exit; }
 
-define('STATUSNET_VERSION', '0.9.0beta6');
+define('STATUSNET_VERSION', '0.9.0beta6+bugfix1');
 define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility
 
 define('STATUSNET_CODENAME', 'Stand');
index d849055c213287b968da6b303ca6ec924815b2d3..7b50242ae27f9ff5b37996f86348e6d2c7485545 100644 (file)
@@ -177,8 +177,8 @@ $default =
         array('source' => 'StatusNet', # source attribute for Twitter
               'taguri' => null), # base for tag URIs
         'twitter' =>
-        array('enabled'       => true,
-              'consumer_key'    => null,
+        array('signin' => true,
+              'consumer_key' => null,
               'consumer_secret' => null),
         'cache' =>
         array('base' => null),
index eabe37f9fa4a944d76f99c91c9a57de69c6c0f23..a6a6de7505c946bbdd0a24b7760dea3ff8abb940 100644 (file)
@@ -390,7 +390,7 @@ class StatusNetOAuthDataStore extends OAuthDataStore
         $sub->subscribed = $user->id;
 
         if (!$sub->find(true)) {
-            return 0;
+            return array();
         }
 
         /* Since we do not use OMB_Service_Provider’s action methods, there
index 17132a594f6ec20ea2e079270d9a791895297f2a..8bbe5e8aac22b39ed4a1651400bcfe995d0555c8 100644 (file)
@@ -77,7 +77,7 @@ function omb_broadcast_notice($notice)
     /* Get remote users subscribed to this profile. */
     $rp = new Remote_profile();
 
-    $rp->query('SELECT postnoticeurl, token, secret ' .
+    $rp->query('SELECT remote_profile.*, secret, token ' .
                'FROM subscription JOIN remote_profile ' .
                'ON subscription.subscriber = remote_profile.id ' .
                'WHERE subscription.subscribed = ' . $notice->profile_id . ' ');
@@ -93,7 +93,8 @@ function omb_broadcast_notice($notice)
 
         /* Post notice. */
         $service = new StatusNet_OMB_Service_Consumer(
-                     array(OMB_ENDPOINT_POSTNOTICE => $rp->postnoticeurl));
+                     array(OMB_ENDPOINT_POSTNOTICE => $rp->postnoticeurl),
+                                                      $rp->uri);
         try {
             $service->setToken($rp->token, $rp->secret);
             $service->postNotice($omb_notice);
@@ -125,7 +126,7 @@ function omb_broadcast_profile($profile)
     /* Get remote users subscribed to this profile. */
     $rp = new Remote_profile();
 
-    $rp->query('SELECT updateprofileurl, token, secret ' .
+    $rp->query('SELECT remote_profile.*, secret, token ' .
                'FROM subscription JOIN remote_profile ' .
                'ON subscription.subscriber = remote_profile.id ' .
                'WHERE subscription.subscribed = ' . $profile->id . ' ');
@@ -141,7 +142,11 @@ function omb_broadcast_profile($profile)
 
         /* Update profile. */
         $service = new StatusNet_OMB_Service_Consumer(
-                     array(OMB_ENDPOINT_UPDATEPROFILE => $rp->updateprofileurl));
+                     array(OMB_ENDPOINT_UPDATEPROFILE => $rp->updateprofileurl),
+                                                      $rp->uri);
+
+        common_debug('service = ' . print_r($service, true));
+
         try {
             $service->setToken($rp->token, $rp->secret);
             $service->updateProfile($omb_profile);
@@ -159,13 +164,14 @@ function omb_broadcast_profile($profile)
 }
 
 class StatusNet_OMB_Service_Consumer extends OMB_Service_Consumer {
-    public function __construct($urls)
+    public function __construct($urls, $listener_uri=null)
     {
         $this->services       = $urls;
         $this->datastore      = omb_oauth_datastore();
         $this->oauth_consumer = omb_oauth_consumer();
         $this->fetcher        = Auth_Yadis_Yadis::getHTTPFetcher();
         $this->fetcher->timeout = intval(common_config('omb', 'timeout'));
+        $this->listener_uri   = $listener_uri;
     }
 
 }
index 32061ec04ce037f55a6e4471e9092ecf96c411d8..7a170a5f5fd58684c94bce53d3802e6016a7778c 100644 (file)
@@ -426,14 +426,14 @@ function common_render_content($text, $notice)
 {
     $r = common_render_text($text);
     $id = $notice->profile_id;
-    $r = common_linkify_mentions($id, $r);
+    $r = common_linkify_mentions($r, $notice);
     $r = preg_replace('/(^|[\s\.\,\:\;]+)!([A-Za-z0-9]{1,64})/e', "'\\1!'.common_group_link($id, '\\2')", $r);
     return $r;
 }
 
-function common_linkify_mentions($profile_id, $text)
+function common_linkify_mentions($text, $notice)
 {
-    $mentions = common_find_mentions($profile_id, $text);
+    $mentions = common_find_mentions($text, $notice);
 
     // We need to go through in reverse order by position,
     // so our positions stay valid despite our fudging with the
@@ -487,11 +487,11 @@ function common_linkify_mention($mention)
     return $output;
 }
 
-function common_find_mentions($profile_id, $text)
+function common_find_mentions($text, $notice)
 {
     $mentions = array();
 
-    $sender = Profile::staticGet('id', $profile_id);
+    $sender = Profile::staticGet('id', $notice->profile_id);
 
     if (empty($sender)) {
         return $mentions;
@@ -499,6 +499,30 @@ function common_find_mentions($profile_id, $text)
 
     if (Event::handle('StartFindMentions', array($sender, $text, &$mentions))) {
 
+        // Get the context of the original notice, if any
+
+        $originalAuthor   = null;
+        $originalNotice   = null;
+        $originalMentions = array();
+
+        // Is it a reply?
+
+        if (!empty($notice) && !empty($notice->reply_to)) {
+            $originalNotice = Notice::staticGet('id', $notice->reply_to);
+            if (!empty($originalNotice)) {
+                $originalAuthor = Profile::staticGet('id', $originalNotice->profile_id);
+
+                $ids = $originalNotice->getReplies();
+
+                foreach ($ids as $id) {
+                    $repliedTo = Profile::staticGet('id', $id);
+                    if (!empty($repliedTo)) {
+                        $originalMentions[$repliedTo->nickname] = $repliedTo;
+                    }
+                }
+            }
+        }
+
         preg_match_all('/^T ([A-Z0-9]{1,64}) /',
                        $text,
                        $tmatches,
@@ -514,7 +538,22 @@ function common_find_mentions($profile_id, $text)
         foreach ($matches as $match) {
 
             $nickname = common_canonical_nickname($match[0]);
-            $mentioned = common_relative_profile($sender, $nickname);
+
+            // Try to get a profile for this nickname.
+            // Start with conversation context, then go to
+            // sender context.
+
+            if (!empty($originalAuthor) && $originalAuthor->nickname == $nickname) {
+
+                $mentioned = $originalAuthor;
+
+            } else if (!empty($originalMentions) &&
+                       array_key_exists($nickname, $originalMentions)) {
+
+                $mention = $originalMentions[$nickname];
+            } else {
+                $mentioned = common_relative_profile($sender, $nickname);
+            }
 
             if (!empty($mentioned)) {
 
@@ -770,8 +809,28 @@ function common_shorten_links($text)
 
 function common_xml_safe_str($str)
 {
-    // Neutralize control codes and surrogates
-       return preg_replace('/[\p{Cc}\p{Cs}]/u', '*', $str);
+    // Replace common eol and extra whitespace input chars
+    $unWelcome = array(
+        "\t",  // tab
+        "\n",  // newline
+        "\r",  // cr
+        "\0",  // null byte eos
+        "\x0B" // vertical tab
+    );
+
+    $replacement = array(
+        ' ', // single space
+        ' ',
+        '',  // nothing
+        '',
+        ' '
+    );
+
+    $str = str_replace($unWelcome, $replacement, $str);
+
+    // Neutralize any additional control codes and UTF-16 surrogates
+    // (Twitter uses '*')
+    return preg_replace('/[\p{Cc}\p{Cs}]/u', '*', $str);
 }
 
 function common_tag_link($tag)
index cf44e2d3c960111c3679c3283fb8a86b6278abe0..8e143449781eea68b849d4c9f543784e0f42e516 100644 (file)
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2010-02-24 23:49+0000\n"
+"POT-Creation-Date: 2010-03-01 14:08+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,7 +17,7 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
 
-#: actions/accessadminpanel.php:54 lib/adminpanelaction.php:326
+#: actions/accessadminpanel.php:54 lib/adminpanelaction.php:337
 msgid "Access"
 msgstr ""
 
@@ -88,14 +88,15 @@ msgstr ""
 #: actions/apitimelinehome.php:79 actions/apitimelinementions.php:79
 #: actions/apitimelineuser.php:81 actions/avatarbynickname.php:75
 #: actions/favoritesrss.php:74 actions/foaf.php:40 actions/foaf.php:58
-#: actions/microsummary.php:62 actions/newmessage.php:116 actions/otp.php:76
-#: actions/remotesubscribe.php:145 actions/remotesubscribe.php:154
-#: actions/replies.php:73 actions/repliesrss.php:38 actions/rsd.php:116
-#: actions/showfavorites.php:105 actions/userbyid.php:74
-#: actions/usergroups.php:91 actions/userrss.php:38 actions/xrds.php:71
-#: lib/command.php:163 lib/command.php:302 lib/command.php:355
-#: lib/command.php:401 lib/command.php:462 lib/command.php:518
-#: lib/galleryaction.php:59 lib/mailbox.php:82 lib/profileaction.php:77
+#: actions/hcard.php:67 actions/microsummary.php:62 actions/newmessage.php:116
+#: actions/otp.php:76 actions/remotesubscribe.php:145
+#: actions/remotesubscribe.php:154 actions/replies.php:73
+#: actions/repliesrss.php:38 actions/rsd.php:116 actions/showfavorites.php:105
+#: actions/userbyid.php:74 actions/usergroups.php:91 actions/userrss.php:38
+#: actions/xrds.php:71 lib/command.php:163 lib/command.php:302
+#: lib/command.php:355 lib/command.php:401 lib/command.php:462
+#: lib/command.php:518 lib/galleryaction.php:59 lib/mailbox.php:82
+#: lib/profileaction.php:77
 msgid "No such user."
 msgstr ""
 
@@ -171,20 +172,20 @@ msgstr ""
 #: actions/apiaccountverifycredentials.php:70 actions/apidirectmessage.php:156
 #: actions/apifavoritecreate.php:99 actions/apifavoritedestroy.php:100
 #: actions/apifriendshipscreate.php:100 actions/apifriendshipsdestroy.php:100
-#: actions/apifriendshipsshow.php:128 actions/apigroupcreate.php:136
+#: actions/apifriendshipsshow.php:128 actions/apigroupcreate.php:138
 #: actions/apigroupismember.php:114 actions/apigroupjoin.php:155
 #: actions/apigroupleave.php:141 actions/apigrouplist.php:132
 #: actions/apigrouplistall.php:120 actions/apigroupmembership.php:106
-#: actions/apigroupshow.php:115 actions/apihelptest.php:88
+#: actions/apigroupshow.php:105 actions/apihelptest.php:88
 #: actions/apistatusesdestroy.php:102 actions/apistatusesretweets.php:112
-#: actions/apistatusesshow.php:108 actions/apistatusnetconfig.php:137
+#: actions/apistatusesshow.php:108 actions/apistatusnetconfig.php:131
 #: actions/apistatusnetversion.php:93 actions/apisubscriptions.php:111
 #: actions/apitimelinefavorites.php:183 actions/apitimelinefriends.php:187
-#: actions/apitimelinegroup.php:195 actions/apitimelinehome.php:184
+#: actions/apitimelinegroup.php:185 actions/apitimelinehome.php:184
 #: actions/apitimelinementions.php:175 actions/apitimelinepublic.php:152
 #: actions/apitimelineretweetedtome.php:121
 #: actions/apitimelineretweetsofme.php:152 actions/apitimelinetag.php:166
-#: actions/apitimelineuser.php:207 actions/apiusershow.php:101
+#: actions/apitimelineuser.php:196 actions/apiusershow.php:101
 msgid "API method not found."
 msgstr ""
 
@@ -216,8 +217,9 @@ msgstr ""
 #: actions/apiaccountupdateprofilebackgroundimage.php:194
 #: actions/apiaccountupdateprofilecolors.php:185
 #: actions/apiaccountupdateprofileimage.php:130 actions/apiusershow.php:108
-#: actions/avatarbynickname.php:80 actions/foaf.php:65 actions/replies.php:80
-#: actions/usergroups.php:98 lib/galleryaction.php:66 lib/profileaction.php:84
+#: actions/avatarbynickname.php:80 actions/foaf.php:65 actions/hcard.php:74
+#: actions/replies.php:80 actions/usergroups.php:98 lib/galleryaction.php:66
+#: lib/profileaction.php:84
 msgid "User has no profile."
 msgstr ""
 
@@ -241,7 +243,7 @@ msgstr ""
 #: actions/apiaccountupdateprofilebackgroundimage.php:146
 #: actions/apiaccountupdateprofilecolors.php:164
 #: actions/apiaccountupdateprofilecolors.php:174
-#: actions/groupdesignsettings.php:287 actions/groupdesignsettings.php:297
+#: actions/groupdesignsettings.php:290 actions/groupdesignsettings.php:300
 #: actions/userdesignsettings.php:210 actions/userdesignsettings.php:220
 #: actions/userdesignsettings.php:263 actions/userdesignsettings.php:273
 msgid "Unable to save your design settings."
@@ -351,87 +353,87 @@ msgstr ""
 msgid "Could not find target user."
 msgstr ""
 
-#: actions/apigroupcreate.php:164 actions/editgroup.php:182
+#: actions/apigroupcreate.php:166 actions/editgroup.php:186
 #: actions/newgroup.php:126 actions/profilesettings.php:215
 #: actions/register.php:205
 msgid "Nickname must have only lowercase letters and numbers and no spaces."
 msgstr ""
 
-#: actions/apigroupcreate.php:173 actions/editgroup.php:186
+#: actions/apigroupcreate.php:175 actions/editgroup.php:190
 #: actions/newgroup.php:130 actions/profilesettings.php:238
 #: actions/register.php:208
 msgid "Nickname already in use. Try another one."
 msgstr ""
 
-#: actions/apigroupcreate.php:180 actions/editgroup.php:189
+#: actions/apigroupcreate.php:182 actions/editgroup.php:193
 #: actions/newgroup.php:133 actions/profilesettings.php:218
 #: actions/register.php:210
 msgid "Not a valid nickname."
 msgstr ""
 
-#: actions/apigroupcreate.php:196 actions/editapplication.php:215
-#: actions/editgroup.php:195 actions/newapplication.php:203
+#: actions/apigroupcreate.php:198 actions/editapplication.php:215
+#: actions/editgroup.php:199 actions/newapplication.php:203
 #: actions/newgroup.php:139 actions/profilesettings.php:222
 #: actions/register.php:217
 msgid "Homepage is not a valid URL."
 msgstr ""
 
-#: actions/apigroupcreate.php:205 actions/editgroup.php:198
+#: actions/apigroupcreate.php:207 actions/editgroup.php:202
 #: actions/newgroup.php:142 actions/profilesettings.php:225
 #: actions/register.php:220
 msgid "Full name is too long (max 255 chars)."
 msgstr ""
 
-#: actions/apigroupcreate.php:213 actions/editapplication.php:190
+#: actions/apigroupcreate.php:215 actions/editapplication.php:190
 #: actions/newapplication.php:172
 #, php-format
 msgid "Description is too long (max %d chars)."
 msgstr ""
 
-#: actions/apigroupcreate.php:224 actions/editgroup.php:204
+#: actions/apigroupcreate.php:226 actions/editgroup.php:208
 #: actions/newgroup.php:148 actions/profilesettings.php:232
 #: actions/register.php:227
 msgid "Location is too long (max 255 chars)."
 msgstr ""
 
-#: actions/apigroupcreate.php:243 actions/editgroup.php:215
+#: actions/apigroupcreate.php:245 actions/editgroup.php:219
 #: actions/newgroup.php:159
 #, php-format
 msgid "Too many aliases! Maximum %d."
 msgstr ""
 
-#: actions/apigroupcreate.php:264 actions/editgroup.php:224
+#: actions/apigroupcreate.php:266 actions/editgroup.php:228
 #: actions/newgroup.php:168
 #, php-format
 msgid "Invalid alias: \"%s\""
 msgstr ""
 
-#: actions/apigroupcreate.php:273 actions/editgroup.php:228
+#: actions/apigroupcreate.php:275 actions/editgroup.php:232
 #: actions/newgroup.php:172
 #, php-format
 msgid "Alias \"%s\" already in use. Try another one."
 msgstr ""
 
-#: actions/apigroupcreate.php:286 actions/editgroup.php:234
+#: actions/apigroupcreate.php:288 actions/editgroup.php:238
 #: actions/newgroup.php:178
 msgid "Alias can't be the same as nickname."
 msgstr ""
 
 #: actions/apigroupismember.php:95 actions/apigroupjoin.php:104
 #: actions/apigroupleave.php:104 actions/apigroupmembership.php:91
-#: actions/apigroupshow.php:82 actions/apitimelinegroup.php:91
+#: actions/apigroupshow.php:90 actions/apitimelinegroup.php:91
 msgid "Group not found!"
 msgstr ""
 
-#: actions/apigroupjoin.php:110 actions/joingroup.php:90
+#: actions/apigroupjoin.php:110 actions/joingroup.php:100
 msgid "You are already a member of that group."
 msgstr ""
 
-#: actions/apigroupjoin.php:119 actions/joingroup.php:95 lib/command.php:221
+#: actions/apigroupjoin.php:119 actions/joingroup.php:105 lib/command.php:221
 msgid "You have been blocked from that group by the admin."
 msgstr ""
 
-#: actions/apigroupjoin.php:138 actions/joingroup.php:124
+#: actions/apigroupjoin.php:138 actions/joingroup.php:134
 #, php-format
 msgid "Could not join user %1$s to group %2$s."
 msgstr ""
@@ -440,7 +442,7 @@ msgstr ""
 msgid "You are not a member of this group."
 msgstr ""
 
-#: actions/apigroupleave.php:124 actions/leavegroup.php:119
+#: actions/apigroupleave.php:124 actions/leavegroup.php:129
 #, php-format
 msgid "Could not remove user %1$s from group %2$s."
 msgstr ""
@@ -471,7 +473,7 @@ msgstr ""
 #: actions/apioauthauthorize.php:123 actions/avatarsettings.php:268
 #: actions/deletenotice.php:157 actions/disfavor.php:74
 #: actions/emailsettings.php:238 actions/favor.php:75 actions/geocode.php:54
-#: actions/groupblock.php:66 actions/grouplogo.php:309
+#: actions/groupblock.php:66 actions/grouplogo.php:312
 #: actions/groupunblock.php:66 actions/imsettings.php:206
 #: actions/invite.php:56 actions/login.php:115 actions/makeadmin.php:66
 #: actions/newmessage.php:135 actions/newnotice.php:103 actions/nudge.php:80
@@ -491,11 +493,11 @@ msgid "Invalid nickname / password!"
 msgstr ""
 
 #: actions/apioauthauthorize.php:159
-msgid "Database error deleting OAuth application user."
+msgid "DB error deleting OAuth app user."
 msgstr ""
 
 #: actions/apioauthauthorize.php:185
-msgid "Database error inserting OAuth application user."
+msgid "DB error inserting OAuth app user."
 msgstr ""
 
 #: actions/apioauthauthorize.php:214
@@ -512,7 +514,7 @@ msgstr ""
 
 #: actions/apioauthauthorize.php:232 actions/avatarsettings.php:281
 #: actions/designadminpanel.php:103 actions/editapplication.php:139
-#: actions/emailsettings.php:256 actions/grouplogo.php:319
+#: actions/emailsettings.php:256 actions/grouplogo.php:322
 #: actions/imsettings.php:220 actions/newapplication.php:121
 #: actions/oauthconnectionssettings.php:147 actions/recoverpassword.php:44
 #: actions/smssettings.php:248 lib/designsettings.php:304
@@ -541,7 +543,7 @@ msgstr ""
 
 #: actions/apioauthauthorize.php:313 actions/login.php:230
 #: actions/profilesettings.php:106 actions/register.php:424
-#: actions/showgroup.php:236 actions/tagother.php:94
+#: actions/showgroup.php:244 actions/tagother.php:94
 #: actions/userauthorization.php:145 lib/groupeditform.php:152
 #: lib/userprofile.php:131
 msgid "Nickname"
@@ -623,12 +625,12 @@ msgid "%1$s updates favorited by %2$s / %2$s."
 msgstr ""
 
 #: actions/apitimelinegroup.php:109 actions/apitimelineuser.php:118
-#: actions/grouprss.php:131 actions/userrss.php:90
+#: actions/grouprss.php:138 actions/userrss.php:90
 #, php-format
 msgid "%s timeline"
 msgstr ""
 
-#: actions/apitimelinegroup.php:114 actions/apitimelineuser.php:126
+#: actions/apitimelinegroup.php:112 actions/apitimelineuser.php:124
 #: actions/userrss.php:92
 #, php-format
 msgid "Updates from %1$s on %2$s!"
@@ -685,8 +687,7 @@ msgstr ""
 #: actions/avatarbynickname.php:59 actions/blockedfromgroup.php:73
 #: actions/editgroup.php:84 actions/groupdesignsettings.php:84
 #: actions/grouplogo.php:86 actions/groupmembers.php:76
-#: actions/grouprss.php:91 actions/joingroup.php:76 actions/leavegroup.php:76
-#: actions/showgroup.php:121
+#: actions/grouprss.php:91 actions/showgroup.php:121
 msgid "No nickname."
 msgstr ""
 
@@ -698,7 +699,7 @@ msgstr ""
 msgid "Invalid size."
 msgstr ""
 
-#: actions/avatarsettings.php:67 actions/showgroup.php:221
+#: actions/avatarsettings.php:67 actions/showgroup.php:229
 #: lib/accountsettingsaction.php:112
 msgid "Avatar"
 msgstr ""
@@ -715,30 +716,30 @@ msgid "User without matching profile"
 msgstr ""
 
 #: actions/avatarsettings.php:119 actions/avatarsettings.php:197
-#: actions/grouplogo.php:251
+#: actions/grouplogo.php:254
 msgid "Avatar settings"
 msgstr ""
 
 #: actions/avatarsettings.php:127 actions/avatarsettings.php:205
-#: actions/grouplogo.php:199 actions/grouplogo.php:259
+#: actions/grouplogo.php:202 actions/grouplogo.php:262
 msgid "Original"
 msgstr ""
 
 #: actions/avatarsettings.php:142 actions/avatarsettings.php:217
-#: actions/grouplogo.php:210 actions/grouplogo.php:271
+#: actions/grouplogo.php:213 actions/grouplogo.php:274
 msgid "Preview"
 msgstr ""
 
 #: actions/avatarsettings.php:149 actions/showapplication.php:252
-#: lib/deleteuserform.php:66 lib/noticelist.php:637
+#: lib/deleteuserform.php:66 lib/noticelist.php:655
 msgid "Delete"
 msgstr ""
 
-#: actions/avatarsettings.php:166 actions/grouplogo.php:233
+#: actions/avatarsettings.php:166 actions/grouplogo.php:236
 msgid "Upload"
 msgstr ""
 
-#: actions/avatarsettings.php:231 actions/grouplogo.php:286
+#: actions/avatarsettings.php:231 actions/grouplogo.php:289
 msgid "Crop"
 msgstr ""
 
@@ -746,7 +747,7 @@ msgstr ""
 msgid "Pick a square area of the image to be your avatar"
 msgstr ""
 
-#: actions/avatarsettings.php:343 actions/grouplogo.php:377
+#: actions/avatarsettings.php:343 actions/grouplogo.php:380
 msgid "Lost our file data."
 msgstr ""
 
@@ -778,22 +779,22 @@ msgid ""
 msgstr ""
 
 #: actions/block.php:143 actions/deleteapplication.php:153
-#: actions/deletenotice.php:145 actions/deleteuser.php:147
+#: actions/deletenotice.php:145 actions/deleteuser.php:150
 #: actions/groupblock.php:178
 msgid "No"
 msgstr ""
 
-#: actions/block.php:143 actions/deleteuser.php:147
+#: actions/block.php:143 actions/deleteuser.php:150
 msgid "Do not block this user"
 msgstr ""
 
 #: actions/block.php:144 actions/deleteapplication.php:158
-#: actions/deletenotice.php:146 actions/deleteuser.php:148
+#: actions/deletenotice.php:146 actions/deleteuser.php:151
 #: actions/groupblock.php:179 lib/repeatform.php:132
 msgid "Yes"
 msgstr ""
 
-#: actions/block.php:144 actions/groupmembers.php:348 lib/blockform.php:80
+#: actions/block.php:144 actions/groupmembers.php:355 lib/blockform.php:80
 msgid "Block this user"
 msgstr ""
 
@@ -801,39 +802,43 @@ msgstr ""
 msgid "Failed to save block information."
 msgstr ""
 
-#: actions/blockedfromgroup.php:80 actions/editgroup.php:96
-#: actions/foafgroup.php:44 actions/foafgroup.php:62 actions/groupblock.php:86
-#: actions/groupbyid.php:83 actions/groupdesignsettings.php:97
-#: actions/grouplogo.php:99 actions/groupmembers.php:83
-#: actions/grouprss.php:98 actions/groupunblock.php:86
-#: actions/joingroup.php:83 actions/leavegroup.php:83 actions/makeadmin.php:86
-#: actions/showgroup.php:137 lib/command.php:212 lib/command.php:260
+#: actions/blockedfromgroup.php:80 actions/blockedfromgroup.php:87
+#: actions/editgroup.php:100 actions/foafgroup.php:44 actions/foafgroup.php:62
+#: actions/foafgroup.php:69 actions/groupblock.php:86 actions/groupbyid.php:83
+#: actions/groupdesignsettings.php:100 actions/grouplogo.php:102
+#: actions/groupmembers.php:83 actions/groupmembers.php:90
+#: actions/grouprss.php:98 actions/grouprss.php:105
+#: actions/groupunblock.php:86 actions/joingroup.php:82
+#: actions/joingroup.php:93 actions/leavegroup.php:82
+#: actions/leavegroup.php:93 actions/makeadmin.php:86
+#: actions/showgroup.php:138 actions/showgroup.php:146 lib/command.php:212
+#: lib/command.php:260
 msgid "No such group."
 msgstr ""
 
-#: actions/blockedfromgroup.php:90
+#: actions/blockedfromgroup.php:97
 #, php-format
 msgid "%s blocked profiles"
 msgstr ""
 
-#: actions/blockedfromgroup.php:93
+#: actions/blockedfromgroup.php:100
 #, php-format
 msgid "%1$s blocked profiles, page %2$d"
 msgstr ""
 
-#: actions/blockedfromgroup.php:108
+#: actions/blockedfromgroup.php:115
 msgid "A list of the users blocked from joining this group."
 msgstr ""
 
-#: actions/blockedfromgroup.php:281
+#: actions/blockedfromgroup.php:288
 msgid "Unblock user from group"
 msgstr ""
 
-#: actions/blockedfromgroup.php:313 lib/unblockform.php:69
+#: actions/blockedfromgroup.php:320 lib/unblockform.php:69
 msgid "Unblock"
 msgstr ""
 
-#: actions/blockedfromgroup.php:313 lib/unblockform.php:80
+#: actions/blockedfromgroup.php:320 lib/unblockform.php:80
 msgid "Unblock this user"
 msgstr ""
 
@@ -876,7 +881,7 @@ msgid "Couldn't delete email confirmation."
 msgstr ""
 
 #: actions/confirmaddress.php:144
-msgid "Confirm address"
+msgid "Confirm Address"
 msgstr ""
 
 #: actions/confirmaddress.php:159
@@ -963,7 +968,7 @@ msgstr ""
 msgid "Do not delete this notice"
 msgstr ""
 
-#: actions/deletenotice.php:146 lib/noticelist.php:637
+#: actions/deletenotice.php:146 lib/noticelist.php:655
 msgid "Delete this notice"
 msgstr ""
 
@@ -979,18 +984,18 @@ msgstr ""
 msgid "Delete user"
 msgstr ""
 
-#: actions/deleteuser.php:135
+#: actions/deleteuser.php:136
 msgid ""
 "Are you sure you want to delete this user? This will clear all data about "
 "the user from the database, without a backup."
 msgstr ""
 
-#: actions/deleteuser.php:148 lib/deleteuserform.php:77
+#: actions/deleteuser.php:151 lib/deleteuserform.php:77
 msgid "Delete this user"
 msgstr ""
 
 #: actions/designadminpanel.php:62 lib/accountsettingsaction.php:124
-#: lib/adminpanelaction.php:316 lib/groupnav.php:119
+#: lib/adminpanelaction.php:327 lib/groupnav.php:119
 msgid "Design"
 msgstr ""
 
@@ -1182,29 +1187,29 @@ msgstr ""
 msgid "You must be logged in to create a group."
 msgstr ""
 
-#: actions/editgroup.php:103 actions/editgroup.php:168
-#: actions/groupdesignsettings.php:104 actions/grouplogo.php:106
+#: actions/editgroup.php:107 actions/editgroup.php:172
+#: actions/groupdesignsettings.php:107 actions/grouplogo.php:109
 msgid "You must be an admin to edit the group."
 msgstr ""
 
-#: actions/editgroup.php:154
+#: actions/editgroup.php:158
 msgid "Use this form to edit the group."
 msgstr ""
 
-#: actions/editgroup.php:201 actions/newgroup.php:145
+#: actions/editgroup.php:205 actions/newgroup.php:145
 #, php-format
 msgid "description is too long (max %d chars)."
 msgstr ""
 
-#: actions/editgroup.php:253
+#: actions/editgroup.php:258
 msgid "Could not update group."
 msgstr ""
 
-#: actions/editgroup.php:259 classes/User_group.php:433
+#: actions/editgroup.php:264 classes/User_group.php:478
 msgid "Could not create aliases."
 msgstr ""
 
-#: actions/editgroup.php:269
+#: actions/editgroup.php:280
 msgid "Options saved."
 msgstr ""
 
@@ -1533,7 +1538,7 @@ msgstr ""
 msgid "User is not a member of group."
 msgstr ""
 
-#: actions/groupblock.php:136 actions/groupmembers.php:316
+#: actions/groupblock.php:136 actions/groupmembers.php:323
 msgid "Block user from group"
 msgstr ""
 
@@ -1565,86 +1570,86 @@ msgstr ""
 msgid "You must be logged in to edit a group."
 msgstr ""
 
-#: actions/groupdesignsettings.php:141
+#: actions/groupdesignsettings.php:144
 msgid "Group design"
 msgstr ""
 
-#: actions/groupdesignsettings.php:152
+#: actions/groupdesignsettings.php:155
 msgid ""
 "Customize the way your group looks with a background image and a colour "
 "palette of your choice."
 msgstr ""
 
-#: actions/groupdesignsettings.php:263 actions/userdesignsettings.php:186
+#: actions/groupdesignsettings.php:266 actions/userdesignsettings.php:186
 #: lib/designsettings.php:391 lib/designsettings.php:413
 msgid "Couldn't update your design."
 msgstr ""
 
-#: actions/groupdesignsettings.php:308 actions/userdesignsettings.php:231
+#: actions/groupdesignsettings.php:311 actions/userdesignsettings.php:231
 msgid "Design preferences saved."
 msgstr ""
 
-#: actions/grouplogo.php:139 actions/grouplogo.php:192
+#: actions/grouplogo.php:142 actions/grouplogo.php:195
 msgid "Group logo"
 msgstr ""
 
-#: actions/grouplogo.php:150
+#: actions/grouplogo.php:153
 #, php-format
 msgid ""
 "You can upload a logo image for your group. The maximum file size is %s."
 msgstr ""
 
-#: actions/grouplogo.php:178
+#: actions/grouplogo.php:181
 msgid "User without matching profile."
 msgstr ""
 
-#: actions/grouplogo.php:362
+#: actions/grouplogo.php:365
 msgid "Pick a square area of the image to be the logo."
 msgstr ""
 
-#: actions/grouplogo.php:396
+#: actions/grouplogo.php:399
 msgid "Logo updated."
 msgstr ""
 
-#: actions/grouplogo.php:398
+#: actions/grouplogo.php:401
 msgid "Failed updating logo."
 msgstr ""
 
-#: actions/groupmembers.php:93 lib/groupnav.php:92
+#: actions/groupmembers.php:100 lib/groupnav.php:92
 #, php-format
 msgid "%s group members"
 msgstr ""
 
-#: actions/groupmembers.php:96
+#: actions/groupmembers.php:103
 #, php-format
 msgid "%1$s group members, page %2$d"
 msgstr ""
 
-#: actions/groupmembers.php:111
+#: actions/groupmembers.php:118
 msgid "A list of the users in this group."
 msgstr ""
 
-#: actions/groupmembers.php:175 lib/action.php:448 lib/groupnav.php:107
+#: actions/groupmembers.php:182 lib/action.php:448 lib/groupnav.php:107
 msgid "Admin"
 msgstr ""
 
-#: actions/groupmembers.php:348 lib/blockform.php:69
+#: actions/groupmembers.php:355 lib/blockform.php:69
 msgid "Block"
 msgstr ""
 
-#: actions/groupmembers.php:443
+#: actions/groupmembers.php:450
 msgid "Make user an admin of the group"
 msgstr ""
 
-#: actions/groupmembers.php:475
+#: actions/groupmembers.php:482
 msgid "Make Admin"
 msgstr ""
 
-#: actions/groupmembers.php:475
+#: actions/groupmembers.php:482
 msgid "Make this user an admin"
 msgstr ""
 
-#: actions/grouprss.php:133
+#: actions/grouprss.php:140
 #, php-format
 msgid "Updates from members of %1$s on %2$s!"
 msgstr ""
@@ -1924,7 +1929,11 @@ msgstr ""
 msgid "You must be logged in to join a group."
 msgstr ""
 
-#: actions/joingroup.php:131
+#: actions/joingroup.php:88 actions/leavegroup.php:88
+msgid "No nickname or ID."
+msgstr ""
+
+#: actions/joingroup.php:141
 #, php-format
 msgid "%1$s joined group %2$s"
 msgstr ""
@@ -1933,11 +1942,11 @@ msgstr ""
 msgid "You must be logged in to leave a group."
 msgstr ""
 
-#: actions/leavegroup.php:90 lib/command.php:265
+#: actions/leavegroup.php:100 lib/command.php:265
 msgid "You are not a member of that group."
 msgstr ""
 
-#: actions/leavegroup.php:127
+#: actions/leavegroup.php:137
 #, php-format
 msgid "%1$s left group %2$s"
 msgstr ""
@@ -2194,8 +2203,8 @@ msgstr ""
 msgid "Only "
 msgstr ""
 
-#: actions/oembed.php:181 actions/oembed.php:200 lib/api.php:1040
-#: lib/api.php:1068 lib/api.php:1177
+#: actions/oembed.php:181 actions/oembed.php:200 lib/apiaction.php:1039
+#: lib/apiaction.php:1067 lib/apiaction.php:1176
 msgid "Not a supported data format."
 msgstr ""
 
@@ -2208,7 +2217,7 @@ msgid "Notice Search"
 msgstr ""
 
 #: actions/othersettings.php:60
-msgid "Other settings"
+msgid "Other Settings"
 msgstr ""
 
 #: actions/othersettings.php:71
@@ -2334,7 +2343,7 @@ msgstr ""
 msgid "Password saved."
 msgstr ""
 
-#: actions/pathsadminpanel.php:59 lib/adminpanelaction.php:331
+#: actions/pathsadminpanel.php:59 lib/adminpanelaction.php:342
 msgid "Paths"
 msgstr ""
 
@@ -2367,7 +2376,7 @@ msgid "Invalid SSL server. The maximum length is 255 characters."
 msgstr ""
 
 #: actions/pathsadminpanel.php:234 actions/siteadminpanel.php:58
-#: lib/adminpanelaction.php:311
+#: lib/adminpanelaction.php:322
 msgid "Site"
 msgstr ""
 
@@ -2535,7 +2544,7 @@ msgid "1-64 lowercase letters or numbers, no punctuation or spaces"
 msgstr ""
 
 #: actions/profilesettings.php:111 actions/register.php:448
-#: actions/showgroup.php:247 actions/tagother.php:104
+#: actions/showgroup.php:255 actions/tagother.php:104
 #: lib/groupeditform.php:157 lib/userprofile.php:149
 msgid "Full name"
 msgstr ""
@@ -2563,7 +2572,7 @@ msgid "Bio"
 msgstr ""
 
 #: actions/profilesettings.php:132 actions/register.php:471
-#: actions/showgroup.php:256 actions/tagother.php:112
+#: actions/showgroup.php:264 actions/tagother.php:112
 #: actions/userauthorization.php:166 lib/groupeditform.php:177
 #: lib/userprofile.php:164
 msgid "Location"
@@ -3032,7 +3041,7 @@ msgstr ""
 msgid "You already repeated that notice."
 msgstr ""
 
-#: actions/repeat.php:114 lib/noticelist.php:656
+#: actions/repeat.php:114 lib/noticelist.php:674
 msgid "Repeated"
 msgstr ""
 
@@ -3105,7 +3114,7 @@ msgid "User is already sandboxed."
 msgstr ""
 
 #: actions/sessionsadminpanel.php:54 actions/sessionsadminpanel.php:170
-#: lib/adminpanelaction.php:336
+#: lib/adminpanelaction.php:347
 msgid "Sessions"
 msgstr ""
 
@@ -3160,7 +3169,7 @@ msgstr ""
 msgid "Description"
 msgstr ""
 
-#: actions/showapplication.php:192 actions/showgroup.php:429
+#: actions/showapplication.php:192 actions/showgroup.php:437
 #: lib/profileaction.php:174
 msgid "Statistics"
 msgstr ""
@@ -3271,67 +3280,67 @@ msgstr ""
 msgid "%1$s group, page %2$d"
 msgstr ""
 
-#: actions/showgroup.php:218
+#: actions/showgroup.php:226
 msgid "Group profile"
 msgstr ""
 
-#: actions/showgroup.php:263 actions/tagother.php:118
+#: actions/showgroup.php:271 actions/tagother.php:118
 #: actions/userauthorization.php:175 lib/userprofile.php:177
 msgid "URL"
 msgstr ""
 
-#: actions/showgroup.php:274 actions/tagother.php:128
+#: actions/showgroup.php:282 actions/tagother.php:128
 #: actions/userauthorization.php:187 lib/userprofile.php:194
 msgid "Note"
 msgstr ""
 
-#: actions/showgroup.php:284 lib/groupeditform.php:184
+#: actions/showgroup.php:292 lib/groupeditform.php:184
 msgid "Aliases"
 msgstr ""
 
-#: actions/showgroup.php:293
+#: actions/showgroup.php:301
 msgid "Group actions"
 msgstr ""
 
-#: actions/showgroup.php:328
+#: actions/showgroup.php:336
 #, php-format
 msgid "Notice feed for %s group (RSS 1.0)"
 msgstr ""
 
-#: actions/showgroup.php:334
+#: actions/showgroup.php:342
 #, php-format
 msgid "Notice feed for %s group (RSS 2.0)"
 msgstr ""
 
-#: actions/showgroup.php:340
+#: actions/showgroup.php:348
 #, php-format
 msgid "Notice feed for %s group (Atom)"
 msgstr ""
 
-#: actions/showgroup.php:345
+#: actions/showgroup.php:353
 #, php-format
 msgid "FOAF for %s group"
 msgstr ""
 
-#: actions/showgroup.php:381 actions/showgroup.php:438 lib/groupnav.php:91
+#: actions/showgroup.php:389 actions/showgroup.php:446 lib/groupnav.php:91
 msgid "Members"
 msgstr ""
 
-#: actions/showgroup.php:386 lib/profileaction.php:117
+#: actions/showgroup.php:394 lib/profileaction.php:117
 #: lib/profileaction.php:148 lib/profileaction.php:236 lib/section.php:95
 #: lib/subscriptionlist.php:126 lib/tagcloudsection.php:71
 msgid "(None)"
 msgstr ""
 
-#: actions/showgroup.php:392
+#: actions/showgroup.php:400
 msgid "All members"
 msgstr ""
 
-#: actions/showgroup.php:432
+#: actions/showgroup.php:440
 msgid "Created"
 msgstr ""
 
-#: actions/showgroup.php:448
+#: actions/showgroup.php:456
 #, php-format
 msgid ""
 "**%s** is a user group on %%%%site.name%%%%, a [micro-blogging](http://en."
@@ -3341,7 +3350,7 @@ msgid ""
 "of this group and many more! ([Read more](%%%%doc.help%%%%))"
 msgstr ""
 
-#: actions/showgroup.php:454
+#: actions/showgroup.php:462
 #, php-format
 msgid ""
 "**%s** is a user group on %%%%site.name%%%%, a [micro-blogging](http://en."
@@ -3350,7 +3359,7 @@ msgid ""
 "their life and interests. "
 msgstr ""
 
-#: actions/showgroup.php:482
+#: actions/showgroup.php:490
 msgid "Admins"
 msgstr ""
 
@@ -3861,7 +3870,7 @@ msgstr ""
 msgid "No such tag."
 msgstr ""
 
-#: actions/twitapitrends.php:87
+#: actions/twitapitrends.php:85
 msgid "API method under construction."
 msgstr ""
 
@@ -3891,7 +3900,7 @@ msgid ""
 "Listenee stream license â€˜%1$s’ is not compatible with site license â€˜%2$s’."
 msgstr ""
 
-#: actions/useradminpanel.php:58 lib/adminpanelaction.php:321
+#: actions/useradminpanel.php:58 lib/adminpanelaction.php:332
 #: lib/personalgroupnav.php:115
 msgid "User"
 msgstr ""
@@ -4164,6 +4173,10 @@ msgstr ""
 msgid "Group leave failed."
 msgstr ""
 
+#: classes/Local_group.php:41
+msgid "Could not update local group."
+msgstr ""
+
 #: classes/Login_token.php:76
 #, php-format
 msgid "Could not create login token for %s"
@@ -4181,43 +4194,43 @@ msgstr ""
 msgid "Could not update message with new URI."
 msgstr ""
 
-#: classes/Notice.php:157
+#: classes/Notice.php:172
 #, php-format
 msgid "DB error inserting hashtag: %s"
 msgstr ""
 
-#: classes/Notice.php:222
+#: classes/Notice.php:239
 msgid "Problem saving notice. Too long."
 msgstr ""
 
-#: classes/Notice.php:226
+#: classes/Notice.php:243
 msgid "Problem saving notice. Unknown user."
 msgstr ""
 
-#: classes/Notice.php:231
+#: classes/Notice.php:248
 msgid ""
 "Too many notices too fast; take a breather and post again in a few minutes."
 msgstr ""
 
-#: classes/Notice.php:237
+#: classes/Notice.php:254
 msgid ""
 "Too many duplicate messages too quickly; take a breather and post again in a "
 "few minutes."
 msgstr ""
 
-#: classes/Notice.php:243
+#: classes/Notice.php:260
 msgid "You are banned from posting notices on this site."
 msgstr ""
 
-#: classes/Notice.php:309 classes/Notice.php:335
+#: classes/Notice.php:326 classes/Notice.php:352
 msgid "Problem saving notice."
 msgstr ""
 
-#: classes/Notice.php:882
+#: classes/Notice.php:911
 msgid "Problem saving group inbox."
 msgstr ""
 
-#: classes/Notice.php:1407
+#: classes/Notice.php:1442
 #, php-format
 msgid "RT @%1$s %2$s"
 msgstr ""
@@ -4242,7 +4255,7 @@ msgstr ""
 msgid "Couldn't delete self-subscription."
 msgstr ""
 
-#: classes/Subscription.php:179 lib/subs.php:69
+#: classes/Subscription.php:179
 msgid "Couldn't delete subscription."
 msgstr ""
 
@@ -4251,14 +4264,22 @@ msgstr ""
 msgid "Welcome to %1$s, @%2$s!"
 msgstr ""
 
-#: classes/User_group.php:423
+#: classes/User_group.php:462
 msgid "Could not create group."
 msgstr ""
 
-#: classes/User_group.php:452
+#: classes/User_group.php:471
+msgid "Could not set group uri."
+msgstr ""
+
+#: classes/User_group.php:492
 msgid "Could not set group membership."
 msgstr ""
 
+#: classes/User_group.php:506
+msgid "Could not save local group info."
+msgstr ""
+
 #: lib/accountsettingsaction.php:108
 msgid "Change your profile settings"
 msgstr ""
@@ -4471,15 +4492,15 @@ msgstr ""
 msgid "Before"
 msgstr ""
 
-#: lib/activity.php:382
+#: lib/activity.php:449
 msgid "Can't handle remote content yet."
 msgstr ""
 
-#: lib/activity.php:410
+#: lib/activity.php:477
 msgid "Can't handle embedded XML content yet."
 msgstr ""
 
-#: lib/activity.php:414
+#: lib/activity.php:481
 msgid "Can't handle embedded Base64 content yet."
 msgstr ""
 
@@ -4503,35 +4524,35 @@ msgstr ""
 msgid "Unable to delete design setting."
 msgstr ""
 
-#: lib/adminpanelaction.php:312
+#: lib/adminpanelaction.php:323
 msgid "Basic site configuration"
 msgstr ""
 
-#: lib/adminpanelaction.php:317
+#: lib/adminpanelaction.php:328
 msgid "Design configuration"
 msgstr ""
 
-#: lib/adminpanelaction.php:322
+#: lib/adminpanelaction.php:333
 msgid "User configuration"
 msgstr ""
 
-#: lib/adminpanelaction.php:327
+#: lib/adminpanelaction.php:338
 msgid "Access configuration"
 msgstr ""
 
-#: lib/adminpanelaction.php:332
+#: lib/adminpanelaction.php:343
 msgid "Paths configuration"
 msgstr ""
 
-#: lib/adminpanelaction.php:337
+#: lib/adminpanelaction.php:348
 msgid "Sessions configuration"
 msgstr ""
 
-#: lib/apiauth.php:95
+#: lib/apiauth.php:94
 msgid "API resource requires read-write access, but you only have read access."
 msgstr ""
 
-#: lib/apiauth.php:273
+#: lib/apiauth.php:272
 #, php-format
 msgid "Failed API auth attempt, nickname = %1$s, proxy = %2$s, ip = %3$s"
 msgstr ""
@@ -4621,11 +4642,11 @@ msgstr ""
 msgid "Tags for this attachment"
 msgstr ""
 
-#: lib/authenticationplugin.php:218 lib/authenticationplugin.php:223
+#: lib/authenticationplugin.php:182 lib/authenticationplugin.php:187
 msgid "Password changing failed"
 msgstr ""
 
-#: lib/authenticationplugin.php:233
+#: lib/authenticationplugin.php:197
 msgid "Password changing is not allowed"
 msgstr ""
 
@@ -4782,7 +4803,7 @@ msgstr ""
 msgid "Subscribed to %s"
 msgstr ""
 
-#: lib/command.php:582 lib/command.php:685
+#: lib/command.php:582
 msgid "Specify the name of the user to unsubscribe from"
 msgstr ""
 
@@ -4820,42 +4841,37 @@ msgstr ""
 msgid "This link is useable only once, and is good for only 2 minutes: %s"
 msgstr ""
 
-#: lib/command.php:692
-#, php-format
-msgid "Unsubscribed  %s"
-msgstr ""
-
-#: lib/command.php:709
+#: lib/command.php:681
 msgid "You are not subscribed to anyone."
 msgstr ""
 
-#: lib/command.php:711
+#: lib/command.php:683
 msgid "You are subscribed to this person:"
 msgid_plural "You are subscribed to these people:"
 msgstr[0] ""
 msgstr[1] ""
 
-#: lib/command.php:731
+#: lib/command.php:703
 msgid "No one is subscribed to you."
 msgstr ""
 
-#: lib/command.php:733
+#: lib/command.php:705
 msgid "This person is subscribed to you:"
 msgid_plural "These people are subscribed to you:"
 msgstr[0] ""
 msgstr[1] ""
 
-#: lib/command.php:753
+#: lib/command.php:725
 msgid "You are not a member of any groups."
 msgstr ""
 
-#: lib/command.php:755
+#: lib/command.php:727
 msgid "You are a member of this group:"
 msgid_plural "You are a member of these groups:"
 msgstr[0] ""
 msgstr[1] ""
 
-#: lib/command.php:769
+#: lib/command.php:741
 msgid ""
 "Commands:\n"
 "on - turn on notifications\n"
@@ -4869,7 +4885,6 @@ msgid ""
 "d <nickname> <text> - direct message to user\n"
 "get <nickname> - get last notice from user\n"
 "whois <nickname> - get profile info on user\n"
-"lose <nickname> - force user to stop following you\n"
 "fav <nickname> - add user's last notice as a 'fave'\n"
 "fav #<notice_id> - add notice with the given id as a 'fave'\n"
 "repeat #<notice_id> - repeat a notice with a given id\n"
@@ -5460,23 +5475,23 @@ msgstr ""
 msgid "at"
 msgstr ""
 
-#: lib/noticelist.php:558
+#: lib/noticelist.php:566
 msgid "in context"
 msgstr ""
 
-#: lib/noticelist.php:583
+#: lib/noticelist.php:601
 msgid "Repeated by"
 msgstr ""
 
-#: lib/noticelist.php:610
+#: lib/noticelist.php:628
 msgid "Reply to this notice"
 msgstr ""
 
-#: lib/noticelist.php:611
+#: lib/noticelist.php:629
 msgid "Reply"
 msgstr ""
 
-#: lib/noticelist.php:655
+#: lib/noticelist.php:673
 msgid "Notice repeated"
 msgstr ""
 
@@ -5613,7 +5628,7 @@ msgstr ""
 msgid "Repeat this notice"
 msgstr ""
 
-#: lib/router.php:665
+#: lib/router.php:668
 msgid "No single user defined for single-user mode."
 msgstr ""
 
@@ -5754,47 +5769,47 @@ msgstr ""
 msgid "Moderate"
 msgstr ""
 
-#: lib/util.php:952
+#: lib/util.php:1000
 msgid "a few seconds ago"
 msgstr ""
 
-#: lib/util.php:954
+#: lib/util.php:1002
 msgid "about a minute ago"
 msgstr ""
 
-#: lib/util.php:956
+#: lib/util.php:1004
 #, php-format
 msgid "about %d minutes ago"
 msgstr ""
 
-#: lib/util.php:958
+#: lib/util.php:1006
 msgid "about an hour ago"
 msgstr ""
 
-#: lib/util.php:960
+#: lib/util.php:1008
 #, php-format
 msgid "about %d hours ago"
 msgstr ""
 
-#: lib/util.php:962
+#: lib/util.php:1010
 msgid "about a day ago"
 msgstr ""
 
-#: lib/util.php:964
+#: lib/util.php:1012
 #, php-format
 msgid "about %d days ago"
 msgstr ""
 
-#: lib/util.php:966
+#: lib/util.php:1014
 msgid "about a month ago"
 msgstr ""
 
-#: lib/util.php:968
+#: lib/util.php:1016
 #, php-format
 msgid "about %d months ago"
 msgstr ""
 
-#: lib/util.php:970
+#: lib/util.php:1018
 msgid "about a year ago"
 msgstr ""
 
index 4266b886d9cae1c9e9162e692de4fe5b9004ba13..014d0d1970fcbebddfbfce39c793344696cf9c74 100644 (file)
@@ -22,7 +22,7 @@
  * @category  Plugin
  * @package   StatusNet
  * @author    Zach Copley <zach@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/
  */
@@ -32,12 +32,12 @@ if (!defined('STATUSNET')) {
 }
 
 define("FACEBOOK_CONNECT_SERVICE", 3);
-define('FACEBOOKPLUGIN_VERSION', '0.9');
 
 require_once INSTALLDIR . '/plugins/Facebook/facebookutil.php';
 
 /**
- * Facebook plugin to add a StatusNet Facebook application
+ * Facebook plugin to add a StatusNet Facebook canvas application
+ * and allow registration and authentication via Facebook Connect
  *
  * @category Plugin
  * @package  StatusNet
@@ -49,6 +49,36 @@ require_once INSTALLDIR . '/plugins/Facebook/facebookutil.php';
 class FacebookPlugin extends Plugin
 {
 
+    const VERSION = STATUSNET_VERSION;
+
+    /**
+     * Initializer for the plugin.
+     */
+
+    function initialize()
+    {
+        // Allow the key and secret to be passed in
+        // Control panel will override
+
+        if (isset($this->apikey)) {
+            $key = common_config('facebook', 'apikey');
+            if (empty($key)) {
+                Config::save('facebook', 'apikey', $this->apikey);
+            }
+        }
+
+        if (isset($this->secret)) {
+            $secret = common_config('facebook', 'secret');
+            if (empty($secret)) {
+                Config::save(
+                    'facebook',
+                    'secret',
+                    $this->secret
+                );
+            }
+        }
+    }
+
     /**
      * Add Facebook app actions to the router table
      *
@@ -70,6 +100,7 @@ class FacebookPlugin extends Plugin
                     array('action' => 'facebooksettings'));
         $m->connect('facebook/app/invite.php', array('action' => 'facebookinvite'));
         $m->connect('facebook/app/remove', array('action' => 'facebookremove'));
+        $m->connect('admin/facebook', array('action' => 'facebookadminpanel'));
 
         // Facebook Connect stuff
 
@@ -98,6 +129,7 @@ class FacebookPlugin extends Plugin
         case 'FacebookinviteAction':
         case 'FacebookremoveAction':
         case 'FacebooksettingsAction':
+        case 'FacebookadminpanelAction':
             include_once INSTALLDIR . '/plugins/Facebook/' .
               strtolower(mb_substr($cls, 0, -6)) . '.php';
             return false;
@@ -122,6 +154,32 @@ class FacebookPlugin extends Plugin
         }
     }
 
+    /**
+     * Add a Facebook tab to the admin panels
+     *
+     * @param Widget $nav Admin panel nav
+     *
+     * @return boolean hook value
+     */
+
+    function onEndAdminPanelNav($nav)
+    {
+        if (AdminPanelAction::canAdmin('facebook')) {
+
+            $action_name = $nav->action->trimmed('action');
+
+            $nav->out->menuItem(
+                common_local_url('facebookadminpanel'),
+                _m('Facebook'),
+                _m('Facebook integration configuration'),
+                $action_name == 'facebookadminpanel',
+                'nav_facebook_admin_panel'
+            );
+        }
+
+        return true;
+    }
+
     /**
      * Override normal HTML output to force the content type to
      * text/html and add in xmlns:fb
@@ -359,8 +417,6 @@ class FacebookPlugin extends Plugin
             $connect = 'imsettings';
         } else if (common_config('sms', 'enabled')) {
             $connect = 'smssettings';
-        } else if (common_config('twitter', 'enabled')) {
-            $connect = 'twittersettings';
         }
 
         if (!empty($user)) {
@@ -525,15 +581,18 @@ class FacebookPlugin extends Plugin
 
     function onPluginVersion(&$versions)
     {
-        $versions[] = array('name' => 'Facebook',
-                            'version' => FACEBOOKPLUGIN_VERSION,
-                            'author' => 'Zach Copley',
-                            'homepage' => 'http://status.net/wiki/Plugin:Facebook',
-                            'rawdescription' =>
-                            _m('The Facebook plugin allows you to integrate ' .
-                               'your StatusNet instance with ' .
-                               '<a href="http://facebook.com/">Facebook</a> ' .
-                               'and Facebook Connect.'));
+        $versions[] = array(
+            'name' => 'Facebook',
+            'version' => self::VERSION,
+            'author' => 'Zach Copley',
+            'homepage' => 'http://status.net/wiki/Plugin:Facebook',
+            'rawdescription' => _m(
+                'The Facebook plugin allows you to integrate ' .
+                'your StatusNet instance with ' .
+                '<a href="http://facebook.com/">Facebook</a> ' .
+                'and Facebook Connect.'
+            )
+        );
         return true;
     }
 
diff --git a/plugins/Facebook/facebookadminpanel.php b/plugins/Facebook/facebookadminpanel.php
new file mode 100644 (file)
index 0000000..ae1c730
--- /dev/null
@@ -0,0 +1,223 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Facebook integration 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 Facebook integration 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 FacebookadminpanelAction extends AdminPanelAction
+{
+    /**
+     * Returns the page title
+     *
+     * @return string page title
+     */
+
+    function title()
+    {
+        return _m('Facebook');
+    }
+
+    /**
+     * Instructions for using this form.
+     *
+     * @return string instructions
+     */
+
+    function getInstructions()
+    {
+        return _m('Facebook integration settings');
+    }
+
+    /**
+     * Show the Facebook admin panel form
+     *
+     * @return void
+     */
+
+    function showForm()
+    {
+        $form = new FacebookAdminPanelForm($this);
+        $form->show();
+        return;
+    }
+
+    /**
+     * Save settings from the form
+     *
+     * @return void
+     */
+
+    function saveSettings()
+    {
+        static $settings = array(
+            'facebook'     => array('apikey', 'secret'),
+        );
+
+        $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)
+    {
+        // Validate consumer key and secret (can't be too long)
+
+        if (mb_strlen($values['facebook']['apikey']) > 255) {
+            $this->clientError(
+                _m("Invalid Facebook API key. Max length is 255 characters.")
+            );
+        }
+
+        if (mb_strlen($values['facebook']['secret']) > 255) {
+            $this->clientError(
+                _m("Invalid Facebook API secret. Max length is 255 characters.")
+            );
+        }
+    }
+}
+
+class FacebookAdminPanelForm extends AdminForm
+{
+    /**
+     * ID of the form
+     *
+     * @return int ID of the form
+     */
+
+    function id()
+    {
+        return 'facebookadminpanel';
+    }
+
+    /**
+     * 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('facebookadminpanel');
+    }
+
+    /**
+     * Data elements of the form
+     *
+     * @return void
+     */
+
+    function formData()
+    {
+        $this->out->elementStart(
+            'fieldset',
+            array('id' => 'settings_facebook-application')
+        );
+        $this->out->element('legend', null, _m('Facebook application settings'));
+        $this->out->elementStart('ul', 'form_data');
+
+        $this->li();
+        $this->input(
+            'apikey',
+            _m('API key'),
+            _m('API key provided by Facebook'),
+            'facebook'
+        );
+        $this->unli();
+
+        $this->li();
+        $this->input(
+            'secret',
+             _m('Secret'),
+            _m('API secret provided by Facebook'),
+            'facebook'
+        );
+        $this->unli();
+
+        $this->out->elementEnd('ul');
+        $this->out->elementEnd('fieldset');
+    }
+
+    /**
+     * Action elements
+     *
+     * @return void
+     */
+
+    function formActions()
+    {
+        $this->out->submit('submit', _('Save'), 'submit', null, _('Save Facebook settings'));
+    }
+}
index cd2531fa727a8f3b7d4d2fd41ed645a8f3d4dc7d..f788639aed7383d7b7fe1ca2ec9042c2cc257f07 100644 (file)
@@ -312,8 +312,6 @@ class MobileProfilePlugin extends WAP20Plugin
             $connect = 'imsettings';
         } else if (common_config('sms', 'enabled')) {
             $connect = 'smssettings';
-        } else if (common_config('twitter', 'enabled')) {
-            $connect = 'twittersettings';
         }
 
         $action->elementStart('ul', array('id' => 'site_nav_global_primary'));
index 720dedd0a0e67733349288b032605b7ca2e5185a..4ffbba45b96c093574c1a2809fdb88037fba938b 100644 (file)
@@ -222,31 +222,62 @@ class OStatusPlugin extends Plugin
     }
 
     /**
-     *
+     * Find any explicit remote mentions. Accepted forms:
+     *   Webfinger: @user@example.com
+     *   Profile link: @example.com/mublog/user
+     * @param Profile $sender (os user?)
+     * @param string $text input markup text
+     * @param array &$mention in/out param: set of found mentions
+     * @return boolean hook return value
      */
 
     function onEndFindMentions($sender, $text, &$mentions)
     {
-        preg_match_all('/(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)/',
+        preg_match_all('!(?:^|\s+)
+                        @(                                # Webfinger:
+                          (?:\w+\.)*\w+                   #   user
+                          @                               #   @
+                          (?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+ #   domain
+                         |                                # Profile:
+                          (?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+ #   domain
+                          (?:/\w+)+                       #   /path1(/path2...)
+                         )!x',
                        $text,
                        $wmatches,
                        PREG_OFFSET_CAPTURE);
 
         foreach ($wmatches[1] as $wmatch) {
-
-            $webfinger = $wmatch[0];
-
-            $this->log(LOG_INFO, "Checking Webfinger for address '$webfinger'");
-
-            $oprofile = Ostatus_profile::ensureWebfinger($webfinger);
+            $target = $wmatch[0];
+            $oprofile = null;
+
+            if (strpos($target, '/') === false) {
+                $this->log(LOG_INFO, "Checking Webfinger for address '$target'");
+                try {
+                    $oprofile = Ostatus_profile::ensureWebfinger($target);
+                } catch (Exception $e) {
+                    $this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage());
+                }
+            } else {
+                $schemes = array('https', 'http');
+                foreach ($schemes as $scheme) {
+                    $url = "$scheme://$target";
+                    $this->log(LOG_INFO, "Checking profile address '$url'");
+                    try {
+                        $oprofile = Ostatus_profile::ensureProfile($url);
+                        if ($oprofile) {
+                            continue;
+                        }
+                    } catch (Exception $e) {
+                        $this->log(LOG_ERR, "Profile check failed: " . $e->getMessage());
+                    }
+                }
+            }
 
             if (empty($oprofile)) {
-
-                $this->log(LOG_INFO, "No Ostatus_profile found for address '$webfinger'");
-
+                $this->log(LOG_INFO, "No Ostatus_profile found for address '$target'");
             } else {
 
-                $this->log(LOG_INFO, "Ostatus_profile found for address '$webfinger'");
+                $this->log(LOG_INFO, "Ostatus_profile found for address '$target'");
 
                 if ($oprofile->isGroup()) {
                     continue;
@@ -261,7 +292,7 @@ class OStatusPlugin extends Plugin
                     }
                 }
                 $mentions[] = array('mentioned' => array($profile),
-                                    'text' => $wmatch[0],
+                                    'text' => $target,
                                     'position' => $pos,
                                     'url' => $profile->profileurl);
             }
index aae22f868a315582098b70df06c7f99e8b09521c..f45e6a8d1abae4bc3633a5e86fdb55ddb2d3429b 100644 (file)
@@ -332,6 +332,7 @@ class OStatusSubAction extends Action
         if ($this->oprofile->isGroup()) {
             $group = $this->oprofile->localGroup();
             if ($user->isMember($group)) {
+                // TRANS: OStatus remote group subscription dialog error.
                 $this->showForm(_m('Already a member!'));
                 return;
             }
@@ -341,18 +342,22 @@ class OStatusSubAction extends Action
                     Event::handle('EndJoinGroup', array($group, $user));
                     $this->successGroup();
                 } else {
+                    // TRANS: OStatus remote group subscription dialog error.
                     $this->showForm(_m('Remote group join failed!'));
                 }
             } else {
+                // TRANS: OStatus remote group subscription dialog error.
                 $this->showForm(_m('Remote group join aborted!'));
             }
         } else {
             $local = $this->oprofile->localProfile();
             if ($user->isSubscribed($local)) {
+                // TRANS: OStatus remote subscription dialog error.
                 $this->showForm(_m('Already subscribed!'));
             } elseif ($this->oprofile->subscribeLocalToRemote($user)) {
                 $this->successUser();
             } else {
+                // TRANS: OStatus remote subscription dialog error.
                 $this->showForm(_m('Remote subscription failed!'));
             }
         }
@@ -450,6 +455,7 @@ class OStatusSubAction extends Action
 
     function title()
     {
+        // TRANS: Page title for OStatus remote subscription form
         return _m('Authorize subscription');
     }
 
index f33690bc4999acc89c1974913ea95a59d9d130ef..842d65e7d29d9eac29518249560aa98c3e37a7e7 100644 (file)
@@ -104,7 +104,7 @@ class PushHubAction extends Action
             throw new ClientException("Invalid hub.secret $secret; must be under 200 bytes.");
         }
 
-        $sub = HubSub::staticGet($sub->topic, $sub->callback);
+        $sub = HubSub::staticGet($topic, $callback);
         if (!$sub) {
             // Creating a new one!
             $sub = new HubSub();
index e599d83a967bf3a8e69551f37dad8fde5c0a455f..3120a70f9fb39dd978c14482c59b79ed90abbbc4 100644 (file)
@@ -260,9 +260,15 @@ class HubSub extends Memcached_DataObject
             $retries = intval(common_config('ostatus', 'hub_retries'));
         }
 
-        $data = array('sub' => clone($this),
+        // 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
+        // individual item from a multi-item query again.
+        $sub = HubSub::staticGet($this->topic, $this->callback);
+        $data = array('sub' => $sub,
                       'atom' => $atom,
                       'retries' => $retries);
+        common_log(LOG_INFO, "Queuing PuSH: $this->topic to $this->callback");
         $qm = QueueManager::get();
         $qm->enqueue($data, 'hubout');
     }
index 96900d876144c80c20e37100bf8f1498390ee876..5a46aeeb6e5f79b8b6ce59fab283685b51521d42 100644 (file)
@@ -146,8 +146,10 @@ class Magicsig extends Memcached_DataObject
         
         $mod = base64_url_decode($matches[1]);
         $exp = base64_url_decode($matches[2]);
-        if ($matches[4]) {
+        if (!empty($matches[4])) {
             $private_exp = base64_url_decode($matches[4]);
+        } else {
+            $private_exp = false;
         }
 
         $params['public_key'] = new Crypt_RSA_KEY($mod, $exp, 'public');
index 7b1aec76baa7eac9b73879b108b7e10a8eee85ff..a33e95d932c02f7faea0c4f7ee3f56eb9ac788d6 100644 (file)
@@ -698,7 +698,7 @@ class Ostatus_profile extends Memcached_DataObject
     {
         // Get the canonical feed URI and check it
         $discover = new FeedDiscovery();
-        if ($hints['feedurl']) {
+        if (isset($hints['feedurl'])) {
             $feeduri = $hints['feedurl'];
             $feeduri = $discover->discoverFromFeedURL($feeduri);
         } else {
@@ -1145,7 +1145,7 @@ class Ostatus_profile extends Memcached_DataObject
 
         if (!empty($poco)) {
             $url = $poco->getPrimaryURL();
-            if ($url->type == 'homepage') {
+            if ($url && $url->type == 'homepage') {
                 $homepage = $url->value;
             }
         }
index 388df0a28feed184d48df8a1887fb7bc0c5a3044..f8449b309ec11bda481710af91b803118921f89c 100644 (file)
@@ -94,7 +94,7 @@ class Discovery
             $links = call_user_func(array($class, 'discover'), $uri);
             if ($link = Discovery::getService($links, Discovery::LRDD_REL)) {
                 // Load the LRDD XRD
-                if ($link['template']) {
+                if (!empty($link['template'])) {
                     $xrd_uri = Discovery::applyTemplate($link['template'], $uri);
                 } else {
                     $xrd_uri = $link['href'];
index 16d27f8eb7f66072d24f1f5b98ba8606d85c9563..85df26c54cb752605c9babb4bccd4ef65f690085 100644 (file)
@@ -53,17 +53,22 @@ class XRD
         $xrd = new XRD();
 
         $dom = new DOMDocument();
-        $dom->loadXML($xml);
+        if (!$dom->loadXML($xml)) {
+            throw new Exception("Invalid XML");
+        }
         $xrd_element = $dom->getElementsByTagName('XRD')->item(0);
 
         // Check for host-meta host
-        $host = $xrd_element->getElementsByTagName('Host')->item(0)->nodeValue;
+        $host = $xrd_element->getElementsByTagName('Host')->item(0);
         if ($host) {
-            $xrd->host = $host;
+            $xrd->host = $host->nodeValue;
         }
 
         // Loop through other elements
         foreach ($xrd_element->childNodes as $node) {
+            if (!($node instanceof DOMElement)) {
+                continue;
+            }
             switch ($node->tagName) {
             case 'Expires':
                 $xrd->expires = $node->nodeValue;
@@ -156,20 +161,20 @@ class XRD
     function saveLink($doc, $link)
     {
         $link_element = $doc->createElement('Link');
-        if ($link['rel']) {
+        if (!empty($link['rel'])) {
             $link_element->setAttribute('rel', $link['rel']);
         }
-        if ($link['type']) {
+        if (!empty($link['type'])) {
             $link_element->setAttribute('type', $link['type']);
         }
-        if ($link['href']) {
+        if (!empty($link['href'])) {
             $link_element->setAttribute('href', $link['href']);
         }
-        if ($link['template']) {
+        if (!empty($link['template'])) {
             $link_element->setAttribute('template', $link['template']);
         }
 
-        if (is_array($link['title'])) {
+        if (!empty($link['title']) && is_array($link['title'])) {
             foreach($link['title'] as $title) {
                 $title = $doc->createElement('Title', $title);
                 $link_element->appendChild($title);
index dedc018e3fbd7200d7e2a07f427a86fc6ce23b7f..ee19cf3dbd8e6a14fa392900d4d72906ad1540f6 100644 (file)
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2009-12-07 20:38-0800\n"
+"POT-Creation-Date: 2010-03-01 14:08-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"
@@ -16,89 +16,297 @@ msgstr ""
 "Content-Type: text/plain; charset=CHARSET\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: tests/gettext-speedtest.php:57 FeedSubPlugin.php:76
-msgid "Feeds"
+#: 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."
 msgstr ""
 
-#: FeedSubPlugin.php:77
-msgid "Feed subscription options"
+#: actions/groupsalmon.php:126 actions/groupsalmon.php:169
+msgid "Groups can't join groups."
 msgstr ""
 
-#: feedmunger.php:215
+#: actions/groupsalmon.php:153
 #, php-format
-msgid "New post: \"%1$s\" %2$s"
+msgid "Could not join remote user %1$s to group %2$s."
 msgstr ""
 
-#: actions/feedsubsettings.php:41
-msgid "Feed subscriptions"
+#: actions/groupsalmon.php:166
+msgid "Can't read profile to cancel group membership."
 msgstr ""
 
-#: actions/feedsubsettings.php:52
-msgid ""
-"You can subscribe to feeds from other sites; updates will appear in your "
-"personal timeline."
+#: actions/groupsalmon.php:182
+#, php-format
+msgid "Could not remove remote user %1$s from group %2$s."
+msgstr ""
+
+#: actions/ostatusinit.php:40
+msgid "You can use the local subscription!"
+msgstr ""
+
+#: actions/ostatusinit.php:61
+msgid "There was a problem with your session token. Try again, please."
+msgstr ""
+
+#: actions/ostatusinit.php:79 actions/ostatussub.php:439
+msgid "Subscribe to user"
+msgstr ""
+
+#: actions/ostatusinit.php:97
+#, php-format
+msgid "Subscribe to %s"
 msgstr ""
 
-#: actions/feedsubsettings.php:96
+#: actions/ostatusinit.php:102
+msgid "User nickname"
+msgstr ""
+
+#: actions/ostatusinit.php:103
+msgid "Nickname of the user you want to follow"
+msgstr ""
+
+#: actions/ostatusinit.php:106
+msgid "Profile Account"
+msgstr ""
+
+#: actions/ostatusinit.php:107
+msgid "Your account id (i.e. user@identi.ca)"
+msgstr ""
+
+#: actions/ostatusinit.php:110 actions/ostatussub.php:115
+#: OStatusPlugin.php:205
 msgid "Subscribe"
 msgstr ""
 
-#: actions/feedsubsettings.php:98
+#: actions/ostatusinit.php:128
+msgid "Must provide a remote profile."
+msgstr ""
+
+#: actions/ostatusinit.php:138
+msgid "Couldn't look up OStatus account profile."
+msgstr ""
+
+#: actions/ostatusinit.php:153
+msgid "Couldn't confirm remote profile address."
+msgstr ""
+
+#: actions/ostatusinit.php:171
+msgid "OStatus Connect"
+msgstr ""
+
+#: actions/ostatussub.php:68
+msgid "Address or profile URL"
+msgstr ""
+
+#: actions/ostatussub.php:70
+msgid "Enter the profile URL of a PubSubHubbub-enabled feed"
+msgstr ""
+
+#: actions/ostatussub.php:74
 msgid "Continue"
 msgstr ""
 
-#: actions/feedsubsettings.php:151
-msgid "Empty feed URL!"
+#: actions/ostatussub.php:112 OStatusPlugin.php:503
+msgid "Join"
+msgstr ""
+
+#: actions/ostatussub.php:113
+msgid "Join this group"
+msgstr ""
+
+#: actions/ostatussub.php:116
+msgid "Subscribe to this user"
+msgstr ""
+
+#: actions/ostatussub.php:137
+msgid "You are already subscribed to this user."
+msgstr ""
+
+#: actions/ostatussub.php:165
+msgid "You are already a member of this group."
 msgstr ""
 
-#: actions/feedsubsettings.php:161
+#: actions/ostatussub.php:286
+msgid "Empty remote profile URL!"
+msgstr ""
+
+#: actions/ostatussub.php:297
+msgid "Invalid address format."
+msgstr ""
+
+#: actions/ostatussub.php:302
 msgid "Invalid URL or could not reach server."
 msgstr ""
 
-#: actions/feedsubsettings.php:164
+#: actions/ostatussub.php:304
 msgid "Cannot read feed; server returned error."
 msgstr ""
 
-#: actions/feedsubsettings.php:167
+#: actions/ostatussub.php:306
 msgid "Cannot read feed; server returned an empty page."
 msgstr ""
 
-#: actions/feedsubsettings.php:170
+#: actions/ostatussub.php:308
 msgid "Bad HTML, could not find feed link."
 msgstr ""
 
-#: actions/feedsubsettings.php:173
+#: actions/ostatussub.php:310
 msgid "Could not find a feed linked from this URL."
 msgstr ""
 
-#: actions/feedsubsettings.php:176
+#: actions/ostatussub.php:312
 msgid "Not a recognized feed type."
 msgstr ""
 
-#: actions/feedsubsettings.php:180
-msgid "Bad feed URL."
+#: actions/ostatussub.php:315
+#, php-format
+msgid "Bad feed URL: %s %s"
+msgstr ""
+
+#. TRANS: OStatus remote group subscription dialog error.
+#: actions/ostatussub.php:336
+msgid "Already a member!"
 msgstr ""
 
-#: actions/feedsubsettings.php:188
-msgid "Feed is not PuSH-enabled; cannot subscribe."
+#. TRANS: OStatus remote group subscription dialog error.
+#: actions/ostatussub.php:346
+msgid "Remote group join failed!"
 msgstr ""
 
-#: actions/feedsubsettings.php:208
-msgid "Feed subscription failed! Bad response from hub."
+#. TRANS: OStatus remote group subscription dialog error.
+#: actions/ostatussub.php:350
+msgid "Remote group join aborted!"
 msgstr ""
 
-#: actions/feedsubsettings.php:218
+#. TRANS: OStatus remote subscription dialog error.
+#: actions/ostatussub.php:356
 msgid "Already subscribed!"
 msgstr ""
 
-#: actions/feedsubsettings.php:220
-msgid "Feed subscribed!"
+#. TRANS: OStatus remote subscription dialog error.
+#: actions/ostatussub.php:361
+msgid "Remote subscription failed!"
 msgstr ""
 
-#: actions/feedsubsettings.php:222
-msgid "Feed subscription failed!"
+#. TRANS: Page title for OStatus remote subscription form
+#: actions/ostatussub.php:459
+msgid "Authorize subscription"
+msgstr ""
+
+#: actions/ostatussub.php:470
+msgid ""
+"You can subscribe to users from other supported sites. Paste their address "
+"or profile URI below:"
+msgstr ""
+
+#: classes/Ostatus_profile.php:789
+#, php-format
+msgid "Tried to update avatar for unsaved remote profile %s"
 msgstr ""
 
-#: actions/feedsubsettings.php:231
-msgid "Previewing feed:"
+#: classes/Ostatus_profile.php:797
+#, php-format
+msgid "Unable to fetch avatar from %s"
+msgstr ""
+
+#: lib/salmonaction.php:41
+msgid "This method requires a POST."
+msgstr ""
+
+#: lib/salmonaction.php:45
+msgid "Salmon requires application/magic-envelope+xml"
+msgstr ""
+
+#: lib/salmonaction.php:55
+msgid "Salmon signature verification failed."
+msgstr ""
+
+#: lib/salmonaction.php:66
+msgid "Salmon post must be an Atom entry."
+msgstr ""
+
+#: lib/salmonaction.php:114
+msgid "Unrecognized activity type."
+msgstr ""
+
+#: lib/salmonaction.php:122
+msgid "This target doesn't understand posts."
+msgstr ""
+
+#: lib/salmonaction.php:127
+msgid "This target doesn't understand follows."
+msgstr ""
+
+#: lib/salmonaction.php:132
+msgid "This target doesn't understand unfollows."
+msgstr ""
+
+#: lib/salmonaction.php:137
+msgid "This target doesn't understand favorites."
+msgstr ""
+
+#: lib/salmonaction.php:142
+msgid "This target doesn't understand unfavorites."
+msgstr ""
+
+#: lib/salmonaction.php:147
+msgid "This target doesn't understand share events."
+msgstr ""
+
+#: lib/salmonaction.php:152
+msgid "This target doesn't understand joins."
+msgstr ""
+
+#: lib/salmonaction.php:157
+msgid "This target doesn't understand leave events."
+msgstr ""
+
+#: OStatusPlugin.php:319
+#, php-format
+msgid "Sent from %s via OStatus"
+msgstr ""
+
+#: OStatusPlugin.php:371
+msgid "Could not set up remote subscription."
+msgstr ""
+
+#: OStatusPlugin.php:487
+msgid "Could not set up remote group membership."
+msgstr ""
+
+#: OStatusPlugin.php:504
+#, php-format
+msgid "%s has joined group %s."
+msgstr ""
+
+#: OStatusPlugin.php:512
+msgid "Failed joining remote group."
+msgstr ""
+
+#: OStatusPlugin.php:553
+msgid "Leave"
+msgstr ""
+
+#: OStatusPlugin.php:554
+#, php-format
+msgid "%s has left group %s."
+msgstr ""
+
+#: OStatusPlugin.php:685
+msgid "Subscribe to remote user"
+msgstr ""
+
+#: OStatusPlugin.php:726
+msgid "Profile update"
+msgstr ""
+
+#: OStatusPlugin.php:727
+#, php-format
+msgid "%s has updated their profile page."
+msgstr ""
+
+#: tests/gettext-speedtest.php:57
+msgid "Feeds"
 msgstr ""
diff --git a/plugins/OStatus/scripts/updateostatus.php b/plugins/OStatus/scripts/updateostatus.php
new file mode 100644 (file)
index 0000000..d553a7d
--- /dev/null
@@ -0,0 +1,127 @@
+#!/usr/bin/env php
+<?php
+/*
+ * StatusNet - a distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+
+$shortoptions = 'i:n:a';
+$longoptions = array('id=', 'nickname=', 'all');
+
+$helptext = <<<END_OF_UPDATEOSTATUS_HELP
+updateostatus.php [options]
+update the OMB subscriptions of a user to use OStatus if possible
+
+  -i --id       ID of user to update
+  -n --nickname nickname of the user to update
+  -a --all      update all
+
+END_OF_UPDATEOSTATUS_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+try {
+    $user = null;
+
+    if (have_option('i', 'id')) {
+        $id = get_option_value('i', 'id');
+        $user = User::staticGet('id', $id);
+        if (empty($user)) {
+            throw new Exception("Can't find user with id '$id'.");
+        }
+        updateProfileURL($user);
+    } else if (have_option('n', 'nickname')) {
+        $nickname = get_option_value('n', 'nickname');
+        $user = User::staticGet('nickname', $nickname);
+        if (empty($user)) {
+            throw new Exception("Can't find user with nickname '$nickname'");
+        }
+        updateProfileURL($user);
+    } else if (have_option('a', 'all')) {
+        $user = new User();
+        if ($user->find()) {
+            while ($user->fetch()) {
+                updateOStatus($user);
+            }
+        }
+    } else {
+        show_help();
+        exit(1);
+    }
+} catch (Exception $e) {
+    print $e->getMessage()."\n";
+    exit(1);
+}
+
+function updateOStatus($user)
+{
+    if (!have_option('q', 'quiet')) {
+        echo "{$user->nickname}...";
+    }
+
+    $up = $user->getProfile();
+
+    $sp = $user->getSubscriptions();
+
+    $rps = array();
+
+    while ($sp->fetch()) {
+        $remote = Remote_profile::staticGet('id', $sp->id);
+
+        if (!empty($remote)) {
+            $rps[] = clone($sp);
+        }
+    }
+
+    if (!have_option('q', 'quiet')) {
+        echo count($rps) . "\n";
+    }
+
+    foreach ($rps as $rp) {
+        try {
+            if (!have_option('q', 'quiet')) {
+                echo "Checking {$rp->nickname}...";
+            }
+
+            $op = Ostatus_profile::ensureProfile($rp->profileurl);
+
+            if (empty($op)) {
+                echo "can't convert.\n";
+                continue;
+            } else {
+                if (!have_option('q', 'quiet')) {
+                    echo "Converting...";
+                }
+                Subscription::cancel($up, $rp);
+                Subscription::start($up, $op->localProfile());
+                if (!have_option('q', 'quiet')) {
+                    echo "done.\n";
+                }
+            }
+
+        } catch (Exception $e) {
+            if (!have_option('q', 'quiet')) {
+                echo "fail.\n";
+            }
+            continue;
+            common_log(LOG_WARNING, "Couldn't convert OMB subscription (" . $up->nickname . ", " . $rp->nickname .
+                       ") to OStatus: " . $e->getMessage());
+            continue;
+        }
+    }
+}
diff --git a/plugins/RegisterThrottle/RegisterThrottlePlugin.php b/plugins/RegisterThrottle/RegisterThrottlePlugin.php
new file mode 100644 (file)
index 0000000..05709b7
--- /dev/null
@@ -0,0 +1,249 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Throttle registration by IP address
+ *
+ * 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  Spam
+ * @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')) {
+    exit(1);
+}
+
+/**
+ * Throttle registration by IP address
+ *
+ * We a) record IP address of registrants and b) throttle registrations.
+ *
+ * @category  Spam
+ * @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 RegisterThrottlePlugin extends Plugin
+{
+    /**
+     * Array of time spans in seconds to limits.
+     *
+     * Default is 3 registrations per hour, 5 per day, 10 per week.
+     */
+
+    public $regLimits = array(604800 => 10, // per week
+                              86400 => 5, // per day
+                              3600 => 3); // per hour
+
+    /**
+     * Database schema setup
+     *
+     * We store user registrations in a table registration_ip.
+     *
+     * @return boolean hook value; true means continue processing, false means stop.
+     */
+
+    function onCheckSchema()
+    {
+        $schema = Schema::get();
+
+        // For storing user-submitted flags on profiles
+
+        $schema->ensureTable('registration_ip',
+                             array(new ColumnDef('user_id', 'integer', null,
+                                                 false, 'PRI'),
+                                   new ColumnDef('ipaddress', 'varchar', 15, false, 'MUL'),
+                                   new ColumnDef('created', 'timestamp', null, false, 'MUL')));
+
+        return true;
+    }
+
+    /**
+     * 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 'Registration_ip':
+            include_once $dir . '/'.$cls.'.php';
+            return false;
+        default:
+            return true;
+        }
+    }
+
+    /**
+     * Called when someone tries to register.
+     *
+     * We check the IP here to determine if it goes over any of our
+     * configured limits.
+     *
+     * @param Action $action Action that is being executed
+     *
+     * @return boolean hook value
+     *
+     */
+
+    function onStartRegistrationTry($action)
+    {
+        $ipaddress = $this->_getIpAddress();
+
+        if (empty($ipaddress)) {
+            throw new ServerException(_m('Cannot find IP address.'));
+        }
+
+        foreach ($this->regLimits as $seconds => $limit) {
+
+            $this->debug("Checking $seconds ($limit)");
+
+            $reg = $this->_getNthReg($ipaddress, $limit);
+
+            if (!empty($reg)) {
+                $this->debug("Got a {$limit}th registration.");
+                $regtime = strtotime($reg->created);
+                $now     = time();
+                $this->debug("Comparing {$regtime} to {$now}");
+                if ($now - $regtime < $seconds) {
+                    throw new Exception(_("Too many registrations. Take a break and try again later."));
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Called after someone registers.
+     *
+     * We record the successful registration and IP address.
+     *
+     * @param Action $action Action that is being executed
+     *
+     * @return boolean hook value
+     *
+     */
+
+    function onEndRegistrationTry($action)
+    {
+        $ipaddress = $this->_getIpAddress();
+
+        if (empty($ipaddress)) {
+            throw new ServerException(_m('Cannot find IP address.'));
+        }
+
+        $user = common_current_user();
+
+        if (empty($user)) {
+            throw new ServerException(_m('Cannot find user after successful registration.'));
+        }
+
+        $reg = new Registration_ip();
+
+        $reg->user_id   = $user->id;
+        $reg->ipaddress = $ipaddress;
+
+        $result = $reg->insert();
+
+        if (!$result) {
+            common_log_db_error($reg, 'INSERT', __FILE__);
+            // @todo throw an exception?
+        }
+
+        return true;
+    }
+
+    /**
+     * Check the version of the plugin.
+     *
+     * @param array &$versions Version array.
+     *
+     * @return boolean hook value
+     */
+
+    function onPluginVersion(&$versions)
+    {
+        $versions[] = array('name' => 'RegisterThrottle',
+                            'version' => STATUSNET_VERSION,
+                            'author' => 'Evan Prodromou',
+                            'homepage' => 'http://status.net/wiki/Plugin:RegisterThrottle',
+                            'description' =>
+                            _m('Throttles excessive registration from a single IP.'));
+        return true;
+    }
+
+    /**
+     * Gets the current IP address.
+     *
+     * @return string IP address or null if not found.
+     */
+
+    private function _getIpAddress()
+    {
+        $keys = array('HTTP_X_FORWARDED_FOR',
+                      'CLIENT-IP',
+                      'REMOTE_ADDR');
+
+        foreach ($keys as $k) {
+            if (!empty($_SERVER[$k])) {
+                return $_SERVER[$k];
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Gets the Nth registration with the given IP address.
+     *
+     * @param string  $ipaddress Address to key on
+     * @param integer $n         Nth address
+     *
+     * @return Registration_ip nth registration or null if not found.
+     */
+
+    private function _getNthReg($ipaddress, $n)
+    {
+        $reg = new Registration_ip();
+
+        $reg->ipaddress = $ipaddress;
+
+        $reg->orderBy('created DESC');
+        $reg->limit($n - 1, 1);
+
+        if ($reg->find(true)) {
+            return $reg;
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/plugins/RegisterThrottle/Registration_ip.php b/plugins/RegisterThrottle/Registration_ip.php
new file mode 100644 (file)
index 0000000..7e61d08
--- /dev/null
@@ -0,0 +1,124 @@
+<?php
+/**
+ * Data class for storing IP addresses of new registrants.
+ *
+ * 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 storing IP addresses of new registrants.
+ *
+ * @category Spam
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://status.net/
+ */
+
+class Registration_ip extends Memcached_DataObject
+{
+    public $__table = 'registration_ip';     // table name
+    public $user_id;                         // int(4)  primary_key not_null
+    public $ipaddress;                       // varchar(15)
+    public $created;                         // timestamp
+
+    /**
+     * Get an instance by key
+     *
+     * @param string $k Key to use to lookup (usually 'user_id' for this class)
+     * @param mixed  $v Value to lookup
+     *
+     * @return User_greeting_count object found, or null for no hits
+     *
+     */
+
+    function staticGet($k, $v=null)
+    {
+        return Memcached_DataObject::staticGet('Registration_ip', $k, $v);
+    }
+
+    /**
+     * return table definition for DB_DataObject
+     *
+     * @return array array of column definitions
+     */
+
+    function table()
+    {
+        return array('user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+                     'ipaddress' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+                     'created' => DB_DATAOBJECT_MYSQLTIMESTAMP + 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('user_id' => '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();
+    }
+
+    /**
+     * Magic formula for non-autoincrementing integer primary keys
+     *
+     * If a table has a single integer column as its primary key, DB_DataObject
+     * assumes that the column is auto-incrementing and makes a sequence table
+     * to do this incrementation. Since we don't need this for our class, we
+     * overload this method and return the magic formula that DB_DataObject needs.
+     *
+     * @return array magic three-false array that stops auto-incrementing.
+     */
+
+    function sequenceKey()
+    {
+        return array(false, false, false);
+    }
+}
index d3bcda5984d7c432c93dbb80ca2d9ee4eb49ed5a..72278b32e6eac525ec38fd19422a66a0872c4975 100644 (file)
@@ -1,47 +1,89 @@
+Twitter Bridge Plugin
+=====================
+
 This Twitter "bridge" plugin allows you to integrate your StatusNet
 instance with Twitter.  Installing it will allow your users to:
 
-    - automatically post notices to thier Twitter accounts
+    - automatically post notices to their Twitter accounts
     - automatically subscribe to other Twitter users who are also using
       your StatusNet install, if possible (requires running a daemon)
     - import their Twitter friends' tweets (requires running a daemon)
+    - allow users to authenticate using Twitter ('Sign in with Twitter')
 
 Installation
 ------------
 
-To enable the plugin, add the following to your config.php:
-
-    addPlugin("TwitterBridge");
-
-OAuth is used to to access protected resources on Twitter (as opposed to
-HTTP Basic Auth)*.  To use Twitter bridging you will need to register
-your instance of StatusNet as an application on Twitter
-(http://twitter.com/apps), and update the following variables in your
-config.php with the consumer key and secret Twitter generates for you:
-
-    $config['twitter']['consumer_key']    = 'YOURKEY';
-    $config['twitter']['consumer_secret'] = 'YOURSECRET';
+OAuth 1.0a (http://oauth.net) is used to to access protected resources
+on Twitter (as opposed to HTTP Basic Auth)*.  To use Twitter bridging
+you will need to register your instance of StatusNet as an application
+on Twitter (http://twitter.com/apps).  During the application
+registration process your application will be assigned a "consumer" key
+and secret, which the plugin will use to make OAuth requests to Twitter.
+You can either pass the consumer key and secret in when you enable the
+plugin, or set it using the Twitter administration panel.
 
 When registering your application with Twitter set the type to "Browser"
 and your Callback URL to:
 
     http://example.org/mublog/twitter/authorization
 
-The default access type should be, "Read & Write".
+(Change "example.org" to your site domain and "mublog" to your site
+path.)
+
+The default access type should be "Read & Write".
+
+To enable the plugin, add the following to your config.php:
+
+    addPlugin(
+        'TwitterBridge',
+        array(
+            'consumer_key'    => 'YOUR_CONSUMER_KEY',
+            'consumer_secret' => 'YOUR_CONSUMER_SECRET'
+        )
+    );
 
 * Note: The plugin will still push notices to Twitter for users who
-  have previously setup the Twitter bridge using their Twitter name and
-  password under an older versions of StatusNet, but all new Twitter
+  have previously set up the Twitter bridge using their Twitter name and
+  password under an older version of StatusNet, but all new Twitter
   bridge connections will use OAuth.
 
-Deamons
+Administration panel
+--------------------
+
+As of StatusNet 0.9.0 there is a new administration panel that allows
+you to configure Twitter bridge settings within StatusNet itself,
+instead of having to specify them manually in your config.php.  To enable
+the administration panel, you will need to add it to the list of active
+administration panels.  You can do this via your config.php. E.g.:
+
+    $config['admin']['panels'][] = 'twitter';
+
+And to access it, you'll need to use a user with the "administrator"
+role (see: scripts/userrole.php).
+
+Sign in with Twitter
+--------------------
+
+With 0.9.0, StatusNet optionally allows users to register and
+authenticate using their Twitter credentials via the "Sign in with
+Twitter" pattern described here:
+
+    http://apiwiki.twitter.com/Sign-in-with-Twitter
+
+The option is _on_ by default when you install the plugin, but it can
+disabled via the Twitter bridge administration panel, or by adding the
+following line to your config.php:
+
+    $config['twitter']['signin'] = false;
+
+Daemons
 -------
 
-For friend syncing and importing notices running two additional daemon
-scripts is necessary (synctwitterfriends.php and
-twitterstatusfetcher.php).
+For friend syncing and importing Twitter tweets, running two
+additional daemon scripts is necessary: synctwitterfriends.php and
+twitterstatusfetcher.php.
 
-In the daemons subidrectory of the plugin are three scripts:
+In the daemons subdirectory of the plugin are three scripts:
 
 * Twitter Friends Syncing (daemons/synctwitterfriends.php)
 
@@ -51,13 +93,13 @@ subscribe to "friends" (people they "follow") on Twitter who also have
 accounts on your StatusNet system, and who have previously set up a link
 for automatically posting notices to Twitter.
 
-The plugin will try to start this daemon when you run
-scripts/startdaemons.sh.
+The plugin will start this daemon when you run scripts/startdaemons.sh.
 
 * Importing statuses from Twitter (daemons/twitterstatusfetcher.php)
 
-To allow your users to import their friends' Twitter statuses, you will
-need to enable the bidirectional Twitter bridge in your config.php:
+You can allow uses to enable importing of your friends' Twitter
+timelines either in the Twitter bridge administration panel or in your
+config.php using the following configuration line:
 
     $config['twitterimport']['enabled'] = true;
 
@@ -66,8 +108,9 @@ other daemons when you run scripts/startdaemons.sh.
 
 Additionally, you will want to set the integration source variable,
 which will keep notices posted to Twitter via StatusNet from looping
-back.  The integration source should be set to the name of your
-application, exactly as you specified it on the settings page for your
+back.  You can do this in the Twitter bridge administration panel, or
+via config.php. The integration source should be set to the name of your
+application _exactly_ as you specified it on the settings page for your
 StatusNet application on Twitter, e.g.:
 
     $config['integration']['source'] = 'YourApp';
@@ -79,7 +122,9 @@ set up Twitter bridging.
 
 It's not strictly necessary to run this queue handler, and sites that
 haven't enabled queuing are still able to push notices to Twitter, but
-for larger sites and sites that wish to improve performance, this
-script allows notices to be sent "offline" via a separate process.
+for larger sites and sites that wish to improve performance the script
+allows notices to be sent "offline" via a separate process.
 
-The plugin will start this script when you run scripts/startdaemons.sh.
+StatusNet will automatically use the TwitterQueueHandler if you have
+enabled the queuing subsystem.  See the "Queues and daemons" section of
+the main README file for more information about how to do that.
index c7f57ffc7753066f29b86d4020b072fe41ddd928..6ce69d5e2b3f5b2434b465b87afb9852372abb32 100644 (file)
@@ -23,7 +23,7 @@
  * @author    Julien C <chaumond@gmail.com>
  * @copyright 2009-2010 Control Yourself, Inc.
  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
- * @link      http://laconi.ca/
+ * @link      http://status.net/
  */
 
 if (!defined('STATUSNET')) {
@@ -32,8 +32,6 @@ if (!defined('STATUSNET')) {
 
 require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
 
-define('TWITTERBRIDGEPLUGIN_VERSION', '0.9');
-
 /**
  * Plugin for sending and importing Twitter statuses
  *
@@ -44,19 +42,41 @@ define('TWITTERBRIDGEPLUGIN_VERSION', '0.9');
  * @author   Zach Copley <zach@status.net>
  * @author   Julien C <chaumond@gmail.com>
  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
- * @link     http://laconi.ca/
+ * @link     http://status.net/
  * @link     http://twitter.com/
  */
 
 class TwitterBridgePlugin extends Plugin
 {
+
+    const VERSION = STATUSNET_VERSION;
+
     /**
      * Initializer for the plugin.
      */
 
-    function __construct()
+    function initialize()
     {
-        parent::__construct();
+        // Allow the key and secret to be passed in
+        // Control panel will override
+
+        if (isset($this->consumer_key)) {
+            $key = common_config('twitter', 'consumer_key');
+            if (empty($key)) {
+                Config::save('twitter', 'consumer_key', $this->consumer_key);
+            }
+        }
+
+        if (isset($this->consumer_secret)) {
+            $secret = common_config('twitter', 'consumer_secret');
+            if (empty($secret)) {
+                Config::save(
+                    'twitter',
+                    'consumer_secret',
+                    $this->consumer_secret
+                );
+            }
+        }
     }
 
     /**
@@ -71,10 +91,17 @@ class TwitterBridgePlugin extends Plugin
 
     function onRouterInitialized($m)
     {
-        $m->connect('twitter/authorization',
-                    array('action' => 'twitterauthorization'));
+        $m->connect(
+            'twitter/authorization',
+            array('action' => 'twitterauthorization')
+        );
         $m->connect('settings/twitter', array('action' => 'twittersettings'));
-        $m->connect('main/twitterlogin', array('action' => 'twitterlogin'));
+
+        if (common_config('twitter', 'signin')) {
+            $m->connect('main/twitterlogin', array('action' => 'twitterlogin'));
+        }
+
+        $m->connect('admin/twitter', array('action' => 'twitteradminpanel'));
 
         return true;
     }
@@ -88,13 +115,16 @@ class TwitterBridgePlugin extends Plugin
      */
     function onEndLoginGroupNav(&$action)
     {
-
         $action_name = $action->trimmed('action');
 
-        $action->menuItem(common_local_url('twitterlogin'),
-                                           _('Twitter'),
-                                           _('Login or register using Twitter'),
-                                             'twitterlogin' === $action_name);
+        if (common_config('twitter', 'signin')) {
+            $action->menuItem(
+                common_local_url('twitterlogin'),
+                _m('Twitter'),
+                _m('Login or register using Twitter'),
+                'twitterlogin' === $action_name
+            );
+        }
 
         return true;
     }
@@ -110,10 +140,12 @@ class TwitterBridgePlugin extends Plugin
     {
         $action_name = $action->trimmed('action');
 
-        $action->menuItem(common_local_url('twittersettings'),
-                          _m('Twitter'),
-                          _m('Twitter integration options'),
-                          $action_name === 'twittersettings');
+        $action->menuItem(
+            common_local_url('twittersettings'),
+            _m('Twitter'),
+            _m('Twitter integration options'),
+            $action_name === 'twittersettings'
+        );
 
         return true;
     }
@@ -132,6 +164,7 @@ class TwitterBridgePlugin extends Plugin
         case 'TwittersettingsAction':
         case 'TwitterauthorizationAction':
         case 'TwitterloginAction':
+        case 'TwitteradminpanelAction':
             include_once INSTALLDIR . '/plugins/TwitterBridge/' .
               strtolower(mb_substr($cls, 0, -6)) . '.php';
             return false;
@@ -173,12 +206,18 @@ class TwitterBridgePlugin extends Plugin
      */
     function onGetValidDaemons($daemons)
     {
-        array_push($daemons, INSTALLDIR .
-                   '/plugins/TwitterBridge/daemons/synctwitterfriends.php');
+        array_push(
+            $daemons,
+            INSTALLDIR
+            . '/plugins/TwitterBridge/daemons/synctwitterfriends.php'
+        );
 
         if (common_config('twitterimport', 'enabled')) {
-            array_push($daemons, INSTALLDIR
-                . '/plugins/TwitterBridge/daemons/twitterstatusfetcher.php');
+            array_push(
+                $daemons,
+                INSTALLDIR
+                . '/plugins/TwitterBridge/daemons/twitterstatusfetcher.php'
+            );
         }
 
         return true;
@@ -197,17 +236,55 @@ class TwitterBridgePlugin extends Plugin
         return true;
     }
 
+    /**
+     * Add a Twitter tab to the admin panel
+     *
+     * @param Widget $nav Admin panel nav
+     *
+     * @return boolean hook value
+     */
+
+    function onEndAdminPanelNav($nav)
+    {
+        if (AdminPanelAction::canAdmin('twitter')) {
+
+            $action_name = $nav->action->trimmed('action');
+
+            $nav->out->menuItem(
+                common_local_url('twitteradminpanel'),
+                _m('Twitter'),
+                _m('Twitter bridge configuration'),
+                $action_name == 'twitteradminpanel',
+                'nav_twitter_admin_panel'
+            );
+        }
+
+        return true;
+    }
+
+    /**
+     * Plugin version data
+     *
+     * @param array &$versions array of version blocks
+     *
+     * @return boolean hook value
+     */
+
     function onPluginVersion(&$versions)
     {
-        $versions[] = array('name' => 'TwitterBridge',
-                            'version' => TWITTERBRIDGEPLUGIN_VERSION,
-                            'author' => 'Zach Copley',
-                            'homepage' => 'http://status.net/wiki/Plugin:TwitterBridge',
-                            'rawdescription' =>
-                            _m('The Twitter "bridge" plugin allows you to integrate ' .
-                               'your StatusNet instance with ' .
-                               '<a href="http://twitter.com/">Twitter</a>.'));
+        $versions[] = array(
+            'name' => 'TwitterBridge',
+            'version' => self::VERSION,
+            'author' => 'Zach Copley, Julien C',
+            'homepage' => 'http://status.net/wiki/Plugin:TwitterBridge',
+            'rawdescription' => _m(
+                'The Twitter "bridge" plugin allows you to integrate ' .
+                'your StatusNet instance with ' .
+                '<a href="http://twitter.com/">Twitter</a>.'
+            )
+        );
         return true;
     }
 
 }
+
diff --git a/plugins/TwitterBridge/twitteradminpanel.php b/plugins/TwitterBridge/twitteradminpanel.php
new file mode 100644 (file)
index 0000000..b22e6d9
--- /dev/null
@@ -0,0 +1,280 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Twitter 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 Twitter bridge 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 TwitteradminpanelAction extends AdminPanelAction
+{
+    /**
+     * Returns the page title
+     *
+     * @return string page title
+     */
+
+    function title()
+    {
+        return _m('Twitter');
+    }
+
+    /**
+     * Instructions for using this form.
+     *
+     * @return string instructions
+     */
+
+    function getInstructions()
+    {
+        return _m('Twitter bridge settings');
+    }
+
+    /**
+     * Show the Twitter admin panel form
+     *
+     * @return void
+     */
+
+    function showForm()
+    {
+        $form = new TwitterAdminPanelForm($this);
+        $form->show();
+        return;
+    }
+
+    /**
+     * Save settings from the form
+     *
+     * @return void
+     */
+
+    function saveSettings()
+    {
+        static $settings = array(
+            'twitter'     => array('consumer_key', 'consumer_secret'),
+            'integration' => array('source')
+        );
+
+        static $booleans = array(
+            'twitter'       => array('signin'),
+            'twitterimport' => array('enabled')
+        );
+
+        $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['twitter']['consumer_key']) > 255) {
+            $this->clientError(
+                _m("Invalid consumer key. Max length is 255 characters.")
+            );
+        }
+
+        if (mb_strlen($values['twitter']['consumer_secret']) > 255) {
+            $this->clientError(
+                _m("Invalid consumer secret. Max length is 255 characters.")
+            );
+        }
+    }
+}
+
+class TwitterAdminPanelForm extends AdminForm
+{
+    /**
+     * ID of the form
+     *
+     * @return int ID of the form
+     */
+
+    function id()
+    {
+        return 'twitteradminpanel';
+    }
+
+    /**
+     * 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('twitteradminpanel');
+    }
+
+    /**
+     * Data elements of the form
+     *
+     * @return void
+     */
+
+    function formData()
+    {
+        $this->out->elementStart(
+            'fieldset',
+            array('id' => 'settings_twitter-application')
+        );
+        $this->out->element('legend', null, _m('Twitter application settings'));
+        $this->out->elementStart('ul', 'form_data');
+
+        $this->li();
+        $this->input(
+            'consumer_key',
+            _m('Consumer key'),
+            _m('Consumer key assigned by Twitter'),
+            'twitter'
+        );
+        $this->unli();
+
+        $this->li();
+        $this->input(
+            'consumer_secret',
+             _m('Consumer secret'),
+            _m('Consumer secret assigned by Twitter'),
+            'twitter'
+        );
+        $this->unli();
+
+        $this->li();
+        $this->input(
+            'source',
+             _m('Integration source'),
+            _m('Name of your Twitter application'),
+            'integration'
+        );
+        $this->unli();
+
+        $this->out->elementEnd('ul');
+        $this->out->elementEnd('fieldset');
+
+        $this->out->elementStart(
+            'fieldset',
+            array('id' => 'settings_twitter-options')
+        );
+        $this->out->element('legend', null, _m('Options'));
+
+        $this->out->elementStart('ul', 'form_data');
+
+        $this->li();
+
+        $this->out->checkbox(
+            'signin', _m('Enable "Sign-in with Twitter"'),
+            (bool) $this->value('signin', 'twitter'),
+            _m('Allow users to login with their Twitter credentials')
+        );
+        $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();
+
+        $this->out->elementEnd('ul');
+
+        $this->out->elementEnd('fieldset');
+    }
+
+    /**
+     * Action elements
+     *
+     * @return void
+     */
+
+    function formActions()
+    {
+        $this->out->submit('submit', _('Save'), 'submit', null, _('Save Twitter settings'));
+    }
+}
index cabf69d7a8b47a0e03dc0fad1cf2eb3c877a5ca9..c93f6666bcc3f718ae42e3ede404abd849005783 100644 (file)
@@ -47,7 +47,7 @@ require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
  * @author   Zach Copley <zach@status.net>
  * @author   Julien C <chaumond@gmail.com>
  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
- * @link     http://laconi.ca/
+ * @link     http://status.net/
  *
  */
 class TwitterauthorizationAction extends Action
index 61a6ac78331d543f66af381255961b103ee9f15a..0bfa62a22e79d2c47a8bfb84094a6d3cb4c3bf5e 100755 (executable)
@@ -36,7 +36,11 @@ xgettext \
     --default-domain=$domain \
     --output=locale/$domain.po \
     --language=PHP \
-    --keyword="_m:1" \
+    --add-comments=TRANS \
+    --keyword="_m:1,1t" \
+    --keyword="_m:1c,2,2t" \
+    --keyword="_m:1,2,3t" \
+    --keyword="_m:1c,2,3,4t" \
     --keyword="pgettext:1c,2" \
     --keyword="npgettext:1c,2,3" \
     actions/*.php \
@@ -62,6 +66,7 @@ xgettext \
     --default-domain=$domain \
     --output=locale/$domain.po \
     --language=PHP \
+    --add-comments=TRANS \
     --keyword='' \
     --keyword="_m:1,1t" \
     --keyword="_m:1c,2,2t" \
index 52f97f6b12b64378be02d92685f87afe49b744e3..f32c57ea451758aa48fa0017817deb74e00cf784 100644 (file)
@@ -799,8 +799,8 @@ list-style-type:none;
 display:inline;
 }
 .entity_tags li {
-float:left;
-margin-right:11px;
+display:inline;
+margin-right:7px;
 }
 
 .aside .section {