]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch '0.9.x' into openidplugin
authorEvan Prodromou <evan@controlyourself.ca>
Fri, 21 Aug 2009 20:27:43 +0000 (16:27 -0400)
committerEvan Prodromou <evan@controlyourself.ca>
Fri, 21 Aug 2009 20:27:43 +0000 (16:27 -0400)
Conflicts:
actions/login.php
actions/register.php

176 files changed:
README
actions/accesstoken.php
actions/all.php
actions/allrss.php
actions/api.php
actions/attachment.php
actions/avatarsettings.php
actions/confirmaddress.php
actions/editgroup.php
actions/emailsettings.php
actions/favorited.php
actions/favoritesrss.php
actions/finishremotesubscribe.php
actions/grouplogo.php
actions/grouprss.php
actions/groupsearch.php
actions/imsettings.php
actions/invite.php
actions/login.php
actions/newgroup.php
actions/newmessage.php
actions/newnotice.php
actions/noticesearch.php
actions/noticesearchrss.php
actions/oembed.php [new file with mode: 0644]
actions/opensearch.php
actions/postnotice.php
actions/profilesettings.php
actions/public.php
actions/publicrss.php
actions/publictagcloud.php
actions/register.php
actions/remotesubscribe.php
actions/replies.php
actions/repliesrss.php
actions/requesttoken.php
actions/showfavorites.php
actions/showgroup.php
actions/shownotice.php
actions/showstream.php
actions/smssettings.php
actions/subscribers.php
actions/subscriptions.php
actions/tag.php
actions/tagrss.php
actions/twitapidirect_messages.php
actions/twitapigroups.php
actions/twitapioembed.php [deleted file]
actions/twitapistatuses.php
actions/twitterauthorization.php [new file with mode: 0644]
actions/twittersettings.php
actions/unsubscribe.php
actions/updateprofile.php
actions/userauthorization.php
actions/userrss.php
actions/xrds.php
classes/Config.php [new file with mode: 0755]
classes/Design.php
classes/File.php
classes/Foreign_link.php
classes/Message.php
classes/Notice.php
classes/Profile.php
classes/User_group.php
classes/laconica.ini
config.php.sample
db/08to09.sql
db/08to09_pg.sql [new file with mode: 0644]
db/laconica.sql
db/notice_source.sql
doc-src/im
doc-src/sms
extlib/libomb/base_url_xrds_mapper.php [new file with mode: 0755]
extlib/libomb/constants.php [new file with mode: 0644]
extlib/libomb/datastore.php [new file with mode: 0755]
extlib/libomb/helper.php [new file with mode: 0644]
extlib/libomb/invalidparameterexception.php [new file with mode: 0755]
extlib/libomb/invalidyadisexception.php [new file with mode: 0755]
extlib/libomb/notice.php [new file with mode: 0755]
extlib/libomb/omb_yadis_xrds.php [new file with mode: 0755]
extlib/libomb/plain_xrds_writer.php [new file with mode: 0755]
extlib/libomb/profile.php [new file with mode: 0755]
extlib/libomb/remoteserviceexception.php [new file with mode: 0755]
extlib/libomb/service_consumer.php [new file with mode: 0755]
extlib/libomb/service_provider.php [new file with mode: 0755]
extlib/libomb/unsupportedserviceexception.php [new file with mode: 0755]
extlib/libomb/xrds_mapper.php [new file with mode: 0755]
extlib/libomb/xrds_writer.php [new file with mode: 0755]
index.php
install.php
js/jcrop/jquery.Jcrop.min.js [new file with mode: 0644]
js/jcrop/jquery.Jcrop.pack.js [deleted file]
js/userdesign.go.js
js/util.js
lib/action.php
lib/arraywrapper.php
lib/command.php
lib/common.php
lib/connectsettingsaction.php
lib/designsettings.php
lib/error.php
lib/facebookaction.php
lib/facebookutil.php
lib/groupeditform.php
lib/htmloutputter.php
lib/jsonsearchresultslist.php
lib/mail.php
lib/messageform.php
lib/noticeform.php
lib/noticelist.php
lib/oauthclient.php [new file with mode: 0644]
lib/oauthstore.php
lib/omb.php
lib/parallelizingdaemon.php [new file with mode: 0644]
lib/profilesection.php
lib/router.php
lib/search_engines.php
lib/servererroraction.php
lib/twitter.php
lib/twitterapi.php
lib/twitteroauthclient.php [new file with mode: 0644]
lib/unqueuemanager.php
lib/util.php
lighttpd.conf.example [new file with mode: 0644]
plugins/Autocomplete/Autocomplete.js [new file with mode: 0644]
plugins/Autocomplete/AutocompletePlugin.php [new file with mode: 0644]
plugins/Autocomplete/jquery-autocomplete/changelog.txt [new file with mode: 0644]
plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.css [new file with mode: 0644]
plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.js [new file with mode: 0644]
plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.min.js [new file with mode: 0644]
plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.pack.js [new file with mode: 0644]
plugins/Autocomplete/jquery-autocomplete/lib/jquery.ajaxQueue.js [new file with mode: 0644]
plugins/Autocomplete/jquery-autocomplete/lib/jquery.bgiframe.min.js [new file with mode: 0644]
plugins/Autocomplete/jquery-autocomplete/lib/jquery.js [new file with mode: 0644]
plugins/Autocomplete/jquery-autocomplete/lib/thickbox-compressed.js [new file with mode: 0644]
plugins/Autocomplete/jquery-autocomplete/lib/thickbox.css [new file with mode: 0644]
plugins/Autocomplete/jquery-autocomplete/todo [new file with mode: 0644]
plugins/Autocomplete/readme.txt [new file with mode: 0644]
plugins/FBConnect/FBC_XDReceiver.php
plugins/FBConnect/FBConnectAuth.php
plugins/FBConnect/FBConnectPlugin.php
plugins/FBConnect/FBConnectSettings.php
plugins/FBConnect/README
plugins/InfiniteScroll/InfiniteScrollPlugin.php [new file with mode: 0644]
plugins/InfiniteScroll/ajax-loader.gif [new file with mode: 0644]
plugins/InfiniteScroll/infinitescroll.js [new file with mode: 0644]
plugins/InfiniteScroll/jquery.infinitescroll.js [new file with mode: 0644]
plugins/InfiniteScroll/jquery.infinitescroll.min.js [new file with mode: 0644]
plugins/InfiniteScroll/readme.txt [new file with mode: 0644]
plugins/OpenID/finishopenidlogin.php
plugins/OpenID/openidlogin.php
plugins/OpenID/openidsettings.php
plugins/Realtime/RealtimePlugin.php
plugins/recaptcha/recaptcha.php
scripts/fixup_utf8.php [changed mode: 0644->0755]
scripts/getvaliddaemons.php
scripts/maildaemon.php
scripts/ombqueuehandler.php
scripts/stopdaemons.sh
scripts/synctwitterfriends.php
scripts/twitterstatusfetcher.php
scripts/xmppdaemon.php
theme/base/css/jquery.Jcrop.css
theme/base/images/icons/icon_atom.png
theme/base/images/icons/icon_rss.png
theme/default/css/display.css
theme/default/default-avatar-mini.png
theme/default/default-avatar-profile.png
theme/default/default-avatar-stream.png
theme/default/logo.png
theme/identica/css/display.css
theme/identica/default-avatar-mini.png
theme/identica/default-avatar-profile.png
theme/identica/default-avatar-stream.png
theme/identica/logo.png
tpl/index.php

diff --git a/README b/README
index ef5a1393468fb7bd22ef1e5d1207a3b2e5e79c98..ccdd9e674e7ddfd7966fe4f0c404b17aeedd38c0 100644 (file)
--- a/README
+++ b/README
@@ -553,25 +553,53 @@ our kind of hacky home-grown DB-based queue solution. See the "queues"
 config section below for how to configure to use STOMP. As of this
 writing, the software has been tested with ActiveMQ (
 
-Twitter Friends Syncing
------------------------
+Twitter Bridge
+--------------
+
+* OAuth
+
+As of 0.8.1, OAuth is used to to access protected resources on Twitter
+instead of HTTP Basic Auth.  To use Twitter bridging you will need
+to register your instance of Laconica 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';
+
+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".
+
+* Importing statuses from Twitter
+
+To allow your users to import their friends' Twitter statuses, you will
+need to enable the bidirectional Twitter bridge in config.php:
 
-As of Laconica 0.6.3, users may set a flag in their settings ("Subscribe
-to my Twitter friends here" under the Twitter tab) to have Laconica
-attempt to locate and subscribe to "friends" (people they "follow") on
-Twitter who also have accounts on your Laconica system, and who have
-previously set up a link for automatically posting notices to Twitter.
+      $config['twitterbridge']['enabled'] = true;
 
-Optionally, there is a script (./scripts/synctwitterfriends.php), meant
-to be run periodically from a job scheduler (e.g.: cron under Unix), to
-look for new additions to users' friends lists. Note that the friends
-syncing only subscribes users to each other, it does not unsubscribe
-users when they stop following each other on Twitter.
+and run the TwitterStatusFetcher daemon (scripts/twitterstatusfetcher.php).
+Additionally, you will want to set the integration source variable,
+which will keep notices posted to Twitter via Laconica 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
+Laconica application on Twitter, e.g.:
 
-Sample cron job:
+      $config['integration']['source'] = 'YourApp';
 
-# Update Twitter friends subscriptions every half hour
-0,30 * * * * /path/to/php /path/to/laconica/scripts/synctwitterfriends.php>&/dev/null
+* Twitter Friends Syncing
+
+Users may set a flag in their settings ("Subscribe to my Twitter friends
+here" under the Twitter tab) to have Laconica attempt to locate and
+subscribe to "friends" (people they "follow") on Twitter who also have
+accounts on your Laconica system, and who have previously set up a link
+for automatically posting notices to Twitter.
+
+As of 0.8.0, this is no longer accomplished via a cron job. Instead you
+must run the SyncTwitterFriends daemon (scripts/synctwitterfreinds.php).
 
 Built-in Facebook Application
 -----------------------------
@@ -940,6 +968,8 @@ closed: If set to 'true', will disallow registration on your site.
        the service, *then* set this variable to 'true'.
 inviteonly: If set to 'true', will only allow registration if the user
            was invited by an existing user.
+openidonly: If set to 'true', will only allow registrations and logins
+           through OpenID.
 private: If set to 'true', anonymous users will be redirected to the
          'login' page. Also, API methods that normally require no
          authentication will require it. Note that this does not turn
@@ -967,6 +997,9 @@ shorturllength: Length of URL at which URLs in a message exceeding 140
 dupelimit: minimum time allowed for one person to say the same thing
            twice. Default 60s. Anything lower is considered a user
            or UI error.
+textlimit: default max size for texts in the site. Defaults to 140.
+           0 means no limit. Can be fine-tuned for notices, messages,
+           profile bios and group descriptions.
 
 db
 --
@@ -1167,6 +1200,14 @@ For configuring invites.
 
 enabled: Whether to allow users to send invites. Default true.
 
+openid
+------
+
+For configuring OpenID.
+
+enabled: Whether to allow users to register and login using OpenID. Default
+        true.
+
 tag
 ---
 
@@ -1228,6 +1269,30 @@ enabled: Set to true to enable. Default false.
 server: a string with the hostname of the sphinx server.
 port: an integer with the port number of the sphinx server.
 
+emailpost
+---------
+
+For post-by-email.
+
+enabled: Whether to enable post-by-email. Defaults to true. You will
+         also need to set up maildaemon.php.
+
+sms
+---
+
+For SMS integration.
+
+enabled: Whether to enable SMS integration. Defaults to true. Queues
+         should also be enabled.
+
+twitter
+-------
+
+For Twitter integration
+
+enabled: Whether to enable Twitter integration. Defaults to true.
+         Queues should also be enabled.
+
 integration
 -----------
 
@@ -1269,6 +1334,8 @@ banned: an array of usernames and/or profile IDs of 'banned' profiles.
         The site will reject any notices by these users -- they will
         not be accepted at all. (Compare with blacklisted users above,
         whose posts just won't show up in the public stream.)
+biolimit: max character length of bio; 0 means no limit; null means to use
+          the site text limit default.
 
 newuser
 -------
@@ -1365,6 +1432,9 @@ Options for group functionality.
 
 maxaliases: maximum number of aliases a group can have. Default 3. Set
             to 0 or less to prevent aliases in a group.
+desclimit: maximum number of characters to allow in group descriptions.
+           null (default) means to use the site-wide text limits. 0
+           means no limit.
 
 oohembed
 --------
@@ -1443,6 +1513,24 @@ linkcolor: Hex color of all links.
 backgroundimage: Image to use for the background.
 disposition: Flags for whether or not to tile the background image.
 
+notice
+------
+
+Configuration options specific to notices.
+
+contentlimit: max length of the plain-text content of a notice.
+              Default is null, meaning to use the site-wide text limit.
+              0 means no limit.
+
+message
+-------
+
+Configuration options specific to messages.
+
+contentlimit: max length of the plain-text content of a message.
+              Default is null, meaning to use the site-wide text limit.
+              0 means no limit.
+
 Plugins
 =======
 
index 2a8cd17134c84833ae1446fa8957c35ac5e06031..dcd04a1b404f79ad0c8635e080ddd30deea374f3 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * Access token class.
+ * Access token class
  *
  * PHP version 5
  *
@@ -32,10 +32,11 @@ if (!defined('LACONICA')) {
     exit(1);
 }
 
+require_once INSTALLDIR.'/extlib/libomb/service_provider.php';
 require_once INSTALLDIR.'/lib/omb.php';
 
 /**
- * Access token class.
+ * Access token class
  *
  * @category Action
  * @package  Laconica
@@ -47,28 +48,23 @@ require_once INSTALLDIR.'/lib/omb.php';
 class AccesstokenAction extends Action
 {
     /**
-     * Class handler.
+     * Class handler
      *
      * @param array $args query arguments
      *
-     * @return boolean false if user doesn't exist
-     */
+     * @return nothing
+     *
+     **/
     function handle($args)
     {
         parent::handle($args);
         try {
-            common_debug('getting request from env variables', __FILE__);
-            common_remove_magic_from_request();
-            $req = OAuthRequest::from_request('POST', common_local_url('accesstoken'));
-            common_debug('getting a server', __FILE__);
-            $server = omb_oauth_server();
-            common_debug('fetching the access token', __FILE__);
-            $token = $server->fetch_access_token($req);
-            common_debug('got this token: "'.print_r($token, true).'"', __FILE__);
-            common_debug('printing the access token', __FILE__);
-            print $token;
-        } catch (OAuthException $e) {
+            $srv = new OMB_Service_Provider(null, omb_oauth_datastore(),
+                                            omb_oauth_server());
+            $srv->writeAccessToken();
+        } catch (Exception $e) {
             $this->serverError($e->getMessage());
         }
     }
 }
+?>
index f06ead2a8c4776f70bd89b5f8d20a9683ede9450..38aee65b64c0c7c85148cdd3dba243ebde124334 100644 (file)
@@ -25,11 +25,31 @@ require_once INSTALLDIR.'/lib/feedlist.php';
 
 class AllAction extends ProfileAction
 {
+    var $notice;
+
     function isReadOnly($args)
     {
         return true;
     }
 
+    function prepare($args)
+    {
+        parent::prepare($args);
+        $cur = common_current_user();
+
+        if (!empty($cur) && $cur->id == $this->user->id) {
+            $this->notice = $this->user->noticeInbox(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
+        } else {
+            $this->notice = $this->user->noticesWithFriends(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
+        }
+
+        if($this->page > 1 && $this->notice->N == 0){
+            $this->serverError(_('No such page'),$code=404);
+        }
+
+        return true;
+    }
+
     function handle($args)
     {
         parent::handle($args);
@@ -88,7 +108,9 @@ class AllAction extends ProfileAction
             }
         }
         else {
-            $message .= sprintf(_('Why not [register an account](%%%%action.register%%%%) and then nudge %s or post a notice to his or her attention.'), $this->user->nickname);
+            $message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and then nudge %s or post a notice to his or her attention.'),
+                                (!common_config('site','openidonly')) ? 'register' : 'openidlogin',
+                                $this->user->nickname);
         }
 
         $this->elementStart('div', 'guide');
@@ -98,15 +120,7 @@ class AllAction extends ProfileAction
 
     function showContent()
     {
-        $cur = common_current_user();
-
-        if (!empty($cur) && $cur->id == $this->user->id) {
-            $notice = $this->user->noticeInbox(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
-        } else {
-            $notice = $this->user->noticesWithFriends(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
-        }
-
-        $nl = new NoticeList($notice, $this);
+        $nl = new NoticeList($this->notice, $this);
 
         $cnt = $nl->show();
 
index 885a67f6188af13b07a0ea7e6bf9a8302078e476..260667090b65b0af175a5920dd0cd948fd6eb731 100644 (file)
@@ -115,8 +115,8 @@ class AllrssAction extends Rss10Action
                    'link' => common_local_url('all',
                                              array('nickname' =>
                                                    $user->nickname)),
-                   'description' => sprintf(_('Feed for friends of %s'),
-                                            $user->nickname));
+                   'description' => sprintf(_('Updates from %1$s and friends on %2$s!'),
+                                            $user->nickname, common_config('site', 'name')));
         return $c;
     }
 
index 99ab262ad77685cdad2a347264e29b079a27382f..6d226af7e604da46be55cbeae10076780a027c7d 100644 (file)
@@ -131,6 +131,8 @@ class ApiAction extends Action
                                 'tags/timeline',
                                 'oembed/oembed',
                                 'groups/show',
+                                'groups/timeline',
+                                'groups/list_all',
                                 'groups/timeline');
 
         static $bareauth = array('statuses/user_timeline',
@@ -140,7 +142,8 @@ class ApiAction extends Action
                                  'statuses/mentions',
                                  'statuses/followers',
                                  'favorites/favorites',
-                                 'friendships/show');
+                                 'friendships/show',
+                                 'groups/list_groups');
 
         $fullname = "$this->api_action/$this->api_method";
 
index c6a5d0d523015e204a5306037450d4b005208ff9..f42906fd824367d2efc6d6a9cb350b3d16b50674 100644 (file)
@@ -103,18 +103,18 @@ class AttachmentAction extends Action
         $this->element('link',array('rel'=>'alternate',
             'type'=>'application/json+oembed',
             'href'=>common_local_url(
-                'api',
-                array('apiaction'=>'oembed','method'=>'oembed.json'),
-                array('url'=>
+                'oembed',
+                array(),
+                array('format'=>'json', 'url'=>
                     common_local_url('attachment',
                         array('attachment' => $this->attachment->id)))),
             'title'=>'oEmbed'),null);
         $this->element('link',array('rel'=>'alternate',
             'type'=>'text/xml+oembed',
             'href'=>common_local_url(
-                'api',
-                array('apiaction'=>'oembed','method'=>'oembed.xml'),
-                array('url'=>
+                'oembed',
+                array(),
+                array('format'=>'xml','url'=>
                     common_local_url('attachment',
                         array('attachment' => $this->attachment->id)))),
             'title'=>'oEmbed'),null);
index c2bb35a39580153a4b696015d104b42b4fb6b104..c45514ff607a39b5cd192bd787d905d9e6b1d0d5 100644 (file)
@@ -382,13 +382,7 @@ class AvatarsettingsAction extends AccountSettingsAction
     function showStylesheets()
     {
         parent::showStylesheets();
-        $jcropStyle =
-          common_path('theme/base/css/jquery.Jcrop.css?version='.LACONICA_VERSION);
-
-        $this->element('link', array('rel' => 'stylesheet',
-                                     'type' => 'text/css',
-                                     'href' => $jcropStyle,
-                                     'media' => 'screen, projection, tv'));
+        $this->cssLink('css/jquery.Jcrop.css','base','screen, projection, tv');
     }
 
     /**
@@ -402,13 +396,8 @@ class AvatarsettingsAction extends AccountSettingsAction
         parent::showScripts();
 
         if ($this->mode == 'crop') {
-            $jcropPack = common_path('js/jcrop/jquery.Jcrop.pack.js');
-            $jcropGo   = common_path('js/jcrop/jquery.Jcrop.go.js');
-
-            $this->element('script', array('type' => 'text/javascript',
-                                           'src' => $jcropPack));
-            $this->element('script', array('type' => 'text/javascript',
-                                           'src' => $jcropGo));
+            $this->script('js/jcrop/jquery.Jcrop.min.js');
+            $this->script('js/jcrop/jquery.Jcrop.go.js');
         }
     }
 }
index 725c1f1e3bb37a3f7da8e03d6f0beef07ba9a295..3c41a5c70d3d89dfbbed7ed8e0d6666d7f647cc0 100644 (file)
@@ -67,7 +67,11 @@ class ConfirmaddressAction extends Action
         parent::handle($args);
         if (!common_logged_in()) {
             common_set_returnto($this->selfUrl());
-            common_redirect(common_local_url('login'));
+            if (!common_config('site', 'openidonly')) {
+                common_redirect(common_local_url('login'));
+            } else {
+                common_redirect(common_local_url('openidlogin'));
+            }
             return;
         }
         $code = $this->trimmed('code');
index 6aa6f8b11f20d82d59b46a57bf8129d81e9b327e..aeeea2b63ce73e3f0aebb675e9f53c0e374079d1 100644 (file)
@@ -196,8 +196,8 @@ class EditgroupAction extends GroupDesignAction
         } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
             $this->showForm(_('Full name is too long (max 255 chars).'));
             return;
-        } else if (!is_null($description) && mb_strlen($description) > 140) {
-            $this->showForm(_('description is too long (max 140 chars).'));
+        } else if (User_group::descriptionTooLong($description)) {
+            $this->showForm(sprintf(_('description is too long (max %d chars).'), User_group::maxDescription()));
             return;
         } else if (!is_null($location) && mb_strlen($location) > 255) {
             $this->showForm(_('Location is too long (max 255 chars).'));
index 634388fdddbdf41acae10d679719d45148c5d869..cdd09282991fab1cabdd3bb2e22ee6018ce04fad 100644 (file)
@@ -122,7 +122,7 @@ class EmailsettingsAction extends AccountSettingsAction
         }
         $this->elementEnd('fieldset');
 
-       if ($user->email) {
+       if (common_config('emailpost', 'enabled') && $user->email) {
             $this->elementStart('fieldset', array('id' => 'settings_email_incoming'));
             $this->element('legend',_('Incoming email'));
             if ($user->incomingemail) {
@@ -173,11 +173,13 @@ class EmailsettingsAction extends AccountSettingsAction
                         _('Allow friends to nudge me and send me an email.'),
                         $user->emailnotifynudge);
         $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->checkbox('emailpost',
-                        _('I want to post notices by email.'),
-                        $user->emailpost);
-        $this->elementEnd('li');
+        if (common_config('emailpost', 'enabled')) {
+            $this->elementStart('li');
+            $this->checkbox('emailpost',
+                            _('I want to post notices by email.'),
+                            $user->emailpost);
+            $this->elementEnd('li');
+        }
         $this->elementStart('li');
         $this->checkbox('emailmicroid',
                         _('Publish a MicroID for my email address.'),
index 156c7a70094a68513f22ec298d568146a0b0324e..a3d1a5e206ed6ec4abc0ab35db97f772329ff320 100644 (file)
@@ -153,7 +153,8 @@ class FavoritedAction extends Action
             $message .= _('Be the first to add a notice to your favorites by clicking the fave button next to any notice you like.');
         }
         else {
-            $message .= _('Why not [register an account](%%action.register%%) and be the first to add a notice to your favorites!');
+            $message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and be the first to add a notice to your favorites!'),
+                                (!common_config('site','openidonly')) ? 'register' : 'openidlogin');
         }
 
         $this->elementStart('div', 'guide');
index c439a9a62b5cc34d7b37ec7533f230b31e7b00e7..5dc09e5e8a9714e43a24d45e80209162c7add23e 100644 (file)
@@ -111,8 +111,8 @@ class FavoritesrssAction extends Rss10Action
                    'link' => common_local_url('showfavorites',
                                         array('nickname' =>
                                         $user->nickname)),
-                   'description' => sprintf(_('Feed of favorite notices of %s'), 
-                                        $user->nickname));
+                   'description' => sprintf(_('Updates favored by %1$s on %2$s!'),
+                                        $user->nickname, common_config('site', 'name')));
         return $c;
     }
 
index 5c764aeb0d320605fcd95b5865af89b1965c0ece..da563cb29014d020e546441177b61fae10890986 100644 (file)
@@ -1,5 +1,16 @@
 <?php
-/*
+/**
+ * Handler for remote subscription finish callback
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @author   Robin Millette <millette@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://laconi.ca/
+ *
  * Laconica - a distributed open-source microblogging tool
  * Copyright (C) 2008, 2009, Control Yourself, Inc.
  *
  *
  * 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('LACONICA')) { exit(1); }
+if (!defined('LACONICA')) {
+    exit(1);
+}
 
-require_once(INSTALLDIR.'/lib/omb.php');
+require_once INSTALLDIR.'/extlib/libomb/service_consumer.php';
+require_once INSTALLDIR.'/lib/omb.php';
 
+/**
+ * Handler for remote subscription finish callback
+ *
+ * When a remote user subscribes a local user, a redirect to this action is
+ * issued after the remote user authorized his service to subscribe.
+ *
+ * @category Action
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @author   Robin Millette <millette@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://laconi.ca/
+ */
 class FinishremotesubscribeAction extends Action
 {
 
+    /**
+     * Class handler.
+     *
+     * @param array $args query arguments
+     *
+     * @return nothing
+     *
+     **/
     function handle($args)
     {
-
         parent::handle($args);
 
-        if (common_logged_in()) {
-            $this->clientError(_('You can use the local subscription!'));
-            return;
-        }
-
-        $omb = $_SESSION['oauth_authorization_request'];
+        /* Restore session data. RemotesubscribeAction should have stored
+           this entry. */
+        $service  = unserialize($_SESSION['oauth_authorization_request']);
 
-        if (!$omb) {
+        if (!$service) {
             $this->clientError(_('Not expecting this response!'));
             return;
         }
 
-        common_debug('stored request: '.print_r($omb,true), __FILE__);
-
-        common_remove_magic_from_request();
-        $req = OAuthRequest::from_request('POST', common_local_url('finishuserauthorization'));
-
-        $token = $req->get_parameter('oauth_token');
-
-        # I think this is the success metric
-
-        if ($token != $omb['token']) {
-            $this->clientError(_('Not authorized.'));
-            return;
-        }
-
-        $version = $req->get_parameter('omb_version');
-
-        if ($version != OMB_VERSION_01) {
-            $this->clientError(_('Unknown version of OMB protocol.'));
-            return;
-        }
-
-        $nickname = $req->get_parameter('omb_listener_nickname');
-
-        if (!$nickname) {
-            $this->clientError(_('No nickname provided by remote server.'));
-            return;
-        }
-
-        $profile_url = $req->get_parameter('omb_listener_profile');
-
-        if (!$profile_url) {
-            $this->clientError(_('No profile URL returned by server.'));
-            return;
-        }
-
-        if (!Validate::uri($profile_url, array('allowed_schemes' => array('http', 'https')))) {
-            $this->clientError(_('Invalid profile URL returned by server.'));
-            return;
-        }
-
-        if ($profile_url == common_local_url('showstream', array('nickname' => $nickname))) {
-            $this->clientError(_('You can use the local subscription!'));
-            return;
-        }
-
-        common_debug('listenee: "'.$omb['listenee'].'"', __FILE__);
+        common_debug('stored request: '. print_r($service, true), __FILE__);
 
-        $user = User::staticGet('nickname', $omb['listenee']);
+        /* Create user objects for both users. Do it early for request
+           validation. */
+        $user = User::staticGet('uri', $service->getListeneeURI());
 
         if (!$user) {
-            $this->clientError(_('User being listened to doesn\'t exist.'));
+            $this->clientError(_('User being listened to does not exist.'));
             return;
         }
 
-        $other = User::staticGet('uri', $omb['listener']);
+        $other = User::staticGet('uri', $service->getListenerURI());
 
         if ($other) {
             $this->clientError(_('You can use the local subscription!'));
             return;
         }
 
-        $fullname = $req->get_parameter('omb_listener_fullname');
-        $homepage = $req->get_parameter('omb_listener_homepage');
-        $bio = $req->get_parameter('omb_listener_bio');
-        $location = $req->get_parameter('omb_listener_location');
-        $avatar_url = $req->get_parameter('omb_listener_avatar');
+        $remote = Remote_profile::staticGet('uri', $service->getListenerURI());
 
-        list($newtok, $newsecret) = $this->access_token($omb);
+        $profile = Profile::staticGet($remote->id);
 
-        if (!$newtok || !$newsecret) {
-            $this->clientError(_('Couldn\'t convert request tokens to access tokens.'));
+        if ($user->hasBlocked($profile)) {
+            $this->clientError(_('That user has blocked you from subscribing.'));
             return;
         }
 
-        # XXX: possible attack point; subscribe and return someone else's profile URI
-
-        $remote = Remote_profile::staticGet('uri', $omb['listener']);
-
-        if ($remote) {
-            $exists = true;
-            $profile = Profile::staticGet($remote->id);
-            $orig_remote = clone($remote);
-            $orig_profile = clone($profile);
-            # XXX: compare current postNotice and updateProfile URLs to the ones
-            # stored in the DB to avoid (possibly...) above attack
-        } else {
-            $exists = false;
-            $remote = new Remote_profile();
-            $remote->uri = $omb['listener'];
-            $profile = new Profile();
-        }
-
-        $profile->nickname = $nickname;
-        $profile->profileurl = $profile_url;
-
-        if (!is_null($fullname)) {
-            $profile->fullname = $fullname;
-        }
-        if (!is_null($homepage)) {
-            $profile->homepage = $homepage;
-        }
-        if (!is_null($bio)) {
-            $profile->bio = $bio;
-        }
-        if (!is_null($location)) {
-            $profile->location = $location;
-        }
-
-        if ($exists) {
-            $profile->update($orig_profile);
-        } else {
-            $profile->created = DB_DataObject_Cast::dateTime(); # current time
-            $id = $profile->insert();
-            if (!$id) {
-                $this->serverError(_('Error inserting new profile'));
-                return;
-            }
-            $remote->id = $id;
-        }
-
-        if ($avatar_url) {
-            if (!$this->add_avatar($profile, $avatar_url)) {
-                $this->serverError(_('Error inserting avatar'));
+        /* Perform the handling itself via libomb. */
+        try {
+            $service->finishAuthorization();
+        } catch (OAuthException $e) {
+            if ($e->getMessage() == 'The authorized token does not equal the ' .
+                                    'submitted token.') {
+                $this->clientError(_('You are not authorized.'));
                 return;
-            }
-        }
-
-        $remote->postnoticeurl = $omb['post_notice_url'];
-        $remote->updateprofileurl = $omb['update_profile_url'];
-
-        if ($exists) {
-            if (!$remote->update($orig_remote)) {
-                $this->serverError(_('Error updating remote profile'));
-                return;
-            }
-        } else {
-            $remote->created = DB_DataObject_Cast::dateTime(); # current time
-            if (!$remote->insert()) {
-                $this->serverError(_('Error inserting remote profile'));
+            } else {
+                $this->clientError(_('Could not convert request token to ' .
+                                     'access token.'));
                 return;
             }
-        }
-
-        if ($user->hasBlocked($profile)) {
-            $this->clientError(_('That user has blocked you from subscribing.'));
+        } catch (OMB_RemoteServiceException $e) {
+            $this->clientError(_('Remote service uses unknown version of ' .
+                                 'OMB protocol.'));
+            return;
+        } catch (Exception $e) {
+            common_debug('Got exception ' . print_r($e, true), __FILE__);
+            $this->clientError($e->getMessage());
             return;
         }
 
-        $sub = new Subscription();
+        /* The service URLs are not accessible from datastore, so setting them
+           after insertion of the profile. */
+        $orig_remote = clone($remote);
 
-        $sub->subscriber = $remote->id;
-        $sub->subscribed = $user->id;
+        $remote->postnoticeurl    =
+                            $service->getServiceURI(OMB_ENDPOINT_POSTNOTICE);
+        $remote->updateprofileurl =
+                            $service->getServiceURI(OMB_ENDPOINT_UPDATEPROFILE);
 
-        $sub_exists = false;
-
-        if ($sub->find(true)) {
-            $sub_exists = true;
-            $orig_sub = clone($sub);
-        } else {
-            $sub_exists = false;
-            $sub->created = DB_DataObject_Cast::dateTime(); # current time
-        }
-
-        $sub->token = $newtok;
-        $sub->secret = $newsecret;
-
-        if ($sub_exists) {
-            $result = $sub->update($orig_sub);
-        } else {
-            $result = $sub->insert();
-        }
-
-        if (!$result) {
-            common_log_db_error($sub, ($sub_exists) ? 'UPDATE' : 'INSERT', __FILE__);
-            $this->clientError(_('Couldn\'t insert new subscription.'));
-            return;
+        if (!$remote->update($orig_remote)) {
+                $this->serverError(_('Error updating remote profile'));
+                return;
         }
 
-        # Notify user, if necessary
-
-        mail_subscribe_notify_profile($user, $profile);
-
-        # Clear the data
+        /* Clear the session data. */
         unset($_SESSION['oauth_authorization_request']);
 
-        # If we show subscriptions in reverse chron order, this should
-        # show up close to the top of the page
-
+        /* If we show subscriptions in reverse chronological order, the new one
+           should show up close to the top of the page. */
         common_redirect(common_local_url('subscribers', array('nickname' =>
                                                              $user->nickname)),
                         303);
     }
-
-    function add_avatar($profile, $url)
-    {
-        $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
-        copy($url, $temp_filename);
-        $imagefile = new ImageFile($profile->id, $temp_filename);
-        $filename = Avatar::filename($profile->id,
-                                     image_type_to_extension($imagefile->type),
-                                     null,
-                                     common_timestamp());
-        rename($temp_filename, Avatar::path($filename));
-        return $profile->setOriginal($filename);
-    }
-
-    function access_token($omb)
-    {
-
-        common_debug('starting request for access token', __FILE__);
-
-        $con = omb_oauth_consumer();
-        $tok = new OAuthToken($omb['token'], $omb['secret']);
-
-        common_debug('using request token "'.$tok.'"', __FILE__);
-
-        $url = $omb['access_token_url'];
-
-        common_debug('using access token url "'.$url.'"', __FILE__);
-
-        # XXX: Is this the right thing to do? Strip off GET params and make them
-        # POST params? Seems wrong to me.
-
-        $parsed = parse_url($url);
-        $params = array();
-        parse_str($parsed['query'], $params);
-
-        $req = OAuthRequest::from_consumer_and_token($con, $tok, "POST", $url, $params);
-
-        $req->set_parameter('omb_version', OMB_VERSION_01);
-
-        # XXX: test to see if endpoint accepts this signature method
-
-        $req->sign_request(omb_hmac_sha1(), $con, $tok);
-
-        # We re-use this tool's fetcher, since it's pretty good
-
-        common_debug('posting to access token url "'.$req->get_normalized_http_url().'"', __FILE__);
-        common_debug('posting request data "'.$req->to_postdata().'"', __FILE__);
-
-        $fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
-        $result = $fetcher->post($req->get_normalized_http_url(),
-                                 $req->to_postdata(),
-                                 array('User-Agent: Laconica/' . LACONICA_VERSION));
-
-        common_debug('got result: "'.print_r($result,true).'"', __FILE__);
-
-        if ($result->status != 200) {
-            return null;
-        }
-
-        parse_str($result->body, $return);
-
-        return array($return['oauth_token'], $return['oauth_token_secret']);
-    }
 }
index 8f6158dacaafbbd9626b525284745d3451c3f548..87c68e2a211ae43d3ba4c06316167926cd620aab 100644 (file)
@@ -428,13 +428,7 @@ class GrouplogoAction extends GroupDesignAction
     function showStylesheets()
     {
         parent::showStylesheets();
-        $jcropStyle =
-          common_path('theme/base/css/jquery.Jcrop.css?version='.LACONICA_VERSION);
-
-        $this->element('link', array('rel' => 'stylesheet',
-                                     'type' => 'text/css',
-                                     'href' => $jcropStyle,
-                                     'media' => 'screen, projection, tv'));
+        $this->cssLink('css/jquery.Jcrop.css','base','screen, projection, tv');
     }
 
     /**
@@ -448,13 +442,8 @@ class GrouplogoAction extends GroupDesignAction
         parent::showScripts();
 
         if ($this->mode == 'crop') {
-            $jcropPack = common_path('js/jcrop/jquery.Jcrop.pack.js');
-            $jcropGo   = common_path('js/jcrop/jquery.Jcrop.go.js');
-
-            $this->element('script', array('type' => 'text/javascript',
-                                           'src' => $jcropPack));
-            $this->element('script', array('type' => 'text/javascript',
-                                           'src' => $jcropGo));
+            $this->script('js/jcrop/jquery.Jcrop.min.js');
+            $this->script('js/jcrop/jquery.Jcrop.go.js');
         }
     }
 
index 2bdcaafb27a56e17ea1bd556741e34ba70880599..e1e2d201854ff166229a172118ccd788323f2fc8 100644 (file)
@@ -132,9 +132,10 @@ class groupRssAction extends Rss10Action
         $c = array('url' => common_local_url('grouprss',
                                              array('nickname' =>
                                                    $group->nickname)),
-                   'title' => $group->nickname,
+                   'title' => sprintf(_('%s timeline'), $group->nickname),
                    'link' => common_local_url('showgroup', array('nickname' => $group->nickname)),
-                   'description' => sprintf(_('Microblog by %s group'), $group->nickname));
+                   'description' => sprintf(_('Updates from members of %1$s on %2$s!'),
+                                            $group->nickname, common_config('site', 'name')));
         return $c;
     }
 
index c50466ce62a4f65f77ec730b58b2784756f66613..7437166e6ae2a70698dbe015301ea9317ba6f070 100644 (file)
@@ -82,7 +82,8 @@ class GroupsearchAction extends SearchAction
                 $message = _('If you can\'t find the group you\'re looking for, you can [create it](%%action.newgroup%%) yourself.');
             }
             else {
-                $message = _('Why not [register an account](%%action.register%%) and [create the group](%%action.newgroup%%) yourself!');
+                $message = sprintf(_('Why not [register an account](%%%%action.%s%%%%) and [create the group](%%%%action.newgroup%%%%) yourself!'),
+                                   (!common_config('site','openidonly')) ? 'register' : 'openidlogin');
             }
             $this->elementStart('div', 'guide');
             $this->raw(common_markup_to_html($message));
index e0f5ede3a7520c1069a9906c229c12416730603c..70a6f37d4f78e0fc4f207b15eb7617c3323fb2ad 100644 (file)
@@ -84,6 +84,12 @@ class ImsettingsAction extends ConnectSettingsAction
 
     function showContent()
     {
+        if (!common_config('xmpp', 'enabled')) {
+            $this->element('div', array('class' => 'error'),
+                           _('IM is not available.'));
+            return;
+        }
+
         $user = common_current_user();
         $this->elementStart('form', array('method' => 'post',
                                           'id' => 'form_settings_im',
index 26c951ed2233d89da409a5c4e7f90d27ad213614..bdc0d34cb3cf65522ed19f129845cedcde57f2f6 100644 (file)
@@ -235,7 +235,7 @@ class InviteAction extends CurrentUserDesignAction
                         common_root_url(),
                         $personal,
                         common_local_url('showstream', array('nickname' => $user->nickname)),
-                        common_local_url('register', array('code' => $invite->code)));
+                        common_local_url((!common_config('site', 'openidonly')) ? 'register' : 'openidlogin', array('code' => $invite->code)));
 
         mail_send($recipients, $headers, $body);
     }
index f5a658bf503b2bf3f49a46de4b6bed6a6363a053..e09fdc76b49d4f82b48de852c57de65a2cd9d4ca 100644 (file)
@@ -247,7 +247,7 @@ class LoginAction extends Action
             return _('For security reasons, please re-enter your ' .
                      'user name and password ' .
                      'before changing your settings.');
-        } else {
+        } else if (common_config('openid', 'enabled')) {
             return _('Login with your username and password. ' .
                      'Don\'t have a username yet? ' .
                      '[Register](%%action.register%%) a new account.');
index 0289e77c2511a7aebd096a4d78e66d54ac72dbe3..71647d83481adcd3a4a529224f10fd0026cac97d 100644 (file)
@@ -146,8 +146,8 @@ class NewgroupAction extends Action
         } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
             $this->showForm(_('Full name is too long (max 255 chars).'));
             return;
-        } else if (!is_null($description) && mb_strlen($description) > 140) {
-            $this->showForm(_('description is too long (max 140 chars).'));
+        } else if (User_group::descriptionTooLong($description)) {
+            $this->showForm(sprintf(_('description is too long (max %d chars).'), User_group::maxDescription()));
             return;
         } else if (!is_null($location) && mb_strlen($location) > 255) {
             $this->showForm(_('Location is too long (max 255 chars).'));
index 52d4899ba2d453d8012465eaa60def69458df4a2..cd26e1640b5b5f64219fd4af82146ad29e06bfbf 100644 (file)
@@ -144,9 +144,10 @@ class NewmessageAction extends Action
         } else {
             $content_shortened = common_shorten_links($this->content);
 
-            if (mb_strlen($content_shortened) > 140) {
-                $this->showForm(_('That\'s too long. ' .
-                    'Max message size is 140 chars.'));
+            if (Message::contentTooLong($content_shortened)) {
+                $this->showForm(sprintf(_('That\'s too long. ' .
+                                          'Max message size is %d chars.'),
+                                        Message::maxContent()));
                 return;
             }
         }
index e254eac4999852389b8623b36db62d10826f5bb3..049d7c322982aadfa3cff3f8d51eeb16d78b1e5c 100644 (file)
@@ -91,8 +91,8 @@ class NewnoticeAction extends Action
             // is losts when size is exceeded
             if (empty($_POST) && $_SERVER['CONTENT_LENGTH']) {
                 $this->clientError(sprintf(_('The server was unable to handle ' .
-                    'that much POST data (%s bytes) due to its current configuration.'),
-                    $_SERVER['CONTENT_LENGTH']));
+                                             'that much POST data (%s bytes) due to its current configuration.'),
+                                           $_SERVER['CONTENT_LENGTH']));
             }
             parent::handle($args);
 
@@ -130,7 +130,7 @@ class NewnoticeAction extends Action
             $hint = '';
         }
         $this->clientError(sprintf(
-            _('%s is not a supported filetype on this server.'), $filetype) . $hint);
+                                   _('%s is not a supported filetype on this server.'), $filetype) . $hint);
     }
 
     function isRespectsQuota($user) {
@@ -162,9 +162,10 @@ class NewnoticeAction extends Action
             $this->clientError(_('No content!'));
         } else {
             $content_shortened = common_shorten_links($content);
-            if (mb_strlen($content_shortened) > 140) {
-                $this->clientError(_('That\'s too long. '.
-                                     'Max notice size is 140 chars.'));
+            if (Notice::contentTooLong($content_shortened)) {
+                $this->clientError(sprintf(_('That\'s too long. '.
+                                             'Max notice size is %d chars.'),
+                                           Notice::maxContent()));
             }
         }
 
@@ -190,37 +191,37 @@ class NewnoticeAction extends Action
 
         if (isset($_FILES['attach']['error'])) {
             switch ($_FILES['attach']['error']) {
-                case UPLOAD_ERR_NO_FILE:
-                    // no file uploaded, nothing to do
-                    break;
+             case UPLOAD_ERR_NO_FILE:
+                // no file uploaded, nothing to do
+                break;
 
-                case UPLOAD_ERR_OK:
-                    $mimetype = $this->getUploadedFileType();
-                    if (!$this->isRespectsQuota($user)) {
-                        die('clientError() should trigger an exception before reaching here.');
-                    }
-                    break;
+             case UPLOAD_ERR_OK:
+                $mimetype = $this->getUploadedFileType();
+                if (!$this->isRespectsQuota($user)) {
+                    die('clientError() should trigger an exception before reaching here.');
+                }
+                break;
 
-                case UPLOAD_ERR_INI_SIZE:
-                    $this->clientError(_('The uploaded file exceeds the upload_max_filesize directive in php.ini.'));
+             case UPLOAD_ERR_INI_SIZE:
+                $this->clientError(_('The uploaded file exceeds the upload_max_filesize directive in php.ini.'));
 
-                case UPLOAD_ERR_FORM_SIZE:
-                    $this->clientError(_('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.'));
+             case UPLOAD_ERR_FORM_SIZE:
+                $this->clientError(_('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.'));
 
-                case UPLOAD_ERR_PARTIAL:
-                    $this->clientError(_('The uploaded file was only partially uploaded.'));
+             case UPLOAD_ERR_PARTIAL:
+                $this->clientError(_('The uploaded file was only partially uploaded.'));
 
-                case  UPLOAD_ERR_NO_TMP_DIR:
-                    $this->clientError(_('Missing a temporary folder.'));
+             case  UPLOAD_ERR_NO_TMP_DIR:
+                $this->clientError(_('Missing a temporary folder.'));
 
-                case UPLOAD_ERR_CANT_WRITE:
-                    $this->clientError(_('Failed to write file to disk.'));
+             case UPLOAD_ERR_CANT_WRITE:
+                $this->clientError(_('Failed to write file to disk.'));
 
-                case UPLOAD_ERR_EXTENSION:
-                    $this->clientError(_('File upload stopped by extension.'));
+             case UPLOAD_ERR_EXTENSION:
+                $this->clientError(_('File upload stopped by extension.'));
 
-                default:
-                    die('Should never reach here.');
+             default:
+                die('Should never reach here.');
             }
         }
 
@@ -233,7 +234,7 @@ class NewnoticeAction extends Action
             $fileRecord = $this->storeFile($filename, $mimetype);
 
             $fileurl = common_local_url('attachment',
-                array('attachment' => $fileRecord->id));
+                                        array('attachment' => $fileRecord->id));
 
             // not sure this is necessary -- Zach
             $this->maybeAddRedir($fileRecord->id, $fileurl);
@@ -241,9 +242,10 @@ class NewnoticeAction extends Action
             $short_fileurl = common_shorten_url($fileurl);
             $content_shortened .= ' ' . $short_fileurl;
 
-            if (mb_strlen($content_shortened) > 140) {
+            if (Notice::contentTooLong($content_shortened)) {
                 $this->deleteFile($filename);
-                $this->clientError(_('Max notice size is 140 chars, including attachment URL.'));
+                $this->clientError(sprintf(_('Max notice size is %d chars, including attachment URL.'),
+                                           Notice::maxContent()));
             }
 
             // Also, not sure this is necessary -- Zach
@@ -367,7 +369,7 @@ class NewnoticeAction extends Action
         File_to_post::processNew($filerec->id, $notice->id);
 
         $this->maybeAddRedir($filerec->id,
-            common_local_url('file', array('notice' => $notice->id)));
+                             common_local_url('file', array('notice' => $notice->id)));
     }
 
     /**
index 49b473d9e9cd542c03be950ff5fc93a60afb111b..90b3309cf6df4047f6ae07d612551d014a66ddb3 100644 (file)
@@ -121,7 +121,9 @@ class NoticesearchAction extends SearchAction
                 $message = sprintf(_('Be the first to [post on this topic](%%%%action.newnotice%%%%?status_textarea=%s)!'), urlencode($q));
             }
             else {
-                $message = sprintf(_('Why not [register an account](%%%%action.register%%%%) and be the first to  [post on this topic](%%%%action.newnotice%%%%?status_textarea=%s)!'), urlencode($q));
+                $message = sprintf(_('Why not [register an account](%%%%action.%s%%%%) and be the first to [post on this topic](%%%%action.newnotice%%%%?status_textarea=%s)!'),
+                                   (!common_config('site','openidonly')) ? 'register' : 'openidlogin',
+                                   urlencode($q));
             }
 
             $this->elementStart('div', 'guide');
index 2a4b2060d3fa3f2d3781d429f27cbd5a372ce9a3..045531c5ac722a2031415679a33ac49fc20cc406 100644 (file)
@@ -86,9 +86,10 @@ class NoticesearchrssAction extends Rss10Action
     {
         $q = $this->trimmed('q');
         $c = array('url' => common_local_url('noticesearchrss', array('q' => $q)),
-                   'title' => common_config('site', 'name') . sprintf(_(' Search Stream for "%s"'), $q),
+                   'title' => sprintf(_('Updates with "%s"'), $q),
                    'link' => common_local_url('noticesearch', array('q' => $q)),
-                   'description' => sprintf(_('All updates matching search term "%s"'), $q));
+                   'description' => sprintf(_('Updates matching search term "%1$s" on %2$s!'),
+                                            $q, common_config('site', 'name')));
         return $c;
     }
 
diff --git a/actions/oembed.php b/actions/oembed.php
new file mode 100644 (file)
index 0000000..3e46a72
--- /dev/null
@@ -0,0 +1,206 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Laconica-only extensions to the Twitter-like API
+ *
+ * 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  Twitter
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2008 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/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+/**
+ * Oembed provider implementation
+ *
+ * This class handles all /main/oembed(.xml|.json)/ requests.
+ *
+ * @category  oEmbed
+ * @package   Laconica
+ * @author    Craig Andrews <candrews@integralblue.com>
+ * @copyright 2008 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/
+ */
+
+class OembedAction extends Action
+{
+
+    function handle($args)
+    {
+        common_debug("in oembed api action");
+
+        $url = $args['url'];
+        if( substr(strtolower($url),0,strlen(common_root_url())) == strtolower(common_root_url()) ){
+            $path = substr($url,strlen(common_root_url()));
+
+            $r = Router::get();
+
+            $proxy_args = $r->map($path);
+
+            if (!$proxy_args) {
+                $this->serverError(_("$path not found"), 404);
+            }
+            $oembed=array();
+            $oembed['version']='1.0';
+            $oembed['provider_name']=common_config('site', 'name');
+            $oembed['provider_url']=common_root_url();
+            switch($proxy_args['action']){
+                case 'shownotice':
+                    $oembed['type']='link';
+                    $id = $proxy_args['notice'];
+                    $notice = Notice::staticGet($id);
+                    if(empty($notice)){
+                        $this->serverError(_("notice $id not found"), 404);
+                    }
+                    $profile = $notice->getProfile();
+                    if (empty($profile)) {
+                        $this->serverError(_('Notice has no profile'), 500);
+                    }
+                    if (!empty($profile->fullname)) {
+                        $authorname = $profile->fullname . ' (' . $profile->nickname . ')';
+                    } else {
+                        $authorname = $profile->nickname;
+                    }
+                    $oembed['title'] = sprintf(_('%1$s\'s status on %2$s'),
+                        $authorname,
+                        common_exact_date($notice->created));
+                    $oembed['author_name']=$authorname;
+                    $oembed['author_url']=$profile->profileurl;
+                    $oembed['url']=($notice->url?$notice->url:$notice->uri);
+                    $oembed['html']=$notice->rendered;
+                    break;
+                case 'attachment':
+                    $id = $proxy_args['attachment'];
+                    $attachment = File::staticGet($id);
+                    if(empty($attachment)){
+                        $this->serverError(_("attachment $id not found"), 404);
+                    }
+                    if(empty($attachment->filename) && $file_oembed = File_oembed::staticGet('file_id', $attachment->id)){
+                        // Proxy the existing oembed information
+                        $oembed['type']=$file_oembed->type;
+                        $oembed['provider']=$file_oembed->provider;
+                        $oembed['provider_url']=$file_oembed->provider_url;
+                        $oembed['width']=$file_oembed->width;
+                        $oembed['height']=$file_oembed->height;
+                        $oembed['html']=$file_oembed->html;
+                        $oembed['title']=$file_oembed->title;
+                        $oembed['author_name']=$file_oembed->author_name;
+                        $oembed['author_url']=$file_oembed->author_url;
+                        $oembed['url']=$file_oembed->url;
+                    }else if(substr($attachment->mimetype,0,strlen('image/'))=='image/'){
+                        $oembed['type']='photo';
+                        //TODO set width and height
+                        //$oembed['width']=
+                        //$oembed['height']=
+                        $oembed['url']=$attachment->url;
+                    }else{
+                        $oembed['type']='link';
+                        $oembed['url']=common_local_url('attachment',
+                            array('attachment' => $attachment->id));
+                    }
+                    if($attachment->title) $oembed['title']=$attachment->title;
+                    break;
+                default:
+                    $this->serverError(_("$path not supported for oembed requests"), 501);
+            }
+            switch($args['format']){
+                case 'xml':
+                    $this->init_document('xml');
+                    $this->elementStart('oembed');
+                    $this->element('version',null,$oembed['version']);
+                    $this->element('type',null,$oembed['type']);
+                    if($oembed['provider_name']) $this->element('provider_name',null,$oembed['provider_name']);
+                    if($oembed['provider_url']) $this->element('provider_url',null,$oembed['provider_url']);
+                    if($oembed['title']) $this->element('title',null,$oembed['title']);
+                    if($oembed['author_name']) $this->element('author_name',null,$oembed['author_name']);
+                    if($oembed['author_url']) $this->element('author_url',null,$oembed['author_url']);
+                    if($oembed['url']) $this->element('url',null,$oembed['url']);
+                    if($oembed['html']) $this->element('html',null,$oembed['html']);
+                    if($oembed['width']) $this->element('width',null,$oembed['width']);
+                    if($oembed['height']) $this->element('height',null,$oembed['height']);
+                    if($oembed['cache_age']) $this->element('cache_age',null,$oembed['cache_age']);
+                    if($oembed['thumbnail_url']) $this->element('thumbnail_url',null,$oembed['thumbnail_url']);
+                    if($oembed['thumbnail_width']) $this->element('thumbnail_width',null,$oembed['thumbnail_width']);
+                    if($oembed['thumbnail_height']) $this->element('thumbnail_height',null,$oembed['thumbnail_height']);
+
+                    $this->elementEnd('oembed');
+                    $this->end_document('xml');
+                    break;
+                case 'json': case '':
+                    $this->init_document('json');
+                    print(json_encode($oembed));
+                    $this->end_document('json');
+                    break;
+                default:
+                    $this->serverError(_('content type ' . $apidata['content-type'] . ' not supported'), 501);
+            }
+        }else{
+            $this->serverError(_('Only ' . common_root_url() . ' urls over plain http please'), 404);
+        }
+    }
+
+    function init_document($type)
+    {
+        switch ($type) {
+        case 'xml':
+            header('Content-Type: application/xml; charset=utf-8');
+            $this->startXML();
+            break;
+        case 'json':
+            header('Content-Type: application/json; charset=utf-8');
+
+            // Check for JSONP callback
+            $callback = $this->arg('callback');
+            if ($callback) {
+                print $callback . '(';
+            }
+            break;
+        default:
+            $this->serverError(_('Not a supported data format.'), 501);
+            break;
+        }
+    }
+
+    function end_document($type='xml')
+    {
+        switch ($type) {
+        case 'xml':
+            $this->endXML();
+            break;
+        case 'json':
+            // Check for JSONP callback
+            $callback = $this->arg('callback');
+            if ($callback) {
+                print ')';
+            }
+            break;
+        default:
+            $this->serverError(_('Not a supported data format.'), 501);
+            break;
+        }
+        return;
+    }
+
+}
index 4fe95c93b5cf318443d1c663638e1bb525021c61..6044568f11042a941bad47be331287a7839bb18b 100644 (file)
@@ -66,7 +66,7 @@ class OpensearchAction extends Action
             $type       = 'noticesearch';
             $short_name = _('Notice Search');
         }
-        header('Content-Type: text/html');
+        header('Content-Type: application/opensearchdescription+xml');
         $this->startXML();
         $this->elementStart('OpenSearchDescription', array('xmlns' => 'http://a9.com/-/spec/opensearch/1.1/'));
         $short_name =  common_config('site', 'name').' '.$short_name;
index eb2d63b61cf08119ba2911b130a6aaf50ee508fd..14152a83d64d36387445e2fdf3cf348ca33a179a 100644 (file)
@@ -1,5 +1,16 @@
 <?php
-/*
+/**
+ * Handle postnotice action
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @author   Robin Millette <millette@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://laconi.ca/
+ *
  * Laconica - a distributed open-source microblogging tool
  * Copyright (C) 2008, 2009, Control Yourself, Inc.
  *
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-if (!defined('LACONICA')) { exit(1); }
+if (!defined('LACONICA')) {
+    exit(1);
+}
 
-require_once(INSTALLDIR.'/lib/omb.php');
+require_once INSTALLDIR.'/lib/omb.php';
+require_once INSTALLDIR.'/extlib/libomb/service_provider.php';
 
+/**
+ * Handler for postnotice action
+ *
+ * @category Action
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @author   Robin Millette <millette@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://laconi.ca/
+ */
 class PostnoticeAction extends Action
 {
+    /**
+     * For initializing members of the class.
+     *
+     * @param array $argarray misc. arguments
+     *
+     * @return boolean true
+     */
+    function prepare($argarray)
+    {
+        parent::prepare($argarray);
+        try {
+            $this->checkNotice();
+        } catch (Exception $e) {
+            $this->clientError($e->getMessage());
+            return false;
+        }
+        return true;
+    }
+
     function handle($args)
     {
         parent::handle($args);
         try {
-            common_remove_magic_from_request();
-            $req = OAuthRequest::from_request('POST', common_local_url('postnotice'));
-            # Note: server-to-server function!
-            $server = omb_oauth_server();
-            list($consumer, $token) = $server->verify_request($req);
-            if ($this->save_notice($req, $consumer, $token)) {
-                print "omb_version=".OMB_VERSION_01;
-            }
-        } catch (OAuthException $e) {
+            $srv = new OMB_Service_Provider(null, omb_oauth_datastore(),
+                                            omb_oauth_server());
+            $srv->handlePostNotice();
+        } catch (Exception $e) {
             $this->serverError($e->getMessage());
             return;
         }
     }
 
-    function save_notice(&$req, &$consumer, &$token)
+    function checkNotice()
     {
-        $version = $req->get_parameter('omb_version');
-        if ($version != OMB_VERSION_01) {
-            $this->clientError(_('Unsupported OMB version'), 400);
-            return false;
-        }
-        # First, check to see
-        $listenee =  $req->get_parameter('omb_listenee');
-        $remote_profile = Remote_profile::staticGet('uri', $listenee);
-        if (!$remote_profile) {
-            $this->clientError(_('Profile unknown'), 403);
-            return false;
-        }
-        $sub = Subscription::staticGet('token', $token->key);
-        if (!$sub) {
-            $this->clientError(_('No such subscription'), 403);
-            return false;
-        }
-        $content = $req->get_parameter('omb_notice_content');
-        $content_shortened = common_shorten_links($content);
-        if (mb_strlen($content_shortened) > 140) {
+        $content = common_shorten_links($_POST['omb_notice_content']);
+        if (Notice::contentTooLong($content)) {
             $this->clientError(_('Invalid notice content'), 400);
             return false;
         }
-        $notice_uri = $req->get_parameter('omb_notice');
-        if (!Validate::uri($notice_uri) &&
-            !common_valid_tag($notice_uri)) {
-            $this->clientError(_('Invalid notice uri'), 400);
-            return false;
-        }
-        $notice_url = $req->get_parameter('omb_notice_url');
-        if ($notice_url && !common_valid_http_url($notice_url)) {
-            $this->clientError(_('Invalid notice url'), 400);
-            return false;
-        }
-        $notice = Notice::staticGet('uri', $notice_uri);
-        if (!$notice) {
-            $notice = Notice::saveNew($remote_profile->id, $content, 'omb', false, null, $notice_uri);
-            if (is_string($notice)) {
-                common_server_serror($notice, 500);
-                return false;
-            }
-            common_broadcast_notice($notice, true);
+        $license      = $_POST['omb_notice_license'];
+        $site_license = common_config('license', 'url');
+        if ($license && !common_compatible_license($license, $site_license)) {
+            throw new Exception(sprintf(_('Notice license ‘%s’ is not ' .
+                                          'compatible with site license ‘%s’.'),
+                                        $license, $site_license));
         }
-        return true;
     }
 }
+?>
index fb847680b9c158e85dc319d89029933edddabc2d..f429a2e516479fe4bf90a9c8e74f6965660d0c88 100644 (file)
@@ -109,9 +109,16 @@ class ProfilesettingsAction extends AccountSettingsAction
                          _('URL of your homepage, blog, or profile on another site'));
             $this->elementEnd('li');
             $this->elementStart('li');
+            $maxBio = Profile::maxBio();
+            if ($maxBio > 0) {
+                $bioInstr = sprintf(_('Describe yourself and your interests in %d chars'),
+                                    $maxBio);
+            } else {
+                $bioInstr = _('Describe yourself and your interests');
+            }
             $this->textarea('bio', _('Bio'),
                             ($this->arg('bio')) ? $this->arg('bio') : $profile->bio,
-                            _('Describe yourself and your interests in 140 chars'));
+                            $bioInstr);
             $this->elementEnd('li');
             $this->elementStart('li');
             $this->input('location', _('Location'),
@@ -189,7 +196,7 @@ class ProfilesettingsAction extends AccountSettingsAction
             // Some validation
             if (!Validate::string($nickname, array('min_length' => 1,
                             'max_length' => 64,
-                            'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
+                            'format' => NICKNAME_FMT))) {
                 $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.'));
                 return;
             } else if (!User::allowed_nickname($nickname)) {
@@ -202,8 +209,9 @@ class ProfilesettingsAction extends AccountSettingsAction
             } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
                 $this->showForm(_('Full name is too long (max 255 chars).'));
                 return;
-            } else if (!is_null($bio) && mb_strlen($bio) > 140) {
-                $this->showForm(_('Bio is too long (max 140 chars).'));
+            } else if (Profile::bioTooLong($bio)) {
+                $this->showForm(sprintf(_('Bio is too long (max %d chars).'),
+                                        Profile::maxBio()));
                 return;
             } else if (!is_null($location) && mb_strlen($location) > 255) {
                 $this->showForm(_('Location is too long (max 255 chars).'));
index 322a52963e8da6d6198957e61caea251dab5e0b8..b68b2ff793dc99d44ca606508a4b5c3e71481b1f 100644 (file)
@@ -59,6 +59,7 @@ class PublicAction extends Action
      */
 
     var $page = null;
+    var $notice;
 
     function isReadOnly($args)
     {
@@ -84,6 +85,18 @@ class PublicAction extends Action
 
         common_set_returnto($this->selfUrl());
 
+        $this->notice = Notice::publicStream(($this->page-1)*NOTICES_PER_PAGE,
+                                       NOTICES_PER_PAGE + 1);
+
+        if (!$this->notice) {
+            $this->serverError(_('Could not retrieve public stream.'));
+            return;
+        }
+
+        if($this->page > 1 && $this->notice->N == 0){
+            $this->serverError(_('No such page'),$code=404);
+        }
+
         return true;
     }
 
@@ -165,7 +178,8 @@ class PublicAction extends Action
         }
         else {
             if (! (common_config('site','closed') || common_config('site','inviteonly'))) {
-                $message .= _('Why not [register an account](%%action.register%%) and be the first to post!');
+                $message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and be the first to post!'),
+                                    (!common_config('site','openidonly')) ? 'register' : 'openidlogin');
             }
        }
 
@@ -185,15 +199,7 @@ class PublicAction extends Action
 
     function showContent()
     {
-        $notice = Notice::publicStream(($this->page-1)*NOTICES_PER_PAGE,
-                                       NOTICES_PER_PAGE + 1);
-
-        if (!$notice) {
-            $this->serverError(_('Could not retrieve public stream.'));
-            return;
-        }
-
-        $nl = new NoticeList($notice, $this);
+        $nl = new NoticeList($this->notice, $this);
 
         $cnt = $nl->show();
 
@@ -220,9 +226,11 @@ class PublicAction extends Action
     function showAnonymousMessage()
     {
         if (! (common_config('site','closed') || common_config('site','inviteonly'))) {
-           $m = _('This is %%site.name%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
-                  'based on the Free Software [Laconica](http://laconi.ca/) tool. ' .
-                  '[Join now](%%action.register%%) to share notices about yourself with friends, family, and colleagues! ([Read more](%%doc.help%%))');
+            $m = sprintf(_('This is %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
+                           'based on the Free Software [Laconica](http://laconi.ca/) tool. ' .
+                           '[Join now](%%%%action.%s%%%%) to share notices about yourself with friends, family, and colleagues! ' .
+                           '([Read more](%%%%doc.help%%%%))'),
+                         (!common_config('site','openidonly')) ? 'register' : 'openidlogin');
         } else {
             $m = _('This is %%site.name%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
                    'based on the Free Software [Laconica](http://laconi.ca/) tool.');
index 7e8df9625113345e521b7083b68176b469c51e59..5c08de641dbdb06696723fff221019837841b440 100644 (file)
@@ -86,9 +86,9 @@ class PublicrssAction extends Rss10Action
     {
         $c = array(
               'url' => common_local_url('publicrss')
-            , 'title' => sprintf(_('%s Public Stream'), common_config('site', 'name'))
+            , 'title' => sprintf(_('%s public timeline'), common_config('site', 'name'))
             , 'link' => common_local_url('public')
-            , 'description' => sprintf(_('All updates for %s'), common_config('site', 'name')));
+            , 'description' => sprintf(_('%s updates from everyone!'), common_config('site', 'name')));
         return $c;
     }
 
index e9f33d58b6d95df7925a07355e20ec31217f1c38..a2772869d4c1a83ccfaedceb6b320ebd9114cda9 100644 (file)
@@ -72,7 +72,8 @@ class PublictagcloudAction extends Action
             $message .= _('Be the first to post one!');
         }
         else {
-            $message .= _('Why not [register an account](%%action.register%%) and be the first to post one!');
+            $message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and be the first to post one!'),
+                                (!common_config('site','openidonly')) ? 'register' : 'openidlogin');
         }
 
         $this->elementStart('div', 'guide');
index dd3edc4ed3948d1b37ada04a9c4ea687af33ee10..aa295c0978d9ed2a93eea8ede341560f99c05fce 100644 (file)
@@ -207,8 +207,9 @@ class RegisterAction extends Action
             } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
                 $this->showForm(_('Full name is too long (max 255 chars).'));
                 return;
-            } else if (!is_null($bio) && mb_strlen($bio) > 140) {
-                $this->showForm(_('Bio is too long (max 140 chars).'));
+            } else if (Profile::bioTooLong($bio)) {
+                $this->showForm(sprintf(_('Bio is too long (max %d chars).'),
+                                        Profile::maxBio()));
                 return;
             } else if (!is_null($location) && mb_strlen($location) > 255) {
                 $this->showForm(_('Location is too long (max 255 chars).'));
@@ -442,10 +443,16 @@ class RegisterAction extends Action
                            'or profile on another site'));
             $this->elementEnd('li');
             $this->elementStart('li');
+            $maxBio = Profile::maxBio();
+            if ($maxBio > 0) {
+                $bioInstr = sprintf(_('Describe yourself and your interests in %d chars'),
+                                    $maxBio);
+            } else {
+                $bioInstr = _('Describe yourself and your interests');
+            }
             $this->textarea('bio', _('Bio'),
                             $this->trimmed('bio'),
-                            _('Describe yourself and your '.
-                              'interests in 140 chars'));
+                            $bioInstr);
             $this->elementEnd('li');
             $this->elementStart('li');
             $this->input('location', _('Location'),
index e658f8d3748ed290159222c5ed13867187e3be3e..90499bbe27fdaeb0c8c42c31599e6b1c24ac304e 100644 (file)
@@ -1,5 +1,16 @@
 <?php
-/*
+/**
+ * Handler for remote subscription
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @author   Robin Millette <millette@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://laconi.ca/
+ *
  * Laconica - a distributed open-source microblogging tool
  * Copyright (C) 2008, 2009, Control Yourself, Inc.
  *
  *
  * 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('LACONICA')) { exit(1); }
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+require_once INSTALLDIR.'/lib/omb.php';
+require_once INSTALLDIR.'/extlib/libomb/service_consumer.php';
+require_once INSTALLDIR.'/extlib/libomb/profile.php';
 
-require_once(INSTALLDIR.'/lib/omb.php');
+/**
+ * Handler for remote subscription
+ *
+ * @category Action
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @author   Robin Millette <millette@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://laconi.ca/
+ */
 
 class RemotesubscribeAction extends Action
 {
@@ -36,7 +62,7 @@ class RemotesubscribeAction extends Action
             return false;
         }
 
-        $this->nickname = $this->trimmed('nickname');
+        $this->nickname    = $this->trimmed('nickname');
         $this->profile_url = $this->trimmed('profile_url');
 
         return true;
@@ -47,7 +73,7 @@ class RemotesubscribeAction extends Action
         parent::handle($args);
 
         if ($_SERVER['REQUEST_METHOD'] == 'POST') {
-            # CSRF protection
+            /* Use a session token for CSRF protection. */
             $token = $this->trimmed('token');
             if (!$token || $token != common_session_token()) {
                 $this->showForm(_('There was a problem with your session token. '.
@@ -71,11 +97,13 @@ class RemotesubscribeAction extends Action
         if ($this->err) {
             $this->element('div', 'error', $this->err);
         } else {
-            $inst = _('To subscribe, you can [login](%%action.login%%),' .
-                      ' or [register](%%action.register%%) a new ' .
-                      ' account. If you already have an account ' .
-                      ' on a [compatible microblogging site](%%doc.openmublog%%), ' .
-                      ' enter your profile URL below.');
+            $inst = sprintf(_('To subscribe, you can [login](%%%%action.%s%%%%),' .
+                              ' or [register](%%%%action.%s%%%%) a new ' .
+                              ' account. If you already have an account ' .
+                              ' on a [compatible microblogging site](%%doc.openmublog%%), ' .
+                              ' enter your profile URL below.'),
+                            (!common_config('site','openidonly')) ? 'login' : 'openidlogin',
+                            (!common_config('site','openidonly')) ? 'register' : 'openidlogin');
             $output = common_markup_to_html($inst);
             $this->elementStart('div', 'instructions');
             $this->raw($output);
@@ -90,8 +118,8 @@ class RemotesubscribeAction extends Action
 
     function showContent()
     {
-        # id = remotesubscribe conflicts with the
-        # button on profile page
+        /* The id 'remotesubscribe' conflicts with the
+           button on profile page. */
         $this->elementStart('form', array('id' => 'form_remote_subscribe',
                                           'method' => 'post',
                                           'class' => 'form_settings',
@@ -117,247 +145,50 @@ class RemotesubscribeAction extends Action
 
     function remoteSubscription()
     {
-        $user = $this->getUser();
-
-        if (!$user) {
+        if (!$this->nickname) {
             $this->showForm(_('No such user.'));
             return;
         }
 
+        $user = User::staticGet('nickname', $this->nickname);
+
         $this->profile_url = $this->trimmed('profile_url');
 
         if (!$this->profile_url) {
-            $this->showForm(_('No such user.'));
+            $this->showForm(_('No such user'));
             return;
         }
 
-        if (!Validate::uri($this->profile_url, array('allowed_schemes' => array('http', 'https')))) {
+        if (!common_valid_http_url($this->profile_url)) {
             $this->showForm(_('Invalid profile URL (bad format)'));
             return;
         }
 
-        $fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
-        $yadis = Auth_Yadis_Yadis::discover($this->profile_url, $fetcher);
-
-        if (!$yadis || $yadis->failed) {
-            $this->showForm(_('Not a valid profile URL (no YADIS document).'));
-            return;
-        }
-
-        # XXX: a little liberal for sites that accidentally put whitespace before the xml declaration
-
-        $xrds =& Auth_Yadis_XRDS::parseXRDS(trim($yadis->response_text));
-
-        if (!$xrds) {
-            $this->showForm(_('Not a valid profile URL (no XRDS defined).'));
-            return;
-        }
-
-        $omb = $this->getOmb($xrds);
-
-        if (!$omb) {
-            $this->showForm(_('Not a valid profile URL (incorrect services).'));
-            return;
-        }
-
-        if (omb_service_uri($omb[OAUTH_ENDPOINT_REQUEST]) ==
-            common_local_url('requesttoken'))
-        {
-            $this->showForm(_('That\'s a local profile! Login to subscribe.'));
+        try {
+            $service = new OMB_Service_Consumer($this->profile_url,
+                                                common_root_url(),
+                                                omb_oauth_datastore());
+        } catch (OMB_InvalidYadisException $e) {
+            $this->showForm(_('Not a valid profile URL (no YADIS document or ' .
+                              'no or invalid XRDS defined).'));
             return;
         }
 
-        if (User::staticGet('uri', omb_local_id($omb[OAUTH_ENDPOINT_REQUEST]))) {
-            $this->showForm(_('That\'s a local profile! Login to subscribe.'));
+        if ($service->getServiceURI(OAUTH_ENDPOINT_REQUEST) ==
+            common_local_url('requesttoken') ||
+            User::staticGet('uri', $service->getRemoteUserURI())) {
+            $this->showForm(_('That’s a local profile! Login to subscribe.'));
             return;
         }
 
-        list($token, $secret) = $this->requestToken($omb);
-
-        if (!$token || !$secret) {
-            $this->showForm(_('Couldn\'t get a request token.'));
+        try {
+            $service->requestToken();
+        } catch (OMB_RemoteServiceException $e) {
+            $this->showForm(_('Couldnt get a request token.'));
             return;
         }
 
-        $this->requestAuthorization($user, $omb, $token, $secret);
-    }
-
-    function getUser()
-    {
-        $user = null;
-        if ($this->nickname) {
-            $user = User::staticGet('nickname', $this->nickname);
-        }
-        return $user;
-    }
-
-    function getOmb($xrds)
-    {
-        static $omb_endpoints = array(OMB_ENDPOINT_UPDATEPROFILE, OMB_ENDPOINT_POSTNOTICE);
-        static $oauth_endpoints = array(OAUTH_ENDPOINT_REQUEST, OAUTH_ENDPOINT_AUTHORIZE,
-                                        OAUTH_ENDPOINT_ACCESS);
-        $omb = array();
-
-        # XXX: the following code could probably be refactored to eliminate dupes
-
-        $oauth_services = omb_get_services($xrds, OAUTH_DISCOVERY);
-
-        if (!$oauth_services) {
-            return null;
-        }
-
-        $oauth_service = $oauth_services[0];
-
-        $oauth_xrd = $this->getXRD($oauth_service, $xrds);
-
-        if (!$oauth_xrd) {
-            return null;
-        }
-
-        if (!$this->addServices($oauth_xrd, $oauth_endpoints, $omb)) {
-            return null;
-        }
-
-        $omb_services = omb_get_services($xrds, OMB_NAMESPACE);
-
-        if (!$omb_services) {
-            return null;
-        }
-
-        $omb_service = $omb_services[0];
-
-        $omb_xrd = $this->getXRD($omb_service, $xrds);
-
-        if (!$omb_xrd) {
-            return null;
-        }
-
-        if (!$this->addServices($omb_xrd, $omb_endpoints, $omb)) {
-            return null;
-        }
-
-        # XXX: check that we got all the services we needed
-
-        foreach (array_merge($omb_endpoints, $oauth_endpoints) as $type) {
-            if (!array_key_exists($type, $omb) || !$omb[$type]) {
-                return null;
-            }
-        }
-
-        if (!omb_local_id($omb[OAUTH_ENDPOINT_REQUEST])) {
-            return null;
-        }
-
-        return $omb;
-    }
-
-    function getXRD($main_service, $main_xrds)
-    {
-        $uri = omb_service_uri($main_service);
-        if (strpos($uri, "#") !== 0) {
-            # FIXME: more rigorous handling of external service definitions
-            return null;
-        }
-        $id = substr($uri, 1);
-        $nodes = $main_xrds->allXrdNodes;
-        $parser = $main_xrds->parser;
-        foreach ($nodes as $node) {
-            $attrs = $parser->attributes($node);
-            if (array_key_exists('xml:id', $attrs) &&
-                $attrs['xml:id'] == $id) {
-                # XXX: trick the constructor into thinking this is the only node
-                $bogus_nodes = array($node);
-                return new Auth_Yadis_XRDS($parser, $bogus_nodes);
-            }
-        }
-        return null;
-    }
-
-    function addServices($xrd, $types, &$omb)
-    {
-        foreach ($types as $type) {
-            $matches = omb_get_services($xrd, $type);
-            if ($matches) {
-                $omb[$type] = $matches[0];
-            } else {
-                # no match for type
-                return false;
-            }
-        }
-        return true;
-    }
-
-    function requestToken($omb)
-    {
-        $con = omb_oauth_consumer();
-
-        $url = omb_service_uri($omb[OAUTH_ENDPOINT_REQUEST]);
-
-        # XXX: Is this the right thing to do? Strip off GET params and make them
-        # POST params? Seems wrong to me.
-
-        $parsed = parse_url($url);
-        $params = array();
-        parse_str($parsed['query'], $params);
-
-        $req = OAuthRequest::from_consumer_and_token($con, null, "POST", $url, $params);
-
-        $listener = omb_local_id($omb[OAUTH_ENDPOINT_REQUEST]);
-
-        if (!$listener) {
-            return null;
-        }
-
-        $req->set_parameter('omb_listener', $listener);
-        $req->set_parameter('omb_version', OMB_VERSION_01);
-
-        # XXX: test to see if endpoint accepts this signature method
-
-        $req->sign_request(omb_hmac_sha1(), $con, null);
-
-        # We re-use this tool's fetcher, since it's pretty good
-
-        $fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
-
-        $result = $fetcher->post($req->get_normalized_http_url(),
-                                 $req->to_postdata(),
-                                 array('User-Agent: Laconica/' . LACONICA_VERSION));
-        if ($result->status != 200) {
-            return null;
-        }
-
-        parse_str($result->body, $return);
-
-        return array($return['oauth_token'], $return['oauth_token_secret']);
-    }
-
-    function requestAuthorization($user, $omb, $token, $secret)
-    {
-        $con = omb_oauth_consumer();
-        $tok = new OAuthToken($token, $secret);
-
-        $url = omb_service_uri($omb[OAUTH_ENDPOINT_AUTHORIZE]);
-
-        # XXX: Is this the right thing to do? Strip off GET params and make them
-        # POST params? Seems wrong to me.
-
-        $parsed = parse_url($url);
-        $params = array();
-        parse_str($parsed['query'], $params);
-
-        $req = OAuthRequest::from_consumer_and_token($con, $tok, 'GET', $url, $params);
-
-        # We send over a ton of information. This lets the other
-        # server store info about our user, and it lets the current
-        # user decide if they really want to authorize the subscription.
-
-        $req->set_parameter('omb_version', OMB_VERSION_01);
-        $req->set_parameter('omb_listener', omb_local_id($omb[OAUTH_ENDPOINT_REQUEST]));
-        $req->set_parameter('omb_listenee', $user->uri);
-        $req->set_parameter('omb_listenee_profile', common_profile_url($user->nickname));
-        $req->set_parameter('omb_listenee_nickname', $user->nickname);
-        $req->set_parameter('omb_listenee_license', common_config('license', 'url'));
-
+        /* Create an OMB_Profile from $user. */
         $profile = $user->getProfile();
         if (!$profile) {
             common_log_db_error($user, 'SELECT', __FILE__);
@@ -365,49 +196,16 @@ class RemotesubscribeAction extends Action
             return;
         }
 
-        if (!is_null($profile->fullname)) {
-            $req->set_parameter('omb_listenee_fullname', $profile->fullname);
-        }
-        if (!is_null($profile->homepage)) {
-            $req->set_parameter('omb_listenee_homepage', $profile->homepage);
-        }
-        if (!is_null($profile->bio)) {
-            $req->set_parameter('omb_listenee_bio', $profile->bio);
-        }
-        if (!is_null($profile->location)) {
-            $req->set_parameter('omb_listenee_location', $profile->location);
-        }
-        $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
-        if ($avatar) {
-            $req->set_parameter('omb_listenee_avatar', $avatar->url);
-        }
-
-        # XXX: add a nonce to prevent replay attacks
-
-        $req->set_parameter('oauth_callback', common_local_url('finishremotesubscribe'));
-
-        # XXX: test to see if endpoint accepts this signature method
-
-        $req->sign_request(omb_hmac_sha1(), $con, $tok);
-
-        # store all our info here
-
-        $omb['listenee'] = $user->nickname;
-        $omb['listener'] = omb_local_id($omb[OAUTH_ENDPOINT_REQUEST]);
-        $omb['token'] = $token;
-        $omb['secret'] = $secret;
-        # call doesn't work after bounce back so we cache; maybe serialization issue...?
-        $omb['access_token_url'] = omb_service_uri($omb[OAUTH_ENDPOINT_ACCESS]);
-        $omb['post_notice_url'] = omb_service_uri($omb[OMB_ENDPOINT_POSTNOTICE]);
-        $omb['update_profile_url'] = omb_service_uri($omb[OMB_ENDPOINT_UPDATEPROFILE]);
+        $target_url = $service->requestAuthorization(
+                                   profile_to_omb_profile($user->uri, $profile),
+                                   common_local_url('finishremotesubscribe'));
 
         common_ensure_session();
 
-        $_SESSION['oauth_authorization_request'] = $omb;
-
-        # Redirect to authorization service
+        $_SESSION['oauth_authorization_request'] = serialize($service);
 
-        common_redirect($req->to_url(), 303);
-        return;
+        /* Redirect to the remote service for authorization. */
+        common_redirect($target_url, 303);
     }
 }
+?>
index d7ed440e9237d0a32fd814eb1dd99269ad6393f9..fcfc3a272514c343fddcb2746e01b57d76204332 100644 (file)
@@ -48,6 +48,7 @@ require_once INSTALLDIR.'/lib/feedlist.php';
 class RepliesAction extends OwnerDesignAction
 {
     var $page = null;
+    var $notice;
 
     /**
      * Prepare the object
@@ -84,6 +85,13 @@ class RepliesAction extends OwnerDesignAction
 
         common_set_returnto($this->selfUrl());
 
+        $this->notice = $this->user->getReplies(($this->page-1) * NOTICES_PER_PAGE,
+             NOTICES_PER_PAGE + 1);
+
+        if($this->page > 1 && $this->notice->N == 0){
+            $this->serverError(_('No such page'),$code=404);
+        }
+
         return true;
     }
 
@@ -159,10 +167,7 @@ class RepliesAction extends OwnerDesignAction
 
     function showContent()
     {
-        $notice = $this->user->getReplies(($this->page-1) * NOTICES_PER_PAGE,
-                                          NOTICES_PER_PAGE + 1);
-
-        $nl = new NoticeList($notice, $this);
+        $nl = new NoticeList($this->notice, $this);
 
         $cnt = $nl->show();
         if (0 === $cnt) {
@@ -187,7 +192,9 @@ class RepliesAction extends OwnerDesignAction
             }
         }
         else {
-            $message .= sprintf(_('Why not [register an account](%%%%action.register%%%%) and then nudge %s or post a notice to his or her attention.'), $this->user->nickname);
+            $message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and then nudge %s or post a notice to his or her attention.'),
+                                (!common_config('site','openidonly')) ? 'register' : 'openidlogin',
+                                $this->user->nickname);
         }
 
         $this->elementStart('div', 'guide');
index a87e2870dc090c3bae2f23cc7aac1b128b9e0287..580bb91f7c889a0fba1789fb87214477e20de3ab 100644 (file)
@@ -68,7 +68,8 @@ class RepliesrssAction extends Rss10Action
                    'link' => common_local_url('replies',
                                               array('nickname' =>
                                                     $user->nickname)),
-                   'description' => sprintf(_('Feed for replies to %s'), $user->nickname));
+                   'description' => sprintf(_('Replies to %1$s on %2$s!'),
+                                              $user->nickname, common_config('site', 'name')));
         return $c;
     }
 
index 8d1e3f004342ea399a9a35efb80386b57063bc58..8328962f2d5a94ec5aab2417df728995263eb740 100644 (file)
@@ -34,6 +34,7 @@ if (!defined('LACONICA')) {
 }
 
 require_once INSTALLDIR.'/lib/omb.php';
+require_once INSTALLDIR.'/extlib/libomb/service_provider.php';
 
 /**
  * Request token action class.
@@ -49,17 +50,17 @@ class RequesttokenAction extends Action
 {
      /**
      * Is read only?
-     * 
+     *
      * @return boolean false
      */
-    function isReadOnly($args)
+    function isReadOnly()
     {
         return false;
     }
-    
+
     /**
      * Class handler.
-     * 
+     *
      * @param array $args array of arguments
      *
      * @return void
@@ -68,14 +69,12 @@ class RequesttokenAction extends Action
     {
         parent::handle($args);
         try {
-            common_remove_magic_from_request();
-            $req    = OAuthRequest::from_request('POST', common_local_url('requesttoken'));
-            $server = omb_oauth_server();
-            $token  = $server->fetch_request_token($req);
-            print $token;
-        } catch (OAuthException $e) {
+            $srv = new OMB_Service_Provider(null, omb_oauth_datastore(),
+                                            omb_oauth_server());
+            $srv->writeRequestToken();
+        } catch (Exception $e) {
             $this->serverError($e->getMessage());
         }
     }
 }
-
+?>
index 8efe9d30aa461ff48fb589b4f0c310653ed9a047..91287cc9630b802a4b1887abb0a11b4343bcd994 100644 (file)
@@ -114,6 +114,29 @@ class ShowfavoritesAction extends OwnerDesignAction
 
         common_set_returnto($this->selfUrl());
 
+        $cur = common_current_user();
+
+        if (!empty($cur) && $cur->id == $this->user->id) {
+
+            // Show imported/gateway notices as well as local if
+            // the user is looking at his own favorites
+
+            $this->notice = $this->user->favoriteNotices(($this->page-1)*NOTICES_PER_PAGE,
+                                                   NOTICES_PER_PAGE + 1, true);
+        } else {
+            $this->notice = $this->user->favoriteNotices(($this->page-1)*NOTICES_PER_PAGE,
+                                                   NOTICES_PER_PAGE + 1, false);
+        }
+
+        if (empty($this->notice)) {
+            $this->serverError(_('Could not retrieve favorite notices.'));
+            return;
+        }
+
+        if($this->page > 1 && $this->notice->N == 0){
+            $this->serverError(_('No such page'),$code=404);
+        }
+
         return true;
     }
 
@@ -173,7 +196,9 @@ class ShowfavoritesAction extends OwnerDesignAction
             }
         }
         else {
-            $message = sprintf(_('%s hasn\'t added any notices to his favorites yet. Why not [register an account](%%%%action.register%%%%) and then post something interesting they would add to thier favorites :)'), $this->user->nickname);
+            $message = sprintf(_('%s hasn\'t added any notices to his favorites yet. Why not [register an account](%%%%action.%s%%%%) and then post something interesting they would add to their favorites :)'),
+                               $this->user->nickname,
+                               (!common_config('site','openidonly')) ? 'register' : 'openidlogin');
         }
 
         $this->elementStart('div', 'guide');
@@ -191,26 +216,7 @@ class ShowfavoritesAction extends OwnerDesignAction
 
     function showContent()
     {
-        $cur = common_current_user();
-
-        if (!empty($cur) && $cur->id == $this->user->id) {
-
-            // Show imported/gateway notices as well as local if
-            // the user is looking at his own favorites
-
-            $notice = $this->user->favoriteNotices(($this->page-1)*NOTICES_PER_PAGE,
-                                                   NOTICES_PER_PAGE + 1, true);
-        } else {
-            $notice = $this->user->favoriteNotices(($this->page-1)*NOTICES_PER_PAGE,
-                                                   NOTICES_PER_PAGE + 1, false);
-        }
-
-        if (empty($notice)) {
-            $this->serverError(_('Could not retrieve favorite notices.'));
-            return;
-        }
-
-        $nl = new NoticeList($notice, $this);
+        $nl = new NoticeList($this->notice, $this);
 
         $cnt = $nl->show();
         if (0 == $cnt) {
index 32ec674a9b7f258614896da713dfc2af6f662e24..b0cc1dbc7df61ab4d720f49debc39c4a83fdc119 100644 (file)
@@ -130,8 +130,18 @@ class ShowgroupAction extends GroupDesignAction
         $this->group = User_group::staticGet('nickname', $nickname);
 
         if (!$this->group) {
-            $this->clientError(_('No such group'), 404);
-            return false;
+            $alias = Group_alias::staticGet('alias', $nickname);
+            if ($alias) {
+                $args = array('id' => $alias->group_id);
+                if ($this->page != 1) {
+                    $args['page'] = $this->page;
+                }
+                common_redirect(common_local_url('groupbyid', $args), 301);
+                return false;
+            } else {
+                $this->clientError(_('No such group'), 404);
+                return false;
+            }
         }
 
         common_set_returnto($this->selfUrl());
@@ -440,8 +450,9 @@ class ShowgroupAction extends GroupDesignAction
             $m = sprintf(_('**%s** is a user group on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
                 'based on the Free Software [Laconica](http://laconi.ca/) tool. Its members share ' .
                 'short messages about their life and interests. '.
-                '[Join now](%%%%action.register%%%%) to become part of this group and many more! ([Read more](%%%%doc.help%%%%))'),
-                     $this->group->nickname);
+                '[Join now](%%%%action.%s%%%%) to become part of this group and many more! ([Read more](%%%%doc.help%%%%))'),
+                     $this->group->nickname,
+                     (!common_config('site','openidonly')) ? 'register' : 'openidlogin');
         } else {
             $m = sprintf(_('**%s** is a user group on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
                 'based on the Free Software [Laconica](http://laconi.ca/) tool. Its members share ' .
index 3d7319489ef489bb14e7655107346dfcecf030e1..82031d90dc5d7c3f83cf6345938ad6a39d5c259b 100644 (file)
@@ -103,8 +103,8 @@ class ShownoticeAction extends OwnerDesignAction
 
         $this->user = User::staticGet('id', $this->profile->id);
 
-        if (empty($this->user)) {
-            $this->serverError(_('Not a local notice'), 500);
+        if (! $this->notice->is_local) {
+            common_redirect($this->notice->uri);
             return false;
         }
 
@@ -196,7 +196,7 @@ class ShownoticeAction extends OwnerDesignAction
     {
         parent::handle($args);
 
-        if ($this->notice->is_local == 0) {
+        if ($this->notice->is_local == Notice::REMOTE_OMB) {
             if (!empty($this->notice->url)) {
                 common_redirect($this->notice->url, 301);
             } else if (!empty($this->notice->uri) && preg_match('/^https?:/', $this->notice->uri)) {
@@ -284,16 +284,16 @@ class ShownoticeAction extends OwnerDesignAction
         $this->element('link',array('rel'=>'alternate',
             'type'=>'application/json+oembed',
             'href'=>common_local_url(
-                'api',
-                array('apiaction'=>'oembed','method'=>'oembed.json'),
-                array('url'=>$this->notice->uri)),
+                'oembed',
+                array(),
+                array('format'=>'json','url'=>$this->notice->uri)),
             'title'=>'oEmbed'),null);
         $this->element('link',array('rel'=>'alternate',
             'type'=>'text/xml+oembed',
             'href'=>common_local_url(
-                'api',
-                array('apiaction'=>'oembed','method'=>'oembed.xml'),
-                array('url'=>$this->notice->uri)),
+                'oembed',
+                array(),
+                array('format'=>'xml','url'=>$this->notice->uri)),
             'title'=>'oEmbed'),null);
     }
 }
index cd5d4bb7013bfb84f87d8eca580ddc0ba8c74bb6..3f603d64f677ef0f95b56c92c124413274101a32 100644 (file)
@@ -358,7 +358,9 @@ class ShowstreamAction extends ProfileAction
             }
         }
         else {
-            $message .= sprintf(_('Why not [register an account](%%%%action.register%%%%) and then nudge %s or post a notice to his or her attention.'), $this->user->nickname);
+            $message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and then nudge %s or post a notice to his or her attention.'),
+                                (!common_config('site','openidonly')) ? 'register' : 'openidlogin',
+                                $this->user->nickname);
         }
 
         $this->elementStart('div', 'guide');
@@ -387,8 +389,10 @@ class ShowstreamAction extends ProfileAction
         if (!(common_config('site','closed') || common_config('site','inviteonly'))) {
             $m = sprintf(_('**%s** has an account on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
                  'based on the Free Software [Laconica](http://laconi.ca/) tool. ' .
-                 '[Join now](%%%%action.register%%%%) to follow **%s**\'s notices and many more! ([Read more](%%%%doc.help%%%%))'),
-                 $this->user->nickname, $this->user->nickname);
+                 '[Join now](%%%%action.%s%%%%) to follow **%s**\'s notices and many more! ([Read more](%%%%doc.help%%%%))'),
+                 $this->user->nickname,
+                 (!common_config('site','openidonly')) ? 'register' : 'openidlogin',
+                 $this->user->nickname);
         } else {
             $m = sprintf(_('**%s** has an account on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
                  'based on the Free Software [Laconica](http://laconi.ca/) tool. '),
index 922bab9a4e3bc2ccec5bd15ad9df102d19e376ff..33b54abf6a531de6c35f84bb2c1062ceace6af8b 100644 (file)
@@ -80,6 +80,12 @@ class SmssettingsAction extends ConnectSettingsAction
 
     function showContent()
     {
+        if (!common_config('sms', 'enabled')) {
+            $this->element('div', array('class' => 'error'),
+                           _('SMS is not available.'));
+            return;
+        }
+
         $user = common_current_user();
 
         $this->elementStart('form', array('method' => 'post',
index 66ac00fb19aa26d64da5441d07fafec8bc08f36a..40473801240a93bf13d6a627e316f507e5fe6447 100644 (file)
@@ -111,7 +111,9 @@ class SubscribersAction extends GalleryAction
             }
         }
         else {
-            $message = sprintf(_('%s has no subscribers. Why not [register an account](%%%%action.register%%%%) and be the first?'), $this->user->nickname);
+            $message = sprintf(_('%s has no subscribers. Why not [register an account](%%%%action.%s%%%%) and be the first?'),
+                               $this->user->nickname,
+                               (!common_config('site','openidonly')) ? 'register' : 'openidlogin');
         }
 
         $this->elementStart('div', 'guide');
index 42bdae10f78c160846de3f97d364bddfa88f390a..0724471ff6c946dd81d9eda5cd271f8401b7d7cc 100644 (file)
@@ -174,14 +174,26 @@ class SubscriptionsListItem extends SubscriptionListItem
             return;
         }
 
+        if (!common_config('xmpp', 'enabled') && !common_config('sms', 'enabled')) {
+            return;
+        }
+
         $this->out->elementStart('form', array('id' => 'subedit-' . $this->profile->id,
                                           'method' => 'post',
                                           'class' => 'form_subscription_edit',
                                           'action' => common_local_url('subedit')));
         $this->out->hidden('token', common_session_token());
         $this->out->hidden('profile', $this->profile->id);
-        $this->out->checkbox('jabber', _('Jabber'), $sub->jabber);
-        $this->out->checkbox('sms', _('SMS'), $sub->sms);
+        if (common_config('xmpp', 'enabled')) {
+            $this->out->checkbox('jabber', _('Jabber'), $sub->jabber);
+        } else {
+            $this->out->hidden('jabber', $sub->jabber);
+        }
+        if (common_config('sms', 'enabled')) {
+            $this->out->checkbox('sms', _('SMS'), $sub->sms);
+        } else {
+            $this->out->hidden('sms', $sub->sms);
+        }
         $this->out->submit('save', _('Save'));
         $this->out->elementEnd('form');
         return;
index 020399d9eeddc2b1d6476f4460fcefc117c26646..771eb2861eec50e7f9bcb6da2fee507fd7d6aa0f 100644 (file)
@@ -21,6 +21,9 @@ if (!defined('LACONICA')) { exit(1); }
 
 class TagAction extends Action
 {
+
+    var $notice;
+
     function prepare($args)
     {
         parent::prepare($args);
@@ -42,6 +45,12 @@ class TagAction extends Action
 
         common_set_returnto($this->selfUrl());
 
+        $this->notice = Notice_tag::getStream($this->tag, (($this->page-1)*NOTICES_PER_PAGE), NOTICES_PER_PAGE + 1);
+
+        if($this->page > 1 && $this->notice->N == 0){
+            $this->serverError(_('No such page'),$code=404);
+        }
+
         return true;
     }
 
@@ -94,9 +103,7 @@ class TagAction extends Action
 
     function showContent()
     {
-        $notice = Notice_tag::getStream($this->tag, (($this->page-1)*NOTICES_PER_PAGE), NOTICES_PER_PAGE + 1);
-
-        $nl = new NoticeList($notice, $this);
+        $nl = new NoticeList($this->notice, $this);
 
         $cnt = $nl->show();
 
index f69374fcac9d932e4a1394020422d2fba39f4301..c3c03b9cd8662f46ebe86b6bd25507c5816ff591 100644 (file)
@@ -61,7 +61,8 @@ class TagrssAction extends Rss10Action
         $c = array('url' => common_local_url('tagrss', array('tag' => $tagname)),
                'title' => $tagname,
                'link' => common_local_url('tagrss', array('tag' => $tagname)),
-               'description' => sprintf(_('Microblog tagged with %s'), $tagname));
+               'description' => sprintf(_('Updates tagged with %1$s on %2$s!'),
+                                        $tagname, common_config('site', 'name')));
         return $c;
     }
 
index bd27e9d20abb25976bc8c7e74b6a578bc9fc1ae8..aac7d63b13fc7ce656b28ae7c94a4c2275b3dabc 100644 (file)
@@ -141,9 +141,10 @@ class Twitapidirect_messagesAction extends TwitterapiAction
                 $code = 406, $apidata['content-type']);
         } else {
             $content_shortened = common_shorten_links($content);
-            if (mb_strlen($content_shortened) > 140) {
-                $this->clientError(_('That\'s too long. Max message size is 140 chars.'),
-                    $code = 406, $apidata['content-type']);
+            if (Message::contentTooLong($content_shortened)) {
+                $this->clientError(sprintf(_('That\'s too long. Max message size is %d chars.'),
+                                           Message::maxContent()),
+                                   $code = 406, $apidata['content-type']);
                 return;
             }
         }
index 82604ebff2b90a44a6fb6fbdfb55a1187b0e947b..bebc07fa19df40589269b10d2be43739a2dfb549 100644 (file)
@@ -51,6 +51,103 @@ require_once INSTALLDIR.'/lib/twitterapi.php';
  class TwitapigroupsAction extends TwitterapiAction
  {
 
+     function list_groups($args, $apidata)
+     {
+         parent::handle($args);
+         
+         common_debug("in groups api action");
+         
+         $this->auth_user = $apidata['user'];
+         $user = $this->get_user($apidata['api_arg'], $apidata);
+
+         if (empty($user)) {
+             $this->clientError('Not Found', 404, $apidata['content-type']);
+             return;
+         }
+
+         $page     = (int)$this->arg('page', 1);
+         $count    = (int)$this->arg('count', 20);
+         $max_id   = (int)$this->arg('max_id', 0);
+         $since_id = (int)$this->arg('since_id', 0);
+         $since    = $this->arg('since');
+         $group = $user->getGroups(($page-1)*$count,
+             $count, $since_id, $max_id, $since);
+
+         $sitename   = common_config('site', 'name');
+         $title      = sprintf(_("%s's groups"), $user->nickname);
+         $taguribase = common_config('integration', 'taguri');
+         $id         = "tag:$taguribase:Groups";
+         $link       = common_root_url();
+         $subtitle   = sprintf(_("groups %s is a member of on %s"), $user->nickname, $sitename);
+
+         switch($apidata['content-type']) {
+         case 'xml':
+             $this->show_xml_groups($group);
+             break;
+         case 'rss':
+             $this->show_rss_groups($group, $title, $link, $subtitle);
+             break;
+         case 'atom':
+             $selfuri = common_root_url() . 'api/laconica/groups/list/' . $user->id . '.atom';
+             $this->show_atom_groups($group, $title, $id, $link,
+                 $subtitle, $selfuri);
+             break;
+         case 'json':
+             $this->show_json_groups($group);
+             break;
+         default:
+             $this->clientError(_('API method not found!'), $code = 404);
+             break;
+         }
+     }
+
+     function list_all($args, $apidata)
+     {
+         parent::handle($args);
+         
+         common_debug("in groups api action");
+         
+         $page     = (int)$this->arg('page', 1);
+         $count    = (int)$this->arg('count', 20);
+         $max_id   = (int)$this->arg('max_id', 0);
+         $since_id = (int)$this->arg('since_id', 0);
+         $since    = $this->arg('since');
+
+         /*     TODO:
+         Use the $page, $count, $max_id, $since_id, and $since parameters
+         */
+         $group = new User_group();
+         $group->orderBy('created DESC');
+         $group->find();
+
+         $sitename   = common_config('site', 'name');
+         $title      = sprintf(_("%s groups"), $sitename);
+         $taguribase = common_config('integration', 'taguri');
+         $id         = "tag:$taguribase:Groups";
+         $link       = common_root_url();
+         $subtitle   = sprintf(_("groups on %s"), $sitename);
+
+         switch($apidata['content-type']) {
+         case 'xml':
+             $this->show_xml_groups($group);
+             break;
+         case 'rss':
+             $this->show_rss_groups($group, $title, $link, $subtitle);
+             break;
+         case 'atom':
+             $selfuri = common_root_url() . 'api/laconica/groups/list_all.atom';
+             $this->show_atom_groups($group, $title, $id, $link,
+                 $subtitle, $selfuri);
+             break;
+         case 'json':
+             $this->show_json_groups($group);
+             break;
+         default:
+             $this->clientError(_('API method not found!'), $code = 404);
+             break;
+         }
+     }
+
      function show($args, $apidata)
      {
          parent::handle($args);
diff --git a/actions/twitapioembed.php b/actions/twitapioembed.php
deleted file mode 100644 (file)
index 3019e58..0000000
+++ /dev/null
@@ -1,173 +0,0 @@
-<?php
-/**
- * Laconica, the distributed open-source microblogging tool
- *
- * Laconica-only extensions to the Twitter-like API
- *
- * 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  Twitter
- * @package   Laconica
- * @author    Evan Prodromou <evan@controlyourself.ca>
- * @copyright 2008 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/
- */
-
-if (!defined('LACONICA')) {
-    exit(1);
-}
-
-require_once INSTALLDIR.'/lib/twitterapi.php';
-
-/**
- * Oembed provider implementation
- *
- * This class handles all /main/oembed(.xml|.json)/ requests.
- *
- * @category  oEmbed
- * @package   Laconica
- * @author    Craig Andrews <candrews@integralblue.com>
- * @copyright 2008 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/
- */
-
-class TwitapioembedAction extends TwitterapiAction
-{
-
-    function oembed($args, $apidata)
-    {
-        parent::handle($args);
-
-        common_debug("in oembed api action");
-
-        $this->auth_user = $apidata['user'];
-
-        $url = $args['url'];
-        if( substr(strtolower($url),0,strlen(common_root_url())) == strtolower(common_root_url()) ){
-            $path = substr($url,strlen(common_root_url()));
-
-            $r = Router::get();
-
-            $proxy_args = $r->map($path);
-
-            if (!$proxy_args) {
-                $this->serverError(_("$path not found"), 404);
-            }
-            $oembed=array();
-            $oembed['version']='1.0';
-            $oembed['provider_name']=common_config('site', 'name');
-            $oembed['provider_url']=common_root_url();
-            switch($proxy_args['action']){
-                case 'shownotice':
-                    $oembed['type']='link';
-                    $id = $proxy_args['notice'];
-                    $notice = Notice::staticGet($id);
-                    if(empty($notice)){
-                        $this->serverError(_("notice $id not found"), 404);
-                    }
-                    $profile = $notice->getProfile();
-                    if (empty($profile)) {
-                        $this->serverError(_('Notice has no profile'), 500);
-                    }
-                    if (!empty($profile->fullname)) {
-                        $authorname = $profile->fullname . ' (' . $profile->nickname . ')';
-                    } else {
-                        $authorname = $profile->nickname;
-                    }
-                    $oembed['title'] = sprintf(_('%1$s\'s status on %2$s'),
-                        $authorname,
-                        common_exact_date($notice->created));
-                    $oembed['author_name']=$authorname;
-                    $oembed['author_url']=$profile->profileurl;
-                    $oembed['url']=($notice->url?$notice->url:$notice->uri);
-                    $oembed['html']=$notice->rendered;
-                    break;
-                case 'attachment':
-                    $id = $proxy_args['attachment'];
-                    $attachment = File::staticGet($id);
-                    if(empty($attachment)){
-                        $this->serverError(_("attachment $id not found"), 404);
-                    }
-                    if(empty($attachment->filename) && $file_oembed = File_oembed::staticGet('file_id', $attachment->id)){
-                        // Proxy the existing oembed information
-                        $oembed['type']=$file_oembed->type;
-                        $oembed['provider']=$file_oembed->provider;
-                        $oembed['provider_url']=$file_oembed->provider_url;
-                        $oembed['width']=$file_oembed->width;
-                        $oembed['height']=$file_oembed->height;
-                        $oembed['html']=$file_oembed->html;
-                        $oembed['title']=$file_oembed->title;
-                        $oembed['author_name']=$file_oembed->author_name;
-                        $oembed['author_url']=$file_oembed->author_url;
-                        $oembed['url']=$file_oembed->url;
-                    }else if(substr($attachment->mimetype,0,strlen('image/'))=='image/'){
-                        $oembed['type']='photo';
-                        //TODO set width and height
-                        //$oembed['width']=
-                        //$oembed['height']=
-                        $oembed['url']=$attachment->url;
-                    }else{
-                        $oembed['type']='link';
-                        $oembed['url']=common_local_url('attachment',
-                            array('attachment' => $attachment->id));
-                    }
-                    if($attachment->title) $oembed['title']=$attachment->title;
-                    break;
-                default:
-                    $this->serverError(_("$path not supported for oembed requests"), 501);
-            }
-
-            switch($apidata['content-type']){
-                case 'xml':
-                    $this->init_document('xml');
-                    $this->elementStart('oembed');
-                    $this->element('version',null,$oembed['version']);
-                    $this->element('type',null,$oembed['type']);
-                    if($oembed['provider_name']) $this->element('provider_name',null,$oembed['provider_name']);
-                    if($oembed['provider_url']) $this->element('provider_url',null,$oembed['provider_url']);
-                    if($oembed['title']) $this->element('title',null,$oembed['title']);
-                    if($oembed['author_name']) $this->element('author_name',null,$oembed['author_name']);
-                    if($oembed['author_url']) $this->element('author_url',null,$oembed['author_url']);
-                    if($oembed['url']) $this->element('url',null,$oembed['url']);
-                    if($oembed['html']) $this->element('html',null,$oembed['html']);
-                    if($oembed['width']) $this->element('width',null,$oembed['width']);
-                    if($oembed['height']) $this->element('height',null,$oembed['height']);
-                    if($oembed['cache_age']) $this->element('cache_age',null,$oembed['cache_age']);
-                    if($oembed['thumbnail_url']) $this->element('thumbnail_url',null,$oembed['thumbnail_url']);
-                    if($oembed['thumbnail_width']) $this->element('thumbnail_width',null,$oembed['thumbnail_width']);
-                    if($oembed['thumbnail_height']) $this->element('thumbnail_height',null,$oembed['thumbnail_height']);
-                    
-
-                    $this->elementEnd('oembed');
-                    $this->end_document('xml');
-                    break;
-                case 'json':
-                    $this->init_document('json');
-                    print(json_encode($oembed));
-                    $this->end_document('json');
-                    break;
-                default:
-                    $this->serverError(_('content type ' . $apidata['content-type'] . ' not supported'), 501);
-            }
-            
-        }else{
-            $this->serverError(_('Only ' . common_root_url() . ' urls over plain http please'), 404);
-        }
-    }
-}
-
index e3d366ecc8dddf7f4914faafaa935d1cbfeeea86..1f3c53beffd72c652d6ba6a6c102f79436a80b7a 100644 (file)
@@ -242,14 +242,15 @@ class TwitapistatusesAction extends TwitterapiAction
 
             $status_shortened = common_shorten_links($status);
 
-            if (mb_strlen($status_shortened) > 140) {
+            if (Notice::contentTooLong($status_shortened)) {
 
                 // XXX: Twitter truncates anything over 140, flags the status
                 // as "truncated." Sending this error may screw up some clients
                 // that assume Twitter will truncate for them.    Should we just
                 // truncate too? -- Zach
-                $this->clientError(_('That\'s too long. Max notice size is 140 chars.'),
-                    $code = 406, $apidata['content-type']);
+                $this->clientError(sprintf(_('That\'s too long. Max notice size is %d chars.'),
+                                           Notice::maxContent()),
+                                   $code = 406, $apidata['content-type']);
                 return;
             }
         }
@@ -455,7 +456,8 @@ class TwitapistatusesAction extends TwitterapiAction
     function friends($args, $apidata)
     {
         parent::handle($args);
-        return $this->subscriptions($apidata, 'subscribed', 'subscriber');
+        $includeStatuses=! (boolean) $args['lite'];
+        return $this->subscriptions($apidata, 'subscribed', 'subscriber', false, $includeStatuses);
     }
 
     function friendsIDs($args, $apidata)
@@ -467,7 +469,8 @@ class TwitapistatusesAction extends TwitterapiAction
     function followers($args, $apidata)
     {
         parent::handle($args);
-        return $this->subscriptions($apidata, 'subscriber', 'subscribed');
+        $includeStatuses=! (boolean) $args['lite'];
+        return $this->subscriptions($apidata, 'subscriber', 'subscribed', false, $includeStatuses);
     }
 
     function followersIDs($args, $apidata)
@@ -476,7 +479,7 @@ class TwitapistatusesAction extends TwitterapiAction
         return $this->subscriptions($apidata, 'subscriber', 'subscribed', true);
     }
 
-    function subscriptions($apidata, $other_attr, $user_attr, $onlyIDs=false)
+    function subscriptions($apidata, $other_attr, $user_attr, $onlyIDs=false, $includeStatuses=true)
     {
         $this->auth_user = $apidata['user'];
         $user = $this->get_user($apidata['api_arg'], $apidata);
@@ -532,26 +535,26 @@ class TwitapistatusesAction extends TwitterapiAction
         if ($onlyIDs) {
             $this->showIDs($others, $type);
         } else {
-            $this->show_profiles($others, $type);
+            $this->show_profiles($others, $type, $includeStatuses);
         }
 
         $this->end_document($type);
     }
 
-    function show_profiles($profiles, $type)
+    function show_profiles($profiles, $type, $includeStatuses)
     {
         switch ($type) {
         case 'xml':
             $this->elementStart('users', array('type' => 'array'));
             foreach ($profiles as $profile) {
-                $this->show_profile($profile);
+                $this->show_profile($profile,$type,null,$includeStatuses);
             }
             $this->elementEnd('users');
             break;
         case 'json':
             $arrays = array();
             foreach ($profiles as $profile) {
-                $arrays[] = $this->twitter_user_array($profile, true);
+                $arrays[] = $this->twitter_user_array($profile, $includeStatuses);
             }
             print json_encode($arrays);
             break;
diff --git a/actions/twitterauthorization.php b/actions/twitterauthorization.php
new file mode 100644 (file)
index 0000000..b04f353
--- /dev/null
@@ -0,0 +1,222 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Class for doing OAuth authentication against Twitter
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Twitter
+ * @package   Laconica
+ * @author    Zach Copely <zach@controlyourself.ca>
+ * @copyright 2009 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/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+/**
+ * Class for doing OAuth authentication against Twitter
+ *
+ * Peforms the OAuth "dance" between Laconica and Twitter -- requests a token,
+ * authorizes it, and exchanges it for an access token.  It also creates a link
+ * (Foreign_link) between the Laconica user and Twitter user and stores the
+ * access token and secret in the link.
+ *
+ * @category Twitter
+ * @package  Laconica
+ * @author   Zach Copley <zach@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ *
+ */
+class TwitterauthorizationAction extends Action
+{
+    /**
+     * Initialize class members. Looks for 'oauth_token' parameter.
+     *
+     * @param array $args misc. arguments
+     *
+     * @return boolean true
+     */
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        $this->oauth_token = $this->arg('oauth_token');
+
+        return true;
+    }
+
+    /**
+     * Handler method
+     *
+     * @param array $args is ignored since it's now passed in in prepare()
+     *
+     * @return nothing
+     */
+    function handle($args)
+    {
+        parent::handle($args);
+
+        if (!common_logged_in()) {
+            $this->clientError(_('Not logged in.'), 403);
+        }
+
+        $user  = common_current_user();
+        $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
+
+        // If there's already a foreign link record, it means we already
+        // have an access token, and this is unecessary. So go back.
+
+        if (isset($flink)) {
+            common_redirect(common_local_url('twittersettings'));
+        }
+
+        // $this->oauth_token is only populated once Twitter authorizes our
+        // request token. If it's empty we're at the beginning of the auth
+        // process
+
+        if (empty($this->oauth_token)) {
+            $this->authorizeRequestToken();
+        } else {
+            $this->saveAccessToken();
+        }
+    }
+
+    /**
+     * Asks Twitter for a request token, and then redirects to Twitter
+     * to authorize it.
+     *
+     * @return nothing
+     */
+    function authorizeRequestToken()
+    {
+        try {
+
+            // Get a new request token and authorize it
+
+            $client  = new TwitterOAuthClient();
+            $req_tok =
+              $client->getRequestToken(TwitterOAuthClient::$requestTokenURL);
+
+            // Sock the request token away in the session temporarily
+
+            $_SESSION['twitter_request_token']        = $req_tok->key;
+            $_SESSION['twitter_request_token_secret'] = $req_tok->secret;
+
+            $auth_link = $client->getAuthorizeLink($req_tok);
+
+        } catch (TwitterOAuthClientException $e) {
+            $msg = sprintf('OAuth client cURL error - code: %1s, msg: %2s',
+                           $e->getCode(), $e->getMessage());
+            $this->serverError(_('Couldn\'t link your Twitter account.'));
+        }
+
+        common_redirect($auth_link);
+    }
+
+    /**
+     * Called when Twitter returns an authorized request token. Exchanges
+     * it for an access token and stores it.
+     *
+     * @return nothing
+     */
+    function saveAccessToken()
+    {
+
+        // Check to make sure Twitter returned the same request
+        // token we sent them
+
+        if ($_SESSION['twitter_request_token'] != $this->oauth_token) {
+            $this->serverError(_('Couldn\'t link your Twitter account.'));
+        }
+
+        try {
+
+            $client = new TwitterOAuthClient($_SESSION['twitter_request_token'],
+                $_SESSION['twitter_request_token_secret']);
+
+            // Exchange the request token for an access token
+
+            $atok = $client->getAccessToken(TwitterOAuthClient::$accessTokenURL);
+
+            // Test the access token and get the user's Twitter info
+
+            $client       = new TwitterOAuthClient($atok->key, $atok->secret);
+            $twitter_user = $client->verifyCredentials();
+
+        } catch (OAuthClientException $e) {
+            $msg = sprintf('OAuth client cURL error - code: %1$s, msg: %2$s',
+                           $e->getCode(), $e->getMessage());
+            $this->serverError(_('Couldn\'t link your Twitter account.'));
+        }
+
+        // Save the access token and Twitter user info
+
+        $this->saveForeignLink($atok, $twitter_user);
+
+        // Clean up the the mess we made in the session
+
+        unset($_SESSION['twitter_request_token']);
+        unset($_SESSION['twitter_request_token_secret']);
+
+        common_redirect(common_local_url('twittersettings'));
+    }
+
+    /**
+     * Saves a Foreign_link between Twitter user and local user,
+     * which includes the access token and secret.
+     *
+     * @param OAuthToken $access_token the access token to save
+     * @param mixed      $twitter_user twitter API user object
+     *
+     * @return nothing
+     */
+    function saveForeignLink($access_token, $twitter_user)
+    {
+        $user = common_current_user();
+
+        $flink = new Foreign_link();
+
+        $flink->user_id     = $user->id;
+        $flink->foreign_id  = $twitter_user->id;
+        $flink->service     = TWITTER_SERVICE;
+
+        $creds = TwitterOAuthClient::packToken($access_token);
+
+        $flink->credentials = $creds;
+        $flink->created     = common_sql_now();
+
+        // Defaults: noticesync on, everything else off
+
+        $flink->set_flags(true, false, false, false);
+
+        $flink_id = $flink->insert();
+
+        if (empty($flink_id)) {
+            common_log_db_error($flink, 'INSERT', __FILE__);
+                $this->serverError(_('Couldn\'t link your Twitter account.'));
+        }
+
+        save_twitter_user($twitter_user->id, $twitter_user->screen_name);
+    }
+
+}
+
index 2b742788eee55419112a718d271f6e4285960163..0859ab9d34ef55c8b45bc9ad71f0e257f6aeb154 100644 (file)
@@ -34,8 +34,6 @@ if (!defined('LACONICA')) {
 require_once INSTALLDIR.'/lib/connectsettingsaction.php';
 require_once INSTALLDIR.'/lib/twitter.php';
 
-define('SUBSCRIPTIONS', 80);
-
 /**
  * Settings for Twitter integration
  *
@@ -69,9 +67,8 @@ class TwittersettingsAction extends ConnectSettingsAction
 
     function getInstructions()
     {
-        return _('Add your Twitter account to automatically send '.
-                 ' your notices to Twitter, ' .
-                 'and subscribe to Twitter friends already here.');
+        return _('Connect your Twitter account to share your updates ' .
+                 'with your Twitter friends and vice-versa.');
     }
 
     /**
@@ -85,6 +82,12 @@ class TwittersettingsAction extends ConnectSettingsAction
 
     function showContent()
     {
+        if (!common_config('twitter', 'enabled')) {
+            $this->element('div', array('class' => 'error'),
+                           _('Twitter is not available.'));
+            return;
+        }
+
         $user = common_current_user();
 
         $profile = $user->getProfile();
@@ -93,7 +96,7 @@ class TwittersettingsAction extends ConnectSettingsAction
 
         $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
 
-        if ($flink) {
+        if (!empty($flink)) {
             $fuser = $flink->getForeignUser();
         }
 
@@ -102,192 +105,86 @@ class TwittersettingsAction extends ConnectSettingsAction
                                           'class' => 'form_settings',
                                           'action' =>
                                           common_local_url('twittersettings')));
-        $this->elementStart('fieldset', array('id' => 'settings_twitter_account'));
-        $this->element('legend', null, _('Twitter Account'));
+
         $this->hidden('token', common_session_token());
-        if ($fuser) {
+
+        $this->elementStart('fieldset', array('id' => 'settings_twitter_account'));
+
+        if (empty($fuser)) {
             $this->elementStart('ul', 'form_data');
-            $this->elementStart('li', array('id' => 'settings_twitter_remove'));
-            $this->element('span', 'twitter_user', $fuser->nickname);
-            $this->element('a', array('href' => $fuser->uri), $fuser->uri);
-            $this->element('p', 'form_note',
-                           _('Current verified Twitter account.'));
-            $this->hidden('flink_foreign_id', $flink->foreign_id);
+            $this->elementStart('li', array('id' => 'settings_twitter_login_button'));
+            $this->element('a', array('href' => common_local_url('twitterauthorization')),
+                           'Connect my Twitter account');
             $this->elementEnd('li');
             $this->elementEnd('ul');
-            $this->submit('remove', _('Remove'));
+
+            $this->elementEnd('fieldset');
         } else {
+            $this->element('legend', null, _('Twitter account'));
+            $this->elementStart('p', array('id' => 'form_confirmed'));
+            $this->element('a', array('href' => $fuser->uri), $fuser->nickname);
+            $this->elementEnd('p');
+            $this->element('p', 'form_note',
+                           _('Connected Twitter account'));
+
+            $this->submit('remove', _('Remove'));
+
+            $this->elementEnd('fieldset');
+
+            $this->elementStart('fieldset', array('id' => 'settings_twitter_preferences'));
+
+            $this->element('legend', null, _('Preferences'));
             $this->elementStart('ul', 'form_data');
-            $this->elementStart('li', array('id' => 'settings_twitter_login'));
-            $this->input('twitter_username', _('Twitter user name'),
-                         ($this->arg('twitter_username')) ?
-                         $this->arg('twitter_username') :
-                         $profile->nickname,
-                         _('No spaces, please.')); // hey, it's what Twitter says
+            $this->elementStart('li');
+            $this->checkbox('noticesend',
+                            _('Automatically send my notices to Twitter.'),
+                            ($flink) ?
+                            ($flink->noticesync & FOREIGN_NOTICE_SEND) :
+                            true);
             $this->elementEnd('li');
             $this->elementStart('li');
-            $this->password('twitter_password', _('Twitter password'));
-            $this->elementend('li');
-            $this->elementEnd('ul');
-        }
-        $this->elementEnd('fieldset');
-
-        $this->elementStart('fieldset',
-                            array('id' => 'settings_twitter_preferences'));
-        $this->element('legend', null, _('Preferences'));
-
-        $this->elementStart('ul', 'form_data');
-        $this->elementStart('li');
-        $this->checkbox('noticesend',
-                        _('Automatically send my notices to Twitter.'),
-                        ($flink) ?
-                        ($flink->noticesync & FOREIGN_NOTICE_SEND) :
-                        true);
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->checkbox('replysync',
-                        _('Send local "@" replies to Twitter.'),
-                        ($flink) ?
-                        ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) :
-                        true);
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->checkbox('friendsync',
-                        _('Subscribe to my Twitter friends here.'),
-                        ($flink) ?
-                        ($flink->friendsync & FOREIGN_FRIEND_RECV) :
-                        false);
-        $this->elementEnd('li');
-
-        if (common_config('twitterbridge','enabled')) {
+            $this->checkbox('replysync',
+                            _('Send local "@" replies to Twitter.'),
+                            ($flink) ?
+                            ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) :
+                            true);
+            $this->elementEnd('li');
             $this->elementStart('li');
-            $this->checkbox('noticerecv',
-                            _('Import my Friends Timeline.'),
+            $this->checkbox('friendsync',
+                            _('Subscribe to my Twitter friends here.'),
                             ($flink) ?
-                            ($flink->noticesync & FOREIGN_NOTICE_RECV) :
+                            ($flink->friendsync & FOREIGN_FRIEND_RECV) :
                             false);
             $this->elementEnd('li');
-        } else {
-            // preserve setting even if bidrection bridge toggled off
-            if ($flink && ($flink->noticesync & FOREIGN_NOTICE_RECV)) {
-                $this->hidden('noticerecv', true, 'noticerecv');
-            }
-        }
-
-        $this->elementEnd('ul');
-
-        if ($flink) {
-            $this->submit('save', _('Save'));
-        } else {
-            $this->submit('add', _('Add'));
-        }
-        $this->elementEnd('fieldset');
-
-        $this->showTwitterSubscriptions();
-
-        $this->elementEnd('form');
-    }
-
-    /**
-     * Gets some of the user's Twitter friends
-     *
-     * Gets the number of Twitter friends that are on this
-     * instance of Laconica.
-     *
-     * @return array array of User objects
-     */
-
-    function subscribedTwitterUsers()
-    {
-
-        $current_user = common_current_user();
-
-        $qry = 'SELECT "user".* ' .
-          'FROM subscription ' .
-          'JOIN "user" ON subscription.subscribed = "user".id ' .
-          'JOIN foreign_link ON foreign_link.user_id = "user".id ' .
-          'WHERE subscriber = %d ' .
-          'ORDER BY "user".nickname';
-
-        $user = new User();
-
-        $user->query(sprintf($qry, $current_user->id));
-
-        $users = array();
-
-        while ($user->fetch()) {
-
-            // Don't include the user's own self-subscription
-            if ($user->id != $current_user->id) {
-                $users[] = clone($user);
-            }
-        }
-
-        return $users;
-    }
-
-    /**
-     * Show user's Twitter friends
-     *
-     * Gets the number of Twitter friends that are on this
-     * instance of Laconica, and shows their mini-avatars.
-     *
-     * @return void
-     */
-
-    function showTwitterSubscriptions()
-    {
-
-        $friends = $this->subscribedTwitterUsers();
-
-        $friends_count = count($friends);
 
-        if ($friends_count > 0) {
-            $this->elementStart('div', array('id' => 'entity_subscriptions',
-                                             'class' => 'section'));
-            $this->element('h2', null, _('Twitter Friends'));
-            $this->elementStart('ul', 'entities users xoxo');
-
-            for ($i = 0; $i < min($friends_count, SUBSCRIPTIONS); $i++) {
+            if (common_config('twitterbridge','enabled')) {
+                $this->elementStart('li');
+                $this->checkbox('noticerecv',
+                                _('Import my Friends Timeline.'),
+                                ($flink) ?
+                                ($flink->noticesync & FOREIGN_NOTICE_RECV) :
+                                false);
+                $this->elementEnd('li');
 
-                $other = Profile::staticGet($friends[$i]->id);
+                // preserve setting even if bidrection bridge toggled off
 
-                if (!$other) {
-                    common_log_db_error($subs, 'SELECT', __FILE__);
-                    continue;
+                if ($flink && ($flink->noticesync & FOREIGN_NOTICE_RECV)) {
+                    $this->hidden('noticerecv', true, 'noticerecv');
                 }
-
-                $this->elementStart('li', 'vcard');
-                $this->elementStart('a', array('title' => ($other->fullname) ?
-                                               $other->fullname :
-                                               $other->nickname,
-                                               'href' => $other->profileurl,
-                                               'class' => 'url'));
-
-                $avatar = $other->getAvatar(AVATAR_MINI_SIZE);
-
-                $avatar_url = ($avatar) ?
-                  $avatar->displayUrl() :
-                  Avatar::defaultImage(AVATAR_MINI_SIZE);
-
-                $this->element('img', array('src' => $avatar_url,
-                                            'width' => AVATAR_MINI_SIZE,
-                                            'height' => AVATAR_MINI_SIZE,
-                                            'class' => 'avatar photo',
-                                            'alt' =>  ($other->fullname) ?
-                                            $other->fullname :
-                                            $other->nickname));
-
-                $this->element('span', 'fn nickname', $other->nickname);
-                $this->elementEnd('a');
-                $this->elementEnd('li');
-
             }
 
             $this->elementEnd('ul');
-            $this->elementEnd('div');
 
+            if ($flink) {
+                $this->submit('save', _('Save'));
+            } else {
+                $this->submit('add', _('Add'));
+            }
+
+            $this->elementEnd('fieldset');
         }
+
+        $this->elementEnd('form');
     }
 
     /**
@@ -303,7 +200,6 @@ class TwittersettingsAction extends ConnectSettingsAction
 
     function handlePost()
     {
-
         // CSRF protection
         $token = $this->trimmed('token');
         if (!$token || $token != common_session_token()) {
@@ -314,8 +210,6 @@ class TwittersettingsAction extends ConnectSettingsAction
 
         if ($this->arg('save')) {
             $this->savePreferences();
-        } else if ($this->arg('add')) {
-            $this->addTwitterAccount();
         } else if ($this->arg('remove')) {
             $this->removeTwitterAccount();
         } else {
@@ -323,82 +217,6 @@ class TwittersettingsAction extends ConnectSettingsAction
         }
     }
 
-    /**
-     * Associate a Twitter account with the user's account
-     *
-     * Validates post input; verifies it against Twitter; and if
-     * successful stores in the database.
-     *
-     * @return void
-     */
-
-    function addTwitterAccount()
-    {
-        $screen_name = $this->trimmed('twitter_username');
-        $password    = $this->trimmed('twitter_password');
-        $noticesend  = $this->boolean('noticesend');
-        $noticerecv  = $this->boolean('noticerecv');
-        $replysync   = $this->boolean('replysync');
-        $friendsync  = $this->boolean('friendsync');
-
-        if (!Validate::string($screen_name,
-                              array('min_length' => 1,
-                                    'max_length' => 15,
-                                    'format' => VALIDATE_NUM.VALIDATE_ALPHA.'_'))) {
-            $this->showForm(_('Username must have only numbers, '.
-                              'upper- and lowercase letters, '.
-                              'and underscore (_). 15 chars max.'));
-            return;
-        }
-
-        if (!$this->verifyCredentials($screen_name, $password)) {
-            $this->showForm(_('Could not verify your Twitter credentials!'));
-            return;
-        }
-
-        $twit_user = twitter_user_info($screen_name, $password);
-
-        if (!$twit_user) {
-            $this->showForm(sprintf(_('Unable to retrieve account information '.
-                                      'For "%s" from Twitter.'),
-                                    $screen_name));
-            return;
-        }
-
-        if (!save_twitter_user($twit_user->id, $screen_name)) {
-            $this->showForm(_('Unable to save your Twitter settings!'));
-            return;
-        }
-
-        $user = common_current_user();
-
-        $flink = new Foreign_link();
-
-        $flink->user_id     = $user->id;
-        $flink->foreign_id  = $twit_user->id;
-        $flink->service     = TWITTER_SERVICE;
-        $flink->credentials = $password;
-        $flink->created     = common_sql_now();
-
-        $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync);
-
-        $flink_id = $flink->insert();
-
-        if (!$flink_id) {
-            common_log_db_error($flink, 'INSERT', __FILE__);
-            $this->showForm(_('Unable to save your Twitter settings!'));
-            return;
-        }
-
-        if ($friendsync) {
-            save_twitter_friends($user, $twit_user->id, $screen_name, $password);
-            $flink->last_friendsync = common_sql_now();
-            $flink->update();
-        }
-
-        $this->showForm(_('Twitter settings saved.'), true);
-    }
-
     /**
      * Disassociate an existing Twitter account from this account
      *
@@ -408,20 +226,11 @@ class TwittersettingsAction extends ConnectSettingsAction
     function removeTwitterAccount()
     {
         $user = common_current_user();
-
-        $flink = Foreign_link::getByUserID($user->id, 1);
-
-        $flink_foreign_id = $this->arg('flink_foreign_id');
-
-        // Maybe an old tab open...?
-        if ($flink->foreign_id != $flink_foreign_id) {
-            $this->showForm(_('That is not your Twitter account.'));
-            return;
-        }
+        $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
 
         $result = $flink->delete();
 
-        if (!$result) {
+        if (empty($result)) {
             common_log_db_error($flink, 'DELETE', __FILE__);
             $this->serverError(_('Couldn\'t remove Twitter user.'));
             return;
@@ -444,32 +253,16 @@ class TwittersettingsAction extends ConnectSettingsAction
         $replysync  = $this->boolean('replysync');
 
         $user = common_current_user();
+        $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
 
-        $flink = Foreign_link::getByUserID($user->id, 1);
-
-        if (!$flink) {
+        if (empty($flink)) {
             common_log_db_error($flink, 'SELECT', __FILE__);
             $this->showForm(_('Couldn\'t save Twitter preferences.'));
             return;
         }
 
-        $twitter_id = $flink->foreign_id;
-        $password   = $flink->credentials;
-
-        $fuser = $flink->getForeignUser();
-
-        if (!$fuser) {
-            common_log_db_error($fuser, 'SELECT', __FILE__);
-            $this->showForm(_('Couldn\'t save Twitter preferences.'));
-            return;
-        }
-
-        $screen_name = $fuser->nickname;
-
         $original = clone($flink);
-
         $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync);
-
         $result = $flink->update($original);
 
         if ($result === false) {
@@ -478,45 +271,7 @@ class TwittersettingsAction extends ConnectSettingsAction
             return;
         }
 
-        if ($friendsync) {
-            save_twitter_friends($user, $flink->foreign_id, $screen_name, $password);
-        }
-
         $this->showForm(_('Twitter preferences saved.'), true);
     }
 
-    /**
-     * Verifies a username and password against Twitter's API
-     *
-     * @param string $screen_name Twitter user name
-     * @param string $password    Twitter password
-     *
-     * @return boolean success flag
-     */
-
-    function verifyCredentials($screen_name, $password)
-    {
-        $uri = 'http://twitter.com/account/verify_credentials.json';
-
-        $data = get_twitter_data($uri, $screen_name, $password);
-
-        if (!$data) {
-            return false;
-        }
-
-        $user = json_decode($data);
-
-        if (!$user) {
-            return false;
-        }
-
-        $twitter_id = $user->id;
-
-        if ($twitter_id) {
-            return $twitter_id;
-        }
-
-        return false;
-    }
-
 }
index 19275041a81e438b6c4944f177fcb543b1fe5f69..46fbcf6571e81d4a7f454c531b9b6e3a904c833a 100644 (file)
@@ -1,5 +1,16 @@
 <?php
-/*
+/**
+ * Unsubscribe handler
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @author   Robin Millette <millette@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://laconi.ca/
+ *
  * Laconica - a distributed open-source microblogging tool
  * Copyright (C) 2008, 2009, Control Yourself, Inc.
  *
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+/**
+ * Unsubscribe handler
+ *
+ * @category Action
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @author   Robin Millette <millette@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://laconi.ca/
+ */
 class UnsubscribeAction extends Action
 {
 
@@ -31,16 +56,18 @@ class UnsubscribeAction extends Action
         $user = common_current_user();
 
         if ($_SERVER['REQUEST_METHOD'] != 'POST') {
-            common_redirect(common_local_url('subscriptions', array('nickname' => $user->nickname)));
+            common_redirect(common_local_url('subscriptions',
+                                             array('nickname' => $user->nickname)));
             return;
         }
 
-        # CSRF protection
+        /* Use a session token for CSRF protection. */
 
         $token = $this->trimmed('token');
 
         if (!$token || $token != common_session_token()) {
-            $this->clientError(_('There was a problem with your session token. Try again, please.'));
+            $this->clientError(_('There was a problem with your session token. ' .
+                                 'Try again, please.'));
             return;
         }
 
@@ -53,7 +80,7 @@ class UnsubscribeAction extends Action
 
         $other = Profile::staticGet('id', $other_id);
 
-        if (!$other_id) {
+        if (!$other) {
             $this->clientError(_('No profile with that id.'));
             return;
         }
@@ -76,8 +103,8 @@ class UnsubscribeAction extends Action
             $this->elementEnd('body');
             $this->elementEnd('html');
         } else {
-            common_redirect(common_local_url('subscriptions', array('nickname' =>
-                                                                    $user->nickname)),
+            common_redirect(common_local_url('subscriptions',
+                                             array('nickname' => $user->nickname)),
                             303);
         }
     }
index d8b62fb09051d59f55c5ad374b6937dec2f3b0c0..b020413b3563fd3e2ebc6b50872ba6a4aec74f50 100644 (file)
@@ -1,5 +1,16 @@
 <?php
-/*
+/**
+ * Handle an updateprofile action
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @author   Robin Millette <millette@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://laconi.ca/
+ *
  * Laconica - a distributed open-source microblogging tool
  * Copyright (C) 2008, 2009, Control Yourself, Inc.
  *
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-if (!defined('LACONICA')) { exit(1); }
+if (!defined('LACONICA')) {
+    exit(1);
+}
 
-require_once(INSTALLDIR.'/lib/omb.php');
+require_once INSTALLDIR.'/lib/omb.php';
+require_once INSTALLDIR.'/extlib/libomb/service_provider.php';
 
+/**
+ * Handle an updateprofile action
+ *
+ * @category Action
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @author   Robin Millette <millette@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://laconi.ca/
+ */
 class UpdateprofileAction extends Action
 {
-    
-    function handle($args)
-    {
-        parent::handle($args);
-        try {
-            common_remove_magic_from_request();
-            $req = OAuthRequest::from_request('POST', common_local_url('updateprofile'));
-            # Note: server-to-server function!
-            $server = omb_oauth_server();
-            list($consumer, $token) = $server->verify_request($req);
-            if ($this->update_profile($req, $consumer, $token)) {
-                header('HTTP/1.1 200 OK');
-                header('Content-type: text/plain');
-                print "omb_version=".OMB_VERSION_01;
-            }
-        } catch (OAuthException $e) {
-            $this->serverError($e->getMessage());
-            return;
-        }
-    }
 
-    function update_profile($req, $consumer, $token)
+    /**
+     * For initializing members of the class.
+     *
+     * @param array $argarray misc. arguments
+     *
+     * @return boolean true
+     */
+    function prepare($argarray)
     {
         $version = $req->get_parameter('omb_version');
         if ($version != OMB_VERSION_01) {
@@ -79,7 +90,7 @@ class UpdateprofileAction extends Action
         $nickname = $req->get_parameter('omb_listenee_nickname');
         if ($nickname && !Validate::string($nickname, array('min_length' => 1,
                                                             'max_length' => 64,
-                                                            'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
+                                                            'format' => NICKNAME_FMT))) {
             $this->clientError(_('Nickname must have only lowercase letters and numbers and no spaces.'));
             return false;
         }
@@ -88,96 +99,20 @@ class UpdateprofileAction extends Action
             $this->clientError(sprintf(_("Invalid license URL '%s'"), $license));
             return false;
         }
-        $profile_url = $req->get_parameter('omb_listenee_profile');
-        if ($profile_url && !common_valid_http_url($profile_url)) {
-            $this->clientError(sprintf(_("Invalid profile URL '%s'."), $profile_url));
-            return false;
-        }
-        # optional stuff
-        $fullname = $req->get_parameter('omb_listenee_fullname');
-        if ($fullname && mb_strlen($fullname) > 255) {
-            $this->clientError(_("Full name is too long (max 255 chars)."));
-            return false;
-        }
-        $homepage = $req->get_parameter('omb_listenee_homepage');
-        if ($homepage && (!common_valid_http_url($homepage) || mb_strlen($homepage) > 255)) {
-            $this->clientError(sprintf(_("Invalid homepage '%s'"), $homepage));
-            return false;
-        }
-        $bio = $req->get_parameter('omb_listenee_bio');
-        if ($bio && mb_strlen($bio) > 140) {
-            $this->clientError(_("Bio is too long (max 140 chars)."));
-            return false;
-        }
-        $location = $req->get_parameter('omb_listenee_location');
-        if ($location && mb_strlen($location) > 255) {
-            $this->clientError(_("Location is too long (max 255 chars)."));
-            return false;
-        }
-        $avatar = $req->get_parameter('omb_listenee_avatar');
-        if ($avatar) {
-            if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
-                $this->clientError(sprintf(_("Invalid avatar URL '%s'"), $avatar));
-                return false;
-            }
-            $size = @getimagesize($avatar);
-            if (!$size) {
-                $this->clientError(sprintf(_("Can't read avatar URL '%s'"), $avatar));
-                return false;
-            }
-            if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
-                $this->clientError(sprintf(_("Wrong size image at '%s'"), $avatar));
-                return false;
-            }
-            if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
-                                          IMAGETYPE_PNG))) {
-                $this->clientError(sprintf(_("Wrong image type for '%s'"), $avatar));
-                return false;
-            }
-        }
-
-        $orig_profile = clone($profile);
+        return true;
+    }
 
-        /* Use values even if they are an empty string. Parsing an empty string in
-           updateProfile is the specified way of clearing a parameter in OMB. */
-        if (!is_null($nickname)) {
-            $profile->nickname = $nickname;
-        }
-        if (!is_null($profile_url)) {
-            $profile->profileurl = $profile_url;
-        }
-        if (!is_null($fullname)) {
-            $profile->fullname = $fullname;
-        }
-        if (!is_null($homepage)) {
-            $profile->homepage = $homepage;
-        }
-        if (!is_null($bio)) {
-            $profile->bio = $bio;
-        }
-        if (!is_null($location)) {
-            $profile->location = $location;
-        }
+    function handle($args)
+    {
+        parent::handle($args);
 
-        if (!$profile->update($orig_profile)) {
-            $this->serverError(_('Could not save new profile info'), 500);
-            return false;
-        } else {
-            if ($avatar) {
-                $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
-                copy($avatar, $temp_filename);
-                $imagefile = new ImageFile($profile->id, $temp_filename);
-                $filename = Avatar::filename($profile->id,
-                                     image_type_to_extension($imagefile->type),
-                                     null,
-                                     common_timestamp());
-                rename($temp_filename, Avatar::path($filename));
-                if (!$profile->setOriginal($filename)) {
-                    $this->serverError(_('Could not save avatar info'), 500);
-                    return false;
-                }
-            }
-            return true;
+        try {
+            $srv = new OMB_Service_Provider(null, omb_oauth_datastore(),
+                                            omb_oauth_server());
+            $srv->handleUpdateProfile();
+        } catch (Exception $e) {
+            $this->serverError($e->getMessage());
+            return;
         }
     }
-}
+}
\ No newline at end of file
index 8dc2c808d6ff849f0da742e342de137203eeacd4..3e7be97479116dfe634b963a441228fd9e8c95bf 100644 (file)
@@ -1,5 +1,16 @@
 <?php
-/*
+/**
+ * Let the user authorize a remote subscription request
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @author   Robin Millette <millette@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://laconi.ca/
+ *
  * Laconica - a distributed open-source microblogging tool
  * Copyright (C) 2008, 2009, Control Yourself, Inc.
  *
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-if (!defined('LACONICA')) { exit(1); }
+if (!defined('LACONICA')) {
+    exit(1);
+}
 
-require_once(INSTALLDIR.'/lib/omb.php');
+require_once INSTALLDIR.'/lib/omb.php';
+require_once INSTALLDIR.'/extlib/libomb/service_provider.php';
+require_once INSTALLDIR.'/extlib/libomb/profile.php';
 define('TIMESTAMP_THRESHOLD', 300);
 
 class UserauthorizationAction extends Action
@@ -32,42 +47,62 @@ class UserauthorizationAction extends Action
         parent::handle($args);
 
         if ($_SERVER['REQUEST_METHOD'] == 'POST') {
-            # CSRF protection
+            /* Use a session token for CSRF protection. */
             $token = $this->trimmed('token');
             if (!$token || $token != common_session_token()) {
-                $params = $this->getStoredParams();
-                $this->showForm($params, _('There was a problem with your session token. '.
-                                        'Try again, please.'));
+                $srv = $this->getStoredParams();
+                $this->showForm($srv->getRemoteUser(), _('There was a problem ' .
+                                        'with your session token. Try again, ' .
+                                        'please.'));
                 return;
             }
-            # We've shown the form, now post user's choice
+            /* We've shown the form, now post user's choice. */
             $this->sendAuthorization();
         } else {
             if (!common_logged_in()) {
-                # Go log in, and then come back
+                /* Go log in, and then come back. */
                 common_set_returnto($_SERVER['REQUEST_URI']);
 
-                common_redirect(common_local_url('login'));
+                if (!common_config('site', 'openidonly')) {
+                    common_redirect(common_local_url('login'));
+                } else {
+                    common_redirect(common_local_url('openidlogin'));
+                }
+                return;
+            }
+
+            $user    = common_current_user();
+            $profile = $user->getProfile();
+            if (!$profile) {
+                common_log_db_error($user, 'SELECT', __FILE__);
+                $this->serverError(_('User without matching profile'));
                 return;
             }
 
+            /* TODO: If no token is passed the user should get a prompt to enter
+               it according to OAuth Core 1.0. */
             try {
-                $this->validateRequest();
-                $this->storeParams($_GET);
-                $this->showForm($_GET);
-            } catch (OAuthException $e) {
+                $this->validateOmb();
+                $srv = new OMB_Service_Provider(
+                        profile_to_omb_profile($user->uri, $profile),
+                        omb_oauth_datastore());
+
+                $remote_user = $srv->handleUserAuth();
+            } catch (Exception $e) {
                 $this->clearParams();
                 $this->clientError($e->getMessage());
                 return;
             }
 
+            $this->storeParams($srv);
+            $this->showForm($remote_user);
         }
     }
 
     function showForm($params, $error=null)
     {
         $this->params = $params;
-        $this->error = $error;
+        $this->error  = $error;
         $this->showPage();
     }
 
@@ -79,23 +114,24 @@ class UserauthorizationAction extends Action
     function showPageNotice()
     {
         $this->element('p', null, _('Please check these details to make sure '.
-                                    'that you want to subscribe to this user\'s notices. '.
-                                    'If you didn\'t just ask to subscribe to someone\'s notices, '.
-                                    'click "Reject".'));
+                                    'that you want to subscribe to this ' .
+                                    'user’s notices. If you didn’t just ask ' .
+                                    'to subscribe to someone’s notices, '.
+                                    'click “Reject”.'));
     }
 
     function showContent()
     {
         $params = $this->params;
 
-        $nickname = $params['omb_listenee_nickname'];
-        $profile = $params['omb_listenee_profile'];
-        $license = $params['omb_listenee_license'];
-        $fullname = $params['omb_listenee_fullname'];
-        $homepage = $params['omb_listenee_homepage'];
-        $bio = $params['omb_listenee_bio'];
-        $location = $params['omb_listenee_location'];
-        $avatar = $params['omb_listenee_avatar'];
+        $nickname = $params->getNickname();
+        $profile  = $params->getProfileURL();
+        $license  = $params->getLicenseURL();
+        $fullname = $params->getFullname();
+        $homepage = $params->getHomepage();
+        $bio      = $params->getBio();
+        $location = $params->getLocation();
+        $avatar   = $params->getAvatarURL();
 
         $this->elementStart('div', array('class' => 'profile'));
         $this->elementStart('div', 'entity_profile vcard');
@@ -172,11 +208,14 @@ class UserauthorizationAction extends Action
                                           'id' => 'userauthorization',
                                           'class' => 'form_user_authorization',
                                           'name' => 'userauthorization',
-                                          'action' => common_local_url('userauthorization')));
+                                          'action' => common_local_url(
+                                                         'userauthorization')));
         $this->hidden('token', common_session_token());
 
-        $this->submit('accept', _('Accept'), 'submit accept', null, _('Subscribe to this user'));
-        $this->submit('reject', _('Reject'), 'submit reject', null, _('Reject this subscription'));
+        $this->submit('accept', _('Accept'), 'submit accept', null,
+                      _('Subscribe to this user'));
+        $this->submit('reject', _('Reject'), 'submit reject', null,
+                      _('Reject this subscription'));
         $this->elementEnd('form');
         $this->elementEnd('li');
         $this->elementEnd('ul');
@@ -186,191 +225,27 @@ class UserauthorizationAction extends Action
 
     function sendAuthorization()
     {
-        $params = $this->getStoredParams();
+        $srv = $this->getStoredParams();
 
-        if (!$params) {
+        if (is_null($srv)) {
             $this->clientError(_('No authorization request!'));
             return;
         }
 
-        $callback = $params['oauth_callback'];
-
-        if ($this->arg('accept')) {
-            if (!$this->authorizeToken($params)) {
-                $this->clientError(_('Error authorizing token'));
-            }
-            if (!$this->saveRemoteProfile($params)) {
-                $this->clientError(_('Error saving remote profile'));
-            }
-            if (!$callback) {
-                $this->showAcceptMessage($params['oauth_token']);
-            } else {
-                $newparams = array();
-                $newparams['oauth_token'] = $params['oauth_token'];
-                $newparams['omb_version'] = OMB_VERSION_01;
-                $user = User::staticGet('uri', $params['omb_listener']);
-                $profile = $user->getProfile();
-                if (!$profile) {
-                    common_log_db_error($user, 'SELECT', __FILE__);
-                    $this->serverError(_('User without matching profile'));
-                    return;
-                }
-                $newparams['omb_listener_nickname'] = $user->nickname;
-                $newparams['omb_listener_profile'] = common_local_url('showstream',
-                                                                   array('nickname' => $user->nickname));
-                if (!is_null($profile->fullname)) {
-                    $newparams['omb_listener_fullname'] = $profile->fullname;
-                }
-                if (!is_null($profile->homepage)) {
-                    $newparams['omb_listener_homepage'] = $profile->homepage;
-                }
-                if (!is_null($profile->bio)) {
-                    $newparams['omb_listener_bio'] = $profile->bio;
-                }
-                if (!is_null($profile->location)) {
-                    $newparams['omb_listener_location'] = $profile->location;
-                }
-                $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
-                if ($avatar) {
-                    $newparams['omb_listener_avatar'] = $avatar->url;
-                }
-                $parts = array();
-                foreach ($newparams as $k => $v) {
-                    $parts[] = $k . '=' . OAuthUtil::urlencode_rfc3986($v);
-                }
-                $query_string = implode('&', $parts);
-                $parsed = parse_url($callback);
-                $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
-                common_redirect($url, 303);
-            }
-        } else {
-            if (!$callback) {
-                $this->showRejectMessage();
-            } else {
-                # XXX: not 100% sure how to signal failure... just redirect without token?
-                common_redirect($callback, 303);
-            }
-        }
-    }
-
-    function authorizeToken(&$params)
-    {
-        $token_field = $params['oauth_token'];
-        $rt = new Token();
-        $rt->tok = $token_field;
-        $rt->type = 0;
-        $rt->state = 0;
-        if ($rt->find(true)) {
-            $orig_rt = clone($rt);
-            $rt->state = 1; # Authorized but not used
-            if ($rt->update($orig_rt)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    # XXX: refactor with similar code in finishremotesubscribe.php
-
-    function saveRemoteProfile(&$params)
-    {
-        # FIXME: we should really do this when the consumer comes
-        # back for an access token. If they never do, we've got stuff in a
-        # weird state.
-
-        $nickname = $params['omb_listenee_nickname'];
-        $fullname = $params['omb_listenee_fullname'];
-        $profile_url = $params['omb_listenee_profile'];
-        $homepage = $params['omb_listenee_homepage'];
-        $bio = $params['omb_listenee_bio'];
-        $location = $params['omb_listenee_location'];
-        $avatar_url = $params['omb_listenee_avatar'];
-
-        $listenee = $params['omb_listenee'];
-        $remote = Remote_profile::staticGet('uri', $listenee);
-
-        if ($remote) {
-            $exists = true;
-            $profile = Profile::staticGet($remote->id);
-            $orig_remote = clone($remote);
-            $orig_profile = clone($profile);
-        } else {
-            $exists = false;
-            $remote = new Remote_profile();
-            $remote->uri = $listenee;
-            $profile = new Profile();
-        }
-
-        $profile->nickname = $nickname;
-        $profile->profileurl = $profile_url;
-
-        if (!is_null($fullname)) {
-            $profile->fullname = $fullname;
-        }
-        if (!is_null($homepage)) {
-            $profile->homepage = $homepage;
-        }
-        if (!is_null($bio)) {
-            $profile->bio = $bio;
-        }
-        if (!is_null($location)) {
-            $profile->location = $location;
-        }
-
-        if ($exists) {
-            $profile->update($orig_profile);
-        } else {
-            $profile->created = DB_DataObject_Cast::dateTime(); # current time
-            $id = $profile->insert();
-            if (!$id) {
-                return false;
-            }
-            $remote->id = $id;
+        $accepted = $this->arg('accept');
+        try {
+            list($val, $token) = $srv->continueUserAuth($accepted);
+        } catch (Exception $e) {
+            $this->clientError($e->getMessage());
+            return;
         }
-
-        if ($exists) {
-            if (!$remote->update($orig_remote)) {
-                return false;
-            }
+        if ($val !== false) {
+            common_redirect($val, 303);
+        } elseif ($accepted) {
+            $this->showAcceptMessage($token);
         } else {
-            $remote->created = DB_DataObject_Cast::dateTime(); # current time
-            if (!$remote->insert()) {
-                return false;
-            }
-        }
-
-        if ($avatar_url) {
-            if (!$this->addAvatar($profile, $avatar_url)) {
-                return false;
-            }
-        }
-
-        $user = common_current_user();
-
-        $sub = new Subscription();
-        $sub->subscriber = $user->id;
-        $sub->subscribed = $remote->id;
-        $sub->token = $params['oauth_token']; # NOTE: request token, not valid for use!
-        $sub->created = DB_DataObject_Cast::dateTime(); # current time
-
-        if (!$sub->insert()) {
-            return false;
+            $this->showRejectMessage();
         }
-
-        return true;
-    }
-
-    function addAvatar($profile, $url)
-    {
-        $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
-        copy($url, $temp_filename);
-        $imagefile = new ImageFile($profile->id, $temp_filename);
-        $filename = Avatar::filename($profile->id,
-                                     image_type_to_extension($imagefile->type),
-                                     null,
-                                     common_timestamp());
-        rename($temp_filename, Avatar::path($filename));
-        return $profile->setOriginal($filename);
     }
 
     function showAcceptMessage($tok)
@@ -378,26 +253,28 @@ class UserauthorizationAction extends Action
         common_show_header(_('Subscription authorized'));
         $this->element('p', null,
                        _('The subscription has been authorized, but no '.
-                         'callback URL was passed. Check with the site\'s instructions for '.
-                         'details on how to authorize the subscription. Your subscription token is:'));
+                         'callback URL was passed. Check with the site’s ' .
+                         'instructions for details on how to authorize the ' .
+                         'subscription. Your subscription token is:'));
         $this->element('blockquote', 'token', $tok);
         common_show_footer();
     }
 
-    function showRejectMessage($tok)
+    function showRejectMessage()
     {
         common_show_header(_('Subscription rejected'));
         $this->element('p', null,
                        _('The subscription has been rejected, but no '.
-                         'callback URL was passed. Check with the site\'s instructions for '.
-                         'details on how to fully reject the subscription.'));
+                         'callback URL was passed. Check with the site’s ' .
+                         'instructions for details on how to fully reject ' .
+                         'the subscription.'));
         common_show_footer();
     }
 
     function storeParams($params)
     {
         common_ensure_session();
-        $_SESSION['userauthorizationparams'] = $params;
+        $_SESSION['userauthorizationparams'] = serialize($params);
     }
 
     function clearParams()
@@ -409,138 +286,74 @@ class UserauthorizationAction extends Action
     function getStoredParams()
     {
         common_ensure_session();
-        $params = $_SESSION['userauthorizationparams'];
+        $params = unserialize($_SESSION['userauthorizationparams']);
         return $params;
     }
 
-    # Throws an OAuthException if anything goes wrong
-
-    function validateRequest()
-    {
-        /* Find token.
-           TODO: If no token is passed the user should get a prompt to enter it
-                 according to OAuth Core 1.0 */
-        $t = new Token();
-        $t->tok = $_GET['oauth_token'];
-        $t->type = 0;
-        if (!$t->find(true)) {
-            throw new OAuthException("Invalid request token: " . $_GET['oauth_token']);
-        }
-
-        $this->validateOmb();
-        return true;
-    }
-
     function validateOmb()
     {
-        foreach (array('omb_version', 'omb_listener', 'omb_listenee',
-                       'omb_listenee_profile', 'omb_listenee_nickname',
-                       'omb_listenee_license') as $param)
-        {
-            if (!isset($_GET[$param]) || is_null($_GET[$param])) {
-                throw new OAuthException("Required parameter '$param' not found");
-            }
-        }
-        # Now, OMB stuff
-        $version = $_GET['omb_version'];
-        if ($version != OMB_VERSION_01) {
-            throw new OAuthException("OpenMicroBlogging version '$version' not supported");
-        }
         $listener = $_GET['omb_listener'];
+        $listenee = $_GET['omb_listenee'];
+        $nickname = $_GET['omb_listenee_nickname'];
+        $profile  = $_GET['omb_listenee_profile'];
+
         $user = User::staticGet('uri', $listener);
         if (!$user) {
-            throw new OAuthException("Listener URI '$listener' not found here");
-        }
-        $cur = common_current_user();
-        if ($cur->id != $user->id) {
-            throw new OAuthException("Can't add for another user!");
-        }
-        $listenee = $_GET['omb_listenee'];
-        if (!Validate::uri($listenee) &&
-            !common_valid_tag($listenee)) {
-            throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
+            throw new Exception(sprintf(_('Listener URI ‘%s’ not found here'),
+                                        $listener));
         }
+
         if (strlen($listenee) > 255) {
-            throw new OAuthException("Listenee URI '$listenee' too long");
+            throw new Exception(sprintf(_('Listenee URI ‘%s’ is too long.'),
+                                        $listenee));
         }
 
         $other = User::staticGet('uri', $listenee);
         if ($other) {
-            throw new OAuthException("Listenee URI '$listenee' is local user");
+            throw new Exception(sprintf(_('Listenee URI ‘%s’ is a local user.'),
+                                        $listenee));
         }
 
         $remote = Remote_profile::staticGet('uri', $listenee);
         if ($remote) {
-            $sub = new Subscription();
+            $sub             = new Subscription();
             $sub->subscriber = $user->id;
             $sub->subscribed = $remote->id;
             if ($sub->find(true)) {
-                throw new OAuthException("Already subscribed to user!");
+                throw new Exception('You are already subscribed to this user.');
             }
         }
-        $nickname = $_GET['omb_listenee_nickname'];
-        if (!Validate::string($nickname, array('min_length' => 1,
-                                               'max_length' => 64,
-                                               'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
-            throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
-        }
-        $profile = $_GET['omb_listenee_profile'];
-        if (!common_valid_http_url($profile)) {
-            throw new OAuthException("Invalid profile URL '$profile'.");
-        }
 
-        if ($profile == common_local_url('showstream', array('nickname' => $nickname))) {
-            throw new OAuthException("Profile URL '$profile' is for a local user.");
-        }
+        if ($profile == common_profile_url($nickname)) {
+            throw new Exception(sprintf(_('Profile URL ‘%s’ is for a local user.'),
+                                        $profile));
 
-        $license = $_GET['omb_listenee_license'];
-        if (!common_valid_http_url($license)) {
-            throw new OAuthException("Invalid license URL '$license'.");
         }
+
+        $license      = $_GET['omb_listenee_license'];
         $site_license = common_config('license', 'url');
         if (!common_compatible_license($license, $site_license)) {
-            throw new OAuthException("Listenee stream license '$license' not compatible with site license '$site_license'.");
-        }
-        # optional stuff
-        $fullname = $_GET['omb_listenee_fullname'];
-        if ($fullname && mb_strlen($fullname) > 255) {
-            throw new OAuthException("Full name '$fullname' too long.");
-        }
-        $homepage = $_GET['omb_listenee_homepage'];
-        if ($homepage && (!common_valid_http_url($homepage) || mb_strlen($homepage) > 255)) {
-            throw new OAuthException("Invalid homepage '$homepage'");
-        }
-        $bio = $_GET['omb_listenee_bio'];
-        if ($bio && mb_strlen($bio) > 140) {
-            throw new OAuthException("Bio too long '$bio'");
-        }
-        $location = $_GET['omb_listenee_location'];
-        if ($location && mb_strlen($location) > 255) {
-            throw new OAuthException("Location too long '$location'");
+            throw new Exception(sprintf(_('Listenee stream license ‘%s’ is not ' .
+                                          'compatible with site license ‘%s’.'),
+                                        $license, $site_license));
         }
+
         $avatar = $_GET['omb_listenee_avatar'];
         if ($avatar) {
             if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
-                throw new OAuthException("Invalid avatar URL '$avatar'");
+                throw new Exception(sprintf(_('Avatar URL ‘%s’ is not valid.'),
+                                            $avatar));
             }
             $size = @getimagesize($avatar);
             if (!$size) {
-                throw new OAuthException("Can't read avatar URL '$avatar'");
-            }
-            if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
-                throw new OAuthException("Wrong size image at '$avatar'");
+                throw new Exception(sprintf(_('Can’t read avatar URL ‘%s’.'),
+                                            $avatar));
             }
             if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
                                           IMAGETYPE_PNG))) {
-                throw new OAuthException("Wrong image type for '$avatar'");
+                throw new Exception(sprintf(_('Wrong image type for avatar URL '.
+                                              '‘%s’.'), $avatar));
             }
         }
-        $callback = $_GET['oauth_callback'];
-        if ($callback && !common_valid_http_url($callback)) {
-            throw new OAuthException("Invalid callback URL '$callback'");
-        }
-        if ($callback && $callback == common_local_url('finishremotesubscribe')) {
-            throw new OAuthException("Callback URL '$callback' is for local site.");
-        }
     }
 }
index 8a940865f947f872e562faad8deb0bc2cb7d5fac..a9f3fd5f89e4103d69bb38d0f4f44894fec67ae9 100644 (file)
@@ -88,9 +88,10 @@ class UserrssAction extends Rss10Action
         $c = array('url' => common_local_url('userrss',
                                              array('nickname' =>
                                                    $user->nickname)),
-                   'title' => $user->nickname,
+                   'title' => sprintf(_('%s timeline'), $user->nickname),
                    'link' => $profile->profileurl,
-                   'description' => sprintf(_('Microblog by %s'), $user->nickname));
+                   'description' => sprintf(_('Updates from %1$s on %2$s!'),
+                                            $user->nickname, common_config('site', 'name')));
         return $c;
     }
 
index 3c752188408955a92076dc44b8bc716a05bd16c4..b3aa8df8e57499bda8e5f18978fcff8ea4ed0e58 100644 (file)
@@ -34,6 +34,8 @@ if (!defined('LACONICA')) {
 }
 
 require_once INSTALLDIR.'/lib/omb.php';
+require_once INSTALLDIR.'/extlib/libomb/service_provider.php';
+require_once INSTALLDIR.'/extlib/libomb/xrds_mapper.php';
 
 /**
  * XRDS for OpenMicroBlogging
@@ -52,7 +54,7 @@ class XrdsAction extends Action
      *
      * @return boolean true
      */
-    function isReadOnly($args)
+    function isReadOnly()
     {
         return true;
     }
@@ -85,89 +87,31 @@ class XrdsAction extends Action
      */
     function showXrds($user)
     {
-        header('Content-Type: application/xrds+xml');
-        $this->startXML();
-        $this->elementStart('XRDS', array('xmlns' => 'xri://$xrds'));
+        $srv = new OMB_Service_Provider(profile_to_omb_profile($user->uri,
+                                        $user->getProfile()));
+        /* Use libomb’s default XRDS Writer. */
+        $xrds_writer = null;
+        $srv->writeXRDS(new Laconica_XRDS_Mapper(), $xrds_writer);
+    }
+}
 
-        $this->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)',
-                                          'xml:id' => 'oauth',
-                                          'xmlns:simple' => 'http://xrds-simple.net/core/1.0',
-                                          'version' => '2.0'));
-        $this->element('Type', null, 'xri://$xrds*simple');
-        $this->showService(OAUTH_ENDPOINT_REQUEST,
-                            common_local_url('requesttoken'),
-                            array(OAUTH_AUTH_HEADER, OAUTH_POST_BODY),
-                            array(OAUTH_HMAC_SHA1),
-                            $user->uri);
-        $this->showService(OAUTH_ENDPOINT_AUTHORIZE,
-                            common_local_url('userauthorization'),
-                            array(OAUTH_AUTH_HEADER, OAUTH_POST_BODY),
-                            array(OAUTH_HMAC_SHA1));
-        $this->showService(OAUTH_ENDPOINT_ACCESS,
-                            common_local_url('accesstoken'),
-                            array(OAUTH_AUTH_HEADER, OAUTH_POST_BODY),
-                            array(OAUTH_HMAC_SHA1));
-        $this->showService(OAUTH_ENDPOINT_RESOURCE,
-                            null,
-                            array(OAUTH_AUTH_HEADER, OAUTH_POST_BODY),
-                            array(OAUTH_HMAC_SHA1));
-        $this->elementEnd('XRD');
+class Laconica_XRDS_Mapper implements OMB_XRDS_Mapper
+{
+    protected $urls;
 
-        // XXX: decide whether to include user's ID/nickname in postNotice URL
-        $this->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)',
-                                          'xml:id' => 'omb',
-                                          'xmlns:simple' => 'http://xrds-simple.net/core/1.0',
-                                          'version' => '2.0'));
-        $this->element('Type', null, 'xri://$xrds*simple');
-        $this->showService(OMB_ENDPOINT_POSTNOTICE,
-                            common_local_url('postnotice'));
-        $this->showService(OMB_ENDPOINT_UPDATEPROFILE,
-                            common_local_url('updateprofile'));
-        $this->elementEnd('XRD');
-        $this->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)',
-                                          'version' => '2.0'));
-        $this->element('Type', null, 'xri://$xrds*simple');
-        $this->showService(OAUTH_DISCOVERY,
-                            '#oauth');
-        $this->showService(OMB_NAMESPACE,
-                            '#omb');
-        $this->elementEnd('XRD');
-        $this->elementEnd('XRDS');
-        $this->endXML();
+    public function __construct()
+    {
+        $this->urls = array(
+            OAUTH_ENDPOINT_REQUEST => 'requesttoken',
+            OAUTH_ENDPOINT_AUTHORIZE => 'userauthorization',
+            OAUTH_ENDPOINT_ACCESS => 'accesstoken',
+            OMB_ENDPOINT_POSTNOTICE => 'postnotice',
+            OMB_ENDPOINT_UPDATEPROFILE => 'updateprofile');
     }
 
-    /**
-     * Show service.
-     *
-     * @param string $type    XRDS type
-     * @param string $uri     URI
-     * @param array  $params  type parameters, null by default
-     * @param array  $sigs    type signatures, null by default
-     * @param string $localId local ID, null by default
-     *
-     * @return void
-     */
-    function showService($type, $uri, $params=null, $sigs=null, $localId=null)
+    public function getURL($action)
     {
-        $this->elementStart('Service');
-        if ($uri) {
-            $this->element('URI', null, $uri);
-        }
-        $this->element('Type', null, $type);
-        if ($params) {
-            foreach ($params as $param) {
-                $this->element('Type', null, $param);
-            }
-        }
-        if ($sigs) {
-            foreach ($sigs as $sig) {
-                $this->element('Type', null, $sig);
-            }
-        }
-        if ($localId) {
-            $this->element('LocalID', null, $localId);
-        }
-        $this->elementEnd('Service');
+        return common_local_url($this->urls[$action]);
     }
 }
-
+?>
diff --git a/classes/Config.php b/classes/Config.php
new file mode 100755 (executable)
index 0000000..5bec13f
--- /dev/null
@@ -0,0 +1,129 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, Control Yourself, 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('LACONICA')) { exit(1); }
+
+/**
+ * Table Definition for config
+ */
+
+require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
+
+class Config extends Memcached_DataObject
+{
+    ###START_AUTOCODE
+    /* the code below is auto generated do not remove the above tag */
+
+    public $__table = 'config';                          // table name
+    public $section;                         // varchar(32)  primary_key not_null
+    public $setting;                         // varchar(32)  primary_key not_null
+    public $value;                           // varchar(255)
+
+    /* Static get */
+    function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Config',$k,$v); }
+
+    /* the code above is auto generated do not remove the tag below */
+    ###END_AUTOCODE
+
+    const settingsKey = 'config:settings';
+
+    static function loadSettings()
+    {
+        $settings = self::_getSettings();
+        if (!empty($settings)) {
+            self::_applySettings($settings);
+        }
+    }
+
+    static function _getSettings()
+    {
+        $c = self::memcache();
+
+        if (!empty($c)) {
+            $settings = $c->get(common_cache_key(self::settingsKey));
+            if (!empty($settings)) {
+                return $settings;
+            }
+        }
+
+        $settings = array();
+
+        $config = new Config();
+
+        $config->find();
+
+        while ($config->fetch()) {
+            $settings[] = array($config->section, $config->setting, $config->value);
+        }
+
+        $config->free();
+
+        if (!empty($c)) {
+            $c->set(common_cache_key(self::settingsKey), $settings);
+        }
+
+        return $settings;
+    }
+
+    static function _applySettings($settings)
+    {
+        global $config;
+
+        foreach ($settings as $s) {
+            list($section, $setting, $value) = $s;
+            $config[$section][$setting] = $value;
+        }
+    }
+
+    function insert()
+    {
+        $result = parent::insert();
+        if ($result) {
+            Config::_blowSettingsCache();
+        }
+        return $result;
+    }
+
+    function delete()
+    {
+        $result = parent::delete();
+        if ($result) {
+            Config::_blowSettingsCache();
+        }
+        return $result;
+    }
+
+    function update($orig=null)
+    {
+        $result = parent::update($orig);
+        if ($result) {
+            Config::_blowSettingsCache();
+        }
+        return $result;
+    }
+
+    function _blowSettingsCache()
+    {
+        $c = self::memcache();
+
+        if (!empty($c)) {
+            $c->delete(common_cache_key(self::settingsKey));
+        }
+    }
+}
index 43544f1c9d54e8bcf2cb37fb56103960335c6717..19c9e0292494e9242702370f263b1a86df6e5ec0 100644 (file)
@@ -107,7 +107,7 @@ class Design extends Memcached_DataObject
 
     static function toWebColor($color)
     {
-        if (is_null($color)) {
+        if ($color == null) {
             return null;
         }
 
@@ -115,7 +115,7 @@ class Design extends Memcached_DataObject
             return new WebColor($color);
         } catch (WebColorException $e) {
             // This shouldn't happen
-            common_log(LOG_ERR, "Unable to create color for design $id.",
+            common_log(LOG_ERR, "Unable to create web color for $color",
                 __FILE__);
             return null;
         }
@@ -204,7 +204,10 @@ class Design extends Memcached_DataObject
                            'disposition');
 
             foreach ($attrs as $attr) {
-                $siteDesign->$attr = common_config('design', $attr);
+                $val = common_config('design', $attr);
+                if ($val !== false) {
+                    $siteDesign->$attr = $val;
+                }
             }
         }
 
index 959301edaeff30672bd716326c4aae1420bda476..b2c510340df11d99d7949d05f15451a010da33c2 100644 (file)
@@ -95,7 +95,8 @@ class File extends Memcached_DataObject
             if (empty($file_redir)) {
                 $redir_data = File_redirection::where($given_url);
                 $redir_url = $redir_data['url'];
-                if ($redir_url === $given_url) {
+                // TODO: max field length
+                if ($redir_url === $given_url || strlen($redir_url) > 255) { 
                     $x = File::saveNew($redir_data, $given_url);
                     $file_id = $x->id;
                 } else {
index c0b356ecedd10473e13447d7a14157b8388dc63f..ae8c22fd84742b6b0c48ece953f364a87d3f7993 100644 (file)
@@ -29,34 +29,38 @@ class Foreign_link extends Memcached_DataObject
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
 
-    // XXX:  This only returns a 1->1 single obj mapping.  Change?  Or make
-    // a getForeignUsers() that returns more than one? --Zach
     static function getByUserID($user_id, $service)
     {
+        if (empty($user_id) || empty($service)) {
+            return null;
+        }
+
         $flink = new Foreign_link();
+
         $flink->service = $service;
         $flink->user_id = $user_id;
         $flink->limit(1);
 
-        if ($flink->find(true)) {
-            return $flink;
-        }
+        $result = $flink->find(true);
+
+        return empty($result) ? null : $flink;
 
-        return null;
     }
 
     static function getByForeignID($foreign_id, $service)
     {
-        $flink = new Foreign_link();
-        $flink->service = $service;
-        $flink->foreign_id = $foreign_id;
-        $flink->limit(1);
+        if (empty($foreign_id) || empty($service)) {
+            return null;
+        } else {
+            $flink = new Foreign_link();
+            $flink->service = $service;
+            $flink->foreign_id = $foreign_id;
+            $flink->limit(1);
 
-        if ($flink->find(true)) {
-            return $flink;
-        }
+            $result = $flink->find(true);
 
-        return null;
+            return empty($result) ? null : $flink;
+        }
     }
 
     function set_flags($noticesend, $noticerecv, $replysync, $friendsync)
@@ -66,7 +70,7 @@ class Foreign_link extends Memcached_DataObject
         } else {
             $this->noticesync &= ~FOREIGN_NOTICE_SEND;
         }
-        
+
         if ($noticerecv) {
             $this->noticesync |= FOREIGN_NOTICE_RECV;
         } else {
index 4806057b4ca1183fa6c752fde2e0ce03c4f20ee0..979e6e87ccbdcb4d79de3c0fec07a19a239a6812 100644 (file)
@@ -4,7 +4,7 @@
  */
 require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
 
-class Message extends Memcached_DataObject 
+class Message extends Memcached_DataObject
 {
     ###START_AUTOCODE
     /* the code below is auto generated do not remove the above tag */
@@ -14,58 +14,73 @@ class Message extends Memcached_DataObject
     public $uri;                             // varchar(255)  unique_key
     public $from_profile;                    // int(4)   not_null
     public $to_profile;                      // int(4)   not_null
-    public $content;                         // varchar(140)  
-    public $rendered;                        // text()  
-    public $url;                             // varchar(255)  
+    public $content;                         // text()
+    public $rendered;                        // text()
+    public $url;                             // varchar(255)
     public $created;                         // datetime()   not_null
     public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP
-    public $source;                          // varchar(32)  
+    public $source;                          // varchar(32)
 
     /* Static get */
-    function staticGet($k,$v=null)
-    { return Memcached_DataObject::staticGet('Message',$k,$v); }
+    function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Message',$k,$v); }
 
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
-    
+
     function getFrom()
     {
         return Profile::staticGet('id', $this->from_profile);
     }
-    
+
     function getTo()
     {
         return Profile::staticGet('id', $this->to_profile);
     }
-    
+
     static function saveNew($from, $to, $content, $source) {
-        
+
         $msg = new Message();
-        
+
         $msg->from_profile = $from;
         $msg->to_profile = $to;
         $msg->content = common_shorten_links($content);
         $msg->rendered = common_render_text($content);
         $msg->created = common_sql_now();
         $msg->source = $source;
-        
+
         $result = $msg->insert();
-        
+
         if (!$result) {
             common_log_db_error($msg, 'INSERT', __FILE__);
             return _('Could not insert message.');
         }
-        
+
         $orig = clone($msg);
         $msg->uri = common_local_url('showmessage', array('message' => $msg->id));
-        
+
         $result = $msg->update($orig);
-        
+
         if (!$result) {
             common_log_db_error($msg, 'UPDATE', __FILE__);
             return _('Could not update message with new URI.');
         }
-        
+
         return $msg;
     }
+
+    static function maxContent()
+    {
+        $desclimit = common_config('message', 'contentlimit');
+        // null => use global limit (distinct from 0!)
+        if (is_null($desclimit)) {
+            $desclimit = common_config('site', 'textlimit');
+        }
+        return $desclimit;
+    }
+
+    static function contentTooLong($content)
+    {
+        $contentlimit = self::maxContent();
+        return ($contentlimit > 0 && !empty($content) && (mb_strlen($content) > $contentlimit));
+    }
 }
index 9578d87b2b149925e8fefffc17c43abd3b3c75b3..48d4a094029f942346551e77265b191d181935b1 100644 (file)
@@ -29,10 +29,6 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
 
 define('NOTICE_CACHE_WINDOW', 61);
 
-define('NOTICE_LOCAL_PUBLIC', 1);
-define('NOTICE_REMOTE_OMB', 0);
-define('NOTICE_LOCAL_NONPUBLIC', -1);
-
 define('MAX_BOXCARS', 128);
 
 class Notice extends Memcached_DataObject
@@ -44,7 +40,7 @@ class Notice extends Memcached_DataObject
     public $id;                              // int(4)  primary_key not_null
     public $profile_id;                      // int(4)   not_null
     public $uri;                             // varchar(255)  unique_key
-    public $content;                         // varchar(140)
+    public $content;                         // text()
     public $rendered;                        // text()
     public $url;                             // varchar(255)
     public $created;                         // datetime()   not_null
@@ -55,14 +51,16 @@ class Notice extends Memcached_DataObject
     public $conversation;                    // int(4)
 
     /* Static get */
-    function staticGet($k,$v=NULL) {
-        return Memcached_DataObject::staticGet('Notice',$k,$v);
-    }
+    function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Notice',$k,$v); }
 
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
 
-    const GATEWAY = -2;
+    /* Notice types */ 
+    const LOCAL_PUBLIC    =  1;
+    const REMOTE_OMB      =  0;
+    const LOCAL_NONPUBLIC = -1;
+    const GATEWAY         = -2;
 
     function getProfile()
     {
@@ -148,13 +146,13 @@ class Notice extends Memcached_DataObject
     }
 
     static function saveNew($profile_id, $content, $source=null,
-                            $is_local=1, $reply_to=null, $uri=null, $created=null) {
+                            $is_local=Notice::LOCAL_PUBLIC, $reply_to=null, $uri=null, $created=null) {
 
         $profile = Profile::staticGet($profile_id);
 
         $final = common_shorten_links($content);
 
-        if (mb_strlen($final) > 140) {
+        if (Notice::contentTooLong($final)) {
             common_log(LOG_INFO, 'Rejecting notice that is too long.');
             return _('Problem saving notice. Too long.');
         }
@@ -191,7 +189,7 @@ class Notice extends Memcached_DataObject
 
         if (($blacklist && in_array($profile_id, $blacklist)) ||
             ($source && $autosource && in_array($source, $autosource))) {
-            $notice->is_local = -1;
+            $notice->is_local = Notice::LOCAL_NONPUBLIC;
         } else {
             $notice->is_local = $is_local;
         }
@@ -523,7 +521,7 @@ class Notice extends Memcached_DataObject
 
     function blowPublicCache($blowLast=false)
     {
-        if ($this->is_local == 1) {
+        if ($this->is_local == Notice::LOCAL_PUBLIC) {
             $cache = common_memcache();
             if ($cache) {
                 $cache->delete(common_cache_key('public'));
@@ -789,10 +787,11 @@ class Notice extends Memcached_DataObject
         }
 
         if (common_config('public', 'localonly')) {
-            $notice->whereAdd('is_local = 1');
+            $notice->whereAdd('is_local = ' . Notice::LOCAL_PUBLIC);
         } else {
-            # -1 == blacklisted
-            $notice->whereAdd('is_local != -1');
+            # -1 == blacklisted, -2 == gateway (i.e. Twitter)
+            $notice->whereAdd('is_local !='. Notice::LOCAL_NONPUBLIC);
+            $notice->whereAdd('is_local !='. Notice::GATEWAY);
         }
 
         if ($since_id != 0) {
@@ -1354,4 +1353,20 @@ class Notice extends Memcached_DataObject
             return $last->id;
         }
     }
+
+    static function maxContent()
+    {
+        $contentlimit = common_config('notice', 'contentlimit');
+        // null => use global limit (distinct from 0!)
+        if (is_null($contentlimit)) {
+            $contentlimit = common_config('site', 'textlimit');
+        }
+        return $contentlimit;
+    }
+
+    static function contentTooLong($content)
+    {
+        $contentlimit = self::maxContent();
+        return ($contentlimit > 0 && !empty($content) && (mb_strlen($content) > $contentlimit));
+    }
 }
index f926b2cef203bbe9210d7dcad3707fd8fdee6da1..8f92b386e963b1167c8326085e6e81c01c2646d9 100644 (file)
@@ -35,14 +35,13 @@ class Profile extends Memcached_DataObject
     public $fullname;                        // varchar(255)  multiple_key
     public $profileurl;                      // varchar(255)
     public $homepage;                        // varchar(255)  multiple_key
-    public $bio;                             // varchar(140)  multiple_key
+    public $bio;                             // text()  multiple_key
     public $location;                        // varchar(255)  multiple_key
     public $created;                         // datetime()   not_null
     public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP
 
     /* Static get */
-    function staticGet($k,$v=null)
-    { return Memcached_DataObject::staticGet('Profile',$k,$v); }
+    function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Profile',$k,$v); }
 
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
@@ -461,4 +460,20 @@ class Profile extends Memcached_DataObject
             $c->delete(common_cache_key('profile:notice_count:'.$this->id));
         }
     }
+
+    static function maxBio()
+    {
+        $biolimit = common_config('profile', 'biolimit');
+        // null => use global limit (distinct from 0!)
+        if (is_null($biolimit)) {
+            $biolimit = common_config('site', 'textlimit');
+        }
+        return $biolimit;
+    }
+
+    static function bioTooLong($bio)
+    {
+        $biolimit = self::maxBio();
+        return ($biolimit > 0 && !empty($bio) && (mb_strlen($bio) > $biolimit));
+    }
 }
index b1ab1c2d313c6537f4fc0db0f01a16b51dfc6c24..310ecff1ef766726197eaa06457b6a3c5cfcc405 100644 (file)
@@ -13,7 +13,7 @@ class User_group extends Memcached_DataObject
     public $nickname;                        // varchar(64)  unique_key
     public $fullname;                        // varchar(255)
     public $homepage;                        // varchar(255)
-    public $description;                     // varchar(140)
+    public $description;                     // text()
     public $location;                        // varchar(255)
     public $original_logo;                   // varchar(255)
     public $homepage_logo;                   // varchar(255)
@@ -297,4 +297,61 @@ class User_group extends Memcached_DataObject
 
         return $ids;
     }
+
+    static function maxDescription()
+    {
+        $desclimit = common_config('group', 'desclimit');
+        // null => use global limit (distinct from 0!)
+        if (is_null($desclimit)) {
+            $desclimit = common_config('site', 'textlimit');
+        }
+        return $desclimit;
+    }
+
+    static function descriptionTooLong($desc)
+    {
+        $desclimit = self::maxDescription();
+        return ($desclimit > 0 && !empty($desc) && (mb_strlen($desc) > $desclimit));
+    }
+
+    function asAtomEntry($namespace=false, $source=false)
+    {
+        $xs = new XMLStringer(true);
+
+        if ($namespace) {
+            $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
+                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0');
+        } else {
+            $attrs = array();
+        }
+
+        $xs->elementStart('entry', $attrs);
+
+        if ($source) {
+            $xs->elementStart('source');
+            $xs->element('title', null, $profile->nickname . " - " . common_config('site', 'name'));
+            $xs->element('link', array('href' => $this->permalink()));
+        }
+
+        if ($source) {
+            $xs->elementEnd('source');
+        }
+
+        $xs->element('title', null, $this->nickname);
+        $xs->element('summary', null, $this->description);
+
+        $xs->element('link', array('rel' => 'alternate',
+                                   'href' => $this->permalink()));
+
+        $xs->element('id', null, $this->permalink());
+
+        $xs->element('published', null, common_date_w3dtf($this->created));
+        $xs->element('updated', null, common_date_w3dtf($this->modified));
+
+        $xs->element('content', array('type' => 'html'), $this->description);
+
+        $xs->elementEnd('entry');
+
+        return $xs->getString();
+    }
 }
index f8d4eebd3f6e60940fa465eef559c7d46877f818..7edeeebe4fa3ec7b0e49ef05615f61b63feee1e7 100755 (executable)
@@ -16,6 +16,15 @@ width = K
 height = K
 url = U
 
+[config]
+section = 130
+setting = 130
+value = 2
+
+[config__keys]
+section = K
+setting = K
+
 [confirm_address]
 code = 130
 user_id = 129
@@ -239,7 +248,7 @@ id = 129
 uri = 2
 from_profile = 129
 to_profile = 129
-content = 2
+content = 34
 rendered = 34
 url = 2
 created = 142
@@ -266,7 +275,7 @@ ts = K
 id = 129
 profile_id = 129
 uri = 2
-content = 2
+content = 34
 rendered = 34
 url = 2
 created = 142
@@ -314,7 +323,7 @@ nickname = 130
 fullname = 2
 profileurl = 2
 homepage = 2
-bio = 2
+bio = 34
 location = 2
 created = 142
 modified = 384
@@ -486,7 +495,7 @@ id = 129
 nickname = 2
 fullname = 2
 homepage = 2
-description = 2
+description = 34
 location = 2
 original_logo = 2
 homepage_logo = 2
index c27645ff874cb98863d1be99fd728344f62edddc..0fc5163b7af4a26e2bda33d215dcbc77c9f2ded0 100644 (file)
@@ -38,6 +38,8 @@ $config['site']['path'] = 'laconica';
 // $config['site']['closed'] = true;
 // Only allow registration for people invited by another user
 // $config['site']['inviteonly'] = true;
+// Only allow registrations and logins through OpenID
+// $config['site']['openidonly'] = true;
 // Make the site invisible to  non-logged-in users
 // $config['site']['private'] = true;
 
@@ -97,6 +99,9 @@ $config['sphinx']['port'] = 3312;
 // $config['xmpp']['public'][] = 'someindexer@example.net';
 // $config['xmpp']['debug'] = false;
 
+// Disable OpenID
+// $config['openid']['enabled'] = false;
+
 // Turn off invites
 // $config['invite']['enabled'] = false;
 
@@ -164,6 +169,15 @@ $config['sphinx']['port'] = 3312;
 // $config['memcached']['server'] = 'localhost';
 // $config['memcached']['port'] = 11211;
 
+// Disable post-by-email
+// $config['emailpost']['enabled'] = false;
+
+// Disable SMS
+// $config['sms']['enabled'] = false;
+
+// Disable Twitter integration
+// $config['twitter']['enabled'] = false;
+
 // Twitter integration source attribute. Note: default is Laconica
 // $config['integration']['source'] = 'Laconica';
 
@@ -173,6 +187,10 @@ $config['sphinx']['port'] = 3312;
 //
 // $config['twitterbridge']['enabled'] = true;
 
+// Twitter OAuth settings
+// $config['twitter']['consumer_key']    = 'YOURKEY';
+// $config['twitter']['consumer_secret'] = 'YOURSECRET';
+
 // Edit throttling. Off by default. If turned on, you can only post 20 notices
 // every 10 minutes. Admins may want to play with the settings to minimize inconvenience for
 // real users without getting uncontrollable floods from spammers or runaway bots.
@@ -243,5 +261,6 @@ $config['sphinx']['port'] = 3312;
 // $config['attachments']['user_quota'] = 50000000;
 // $config['attachments']['monthly_quota'] = 15000000;
 // $config['attachments']['uploads'] = true;
+// $config['attachments']['path'] = "/file/";
 
 // $config['oohembed']['endpoint'] = 'http://oohembed.com/oohembed/';
index 892df4a39f626e72dd850aeb89929ad7221efef2..4d1830611a09fc56089ca9e15df47c648abfb2ee 100644 (file)
@@ -1,2 +1,12 @@
-// SQL commands to update an 0.8.x version of Laconica
-// to 0.9.x.
+alter table notice
+     modify column content text comment 'update content';
+
+alter table message
+     modify column content text comment 'message content';
+
+alter table profile
+     modify column bio text comment 'descriptive biography';
+
+alter table user_group
+     modify column description text comment 'group description';
+
diff --git a/db/08to09_pg.sql b/db/08to09_pg.sql
new file mode 100644 (file)
index 0000000..892df4a
--- /dev/null
@@ -0,0 +1,2 @@
+// SQL commands to update an 0.8.x version of Laconica
+// to 0.9.x.
index f0110717606304fbdf228fd9303edcf73670191a..1662ef7a8be6bc074c1c76b4a25937cd3955ad3d 100644 (file)
@@ -6,7 +6,7 @@ create table profile (
     fullname varchar(255) comment 'display name',
     profileurl varchar(255) comment 'URL, cached so we dont regenerate',
     homepage varchar(255) comment 'identifying URL',
-    bio varchar(140) comment 'descriptive biography',
+    bio text comment 'descriptive biography',
     location varchar(255) comment 'physical location',
     created datetime not null comment 'date this record was created',
     modified timestamp comment 'date this record was modified',
@@ -110,7 +110,7 @@ create table notice (
     id integer auto_increment primary key comment 'unique identifier',
     profile_id integer not null comment 'who made the update' references profile (id),
     uri varchar(255) unique key comment 'universally unique identifier, usually a tag URI',
-    content varchar(140) comment 'update content',
+    content text comment 'update content',
     rendered text comment 'HTML version of the content',
     url varchar(255) comment 'URL of any attachment (image, video, bookmark, whatever)',
     created datetime not null comment 'date this record was created',
@@ -331,7 +331,7 @@ create table message (
     uri varchar(255) unique key comment 'universally unique identifier',
     from_profile integer not null comment 'who the message is from' references profile (id),
     to_profile integer not null comment 'who the message is to' references profile (id),
-    content varchar(140) comment 'message content',
+    content text comment 'message content',
     rendered text comment 'HTML version of the content',
     url varchar(255) comment 'URL of any attachment (image, video, bookmark, whatever)',
     created datetime not null comment 'date this record was created',
@@ -380,7 +380,7 @@ create table user_group (
     nickname varchar(64) unique key comment 'nickname for addressing',
     fullname varchar(255) comment 'display name',
     homepage varchar(255) comment 'URL, cached so we dont regenerate',
-    description varchar(140) comment 'descriptive biography',
+    description text comment 'group description',
     location varchar(255) comment 'related physical location, if any',
 
     original_logo varchar(255) comment 'original size logo',
@@ -547,3 +547,13 @@ create table deleted_notice (
     index deleted_notice_profile_id_idx (profile_id)
 
 ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table config (
+
+    section varchar(32) comment 'configuration section',
+    setting varchar(32) comment 'configuration setting',
+    value varchar(255) comment 'configuration value',
+
+    constraint primary key (section, setting)
+
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
index 983ea915023ded6aeaae99a9049cfde4ec29b05e..f590d1b97a7fa2cd83017b309688e372b37c99d3 100644 (file)
@@ -22,6 +22,8 @@ VALUES
     ('IdentiFox','IdentiFox','http://www.bitbucket.org/uncryptic/identifox/', now()),
     ('identitwitch','IdentiTwitch','http://richfish.org/identitwitch/', now()),
     ('LaTwit','LaTwit','http://latwit.mac65.com/', now()),
+    ('LiveTweeter', 'LiveTweeter', 'http://addons.songbirdnest.com/addon/1204', now()),
+    ('livetweeter', 'livetweeter', 'http://addons.songbirdnest.com/addon/1204', now()),
     ('maisha', 'Maisha', 'http://maisha.grango.org/', now()),
     ('mbpidgin','mbpidgin','http://code.google.com/p/microblog-purple/', now()),
     ('Mobidentica', 'Mobidentica', 'http://www.substanceofcode.com/software/mobidentica/', now()),
@@ -34,6 +36,7 @@ VALUES
     ('pocketwit','PockeTwit','http://code.google.com/p/pocketwit/', now()),
     ('posty','Posty','http://spreadingfunkyness.com/posty/', now()),
     ('qtwitter','qTwitter','http://qtwitter.ayoy.net/', now()),
+    ('qwit', 'Qwit', 'http://code.google.com/p/qwit/', now()),
     ('royalewithcheese','Royale With Cheese','http://p.hellyeah.org/', now()),
     ('rssdent','rssdent','http://github.com/zcopley/rssdent/tree/master', now()),
     ('rygh.no','rygh.no','http://rygh.no/', now()),
index da07f9fe7b1f482afe9aa2598b5f1f6fe08ab440..c722a4e2cb8fcbf5ca59a4cdb2710ee05d13ec4b 100644 (file)
@@ -32,4 +32,15 @@ currently-implemented commands:
   you subscribe to.
 * **off**: Turn off notifications. You'll no longer receive Jabber
   notifications.
-
+* **stop**: Same as 'off'
+* **quit**: Same as 'off'
+* **help**: Show this help. List available Jabber/XMPP commands
+* **follow &lt;nickname&gt;**: Subscribe to &lt;nickname&gt;
+* **sub &lt;nickname&gt;**: Same as follow
+* **leave &lt;nickname&gt;**: Subscribe to &lt;nickname&gt;
+* **unsub &lt;nickname&gt;**: Same as leave
+* **d &lt;nickname&gt; &lt;text&gt;**: Send direct message to &lt;nickname&gt; with message body &lt;text&gt;
+* **get &lt;nickname&gt;**: Get last notice from &lt;nickname&gt;
+* **last &lt;nickname&gt;**: Same as 'get'
+* **whois &lt;nickname&gt;**: Get Profile info on &lt;nickname&gt;
+* **fav &lt;nickname&gt;**: Add user's last notice as a favorite
\ No newline at end of file
index 1beb49786d85a3d195f67385fdc6f599c63069d2..1a3064318fa7f6ce42dbd8814e08617a01505ea7 100644 (file)
@@ -44,24 +44,24 @@ You can use the following commands with %%site.name%%.
 * on - turn on notifications
 * off - turn off notifications
 * help - show this help
-* follow <nickname> - subscribe to user
-* leave <nickname> - unsubscribe from user
-* d <nickname> <text> - direct message to user
-* get <nickname> - get last notice from user
-* whois <nickname> - get profile info on user
-* fav <nickname> - add user's last notice as a 'fave'
+* follow &lt;nickname&gt; - subscribe to user
+* leave &lt;nickname&gt; - unsubscribe from user
+* d &lt;nickname&gt; &lt;text&gt; - direct message to user
+* get &lt;nickname&gt; - get last notice from user
+* whois &lt;nickname&gt; - get profile info on user
+* fav &lt;nickname&gt; - add user's last notice as a 'fave'
 * stats - get your stats
 * stop - same as 'off'
 * quit - same as 'off'
-* sub <nickname> - same as 'follow'
-* unsub <nickname> - same as 'leave'
-* last <nickname> - same as 'get'
-* on <nickname> - not yet implemented.
-* off <nickname> - not yet implemented.
-* nudge <nickname> - not yet implemented.
-* invite <phone number> - not yet implemented.
-* track <word> - not yet implemented.
-* untrack <word> - not yet implemented.
+* sub &lt;nickname&gt; - same as 'follow'
+* unsub &lt;nickname&gt; - same as 'leave'
+* last &lt;nickname&gt; - same as 'get'
+* on &lt;nickname&gt; - not yet implemented.
+* off &lt;nickname&gt; - not yet implemented.
+* nudge &lt;nickname&gt; - not yet implemented.
+* invite &lt;phone number&gt; - not yet implemented.
+* track &lt;word&gt; - not yet implemented.
+* untrack &lt;word&gt; - not yet implemented.
 * track off - not yet implemented.
 * untrack all - not yet implemented.
 * tracks - not yet implemented.
diff --git a/extlib/libomb/base_url_xrds_mapper.php b/extlib/libomb/base_url_xrds_mapper.php
new file mode 100755 (executable)
index 0000000..6454595
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+require_once 'xrds_mapper.php';
+require_once 'constants.php';
+
+/**
+ * Map XRDS actions to URLs using base URLs.
+ *
+ * This interface specifies classes which write the XRDS file announcing
+ * the OMB server. An instance of an implementing class should be passed to
+ * OMB_Service_Provider->writeXRDS.
+ *
+ * PHP version 5
+ *
+ * LICENSE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   OMB
+ * @author    Adrian Lang <mail@adrianlang.de>
+ * @copyright 2009 Adrian Lang
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
+ **/
+
+class OMB_Base_URL_XRDS_Mapper implements OMB_XRDS_Mapper {
+
+  protected $urls;
+
+  public function __construct($oauth_base, $omb_base) {
+    $this->urls = array(
+        OAUTH_ENDPOINT_REQUEST => $oauth_base . 'requesttoken',
+        OAUTH_ENDPOINT_AUTHORIZE => $oauth_base . 'userauthorization',
+        OAUTH_ENDPOINT_ACCESS => $oauth_base . 'accesstoken',
+        OMB_ENDPOINT_POSTNOTICE => $omb_base . 'postnotice',
+        OMB_ENDPOINT_UPDATEPROFILE => $omb_base . 'updateprofile');
+  }
+
+  public function getURL($action) {
+    return $this->urls[$action];
+  }
+}
+?>
diff --git a/extlib/libomb/constants.php b/extlib/libomb/constants.php
new file mode 100644 (file)
index 0000000..a097443
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+/**
+ * Constants for libomb
+ *
+ * This file contains constant definitions for libomb. The defined constants
+ * are service and namespace URIs for OAuth and OMB as used in XRDS.
+ *
+ * PHP version 5
+ *
+ * LICENSE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   OMB
+ * @author    Adrian Lang <mail@adrianlang.de>
+ * @copyright 2009 Adrian Lang
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
+ **/
+
+/**
+ * The OMB constants.
+ **/
+
+define('OMB_VERSION_01', 'http://openmicroblogging.org/protocol/0.1');
+
+/* The OMB version supported by this libomb version. */
+define('OMB_VERSION', OMB_VERSION_01);
+
+define('OMB_ENDPOINT_UPDATEPROFILE', OMB_VERSION . '/updateProfile');
+define('OMB_ENDPOINT_POSTNOTICE', OMB_VERSION . '/postNotice');
+
+/**
+ * The OAuth constants.
+ **/
+
+define('OAUTH_NAMESPACE', 'http://oauth.net/core/1.0/');
+
+define('OAUTH_ENDPOINT_REQUEST', OAUTH_NAMESPACE.'endpoint/request');
+define('OAUTH_ENDPOINT_AUTHORIZE', OAUTH_NAMESPACE.'endpoint/authorize');
+define('OAUTH_ENDPOINT_ACCESS', OAUTH_NAMESPACE.'endpoint/access');
+define('OAUTH_ENDPOINT_RESOURCE', OAUTH_NAMESPACE.'endpoint/resource');
+
+define('OAUTH_AUTH_HEADER', OAUTH_NAMESPACE.'parameters/auth-header');
+define('OAUTH_POST_BODY', OAUTH_NAMESPACE.'parameters/post-body');
+
+define('OAUTH_HMAC_SHA1', OAUTH_NAMESPACE.'signature/HMAC-SHA1');
+
+define('OAUTH_DISCOVERY', 'http://oauth.net/discovery/1.0');
+?>
diff --git a/extlib/libomb/datastore.php b/extlib/libomb/datastore.php
new file mode 100755 (executable)
index 0000000..ab52de5
--- /dev/null
@@ -0,0 +1,200 @@
+<?php
+
+require_once 'OAuth.php';
+
+/**
+ * Data access interface
+ *
+ * This interface specifies data access methods libomb needs. It should be
+ * implemented by libomb users. OMB_Datastore is libomb’s main interface to the
+ * application’s data. Objects corresponding to this interface are used in
+ * OMB_Service_Provider and OMB_Service_Consumer.
+ *
+ * Note that it’s implemented as a class since OAuthDataStore is as well a
+ * class, though only declaring methods.
+ *
+ * OMB_Datastore extends OAuthDataStore with two OAuth-related methods for token
+ * revoking and authorizing and all OMB-related methods.
+ * Refer to OAuth.php for a complete specification of OAuth-related methods.
+ *
+ * It is the user’s duty to signal and handle errors. libomb does not check
+ * return values nor handle exceptions. It is suggested to use exceptions.
+ * Note that lookup_token and getProfile return null if the requested object
+ * is not available. This is NOT an error and should not raise an exception.
+ * Same applies for lookup_nonce which returns a boolean value. These methods
+ * may nevertheless throw an exception, for example in case of a storage errors.
+ *
+ * Most of the parameters passed to these methods are unescaped and unverified
+ * user input. Therefore they should be handled with extra care to avoid
+ * security problems like SQL injections.
+ *
+ * PHP version 5
+ *
+ * LICENSE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   OMB
+ * @author    Adrian Lang <mail@adrianlang.de>
+ * @copyright 2009 Adrian Lang
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
+ **/
+
+class OMB_Datastore extends OAuthDataStore {
+
+  /*********
+   * OAUTH *
+   *********/
+
+  /**
+   * Revoke specified OAuth token
+   *
+   * Revokes the authorization token specified by $token_key.
+   * Throws exceptions in case of error.
+   *
+   * @param string $token_key The key of the token to be revoked
+   *
+   * @access public
+   **/
+  public function revoke_token($token_key) {
+    throw new Exception();
+  }
+
+  /**
+   * Authorize specified OAuth token
+   *
+   * Authorizes the authorization token specified by $token_key.
+   * Throws exceptions in case of error.
+   *
+   * @param string $token_key The key of the token to be authorized
+   *
+   * @access public
+   **/
+  public function authorize_token($token_key) {
+    throw new Exception();
+  }
+
+  /*********
+   *  OMB  *
+   *********/
+
+  /**
+   * Get profile by identifying URI
+   *
+   * Returns an OMB_Profile object representing the OMB profile identified by
+   * $identifier_uri.
+   * Returns null if there is no such OMB profile.
+   * Throws exceptions in case of other error.
+   *
+   * @param string $identifier_uri The OMB identifier URI specifying the
+   *                               requested profile
+   *
+   * @access public
+   *
+   * @return OMB_Profile The corresponding profile
+   **/
+  public function getProfile($identifier_uri) {
+    throw new Exception();
+  }
+
+  /**
+   * Save passed profile
+   *
+   * Stores the OMB profile $profile. Overwrites an existing entry.
+   * Throws exceptions in case of error.
+   *
+   * @param OMB_Profile $profile   The OMB profile which should be saved
+   *
+   * @access public
+   **/
+  public function saveProfile($profile) {
+    throw new Exception();
+  }
+
+  /**
+   * Save passed notice
+   *
+   * Stores the OMB notice $notice. The datastore may change the passed notice.
+   * This might by neccessary for URIs depending on a database key. Note that
+   * it is the user’s duty to present a mechanism for his OMB_Datastore to
+   * appropriately change his OMB_Notice. TODO: Ugly.
+   * Throws exceptions in case of error.
+   *
+   * @param OMB_Notice $notice The OMB notice which should be saved
+   *
+   * @access public
+   **/
+  public function saveNotice(&$notice) {
+    throw new Exception();
+  }
+
+  /**
+   * Get subscriptions of a given profile
+   *
+   * Returns an array containing subscription informations for the specified
+   * profile. Every array entry should in turn be an array with keys
+   *   'uri´: The identifier URI of the subscriber
+   *   'token´: The subscribe token
+   *   'secret´: The secret token
+   * Throws exceptions in case of error.
+   *
+   * @param string $subscribed_user_uri The OMB identifier URI specifying the
+   *                                    subscribed profile
+   *
+   * @access public
+   *
+   * @return mixed An array containing the subscriptions or 0 if no
+   *               subscription has been found.
+   **/
+  public function getSubscriptions($subscribed_user_uri) {
+    throw new Exception();
+  }
+
+  /**
+   * Delete a subscription
+   *
+   * Deletes the subscription from $subscriber_uri to $subscribed_user_uri.
+   * Throws exceptions in case of error.
+   *
+   * @param string $subscriber_uri      The OMB identifier URI specifying the
+   *                                    subscribing profile
+   *
+   * @param string $subscribed_user_uri The OMB identifier URI specifying the
+   *                                    subscribed profile
+   *
+   * @access public
+   **/
+  public function deleteSubscription($subscriber_uri, $subscribed_user_uri) {
+    throw new Exception();
+  }
+
+  /**
+   * Save a subscription
+   *
+   * Saves the subscription from $subscriber_uri to $subscribed_user_uri.
+   * Throws exceptions in case of error.
+   *
+   * @param string     $subscriber_uri      The OMB identifier URI specifying
+   *                                        the subscribing profile
+   *
+   * @param string     $subscribed_user_uri The OMB identifier URI specifying
+   *                                        the subscribed profile
+   * @param OAuthToken $token               The access token
+   *
+   * @access public
+   **/
+  public function saveSubscription($subscriber_uri, $subscribed_user_uri,
+                                                                       $token) {
+    throw new Exception();
+  }
+}
+?>
diff --git a/extlib/libomb/helper.php b/extlib/libomb/helper.php
new file mode 100644 (file)
index 0000000..a1f21f2
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+
+require_once 'Validate.php';
+
+/**
+ * Helper functions for libomb
+ *
+ * This file contains helper functions for libomb.
+ *
+ * PHP version 5
+ *
+ * LICENSE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   OMB
+ * @author    Adrian Lang <mail@adrianlang.de>
+ * @copyright 2009 Adrian Lang
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
+ **/
+
+class OMB_Helper {
+
+  /**
+   * Non-scalar constants
+   *
+   * The set of OMB and OAuth Services an OMB Server has to implement.
+   */
+
+  public static $OMB_SERVICES =
+    array(OMB_ENDPOINT_UPDATEPROFILE, OMB_ENDPOINT_POSTNOTICE);
+  public static $OAUTH_SERVICES =
+    array(OAUTH_ENDPOINT_REQUEST, OAUTH_ENDPOINT_AUTHORIZE, OAUTH_ENDPOINT_ACCESS);
+
+  /**
+   * Validate URL
+   *
+   * Basic URL validation. Currently http, https, ftp and gopher are supported
+   * schemes.
+   *
+   * @param string $url The URL which is to be validated.
+   *
+   * @return bool Whether URL is valid.
+   *
+   * @access public
+   */
+  public static function validateURL($url) {
+    return Validate::uri($url, array('allowed_schemes' => array('http', 'https',
+            'gopher', 'ftp')));
+  }
+
+  /**
+   * Validate Media type
+   *
+   * Basic Media type validation. Checks for valid maintype and correct format.
+   *
+   * @param string $mediatype The Media type which is to be validated.
+   *
+   * @return bool Whether media type is valid.
+   *
+   * @access public
+   */
+  public static function validateMediaType($mediatype) {
+    if (0 === preg_match('/^(\w+)\/([\w\d-+.]+)$/', $mediatype, $subtypes)) {
+      return false;
+    }
+    if (!in_array(strtolower($subtypes[1]), array('application', 'audio', 'image',
+              'message', 'model', 'multipart', 'text', 'video'))) {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Remove escaping from request parameters
+   *
+   * Neutralise the evil effects of magic_quotes_gpc in the current request.
+   * This is used before handing a request off to OAuthRequest::from_request.
+   * Many thanks to Ciaran Gultnieks for this fix.
+   *
+   * @access public
+   */
+  public static function removeMagicQuotesFromRequest() {
+    if(get_magic_quotes_gpc() == 1) {
+      $_POST = array_map('stripslashes', $_POST);
+      $_GET = array_map('stripslashes', $_GET);
+    }
+  }
+}
+?>
diff --git a/extlib/libomb/invalidparameterexception.php b/extlib/libomb/invalidparameterexception.php
new file mode 100755 (executable)
index 0000000..163e1dd
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+/**
+ * Exception stating that a passed parameter is invalid
+ *
+ * This exception is raised when a parameter does not obey the OMB standard.
+ *
+ * PHP version 5
+ *
+ * LICENSE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   OMB
+ * @author    Adrian Lang <mail@adrianlang.de>
+ * @copyright 2009 Adrian Lang
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
+ **/
+class OMB_InvalidParameterException extends Exception {
+  public function __construct($value, $type, $parameter) {
+    parent::__construct("Invalid value $value for parameter $parameter in $type");
+  }
+}
+?>
diff --git a/extlib/libomb/invalidyadisexception.php b/extlib/libomb/invalidyadisexception.php
new file mode 100755 (executable)
index 0000000..797b7b9
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Exception stating that a requested url does not resolve to a valid yadis
+ *
+ * This exception is raised when OMB_Service is not able to discover a valid
+ * yadis location with XRDS.
+ *
+ * PHP version 5
+ *
+ * LICENSE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   OMB
+ * @author    Adrian Lang <mail@adrianlang.de>
+ * @copyright 2009 Adrian Lang
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
+ **/
+class OMB_InvalidYadisException extends Exception {
+
+}
+?>
diff --git a/extlib/libomb/notice.php b/extlib/libomb/notice.php
new file mode 100755 (executable)
index 0000000..9ac3664
--- /dev/null
@@ -0,0 +1,272 @@
+<?php
+require_once 'invalidparameterexception.php';
+require_once 'Validate.php';
+require_once 'helper.php';
+
+/**
+ * OMB Notice representation
+ *
+ * This class represents an OMB notice.
+ *
+ * Do not call the setters with null values. Instead, if you want to delete a
+ * field, pass an empty string. The getters will return null for empty fields.
+ *
+ * PHP version 5
+ *
+ * LICENSE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   OMB
+ * @author    Adrian Lang <mail@adrianlang.de>
+ * @copyright 2009 Adrian Lang
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
+ **/
+
+class OMB_Notice {
+  protected $author;
+  protected $uri;
+  protected $content;
+  protected $url;
+  protected $license_url; /* url is an own addition for clarification. */
+  protected $seealso_url; /* url is an own addition for clarification. */
+  protected $seealso_disposition;
+  protected $seealso_mediatype;
+  protected $seealso_license_url; /* url is an addition for clarification. */
+
+  /* The notice as OMB param array. Cached and rebuild on usage.
+     false while outdated. */
+  protected $param_array;
+
+  /**
+   * Constructor for OMB_Notice
+   *
+   * Initializes the OMB_Notice object with author, uri and content.
+   * These parameters are mandatory for postNotice.
+   *
+   * @param object $author  An OMB_Profile object representing the author of the
+   *                        notice.
+   * @param string $uri     The notice URI as defined by the OMB. A unique and
+   *                        unchanging identifier for a notice.
+   * @param string $content The content of the notice. 140 chars recommended,
+   *                        but there is no limit.
+   *
+   * @access public
+   */
+  public function __construct($author, $uri, $content) {
+    $this->content = $content;
+    if (is_null($author)) {
+      throw new OMB_InvalidParameterException('', 'notice', 'omb_listenee');
+    }
+    $this->author = $author;
+
+    if (!Validate::uri($uri)) {
+      throw new OMB_InvalidParameterException($uri, 'notice', 'omb_notice');
+    }
+    $this->uri = $uri;
+
+    $this->param_array = false;
+  }
+
+  /**
+   * Returns the notice as array
+   *
+   * The method returns an array which contains the whole notice as array. The
+   * array is cached and only rebuilt on changes of the notice.
+   * Empty optional values are not passed.
+   *
+   *  @access  public
+   *  @returns array The notice as parameter array
+   */
+  public function asParameters() {
+    if ($this->param_array !== false) {
+      return $this->param_array;
+    }
+
+    $this->param_array = array(
+                 'omb_notice' => $this->uri,
+                 'omb_notice_content' => $this->content);
+
+    if (!is_null($this->url))
+      $this->param_array['omb_notice_url'] = $this->url;
+
+    if (!is_null($this->license_url))
+      $this->param_array['omb_notice_license'] = $this->license_url;
+
+    if (!is_null($this->seealso_url)) {
+      $this->param_array['omb_seealso'] = $this->seealso_url;
+
+      /* This is actually a free interpretation of the OMB standard. We assume
+         that additional seealso parameters are not of any use if seealso itself
+         is not set. */
+      if (!is_null($this->seealso_disposition))
+        $this->param_array['omb_seealso_disposition'] =
+                                                     $this->seealso_disposition;
+
+      if (!is_null($this->seealso_mediatype))
+        $this->param_array['omb_seealso_mediatype'] = $this->seealso_mediatype;
+
+      if (!is_null($this->seealso_license_url))
+        $this->param_array['omb_seealso_license'] = $this->seealso_license_url;
+    }
+    return $this->param_array;
+  }
+
+  /**
+   * Builds an OMB_Notice object from array
+   *
+   * The method builds an OMB_Notice object from the passed parameters array.
+   * The array MUST provide a notice URI and content. The array fields HAVE TO
+   * be named according to the OMB standard, i. e. omb_notice_* and
+   * omb_seealso_*. Values are handled as not passed if the corresponding array
+   * fields are not set or the empty string.
+   *
+   * @param object $author     An OMB_Profile object representing the author of
+   *                           the notice.
+   * @param string $parameters An array containing the notice parameters.
+   *
+   * @access public
+   *
+   * @returns OMB_Notice The built OMB_Notice.
+   */
+  public static function fromParameters($author, $parameters) {
+    $notice = new OMB_Notice($author, $parameters['omb_notice'],
+                             $parameters['omb_notice_content']);
+
+    if (isset($parameters['omb_notice_url'])) {
+      $notice->setURL($parameters['omb_notice_url']);
+    }
+
+    if (isset($parameters['omb_notice_license'])) {
+      $notice->setLicenseURL($parameters['omb_notice_license']);
+    }
+
+    if (isset($parameters['omb_seealso'])) {
+      $notice->setSeealsoURL($parameters['omb_seealso']);
+    }
+
+    if (isset($parameters['omb_seealso_disposition'])) {
+      $notice->setSeealsoDisposition($parameters['omb_seealso_disposition']);
+    }
+
+    if (isset($parameters['omb_seealso_mediatype'])) {
+      $notice->setSeealsoMediatype($parameters['omb_seealso_mediatype']);
+    }
+
+    if (isset($parameters['omb_seealso_license'])) {
+      $notice->setSeealsoLicenseURL($parameters['omb_seealso_license']);
+    }
+    return $notice;
+  }
+
+  public function getAuthor() {
+    return $this->author;
+  }
+
+  public function getIdentifierURI() {
+    return $this->uri;
+  }
+
+  public function getContent() {
+    return $this->content;
+  }
+
+  public function getURL() {
+    return $this->url;
+  }
+
+  public function getLicenseURL() {
+    return $this->license_url;
+  }
+
+  public function getSeealsoURL() {
+    return $this->seealso_url;
+  }
+
+  public function getSeealsoDisposition() {
+    return $this->seealso_disposition;
+  }
+
+  public function getSeealsoMediatype() {
+    return $this->seealso_mediatype;
+  }
+
+  public function getSeealsoLicenseURL() {
+    return $this->seealso_license_url;
+  }
+
+  public function setURL($url) {
+    if ($url === '') {
+      $url = null;
+    } elseif (!OMB_Helper::validateURL($url)) {
+      throw new OMB_InvalidParameterException($url, 'notice', 'omb_notice_url');
+    }
+    $this->url = $url;
+    $this->param_array = false;
+  }
+
+  public function setLicenseURL($license_url) {
+    if ($license_url === '') {
+      $license_url = null;
+    } elseif (!OMB_Helper::validateURL($license_url)) {
+      throw new OMB_InvalidParameterException($license_url, 'notice',
+                                              'omb_notice_license');
+    }
+    $this->license_url = $license_url;
+    $this->param_array = false;
+  }
+
+  public function setSeealsoURL($seealso_url) {
+    if ($seealso_url === '') {
+      $seealso_url = null;
+    } elseif (!OMB_Helper::validateURL($seealso_url)) {
+      throw new OMB_InvalidParameterException($seealso_url, 'notice',
+                                              'omb_seealso');
+    }
+    $this->seealso_url = $seealso_url;
+    $this->param_array = false;
+  }
+
+  public function setSeealsoDisposition($seealso_disposition) {
+    if ($seealso_disposition === '') {
+      $seealso_disposition = null;
+    } elseif ($seealso_disposition !== 'link' && $seealso_disposition !== 'inline') {
+      throw new OMB_InvalidParameterException($seealso_disposition, 'notice',
+                                              'omb_seealso_disposition');
+    }
+    $this->seealso_disposition = $seealso_disposition;
+    $this->param_array = false;
+  }
+
+  public function setSeealsoMediatype($seealso_mediatype) {
+    if ($seealso_mediatype === '') {
+      $seealso_mediatype = null;
+    } elseif (!OMB_Helper::validateMediaType($seealso_mediatype)) {
+      throw new OMB_InvalidParameterException($seealso_mediatype, 'notice',
+                                              'omb_seealso_mediatype');
+    }
+    $this->seealso_mediatype = $seealso_mediatype;
+    $this->param_array = false;
+  }
+
+  public function setSeealsoLicenseURL($seealso_license_url) {
+    if ($seealso_license_url === '') {
+      $seealso_license_url = null;
+    } elseif (!OMB_Helper::validateURL($seealso_license_url)) {
+      throw new OMB_InvalidParameterException($seealso_license_url, 'notice',
+                                              'omb_seealso_license');
+    }
+    $this->seealso_license_url = $seealso_license_url;
+    $this->param_array = false;
+  }
+}
+?>
diff --git a/extlib/libomb/omb_yadis_xrds.php b/extlib/libomb/omb_yadis_xrds.php
new file mode 100755 (executable)
index 0000000..8992120
--- /dev/null
@@ -0,0 +1,196 @@
+<?php
+
+require_once 'Auth/Yadis/Yadis.php';
+require_once 'unsupportedserviceexception.php';
+require_once 'invalidyadisexception.php';
+
+/**
+ * OMB XRDS representation
+ *
+ * This class represents a Yadis XRDS file for OMB. It adds some useful methods to
+ * Auth_Yadis_XRDS.
+ *
+ * PHP version 5
+ *
+ * LICENSE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   OMB
+ * @author    Adrian Lang <mail@adrianlang.de>
+ * @copyright 2009 Adrian Lang
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
+ **/
+
+class OMB_Yadis_XRDS extends Auth_Yadis_XRDS {
+
+  protected $fetcher;
+
+  /**
+   * Create an instance from URL
+   *
+   * Constructs an OMB_Yadis_XRDS object from a given URL. A full Yadis
+   * discovery is performed on the URL and the XRDS is parsed.
+   * Throws an OMB_InvalidYadisException when no Yadis is discovered or the
+   * detected XRDS file is broken.
+   *
+   * @param string                 $url     The URL on which Yadis discovery
+   *                                        should be performed on
+   * @param Auth_Yadis_HTTPFetcher $fetcher A fetcher used to get HTTP
+   *                                        resources
+   *
+   * @access public
+   *
+   * @return OMB_Yadis_XRDS The initialized object representing the given
+   *                        resource
+   **/
+  public static function fromYadisURL($url, $fetcher) {
+    /* Perform a Yadis discovery. */
+    $yadis = Auth_Yadis_Yadis::discover($url, $fetcher);
+    if ($yadis->failed) {
+      throw new OMB_InvalidYadisException($url);
+    }
+
+    /* Parse the XRDS file. */
+    $xrds = OMB_Yadis_XRDS::parseXRDS($yadis->response_text);
+    if ($xrds === null) {
+      throw new OMB_InvalidYadisException($url);
+    }
+    $xrds->fetcher = $fetcher;
+    return $xrds;
+  }
+
+  /**
+   * Get a specific service
+   *
+   * Returns the Auth_Yadis_Service object corresponding to the given service
+   * URI.
+   * Throws an OMB_UnsupportedServiceException if the service is not available.
+   *
+   * @param string $service URI specifier of the requested service
+   *
+   * @access public
+   *
+   * @return Auth_Yadis_Service The object representing the requested service
+   **/
+  public function getService($service) {
+    $match = $this->services(array( create_function('$s',
+                           "return in_array('$service', \$s->getTypes());")));
+    if ($match === array()) {
+      throw new OMB_UnsupportedServiceException($service);
+    }
+    return $match[0];
+  }
+
+  /**
+   * Get a specific XRD
+   *
+   * Returns the OMB_Yadis_XRDS object corresponding to the given URI.
+   * Throws an OMB_UnsupportedServiceException if the XRD is not available.
+   * Note that getXRD tries to resolve external XRD parts as well.
+   *
+   * @param string $uri URI specifier of the requested XRD
+   *
+   * @access public
+   *
+   * @return OMB_Yadis_XRDS The object representing the requested XRD
+   **/
+  public function getXRD($uri) {
+    $nexthash = strpos($uri, '#');
+    if ($nexthash !== 0) {
+      if ($nexthash !== false) {
+        $cururi = substr($uri, 0, $nexthash);
+        $nexturi = substr($uri, $nexthash);
+      }
+      return
+        OMB_Yadis_XRDS::fromYadisURL($cururi, $this->fetcher)->getXRD($nexturi);
+    }
+
+    $id = substr($uri, 1);
+    foreach ($this->allXrdNodes as $node) {
+      $attrs = $this->parser->attributes($node);
+      if (array_key_exists('xml:id', $attrs) && $attrs['xml:id'] == $id) {
+        /* Trick the constructor into thinking this is the only node. */
+        $bogus_nodes = array($node);
+        return new OMB_Yadis_XRDS($this->parser, $bogus_nodes);
+      }
+    }
+    throw new OMB_UnsupportedServiceException($uri);
+  }
+
+  /**
+   * Parse an XML string containing a XRDS document
+   *
+   * Parse an XML string (XRDS document) and return either a
+   * Auth_Yadis_XRDS object or null, depending on whether the
+   * XRDS XML is valid.
+   * Copy and paste from parent to select correct constructor.
+   *
+   * @param string $xml_string An XRDS XML string.
+   *
+   * @access public
+   *
+   * @return mixed An instance of OMB_Yadis_XRDS or null,
+   *               depending on the validity of $xml_string
+   **/
+
+  public function &parseXRDS($xml_string, $extra_ns_map = null) {
+    $_null = null;
+
+    if (!$xml_string) {
+      return $_null;
+    }
+
+    $parser = Auth_Yadis_getXMLParser();
+
+    $ns_map = Auth_Yadis_getNSMap();
+
+    if ($extra_ns_map && is_array($extra_ns_map)) {
+      $ns_map = array_merge($ns_map, $extra_ns_map);
+    }
+
+    if (!($parser && $parser->init($xml_string, $ns_map))) {
+      return $_null;
+    }
+
+    // Try to get root element.
+    $root = $parser->evalXPath('/xrds:XRDS[1]');
+    if (!$root) {
+      return $_null;
+    }
+
+    if (is_array($root)) {
+      $root = $root[0];
+    }
+
+    $attrs = $parser->attributes($root);
+
+    if (array_key_exists('xmlns:xrd', $attrs) &&
+          $attrs['xmlns:xrd'] != Auth_Yadis_XMLNS_XRDS) {
+      return $_null;
+    } else if (array_key_exists('xmlns', $attrs) &&
+                   preg_match('/xri/', $attrs['xmlns']) &&
+                   $attrs['xmlns'] != Auth_Yadis_XMLNS_XRD_2_0) {
+      return $_null;
+    }
+
+    // Get the last XRD node.
+    $xrd_nodes = $parser->evalXPath('/xrds:XRDS[1]/xrd:XRD');
+
+    if (!$xrd_nodes) {
+      return $_null;
+    }
+
+    $xrds = new OMB_Yadis_XRDS($parser, $xrd_nodes);
+    return $xrds;
+  }
+}
diff --git a/extlib/libomb/plain_xrds_writer.php b/extlib/libomb/plain_xrds_writer.php
new file mode 100755 (executable)
index 0000000..b4a6e99
--- /dev/null
@@ -0,0 +1,124 @@
+<?php
+
+require_once 'xrds_writer.php';
+
+/**
+ * Write OMB-specific XRDS using XMLWriter.
+ *
+ * This class writes the XRDS file announcing the OMB server. It uses
+ * OMB_XMLWriter, which is a subclass of XMLWriter. An instance of
+ * OMB_Plain_XRDS_Writer should be passed to OMB_Service_Provider->writeXRDS.
+ *
+ * PHP version 5
+ *
+ * LICENSE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   OMB
+ * @author    Adrian Lang <mail@adrianlang.de>
+ * @copyright 2009 Adrian Lang
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
+ **/
+
+class OMB_Plain_XRDS_Writer implements OMB_XRDS_Writer {
+  public function writeXRDS($user, $mapper) {
+    header('Content-Type: application/xrds+xml');
+    $xw = new XMLWriter();
+    $xw->openURI('php://output');
+    $xw->setIndent(true);
+
+    $xw->startDocument('1.0', 'UTF-8');
+    $this->writeFullElement($xw, 'XRDS',  array('xmlns' => 'xri://$xrds'), array(
+        array('XRD',  array('xmlns' => 'xri://$xrd*($v*2.0)',
+                                          'xml:id' => 'oauth',
+                                          'xmlns:simple' => 'http://xrds-simple.net/core/1.0',
+                                          'version' => '2.0'), array(
+          array('Type', null, 'xri://$xrds*simple'),
+          array('Service', null, array(
+            array('Type', null, OAUTH_ENDPOINT_REQUEST),
+            array('URI', null, $mapper->getURL(OAUTH_ENDPOINT_REQUEST)),
+            array('Type', null, OAUTH_AUTH_HEADER),
+            array('Type', null, OAUTH_POST_BODY),
+            array('Type', null, OAUTH_HMAC_SHA1),
+            array('LocalID', null, $user->getIdentifierURI())
+          )),
+          array('Service', null, array(
+            array('Type', null, OAUTH_ENDPOINT_AUTHORIZE),
+            array('URI', null, $mapper->getURL(OAUTH_ENDPOINT_AUTHORIZE)),
+            array('Type', null, OAUTH_AUTH_HEADER),
+            array('Type', null, OAUTH_POST_BODY),
+            array('Type', null, OAUTH_HMAC_SHA1)
+          )),
+          array('Service', null, array(
+            array('Type', null, OAUTH_ENDPOINT_ACCESS),
+            array('URI', null, $mapper->getURL(OAUTH_ENDPOINT_ACCESS)),
+            array('Type', null, OAUTH_AUTH_HEADER),
+            array('Type', null, OAUTH_POST_BODY),
+            array('Type', null, OAUTH_HMAC_SHA1)
+          )),
+          array('Service', null, array(
+            array('Type', null, OAUTH_ENDPOINT_RESOURCE),
+            array('Type', null, OAUTH_AUTH_HEADER),
+            array('Type', null, OAUTH_POST_BODY),
+            array('Type', null, OAUTH_HMAC_SHA1)
+          ))
+        )),
+        array('XRD',  array('xmlns' => 'xri://$xrd*($v*2.0)',
+                                          'xml:id' => 'omb',
+                                          'xmlns:simple' => 'http://xrds-simple.net/core/1.0',
+                                          'version' => '2.0'), array(
+          array('Type', null, 'xri://$xrds*simple'),
+          array('Service', null, array(
+            array('Type', null, OMB_ENDPOINT_POSTNOTICE),
+            array('URI', null, $mapper->getURL(OMB_ENDPOINT_POSTNOTICE))
+          )),
+          array('Service', null, array(
+            array('Type', null, OMB_ENDPOINT_UPDATEPROFILE),
+            array('URI', null, $mapper->getURL(OMB_ENDPOINT_UPDATEPROFILE))
+          ))
+        )),
+        array('XRD',  array('xmlns' => 'xri://$xrd*($v*2.0)',
+                                          'version' => '2.0'), array(
+          array('Type', null, 'xri://$xrds*simple'),
+          array('Service', null, array(
+            array('Type', null, OAUTH_DISCOVERY),
+            array('URI', null, '#oauth')
+          )),
+          array('Service', null, array(
+            array('Type', null, OMB_VERSION),
+            array('URI', null, '#omb')
+          ))
+        ))
+      ));
+    $xw->endDocument();
+    $xw->flush();
+  }
+
+  public static function writeFullElement($xw, $tag, $attributes, $content) {
+    $xw->startElement($tag);
+    if (!is_null($attributes)) {
+      foreach ($attributes as $name => $value) {
+        $xw->writeAttribute($name, $value);
+      }
+    }
+    if (is_array($content)) {
+      foreach ($content as $values) {
+        OMB_Plain_XRDS_Writer::writeFullElement($xw, $values[0], $values[1], $values[2]);
+      }
+    } else {
+      $xw->text($content);
+    }
+    $xw->fullEndElement();
+  }
+}
+?>
diff --git a/extlib/libomb/profile.php b/extlib/libomb/profile.php
new file mode 100755 (executable)
index 0000000..13314d3
--- /dev/null
@@ -0,0 +1,317 @@
+<?php
+require_once 'invalidparameterexception.php';
+require_once 'Validate.php';
+require_once 'helper.php';
+
+/**
+ * OMB profile representation
+ *
+ * This class represents an OMB profile.
+ *
+ * Do not call the setters with null values. Instead, if you want to delete a
+ * field, pass an empty string. The getters will return null for empty fields.
+ *
+ * PHP version 5
+ *
+ * LICENSE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   OMB
+ * @author    Adrian Lang <mail@adrianlang.de>
+ * @copyright 2009 Adrian Lang
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
+ **/
+
+class OMB_Profile {
+  protected $identifier_uri;
+  protected $profile_url;
+  protected $nickname;
+  protected $license_url;
+  protected $fullname;
+  protected $homepage;
+  protected $bio;
+  protected $location;
+  protected $avatar_url;
+
+  /* The profile as OMB param array. Cached and rebuild on usage.
+     false while outdated. */
+  protected $param_array;
+
+  /**
+   * Constructor for OMB_Profile
+   *
+   * Initializes the OMB_Profile object with an identifier uri.
+   *
+   * @param string $identifier_uri The profile URI as defined by the OMB. A unique
+   *                               and unchanging identifier for a profile.
+   *
+   * @access public
+   */
+  public function __construct($identifier_uri) {
+    if (!Validate::uri($identifier_uri)) {
+      throw new OMB_InvalidParameterException($identifier_uri, 'profile',
+                                                'omb_listenee or omb_listener');
+    }
+    $this->identifier_uri = $identifier_uri;
+    $this->param_array = false;
+  }
+
+  /**
+   * Returns the profile as array
+   *
+   * The method returns an array which contains the whole profile as array. The
+   * array is cached and only rebuilt on changes of the profile.
+   *
+   * @param bool   $force_all Specifies whether empty fields should be added to
+   *                          the array as well. This is neccessary to clear
+   *                          fields via updateProfile.
+   *
+   * @param string $prefix    The common prefix to the key for all parameters.
+   *
+   * @access public
+   *
+   * @return array The profile as parameter array
+   */
+  public function asParameters($prefix, $force_all = false) {
+    if ($this->param_array === false) {
+      $this->param_array = array('' => $this->identifier_uri);
+
+      if ($force_all || !is_null($this->profile_url)) {
+        $this->param_array['_profile'] = $this->profile_url;
+      }
+
+      if ($force_all || !is_null($this->homepage)) {
+        $this->param_array['_homepage'] = $this->homepage;
+      }
+
+      if ($force_all || !is_null($this->nickname)) {
+        $this->param_array['_nickname'] = $this->nickname;
+      }
+
+      if ($force_all || !is_null($this->license_url)) {
+        $this->param_array['_license'] = $this->license_url;
+      }
+
+      if ($force_all || !is_null($this->fullname)) {
+        $this->param_array['_fullname'] = $this->fullname;
+      }
+
+      if ($force_all || !is_null($this->bio)) {
+        $this->param_array['_bio'] = $this->bio;
+      }
+
+      if ($force_all || !is_null($this->location)) {
+        $this->param_array['_location'] = $this->location;
+      }
+
+      if ($force_all || !is_null($this->avatar_url)) {
+        $this->param_array['_avatar'] = $this->avatar_url;
+      }
+
+    }
+    $ret = array();
+    foreach ($this->param_array as $k => $v) {
+      $ret[$prefix . $k] = $v;
+    }
+    return $ret;
+  }
+
+  /**
+   * Builds an OMB_Profile object from array
+   *
+   * The method builds an OMB_Profile object from the passed parameters array. The
+   * array MUST provide a profile URI. The array fields HAVE TO be named according
+   * to the OMB standard. The prefix (omb_listener or omb_listenee) is passed as a
+   * parameter.
+   *
+   * @param string $parameters An array containing the profile parameters.
+   * @param string $prefix     The common prefix of the profile parameter keys.
+   *
+   * @access public
+   *
+   * @returns OMB_Profile The built OMB_Profile.
+   */
+  public static function fromParameters($parameters, $prefix) {
+    if (!isset($parameters[$prefix])) {
+      throw new OMB_InvalidParameterException('', 'profile', $prefix);
+    }
+
+    $profile = new OMB_Profile($parameters[$prefix]);
+    $profile->updateFromParameters($parameters, $prefix);
+    return $profile;
+  }
+
+  /**
+   * Update from array
+   *
+   * Updates from the passed parameters array. The array does not have to
+   * provide a profile URI. The array fields HAVE TO be named according to the
+   * OMB standard. The prefix (omb_listener or omb_listenee) is passed as a
+   * parameter.
+   *
+   * @param string $parameters An array containing the profile parameters.
+   * @param string $prefix     The common prefix of the profile parameter keys.
+   *
+   * @access public
+   */
+  public function updateFromParameters($parameters, $prefix) {
+    if (isset($parameters[$prefix.'_profile'])) {
+      $this->setProfileURL($parameters[$prefix.'_profile']);
+    }
+
+    if (isset($parameters[$prefix.'_license'])) {
+      $this->setLicenseURL($parameters[$prefix.'_license']);
+    }
+
+    if (isset($parameters[$prefix.'_nickname'])) {
+      $this->setNickname($parameters[$prefix.'_nickname']);
+    }
+
+    if (isset($parameters[$prefix.'_fullname'])) {
+      $this->setFullname($parameters[$prefix.'_fullname']);
+    }
+
+    if (isset($parameters[$prefix.'_homepage'])) {
+      $this->setHomepage($parameters[$prefix.'_homepage']);
+    }
+
+    if (isset($parameters[$prefix.'_bio'])) {
+      $this->setBio($parameters[$prefix.'_bio']);
+    }
+
+    if (isset($parameters[$prefix.'_location'])) {
+      $this->setLocation($parameters[$prefix.'_location']);
+    }
+
+    if (isset($parameters[$prefix.'_avatar'])) {
+      $this->setAvatarURL($parameters[$prefix.'_avatar']);
+    }
+  }
+
+  public function getIdentifierURI() {
+    return $this->identifier_uri;
+  }
+
+  public function getProfileURL() {
+    return $this->profile_url;
+  }
+
+  public function getHomepage() {
+    return $this->homepage;
+  }
+
+  public function getNickname() {
+    return $this->nickname;
+  }
+
+  public function getLicenseURL() {
+    return $this->license_url;
+  }
+
+  public function getFullname() {
+    return $this->fullname;
+  }
+
+  public function getBio() {
+    return $this->bio;
+  }
+
+  public function getLocation() {
+    return $this->location;
+  }
+
+  public function getAvatarURL() {
+    return $this->avatar_url;
+  }
+
+  public function setProfileURL($profile_url) {
+    if (!OMB_Helper::validateURL($profile_url)) {
+      throw new OMB_InvalidParameterException($profile_url, 'profile',
+                                    'omb_listenee_profile or omb_listener_profile');
+    }
+    $this->profile_url = $profile_url;
+    $this->param_array = false;
+  }
+
+  public function setNickname($nickname) {
+    if (!Validate::string($nickname,
+                          array('min_length' => 1,
+                                'max_length' => 64,
+                                'format' => VALIDATE_NUM . VALIDATE_ALPHA))) {
+      throw new OMB_InvalidParameterException($nickname, 'profile', 'nickname');
+    }
+
+    $this->nickname = $nickname;
+    $this->param_array = false;
+  }
+
+  public function setLicenseURL($license_url) {
+    if (!OMB_Helper::validateURL($license_url)) {
+      throw new OMB_InvalidParameterException($license_url, 'profile',
+                                    'omb_listenee_license or omb_listener_license');
+    }
+    $this->license_url = $license_url;
+    $this->param_array = false;
+  }
+
+  public function setFullname($fullname) {
+    if ($fullname === '') {
+      $fullname = null;
+    } elseif (!Validate::string($fullname, array('max_length' => 255))) {
+      throw new OMB_InvalidParameterException($fullname, 'profile', 'fullname');
+    }
+    $this->fullname = $fullname;
+    $this->param_array = false;
+  }
+
+  public function setHomepage($homepage) {
+    if ($homepage === '') {
+      $homepage = null;
+    }
+    $this->homepage = $homepage;
+    $this->param_array = false;
+  }
+
+  public function setBio($bio) {
+    if ($bio === '') {
+      $bio = null;
+    } elseif (!Validate::string($bio, array('max_length' => 140))) {
+      throw new OMB_InvalidParameterException($bio, 'profile', 'fullname');
+    }
+    $this->bio = $bio;
+    $this->param_array = false;
+  }
+
+  public function setLocation($location) {
+    if ($location === '') {
+      $location = null;
+    } elseif (!Validate::string($location, array('max_length' => 255))) {
+      throw new OMB_InvalidParameterException($location, 'profile', 'fullname');
+    }
+    $this->location = $location;
+    $this->param_array = false;
+  }
+
+  public function setAvatarURL($avatar_url) {
+    if ($avatar_url === '') {
+      $avatar_url = null;
+    } elseif (!OMB_Helper::validateURL($avatar_url)) {
+      throw new OMB_InvalidParameterException($avatar_url, 'profile',
+                                      'omb_listenee_avatar or omb_listener_avatar');
+    }
+    $this->avatar_url = $avatar_url;
+    $this->param_array = false;
+  }
+
+}
+?>
diff --git a/extlib/libomb/remoteserviceexception.php b/extlib/libomb/remoteserviceexception.php
new file mode 100755 (executable)
index 0000000..374d159
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+/**
+ * Exception stating that the remote service had a failure
+ *
+ * This exception is raised when a remote service failed to return a valid
+ * response to a request or send a valid request.
+ *
+ * PHP version 5
+ *
+ * LICENSE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   OMB
+ * @author    Adrian Lang <mail@adrianlang.de>
+ * @copyright 2009 Adrian Lang
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
+ **/
+class OMB_RemoteServiceException extends Exception {
+  public static function fromYadis($request_uri, $result) {
+    if ($result->status == 200) {
+        $err = 'Got wrong response ' . $result->body;
+    } else {
+        $err = 'Got error code ' . $result->status . ' with response ' . $result->body;
+    }
+    return new OMB_RemoteServiceException($request_uri . ': ' .  $err);
+  }
+
+  public static function forRequest($action_uri, $failure) {
+    return new OMB_RemoteServiceException("Handler for $action_uri: " .  $failure);
+  }
+}
+?>
diff --git a/extlib/libomb/service_consumer.php b/extlib/libomb/service_consumer.php
new file mode 100755 (executable)
index 0000000..273fd05
--- /dev/null
@@ -0,0 +1,430 @@
+<?php
+
+require_once 'constants.php';
+require_once 'Validate.php';
+require_once 'Auth/Yadis/Yadis.php';
+require_once 'OAuth.php';
+require_once 'unsupportedserviceexception.php';
+require_once 'remoteserviceexception.php';
+require_once 'omb_yadis_xrds.php';
+require_once 'helper.php';
+
+/**
+ * OMB service representation
+ *
+ * This class represents a complete remote OMB service. It provides discovery
+ * and execution of the service’s methods.
+ *
+ * PHP version 5
+ *
+ * LICENSE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   OMB
+ * @author    Adrian Lang <mail@adrianlang.de>
+ * @copyright 2009 Adrian Lang
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
+ **/
+
+class OMB_Service_Consumer {
+  protected $url; /* The service URL */
+  protected $services; /* An array of strings mapping service URI to
+                          service URL */
+
+  protected $token; /* An OAuthToken */
+
+  protected $listener_uri; /* The URI identifying the listener, i. e. the
+                              remote user. */
+
+  protected $listenee_uri; /* The URI identifying the listenee, i. e. the
+                              local user during an auth request. */
+
+  /**
+   * According to OAuth Core 1.0, an user authorization request is no full-blown
+   * OAuth request. nonce, timestamp, consumer_key and signature are not needed
+   * in this step. See http://laconi.ca/trac/ticket/827 for more informations.
+   *
+   * Since Laconica up to version 0.7.2 performs a full OAuth request check, a
+   * correct request would fail.
+   **/
+  public $performLegacyAuthRequest = true;
+
+  /* Helper stuff we are going to need. */
+  protected $fetcher;
+  protected $oauth_consumer;
+  protected $datastore;
+
+  /**
+   * Constructor for OMB_Service_Consumer
+   *
+   * Initializes an OMB_Service_Consumer object representing the OMB service
+   * specified by $service_url. Performs a complete service discovery using
+   * Yadis.
+   * Throws OMB_UnsupportedServiceException if XRDS file does not specify a
+   * complete OMB service.
+   *
+   * @param string        $service_url  The URL of the service
+   * @param string        $consumer_url An URL representing the consumer
+   * @param OMB_Datastore $datastore    An instance of a class implementing
+   *                                    OMB_Datastore
+   *
+   * @access public
+   **/
+  public function __construct ($service_url, $consumer_url, $datastore) {
+    $this->url = $service_url;
+    $this->fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
+    $this->datastore = $datastore;
+    $this->oauth_consumer = new OAuthConsumer($consumer_url, '');
+
+    $xrds = OMB_Yadis_XRDS::fromYadisURL($service_url, $this->fetcher);
+
+    /* Detect our services. This performs a validation as well, since
+       getService und getXRD throw exceptions on failure. */
+    $this->services = array();
+
+    foreach (array(OAUTH_DISCOVERY => OMB_Helper::$OAUTH_SERVICES,
+                   OMB_VERSION     => OMB_Helper::$OMB_SERVICES)
+             as $service_root => $targetservices) {
+      $uris = $xrds->getService($service_root)->getURIs();
+      $xrd = $xrds->getXRD($uris[0]);
+      foreach ($targetservices as $targetservice) {
+        $yadis_service = $xrd->getService($targetservice);
+        if ($targetservice == OAUTH_ENDPOINT_REQUEST) {
+            $localid = $yadis_service->getElements('xrd:LocalID');
+            $this->listener_uri = $yadis_service->parser->content($localid[0]);
+        }
+        $uris = $yadis_service->getURIs();
+        $this->services[$targetservice] = $uris[0];
+      }
+    }
+  }
+
+  /**
+   * Get the handler URI for a service
+   *
+   * Returns the URI the remote web service has specified for the given
+   * service.
+   *
+   * @param string $service The URI identifying the service
+   *
+   * @access public
+   *
+   * @return string The service handler URI
+   **/
+  public function getServiceURI($service) {
+    return $this->services[$service];
+  }
+
+  /**
+   * Get the remote user’s URI
+   *
+   * Returns the URI of the remote user, i. e. the listener.
+   *
+   * @access public
+   *
+   * @return string The remote user’s URI
+   **/
+  public function getRemoteUserURI() {
+    return $this->listener_uri;
+  }
+
+  /**
+   * Get the listenee’s URI
+   *
+   * Returns the URI of the user being subscribed to, i. e. the local user.
+   *
+   * @access public
+   *
+   * @return string The local user’s URI
+   **/
+  public function getListeneeURI() {
+    return $this->listenee_uri;
+  }
+
+  /**
+   * Request a request token
+   *
+   * Performs a token request on the service. Returns an OAuthToken on success.
+   * Throws an exception if the request fails.
+   *
+   * @access public
+   *
+   * @return OAuthToken An unauthorized request token
+   **/
+  public function requestToken() {
+    /* Set the token to null just in case the user called setToken. */
+    $this->token = null;
+
+    $result = $this->performAction(OAUTH_ENDPOINT_REQUEST,
+                                  array('omb_listener' => $this->listener_uri));
+    if ($result->status != 200) {
+      throw OMB_RemoteServiceException::fromYadis(OAUTH_ENDPOINT_REQUEST,
+                                                  $result);
+    }
+    parse_str($result->body, $return);
+    if (!isset($return['oauth_token']) || !isset($return['oauth_token_secret'])) {
+      throw OMB_RemoteServiceException::fromYadis(OAUTH_ENDPOINT_REQUEST,
+                                                  $result);
+    }
+    $this->setToken($return['oauth_token'], $return['oauth_token_secret']);
+    return $this->token;
+  }
+
+  /**
+   *
+   * Request authorization
+   *
+   * Returns an URL which equals to an authorization request. The end user
+   * should be redirected to this location to perform authorization.
+   * The $finish_url should be a local resource which invokes
+   * OMB_Consumer::finishAuthorization on request.
+   *
+   * @param OMB_Profile $profile    An OMB_Profile object representing the
+   *                                soon-to-be subscribed (i. e. local) user
+   * @param string      $finish_url Target location after successful
+   *                                authorization
+   *
+   * @access public
+   *
+   * @return string An URL representing an authorization request
+   **/
+  public function requestAuthorization($profile, $finish_url) {
+    if ($this->performLegacyAuthRequest) {
+      $params = $profile->asParameters('omb_listenee', false);
+      $params['omb_listener'] = $this->listener_uri;
+      $params['oauth_callback'] = $finish_url;
+
+      $url = $this->prepareAction(OAUTH_ENDPOINT_AUTHORIZE, $params, 'GET')->to_url();
+    } else {
+
+      $params = array(
+                'oauth_callback' => $finish_url,
+                'oauth_token'    => $this->token->key,
+                'omb_version'    => OMB_VERSION,
+                'omb_listener'   => $this->listener_uri);
+
+      $params = array_merge($profile->asParameters('omb_listenee', false). $params);
+
+      /* Build result URL. */
+      $url = $this->services[OAUTH_ENDPOINT_AUTHORIZE];
+      $url .= (strrpos($url, '?') === false ? '?' : '&');
+      foreach ($params as $k => $v) {
+        $url .= OAuthUtil::urlencode_rfc3986($k) . '=' . OAuthUtil::urlencode_rfc3986($v) . '&';
+      }
+    }
+
+    $this->listenee_uri = $profile->getIdentifierURI();
+
+    return $url;
+  }
+
+  /**
+   * Finish authorization
+   *
+   * Finish the subscription process by converting the received and authorized
+   * request token into an access token. After that, the subscriber’s profile
+   * and the subscription are stored in the database.
+   * Expects an OAuthRequest in query parameters.
+   * Throws exceptions on failure.
+   *
+   * @access public
+   **/
+  public function finishAuthorization() {
+    OMB_Helper::removeMagicQuotesFromRequest();
+    $req = OAuthRequest::from_request();
+    if ($req->get_parameter('oauth_token') !=
+          $this->token->key) {
+      /* That’s not the token I wanted to get authorized. */
+      throw new OAuthException('The authorized token does not equal the ' .
+                               'submitted token.');
+    }
+
+    if ($req->get_parameter('omb_version') != OMB_VERSION) {
+      throw new OMB_RemoteServiceException('The remote service uses an ' .
+                                           'unsupported OMB version');
+    }
+
+    /* Construct the profile to validate it. */
+
+    /* Fix OMB bug. Listener URI is not passed. */
+    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+      $params = $_POST;
+    } else {
+      $params = $_GET;
+    }
+    $params['omb_listener'] = $this->listener_uri;
+
+    require_once 'profile.php';
+    $listener = OMB_Profile::fromParameters($params, 'omb_listener');
+
+    /* Ask the remote service to convert the authorized request token into an
+       access token. */
+
+    $result = $this->performAction(OAUTH_ENDPOINT_ACCESS, array());
+    if ($result->status != 200) {
+      throw new OAuthException('Could not get access token');
+    }
+
+    parse_str($result->body, $return);
+    if (!isset($return['oauth_token']) || !isset($return['oauth_token_secret'])) {
+      throw new OAuthException('Could not get access token');
+    }
+    $this->setToken($return['oauth_token'], $return['oauth_token_secret']);
+
+    /* Subscription is finished and valid. Now store the new subscriber and the
+       subscription in the database. */
+
+    $this->datastore->saveProfile($listener);
+    $this->datastore->saveSubscription($this->listener_uri,
+                                       $this->listenee_uri,
+                                       $this->token);
+  }
+
+  /**
+   * Return the URI identifying the listener
+   *
+   * Returns the URI for the OMB user who tries to subscribe or already has
+   * subscribed our user. This method is a workaround for a serious OMB flaw:
+   * The Listener URI is not passed in the finishauthorization call.
+   *
+   * @access public
+   *
+   * @return string the listener’s URI
+   **/
+  public function getListenerURI() {
+    return $this->listener_uri;
+  }
+
+  /**
+   * Inform the service about a profile update
+   *
+   * Sends an updated profile to the service.
+   *
+   * @param OMB_Profile $profile The profile that has changed
+   *
+   * @access public
+   **/
+  public function updateProfile($profile) {
+    $params = $profile->asParameters('omb_listenee', true);
+    $this->performOMBAction(OMB_ENDPOINT_UPDATEPROFILE, $params, $profile->getIdentifierURI());
+  }
+
+  /**
+   * Inform the service about a new notice
+   *
+   * Sends a notice to the service.
+   *
+   * @param OMB_Notice $notice The notice
+   *
+   * @access public
+   **/
+  public function postNotice($notice) {
+    $params = $notice->asParameters();
+    $params['omb_listenee'] = $notice->getAuthor()->getIdentifierURI();
+    $this->performOMBAction(OMB_ENDPOINT_POSTNOTICE, $params, $params['omb_listenee']);
+  }
+
+  /**
+   * Set the token member variable
+   *
+   * Initializes the token based on given token and secret token.
+   *
+   * @param string $token  The token
+   * @param string $secret The secret token
+   *
+   * @access public
+   **/
+  public function setToken($token, $secret) {
+    $this->token = new OAuthToken($token, $secret);
+  }
+
+  /**
+   * Prepare an OAuthRequest object
+   *
+   * Creates an OAuthRequest object mapping the request specified by the
+   * parameters.
+   *
+   * @param string $action_uri The URI specifying the target service
+   * @param array  $params     Additional parameters for the service call
+   * @param string $method     The HTTP method used to call the service
+   *                           ('POST' or 'GET', usually)
+   *
+   * @access protected
+   *
+   * @return OAuthRequest the prepared request
+   **/
+  protected function prepareAction($action_uri, $params, $method) {
+    $url = $this->services[$action_uri];
+
+    $url_params = array();
+    parse_str(parse_url($url, PHP_URL_QUERY), $url_params);
+
+    /* Add OMB version. */
+    $url_params['omb_version'] = OMB_VERSION;
+
+    /* Add user-defined parameters. */
+    $url_params = array_merge($url_params, $params);
+
+    $req = OAuthRequest::from_consumer_and_token($this->oauth_consumer,
+                                      $this->token, $method, $url, $url_params);
+
+    /* Sign the request. */
+    $req->sign_request(new OAuthSignatureMethod_HMAC_SHA1(),
+                                           $this->oauth_consumer, $this->token);
+
+    return $req;
+  }
+
+  /**
+   * Perform a service call
+   *
+   * Creates an OAuthRequest object and execute the mapped call as POST request.
+   *
+   * @param string $action_uri The URI specifying the target service
+   * @param array  $params     Additional parameters for the service call
+   *
+   * @access protected
+   *
+   * @return Auth_Yadis_HTTPResponse The POST request response
+   **/
+  protected function performAction($action_uri, $params) {
+    $req = $this->prepareAction($action_uri, $params, 'POST');
+
+    /* Return result page. */
+    return $this->fetcher->post($req->get_normalized_http_url(), $req->to_postdata(), array());
+  }
+
+  /**
+   * Perform an OMB action
+   *
+   * Executes an OMB action – to date, it’s one of updateProfile or postNotice.
+   *
+   * @param string $action_uri   The URI specifying the target service
+   * @param array  $params       Additional parameters for the service call
+   * @param string $listenee_uri The URI identifying the local user for whom
+   *                             the action is performed
+   *
+   * @access protected
+   **/
+  protected function performOMBAction($action_uri, $params, $listenee_uri) {
+    $result = $this->performAction($action_uri, $params);
+    if ($result->status == 403) {
+      /* The remote user unsubscribed us. */
+      $this->datastore->deleteSubscription($this->listener_uri, $listenee_uri);
+    } else if ($result->status != 200 ||
+               strpos($result->body, 'omb_version=' . OMB_VERSION) === false) {
+      /* The server signaled an error or sent an incorrect response. */
+      throw OMB_RemoteServiceException::fromYadis($action_uri, $result);
+    }
+  }
+}
diff --git a/extlib/libomb/service_provider.php b/extlib/libomb/service_provider.php
new file mode 100755 (executable)
index 0000000..7531527
--- /dev/null
@@ -0,0 +1,425 @@
+<?php
+
+require_once 'constants.php';
+require_once 'remoteserviceexception.php';
+require_once 'helper.php';
+
+/**
+ * OMB service realization
+ *
+ * This class realizes a complete, simple OMB service.
+ *
+ * PHP version 5
+ *
+ * LICENSE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   OMB
+ * @author    Adrian Lang <mail@adrianlang.de>
+ * @copyright 2009 Adrian Lang
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
+ **/
+
+class OMB_Service_Provider {
+  protected $user; /* An OMB_Profile representing the user */
+  protected $datastore; /* AN OMB_Datastore */
+
+  protected $remote_user; /* An OMB_Profile representing the remote user during
+                            the authorization process */
+
+  protected $oauth_server; /* An OAuthServer; should only be accessed via
+                              getOAuthServer. */
+
+  /**
+   * Initialize an OMB_Service_Provider object
+   *
+   * Constructs an OMB_Service_Provider instance that provides OMB services
+   * referring to a particular user.
+   *
+   * @param OMB_Profile   $user         An OMB_Profile; mandatory for XRDS
+   *                                    output, user auth handling and OMB
+   *                                    action performing
+   * @param OMB_Datastore $datastore    An OMB_Datastore; mandatory for
+   *                                    everything but XRDS output
+   * @param OAuthServer   $oauth_server An OAuthServer; used for token writing
+   *                                    and OMB action handling; will use
+   *                                    default value if not set
+   *
+   * @access public
+   **/
+  public function __construct ($user = null, $datastore = null, $oauth_server = null) {
+    $this->user = $user;
+    $this->datastore = $datastore;
+    $this->oauth_server = $oauth_server;
+  }
+
+  public function getRemoteUser() {
+    return $this->remote_user;
+  }
+
+  /**
+   * Write a XRDS document
+   *
+   * Writes a XRDS document specifying the OMB service. Optionally uses a
+   * given object of a class implementing OMB_XRDS_Writer for output. Else
+   * OMB_Plain_XRDS_Writer is used.
+   *
+   * @param OMB_XRDS_Mapper $xrds_mapper An object mapping actions to URLs
+   * @param OMB_XRDS_Writer $xrds_writer Optional; The OMB_XRDS_Writer used to
+   *                                     write the XRDS document
+   *
+   * @access public
+   *
+   * @return mixed Depends on the used OMB_XRDS_Writer; OMB_Plain_XRDS_Writer
+   *               returns nothing.
+   **/
+  public function writeXRDS($xrds_mapper, $xrds_writer = null) {
+    if ($xrds_writer == null) {
+        require_once 'plain_xrds_writer.php';
+        $xrds_writer = new OMB_Plain_XRDS_Writer();
+    }
+    return $xrds_writer->writeXRDS($this->user, $xrds_mapper);
+  }
+
+  /**
+   * Echo a request token
+   *
+   * Outputs an unauthorized request token for the query found in $_GET or
+   * $_POST.
+   *
+   * @access public
+   **/
+  public function writeRequestToken() {
+    OMB_Helper::removeMagicQuotesFromRequest();
+    echo $this->getOAuthServer()->fetch_request_token(OAuthRequest::from_request());
+  }
+
+  /**
+   * Handle an user authorization request.
+   *
+   * Parses an authorization request. This includes OAuth and OMB verification.
+   * Throws exceptions on failures. Returns an OMB_Profile object representing
+   * the remote user.
+   *
+   * The OMB_Profile passed to the constructor of OMB_Service_Provider should
+   * not represent the user specified in the authorization request, but the one
+   * currently logged in to the service. This condition being satisfied,
+   * handleUserAuth will check whether the listener specified in the request is
+   * identical to the logged in user.
+   *
+   * @access public
+   *
+   * @return OMB_Profile The profile of the soon-to-be subscribed, i. e. remote
+   *                     user
+   **/
+  public function handleUserAuth() {
+    OMB_Helper::removeMagicQuotesFromRequest();
+
+    /* Verify the request token. */
+
+    $this->token = $this->datastore->lookup_token(null, "request", $_GET['oauth_token']);
+    if (is_null($this->token)) {
+      throw new OAuthException('The given request token has not been issued ' .
+                               'by this service.');
+    }
+
+    /* Verify the OMB part. */
+
+    if ($_GET['omb_version'] !== OMB_VERSION) {
+      throw OMB_RemoteServiceException::forRequest(OAUTH_ENDPOINT_AUTHORIZE,
+                                   'Wrong OMB version ' . $_GET['omb_version']);
+    }
+
+    if ($_GET['omb_listener'] !== $this->user->getIdentifierURI()) {
+      throw OMB_RemoteServiceException::forRequest(OAUTH_ENDPOINT_AUTHORIZE,
+                                 'Wrong OMB listener ' . $_GET['omb_listener']);
+    }
+
+    foreach (array('omb_listenee', 'omb_listenee_profile',
+                   'omb_listenee_nickname', 'omb_listenee_license') as $param) {
+      if (!isset($_GET[$param]) || is_null($_GET[$param])) {
+        throw OMB_RemoteServiceException::forRequest(OAUTH_ENDPOINT_AUTHORIZE,
+                                       "Required parameter '$param' not found");
+      }
+    }
+
+    /* Store given callback for later use. */
+    if (isset($_GET['oauth_callback']) && $_GET['oauth_callback'] !== '') {
+      $this->callback = $_GET['oauth_callback'];
+      if (!OMB_Helper::validateURL($this->callback)) {
+        throw OMB_RemoteServiceException::forRequest(OAUTH_ENDPOINT_AUTHORIZE,
+                                              'Invalid callback URL specified');
+      }
+    }
+    $this->remote_user = OMB_Profile::fromParameters($_GET, 'omb_listenee');
+
+    return $this->remote_user;
+  }
+
+  /**
+   * Continue the OAuth dance after user authorization
+   *
+   * Performs the appropriate actions after user answered the authorization
+   * request.
+   *
+   * @param bool $accepted Whether the user granted authorization
+   *
+   * @access public
+   *
+   * @return array A two-component array with the values:
+   *                - callback The callback URL or null if none given
+   *                - token    The authorized request token or null if not
+   *                           authorized.
+   **/
+  public function continueUserAuth($accepted) {
+    $callback = $this->callback;
+    if (!$accepted) {
+      $this->datastore->revoke_token($this->token->key);
+      $this->token = null;
+      /* TODO: The handling is probably wrong in terms of OAuth 1.0 but the way
+               laconica works. Moreover I don’t know the right way either. */
+
+    } else {
+      $this->datastore->authorize_token($this->token->key);
+      $this->datastore->saveProfile($this->remote_user);
+      $this->datastore->saveSubscription($this->user->getIdentifierURI(),
+                          $this->remote_user->getIdentifierURI(), $this->token);
+
+      if (!is_null($this->callback)) {
+        /* Callback wants to get some informations as well. */
+        $params = $this->user->asParameters('omb_listener', false);
+
+        $params['oauth_token'] = $this->token->key;
+        $params['omb_version'] = OMB_VERSION;
+
+        $callback .= (parse_url($this->callback, PHP_URL_QUERY) ? '&' : '?');
+        foreach ($params as $k => $v) {
+          $callback .= OAuthUtil::urlencode_rfc3986($k) . '=' .
+                       OAuthUtil::urlencode_rfc3986($v) . '&';
+        }
+      }
+    }
+    return array($callback, $this->token);
+  }
+
+  /**
+   * Echo an access token
+   *
+   * Outputs an access token for the query found in $_POST. OMB 0.1 specifies
+   * that the access token request has to be a POST even if OAuth allows GET as
+   * well.
+   *
+   * @access public
+   **/
+  public function writeAccessToken() {
+    OMB_Helper::removeMagicQuotesFromRequest();
+    echo $this->getOAuthServer()->fetch_access_token(
+                                            OAuthRequest::from_request('POST'));
+  }
+
+  /**
+   * Handle an updateprofile request
+   *
+   * Handles an updateprofile request posted to this service. Updates the
+   * profile through the OMB_Datastore.
+   *
+   * @access public
+   *
+   * @return OMB_Profile The updated profile
+   **/
+  public function handleUpdateProfile() {
+    list($req, $profile) = $this->handleOMBRequest(OMB_ENDPOINT_UPDATEPROFILE);
+    $profile->updateFromParameters($req->get_parameters(), 'omb_listenee');
+    $this->datastore->saveProfile($profile);
+    $this->finishOMBRequest();
+    return $profile;
+  }
+
+  /**
+   * Handle a postnotice request
+   *
+   * Handles a postnotice request posted to this service. Saves the notice
+   * through the OMB_Datastore.
+   *
+   * @access public
+   *
+   * @return OMB_Notice The received notice
+   **/
+  public function handlePostNotice() {
+    list($req, $profile) = $this->handleOMBRequest(OMB_ENDPOINT_POSTNOTICE);
+    require_once 'notice.php';
+    $notice = OMB_Notice::fromParameters($profile, $req->get_parameters());
+    $this->datastore->saveNotice($notice);
+    $this->finishOMBRequest();
+    return $notice;
+  }
+
+  /**
+   * Handle an OMB request
+   *
+   * Performs common OMB request handling.
+   *
+   * @param string $uri The URI defining the OMB endpoint being served
+   *
+   * @access protected
+   *
+   * @return array(OAuthRequest, OMB_Profile)
+   **/
+  protected function handleOMBRequest($uri) {
+
+    OMB_Helper::removeMagicQuotesFromRequest();
+    $req = OAuthRequest::from_request('POST');
+    $listenee =  $req->get_parameter('omb_listenee');
+
+    try {
+        list($consumer, $token) = $this->getOAuthServer()->verify_request($req);
+    } catch (OAuthException $e) {
+      header('HTTP/1.1 403 Forbidden');
+      throw OMB_RemoteServiceException::forRequest($uri,
+                                   'Revoked accesstoken for ' . $listenee);
+    }
+
+    $version = $req->get_parameter('omb_version');
+    if ($version !== OMB_VERSION) {
+      header('HTTP/1.1 400 Bad Request');
+      throw OMB_RemoteServiceException::forRequest($uri,
+                                   'Wrong OMB version ' . $version);
+    }
+
+    $profile = $this->datastore->getProfile($listenee);
+    if (is_null($profile)) {
+      header('HTTP/1.1 400 Bad Request');
+      throw OMB_RemoteServiceException::forRequest($uri,
+                                   'Unknown remote profile ' . $listenee);
+    }
+
+    $subscribers = $this->datastore->getSubscriptions($listenee);
+    if (count($subscribers) === 0) {
+      header('HTTP/1.1 403 Forbidden');
+      throw OMB_RemoteServiceException::forRequest($uri,
+                                   'No subscriber for ' . $listenee);
+    }
+
+    return array($req, $profile);
+  }
+
+  /**
+   * Finishes an OMB request handling
+   *
+   * Performs common OMB request handling finishing.
+   *
+   * @access protected
+   **/
+  protected function finishOMBRequest() {
+    header('HTTP/1.1 200 OK');
+    header('Content-type: text/plain');
+    /* There should be no clutter but the version. */
+    echo "omb_version=" . OMB_VERSION;
+  }
+
+  /**
+   * Return an OAuthServer
+   *
+   * Checks whether the OAuthServer is null. If so, initializes it with a
+   * default value. Returns the OAuth server.
+   *
+   * @access protected
+   **/
+  protected function getOAuthServer() {
+    if (is_null($this->oauth_server)) {
+      $this->oauth_server = new OAuthServer($this->datastore);
+      $this->oauth_server->add_signature_method(
+                                          new OAuthSignatureMethod_HMAC_SHA1());
+    }
+    return $this->oauth_server;
+  }
+
+  /**
+   * Publish a notice
+   *
+   * Posts an OMB notice. This includes storing the notice and posting it to
+   * subscribed users.
+   *
+   * @param OMB_Notice $notice The new notice
+   *
+   * @access public
+   *
+   * @return array An array mapping subscriber URIs to the exception posting to
+   *               them has raised; Empty array if no exception occured
+   **/
+  public function postNotice($notice) {
+    $uri = $this->user->getIdentifierURI();
+
+    /* $notice is passed by reference and may change. */
+    $this->datastore->saveNotice($notice);
+    $subscribers = $this->datastore->getSubscriptions($uri);
+
+    /* No one to post to. */
+    if (is_null($subscribers)) {
+        return array();
+    }
+
+    require_once 'service_consumer.php';
+
+    $err = array();
+    foreach($subscribers as $subscriber) {
+      try {
+        $service = new OMB_Service_Consumer($subscriber['uri'], $uri, $this->datastore);
+        $service->setToken($subscriber['token'], $subscriber['secret']);
+        $service->postNotice($notice);
+      } catch (Exception $e) {
+        $err[$subscriber['uri']] = $e;
+        continue;
+      }
+    }
+    return $err;
+  }
+
+  /**
+   * Publish a profile update
+   *
+   * Posts the current profile as an OMB profile update. This includes updating
+   * the stored profile and posting it to subscribed users.
+   *
+   * @access public
+   *
+   * @return array An array mapping subscriber URIs to the exception posting to
+   *               them has raised; Empty array if no exception occured
+   **/
+  public function updateProfile() {
+    $uri = $this->user->getIdentifierURI();
+
+    $this->datastore->saveProfile($this->user);
+    $subscribers = $this->datastore->getSubscriptions($uri);
+
+    /* No one to post to. */
+    if (is_null($subscribers)) {
+        return array();
+    }
+
+    require_once 'service_consumer.php';
+
+    $err = array();
+    foreach($subscribers as $subscriber) {
+      try {
+        $service = new OMB_Service_Consumer($subscriber['uri'], $uri, $this->datastore);
+        $service->setToken($subscriber['token'], $subscriber['secret']);
+        $service->updateProfile($this->user);
+      } catch (Exception $e) {
+        $err[$subscriber['uri']] = $e;
+        continue;
+      }
+    }
+    return $err;
+  }
+}
diff --git a/extlib/libomb/unsupportedserviceexception.php b/extlib/libomb/unsupportedserviceexception.php
new file mode 100755 (executable)
index 0000000..4dab63e
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Exception stating that a requested service is not available
+ *
+ * This exception is raised when OMB_Service is asked to call a service the remote
+ * server does not provide.
+ *
+ * PHP version 5
+ *
+ * LICENSE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   OMB
+ * @author    Adrian Lang <mail@adrianlang.de>
+ * @copyright 2009 Adrian Lang
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
+ **/
+class OMB_UnsupportedServiceException extends Exception {
+
+}
+?>
diff --git a/extlib/libomb/xrds_mapper.php b/extlib/libomb/xrds_mapper.php
new file mode 100755 (executable)
index 0000000..7552154
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Map XRDS actions to URLs
+ *
+ * This interface specifies classes which write the XRDS file announcing
+ * the OMB server. An instance of an implementing class should be passed to
+ * OMB_Service_Provider->writeXRDS.
+ *
+ * PHP version 5
+ *
+ * LICENSE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   OMB
+ * @author    Adrian Lang <mail@adrianlang.de>
+ * @copyright 2009 Adrian Lang
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
+ **/
+
+interface OMB_XRDS_Mapper {
+  public function getURL($action);
+}
+?>
diff --git a/extlib/libomb/xrds_writer.php b/extlib/libomb/xrds_writer.php
new file mode 100755 (executable)
index 0000000..31b451b
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Write OMB-specific XRDS
+ *
+ * This interface specifies classes which write the XRDS file announcing
+ * the OMB server. An instance of an implementing class should be passed to
+ * OMB_Service_Provider->writeXRDS.
+ *
+ * PHP version 5
+ *
+ * LICENSE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   OMB
+ * @author    Adrian Lang <mail@adrianlang.de>
+ * @copyright 2009 Adrian Lang
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL 3.0
+ **/
+
+interface OMB_XRDS_Writer {
+  public function writeXRDS($user, $mapper);
+}
+?>
index c1245136e917e185118e72589d6a96f3cdb1c94d..b9ce43c607bbca4a852d362e961e9d015fa18a21 100644 (file)
--- a/index.php
+++ b/index.php
@@ -73,7 +73,7 @@ function handleError($error)
     exit(-1);
 }
 
-function checkMirror($action_obj)
+function checkMirror($action_obj, $args)
 {
     global $config;
 
@@ -120,6 +120,25 @@ function isLoginAction($action)
 
 function main()
 {
+    // fake HTTP redirects using lighttpd's 404 redirects
+    if (strpos($_SERVER['SERVER_SOFTWARE'], 'lighttpd') !== false) {
+        $_lighty_url = $base_url.$_SERVER['REQUEST_URI'];
+        $_lighty_url = @parse_url($_lighty_url);
+
+        if ($_lighty_url['path'] != '/index.php' && $_lighty_url['path'] != '/') {
+            $_lighty_path = preg_replace('/^'.preg_quote(common_config('site','path')).'\//', '', substr($_lighty_url['path'], 1));
+            $_SERVER['QUERY_STRING'] = 'p='.$_lighty_path;
+            if ($_lighty_url['query'])
+                $_SERVER['QUERY_STRING'] .= '&'.$_lighty_url['query'];
+            parse_str($_lighty_url['query'], $_lighty_query);
+            foreach ($_lighty_query as $key => $val) {
+                $_GET[$key] = $_REQUEST[$key] = $val;
+            }
+            $_GET['p'] = $_REQUEST['p'] = $_lighty_path;
+        }
+    }
+    $_SERVER['REDIRECT_URL'] = preg_replace("/\?.+$/", "", $_SERVER['REQUEST_URI']);
+
     // quick check for fancy URL auto-detection support in installer.
     if (isset($_SERVER['REDIRECT_URL']) && (preg_replace("/^\/$/","",(dirname($_SERVER['REQUEST_URI']))) . '/check-fancy') === $_SERVER['REDIRECT_URL']) {
         die("Fancy URL support detection succeeded. We suggest you enable this to get fancy (pretty) URLs.");
@@ -191,7 +210,7 @@ function main()
     } else {
         $action_obj = new $action_class();
 
-        checkMirror($action_obj);
+        checkMirror($action_obj, $args);
 
         try {
             if ($action_obj->prepare($args)) {
index c222afa7b5fc34083be68b358c6f50a87891c12d..1826a8494bc18b9f575efb54ab3647f5d8f890eb 100644 (file)
@@ -163,7 +163,7 @@ E_O_T;
 function updateStatus($status, $error=false)
 {
 ?>
-                <li <?php echo ($error) ? 'class="error"': ''; ?>><?print $status;?></li>
+                <li <?php echo ($error) ? 'class="error"': ''; ?>><?php echo $status;?></li>
 
 <?php
 }
@@ -180,6 +180,9 @@ function handlePost()
     $password = $_POST['password'];
     $sitename = $_POST['sitename'];
     $fancy    = !empty($_POST['fancy']);
+    $server = $_SERVER['HTTP_HOST'];
+    $path = substr(dirname($_SERVER['PHP_SELF']), 1);
+    
 ?>
     <dl class="system_notice">
         <dt>Page notice</dt>
@@ -219,20 +222,42 @@ function handlePost()
     }
     
     switch($dbtype) {
-      case 'mysql':    mysql_db_installer($host, $database, $username, $password, $sitename, $fancy);
-      break;
-      case 'pgsql':    pgsql_db_installer($host, $database, $username, $password, $sitename, $fancy);
-      break;
-      default:
+        case 'mysql':
+            $db = mysql_db_installer($host, $database, $username, $password);
+            break;
+        case 'pgsql':
+            $db = pgsql_db_installer($host, $database, $username, $password);
+            break;
+        default:
+    }
+    
+    if (!$db) {
+        // database connection failed, do not move on to create config file.
+        return false;
+    }
+    
+    updateStatus("Writing config file...");
+    $res = writeConf($sitename, $server, $path, $fancy, $db);
+    
+    if (!$res) {
+        updateStatus("Can't write config file.", true);
+        showForm();
+        return;
     }
-    if ($path) $path .= '/';
-    updateStatus("You can visit your <a href='/$path'>new Laconica site</a>.");
+    
+    /*
+        TODO https needs to be considered
+    */
+    $link = "http://".$server.'/'.$path;
+    
+    updateStatus("Laconica has been installed at $link");
+    updateStatus("You can visit your <a href='$link'>new Laconica site</a>.");
 ?>
 
 <?php
 }
 
-function pgsql_db_installer($host, $database, $username, $password, $sitename, $fancy) {
+function pgsql_db_installer($host, $database, $username, $password) {
   $connstring = "dbname=$database host=$host user=$username";
 
   //No password would mean trust authentication used.
@@ -265,7 +290,7 @@ function pgsql_db_installer($host, $database, $username, $password, $sitename, $
   if ($res === false) {
       updateStatus("Can't run database script.", true);
       showForm();
-      return;
+      return false;
   }
   foreach (array('sms_carrier' => 'SMS carrier',
                 'notice_source' => 'notice source',
@@ -276,29 +301,24 @@ function pgsql_db_installer($host, $database, $username, $password, $sitename, $
       if ($res === false) {
           updateStatus(sprintf("Can't run %d script.", $name), true);
           showForm();
-          return;
+          return false;
       }
   }
   pg_query($conn, 'COMMIT');
 
-  updateStatus("Writing config file...");
   if (empty($password)) {
     $sqlUrl = "pgsql://$username@$host/$database";
   }
   else {
     $sqlUrl = "pgsql://$username:$password@$host/$database";
   }
-  $res = writeConf($sitename, $sqlUrl, $fancy, 'pgsql');
-  if (!$res) {
-      updateStatus("Can't write config file.", true);
-      showForm();
-      return;
-  }
-  updateStatus("Done!");
-      
+  
+  $db = array('type' => 'pgsql', 'database' => $sqlUrl);
+  
+  return $db;
 }
 
-function mysql_db_installer($host, $database, $username, $password, $sitename, $fancy) {
+function mysql_db_installer($host, $database, $username, $password) {
   updateStatus("Starting installation...");
   updateStatus("Checking database...");
 
@@ -306,21 +326,21 @@ function mysql_db_installer($host, $database, $username, $password, $sitename, $
   if (!$conn) {
       updateStatus("Can't connect to server '$host' as '$username'.", true);
       showForm();
-      return;
+      return false;
   }
   updateStatus("Changing to database...");
   $res = mysql_select_db($database, $conn);
   if (!$res) {
       updateStatus("Can't change to database.", true);
       showForm();
-      return;
+      return false;
   }
   updateStatus("Running database script...");
   $res = runDbScript(INSTALLDIR.'/db/laconica.sql', $conn);
   if ($res === false) {
       updateStatus("Can't run database script.", true);
       showForm();
-      return;
+      return false;
   }
   foreach (array('sms_carrier' => 'SMS carrier',
                 'notice_source' => 'notice source',
@@ -331,35 +351,44 @@ function mysql_db_installer($host, $database, $username, $password, $sitename, $
       if ($res === false) {
           updateStatus(sprintf("Can't run %d script.", $name), true);
           showForm();
-          return;
+          return false;
       }
   }
       
-      updateStatus("Writing config file...");
       $sqlUrl = "mysqli://$username:$password@$host/$database";
-      $res = writeConf($sitename, $sqlUrl, $fancy);
-      if (!$res) {
-          updateStatus("Can't write config file.", true);
-          showForm();
-          return;
-      }
-      updateStatus("Done!");
-    }
-function writeConf($sitename, $sqlUrl, $fancy, $type='mysql')
+      $db = array('type' => 'mysql', 'database' => $sqlUrl);
+      return $db;
+}
+
+function writeConf($sitename, $server, $path, $fancy, $db)
 {
-    $res = file_put_contents(INSTALLDIR.'/config.php',
-                             "<?php\n".
-                             "if (!defined('LACONICA')) { exit(1); }\n\n".
-                             "\$config['site']['name'] = \"$sitename\";\n\n".
-                             ($fancy ? "\$config['site']['fancy'] = true;\n\n":'').
-                             "\$config['db']['database'] = \"$sqlUrl\";\n\n".
-                             ($type == 'pgsql' ? "\$config['db']['quote_identifiers'] = true;\n\n" .
-                             "\$config['db']['type'] = \"$type\";\n\n" : '').
-                             "?>");
+    // assemble configuration file in a string
+    $cfg =  "<?php\n".
+            "if (!defined('LACONICA')) { exit(1); }\n\n".
+            
+            // site name
+            "\$config['site']['name'] = '$sitename';\n\n".
+            
+            // site location
+            "\$config['site']['server'] = '$server';\n".
+            "\$config['site']['path'] = '$path'; \n\n".
+            
+            // checks if fancy URLs are enabled
+            ($fancy ? "\$config['site']['fancy'] = true;\n\n":'').
+            
+            // database
+            "\$config['db']['database'] = '{$db['database']}';\n\n".
+            ($type == 'pgsql' ? "\$config['db']['quote_identifiers'] = true;\n\n":'').
+            "\$config['db']['type'] = '{$db['type']}';\n\n".
+            
+            "?>";
+    // write configuration file out to install directory
+    $res = file_put_contents(INSTALLDIR.'/config.php', $cfg);
+
     return $res;
 }
 
-function runDbScript($filename, $conn, $type='mysql')
+function runDbScript($filename, $conn, $type = 'mysql')
 {
     $sql = trim(file_get_contents($filename));
     $stmts = explode(';', $sql);
@@ -368,10 +397,15 @@ function runDbScript($filename, $conn, $type='mysql')
         if (!mb_strlen($stmt)) {
             continue;
         }
-        if ($type == 'mysql') {
-          $res = mysql_query($stmt, $conn);
-        } elseif ($type=='pgsql') {
-          $res = pg_query($conn, $stmt);
+        switch ($type) {
+        case 'mysql':
+            $res = mysql_query($stmt, $conn);
+            break;
+        case 'pgsql':
+            $res = pg_query($conn, $stmt);
+            break;
+        default:
+            updateStatus("runDbScript() error: unknown database type ". $type ." provided.");
         }
         if ($res === false) {
             updateStatus("FAILED SQL: $stmt");
diff --git a/js/jcrop/jquery.Jcrop.min.js b/js/jcrop/jquery.Jcrop.min.js
new file mode 100644 (file)
index 0000000..9002b97
--- /dev/null
@@ -0,0 +1,163 @@
+/**
+ * Jcrop v.0.9.8 (minimized)
+ * (c) 2008 Kelly Hallman and DeepLiquid.com
+ * More information: http://deepliquid.com/content/Jcrop.html
+ * Released under MIT License - this header must remain with code
+ */
+
+
+(function($){$.Jcrop=function(obj,opt)
+{var obj=obj,opt=opt;if(typeof(obj)!=='object')obj=$(obj)[0];if(typeof(opt)!=='object')opt={};if(!('trackDocument'in opt))
+{opt.trackDocument=$.browser.msie?false:true;if($.browser.msie&&$.browser.version.split('.')[0]=='8')
+opt.trackDocument=true;}
+if(!('keySupport'in opt))
+opt.keySupport=$.browser.msie?false:true;var defaults={trackDocument:false,baseClass:'jcrop',addClass:null,bgColor:'black',bgOpacity:.6,borderOpacity:.4,handleOpacity:.5,handlePad:5,handleSize:9,handleOffset:5,edgeMargin:14,aspectRatio:0,keySupport:true,cornerHandles:true,sideHandles:true,drawBorders:true,dragEdges:true,boxWidth:0,boxHeight:0,boundary:8,animationDelay:20,swingSpeed:3,allowSelect:true,allowMove:true,allowResize:true,minSelect:[0,0],maxSize:[0,0],minSize:[0,0],onChange:function(){},onSelect:function(){}};var options=defaults;setOptions(opt);var $origimg=$(obj);var $img=$origimg.clone().removeAttr('id').css({position:'absolute'});$img.width($origimg.width());$img.height($origimg.height());$origimg.after($img).hide();presize($img,options.boxWidth,options.boxHeight);var boundx=$img.width(),boundy=$img.height(),$div=$('<div />').width(boundx).height(boundy).addClass(cssClass('holder')).css({position:'relative',backgroundColor:options.bgColor}).insertAfter($origimg).append($img);;if(options.addClass)$div.addClass(options.addClass);var $img2=$('<img />').attr('src',$img.attr('src')).css('position','absolute').width(boundx).height(boundy);var $img_holder=$('<div />').width(pct(100)).height(pct(100)).css({zIndex:310,position:'absolute',overflow:'hidden'}).append($img2);var $hdl_holder=$('<div />').width(pct(100)).height(pct(100)).css('zIndex',320);var $sel=$('<div />').css({position:'absolute',zIndex:300}).insertBefore($img).append($img_holder,$hdl_holder);var bound=options.boundary;var $trk=newTracker().width(boundx+(bound*2)).height(boundy+(bound*2)).css({position:'absolute',top:px(-bound),left:px(-bound),zIndex:290}).mousedown(newSelection);var xlimit,ylimit,xmin,ymin;var xscale,yscale,enabled=true;var docOffset=getPos($img),btndown,lastcurs,dimmed,animating,shift_down;var Coords=function()
+{var x1=0,y1=0,x2=0,y2=0,ox,oy;function setPressed(pos)
+{var pos=rebound(pos);x2=x1=pos[0];y2=y1=pos[1];};function setCurrent(pos)
+{var pos=rebound(pos);ox=pos[0]-x2;oy=pos[1]-y2;x2=pos[0];y2=pos[1];};function getOffset()
+{return[ox,oy];};function moveOffset(offset)
+{var ox=offset[0],oy=offset[1];if(0>x1+ox)ox-=ox+x1;if(0>y1+oy)oy-=oy+y1;if(boundy<y2+oy)oy+=boundy-(y2+oy);if(boundx<x2+ox)ox+=boundx-(x2+ox);x1+=ox;x2+=ox;y1+=oy;y2+=oy;};function getCorner(ord)
+{var c=getFixed();switch(ord)
+{case'ne':return[c.x2,c.y];case'nw':return[c.x,c.y];case'se':return[c.x2,c.y2];case'sw':return[c.x,c.y2];}};function getFixed()
+{if(!options.aspectRatio)return getRect();var aspect=options.aspectRatio,min_x=options.minSize[0]/xscale,min_y=options.minSize[1]/yscale,max_x=options.maxSize[0]/xscale,max_y=options.maxSize[1]/yscale,rw=x2-x1,rh=y2-y1,rwa=Math.abs(rw),rha=Math.abs(rh),real_ratio=rwa/rha,xx,yy;if(max_x==0){max_x=boundx*10}
+if(max_y==0){max_y=boundy*10}
+if(real_ratio<aspect)
+{yy=y2;w=rha*aspect;xx=rw<0?x1-w:w+x1;if(xx<0)
+{xx=0;h=Math.abs((xx-x1)/aspect);yy=rh<0?y1-h:h+y1;}
+else if(xx>boundx)
+{xx=boundx;h=Math.abs((xx-x1)/aspect);yy=rh<0?y1-h:h+y1;}}
+else
+{xx=x2;h=rwa/aspect;yy=rh<0?y1-h:y1+h;if(yy<0)
+{yy=0;w=Math.abs((yy-y1)*aspect);xx=rw<0?x1-w:w+x1;}
+else if(yy>boundy)
+{yy=boundy;w=Math.abs(yy-y1)*aspect;xx=rw<0?x1-w:w+x1;}}
+if(xx>x1){if(xx-x1<min_x){xx=x1+min_x;}else if(xx-x1>max_x){xx=x1+max_x;}
+if(yy>y1){yy=y1+(xx-x1)/aspect;}else{yy=y1-(xx-x1)/aspect;}}else if(xx<x1){if(x1-xx<min_x){xx=x1-min_x}else if(x1-xx>max_x){xx=x1-max_x;}
+if(yy>y1){yy=y1+(x1-xx)/aspect;}else{yy=y1-(x1-xx)/aspect;}}
+if(xx<0){x1-=xx;xx=0;}else if(xx>boundx){x1-=xx-boundx;xx=boundx;}
+if(yy<0){y1-=yy;yy=0;}else if(yy>boundy){y1-=yy-boundy;yy=boundy;}
+return last=makeObj(flipCoords(x1,y1,xx,yy));};function rebound(p)
+{if(p[0]<0)p[0]=0;if(p[1]<0)p[1]=0;if(p[0]>boundx)p[0]=boundx;if(p[1]>boundy)p[1]=boundy;return[p[0],p[1]];};function flipCoords(x1,y1,x2,y2)
+{var xa=x1,xb=x2,ya=y1,yb=y2;if(x2<x1)
+{xa=x2;xb=x1;}
+if(y2<y1)
+{ya=y2;yb=y1;}
+return[Math.round(xa),Math.round(ya),Math.round(xb),Math.round(yb)];};function getRect()
+{var xsize=x2-x1;var ysize=y2-y1;if(xlimit&&(Math.abs(xsize)>xlimit))
+x2=(xsize>0)?(x1+xlimit):(x1-xlimit);if(ylimit&&(Math.abs(ysize)>ylimit))
+y2=(ysize>0)?(y1+ylimit):(y1-ylimit);if(ymin&&(Math.abs(ysize)<ymin))
+y2=(ysize>0)?(y1+ymin):(y1-ymin);if(xmin&&(Math.abs(xsize)<xmin))
+x2=(xsize>0)?(x1+xmin):(x1-xmin);if(x1<0){x2-=x1;x1-=x1;}
+if(y1<0){y2-=y1;y1-=y1;}
+if(x2<0){x1-=x2;x2-=x2;}
+if(y2<0){y1-=y2;y2-=y2;}
+if(x2>boundx){var delta=x2-boundx;x1-=delta;x2-=delta;}
+if(y2>boundy){var delta=y2-boundy;y1-=delta;y2-=delta;}
+if(x1>boundx){var delta=x1-boundy;y2-=delta;y1-=delta;}
+if(y1>boundy){var delta=y1-boundy;y2-=delta;y1-=delta;}
+return makeObj(flipCoords(x1,y1,x2,y2));};function makeObj(a)
+{return{x:a[0],y:a[1],x2:a[2],y2:a[3],w:a[2]-a[0],h:a[3]-a[1]};};return{flipCoords:flipCoords,setPressed:setPressed,setCurrent:setCurrent,getOffset:getOffset,moveOffset:moveOffset,getCorner:getCorner,getFixed:getFixed};}();var Selection=function()
+{var start,end,dragmode,awake,hdep=370;var borders={};var handle={};var seehandles=false;var hhs=options.handleOffset;if(options.drawBorders){borders={top:insertBorder('hline').css('top',$.browser.msie?px(-1):px(0)),bottom:insertBorder('hline'),left:insertBorder('vline'),right:insertBorder('vline')};}
+if(options.dragEdges){handle.t=insertDragbar('n');handle.b=insertDragbar('s');handle.r=insertDragbar('e');handle.l=insertDragbar('w');}
+options.sideHandles&&createHandles(['n','s','e','w']);options.cornerHandles&&createHandles(['sw','nw','ne','se']);function insertBorder(type)
+{var jq=$('<div />').css({position:'absolute',opacity:options.borderOpacity}).addClass(cssClass(type));$img_holder.append(jq);return jq;};function dragDiv(ord,zi)
+{var jq=$('<div />').mousedown(createDragger(ord)).css({cursor:ord+'-resize',position:'absolute',zIndex:zi});$hdl_holder.append(jq);return jq;};function insertHandle(ord)
+{return dragDiv(ord,hdep++).css({top:px(-hhs+1),left:px(-hhs+1),opacity:options.handleOpacity}).addClass(cssClass('handle'));};function insertDragbar(ord)
+{var s=options.handleSize,o=hhs,h=s,w=s,t=o,l=o;switch(ord)
+{case'n':case's':w=pct(100);break;case'e':case'w':h=pct(100);break;}
+return dragDiv(ord,hdep++).width(w).height(h).css({top:px(-t+1),left:px(-l+1)});};function createHandles(li)
+{for(i in li)handle[li[i]]=insertHandle(li[i]);};function moveHandles(c)
+{var midvert=Math.round((c.h/2)-hhs),midhoriz=Math.round((c.w/2)-hhs),north=west=-hhs+1,east=c.w-hhs,south=c.h-hhs,x,y;'e'in handle&&handle.e.css({top:px(midvert),left:px(east)})&&handle.w.css({top:px(midvert)})&&handle.s.css({top:px(south),left:px(midhoriz)})&&handle.n.css({left:px(midhoriz)});'ne'in handle&&handle.ne.css({left:px(east)})&&handle.se.css({top:px(south),left:px(east)})&&handle.sw.css({top:px(south)});'b'in handle&&handle.b.css({top:px(south)})&&handle.r.css({left:px(east)});};function moveto(x,y)
+{$img2.css({top:px(-y),left:px(-x)});$sel.css({top:px(y),left:px(x)});};function resize(w,h)
+{$sel.width(w).height(h);};function refresh()
+{var c=Coords.getFixed();Coords.setPressed([c.x,c.y]);Coords.setCurrent([c.x2,c.y2]);updateVisible();};function updateVisible()
+{if(awake)return update();};function update()
+{var c=Coords.getFixed();resize(c.w,c.h);moveto(c.x,c.y);options.drawBorders&&borders['right'].css({left:px(c.w-1)})&&borders['bottom'].css({top:px(c.h-1)});seehandles&&moveHandles(c);awake||show();options.onChange(unscale(c));};function show()
+{$sel.show();$img.css('opacity',options.bgOpacity);awake=true;};function release()
+{disableHandles();$sel.hide();$img.css('opacity',1);awake=false;};function showHandles()
+{if(seehandles)
+{moveHandles(Coords.getFixed());$hdl_holder.show();}};function enableHandles()
+{seehandles=true;if(options.allowResize)
+{moveHandles(Coords.getFixed());$hdl_holder.show();return true;}};function disableHandles()
+{seehandles=false;$hdl_holder.hide();};function animMode(v)
+{(animating=v)?disableHandles():enableHandles();};function done()
+{animMode(false);refresh();};var $track=newTracker().mousedown(createDragger('move')).css({cursor:'move',position:'absolute',zIndex:360})
+$img_holder.append($track);disableHandles();return{updateVisible:updateVisible,update:update,release:release,refresh:refresh,setCursor:function(cursor){$track.css('cursor',cursor);},enableHandles:enableHandles,enableOnly:function(){seehandles=true;},showHandles:showHandles,disableHandles:disableHandles,animMode:animMode,done:done};}();var Tracker=function()
+{var onMove=function(){},onDone=function(){},trackDoc=options.trackDocument;if(!trackDoc)
+{$trk.mousemove(trackMove).mouseup(trackUp).mouseout(trackUp);}
+function toFront()
+{$trk.css({zIndex:450});if(trackDoc)
+{$(document).mousemove(trackMove).mouseup(trackUp);}}
+function toBack()
+{$trk.css({zIndex:290});if(trackDoc)
+{$(document).unbind('mousemove',trackMove).unbind('mouseup',trackUp);}}
+function trackMove(e)
+{onMove(mouseAbs(e));};function trackUp(e)
+{e.preventDefault();e.stopPropagation();if(btndown)
+{btndown=false;onDone(mouseAbs(e));options.onSelect(unscale(Coords.getFixed()));toBack();onMove=function(){};onDone=function(){};}
+return false;};function activateHandlers(move,done)
+{btndown=true;onMove=move;onDone=done;toFront();return false;};function setCursor(t){$trk.css('cursor',t);};$img.before($trk);return{activateHandlers:activateHandlers,setCursor:setCursor};}();var KeyManager=function()
+{var $keymgr=$('<input type="radio" />').css({position:'absolute',left:'-30px'}).keypress(parseKey).blur(onBlur),$keywrap=$('<div />').css({position:'absolute',overflow:'hidden'}).append($keymgr);function watchKeys()
+{if(options.keySupport)
+{$keymgr.show();$keymgr.focus();}};function onBlur(e)
+{$keymgr.hide();};function doNudge(e,x,y)
+{if(options.allowMove){Coords.moveOffset([x,y]);Selection.updateVisible();};e.preventDefault();e.stopPropagation();};function parseKey(e)
+{if(e.ctrlKey)return true;shift_down=e.shiftKey?true:false;var nudge=shift_down?10:1;switch(e.keyCode)
+{case 37:doNudge(e,-nudge,0);break;case 39:doNudge(e,nudge,0);break;case 38:doNudge(e,0,-nudge);break;case 40:doNudge(e,0,nudge);break;case 27:Selection.release();break;case 9:return true;}
+return nothing(e);};if(options.keySupport)$keywrap.insertBefore($img);return{watchKeys:watchKeys};}();function px(n){return''+parseInt(n)+'px';};function pct(n){return''+parseInt(n)+'%';};function cssClass(cl){return options.baseClass+'-'+cl;};function getPos(obj)
+{var pos=$(obj).offset();return[pos.left,pos.top];};function mouseAbs(e)
+{return[(e.pageX-docOffset[0]),(e.pageY-docOffset[1])];};function myCursor(type)
+{if(type!=lastcurs)
+{Tracker.setCursor(type);lastcurs=type;}};function startDragMode(mode,pos)
+{docOffset=getPos($img);Tracker.setCursor(mode=='move'?mode:mode+'-resize');if(mode=='move')
+return Tracker.activateHandlers(createMover(pos),doneSelect);var fc=Coords.getFixed();var opp=oppLockCorner(mode);var opc=Coords.getCorner(oppLockCorner(opp));Coords.setPressed(Coords.getCorner(opp));Coords.setCurrent(opc);Tracker.activateHandlers(dragmodeHandler(mode,fc),doneSelect);};function dragmodeHandler(mode,f)
+{return function(pos){if(!options.aspectRatio)switch(mode)
+{case'e':pos[1]=f.y2;break;case'w':pos[1]=f.y2;break;case'n':pos[0]=f.x2;break;case's':pos[0]=f.x2;break;}
+else switch(mode)
+{case'e':pos[1]=f.y+1;break;case'w':pos[1]=f.y+1;break;case'n':pos[0]=f.x+1;break;case's':pos[0]=f.x+1;break;}
+Coords.setCurrent(pos);Selection.update();};};function createMover(pos)
+{var lloc=pos;KeyManager.watchKeys();return function(pos)
+{Coords.moveOffset([pos[0]-lloc[0],pos[1]-lloc[1]]);lloc=pos;Selection.update();};};function oppLockCorner(ord)
+{switch(ord)
+{case'n':return'sw';case's':return'nw';case'e':return'nw';case'w':return'ne';case'ne':return'sw';case'nw':return'se';case'se':return'nw';case'sw':return'ne';};};function createDragger(ord)
+{return function(e){if(options.disabled)return false;if((ord=='move')&&!options.allowMove)return false;btndown=true;startDragMode(ord,mouseAbs(e));e.stopPropagation();e.preventDefault();return false;};};function presize($obj,w,h)
+{var nw=$obj.width(),nh=$obj.height();if((nw>w)&&w>0)
+{nw=w;nh=(w/$obj.width())*$obj.height();}
+if((nh>h)&&h>0)
+{nh=h;nw=(h/$obj.height())*$obj.width();}
+xscale=$obj.width()/nw;yscale=$obj.height()/nh;$obj.width(nw).height(nh);};function unscale(c)
+{return{x:parseInt(c.x*xscale),y:parseInt(c.y*yscale),x2:parseInt(c.x2*xscale),y2:parseInt(c.y2*yscale),w:parseInt(c.w*xscale),h:parseInt(c.h*yscale)};};function doneSelect(pos)
+{var c=Coords.getFixed();if(c.w>options.minSelect[0]&&c.h>options.minSelect[1])
+{Selection.enableHandles();Selection.done();}
+else
+{Selection.release();}
+Tracker.setCursor(options.allowSelect?'crosshair':'default');};function newSelection(e)
+{if(options.disabled)return false;if(!options.allowSelect)return false;btndown=true;docOffset=getPos($img);Selection.disableHandles();myCursor('crosshair');var pos=mouseAbs(e);Coords.setPressed(pos);Tracker.activateHandlers(selectDrag,doneSelect);KeyManager.watchKeys();Selection.update();e.stopPropagation();e.preventDefault();return false;};function selectDrag(pos)
+{Coords.setCurrent(pos);Selection.update();};function newTracker()
+{var trk=$('<div></div>').addClass(cssClass('tracker'));$.browser.msie&&trk.css({opacity:0,backgroundColor:'white'});return trk;};function animateTo(a)
+{var x1=a[0]/xscale,y1=a[1]/yscale,x2=a[2]/xscale,y2=a[3]/yscale;if(animating)return;var animto=Coords.flipCoords(x1,y1,x2,y2);var c=Coords.getFixed();var animat=initcr=[c.x,c.y,c.x2,c.y2];var interv=options.animationDelay;var x=animat[0];var y=animat[1];var x2=animat[2];var y2=animat[3];var ix1=animto[0]-initcr[0];var iy1=animto[1]-initcr[1];var ix2=animto[2]-initcr[2];var iy2=animto[3]-initcr[3];var pcent=0;var velocity=options.swingSpeed;Selection.animMode(true);var animator=function()
+{return function()
+{pcent+=(100-pcent)/velocity;animat[0]=x+((pcent/100)*ix1);animat[1]=y+((pcent/100)*iy1);animat[2]=x2+((pcent/100)*ix2);animat[3]=y2+((pcent/100)*iy2);if(pcent<100)animateStart();else Selection.done();if(pcent>=99.8)pcent=100;setSelectRaw(animat);};}();function animateStart()
+{window.setTimeout(animator,interv);};animateStart();};function setSelect(rect)
+{setSelectRaw([rect[0]/xscale,rect[1]/yscale,rect[2]/xscale,rect[3]/yscale]);};function setSelectRaw(l)
+{Coords.setPressed([l[0],l[1]]);Coords.setCurrent([l[2],l[3]]);Selection.update();};function setOptions(opt)
+{if(typeof(opt)!='object')opt={};options=$.extend(options,opt);if(typeof(options.onChange)!=='function')
+options.onChange=function(){};if(typeof(options.onSelect)!=='function')
+options.onSelect=function(){};};function tellSelect()
+{return unscale(Coords.getFixed());};function tellScaled()
+{return Coords.getFixed();};function setOptionsNew(opt)
+{setOptions(opt);interfaceUpdate();};function disableCrop()
+{options.disabled=true;Selection.disableHandles();Selection.setCursor('default');Tracker.setCursor('default');};function enableCrop()
+{options.disabled=false;interfaceUpdate();};function cancelCrop()
+{Selection.done();Tracker.activateHandlers(null,null);};function destroy()
+{$div.remove();$origimg.show();};function interfaceUpdate(alt)
+{options.allowResize?alt?Selection.enableOnly():Selection.enableHandles():Selection.disableHandles();Tracker.setCursor(options.allowSelect?'crosshair':'default');Selection.setCursor(options.allowMove?'move':'default');$div.css('backgroundColor',options.bgColor);if('setSelect'in options){setSelect(opt.setSelect);Selection.done();delete(options.setSelect);}
+if('trueSize'in options){xscale=options.trueSize[0]/boundx;yscale=options.trueSize[1]/boundy;}
+xlimit=options.maxSize[0]||0;ylimit=options.maxSize[1]||0;xmin=options.minSize[0]||0;ymin=options.minSize[1]||0;if('outerImage'in options)
+{$img.attr('src',options.outerImage);delete(options.outerImage);}
+Selection.refresh();};$hdl_holder.hide();interfaceUpdate(true);var api={animateTo:animateTo,setSelect:setSelect,setOptions:setOptionsNew,tellSelect:tellSelect,tellScaled:tellScaled,disable:disableCrop,enable:enableCrop,cancel:cancelCrop,focus:KeyManager.watchKeys,getBounds:function(){return[boundx*xscale,boundy*yscale];},getWidgetSize:function(){return[boundx,boundy];},release:Selection.release,destroy:destroy};$origimg.data('Jcrop',api);return api;};$.fn.Jcrop=function(options)
+{function attachWhenDone(from)
+{var loadsrc=options.useImg||from.src;var img=new Image();img.onload=function(){$.Jcrop(from,options);};img.src=loadsrc;};if(typeof(options)!=='object')options={};this.each(function()
+{if($(this).data('Jcrop'))
+{if(options=='api')return $(this).data('Jcrop');else $(this).data('Jcrop').setOptions(options);}
+else attachWhenDone(this);});return this;};})(jQuery);
\ No newline at end of file
diff --git a/js/jcrop/jquery.Jcrop.pack.js b/js/jcrop/jquery.Jcrop.pack.js
deleted file mode 100644 (file)
index aa82e8a..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-/**
- * Jcrop v.0.9.5 (packed)
- * (c) 2008 Kelly Hallman and DeepLiquid.com
- * More information: http://deepliquid.com/content/Jcrop.html
- * Released under MIT License - this header must remain with code
- */
-
-eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('$.1n=7(G,F){d G=G,F=F;g(1p(G)!==\'2d\')G=$(G)[0];g(1p(F)!==\'2d\')F={};g(!(\'2x\'1a F))F.2x=$.3d.3e?K:M;g(!(\'2c\'1a F))F.2c=$.3d.3e?K:M;d 4f={2x:K,3W:\'4C\',1f:4D,3T:\'4Y\',3x:.6,3O:.4,3P:.5,53:5,3N:9,3D:5,51:14,25:0,2c:M,3I:M,3B:M,30:M,3A:M,49:0,4p:0,4k:8,3V:20,3X:3,2f:K,3n:[0,0],2z:[0,0],2O:[0,0],2D:7(){},2G:7(){}};d j=4f;21(F);d $I=$(G).B({16:\'1b\'});47($I,j.49,j.4p);d S=$I.W(),L=$I.U(),$12=$(\'<12 />\').W(S).U(L).1f(1L(\'4F\')).B({16:\'4H\',4B:j.3T});g(j.1f)$12.1f(j.1f);$I.54($12);d $34=$(\'<I />\').3Y(\'2N\',$I.3Y(\'2N\')).B(\'16\',\'1b\').W(S).U(L);d $2C=$(\'<12 />\').W(1t(V)).U(1t(V)).B({1l:59,16:\'1b\',4o:\'4g\'}).1P($34);d $2g=$(\'<12 />\').W(1t(V)).U(1t(V)).B({1l:5b});d $28=$(\'<12 />\').B({16:\'1b\',1l:55}).3U($I).1P($2C,$2g);d 23=j.4k;d $1S=$(\'<12 />\').1f(1L(\'3v\')).W(S+(23*2)).U(L+(23*2)).B({16:\'1b\',R:D(-23),P:D(-23),1l:3R,1z:0}).3q(48);d 1I,1Q;d 2u=2Q(G),1q,1B,3i,58,3h,1O;g(\'36\'1a j){1I=j.36[0]/S;1Q=j.36[1]/L}d E=7(){d A=0,u=0,q=0,m=0,Z,Y;7 1A(z){d z=2T(z);q=A=z[0];m=u=z[1]};7 1y(z){d z=2T(z);Z=z[0]-q;Y=z[1]-m;q=z[0];m=z[1]};7 3f(){k[Z,Y]};7 2b(2y){d Z=2y[0],Y=2y[1];g(0>A+Z)Z-=Z+A;g(0>u+Y)Y-=Y+u;g(L<m+Y)Y+=L-(m+Y);g(S<q+Z)Z+=S-(q+Z);A+=Z;q+=Z;u+=Y;m+=Y};7 2K(T){d c=Q();1E(T){C\'1s\':k[c.q,c.y];C\'11\':k[c.x,c.y];C\'2e\':k[c.q,c.m];C\'1M\':k[c.x,c.m]}};7 Q(){g(!j.25&&!1B)k 3F();d 1k=j.25?j.25:1B,5c=j.2O,4u=j.2z,1V=q-A,1Z=m-u,3c=N.17(1V),3j=N.17(1Z),3M=3c/3j,15,13;g(3M<1k){13=m;w=3j*1k;15=1V<0?A-w:w+A;g(15<0){15=0;h=N.17((15-A)/1k);13=1Z<0?u-h:h+u}1g g(15>S){15=S;h=N.17((15-A)/1k);13=1Z<0?u-h:h+u}}1g{15=q;h=3c/1k;13=1Z<0?u-h:u+h;g(13<0){13=0;w=N.17((13-u)*1k);15=1V<0?A-w:w+A}1g g(13>L){13=L;w=N.17(13-u)*1k;15=1V<0?A-w:w+A}}k 4E=3g(1F(A,u,15,13))};7 2T(p){g(p[0]<0)p[0]=0;g(p[1]<0)p[1]=0;g(p[0]>S)p[0]=S;g(p[1]>L)p[1]=L;k[p[0],p[1]]};7 1F(A,u,q,m){d 2R=A,3r=q,3o=u,3l=m;g(q<A){2R=q;3r=A}g(m<u){3o=m;3l=u}k[N.1K(2R),N.1K(3o),N.1K(3r),N.1K(3l)]};7 3F(){d 1U=q-A;d 22=m-u;g(2q&&(N.17(1U)>2q))q=(1U>0)?(A+2q):(A-2q);g(2n&&(N.17(22)>2n))m=(22>0)?(u+2n):(u-2n);g(2i&&(N.17(22)<2i))m=(22>0)?(u+2i):(u-2i);g(2m&&(N.17(1U)<2m))q=(1U>0)?(A+2m):(A-2m);g(A<0){q-=A;A-=A}g(u<0){m-=u;u-=u}g(q<0){A-=q;q-=q}g(m<0){u-=m;m-=m}g(q>S){d X=q-S;A-=X;q-=X}g(m>L){d X=m-L;u-=X;m-=X}g(A>S){d X=A-L;m-=X;u-=X}g(u>L){d X=u-L;m-=X;u-=X}k 3g(1F(A,u,q,m))};7 3g(a){k{x:a[0],y:a[1],q:a[2],m:a[3],w:a[2]-a[0],h:a[3]-a[1]}};k{1F:1F,1A:1A,1y:1y,3f:3f,2b:2b,2K:2K,Q:Q}}();d J=7(){d 4v,4z,4y,1R,2U=4x;d 2F={};d H={};d 2E=K;d 1i=j.3D;g(j.30){2F={R:1Y(\'3C\').B(\'R\',$.3d.3e?D(-1):D(0)),3Q:1Y(\'3C\'),P:1Y(\'3z\'),3L:1Y(\'3z\')}}g(j.3A){H.t=1W(\'n\');H.b=1W(\'s\');H.r=1W(\'e\');H.l=1W(\'w\')}j.3B&&2Y([\'n\',\'s\',\'e\',\'w\']);j.3I&&2Y([\'1M\',\'11\',\'1s\',\'2e\']);7 1Y(1u){d 1J=$(\'<12 />\').B({16:\'1b\',1z:j.3O}).1f(1L(1u));$2C.1P(1J);k 1J};7 2W(T,3y){d 1J=$(\'<12 />\').3q(3b(T)).B({3p:T+\'-2A\',16:\'1b\',1l:3y});$2g.1P(1J);k 1J};7 3J(T){k 2W(T,2U++).B({R:D(-1i+1),P:D(-1i+1),1z:j.3P}).1f(1L(\'H\'))};7 1W(T){d s=j.3N,o=1i,h=s,w=s,t=o,l=o;1E(T){C\'n\':C\'s\':w=1t(V);O;C\'e\':C\'w\':h=1t(V);O}k 2W(T,2U++).W(w).U(h).B({R:D(-t+1),P:D(-l+1)})};7 2Y(2J){4U(i 1a 2J)H[2J[i]]=3J(2J[i])};7 31(c){d 3a=N.1K((c.h/2)-1i),35=N.1K((c.w/2)-1i),4V=4W=-1i+1,2a=c.w-1i,1X=c.h-1i,x,y;\'e\'1a H&&H.e.B({R:D(3a),P:D(2a)})&&H.w.B({R:D(3a)})&&H.s.B({R:D(1X),P:D(35)})&&H.n.B({P:D(35)});\'1s\'1a H&&H.1s.B({P:D(2a)})&&H.2e.B({R:D(1X),P:D(2a)})&&H.1M.B({R:D(1X)});\'b\'1a H&&H.b.B({R:D(1X)})&&H.r.B({P:D(2a)})};7 3K(x,y){$34.B({R:D(-y),P:D(-x)});$28.B({R:D(y),P:D(x)})};7 2A(w,h){$28.W(w).U(h)};7 3s(){d p=E.Q();E.1A([p.x,p.y]);E.1y([p.q,p.m])};7 2I(){g(1R)k 1e()};7 1e(){d c=E.Q();2A(c.w,c.h);3K(c.x,c.y);j.30&&2F[\'3L\'].B({P:D(c.w-1)})&&2F[\'3Q\'].B({R:D(c.h-1)});2E&&31(c);1R||1w();j.2D(2H(c))};7 1w(){$28.1w();$I.B(\'1z\',j.3x);1R=M};7 1r(){1o();$28.1v();$I.B(\'1z\',1);1R=K};7 1v(){1r();$I.B(\'1z\',1);1R=K};7 2t(){2E=M;31(E.Q());$2g.1w()};7 1o(){2E=K;$2g.1v()};7 2o(v){(3h=v)?1o():2t()};7 1h(){d c=E.Q();2o(K);3s()};1o();$2C.1P($(\'<12 />\').1f(1L(\'3v\')).3q(3b(\'1N\')).B({3p:\'1N\',16:\'1b\',1l:4M,1z:0}));k{2I:2I,1e:1e,1r:1r,1w:1w,1v:1v,2t:2t,1o:1o,2o:2o,1h:1h}}();d 1j=7(){d 2w=7(){},2v=7(){},2L=j.2x;g(!2L){$1S.3k(2B).2S(26).4N(26)}7 4j(){g(2L){$(3t).3k(2B).2S(26)}$1S.B({1l:4G})}7 4i(){g(2L){$(3t).3H(\'3k\',2B).3H(\'2S\',26)}$1S.B({1l:3R})}7 2B(e){2w(2r(e))};7 26(e){e.2j();e.2k();g(1q){1q=K;2v(2r(e));j.2G(2H(E.Q()));4i();2w=7(){};2v=7(){}}k K};7 1G(1N,1h){1q=M;2w=1N;2v=1h;4j();k K};7 1x(t){$1S.B(\'3p\',t)};$I.4s($1S);k{1G:1G,1x:1x}}();d 33=7(){d $24=$(\'<4w 1u="4L" />\').B({16:\'1b\',P:\'-4O\'}).57(43).56(2f).5a(41),$3S=$(\'<12 />\').B({16:\'1b\',4o:\'4g\'}).1P($24);7 2l(){g(j.2c){$24.1w();$24.4Z()}};7 41(e){$24.1v()};7 2f(e){g(!j.2f)k;d 42=1O,1C;1O=e.4Q?M:K;g(42!=1O){g(1O&&1q){1C=E.Q();1B=1C.w/1C.h}1g 1B=0;J.1e()}e.2k();e.2j();k K};7 29(e,x,y){E.2b([x,y]);J.2I();e.2j();e.2k()};7 43(e){g(e.4T)k M;2f(e);d 2h=1O?10:1;1E(e.5d){C 37:29(e,-2h,0);O;C 39:29(e,2h,0);O;C 38:29(e,0,-2h);O;C 40:29(e,0,2h);O;C 27:J.1r();O;C 9:k M}k K};g(j.2c)$3S.3U($I);k{2l:2l}}();7 D(n){k\'\'+1m(n)+\'D\'};7 1t(n){k\'\'+1m(n)+\'%\'};7 1L(44){k j.3W+\'-\'+44};7 2Q(G){d z=$(G).2y();k[z.P,z.R]};7 2r(e){k[(e.4q-2u[0]),(e.4r-2u[1])]};7 46(1u){g(1u!=3i){1j.1x(1u);3i=1u}};7 4a(19,z){2u=2Q(G);1j.1x(19==\'1N\'?19:19+\'-2A\');g(19==\'1N\')k 1j.1G(4e(z),2P);d 1C=E.Q();E.1A(E.2K(4b(19)));1j.1G(45(19,1C),2P)};7 45(19,f){k 7(z){g(!j.25&&!1B)1E(19){C\'e\':z[1]=f.m;O;C\'w\':z[1]=f.m;O;C\'n\':z[0]=f.q;O;C\'s\':z[0]=f.q;O}1g 1E(19){C\'e\':z[1]=f.y+1;O;C\'w\':z[1]=f.y+1;O;C\'n\':z[0]=f.x+1;O;C\'s\':z[0]=f.x+1;O}E.1y(z);J.1e()}};7 4e(z){d 2M=z;33.2l();k 7(z){E.2b([z[0]-2M[0],z[1]-2M[1]]);2M=z;J.1e()}};7 4b(T){1E(T){C\'n\':k\'1M\';C\'s\':k\'11\';C\'e\':k\'11\';C\'w\':k\'1s\';C\'1s\':k\'1M\';C\'11\':k\'2e\';C\'2e\':k\'11\';C\'1M\':k\'1s\'}};7 3b(T){k 7(e){1q=M;4a(T,2r(e));e.2k();e.2j();k K}};7 47($G,w,h){d 11=$G.W(),1H=$G.U();g((11>w)&&w>0){11=w;1H=(w/$G.W())*$G.U()}g((1H>h)&&h>0){1H=h;11=(h/$G.U())*$G.W()}1I=$G.W()/11;1Q=$G.U()/1H;$G.W(11).U(1H)};7 2H(c){k{x:1m(c.x*1I),y:1m(c.y*1Q),q:1m(c.q*1I),m:1m(c.m*1Q),w:1m(c.w*1I),h:1m(c.h*1Q)}};7 2P(z){d c=E.Q();g(c.w>j.3n[0]&&c.h>j.3n[1]){J.2t();J.1h()}1g{J.1r()}1j.1x(\'2X\')};7 48(e){1q=M;2u=2Q(G);J.1r();J.1o();46(\'2X\');E.1A(2r(e));1j.1G(4c,2P);33.2l();e.2k();e.2j();k K};7 4c(z){E.1y(z);J.1e()};7 2Z(a){d A=a[0],u=a[1],q=a[2],m=a[3];g(3h)k;d 2s=E.1F(A,u,q,m);d c=E.Q();d 18=2p=[c.x,c.y,c.q,c.m];d 3w=j.3V;d x=18[0];d y=18[1];d q=18[2];d m=18[3];d 3Z=2s[0]-2p[0];d 4m=2s[1]-2p[1];d 4n=2s[2]-2p[2];d 4l=2s[3]-2p[3];d 1c=0;d 4h=j.3X;J.2o(M);d 3u=7(){k 7(){1c+=(V-1c)/4h;18[0]=x+((1c/V)*3Z);18[1]=y+((1c/V)*4m);18[2]=q+((1c/V)*4n);18[3]=m+((1c/V)*4l);g(1c<V)32();1g J.1h();g(1c>=4K.8)1c=V;1d(18)}}();7 32(){4I.4t(3u,3w)};32()};7 1d(l){E.1A([l[0],l[1]]);E.1y([l[2],l[3]]);J.1e()};7 21(F){g(1p(F)!=\'2d\')F={};j=$.4X(j,F);g(1p(j.2D)!==\'7\')j.2D=7(){};g(1p(j.2G)!==\'7\')j.2G=7(){}};7 3m(){k 2H(E.Q())};7 2V(){k E.Q()};7 3E(F){21(F);g(\'1d\'1a F){1d(F.1d);J.1h()}};g(1p(F)!=\'2d\')F={};g(\'1d\'1a F){1d(F.1d);J.1h()}d 2q=j.2z[0]||0;d 2n=j.2z[1]||0;d 2m=j.2O[0]||0;d 2i=j.2O[1]||0;1j.1x(\'2X\');k{2Z:2Z,1d:1d,21:3E,3m:3m,2V:2V}};$.5e.1n=7(j){7 3G(1D){d 4d=j.4R||1D.2N;d I=4P 4S();d 1D=1D;I.50=7(){$(1D).1v().4A(I);1D.1n=$.1n(I,j)};I.2N=4d};g(1p(j)!==\'2d\')j={};1T.4J(7(){g(\'1n\'1a 1T){g(j==\'52\')k 1T.1n;1g 1T.1n.21(j)}1g 3G(1T)});k 1T};',62,325,'|||||||function||||||var|||if|||options|return||y2||||x2||||y1|||||pos|x1|css|case|px|Coords|opt|obj|handle|img|Selection|false|boundy|true|Math|break|left|getFixed|top|boundx|ord|height|100|width|delta|oy|ox||nw|div|yy||xx|position|abs|animat|mode|in|absolute|pcent|setSelect|update|addClass|else|done|hhs|Tracker|aspect|zIndex|parseInt|Jcrop|disableHandles|typeof|btndown|release|ne|pct|type|hide|show|setCursor|setCurrent|opacity|setPressed|aspectLock|fc|from|switch|flipCoords|activateHandlers|nh|xscale|jq|round|cssClass|sw|move|shift_down|append|yscale|awake|trk|this|xsize|rw|insertDragbar|south|insertBorder|rh||setOptions|ysize|bound|keymgr|aspectRatio|trackUp||sel|doNudge|east|moveOffset|keySupport|object|se|watchShift|hdl_holder|nudge|ymin|preventDefault|stopPropagation|watchKeys|xmin|ylimit|animMode|initcr|xlimit|mouseAbs|animto|enableHandles|docOffset|onDone|onMove|trackDocument|offset|maxSize|resize|trackMove|img_holder|onChange|seehandles|borders|onSelect|unscale|updateVisible|li|getCorner|trackDoc|lloc|src|minSize|doneSelect|getPos|xa|mouseup|rebound|hdep|tellScaled|dragDiv|crosshair|createHandles|animateTo|drawBorders|moveHandles|animateStart|KeyManager|img2|midhoriz|trueSize||||midvert|createDragger|rwa|browser|msie|getOffset|makeObj|animating|lastcurs|rha|mousemove|yb|tellSelect|minSelect|ya|cursor|mousedown|xb|refresh|document|animator|tracker|interv|bgOpacity|zi|vline|dragEdges|sideHandles|hline|handleOffset|setOptionsNew|getRect|attachWhenDone|unbind|cornerHandles|insertHandle|moveto|right|real_ratio|handleSize|borderOpacity|handleOpacity|bottom|290|keywrap|bgColor|insertBefore|animationDelay|baseClass|swingSpeed|attr|ix1||onBlur|init_shift|parseKey|cl|dragmodeHandler|myCursor|presize|newSelection|boxWidth|startDragMode|oppLockCorner|selectDrag|loadsrc|createMover|defaults|hidden|velocity|toBack|toFront|boundary|iy2|iy1|ix2|overflow|boxHeight|pageX|pageY|before|setTimeout|max|start|input|370|dragmode|end|after|backgroundColor|jcrop|null|last|holder|450|relative|window|each|99|radio|360|mouseout|30px|new|shiftKey|useImg|Image|ctrlKey|for|north|west|extend|black|focus|onload|edgeMargin|api|handlePad|wrap|300|keyup|keydown|dimmed|310|blur|320|min|keyCode|fn'.split('|'),0,{}))
index 70dd9c7de7408d994d817a1f68df33ccd5603f99..c53569beab859025860b07194dedeb6195f764b7 100644 (file)
@@ -27,13 +27,14 @@ $(document).ready(function() {
         }
     }
 
+    /* rgb2hex written by R0bb13 <robertorebollo@gmail.com> */
     function rgb2hex(rgb) {
         rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
-        function hex(x) {
-            hexDigits = new Array("0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F");
-            return isNaN(x) ? "00" : hexDigits[(x - x % 16) / 16] + hexDigits[x % 16];
-        }
-        return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
+        return '#' + dec2hex(rgb[1]) + dec2hex(rgb[2]) + dec2hex(rgb[3]);
+    }
+    function dec2hex(x) {
+        hexDigits = new Array('0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F');
+        return isNaN(x) ? '00' : hexDigits[(x - x % 16) / 16] + hexDigits[x % 16];
     }
 
     function UpdateColors(S) {
index f3ed918cf26bb00963663c08c6209f2f3580d223..4f731f30a1e62aa6e9d0b6001f2aa62cc889072c 100644 (file)
  */
 
 $(document).ready(function(){
+       var counterBlackout = false;
+       
        // count character on keyup
        function counter(event){
-               var maxLength = 140;
+         if (maxLength <= 0) {
+              return;
+         }
                var currentLength = $("#notice_data-text").val().length;
                var remaining = maxLength - currentLength;
                var counter = $("#notice_text-count");
-               counter.text(remaining);
+               
+               if (remaining.toString() != counter.text()) {
+                   if (!counterBlackout || remaining == 0) {
+                        if (counter.text() != String(remaining)) {
+                            counter.text(remaining);
+                       }
 
-               if (remaining <= 0) {
-                       $("#form_notice").addClass("warning");
-               } else {
-                       $("#form_notice").removeClass("warning");
-               }
+                        if (remaining < 0) {
+                            $("#form_notice").addClass("warning");
+                        } else {
+                            $("#form_notice").removeClass("warning");
+                        }
+                        // Skip updates for the next 500ms.
+                        // On slower hardware, updating on every keypress is unpleasant.
+                        if (!counterBlackout) {
+                            counterBlackout = true;
+                            window.setTimeout(clearCounterBlackout, 500);
+                        }
+                    }
+                }
+       }
+       
+       function clearCounterBlackout() {
+               // Allow keyup events to poke the counter again
+               counterBlackout = false;
+               // Check if the string changed since we last looked
+               counter(null);
        }
 
        function submitonreturn(event) {
-               if (event.keyCode == 13) {
+               if (event.keyCode == 13 || event.keyCode == 10) {
+                       // iPhone sends \n not \r for 'return'
                        $("#form_notice").submit();
                        event.preventDefault();
                        event.stopPropagation();
+                       $("#notice_data-text").blur();
+                       $("body").focus();
                        return false;
                }
                return true;
        }
 
+     // define maxLength if it wasn't defined already
+
+    if (typeof(maxLength) == "undefined") {
+         maxLength = 140;
+    }
+
        if ($("#notice_data-text").length) {
-               $("#notice_data-text").bind("keyup", counter);
-               $("#notice_data-text").bind("keydown", submitonreturn);
+         if (maxLength > 0) {
+              $("#notice_data-text").bind("keyup", counter);
+              // run once in case there's something in there
+              counter();
+         }
 
-               // run once in case there's something in there
-               counter();
+               $("#notice_data-text").bind("keydown", submitonreturn);
 
         if($('body')[0].id != 'conversation') {
             $("#notice_data-text").focus();
@@ -57,6 +92,10 @@ $(document).ready(function(){
        // XXX: refactor this code
 
        var favoptions = { dataType: 'xml',
+                                          beforeSubmit: function(data, target, options) {
+                                                                                               $(target).addClass('processing');
+                                                                                               return true;
+                                                                                         },
                                           success: function(xml) { var new_form = document._importNode($('form', xml).get(0), true);
                                                                                                var dis = new_form.id;
                                                                                                var fav = dis.replace('disfavor', 'favor');
@@ -66,6 +105,10 @@ $(document).ready(function(){
                                         };
 
        var disoptions = { dataType: 'xml',
+                                          beforeSubmit: function(data, target, options) {
+                                                                                               $(target).addClass('processing');
+                                                                                               return true;
+                                                                                         },
                                           success: function(xml) { var new_form = document._importNode($('form', xml).get(0), true);
                                                                                                var fav = new_form.id;
                                                                                                var dis = fav.replace('favor', 'disfavor');
@@ -185,7 +228,9 @@ $(document).ready(function(){
                                                                                                                                                                }
                                                                                                                                                                else {
                                                                                                                                                                        $("#notice_data-text").val("");
-                                                                                                                                                                       counter();
+                                                                                     if (maxLength > 0) {
+                                                                                          counter();
+                                                                                     }
                                                                                                                                                                }
                                                                                                                                                        }
                                                                                                                                                }
@@ -225,7 +270,9 @@ $(document).ready(function(){
                                                                                                $("#notice_data-attach").val("");
                                                                                                $("#notice_in-reply-to").val("");
                                                     $('#notice_data-attach_selected').remove();
-                                                    counter();
+                                                     if (maxLength > 0) {
+                                                          counter();
+                                                     }
                                                                                                }
                                                                                                $("#form_notice").removeClass("processing");
                                                                                                $("#notice_action-submit").removeAttr("disabled");
@@ -244,7 +291,7 @@ function NoticeReply() {
         $('#content .notice').each(function() {
             var notice = $(this)[0];
             $($('.notice_reply', notice)[0]).click(function() {
-                var nickname = ($('.author .nickname', notice).length > 0) ? $($('.author .nickname', notice)[0]) : $('.author .nickname');
+                var nickname = ($('.author .nickname', notice).length > 0) ? $($('.author .nickname', notice)[0]) : $('.author .nickname.uid');
                 NoticeReplySet(nickname.text(), $($('.notice_id', notice)[0]).text());
                 return false;
             });
@@ -255,11 +302,16 @@ function NoticeReply() {
 function NoticeReplySet(nick,id) {
        rgx_username = /^[0-9a-zA-Z\-_.]*$/;
        if (nick.match(rgx_username)) {
-               replyto = "@" + nick + " ";
-               if ($("#notice_data-text").length) {
-                       $("#notice_data-text").val(replyto);
+               var text = $("#notice_data-text");
+               if (text.length) {
+                       replyto = "@" + nick + " ";
+                       text.val(replyto + text.val().replace(RegExp(replyto, 'i'), ''));
                        $("#form_notice input#notice_in-reply-to").val(id);
-                       $("#notice_data-text").focus();
+                       if (text.get(0).setSelectionRange) {
+                               var len = text.val().length;
+                               text.get(0).setSelectionRange(len,len);
+                               text.get(0).focus();
+                       }
                        return false;
                }
        }
index 158870fa8e8d02da57a2e4177161e469fb5adadb..4d724fba519f3f20e5d1385c0b2d54d294079ead 100644 (file)
@@ -196,21 +196,12 @@ class Action extends HTMLOutputter // lawsuit
         if (Event::handle('StartShowStyles', array($this))) {
 
             if (Event::handle('StartShowLaconicaStyles', array($this))) {
-                $this->element('link', array('rel' => 'stylesheet',
-                                             'type' => 'text/css',
-                                             'href' => theme_path('css/display.css', null) . '?version=' . LACONICA_VERSION,
-                                             'media' => 'screen, projection, tv'));
+                $this->cssLink('css/display.css',null,'screen, projection, tv');
                 if (common_config('site', 'mobile')) {
-                    $this->element('link', array('rel' => 'stylesheet',
-                                                 'type' => 'text/css',
-                                                 'href' => theme_path('css/mobile.css', 'base') . '?version=' . LACONICA_VERSION,
-                                                 // TODO: "handheld" CSS for other mobile devices
-                                                 'media' => 'only screen and (max-device-width: 480px)')); // Mobile WebKit
+                    // TODO: "handheld" CSS for other mobile devices
+                    $this->cssLink('css/mobile.css','base','only screen and (max-device-width: 480px)'); // Mobile WebKit
                 }
-                $this->element('link', array('rel' => 'stylesheet',
-                                             'type' => 'text/css',
-                                             'href' => theme_path('css/print.css', 'base') . '?version=' . LACONICA_VERSION,
-                                             'media' => 'print'));
+                $this->cssLink('css/print.css','base','print');
                 Event::handle('EndShowLaconicaStyles', array($this));
             }
 
@@ -256,26 +247,14 @@ class Action extends HTMLOutputter // lawsuit
     {
         if (Event::handle('StartShowScripts', array($this))) {
             if (Event::handle('StartShowJQueryScripts', array($this))) {
-                $this->element('script', array('type' => 'text/javascript',
-                                               'src' => common_path('js/jquery.min.js')),
-                               ' ');
-                $this->element('script', array('type' => 'text/javascript',
-                                               'src' => common_path('js/jquery.form.js')),
-                               ' ');
-
-                $this->element('script', array('type' => 'text/javascript',
-                                               'src' => common_path('js/jquery.joverlay.min.js')),
-                               ' ');
-
+                $this->script('js/jquery.min.js');
+                $this->script('js/jquery.form.js');
+                $this->script('js/jquery.joverlay.min.js');
                 Event::handle('EndShowJQueryScripts', array($this));
             }
             if (Event::handle('StartShowLaconicaScripts', array($this))) {
-                $this->element('script', array('type' => 'text/javascript',
-                                               'src' => common_path('js/xbImportNode.js')),
-                               ' ');
-                $this->element('script', array('type' => 'text/javascript',
-                                               'src' => common_path('js/util.js?version='.LACONICA_VERSION)),
-                               ' ');
+                $this->script('js/xbImportNode.js');
+                $this->script('js/util.js');
                 // Frame-busting code to avoid clickjacking attacks.
                 $this->element('script', array('type' => 'text/javascript'),
                                'if (window.top !== window.self) { window.top.location.href = window.self.location.href; }');
@@ -426,6 +405,14 @@ class Action extends HTMLOutputter // lawsuit
     function showPrimaryNav()
     {
         $user = common_current_user();
+        $connect = '';
+        if (common_config('xmpp', 'enabled')) {
+            $connect = 'imsettings';
+        } else if (common_config('sms', 'enabled')) {
+            $connect = 'smssettings';
+        } else if (common_config('twitter', 'enabled')) {
+            $connect = 'twittersettings';
+        }
 
         $this->elementStart('dl', array('id' => 'site_nav_global_primary'));
         $this->element('dt', null, _('Primary site navigation'));
@@ -437,12 +424,9 @@ class Action extends HTMLOutputter // lawsuit
                                 _('Home'), _('Personal profile and friends timeline'), false, 'nav_home');
                 $this->menuItem(common_local_url('profilesettings'),
                                 _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account');
-                if (common_config('xmpp', 'enabled')) {
-                    $this->menuItem(common_local_url('imsettings'),
-                                    _('Connect'), _('Connect to IM, SMS, Twitter'), false, 'nav_connect');
-                } else {
-                    $this->menuItem(common_local_url('smssettings'),
-                                    _('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect');
+                if ($connect) {
+                    $this->menuItem(common_local_url($connect),
+                                    _('Connect'), _('Connect to services'), false, 'nav_connect');
                 }
                 if (common_config('invite', 'enabled')) {
                     $this->menuItem(common_local_url('invite'),
@@ -455,17 +439,24 @@ class Action extends HTMLOutputter // lawsuit
                                 _('Logout'), _('Logout from the site'), false, 'nav_logout');
             }
             else {
-                if (!common_config('site', 'closed')) {
-                    $this->menuItem(common_local_url('register'),
-                                    _('Register'), _('Create an account'), false, 'nav_register');
+                if (!common_config('site', 'openidonly')) {
+                    if (!common_config('site', 'closed')) {
+                        $this->menuItem(common_local_url('register'),
+                                        _('Register'), _('Create an account'), false, 'nav_register');
+                    }
+                    $this->menuItem(common_local_url('login'),
+                                    _('Login'), _('Login to the site'), false, 'nav_login');
+                } else {
+                    $this->menuItem(common_local_url('openidlogin'),
+                                    _('OpenID'), _('Login with OpenID'), false, 'nav_openid');
                 }
-                $this->menuItem(common_local_url('login'),
-                                _('Login'), _('Login to the site'), false, 'nav_login');
             }
             $this->menuItem(common_local_url('doc', array('title' => 'help')),
                             _('Help'), _('Help me!'), false, 'nav_help');
-            $this->menuItem(common_local_url('peoplesearch'),
-                            _('Search'), _('Search for people or text'), false, 'nav_search');
+            if ($user || !common_config('site', 'private')) {
+                $this->menuItem(common_local_url('peoplesearch'),
+                                _('Search'), _('Search for people or text'), false, 'nav_search');
+            }
             Event::handle('EndPrimaryNav', array($this));
         }
         $this->elementEnd('ul');
index a8a12b3bb35668ec1bc8e41872e8d5a7ab962b09..47ae057dccdf96473df6bda7041cff71d15094e3 100644 (file)
@@ -25,12 +25,14 @@ class ArrayWrapper
 {
     var $_items = null;
     var $_count = 0;
+    var $N = 0;
     var $_i = -1;
 
     function __construct($items)
     {
         $this->_items = $items;
         $this->_count = count($this->_items);
+        $this->N = $this->_count;
     }
 
     function fetch()
@@ -76,4 +78,4 @@ class ArrayWrapper
         $item =& $this->_items[$this->_i];
         return call_user_func_array(array($item, $name), $args);
     }
-}
\ No newline at end of file
+}
index 4e2280bc8070d58bc5edefc5edd09230f5a7c95b..371386dc58ea516155649ff895ed9eb37e7836eb 100644 (file)
@@ -211,16 +211,20 @@ class MessageCommand extends Command
     function execute($channel)
     {
         $other = User::staticGet('nickname', common_canonical_nickname($this->other));
+
         $len = mb_strlen($this->text);
+
         if ($len == 0) {
             $channel->error($this->user, _('No content!'));
             return;
-        } else if ($len > 140) {
-            $content = common_shorten_links($content);
-            if (mb_strlen($content) > 140) {
-                $channel->error($this->user, sprintf(_('Message too long - maximum is 140 characters, you sent %d'), $len));
-                return;
-            }
+        }
+
+        $this->text = common_shorten_links($this->text);
+
+        if (Message::contentTooLong($this->text)) {
+            $channel->error($this->user, sprintf(_('Message too long - maximum is %d characters, you sent %d'),
+                                                 Message::maxContent(), mb_strlen($this->text)));
+            return;
         }
 
         if (!$other) {
index 5cecf309a0bbd1390f13dacf8c832e2a7deb5861..067a5a2a61bafcdfb8a6cb42c7078e1dcf58298b 100644 (file)
@@ -21,6 +21,8 @@ if (!defined('LACONICA')) { exit(1); }
 
 define('LACONICA_VERSION', '0.9.0dev');
 
+// XXX: move these to class variables
+
 define('AVATAR_PROFILE_SIZE', 96);
 define('AVATAR_STREAM_SIZE', 48);
 define('AVATAR_MINI_SIZE', 24);
@@ -82,7 +84,7 @@ if (isset($server)) {
 if (isset($path)) {
     $_path = $path;
 } else {
-    $_path = array_key_exists('SCRIPT_NAME', $_SERVER) ?
+    $_path = (array_key_exists('SERVER_NAME', $_SERVER) && array_key_exists('SCRIPT_NAME', $_SERVER)) ?
       _sn_to_path($_SERVER['SCRIPT_NAME']) :
     null;
 }
@@ -109,11 +111,14 @@ $config =
               'broughtbyurl' => null,
               'closed' => false,
               'inviteonly' => false,
+              'openidonly' => false,
               'private' => false,
               'ssl' => 'never',
               'sslserver' => null,
               'shorturllength' => 30,
-              'dupelimit' => 60), # default for same person saying the same thing
+              'dupelimit' => 60, # default for same person saying the same thing
+              'textlimit' => 140,
+              ),
         'syslog' =>
         array('appname' => 'laconica', # for syslog
               'priority' => 'debug', # XXX: currently ignored
@@ -137,7 +142,8 @@ $config =
         array('blacklist' => array(),
               'featured' => array()),
         'profile' =>
-        array('banned' => array()),
+        array('banned' => array(),
+              'biolimit' => null),
         'avatar' =>
         array('server' => null,
               'dir' => INSTALLDIR . '/avatar/',
@@ -169,6 +175,8 @@ $config =
               'host' => null, # only set if != server
               'debug' => false, # print extra debug info
               'public' => array()), # JIDs of users who want to receive the public stream
+        'openid' =>
+        array('enabled' => true),
         'invite' =>
         array('enabled' => true),
         'sphinx' =>
@@ -183,11 +191,20 @@ $config =
         array('piddir' => '/var/run',
               'user' => false,
               'group' => false),
+        'emailpost' =>
+        array('enabled' => true),
+        'sms' =>
+        array('enabled' => true),
+        'twitter' =>
+        array('enabled' => true),
         'twitterbridge' =>
         array('enabled' => false),
         'integration' =>
         array('source' => 'Laconica', # source attribute for Twitter
               'taguri' => $_server.',2009'), # base for tag URIs
+       'twitter' =>
+       array('consumer_key'    => null,
+             'consumer_secret' => null),
         'memcached' =>
         array('enabled' => false,
               'server' => 'localhost',
@@ -246,7 +263,8 @@ $config =
         'filecommand' => '/usr/bin/file',
         ),
         'group' =>
-        array('maxaliases' => 3),
+        array('maxaliases' => 3,
+              'desclimit' => null),
         'oohembed' => array('endpoint' => 'http://oohembed.com/oohembed/'),
         'search' =>
         array('type' => 'fulltext'),
@@ -261,6 +279,10 @@ $config =
               'linkcolor' => null,
               'backgroundimage' => null,
               'disposition' => null),
+        'notice' =>
+        array('contentlimit' => null),
+        'message' =>
+        array('contentlimit' => null),
         );
 
 $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options');
@@ -361,7 +383,31 @@ if ($_db_name != 'laconica' && !array_key_exists('ini_'.$_db_name, $config['db']
     $config['db']['ini_'.$_db_name] = INSTALLDIR.'/classes/laconica.ini';
 }
 
+// Ignore openidonly if OpenID is disabled
+
+if (!$config['openid']['enabled']) {
+    $config['site']['openidonly'] = false;
+}
+
+function __autoload($cls)
+{
+    if (file_exists(INSTALLDIR.'/classes/' . $cls . '.php')) {
+        require_once(INSTALLDIR.'/classes/' . $cls . '.php');
+    } else if (file_exists(INSTALLDIR.'/lib/' . strtolower($cls) . '.php')) {
+        require_once(INSTALLDIR.'/lib/' . strtolower($cls) . '.php');
+    } else if (mb_substr($cls, -6) == 'Action' &&
+               file_exists(INSTALLDIR.'/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php')) {
+        require_once(INSTALLDIR.'/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php');
+    } else if ($cls == 'OAuthRequest') {
+        require_once('OAuth.php');
+    } else {
+        Event::handle('Autoload', array(&$cls));
+    }
+}
+
 // XXX: how many of these could be auto-loaded on use?
+// XXX: note that these files should not use config options
+// at compile time since DB config options are not yet loaded.
 
 require_once 'Validate.php';
 require_once 'markdown.php';
@@ -377,26 +423,14 @@ require_once INSTALLDIR.'/lib/twitter.php';
 require_once INSTALLDIR.'/lib/clientexception.php';
 require_once INSTALLDIR.'/lib/serverexception.php';
 
+// Load settings from database; note we need autoload for this
+
+Config::loadSettings();
+
 // XXX: other formats here
 
 define('NICKNAME_FMT', VALIDATE_NUM.VALIDATE_ALPHA_LOWER);
 
-function __autoload($cls)
-{
-    if (file_exists(INSTALLDIR.'/classes/' . $cls . '.php')) {
-        require_once(INSTALLDIR.'/classes/' . $cls . '.php');
-    } else if (file_exists(INSTALLDIR.'/lib/' . strtolower($cls) . '.php')) {
-        require_once(INSTALLDIR.'/lib/' . strtolower($cls) . '.php');
-    } else if (mb_substr($cls, -6) == 'Action' &&
-               file_exists(INSTALLDIR.'/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php')) {
-        require_once(INSTALLDIR.'/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php');
-    } else if ($cls == 'OAuthRequest') {
-        require_once('OAuth.php');
-    } else {
-        Event::handle('Autoload', array(&$cls));
-    }
-}
-
 // Give plugins a chance to initialize in a fully-prepared environment
 
 Event::handle('InitializePlugin');
index 30629680ecbb124cecba705c510a3e42b7c2501a..02c468a359b7f166ce012e85ef9db5d0b9f17959 100644 (file)
@@ -99,25 +99,27 @@ class ConnectSettingsNav extends Widget
     function show()
     {
         # action => array('prompt', 'title')
-        $menu =
-          array('imsettings' =>
-                array(_('IM'),
-                      _('Updates by instant messenger (IM)')),
-                'smssettings' =>
-                array(_('SMS'),
-                      _('Updates by SMS')),
-                'twittersettings' =>
-                array(_('Twitter'),
-                      _('Twitter integration options')));
+        $menu = array();
+        if (common_config('xmpp', 'enabled')) {
+            $menu['imsettings'] =
+              array(_('IM'),
+                    _('Updates by instant messenger (IM)'));
+        }
+        if (common_config('sms', 'enabled')) {
+            $menu['smssettings'] =
+              array(_('SMS'),
+                    _('Updates by SMS'));
+        }
+        if (common_config('twitter', 'enabled')) {
+            $menu['twittersettings'] =
+              array(_('Twitter'),
+                    _('Twitter integration options'));
+        }
 
         $action_name = $this->action->trimmed('action');
         $this->action->elementStart('ul', array('class' => 'nav'));
 
         foreach ($menu as $menuaction => $menudesc) {
-            if ($menuaction == 'imsettings' &&
-                !common_config('xmpp', 'enabled')) {
-                continue;
-            }
             $this->action->menuItem(common_local_url($menuaction),
                                    $menudesc[0],
                                    $menudesc[1],
index 1b0e62166926ac7dbe60db6b5b1a8058fd862b68..a48ec9d2274531fecdfd24a39af3f3080dfdabee 100644 (file)
@@ -311,13 +311,7 @@ class DesignSettingsAction extends AccountSettingsAction
     function showStylesheets()
     {
         parent::showStylesheets();
-        $farbtasticStyle =
-          common_path('theme/base/css/farbtastic.css?version='.LACONICA_VERSION);
-
-        $this->element('link', array('rel' => 'stylesheet',
-                                     'type' => 'text/css',
-                                     'href' => $farbtasticStyle,
-                                     'media' => 'screen, projection, tv'));
+        $this->cssLink('css/farbtastic.css','base','screen, projection, tv');
     }
 
     /**
@@ -330,13 +324,8 @@ class DesignSettingsAction extends AccountSettingsAction
     {
         parent::showScripts();
 
-        $farbtasticPack = common_path('js/farbtastic/farbtastic.js');
-        $userDesignGo   = common_path('js/userdesign.go.js');
-
-        $this->element('script', array('type' => 'text/javascript',
-                                       'src' => $farbtasticPack));
-        $this->element('script', array('type' => 'text/javascript',
-                                       'src' => $userDesignGo));
+        $this->script('js/farbtastic/farbtastic.js');
+        $this->script('js/farbtastic/farbtastic.go.js');
     }
 
     /**
index bbf9987cff4632281c888ec0fd08c9815ad50a2a..3127c83feb3d694aecc2e52e57717e9b622586d1 100644 (file)
@@ -72,7 +72,7 @@ class ErrorAction extends Action
         $status_string = $this->status[$this->code];
         header('HTTP/1.1 '.$this->code.' '.$status_string);
     }
-    
+
     /**
      * Display content.
      *
@@ -97,11 +97,11 @@ class ErrorAction extends Action
     {
         return true;
     }
-    
-    function showPage() 
+
+    function showPage()
     {
         parent::showPage();
-        
+
         // We don't want to have any more output after this
         exit();
     }
index 5be2f2fe6603f9296a870e4ad7f99ae9b47dcb0f..4edd3a077cd34825036b5b6c122202d50dd96835 100644 (file)
@@ -35,7 +35,6 @@ if (!defined('LACONICA'))
 require_once INSTALLDIR.'/lib/facebookutil.php';
 require_once INSTALLDIR.'/lib/noticeform.php';
 
-
 class FacebookAction extends Action
 {
 
@@ -95,34 +94,13 @@ class FacebookAction extends Action
 
     function showStylesheets()
     {
-        // Add a timestamp to the file so Facebook cache wont ignore our changes
-        $ts = filemtime(INSTALLDIR.'/theme/base/css/display.css');
-
-    $this->element('link', array('rel' => 'stylesheet',
-               'type' => 'text/css',
-               'href' => theme_path('css/display.css', 'base') . '?ts=' . $ts));
-
-        $theme = common_config('site', 'theme');
-
-        $ts = filemtime(INSTALLDIR. '/theme/' . $theme .'/css/display.css');
-
-        $this->element('link', array('rel' => 'stylesheet',
-                                     'type' => 'text/css',
-                                     'href' => theme_path('css/display.css', null) . '?ts=' . $ts));
-
-        $ts = filemtime(INSTALLDIR.'/theme/base/css/facebookapp.css');
-
-        $this->element('link', array('rel' => 'stylesheet',
-                                     'type' => 'text/css',
-                                     'href' => theme_path('css/facebookapp.css', 'base') . '?ts=' . $ts));
+        $this->cssLink('css/display.css', 'base');
+        $this->cssLink('css/facebookapp.css', 'base');
     }
 
     function showScripts()
     {
-        // Add a timestamp to the file so Facebook cache wont ignore our changes
-        $ts = filemtime(INSTALLDIR.'/js/facebookapp.js');
-
-        $this->element('script', array('src' => common_path('js/facebookapp.js') . '?ts=' . $ts));
+        $this->script('js/facebookapp.js');
     }
 
     /**
@@ -201,7 +179,6 @@ class FacebookAction extends Action
 
     }
 
-
     // Make this into a widget later
     function showLocalNav()
     {
@@ -261,7 +238,6 @@ class FacebookAction extends Action
         $this->endHTML();
     }
 
-
     function showInstructions()
     {
 
@@ -277,8 +253,13 @@ class FacebookAction extends Action
         $this->elementStart('dd');
         $this->elementStart('p');
         $this->text(sprintf($loginmsg_part1, common_config('site', 'name')));
-        $this->element('a',
-            array('href' => common_local_url('register')), _('Register'));
+        if (!common_config('site', 'openidonly')) {
+            $this->element('a',
+                array('href' => common_local_url('register')), _('Register'));
+        } else {
+            $this->element('a',
+                array('href' => common_local_url('openidlogin')), _('Register'));
+        }
         $this->text($loginmsg_part2);
     $this->elementEnd('p');
         $this->elementEnd('dd');
@@ -287,7 +268,6 @@ class FacebookAction extends Action
         $this->elementEnd('div');
     }
 
-
     function showLoginForm($msg = null)
     {
 
@@ -332,7 +312,6 @@ class FacebookAction extends Action
 
     }
 
-
     function updateProfileBox($notice)
     {
 
@@ -414,7 +393,6 @@ class FacebookAction extends Action
         $this->xw->openURI('php://output');
     }
 
-
     /**
      * Generate pagination links
      *
@@ -473,8 +451,9 @@ class FacebookAction extends Action
         } else {
             $content_shortened = common_shorten_links($content);
 
-            if (mb_strlen($content_shortened) > 140) {
-                $this->showPage(_('That\'s too long. Max notice size is 140 chars.'));
+            if (Notice::contentTooLong($content_shortened)) {
+                $this->showPage(sprintf(_('That\'s too long. Max notice size is %d chars.'),
+                                        Notice::maxContent()));
                 return;
             }
         }
index b7688f04f00c51e1641f2efa397d9267e743dfa5..e31a71f5ebbd45602e5d7d5ed18e1bdd2ea80236 100644 (file)
@@ -36,7 +36,7 @@ function getFacebook()
         $facebook = new Facebook($apikey, $secret);
     }
 
-    if (!$facebook) {
+    if (empty($facebook)) {
         common_log(LOG_ERR, 'Could not make new Facebook client obj!',
             __FILE__);
     }
@@ -44,71 +44,37 @@ function getFacebook()
     return $facebook;
 }
 
-function updateProfileBox($facebook, $flink, $notice) {
-    $fbaction = new FacebookAction($output='php://output', $indent=true, $facebook, $flink);
-    $fbaction->updateProfileBox($notice);
-}
-
 function isFacebookBound($notice, $flink) {
 
     if (empty($flink)) {
         return false;
     }
 
+    // Avoid a loop
+
+    if ($notice->source == 'Facebook') {
+        common_log(LOG_INFO, "Skipping notice $notice->id because its " .
+                   'source is Facebook.');
+        return false;
+    }
+
     // If the user does not want to broadcast to Facebook, move along
+
     if (!($flink->noticesync & FOREIGN_NOTICE_SEND == FOREIGN_NOTICE_SEND)) {
         common_log(LOG_INFO, "Skipping notice $notice->id " .
             'because user has FOREIGN_NOTICE_SEND bit off.');
         return false;
     }
 
-    $success = false;
+    // If it's not a reply, or if the user WANTS to send @-replies,
+    // then, yeah, it can go to Facebook.
 
-    // If it's not a reply, or if the user WANTS to send @-replies...
     if (!preg_match('/@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) ||
         ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) {
-
-        $success = true;
-
-        // The two condition below are deal breakers:
-
-        // Avoid a loop
-        if ($notice->source == 'Facebook') {
-            common_log(LOG_INFO, "Skipping notice $notice->id because its " .
-                'source is Facebook.');
-            $success = false;
-        }
-
-        $facebook = getFacebook();
-        $fbuid = $flink->foreign_id;
-
-        try {
-
-            // Check to see if the user has given the FB app status update perms
-            $result = $facebook->api_client->
-                users_hasAppPermission('publish_stream', $fbuid);
-
-            if ($result != 1) {
-                $result = $facebook->api_client->
-                    users_hasAppPermission('status_update', $fbuid);
-            }
-            if ($result != 1) {
-                $user = $flink->getUser();
-                $msg = "Not sending notice $notice->id to Facebook " .
-                    "because user $user->nickname hasn't given the " .
-                    'Facebook app \'status_update\' or \'publish_stream\' permission.';
-                common_debug($msg);
-                $success = false;
-            }
-
-        } catch(FacebookRestClientException $e){
-            common_log(LOG_ERR, $e->getMessage());
-            $success = false;
-        }
-
+        return true;
     }
 
-    return $success;
+    return false;
 
 }
 
@@ -119,88 +85,65 @@ function facebookBroadcastNotice($notice)
 
     if (isFacebookBound($notice, $flink)) {
 
+        // Okay, we're good to go, update the FB status
+
         $status = null;
         $fbuid = $flink->foreign_id;
-
         $user = $flink->getUser();
-
-        // Get the status 'verb' (prefix) the user has set
+        $attachments  = $notice->attachments();
 
         try {
-            $prefix = $facebook->api_client->
-                data_getUserPreference(FACEBOOK_NOTICE_PREFIX, $fbuid);
+
+            // Get the status 'verb' (prefix) the user has set
+
+            // XXX: Does this call count against our per user FB request limit?
+            // If so we should consider storing verb elsewhere or not storing
+
+            $prefix = $facebook->api_client->data_getUserPreference(FACEBOOK_NOTICE_PREFIX,
+                                                                    $fbuid);
 
             $status = "$prefix $notice->content";
 
-        } catch(FacebookRestClientException $e) {
-            common_log(LOG_WARNING, $e->getMessage());
-            common_log(LOG_WARNING,
-                'Unable to get the status verb setting from Facebook ' .
-                "for $user->nickname (user id: $user->id).");
-        }
+            $can_publish = $facebook->api_client->users_hasAppPermission('publish_stream',
+                                                                         $fbuid);
 
-        // Okay, we're good to go, update the FB status
+            $can_update  = $facebook->api_client->users_hasAppPermission('status_update',
+                                                                         $fbuid);
 
-        try {
-            $result = $facebook->api_client->
-                users_hasAppPermission('publish_stream', $fbuid);
-            if($result == 1){
-                // authorized to use the stream api, so use it
-                $fbattachment = null;
-                $attachments = $notice->attachments();
-                if($attachments){
-                    $fbattachment=array();
-                    $fbattachment['media']=array();
-                    //facebook only supports one attachment per item
-                    $attachment = $attachments[0];
-                    $fbmedia=array();
-                    if(strncmp($attachment->mimetype,'image/',strlen('image/'))==0){
-                        $fbmedia['type']='image';
-                        $fbmedia['src']=$attachment->url;
-                        $fbmedia['href']=$attachment->url;
-                        $fbattachment['media'][]=$fbmedia;
-/* Video doesn't seem to work. The notice never makes it to facebook, and no error is reported.
-                    }else if(strncmp($attachment->mimetype,'video/',strlen('image/'))==0 || $attachment->mimetype="application/ogg"){
-                        $fbmedia['type']='video';
-                        $fbmedia['video_src']=$attachment->url;
-                        // http://wiki.developers.facebook.com/index.php/Attachment_%28Streams%29
-                        // says that preview_img is required... but we have no value to put in it
-                        // $fbmedia['preview_img']=$attachment->url;
-                        if($attachment->title){
-                            $fbmedia['video_title']=$attachment->title;
-                        }
-                        $fbmedia['video_type']=$attachment->mimetype;
-                        $fbattachment['media'][]=$fbmedia;
-*/
-                    }else if($attachment->mimetype=='audio/mpeg'){
-                        $fbmedia['type']='mp3';
-                        $fbmedia['src']=$attachment->url;
-                        $fbattachment['media'][]=$fbmedia;
-                    }else if($attachment->mimetype=='application/x-shockwave-flash'){
-                        $fbmedia['type']='flash';
-                        // http://wiki.developers.facebook.com/index.php/Attachment_%28Streams%29
-                        // says that imgsrc is required... but we have no value to put in it
-                        // $fbmedia['imgsrc']='';
-                        $fbmedia['swfsrc']=$attachment->url;
-                        $fbattachment['media'][]=$fbmedia;
-                    }else{
-                        $fbattachment['name']=($attachment->title?$attachment->title:$attachment->url);
-                        $fbattachment['href']=$attachment->url;
-                    }
-                }
-                $facebook->api_client->stream_publish($status, $fbattachment, null, null, $fbuid);
-            }else{
+            if (!empty($attachments) && $can_publish == 1) {
+                $fbattachment = format_attachments($attachments);
+                $facebook->api_client->stream_publish($status, $fbattachment,
+                                                      null, null, $fbuid);
+                common_log(LOG_INFO,
+                           "Posted notice $notice->id w/attachment " .
+                           "to Facebook user's stream (fbuid = $fbuid).");
+            } elseif ($can_update == 1 || $can_publish == 1) {
                 $facebook->api_client->users_setStatus($status, $fbuid, false, true);
+                common_log(LOG_INFO,
+                           "Posted notice $notice->id to Facebook " .
+                           "as a status update (fbuid = $fbuid).");
+            } else {
+                $msg = "Not sending notice $notice->id to Facebook " .
+                  "because user $user->nickname hasn't given the " .
+                  'Facebook app \'status_update\' or \'publish_stream\' permission.';
+                common_log(LOG_WARNING, $msg);
+            }
+
+            // Finally, attempt to update the user's profile box
+
+            if ($can_publish == 1 || $can_update == 1) {
+                updateProfileBox($facebook, $flink, $notice);
             }
-        } catch(FacebookRestClientException $e) {
+
+        } catch (FacebookRestClientException $e) {
 
             $code = $e->getCode();
 
-            common_log(LOG_ERR, 'Facebook returned error code ' .
-                $code . ': ' . $e->getMessage());
-            common_log(LOG_ERR,
-                'Unable to update Facebook status for ' .
-                "$user->nickname (user id: $user->id)!");
+            common_log(LOG_WARNING, 'Facebook returned error code ' .
+                       $code . ': ' . $e->getMessage());
+            common_log(LOG_WARNING,
+                       'Unable to update Facebook status for ' .
+                       "$user->nickname (user id: $user->id)!");
 
             if ($code == 200 || $code == 250) {
 
@@ -209,25 +152,62 @@ function facebookBroadcastNotice($notice)
                 // see: http://wiki.developers.facebook.com/index.php/Users.setStatus#Example_Return_XML
 
                 remove_facebook_app($flink);
+
+            } else {
+
+                // Try sending again later.
+
+                return false;
             }
 
         }
+    }
 
-        // Now try to update the profile box
+    return true;
 
-        try {
-            updateProfileBox($facebook, $flink, $notice);
-        } catch(FacebookRestClientException $e) {
-            common_log(LOG_ERR, 'Facebook returned error code ' .
-                $e->getCode() . ': ' . $e->getMessage());
-            common_log(LOG_WARNING,
-                'Unable to update Facebook profile box for ' .
-                "$user->nickname (user id: $user->id).");
-        }
+}
 
+function updateProfileBox($facebook, $flink, $notice) {
+    $fbaction = new FacebookAction($output = 'php://output',
+                                   $indent = true, $facebook, $flink);
+    $fbaction->updateProfileBox($notice);
+}
+
+function format_attachments($attachments)
+{
+    $fbattachment          = array();
+    $fbattachment['media'] = array();
+
+    // Facebook only supports one attachment per item
+
+    $attachment = $attachments[0];
+    $fbmedia    = array();
+
+    if (strncmp($attachment->mimetype, 'image/', strlen('image/')) == 0) {
+        $fbmedia['type']         = 'image';
+        $fbmedia['src']          = $attachment->url;
+        $fbmedia['href']         = $attachment->url;
+        $fbattachment['media'][] = $fbmedia;
+    } else if ($attachment->mimetype == 'audio/mpeg') {
+        $fbmedia['type']         = 'mp3';
+        $fbmedia['src']          = $attachment->url;
+        $fbattachment['media'][] = $fbmedia;
+    }else if ($attachment->mimetype == 'application/x-shockwave-flash') {
+        $fbmedia['type']         = 'flash';
+
+        // http://wiki.developers.facebook.com/index.php/Attachment_%28Streams%29
+        // says that imgsrc is required... but we have no value to put in it
+        // $fbmedia['imgsrc']='';
+
+        $fbmedia['swfsrc']       = $attachment->url;
+        $fbattachment['media'][] = $fbmedia;
+    }else{
+        $fbattachment['name'] = ($attachment->title ?
+                                 $attachment->title : $attachment->url);
+        $fbattachment['href'] = $attachment->url;
     }
 
-    return true;
+    return $fbattachment;
 }
 
 function remove_facebook_app($flink)
index fbb39129bc3bc4f7b05f552b318bf0e94029b8c8..47e62d469ec3486d63d3af14a80359c512e5f0bc 100644 (file)
@@ -150,27 +150,33 @@ class GroupEditForm extends Form
         $this->out->elementStart('li');
         $this->out->hidden('groupid', $id);
         $this->out->input('nickname', _('Nickname'),
-                     ($this->out->arg('nickname')) ? $this->out->arg('nickname') : $nickname,
-                     _('1-64 lowercase letters or numbers, no punctuation or spaces'));
+                          ($this->out->arg('nickname')) ? $this->out->arg('nickname') : $nickname,
+                          _('1-64 lowercase letters or numbers, no punctuation or spaces'));
         $this->out->elementEnd('li');
         $this->out->elementStart('li');
         $this->out->input('fullname', _('Full name'),
-                     ($this->out->arg('fullname')) ? $this->out->arg('fullname') : $fullname);
+                          ($this->out->arg('fullname')) ? $this->out->arg('fullname') : $fullname);
         $this->out->elementEnd('li');
         $this->out->elementStart('li');
         $this->out->input('homepage', _('Homepage'),
-                     ($this->out->arg('homepage')) ? $this->out->arg('homepage') : $homepage,
-                     _('URL of the homepage or blog of the group or topic'));
+                          ($this->out->arg('homepage')) ? $this->out->arg('homepage') : $homepage,
+                          _('URL of the homepage or blog of the group or topic'));
         $this->out->elementEnd('li');
         $this->out->elementStart('li');
+        $desclimit = User_group::maxDescription();
+        if ($desclimit == 0) {
+            $descinstr = _('Describe the group or topic');
+        } else {
+            $descinstr = sprintf(_('Describe the group or topic in %d characters'), $desclimit);
+        }
         $this->out->textarea('description', _('Description'),
-                        ($this->out->arg('description')) ? $this->out->arg('description') : $description,
-                        _('Describe the group or topic in 140 chars'));
+                             ($this->out->arg('description')) ? $this->out->arg('description') : $description,
+                             $descinstr);
         $this->out->elementEnd('li');
         $this->out->elementStart('li');
         $this->out->input('location', _('Location'),
-                     ($this->out->arg('location')) ? $this->out->arg('location') : $location,
-                     _('Location for the group, if any, like "City, State (or Region), Country"'));
+                          ($this->out->arg('location')) ? $this->out->arg('location') : $location,
+                          _('Location for the group, if any, like "City, State (or Region), Country"'));
         $this->out->elementEnd('li');
         if (common_config('group', 'maxaliases') > 0) {
             $aliases = (empty($this->group)) ? array() : $this->group->getAliases();
index 06603ac05485660b206726ad217a769756f9713b..683a5e0b71fb58da3a07e3cc7015250a70c981c2 100644 (file)
@@ -109,10 +109,11 @@ class HTMLOutputter extends XMLOutputter
         header('Content-Type: '.$type);
 
         $this->extraHeaders();
-
-        $this->startXML('html',
-                        '-//W3C//DTD XHTML 1.0 Strict//EN',
-                        'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
+        if( ! substr($type,0,strlen('text/html'))=='text/html' ){
+            // Browsers don't like it when <?xml it output for non-xhtml documents
+            $this->xw->startDocument('1.0', 'UTF-8');
+        }
+        $this->xw->writeDTD('html');
 
         $language = $this->getLanguage();
 
@@ -338,6 +339,52 @@ class HTMLOutputter extends XMLOutputter
                                       'title' => $title));
     }
 
+    /**
+     * output a script (almost always javascript) tag
+     *
+     * @param string $src          relative or absolute script path
+     * @param string $type         'type' attribute value of the tag
+     *
+     * @return void
+     */
+    function script($src, $type='text/javascript')
+    {
+        $url = parse_url($src);
+        if( empty($url->scheme) && empty($url->host) && empty($url->query) && empty($url->fragment))
+        {
+            $src = common_path($src) . '?version=' . LACONICA_VERSION;
+        }
+        $this->element('script', array('type' => $type,
+                                               'src' => $src),
+                               ' ');
+    }
+
+    /**
+     * output a css link
+     *
+     * @param string $src     relative path within the theme directory, or an absolute path
+     * @param string $theme        'theme' that contains the stylesheet
+     * @param string media         'media' attribute of the tag
+     *
+     * @return void
+     */
+    function cssLink($src,$theme=null,$media=null)
+    {
+        $url = parse_url($src);
+        if( empty($url->scheme) && empty($url->host) && empty($url->query) && empty($url->fragment))
+        {
+            if(file_exists(theme_file($src,$theme))){
+               $src = theme_path($src, $theme) . '?version=' . LACONICA_VERSION;
+            }else{
+               $src = common_path($src);
+            }
+        }
+        $this->element('link', array('rel' => 'stylesheet',
+                                'type' => 'text/css',
+                                'href' => $src,
+                                'media' => $media));
+    }
+
     /**
      * output an HTML textarea and associated elements
      *
index 7beea9328d27649d507b4e8a753324ae836648d5..34a3d530e8d6bbd17247269f807a584a8cc00e59 100644 (file)
@@ -207,7 +207,7 @@ class ResultItem
         $replier_profile = null;
 
         if ($this->notice->reply_to) {
-            $reply = Notice::staticGet(intval($notice->reply_to));
+            $reply = Notice::staticGet(intval($this->notice->reply_to));
             if ($reply) {
                 $replier_profile = $reply->getProfile();
             }
@@ -224,7 +224,7 @@ class ResultItem
 
         $user = User::staticGet('id', $this->profile->id);
 
-        $this->iso_language_code = $this->user->language;
+        $this->iso_language_code = $user->language;
 
         $this->source = $this->getSourceLink($this->notice->source);
 
index 0050ad8104b2ba4935166fa582a7d29f19b4a7b4..2e471fd6d9407cfaf9484444a012020211357e4e 100644 (file)
@@ -596,32 +596,44 @@ function mail_notify_attn($user, $notice)
     $bestname = $sender->getBestName();
 
     common_init_locale($user->language);
-
+       
+       if ($notice->conversation != $notice->id) {
+               $conversationEmailText = "The full conversation can be read here:\n\n".
+                                                                "\t%5\$s\n\n ";
+               $conversationUrl           = common_local_url('conversation',
+                                 array('id' => $notice->conversation)).'#notice-'.$notice->id;
+       } else {
+               $conversationEmailText = "%5\$s";
+               $conversationUrl = null;
+       }
+       
     $subject = sprintf(_('%s sent a notice to your attention'), $bestname);
-
-    $body = sprintf(_("%1\$s just sent a notice to your attention (an '@-reply') on %2\$s.\n\n".
+       
+       $body = sprintf(_("%1\$s just sent a notice to your attention (an '@-reply') on %2\$s.\n\n".
                       "The notice is here:\n\n".
                       "\t%3\$s\n\n" .
                       "It reads:\n\n".
                       "\t%4\$s\n\n" .
+                      $conversationEmailText .
                       "You can reply back here:\n\n".
-                      "\t%5\$s\n\n" .
+                      "\t%6\$s\n\n" .
                       "The list of all @-replies for you here:\n\n" .
-                      "%6\$s\n\n" .
+                      "%7\$s\n\n" .
                       "Faithfully yours,\n" .
                       "%2\$s\n\n" .
-                      "P.S. You can turn off these email notifications here: %7\$s\n"),
-                    $bestname,
-                    common_config('site', 'name'),
+                      "P.S. You can turn off these email notifications here: %8\$s\n"),
+                    $bestname,//%1
+                    common_config('site', 'name'),//%2
                     common_local_url('shownotice',
-                                     array('notice' => $notice->id)),
-                    $notice->content,
+                                     array('notice' => $notice->id)),//%3
+                    $notice->content,//%4
+                                       $conversationUrl,//%5
                     common_local_url('newnotice',
-                                     array('replyto' => $sender->nickname)),
+                                     array('replyto' => $sender->nickname)),//%6
                     common_local_url('replies',
-                                     array('nickname' => $user->nickname)),
-                    common_local_url('emailsettings'));
-
+                                     array('nickname' => $user->nickname)),//%7
+                    common_local_url('emailsettings'));//%8
+       
     common_init_locale();
     mail_to_user($user, $subject, $body);
 }
@@ -645,13 +657,14 @@ function mail_twitter_bridge_removed($user)
 
     $subject = sprintf(_('Your Twitter bridge has been disabled.'));
 
-    $body = sprintf(_("Hi, %1\$s. We're sorry to inform you that your " .
-        'link to Twitter has been disabled. Your Twitter credentials ' .
-        'have either changed (did you recently change your Twitter ' .
-        'password?) or you have otherwise revoked our access to your ' .
-        "Twitter account.\n\n" .
-        'You can re-enable your Twitter bridge by visiting your ' .
-        "Twitter settings page:\n\n\t%2\$s\n\n" .
+    $site_name = common_config('site', 'name');
+
+    $body = sprintf(_('Hi, %1$s. We\'re sorry to inform you that your ' .
+        'link to Twitter has been disabled. We no longer seem to have ' .
+    'permission to update your Twitter status. (Did you revoke ' .
+    '%3$s\'s access?)' . "\n\n" .
+    'You can re-enable your Twitter bridge by visiting your ' .
+    "Twitter settings page:\n\n\t%2\$s\n\n" .
         "Regards,\n%3\$s\n"),
         $profile->getBestName(),
         common_local_url('twittersettings'),
index 8ea2b36c27b21df3006297edeb2ded887d205d50..044fdc719d31c43c79608162d7ac4102ba948f04 100644 (file)
@@ -140,12 +140,19 @@ class MessageForm extends Form
                                               'rows' => 4,
                                               'name' => 'content'),
                             ($this->content) ? $this->content : '');
-        $this->out->elementStart('dl', 'form_note');
-        $this->out->element('dt', null, _('Available characters'));
-        $this->out->element('dd', array('id' => 'notice_text-count'),
-                            '140');
-        $this->out->elementEnd('dl');
 
+        $contentLimit = Message::maxContent();
+
+        $this->out->element('script', array('type' => 'text/javascript'),
+                            'maxLength = ' . $contentLimit . ';');
+
+        if ($contentLimit > 0) {
+            $this->out->elementStart('dl', 'form_note');
+            $this->out->element('dt', null, _('Available characters'));
+            $this->out->element('dd', array('id' => 'notice_text-count'),
+                                $contentLimit);
+            $this->out->elementEnd('dl');
+        }
     }
 
     /**
index 4e2a2edd61ff78c085d46e5c7419228e7a83a57a..35a21c6bd00518bf52ed0c6a6f98cb65c5abe80e 100644 (file)
@@ -83,7 +83,7 @@ class NoticeForm extends Form
 
         $this->action  = $action;
         $this->content = $content;
-        
+
         if ($user) {
             $this->user = $user;
         } else {
@@ -117,7 +117,6 @@ class NoticeForm extends Form
         return common_local_url('newnotice');
     }
 
-
     /**
      * Legend of the Form
      *
@@ -128,7 +127,6 @@ class NoticeForm extends Form
         $this->out->element('legend', null, _('Send a notice'));
     }
 
-
     /**
      * Data elements
      *
@@ -145,11 +143,20 @@ class NoticeForm extends Form
                                               'rows' => 4,
                                               'name' => 'status_textarea'),
                             ($this->content) ? $this->content : '');
-        $this->out->elementStart('dl', 'form_note');
-        $this->out->element('dt', null, _('Available characters'));
-        $this->out->element('dd', array('id' => 'notice_text-count'),
-                            '140');
-        $this->out->elementEnd('dl');
+
+        $contentLimit = Notice::maxContent();
+
+        $this->out->element('script', array('type' => 'text/javascript'),
+                            'maxLength = ' . $contentLimit . ';');
+
+        if ($contentLimit > 0) {
+            $this->out->elementStart('dl', 'form_note');
+            $this->out->element('dt', null, _('Available characters'));
+            $this->out->element('dd', array('id' => 'notice_text-count'),
+                                $contentLimit);
+            $this->out->elementEnd('dl');
+        }
+
         if (common_config('attachments', 'uploads')) {
             $this->out->element('label', array('for' => 'notice_data-attach'),_('Attach'));
             $this->out->element('input', array('id' => 'notice_data-attach',
index a8d5059ca49bb443c17c21caf93c6fe02af8e150..5429d943f14b8039977d62aa2d44d0506855305b 100644 (file)
@@ -350,11 +350,10 @@ class NoticeListItem extends Widget
 
     function showNoticeLink()
     {
-        $noticeurl = common_local_url('shownotice',
+        if($this->notice->is_local){
+            $noticeurl = common_local_url('shownotice',
                                       array('notice' => $this->notice->id));
-        // XXX: we need to figure this out better. Is this right?
-        if (strcmp($this->notice->uri, $noticeurl) != 0 &&
-            preg_match('/^http/', $this->notice->uri)) {
+        }else{
             $noticeurl = $this->notice->uri;
         }
         $this->out->elementStart('a', array('rel' => 'bookmark',
diff --git a/lib/oauthclient.php b/lib/oauthclient.php
new file mode 100644 (file)
index 0000000..b66a24b
--- /dev/null
@@ -0,0 +1,225 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Base class for doing OAuth calls as a consumer
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Action
+ * @package   Laconica
+ * @author    Zach Copley <zach@controlyourself.ca>
+ * @copyright 2008 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/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+require_once 'OAuth.php';
+
+/**
+ * Exception wrapper for cURL errors
+ *
+ * @category Integration
+ * @package  Laconica
+ * @author   Zach Copley <zach@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ *
+ */
+class OAuthClientCurlException extends Exception
+{
+}
+
+/**
+ * Base class for doing OAuth calls as a consumer
+ *
+ * @category Integration
+ * @package  Laconica
+ * @author   Zach Copley <zach@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ *
+ */
+class OAuthClient
+{
+    var $consumer;
+    var $token;
+
+    /**
+     * Constructor
+     *
+     * Can be initialized with just consumer key and secret for requesting new
+     * tokens or with additional request token or access token
+     *
+     * @param string $consumer_key       consumer key
+     * @param string $consumer_secret    consumer secret
+     * @param string $oauth_token        user's token
+     * @param string $oauth_token_secret user's secret
+     *
+     * @return nothing
+     */
+    function __construct($consumer_key, $consumer_secret,
+                         $oauth_token = null, $oauth_token_secret = null)
+    {
+        $this->sha1_method = new OAuthSignatureMethod_HMAC_SHA1();
+        $this->consumer    = new OAuthConsumer($consumer_key, $consumer_secret);
+        $this->token       = null;
+
+        if (isset($oauth_token) && isset($oauth_token_secret)) {
+            $this->token = new OAuthToken($oauth_token, $oauth_token_secret);
+        }
+    }
+
+    /**
+     * Gets a request token from the given url
+     *
+     * @param string $url OAuth endpoint for grabbing request tokens
+     *
+     * @return OAuthToken $token the request token
+     */
+    function getRequestToken($url)
+    {
+        $response = $this->oAuthGet($url);
+        parse_str($response);
+        $token = new OAuthToken($oauth_token, $oauth_token_secret);
+        return $token;
+    }
+
+    /**
+     * Builds a link that can be redirected to in order to
+     * authorize a request token.
+     *
+     * @param string     $url            endpoint for authorizing request tokens
+     * @param OAuthToken $request_token  the request token to be authorized
+     * @param string     $oauth_callback optional callback url
+     *
+     * @return string $authorize_url the url to redirect to
+     */
+    function getAuthorizeLink($url, $request_token, $oauth_callback = null)
+    {
+        $authorize_url = $url . '?oauth_token=' .
+            $request_token->key;
+
+        if (isset($oauth_callback)) {
+            $authorize_url .= '&oauth_callback=' . urlencode($oauth_callback);
+        }
+
+        return $authorize_url;
+    }
+
+    /**
+     * Fetches an access token
+     *
+     * @param string $url OAuth endpoint for exchanging authorized request tokens
+     *                     for access tokens
+     *
+     * @return OAuthToken $token the access token
+     */
+    function getAccessToken($url)
+    {
+        $response = $this->oAuthPost($url);
+        parse_str($response);
+        $token = new OAuthToken($oauth_token, $oauth_token_secret);
+        return $token;
+    }
+
+    /**
+     * Use HTTP GET to make a signed OAuth request
+     *
+     * @param string $url OAuth endpoint
+     *
+     * @return mixed the request
+     */
+    function oAuthGet($url)
+    {
+        $request = OAuthRequest::from_consumer_and_token($this->consumer,
+            $this->token, 'GET', $url, null);
+        $request->sign_request($this->sha1_method,
+            $this->consumer, $this->token);
+
+        return $this->httpRequest($request->to_url());
+    }
+
+    /**
+     * Use HTTP POST to make a signed OAuth request
+     *
+     * @param string $url    OAuth endpoint
+     * @param array  $params additional post parameters
+     *
+     * @return mixed the request
+     */
+    function oAuthPost($url, $params = null)
+    {
+        $request = OAuthRequest::from_consumer_and_token($this->consumer,
+            $this->token, 'POST', $url, $params);
+        $request->sign_request($this->sha1_method,
+            $this->consumer, $this->token);
+
+        return $this->httpRequest($request->get_normalized_http_url(),
+            $request->to_postdata());
+    }
+
+    /**
+     * Make a HTTP request using cURL.
+     *
+     * @param string $url    Where to make the
+     * @param array  $params post parameters
+     *
+     * @return mixed the request
+     */
+    function httpRequest($url, $params = null)
+    {
+        $options = array(
+            CURLOPT_RETURNTRANSFER => true,
+            CURLOPT_FAILONERROR    => true,
+            CURLOPT_HEADER         => false,
+            CURLOPT_FOLLOWLOCATION => true,
+            CURLOPT_USERAGENT      => 'Laconica',
+            CURLOPT_CONNECTTIMEOUT => 120,
+            CURLOPT_TIMEOUT        => 120,
+            CURLOPT_HTTPAUTH       => CURLAUTH_ANY,
+            CURLOPT_SSL_VERIFYPEER => false,
+
+            // Twitter is strict about accepting invalid "Expect" headers
+
+            CURLOPT_HTTPHEADER => array('Expect:')
+        );
+
+        if (isset($params)) {
+            $options[CURLOPT_POST]       = true;
+            $options[CURLOPT_POSTFIELDS] = $params;
+        }
+
+        $ch = curl_init($url);
+        curl_setopt_array($ch, $options);
+        $response = curl_exec($ch);
+
+        if ($response === false) {
+            $msg  = curl_error($ch);
+            $code = curl_errno($ch);
+            throw new OAuthClientCurlException($msg, $code);
+        }
+
+        curl_close($ch);
+
+        return $response;
+    }
+
+}
index f224c6c2213ed3324ad49d3f87567abc4ef988fd..87d8cf2137b197718d93b4d5803502e5c50f867f 100644 (file)
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-if (!defined('LACONICA')) { exit(1); }
+if (!defined('LACONICA')) {
+    exit(1);
+}
 
-require_once(INSTALLDIR.'/lib/omb.php');
+require_once 'libomb/datastore.php';
 
-class LaconicaOAuthDataStore extends OAuthDataStore
+class LaconicaDataStore extends OMB_Datastore
 {
 
     // We keep a record of who's contacted us
-
     function lookup_consumer($consumer_key)
     {
         $con = Consumer::staticGet('consumer_key', $consumer_key);
@@ -44,7 +45,9 @@ class LaconicaOAuthDataStore extends OAuthDataStore
     function lookup_token($consumer, $token_type, $token_key)
     {
         $t = new Token();
-        $t->consumer_key = $consumer->key;
+        if (!is_null($consumer)) {
+            $t->consumer_key = $consumer->key;
+        }
         $t->tok = $token_key;
         $t->type = ($token_type == 'access') ? 1 : 0;
         if ($t->find(true)) {
@@ -154,4 +157,348 @@ class LaconicaOAuthDataStore extends OAuthDataStore
     {
         return $this->new_access_token($consumer);
     }
+
+
+    /**
+     * Revoke specified OAuth token
+     *
+     * Revokes the authorization token specified by $token_key.
+     * Throws exceptions in case of error.
+     *
+     * @param string $token_key The token to be revoked
+     *
+     * @access public
+     **/
+    public function revoke_token($token_key) {
+        $rt = new Token();
+        $rt->tok = $token_key;
+        $rt->type = 0;
+        $rt->state = 0;
+        if (!$rt->find(true)) {
+            throw new Exception('Tried to revoke unknown token');
+        }
+        if (!$rt->delete()) {
+            throw new Exception('Failed to delete revoked token');
+        }
+    }
+
+    /**
+     * Authorize specified OAuth token
+     *
+     * Authorizes the authorization token specified by $token_key.
+     * Throws exceptions in case of error.
+     *
+     * @param string $token_key The token to be authorized
+     *
+     * @access public
+     **/
+    public function authorize_token($token_key) {
+        $rt = new Token();
+        $rt->tok = $token_key;
+        $rt->type = 0;
+        $rt->state = 0;
+        if (!$rt->find(true)) {
+            throw new Exception('Tried to authorize unknown token');
+        }
+        $orig_rt = clone($rt);
+        $rt->state = 1; # Authorized but not used
+        if (!$rt->update($orig_rt)) {
+            throw new Exception('Failed to authorize token');
+        }
+    }
+
+    /**
+     * Get profile by identifying URI
+     *
+     * Returns an OMB_Profile object representing the OMB profile identified by
+     * $identifier_uri.
+     * Returns null if there is no such OMB profile.
+     * Throws exceptions in case of other error.
+     *
+     * @param string $identifier_uri The OMB identifier URI specifying the
+     *                               requested profile
+     *
+     * @access public
+     *
+     * @return OMB_Profile The corresponding profile
+     **/
+    public function getProfile($identifier_uri) {
+        /* getProfile is only used for remote profiles by libomb.
+           TODO: Make it work with local ones anyway. */
+        $remote = Remote_profile::staticGet('uri', $identifier_uri);
+        if (!$remote) throw new Exception('No such remote profile');
+        $profile = Profile::staticGet('id', $remote->id);
+        if (!$profile) throw new Exception('No profile for remote user');
+
+        require_once INSTALLDIR.'/lib/omb.php';
+        return profile_to_omb_profile($identifier_uri, $profile);
+    }
+
+    /**
+     * Save passed profile
+     *
+     * Stores the OMB profile $profile. Overwrites an existing entry.
+     * Throws exceptions in case of error.
+     *
+     * @param OMB_Profile $profile   The OMB profile which should be saved
+     *
+     * @access public
+     **/
+    public function saveProfile($omb_profile) {
+        if (common_profile_url($omb_profile->getNickname()) ==
+                                                $omb_profile->getProfileURL()) {
+            throw new Exception('Not implemented');
+        } else {
+            $remote = Remote_profile::staticGet('uri', $omb_profile->getIdentifierURI());
+
+            if ($remote) {
+                $exists = true;
+                $profile = Profile::staticGet($remote->id);
+                $orig_remote = clone($remote);
+                $orig_profile = clone($profile);
+                # XXX: compare current postNotice and updateProfile URLs to the ones
+                # stored in the DB to avoid (possibly...) above attack
+            } else {
+                $exists = false;
+                $remote = new Remote_profile();
+                $remote->uri = $omb_profile->getIdentifierURI();
+                $profile = new Profile();
+            }
+
+            $profile->nickname = $omb_profile->getNickname();
+            $profile->profileurl = $omb_profile->getProfileURL();
+
+            $fullname = $omb_profile->getFullname();
+            $profile->fullname = is_null($fullname) ? '' : $fullname;
+            $homepage = $omb_profile->getHomepage();
+            $profile->homepage = is_null($homepage) ? '' : $homepage;
+            $bio = $omb_profile->getBio();
+            $profile->bio = is_null($bio) ? '' : $bio;
+            $location = $omb_profile->getLocation();
+            $profile->location = is_null($location) ? '' : $location;
+
+            if ($exists) {
+                $profile->update($orig_profile);
+            } else {
+                $profile->created = DB_DataObject_Cast::dateTime(); # current time
+                $id = $profile->insert();
+                if (!$id) {
+                    throw new Exception(_('Error inserting new profile'));
+                }
+                $remote->id = $id;
+            }
+
+            $avatar_url = $omb_profile->getAvatarURL();
+            if ($avatar_url) {
+                if (!$this->add_avatar($profile, $avatar_url)) {
+                    throw new Exception(_('Error inserting avatar'));
+                }
+            } else {
+                $avatar = $profile->getOriginalAvatar();
+                if($avatar) $avatar->delete();
+                $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
+                if($avatar) $avatar->delete();
+                $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
+                if($avatar) $avatar->delete();
+                $avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
+                if($avatar) $avatar->delete();
+            }
+
+            if ($exists) {
+                if (!$remote->update($orig_remote)) {
+                    throw new Exception(_('Error updating remote profile'));
+                }
+            } else {
+                $remote->created = DB_DataObject_Cast::dateTime(); # current time
+                if (!$remote->insert()) {
+                    throw new Exception(_('Error inserting remote profile'));
+                }
+            }
+        }
+    }
+
+    function add_avatar($profile, $url)
+    {
+        $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
+        copy($url, $temp_filename);
+        $imagefile = new ImageFile($profile->id, $temp_filename);
+        $filename = Avatar::filename($profile->id,
+                                     image_type_to_extension($imagefile->type),
+                                     null,
+                                     common_timestamp());
+        rename($temp_filename, Avatar::path($filename));
+        return $profile->setOriginal($filename);
+    }
+
+    /**
+     * Save passed notice
+     *
+     * Stores the OMB notice $notice. The datastore may change the passed notice.
+     * This might by neccessary for URIs depending on a database key. Note that
+     * it is the user’s duty to present a mechanism for his OMB_Datastore to
+     * appropriately change his OMB_Notice.
+     * Throws exceptions in case of error.
+     *
+     * @param OMB_Notice $notice The OMB notice which should be saved
+     *
+     * @access public
+     **/
+    public function saveNotice(&$omb_notice) {
+        if (Notice::staticGet('uri', $omb_notice->getIdentifierURI())) {
+            throw new Exception(_('Duplicate notice'));
+        }
+        $author_uri = $omb_notice->getAuthor()->getIdentifierURI();
+        common_log(LOG_DEBUG, $author_uri, __FILE__);
+        $author = Remote_profile::staticGet('uri', $author_uri);
+        if (!$author) {
+            $author = User::staticGet('uri', $author_uri);
+        }
+        if (!$author) {
+            throw new Exception('No such user');
+        }
+
+        common_log(LOG_DEBUG, print_r($author, true), __FILE__);
+
+        $notice = Notice::saveNew($author->id,
+                                  $omb_notice->getContent(),
+                                  'omb',
+                                  false,
+                                  null,
+                                  $omb_notice->getIdentifierURI());
+        if (is_string($notice)) {
+            throw new Exception($notice);
+        }
+        common_broadcast_notice($notice, true);
+    }
+
+    /**
+     * Get subscriptions of a given profile
+     *
+     * Returns an array containing subscription informations for the specified
+     * profile. Every array entry should in turn be an array with keys
+     *   'uri´: The identifier URI of the subscriber
+     *   'token´: The subscribe token
+     *   'secret´: The secret token
+     * Throws exceptions in case of error.
+     *
+     * @param string $subscribed_user_uri The OMB identifier URI specifying the
+     *                                    subscribed profile
+     *
+     * @access public
+     *
+     * @return mixed An array containing the subscriptions or 0 if no
+     *               subscription has been found.
+     **/
+    public function getSubscriptions($subscribed_user_uri) {
+        $sub = new Subscription();
+
+        $user = $this->_getAnyProfile($subscribed_user_uri);
+
+        $sub->subscribed = $user->id;
+
+        if (!$sub->find(true)) {
+            return 0;
+        }
+
+        /* Since we do not use OMB_Service_Provider’s action methods, there
+           is no need to actually return the subscriptions. */
+        return 1;
+    }
+
+    private function _getAnyProfile($uri)
+    {
+        $user = Remote_profile::staticGet('uri', $uri);
+        if (!$user) {
+            $user = User::staticGet('uri', $uri);
+        }
+        if (!$user) {
+            throw new Exception('No such user');
+        }
+        return $user;
+    }
+
+    /**
+     * Delete a subscription
+     *
+     * Deletes the subscription from $subscriber_uri to $subscribed_user_uri.
+     * Throws exceptions in case of error.
+     *
+     * @param string $subscriber_uri      The OMB identifier URI specifying the
+     *                                    subscribing profile
+     *
+     * @param string $subscribed_user_uri The OMB identifier URI specifying the
+     *                                    subscribed profile
+     *
+     * @access public
+     **/
+    public function deleteSubscription($subscriber_uri, $subscribed_user_uri)
+    {
+        $sub = new Subscription();
+
+        $subscribed = $this->_getAnyProfile($subscribed_user_uri);
+        $subscriber = $this->_getAnyProfile($subscriber_uri);
+
+        $sub->subscribed = $subscribed->id;
+        $sub->subscriber = $subscriber->id;
+
+        $sub->delete();
+    }
+
+    /**
+     * Save a subscription
+     *
+     * Saves the subscription from $subscriber_uri to $subscribed_user_uri.
+     * Throws exceptions in case of error.
+     *
+     * @param string     $subscriber_uri      The OMB identifier URI specifying
+     *                                        the subscribing profile
+     *
+     * @param string     $subscribed_user_uri The OMB identifier URI specifying
+     *                                        the subscribed profile
+     * @param OAuthToken $token               The access token
+     *
+     * @access public
+     **/
+    public function saveSubscription($subscriber_uri, $subscribed_user_uri,
+                                                                       $token)
+    {
+        $sub = new Subscription();
+
+        $subscribed = $this->_getAnyProfile($subscribed_user_uri);
+        $subscriber = $this->_getAnyProfile($subscriber_uri);
+
+        $sub->subscribed = $subscribed->id;
+        $sub->subscriber = $subscriber->id;
+
+        $sub_exists = $sub->find(true);
+
+        if ($sub_exists) {
+            $orig_sub = clone($sub);
+        } else {
+            $sub->created = DB_DataObject_Cast::dateTime();
+        }
+
+        $sub->token  = $token->key;
+        $sub->secret = $token->secret;
+
+        if ($sub_exists) {
+            $result = $sub->update($orig_sub);
+        } else {
+            $result = $sub->insert();
+        }
+
+        if (!$result) {
+            common_log_db_error($sub, ($sub_exists) ? 'UPDATE' : 'INSERT', __FILE__);
+            throw new Exception(_('Couldn\'t insert new subscription.'));
+            return;
+        }
+
+        /* Notify user, if necessary. */
+
+        if ($subscribed instanceof User) {
+            mail_subscribe_notify_profile($subscribed,
+                                          Profile::staticGet($subscriber->id));
+        }
+    }
 }
+?>
index 4f6a9609541ded863edcc62edef1ffcff10f4283..b9d0eef64e6291e69e38d505590acd00a623f45c 100644 (file)
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-if (!defined('LACONICA')) { exit(1); }
-
-require_once('OAuth.php');
-require_once(INSTALLDIR.'/lib/oauthstore.php');
-
-require_once(INSTALLDIR.'/classes/Consumer.php');
-require_once(INSTALLDIR.'/classes/Nonce.php');
-require_once(INSTALLDIR.'/classes/Token.php');
-
-require_once('Auth/Yadis/Yadis.php');
-
-define('OAUTH_NAMESPACE', 'http://oauth.net/core/1.0/');
-define('OMB_NAMESPACE', 'http://openmicroblogging.org/protocol/0.1');
-define('OMB_VERSION_01', 'http://openmicroblogging.org/protocol/0.1');
-define('OAUTH_DISCOVERY', 'http://oauth.net/discovery/1.0');
+if (!defined('LACONICA')) {
+    exit(1);
+}
 
-define('OMB_ENDPOINT_UPDATEPROFILE', OMB_NAMESPACE.'/updateProfile');
-define('OMB_ENDPOINT_POSTNOTICE', OMB_NAMESPACE.'/postNotice');
-define('OAUTH_ENDPOINT_REQUEST', OAUTH_NAMESPACE.'endpoint/request');
-define('OAUTH_ENDPOINT_AUTHORIZE', OAUTH_NAMESPACE.'endpoint/authorize');
-define('OAUTH_ENDPOINT_ACCESS', OAUTH_NAMESPACE.'endpoint/access');
-define('OAUTH_ENDPOINT_RESOURCE', OAUTH_NAMESPACE.'endpoint/resource');
-define('OAUTH_AUTH_HEADER', OAUTH_NAMESPACE.'parameters/auth-header');
-define('OAUTH_POST_BODY', OAUTH_NAMESPACE.'parameters/post-body');
-define('OAUTH_HMAC_SHA1', OAUTH_NAMESPACE.'signature/HMAC-SHA1');
+require_once INSTALLDIR.'/lib/oauthstore.php';
+require_once 'OAuth.php';
+require_once 'libomb/constants.php';
+require_once 'libomb/service_consumer.php';
+require_once 'libomb/notice.php';
+require_once 'libomb/profile.php';
+require_once 'Auth/Yadis/Yadis.php';
 
 function omb_oauth_consumer()
 {
     static $con = null;
-    if (!$con) {
+    if (is_null($con)) {
         $con = new OAuthConsumer(common_root_url(), '');
     }
     return $con;
@@ -55,7 +41,7 @@ function omb_oauth_consumer()
 function omb_oauth_server()
 {
     static $server = null;
-    if (!$server) {
+    if (is_null($server)) {
         $server = new OAuthServer(omb_oauth_datastore());
         $server->add_signature_method(omb_hmac_sha1());
     }
@@ -65,8 +51,8 @@ function omb_oauth_server()
 function omb_oauth_datastore()
 {
     static $store = null;
-    if (!$store) {
-        $store = new LaconicaOAuthDataStore();
+    if (is_null($store)) {
+        $store = new LaconicaDataStore();
     }
     return $store;
 }
@@ -74,57 +60,18 @@ function omb_oauth_datastore()
 function omb_hmac_sha1()
 {
     static $hmac_method = null;
-    if (!$hmac_method) {
+    if (is_null($hmac_method)) {
         $hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
     }
     return $hmac_method;
 }
 
-function omb_get_services($xrd, $type)
+function omb_broadcast_notice($notice)
 {
-    return $xrd->services(array(omb_service_filter($type)));
-}
 
-function omb_service_filter($type)
-{
-    return create_function('$s',
-                           'return omb_match_service($s, \''.$type.'\');');
-}
+    $omb_notice = notice_to_omb_notice($notice);
 
-function omb_match_service($service, $type)
-{
-    return in_array($type, $service->getTypes());
-}
-
-function omb_service_uri($service)
-{
-    if (!$service) {
-        return null;
-    }
-    $uris = $service->getURIs();
-    if (!$uris) {
-        return null;
-    }
-    return $uris[0];
-}
-
-function omb_local_id($service)
-{
-    if (!$service) {
-        return null;
-    }
-    $els = $service->getElements('xrd:LocalID');
-    if (!$els) {
-        return null;
-    }
-    $el = $els[0];
-    return $service->parser->content($el);
-}
-
-function omb_broadcast_remote_subscribers($notice)
-{
-
-    # First, get remote users subscribed to this profile
+    /* Get remote users subscribed to this profile. */
     $rp = new Remote_profile();
 
     $rp->query('SELECT postnoticeurl, token, secret ' .
@@ -135,170 +82,148 @@ function omb_broadcast_remote_subscribers($notice)
     $posted = array();
 
     while ($rp->fetch()) {
-        if (!$posted[$rp->postnoticeurl]) {
-            common_log(LOG_DEBUG, 'Posting to ' . $rp->postnoticeurl);
-            if (omb_post_notice_keys($notice, $rp->postnoticeurl, $rp->token, $rp->secret)) {
-                common_log(LOG_DEBUG, 'Finished to ' . $rp->postnoticeurl);
-                $posted[$rp->postnoticeurl] = true;
-            } else {
-                common_log(LOG_DEBUG, 'Failed posting to ' . $rp->postnoticeurl);
-            }
+        if (isset($posted[$rp->postnoticeurl])) {
+            /* We already posted to this url. */
+            continue;
         }
-    }
-
-    $rp->free();
-    unset($rp);
+        common_debug('Posting to ' . $rp->postnoticeurl, __FILE__);
+
+        /* Post notice. */
+        $service = new Laconica_OMB_Service_Consumer(
+                     array(OMB_ENDPOINT_POSTNOTICE => $rp->postnoticeurl));
+        try {
+            $service->setToken($rp->token, $rp->secret);
+            $service->postNotice($omb_notice);
+        } catch (Exception $e) {
+            common_log(LOG_ERR, 'Failed posting to ' . $rp->postnoticeurl);
+            common_log(LOG_ERR, 'Error status '.$e);
+            continue;
+        }
+        $posted[$rp->postnoticeurl] = true;
 
-    return true;
-}
+        common_debug('Finished to ' . $rp->postnoticeurl, __FILE__);
+    }
 
-function omb_post_notice($notice, $remote_profile, $subscription)
-{
-    return omb_post_notice_keys($notice, $remote_profile->postnoticeurl, $subscription->token, $subscription->secret);
+    return;
 }
 
-function omb_post_notice_keys($notice, $postnoticeurl, $tk, $secret)
+function omb_broadcast_profile($profile)
 {
-    $user = User::staticGet('id', $notice->profile_id);
+    $user = User::staticGet('id', $profile->id);
 
     if (!$user) {
         return false;
     }
 
-    $con = omb_oauth_consumer();
-
-    $token = new OAuthToken($tk, $secret);
-
-    $url = $postnoticeurl;
-    $parsed = parse_url($url);
-    $params = array();
-    parse_str($parsed['query'], $params);
+    $profile = $user->getProfile();
 
-    $req = OAuthRequest::from_consumer_and_token($con, $token,
-                                                 'POST', $url, $params);
+    $omb_profile = profile_to_omb_profile($user->uri, $profile, true);
 
-    $req->set_parameter('omb_version', OMB_VERSION_01);
-    $req->set_parameter('omb_listenee', $user->uri);
-    $req->set_parameter('omb_notice', $notice->uri);
-    $req->set_parameter('omb_notice_content', $notice->content);
-    $req->set_parameter('omb_notice_url', common_local_url('shownotice',
-                                                           array('notice' =>
-                                                                 $notice->id)));
-    $req->set_parameter('omb_notice_license', common_config('license', 'url'));
-
-    $user->free();
-    unset($user);
+    /* Get remote users subscribed to this profile. */
+    $rp = new Remote_profile();
 
-    $req->sign_request(omb_hmac_sha1(), $con, $token);
+    $rp->query('SELECT updateprofileurl, token, secret ' .
+               'FROM subscription JOIN remote_profile ' .
+               'ON subscription.subscriber = remote_profile.id ' .
+               'WHERE subscription.subscribed = ' . $profile->id . ' ');
 
-    # We re-use this tool's fetcher, since it's pretty good
+    $posted = array();
 
-    $fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
+    while ($rp->fetch()) {
+        if (isset($posted[$rp->updateprofileurl])) {
+            /* We already posted to this url. */
+            continue;
+        }
+        common_debug('Posting to ' . $rp->updateprofileurl, __FILE__);
+
+        /* Update profile. */
+        $service = new Laconica_OMB_Service_Consumer(
+                     array(OMB_ENDPOINT_UPDATEPROFILE => $rp->updateprofileurl));
+        try {
+            $service->setToken($rp->token, $rp->secret);
+            $service->updateProfile($omb_profile);
+        } catch (Exception $e) {
+            common_log(LOG_ERR, 'Failed posting to ' . $rp->updateprofileurl);
+            common_log(LOG_ERR, 'Error status '.$e);
+            continue;
+        }
+        $posted[$rp->updateprofileurl] = true;
 
-    if (!$fetcher) {
-        common_log(LOG_WARNING, 'Failed to initialize Yadis fetcher.', __FILE__);
-        return false;
+        common_debug('Finished to ' . $rp->updateprofileurl, __FILE__);
     }
 
-    $result = $fetcher->post($req->get_normalized_http_url(),
-                             $req->to_postdata(),
-                             array('User-Agent: Laconica/' . LACONICA_VERSION));
-
-    if ($result->status == 403) { # not authorized, don't send again
-        common_debug('403 result, deleting subscription', __FILE__);
-        # FIXME: figure out how to delete this
-        # $subscription->delete();
-        return false;
-    } else if ($result->status != 200) {
-        common_debug('Error status '.$result->status, __FILE__);
-        return false;
-    } else { # success!
-        parse_str($result->body, $return);
-        if ($return['omb_version'] == OMB_VERSION_01) {
-            return true;
-        } else {
-            return false;
-        }
-    }
+    return;
 }
 
-function omb_broadcast_profile($profile)
-{
-    # First, get remote users subscribed to this profile
-    # XXX: use a join here rather than looping through results
-    $sub = new Subscription();
-    $sub->subscribed = $profile->id;
-    if ($sub->find()) {
-        $updated = array();
-        while ($sub->fetch()) {
-            $rp = Remote_profile::staticGet('id', $sub->subscriber);
-            if ($rp) {
-                if (!array_key_exists($rp->updateprofileurl, $updated)) {
-                    if (omb_update_profile($profile, $rp, $sub)) {
-                        $updated[$rp->updateprofileurl] = true;
-                    }
-                }
-            }
-        }
+class Laconica_OMB_Service_Consumer extends OMB_Service_Consumer {
+    public function __construct($urls)
+    {
+        $this->services       = $urls;
+        $this->datastore      = omb_oauth_datastore();
+        $this->oauth_consumer = omb_oauth_consumer();
+        $this->fetcher        = Auth_Yadis_Yadis::getHTTPFetcher();
     }
+
 }
 
-function omb_update_profile($profile, $remote_profile, $subscription)
+function profile_to_omb_profile($uri, $profile, $force = false)
 {
-    $user = User::staticGet($profile->id);
-    $con = omb_oauth_consumer();
-    $token = new OAuthToken($subscription->token, $subscription->secret);
-    $url = $remote_profile->updateprofileurl;
-    $parsed = parse_url($url);
-    $params = array();
-    parse_str($parsed['query'], $params);
-    $req = OAuthRequest::from_consumer_and_token($con, $token,
-                                                 "POST", $url, $params);
-    $req->set_parameter('omb_version', OMB_VERSION_01);
-    $req->set_parameter('omb_listenee', $user->uri);
-    $req->set_parameter('omb_listenee_profile', common_profile_url($profile->nickname));
-    $req->set_parameter('omb_listenee_nickname', $profile->nickname);
-
-    # We use blanks to force emptying any existing values in these optional fields
-
-    $req->set_parameter('omb_listenee_fullname',
-                        ($profile->fullname) ? $profile->fullname : '');
-    $req->set_parameter('omb_listenee_homepage',
-                        ($profile->homepage) ? $profile->homepage : '');
-    $req->set_parameter('omb_listenee_bio',
-                        ($profile->bio) ? $profile->bio : '');
-    $req->set_parameter('omb_listenee_location',
-                        ($profile->location) ? $profile->location : '');
+    $omb_profile = new OMB_Profile($uri);
+    $omb_profile->setNickname($profile->nickname);
+    $omb_profile->setLicenseURL(common_config('license', 'url'));
+    if (!is_null($profile->fullname)) {
+        $omb_profile->setFullname($profile->fullname);
+    } elseif ($force) {
+        $omb_profile->setFullname('');
+    }
+    if (!is_null($profile->homepage)) {
+        $omb_profile->setHomepage($profile->homepage);
+    } elseif ($force) {
+        $omb_profile->setHomepage('');
+    }
+    if (!is_null($profile->bio)) {
+        $omb_profile->setBio($profile->bio);
+    } elseif ($force) {
+        $omb_profile->setBio('');
+    }
+    if (!is_null($profile->location)) {
+        $omb_profile->setLocation($profile->location);
+    } elseif ($force) {
+        $omb_profile->setLocation('');
+    }
+    if (!is_null($profile->profileurl)) {
+        $omb_profile->setProfileURL($profile->profileurl);
+    } elseif ($force) {
+        $omb_profile->setProfileURL('');
+    }
 
     $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
-    $req->set_parameter('omb_listenee_avatar',
-                        ($avatar) ? $avatar->url : '');
+    if ($avatar) {
+        $omb_profile->setAvatarURL($avatar->url);
+    } elseif ($force) {
+        $omb_profile->setAvatarURL('');
+    }
+    return $omb_profile;
+}
 
-    $req->sign_request(omb_hmac_sha1(), $con, $token);
+function notice_to_omb_notice($notice)
+{
+    /* Create an OMB_Notice for $notice. */
+    $user = User::staticGet('id', $notice->profile_id);
 
-    # We re-use this tool's fetcher, since it's pretty good
+    if (!$user) {
+        return null;
+    }
 
-    $fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
+    $profile = $user->getProfile();
 
-    $result = $fetcher->post($req->get_normalized_http_url(),
-                             $req->to_postdata(),
-                             array('User-Agent: Laconica/' . LACONICA_VERSION));
+    $omb_notice = new OMB_Notice(profile_to_omb_profile($user->uri, $profile),
+                                 $notice->uri,
+                                 $notice->content);
+    $omb_notice->setURL(common_local_url('shownotice', array('notice' =>
+                                                                 $notice->id)));
+    $omb_notice->setLicenseURL(common_config('license', 'url'));
 
-    if (empty($result) || !$result) {
-        common_debug("Unable to contact " . $req->get_normalized_http_url());
-    } else if ($result->status == 403) { # not authorized, don't send again
-        common_debug('403 result, deleting subscription', __FILE__);
-        $subscription->delete();
-        return false;
-    } else if ($result->status != 200) {
-        common_debug('Error status '.$result->status, __FILE__);
-        return false;
-    } else { # success!
-        parse_str($result->body, $return);
-        if (isset($return['omb_version']) && $return['omb_version'] === OMB_VERSION_01) {
-            return true;
-        } else {
-            return false;
-        }
-    }
+    return $omb_notice;
 }
+?>
diff --git a/lib/parallelizingdaemon.php b/lib/parallelizingdaemon.php
new file mode 100644 (file)
index 0000000..dc28b56
--- /dev/null
@@ -0,0 +1,229 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Base class for making daemons that can do several tasks in parallel.
+ *
+ * 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  Daemon
+ * @package   Laconica
+ * @author    Zach Copley <zach@controlyourself.ca>
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 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/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+declare(ticks = 1);
+
+/**
+ * Daemon able to spawn multiple child processes to do work in parallel
+ *
+ * @category Daemon
+ * @package  Laconica
+ * @author   Zach Copley <zach@controlyourself.ca>
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class ParallelizingDaemon extends Daemon
+{
+    private $_children     = array();
+    private $_interval     = 0; // seconds
+    private $_max_children = 0; // maximum number of children
+    private $_debug        = false;
+
+    /**
+     *  Constructor
+     *
+     * @param string  $id           the name/id of this daemon
+     * @param int     $interval     sleep this long before doing everything again
+     * @param int     $max_children maximum number of child processes at a time
+     * @param boolean $debug        debug output flag
+     *
+     * @return void
+     *
+     **/
+
+    function __construct($id = null, $interval = 60, $max_children = 2,
+                         $debug = null)
+    {
+        parent::__construct(true); // daemonize
+
+        $this->_interval     = $interval;
+        $this->_max_children = $max_children;
+        $this->_debug        = $debug;
+
+        if (isset($id)) {
+            $this->set_id($id);
+        }
+    }
+
+    /**
+     * Run the daemon
+     *
+     * @return void
+     */
+
+    function run()
+    {
+        if (isset($this->_debug)) {
+            echo $this->name() . " - Debugging output enabled.\n";
+        }
+
+        do {
+
+            $objects = $this->getObjects();
+
+            foreach ($objects as $o) {
+
+                // Fork a child for each object
+
+                $pid = pcntl_fork();
+
+                if ($pid == -1) {
+                    die ($this->name() . ' - Couldn\'t fork!');
+                }
+
+                if ($pid) {
+
+                    // Parent
+
+                    if (isset($this->_debug)) {
+                        echo $this->name() .
+                          " - Forked new child - pid $pid.\n";
+
+                    }
+
+                    $this->_children[] = $pid;
+
+                } else {
+
+                    // Child
+
+                    // Do something with each object
+
+                    $this->childTask($o);
+
+                    exit();
+                }
+
+                // Remove child from ps list as it finishes
+
+                while (($c = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) {
+
+                    if (isset($this->_debug)) {
+                        echo $this->name() . " - Child $c finished.\n";
+                    }
+
+                    $this->removePs($this->_children, $c);
+                }
+
+                // Wait! We have too many damn kids.
+
+                if (sizeof($this->_children) >= $this->_max_children) {
+
+                    if (isset($this->_debug)) {
+                        echo $this->name() . " - Too many children. Waiting...\n";
+                    }
+
+                    if (($c = pcntl_wait($status, WUNTRACED)) > 0) {
+
+                        if (isset($this->_debug)) {
+                            echo $this->name() .
+                              " - Finished waiting for child $c.\n";
+                        }
+
+                        $this->removePs($this->_children, $c);
+                    }
+                }
+            }
+
+            // Remove all children from the process list before restarting
+            while (($c = pcntl_wait($status, WUNTRACED)) > 0) {
+
+                if (isset($this->_debug)) {
+                    echo $this->name() . " - Child $c finished.\n";
+                }
+
+                $this->removePs($this->_children, $c);
+            }
+
+            // Rest for a bit
+
+            if (isset($this->_debug)) {
+                echo $this->name() . ' - Waiting ' . $this->_interval .
+                  " secs before running again.\n";
+            }
+
+            if ($this->_interval > 0) {
+                sleep($this->_interval);
+            }
+
+        } while (true);
+    }
+
+    /**
+     * Remove a child process from the list of children
+     *
+     * @param array &$plist array of processes
+     * @param int   $ps     process id
+     *
+     * @return void
+     */
+
+    function removePs(&$plist, $ps)
+    {
+        for ($i = 0; $i < sizeof($plist); $i++) {
+            if ($plist[$i] == $ps) {
+                unset($plist[$i]);
+                $plist = array_values($plist);
+                break;
+            }
+        }
+    }
+
+    /**
+     * Get a list of objects to work on in parallel
+     *
+     * @return array An array of objects to work on
+     */
+
+    function getObjects()
+    {
+        die('Implement ParallelizingDaemon::getObjects().');
+    }
+
+    /**
+     * Do something with each object in parallel
+     *
+     * @param mixed $object data to work on
+     *
+     * @return void
+     */
+
+    function childTask($object)
+    {
+        die("Implement ParallelizingDaemon::childTask($object).");
+    }
+
+}
\ No newline at end of file
index 9ff243fb53fab479a7ebd9142032409cbab4366f..d463a07b0825e0d0bb69d2110173d99785e7b1e5 100644 (file)
@@ -97,7 +97,7 @@ class ProfileSection extends Section
         $this->out->elementEnd('a');
         $this->out->elementEnd('span');
         $this->out->elementEnd('td');
-        if ($profile->value) {
+        if (isset($profile->value)) {
             $this->out->element('td', 'value', $profile->value);
         }
 
index 90ffaec16a62629234006e7675da6fcc9d4d9aa2..08bc0566dd3ed5799553799cbaaa94b696ba194b 100644 (file)
@@ -86,6 +86,10 @@ class Router
 
         $m->connect('doc/:title', array('action' => 'doc'));
 
+        // Twitter
+
+        $m->connect('twitter/authorization', array('action' => 'twitterauthorization'));
+
         // facebook
 
         $m->connect('facebook', array('action' => 'facebookhome'));
@@ -111,15 +115,8 @@ class Router
 
         $m->connect('main/tagother/:id', array('action' => 'tagother'));
 
-        $m->connect('main/oembed.xml',
-                    array('action' => 'api',
-                          'method' => 'oembed.xml',
-                          'apiaction' => 'oembed'));
-
-        $m->connect('main/oembed.json',
-                    array('action' => 'api',
-                          'method' => 'oembed.json',
-                          'apiaction' => 'oembed'));
+        $m->connect('main/oembed',
+                    array('action' => 'oembed'));
 
         // these take a code
 
@@ -406,6 +403,28 @@ class Router
                           'apiaction' => 'laconica'));
 
         // Groups
+        //'list' has to be handled differently, as php will not allow a method to be named 'list'
+        $m->connect('api/laconica/groups/list/:argument',
+                    array('action' => 'api',
+                          'method' => 'list_groups',
+                          'apiaction' => 'groups'));
+        foreach (array('xml', 'json', 'rss', 'atom') as $e) {
+            $m->connect('api/laconica/groups/list.' . $e,
+                    array('action' => 'api',
+                          'method' => 'list_groups.' . $e,
+                          'apiaction' => 'groups'));
+        }
+
+        $m->connect('api/laconica/groups/:method',
+                    array('action' => 'api',
+                          'apiaction' => 'statuses'),
+                    array('method' => '(list_all|)(\.(atom|rss|xml|json))?'));
+
+        $m->connect('api/statuses/:method/:argument',
+                    array('action' => 'api',
+                          'apiaction' => 'statuses'),
+                    array('method' => '(|user_timeline|friends_timeline|replies|mentions|show|destroy|friends|followers)'));
+
         $m->connect('api/laconica/groups/:method/:argument',
                     array('action' => 'api',
                           'apiaction' => 'groups'));
index 772f41883b9dde17f5b5e59f39c276628fe189ad..7c26363fc5e385a79ed6d9272ee39370ad58d91b 100644 (file)
@@ -120,7 +120,7 @@ class MySQLSearch extends SearchEngine
         } else if ('identica_notices' === $this->table) {
 
             // Don't show imported notices
-            $this->target->whereAdd('notice.is_local != ' . NOTICE_GATEWAY);
+            $this->target->whereAdd('notice.is_local != ' . Notice::GATEWAY);
 
             if (strtolower($q) != $q) {
                 $this->target->whereAdd("( MATCH(content) AGAINST ('" . addslashes($q) .
index db735216685848daf9b436b8104a47c06e811dfe..c46f3228b0706044001d98f001c684a92471d0fb 100644 (file)
@@ -52,6 +52,7 @@ require_once INSTALLDIR.'/lib/error.php';
  * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
  * @link     http://laconi.ca/
  */
+
 class ServerErrorAction extends ErrorAction
 {
     function __construct($message='Error', $code=500)
@@ -66,6 +67,10 @@ class ServerErrorAction extends ErrorAction
                                505 => 'HTTP Version Not Supported');
 
         $this->default = 500;
+
+        // Server errors must be logged.
+
+        common_log(LOG_ERR, "ServerErrorAction: $code $message");
     }
 
     // XXX: Should these error actions even be invokable via URI?
index 47af32e61f3221d72de65136e9b43805a31af504..280cdb0a33d93d6fcfcdef2e8669f9cee55e383d 100644 (file)
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-if (!defined('LACONICA')) { exit(1); }
-
-define('TWITTER_SERVICE', 1); // Twitter is foreign_service ID 1
-
-function get_twitter_data($uri, $screen_name, $password)
-{
-
-    $options = array(
-            CURLOPT_USERPWD => sprintf("%s:%s", $screen_name, $password),
-            CURLOPT_RETURNTRANSFER    => true,
-            CURLOPT_FAILONERROR        => true,
-            CURLOPT_HEADER            => false,
-            CURLOPT_FOLLOWLOCATION    => true,
-            CURLOPT_USERAGENT      => "Laconica",
-            CURLOPT_CONNECTTIMEOUT    => 120,
-            CURLOPT_TIMEOUT            => 120,
-            # Twitter is strict about accepting invalid "Expect" headers
-            CURLOPT_HTTPHEADER => array('Expect:')
-    );
-
-    $ch = curl_init($uri);
-    curl_setopt_array($ch, $options);
-    $data = curl_exec($ch);
-    $errmsg = curl_error($ch);
-
-    if ($errmsg) {
-        common_debug("Twitter bridge - cURL error: $errmsg - trying to load: $uri with user $screen_name.",
-            __FILE__);
-
-        if (defined('SCRIPT_DEBUG')) {
-            print "cURL error: $errmsg - trying to load: $uri with user $screen_name.\n";
-        }
-    }
-
-    curl_close($ch);
-
-    return $data;
-}
-
-function twitter_json_data($uri, $screen_name, $password)
-{
-    $json_data = get_twitter_data($uri, $screen_name, $password);
-
-    if (!$json_data) {
-        return false;
-    }
-
-    $data = json_decode($json_data);
-
-    if (!$data) {
-        return false;
-    }
-
-    return $data;
-}
-
-function twitter_user_info($screen_name, $password)
-{
-    $uri = "http://twitter.com/users/show/$screen_name.json";
-    return twitter_json_data($uri, $screen_name, $password);
+if (!defined('LACONICA')) {
+    exit(1);
 }
 
-function twitter_friends_ids($screen_name, $password)
-{
-    $uri = "http://twitter.com/friends/ids/$screen_name.json";
-    return twitter_json_data($uri, $screen_name, $password);
-}
+define('TWITTER_SERVICE', 1); // Twitter is foreign_service ID 1
 
 function update_twitter_user($twitter_id, $screen_name)
 {
     $uri = 'http://twitter.com/' . $screen_name;
-
     $fuser = new Foreign_user();
 
     $fuser->query('BEGIN');
 
-    // Dropping down to SQL because regular db_object udpate stuff doesn't seem
+    // Dropping down to SQL because regular DB_DataObject udpate stuff doesn't seem
     // to work so good with tables that have multiple column primary keys
 
     // Any time we update the uri for a forein user we have to make sure there
@@ -102,35 +39,14 @@ function update_twitter_user($twitter_id, $screen_name)
     $qry = 'UPDATE foreign_user set uri = \'\' WHERE uri = ';
     $qry .= '\'' . $uri . '\'' . ' AND service = ' . TWITTER_SERVICE;
 
-    $result = $fuser->query($qry);
-
-    if ($result) {
-        common_debug("Removed uri ($uri) from another foreign_user who was squatting on it.");
-        if (defined('SCRIPT_DEBUG')) {
-            print("Removed uri ($uri) from another Twitter user who was squatting on it.\n");
-        }
-    }
+    $fuser->query($qry);
 
     // Update the user
+
     $qry = 'UPDATE foreign_user SET nickname = ';
     $qry .= '\'' . $screen_name . '\'' . ', uri = \'' . $uri . '\' ';
     $qry .= 'WHERE id = ' . $twitter_id . ' AND service = ' . TWITTER_SERVICE;
 
-    $result = $fuser->query($qry);
-
-    if (!$result) {
-        common_log(LOG_WARNING,
-            "Couldn't update foreign_user data for Twitter user: $screen_name");
-        common_log_db_error($fuser, 'UPDATE', __FILE__);
-        if (defined('SCRIPT_DEBUG')) {
-            print "UPDATE failed: for Twitter user:  $twitter_id - $screen_name. - ";
-            print common_log_objstring($fuser) . "\n";
-            $error = &PEAR::getStaticProperty('DB_DataObject','lastError');
-            print "DB_DataObject Error: " . $error->getMessage() . "\n";
-        }
-        return false;
-    }
-
     $fuser->query('COMMIT');
 
     $fuser->free();
@@ -147,23 +63,22 @@ function add_twitter_user($twitter_id, $screen_name)
     // Clear out any bad old foreign_users with the new user's legit URL
     // This can happen when users move around or fakester accounts get
     // repoed, and things like that.
+
     $luser = new Foreign_user();
     $luser->uri = $new_uri;
     $luser->service = TWITTER_SERVICE;
     $result = $luser->delete();
 
-    if ($result) {
+    if (empty($result)) {
         common_log(LOG_WARNING,
             "Twitter bridge - removed invalid Twitter user squatting on uri: $new_uri");
-        if (defined('SCRIPT_DEBUG')) {
-            print "Removed invalid Twitter user squatting on uri: $new_uri\n";
-        }
     }
 
     $luser->free();
     unset($luser);
 
     // Otherwise, create a new Twitter user
+
     $fuser = new Foreign_user();
 
     $fuser->nickname = $screen_name;
@@ -173,21 +88,12 @@ function add_twitter_user($twitter_id, $screen_name)
     $fuser->created = common_sql_now();
     $result = $fuser->insert();
 
-    if (!$result) {
+    if (empty($result)) {
         common_log(LOG_WARNING,
             "Twitter bridge - failed to add new Twitter user: $twitter_id - $screen_name.");
         common_log_db_error($fuser, 'INSERT', __FILE__);
-        if (defined('SCRIPT_DEBUG')) {
-            print "INSERT failed: could not add new Twitter user: $twitter_id - $screen_name. - ";
-            print common_log_objstring($fuser) . "\n";
-            $error = &PEAR::getStaticProperty('DB_DataObject','lastError');
-            print "DB_DataObject Error: " . $error->getMessage() . "\n";
-        }
     } else {
         common_debug("Twitter bridge - Added new Twitter user: $screen_name ($twitter_id).");
-        if (defined('SCRIPT_DEBUG')) {
-            print "Added new Twitter user: $screen_name ($twitter_id).\n";
-        }
     }
 
     return $result;
@@ -199,23 +105,20 @@ function save_twitter_user($twitter_id, $screen_name)
 
     // Check to see whether the Twitter user is already in the system,
     // and update its screen name and uri if so.
+
     $fuser = Foreign_user::getForeignUser($twitter_id, TWITTER_SERVICE);
 
-    if ($fuser) {
+    if (!empty($fuser)) {
 
         $result = true;
 
         // Only update if Twitter screen name has changed
+
         if ($fuser->nickname != $screen_name) {
             $result = update_twitter_user($twitter_id, $screen_name);
 
             common_debug('Twitter bridge - Updated nickname (and URI) for Twitter user ' .
                 "$fuser->id to $screen_name, was $fuser->nickname");
-
-            if (defined('SCRIPT_DEBUG')) {
-                print 'Updated nickname (and URI) for Twitter user ' .
-                    "$fuser->id to $screen_name, was $fuser->nickname\n";
-            }
         }
 
         return $result;
@@ -230,119 +133,6 @@ function save_twitter_user($twitter_id, $screen_name)
     return true;
 }
 
-function retreive_twitter_friends($twitter_id, $screen_name, $password)
-{
-    $friends = array();
-
-    $uri = "http://twitter.com/statuses/friends/$twitter_id.json?page=";
-    $friends_ids = twitter_friends_ids($screen_name, $password);
-
-    if (!$friends_ids) {
-        return $friends;
-    }
-
-    if (defined('SCRIPT_DEBUG')) {
-        print "Twitter 'social graph' ids method says $screen_name has " .
-            count($friends_ids) . " friends.\n";
-    }
-
-    // Calculate how many pages to get...
-    $pages = ceil(count($friends_ids) / 100);
-
-    if ($pages == 0) {
-        common_log(LOG_WARNING,
-            "Twitter bridge - $screen_name seems to have no friends.");
-        if (defined('SCRIPT_DEBUG')) {
-            print "$screen_name seems to have no friends.\n";
-        }
-    }
-
-    for ($i = 1; $i <= $pages; $i++) {
-
-        $data = get_twitter_data($uri . $i, $screen_name, $password);
-
-        if (!$data) {
-            common_log(LOG_WARNING,
-                "Twitter bridge - Couldn't retrieve page $i of $screen_name's friends.");
-            if (defined('SCRIPT_DEBUG')) {
-                print "Couldn't retrieve page $i of $screen_name's friends.\n";
-            }
-            continue;
-        }
-
-        $more_friends = json_decode($data);
-
-        if (!$more_friends) {
-
-            common_log(LOG_WARNING,
-                "Twitter bridge - No data for page $i of $screen_name's friends.");
-            if (defined('SCRIPT_DEBUG')) {
-                print "No data for page $i of $screen_name's friends.\n";
-            }
-            continue;
-        }
-
-         $friends = array_merge($friends, $more_friends);
-    }
-
-    return $friends;
-}
-
-function save_twitter_friends($user, $twitter_id, $screen_name, $password)
-{
-
-    $friends = retreive_twitter_friends($twitter_id, $screen_name, $password);
-
-    if (empty($friends)) {
-        common_debug("Twitter bridge - Couldn't get friends data from Twitter for $screen_name.");
-        if (defined('SCRIPT_DEBUG')) {
-            print "Couldn't get friends data from Twitter for $screen_name.\n";
-        }
-        return false;
-    }
-
-    foreach ($friends as $friend) {
-
-        $friend_name = $friend->screen_name;
-        $friend_id = (int) $friend->id;
-
-        // Update or create the Foreign_user record
-        if (!save_twitter_user($friend_id, $friend_name)) {
-            common_log(LOG_WARNING,
-                "Twitter bridge - couldn't save $screen_name's friend, $friend_name.");
-            if (defined('SCRIPT_DEBUG')) {
-                print "Couldn't save $screen_name's friend, $friend_name.\n";
-            }
-            continue;
-        }
-
-        // Check to see if there's a related local user
-        $flink = Foreign_link::getByForeignID($friend_id, 1);
-
-        if ($flink) {
-
-            // Get associated user and subscribe her
-            $friend_user = User::staticGet('id', $flink->user_id);
-            if (!empty($friend_user)) {
-                $result = subs_subscribe_to($user, $friend_user);
-
-                if ($result === true) {
-                    common_debug("Twitter bridge - subscribed $friend_user->nickname to $user->nickname.");
-                    if (defined('SCRIPT_DEBUG')) {
-                        print("Subscribed $friend_user->nickname to $user->nickname.\n");
-                    }
-                } else {
-                    if (defined('SCRIPT_DEBUG')) {
-                        print "$result ($friend_user->nickname to $user->nickname)\n";
-                    }
-                }
-            }
-        }
-    }
-
-    return true;
-}
-
 function is_twitter_bound($notice, $flink) {
 
     // Check to see if notice should go to Twitter
@@ -351,7 +141,7 @@ function is_twitter_bound($notice, $flink) {
         // If it's not a Twitter-style reply, or if the user WANTS to send replies.
         if (!preg_match('/^@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) ||
             ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) {
-                return true;
+            return true;
         }
     }
 
@@ -360,104 +150,73 @@ function is_twitter_bound($notice, $flink) {
 
 function broadcast_twitter($notice)
 {
-
     $flink = Foreign_link::getByUserID($notice->profile_id,
-        TWITTER_SERVICE);
+                                       TWITTER_SERVICE);
 
     if (is_twitter_bound($notice, $flink)) {
 
-        $fuser = $flink->getForeignUser();
-        $twitter_user = $fuser->nickname;
-        $twitter_password = $flink->credentials;
-        $uri = 'http://www.twitter.com/statuses/update.json';
+        $user = $flink->getUser();
 
         // XXX: Hack to get around PHP cURL's use of @ being a a meta character
         $statustxt = preg_replace('/^@/', ' @', $notice->content);
 
-        $options = array(
-            CURLOPT_USERPWD        => "$twitter_user:$twitter_password",
-            CURLOPT_POST           => true,
-            CURLOPT_POSTFIELDS     =>
-                array(
-                        'status' => $statustxt,
-                        'source' => common_config('integration', 'source')
-                     ),
-            CURLOPT_RETURNTRANSFER => true,
-            CURLOPT_FAILONERROR    => true,
-            CURLOPT_HEADER         => false,
-            CURLOPT_FOLLOWLOCATION => true,
-            CURLOPT_USERAGENT      => "Laconica",
-            CURLOPT_CONNECTTIMEOUT => 120,  // XXX: How long should this be?
-            CURLOPT_TIMEOUT        => 120,
-
-            # Twitter is strict about accepting invalid "Expect" headers
-            CURLOPT_HTTPHEADER => array('Expect:')
-            );
-
-        $ch = curl_init($uri);
-        curl_setopt_array($ch, $options);
-        $data = curl_exec($ch);
-        $errmsg = curl_error($ch);
-        $errno = curl_errno($ch);
-
-        if (!empty($errmsg)) {
-            common_debug("cURL error ($errno): $errmsg - " .
-                "trying to send notice for $twitter_user.",
-                         __FILE__);
-
-            $user = $flink->getUser();
-
-            if ($errmsg == 'The requested URL returned error: 401') {
-                common_debug(sprintf('User %s (user id: %s) ' .
-                    'has bad Twitter credentials!',
-                    $user->nickname, $user->id));
-
-                    // Bad credentials we need to delete the foreign_link
-                    // to Twitter and inform the user.
-
-                    remove_twitter_link($flink);
-
-                    return true;
+        $token = TwitterOAuthClient::unpackToken($flink->credentials);
 
-            } else {
+        $client = new TwitterOAuthClient($token->key, $token->secret);
 
-                // Some other error happened, so we should try to
-                // send again later
+        $status = null;
 
-                return false;
-            }
+        try {
+            $status = $client->statusesUpdate($statustxt);
+        } catch (OAuthClientCurlException $e) {
 
-        }
-
-        curl_close($ch);
+            if ($e->getMessage() == 'The requested URL returned error: 401') {
 
-        if (empty($data)) {
-            common_debug("No data returned by Twitter's " .
-                "API trying to send update for $twitter_user",
-                         __FILE__);
+                $errmsg = sprintf('User %1$s (user id: %2$s) has an invalid ' .
+                                  'Twitter OAuth access token.',
+                                  $user->nickname, $user->id);
+                common_log(LOG_WARNING, $errmsg);
 
-            // XXX: Not sure this represents a failure to send, but it
-            // probably does
+                // Bad auth token! We need to delete the foreign_link
+                // to Twitter and inform the user.
 
-            return false;
-
-        } else {
+                remove_twitter_link($flink);
+                return true;
 
-            // Twitter should return a status
-            $status = json_decode($data);
+            } else {
 
-            if (empty($status)) {
-                common_debug("Unexpected data returned by Twitter " .
-                    " API trying to send update for $twitter_user",
-                        __FILE__);
+                // Some other error happened, so we should probably
+                // try to send again later.
 
-                // XXX: Again, this could represent a failure posting
-                // or the Twitter API might just be behaving flakey.
-                // We're treating it as a failure to post.
+                $errmsg = sprintf('cURL error trying to send notice to Twitter ' .
+                                  'for user %1$s (user id: %2$s) - ' .
+                                  'code: %3$s message: $4$s.',
+                                  $user->nickname, $user->id,
+                                  $e->getCode(), $e->getMessage());
+                common_log(LOG_WARNING, $errmsg);
 
                 return false;
             }
         }
+
+        if (empty($status)) {
+
+            // This could represent a failure posting,
+            // or the Twitter API might just be behaving flakey.
+
+            $errmsg = sprint('No data returned by Twitter API when ' .
+                             'trying to send update for %1$s (user id %2$s).',
+                             $user->nickname, $user->id);
+            common_log(LOG_WARNING, $errmsg);
+
+            return false;
+        }
+
+        // Notice crossed the great divide
+
+        $msg = sprintf('Twitter bridge posted notice %s to Twitter.',
+                       $notice->id);
+        common_log(LOG_INFO, $msg);
     }
 
     return true;
@@ -474,22 +233,25 @@ function remove_twitter_link($flink)
 
     if (empty($result)) {
         common_log(LOG_ERR, 'Could not remove Twitter bridge ' .
-            "Foreign_link for $user->nickname (user id: $user->id)!");
+                   "Foreign_link for $user->nickname (user id: $user->id)!");
         common_log_db_error($flink, 'DELETE', __FILE__);
     }
 
     // Notify the user that her Twitter bridge is down
 
-    $result = mail_twitter_bridge_removed($user);
+    if (isset($user->email)) {
+
+        $result = mail_twitter_bridge_removed($user);
 
-    if (!$result) {
+        if (!$result) {
 
-        $msg = 'Unable to send email to notify ' .
-            "$user->nickname (user id: $user->id) " .
-            'that their Twitter bridge link was ' .
-            'removed!';
+            $msg = 'Unable to send email to notify ' .
+              "$user->nickname (user id: $user->id) " .
+              'that their Twitter bridge link was ' .
+              'removed!';
 
-        common_log(LOG_WARNING, $msg);
+            common_log(LOG_WARNING, $msg);
+        }
     }
 
 }
index 4115d9dcb48d7e908d2a679887c602b908476fac..58300720865ced1b66ee77f2795a3f6c449ea862 100644 (file)
@@ -188,20 +188,20 @@ class TwitterapiAction extends Action
 
         // Enclosures
         $attachments = $notice->attachments();
-        $enclosures = array();
 
-        foreach ($attachments as $attachment) {
-            if ($attachment->isEnclosure()) {
-                 $enclosure = array();
-                 $enclosure['url'] = $attachment->url;
-                 $enclosure['mimetype'] = $attachment->mimetype;
-                 $enclosure['size'] = $attachment->size;
-                 $enclosures[] = $enclosure;
-            }
-        }
+        if (!empty($attachments)) {
 
-        if (!empty($enclosures)) {
-            $twitter_status['attachments'] = $enclosures;
+            $twitter_status['attachments'] = array();
+
+            foreach ($attachments as $attachment) {
+                if ($attachment->isEnclosure()) {
+                    $enclosure = array();
+                    $enclosure['url'] = $attachment->url;
+                    $enclosure['mimetype'] = $attachment->mimetype;
+                    $enclosure['size'] = $attachment->size;
+                    $twitter_status['attachments'][] = $enclosure;
+                }
+            }
         }
 
         if ($include_user) {
@@ -233,6 +233,24 @@ class TwitterapiAction extends Action
         return $twitter_group;
     }
 
+    function twitter_rss_group_array($group)
+    {
+        $entry = array();
+        $entry['content']=$group->description;
+        $entry['title']=$group->nickname;
+        $entry['link']=$group->permalink();
+        $entry['published']=common_date_iso8601($group->created);
+        $entry['updated']==common_date_iso8601($group->modified);
+        $taguribase = common_config('integration', 'groupuri');
+        $entry['id'] = "group:$groupuribase:$entry[link]";
+
+        $entry['description'] = $entry['content'];
+        $entry['pubDate'] = common_date_rfc2822($group->created);
+        $entry['guid'] = $entry['link'];
+
+        return $entry;
+    }
+
     function twitter_rss_entry_array($notice)
     {
         $profile = $notice->getProfile();
@@ -644,6 +662,65 @@ class TwitterapiAction extends Action
 
     }
 
+    function show_rss_groups($group, $title, $link, $subtitle)
+    {
+
+        $this->init_document('rss');
+
+        $this->elementStart('channel');
+        $this->element('title', null, $title);
+        $this->element('link', null, $link);
+        $this->element('description', null, $subtitle);
+        $this->element('language', null, 'en-us');
+        $this->element('ttl', null, '40');
+
+        if (is_array($group)) {
+            foreach ($group as $g) {
+                $twitter_group = $this->twitter_rss_group_array($g);
+                $this->show_twitter_rss_item($twitter_group);
+            }
+        } else {
+            while ($group->fetch()) {
+                $twitter_group = $this->twitter_rss_group_array($group);
+                $this->show_twitter_rss_item($twitter_group);
+            }
+        }
+
+        $this->elementEnd('channel');
+        $this->end_twitter_rss();
+    }
+
+    function show_atom_groups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
+    {
+
+        $this->init_document('atom');
+
+        $this->element('title', null, $title);
+        $this->element('id', null, $id);
+        $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
+
+        if (!is_null($selfuri)) {
+            $this->element('link', array('href' => $selfuri,
+                'rel' => 'self', 'type' => 'application/atom+xml'), null);
+        }
+
+        $this->element('updated', null, common_date_iso8601('now'));
+        $this->element('subtitle', null, $subtitle);
+
+        if (is_array($group)) {
+            foreach ($group as $g) {
+                $this->raw($g->asAtomEntry());
+            }
+        } else {
+            while ($group->fetch()) {
+                $this->raw($group->asAtomEntry());
+            }
+        }
+
+        $this->end_document('atom');
+
+    }
+
     function show_json_timeline($notice)
     {
 
@@ -668,6 +745,52 @@ class TwitterapiAction extends Action
         $this->end_document('json');
     }
 
+    function show_json_groups($group)
+    {
+
+        $this->init_document('json');
+
+        $groups = array();
+
+        if (is_array($group)) {
+            foreach ($group as $g) {
+                $twitter_group = $this->twitter_group_array($g);
+                array_push($groups, $twitter_group);
+            }
+        } else {
+            while ($group->fetch()) {
+                $twitter_group = $this->twitter_group_array($group);
+                array_push($groups, $twitter_group);
+            }
+        }
+
+        $this->show_json_objects($groups);
+
+        $this->end_document('json');
+    }
+
+    function show_xml_groups($group)
+    {
+
+        $this->init_document('xml');
+        $this->elementStart('groups', array('type' => 'array'));
+
+        if (is_array($group)) {
+            foreach ($group as $g) {
+                $twitter_group = $this->twitter_group_array($g);
+                $this->show_twitter_xml_group($twitter_group);
+            }
+        } else {
+            while ($group->fetch()) {
+                $twitter_group = $this->twitter_group_array($group);
+                $this->show_twitter_xml_group($twitter_group);
+            }
+        }
+
+        $this->elementEnd('groups');
+        $this->end_document('xml');
+    }
+
     function show_single_json_group($group)
     {
         $this->init_document('json');
@@ -844,9 +967,9 @@ class TwitterapiAction extends Action
         $this->endXML();
     }
 
-    function show_profile($profile, $content_type='xml', $notice=null)
+    function show_profile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
     {
-        $profile_array = $this->twitter_user_array($profile, true);
+        $profile_array = $this->twitter_user_array($profile, $includeStatuses);
         switch ($content_type) {
         case 'xml':
             $this->show_twitter_xml_user($profile_array);
diff --git a/lib/twitteroauthclient.php b/lib/twitteroauthclient.php
new file mode 100644 (file)
index 0000000..b7dc4a8
--- /dev/null
@@ -0,0 +1,220 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Class for doing OAuth calls against Twitter
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Integration
+ * @package   Laconica
+ * @author    Zach Copley <zach@controlyourself.ca>
+ * @copyright 2008 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/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+/**
+ * Class for talking to the Twitter API with OAuth.
+ *
+ * @category Integration
+ * @package  Laconica
+ * @author   Zach Copley <zach@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ *
+ */
+class TwitterOAuthClient extends OAuthClient
+{
+    public static $requestTokenURL = 'https://twitter.com/oauth/request_token';
+    public static $authorizeURL    = 'https://twitter.com/oauth/authorize';
+    public static $accessTokenURL  = 'https://twitter.com/oauth/access_token';
+
+    /**
+     * Constructor
+     *
+     * @param string $oauth_token        the user's token
+     * @param string $oauth_token_secret the user's token secret
+     *
+     * @return nothing
+     */
+    function __construct($oauth_token = null, $oauth_token_secret = null)
+    {
+        $consumer_key    = common_config('twitter', 'consumer_key');
+        $consumer_secret = common_config('twitter', 'consumer_secret');
+
+        parent::__construct($consumer_key, $consumer_secret,
+                            $oauth_token, $oauth_token_secret);
+    }
+
+    // XXX: the following two functions are to support the horrible hack
+    // of using the credentils field in Foreign_link to store both
+    // the access token and token secret.  This hack should go away with
+    // 0.9, in which we can make DB changes and add a new column for the
+    // token itself.
+
+    static function packToken($token)
+    {
+        return implode(chr(0), array($token->key, $token->secret));
+    }
+
+    static function unpackToken($str)
+    {
+        $vals = explode(chr(0), $str);
+        return new OAuthToken($vals[0], $vals[1]);
+    }
+
+    /**
+     * Builds a link to Twitter's endpoint for authorizing a request token
+     *
+     * @param OAuthToken $request_token token to authorize
+     *
+     * @return the link
+     */
+    function getAuthorizeLink($request_token)
+    {
+        return parent::getAuthorizeLink(self::$authorizeURL,
+                                        $request_token,
+                                        common_local_url('twitterauthorization'));
+    }
+
+    /**
+     * Calls Twitter's /account/verify_credentials API method
+     *
+     * @return mixed the Twitter user
+     */
+    function verifyCredentials()
+    {
+        $url          = 'https://twitter.com/account/verify_credentials.json';
+        $response     = $this->oAuthGet($url);
+        $twitter_user = json_decode($response);
+        return $twitter_user;
+    }
+
+    /**
+     * Calls Twitter's /stutuses/update API method
+     *
+     * @param string $status                text of the status
+     * @param int    $in_reply_to_status_id optional id of the status it's
+     *                                      a reply to
+     *
+     * @return mixed the status
+     */
+    function statusesUpdate($status, $in_reply_to_status_id = null)
+    {
+        $url      = 'https://twitter.com/statuses/update.json';
+        $params   = array('status' => $status,
+            'in_reply_to_status_id' => $in_reply_to_status_id);
+        $response = $this->oAuthPost($url, $params);
+        $status   = json_decode($response);
+        return $status;
+    }
+
+    /**
+     * Calls Twitter's /stutuses/friends_timeline API method
+     *
+     * @param int $since_id show statuses after this id
+     * @param int $max_id   show statuses before this id
+     * @param int $cnt      number of statuses to show
+     * @param int $page     page number
+     *
+     * @return mixed an array of statuses
+     */
+    function statusesFriendsTimeline($since_id = null, $max_id = null,
+                                     $cnt = null, $page = null)
+    {
+
+        $url    = 'https://twitter.com/statuses/friends_timeline.json';
+        $params = array('since_id' => $since_id,
+                        'max_id' => $max_id,
+                        'count' => $cnt,
+                        'page' => $page);
+        $qry    = http_build_query($params);
+
+        if (!empty($qry)) {
+            $url .= "?$qry";
+        }
+
+        $response = $this->oAuthGet($url);
+        $statuses = json_decode($response);
+        return $statuses;
+    }
+
+    /**
+     * Calls Twitter's /stutuses/friends API method
+     *
+     * @param int $id          id of the user whom you wish to see friends of
+     * @param int $user_id     numerical user id
+     * @param int $screen_name screen name
+     * @param int $page        page number
+     *
+     * @return mixed an array of twitter users and their latest status
+     */
+    function statusesFriends($id = null, $user_id = null, $screen_name = null,
+                             $page = null)
+    {
+        $url = "https://twitter.com/statuses/friends.json";
+
+        $params = array('id' => $id,
+                        'user_id' => $user_id,
+                        'screen_name' => $screen_name,
+                        'page' => $page);
+        $qry    = http_build_query($params);
+
+        if (!empty($qry)) {
+            $url .= "?$qry";
+        }
+
+        $response = $this->oAuthGet($url);
+        $friends  = json_decode($response);
+        return $friends;
+    }
+
+    /**
+     * Calls Twitter's /stutuses/friends/ids API method
+     *
+     * @param int $id          id of the user whom you wish to see friends of
+     * @param int $user_id     numerical user id
+     * @param int $screen_name screen name
+     * @param int $page        page number
+     *
+     * @return mixed a list of ids, 100 per page
+     */
+    function friendsIds($id = null, $user_id = null, $screen_name = null,
+                         $page = null)
+    {
+        $url = "https://twitter.com/friends/ids.json";
+
+        $params = array('id' => $id,
+                        'user_id' => $user_id,
+                        'screen_name' => $screen_name,
+                        'page' => $page);
+        $qry    = http_build_query($params);
+
+        if (!empty($qry)) {
+            $url .= "?$qry";
+        }
+
+        $response = $this->oAuthGet($url);
+        $ids      = json_decode($response);
+        return $ids;
+    }
+
+}
index 51546107255730b7cf7a661ef8cbfb59c3b5152c..920313c0c0c25479c766271039f8e43a3532f19b 100644 (file)
@@ -39,7 +39,7 @@ class UnQueueManager
          case 'omb':
             if ($this->_isLocal($notice)) {
                 require_once(INSTALLDIR.'/lib/omb.php');
-                omb_broadcast_remote_subscribers($notice);
+                omb_broadcast_notice($notice);
             }
             break;
          case 'public':
@@ -79,7 +79,7 @@ class UnQueueManager
 
     function _isLocal($notice)
     {
-        return ($notice->is_local == NOTICE_LOCAL_PUBLIC ||
-                $notice->is_local == NOTICE_LOCAL_NONPUBLIC);
+        return ($notice->is_local == Notice::LOCAL_PUBLIC ||
+                $notice->is_local == Notice::LOCAL_NONPUBLIC);
     }
 }
\ No newline at end of file
index 1f9dd429c7948b6bd009696027a81c2979b52795..b798cfe1573cdf8e2acb39ec116d8bf51ff001fc 100644 (file)
@@ -412,73 +412,43 @@ function common_render_text($text)
 function common_replace_urls_callback($text, $callback, $notice_id = null) {
     // Start off with a regex
     $regex = '#'.
-    '(?:'.
+    '(?:^|\s+)('.
         '(?:'.
             '(?:https?|ftps?|mms|rtsp|gopher|news|nntp|telnet|wais|file|prospero|webcal|irc)://'.
             '|'.
             '(?:mailto|aim|tel|xmpp):'.
+        ')?'.
+        '(?:'.
+        '(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)'. //IPv4
+        '|(?:'.
+            '(?:[0-9a-f]{1,4}:){1,1}(?::[0-9a-f]{1,4}){1,6}|'. //IPv6
+            '(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,5}|'.
+            '(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,4}|'.
+            '(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,3}|'.
+            '(?:[0-9a-f]{1,4}:){1,5}(?::[0-9a-f]{1,4}){1,2}|'.
+            '(?:[0-9a-f]{1,4}:){1,6}(?::[0-9a-f]{1,4}){1,1}|'.
+            '(?:(?:[0-9a-f]{1,4}:){1,7}|:):|'.
+            ':(?::[0-9a-f]{1,4}){1,7}|'.
+            '(?:(?:(?:[0-9a-f]{1,4}:){6})(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})|'.
+            '(?:(?:[0-9a-f]{1,4}:){5}[0-9a-f]{1,4}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})|'.
+            '(?:[0-9a-f]{1,4}:){5}:[0-9a-f]{1,4}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
+            '(?:[0-9a-f]{1,4}:){1,1}(?::[0-9a-f]{1,4}){1,4}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
+            '(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,3}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
+            '(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,2}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
+            '(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,1}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
+            '(?:(?:[0-9a-f]{1,4}:){1,5}|:):(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
+            ':(?::[0-9a-f]{1,4}){1,5}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'.
+        ')|'.
+        '(?:[^.\s/:]+\.)+'. //DNS
+        '(?:museum|travel|onion|[a-z]{2,4})'.
         ')'.
-        '[^.\s]+\.[^\s]+'.
-        '|'.
-        '(?:[^.\s/:]+\.)+'.
-        '(?:museum|travel|[a-z]{2,4})'.
         '(?:[:/][^\s]*)?'.
     ')'.
     '#ix';
     preg_match_all($regex, $text, $matches);
-
     // Then clean up what the regex left behind
     $offset = 0;
-    foreach($matches[0] as $orig_url) {
-        $url = htmlspecialchars_decode($orig_url);
-
-        // Make sure we didn't pick up an email address
-        if (preg_match('#^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$#i', $url)) continue;
-
-        // Remove surrounding punctuation
-        $url = trim($url, '.?!,;:\'"`([<');
-
-        // Remove surrounding parens and the like
-        preg_match('/[)\]>]+$/', $url, $trailing);
-        if (isset($trailing[0])) {
-            preg_match_all('/[(\[<]/', $url, $opened);
-            preg_match_all('/[)\]>]/', $url, $closed);
-            $unopened = count($closed[0]) - count($opened[0]);
-
-            // Make sure not to take off more closing parens than there are at the end
-            $unopened = ($unopened > mb_strlen($trailing[0])) ? mb_strlen($trailing[0]):$unopened;
-
-            $url = ($unopened > 0) ? mb_substr($url, 0, $unopened * -1):$url;
-        }
-
-        // Remove trailing punctuation again (in case there were some inside parens)
-        $url = rtrim($url, '.?!,;:\'"`');
-
-        // Make sure we didn't capture part of the next sentence
-        preg_match('#((?:[^.\s/]+\.)+)(museum|travel|[a-z]{2,4})#i', $url, $url_parts);
-
-        // Were the parts capitalized any?
-        $last_part = (mb_strtolower($url_parts[2]) !== $url_parts[2]) ? true:false;
-        $prev_part = (mb_strtolower($url_parts[1]) !== $url_parts[1]) ? true:false;
-
-        // If the first part wasn't cap'd but the last part was, we captured too much
-        if ((!$prev_part && $last_part)) {
-            $url = mb_substr($url, 0 , mb_strpos($url, '.'.$url_parts['2'], 0));
-        }
-
-        // Capture the new TLD
-        preg_match('#((?:[^.\s/]+\.)+)(museum|travel|[a-z]{2,4})#i', $url, $url_parts);
-
-        $tlds = array('ac', 'ad', 'ae', 'aero', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq', 'ar', 'arpa', 'as', 'asia', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh', 'bi', 'biz', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cat', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'com', 'coop', 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'edu', 'ee', 'eg', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gov', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'info', 'int', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo', 'jobs', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg', 'mh', 'mil', 'mk', 'ml', 'mm', 'mn', 'mo', 'mobi', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'museum', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'name', 'nc', 'ne', 'net', 'nf', 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'org', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'pro', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so', 'sr', 'st', 'su', 'sv', 'sy', 'sz', 'tc', 'td', 'tel', 'tf', 'tg', 'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tp', 'tr', 'travel', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'ye', 'yt', 'yu', 'za', 'zm', 'zw');
-
-        if (!in_array($url_parts[2], $tlds)) continue;
-
-        // Make sure we didn't capture a hash tag
-        if (strpos($url, '#') === 0) continue;
-
-        // Put the url back the way we found it.
-        $url = (mb_strpos($orig_url, htmlspecialchars($url)) === FALSE) ? $url:htmlspecialchars($url);
-
+    foreach($matches[1] as $url) {
         // Call user specified func
         if (empty($notice_id)) {
             $modified_url = call_user_func($callback, $url);
@@ -570,7 +540,8 @@ function common_linkify($url) {
 
 function common_shorten_links($text)
 {
-    if (mb_strlen($text) <= 140) return $text;
+    $maxLength = Notice::maxContent();
+    if ($maxLength == 0 || mb_strlen($text) <= $maxLength) return $text;
     return common_replace_urls_callback($text, array('File_redirection', 'makeShort'));
 }
 
@@ -893,8 +864,8 @@ function common_enqueue_notice($notice)
         $transports[] = 'jabber';
     }
 
-    if ($notice->is_local == NOTICE_LOCAL_PUBLIC ||
-        $notice->is_local == NOTICE_LOCAL_NONPUBLIC) {
+    if ($notice->is_local == Notice::LOCAL_PUBLIC ||
+        $notice->is_local == Notice::LOCAL_NONPUBLIC) {
         $transports = array_merge($transports, $localTransports);
         if ($xmpp) {
             $transports[] = 'public';
@@ -1151,7 +1122,8 @@ function common_negotiate_type($cprefs, $sprefs)
 function common_config($main, $sub)
 {
     global $config;
-    return isset($config[$main][$sub]) ? $config[$main][$sub] : false;
+    return (array_key_exists($main, $config) &&
+            array_key_exists($sub, $config[$main])) ? $config[$main][$sub] : false;
 }
 
 function common_copy_args($from)
diff --git a/lighttpd.conf.example b/lighttpd.conf.example
new file mode 100644 (file)
index 0000000..b8baafc
--- /dev/null
@@ -0,0 +1,2 @@
+# Add this line to lighttpd.conf to enable pseudo-rewrites using 404s
+server.error-handler-404 = "/index.php"
diff --git a/plugins/Autocomplete/Autocomplete.js b/plugins/Autocomplete/Autocomplete.js
new file mode 100644 (file)
index 0000000..e799c11
--- /dev/null
@@ -0,0 +1,38 @@
+$(document).ready(function(){
+    $.getJSON($('address .url')[0].href+'/api/statuses/friends.json?user_id=' + current_user['id'] + '&lite=true&callback=?',
+        function(friends){
+            $('#notice_data-text').autocomplete(friends, {
+                multiple: true,
+                multipleSeparator: " ",
+                minChars: 1,
+                formatItem: function(row, i, max){
+                    return '@' + row.screen_name + ' (' + row.name + ')';
+                },
+                formatMatch: function(row, i, max){
+                    return '@' + row.screen_name;
+                },
+                formatResult: function(row){
+                    return '@' + row.screen_name;
+                }
+            });
+        }
+    );
+    $.getJSON($('address .url')[0].href+'/api/laconica/groups/list.json?user_id=' + current_user['id'] + '&callback=?',
+        function(groups){
+            $('#notice_data-text').autocomplete(groups, {
+                multiple: true,
+                multipleSeparator: " ",
+                minChars: 1,
+                formatItem: function(row, i, max){
+                    return '!' + row.nickname + ' (' + row.fullname + ')';
+                },
+                formatMatch: function(row, i, max){
+                    return '!' + row.nickname;
+                },
+                formatResult: function(row){
+                    return '!' + row.nickname;
+                }
+            });
+        }
+    );
+});
diff --git a/plugins/Autocomplete/AutocompletePlugin.php b/plugins/Autocomplete/AutocompletePlugin.php
new file mode 100644 (file)
index 0000000..58b6a84
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Plugin to enable nickname completion in the enter status box
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Plugin
+ * @package   Laconica
+ * @author    Craig Andrews <candrews@integralblue.com>
+ * @copyright 2009 Craig Andrews http://candrews.integralblue.com
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+class AutocompletePlugin extends Plugin
+{
+    function __construct()
+    {
+        parent::__construct();
+    }
+
+    function onEndShowScripts($action){
+        if (common_logged_in()) {
+            $current_user = common_current_user();
+            $js_string = <<<EOT
+<script type="text/javascript">
+var current_user = { id: '$current_user->id' };
+</script>
+EOT;
+            $action->raw($js_string);
+            $action->script('plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.pack.js');
+            $action->script('plugins/Autocomplete/Autocomplete.js');
+        }
+    }
+
+    function onEndShowLaconicaStyles($action)
+    {
+        if (common_logged_in()) {
+            $action->cssLink('plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.css');
+        }
+    }
+
+}
+?>
diff --git a/plugins/Autocomplete/jquery-autocomplete/changelog.txt b/plugins/Autocomplete/jquery-autocomplete/changelog.txt
new file mode 100644 (file)
index 0000000..94cb5cc
--- /dev/null
@@ -0,0 +1,20 @@
+1.0.2
+-----
+* Fixed missing semicolon
+
+1.0.1
+-----
+* Fixed element creation (<ul> to <ul/> and <li> to </li>)
+* Fixed ac_even class (was ac_event)
+* Fixed bgiframe usage: now its really optional
+* Removed the blur-on-return workaround, added a less obtrusive one only for Opera
+* Fixed hold cursor keys: Opera needs keypress, everyone else keydown to scroll through result list when holding cursor key
+* Updated package to jQuery 1.2.5, removing dimensions
+* Fixed multiple-mustMatch: Remove only the last term when no match is found
+* Fixed multiple without mustMatch: Don't select the last active when no match is found (on tab/return)
+* Fixed multiple cursor position: Put cursor at end of input after selecting a value
+
+1.0
+---
+
+* First release.
\ No newline at end of file
diff --git a/plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.css b/plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.css
new file mode 100644 (file)
index 0000000..91b6228
--- /dev/null
@@ -0,0 +1,48 @@
+.ac_results {
+       padding: 0px;
+       border: 1px solid black;
+       background-color: white;
+       overflow: hidden;
+       z-index: 99999;
+}
+
+.ac_results ul {
+       width: 100%;
+       list-style-position: outside;
+       list-style: none;
+       padding: 0;
+       margin: 0;
+}
+
+.ac_results li {
+       margin: 0px;
+       padding: 2px 5px;
+       cursor: default;
+       display: block;
+       /* 
+       if width will be 100% horizontal scrollbar will apear 
+       when scroll mode will be used
+       */
+       /*width: 100%;*/
+       font: menu;
+       font-size: 12px;
+       /* 
+       it is very important, if line-height not setted or setted 
+       in relative units scroll will be broken in firefox
+       */
+       line-height: 16px;
+       overflow: hidden;
+}
+
+.ac_loading {
+       background: white url('indicator.gif') right center no-repeat;
+}
+
+.ac_odd {
+       background-color: #eee;
+}
+
+.ac_over {
+       background-color: #0A246A;
+       color: white;
+}
diff --git a/plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.js b/plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.js
new file mode 100644 (file)
index 0000000..5ad9178
--- /dev/null
@@ -0,0 +1,759 @@
+/*
+ * Autocomplete - jQuery plugin 1.0.2
+ *
+ * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
+ *
+ * Dual licensed under the MIT and GPL licenses:
+ *   http://www.opensource.org/licenses/mit-license.php
+ *   http://www.gnu.org/licenses/gpl.html
+ *
+ * Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $
+ *
+ */
+
+;(function($) {
+       
+$.fn.extend({
+       autocomplete: function(urlOrData, options) {
+               var isUrl = typeof urlOrData == "string";
+               options = $.extend({}, $.Autocompleter.defaults, {
+                       url: isUrl ? urlOrData : null,
+                       data: isUrl ? null : urlOrData,
+                       delay: isUrl ? $.Autocompleter.defaults.delay : 10,
+                       max: options && !options.scroll ? 10 : 150
+               }, options);
+               
+               // if highlight is set to false, replace it with a do-nothing function
+               options.highlight = options.highlight || function(value) { return value; };
+               
+               // if the formatMatch option is not specified, then use formatItem for backwards compatibility
+               options.formatMatch = options.formatMatch || options.formatItem;
+               
+               return this.each(function() {
+                       new $.Autocompleter(this, options);
+               });
+       },
+       result: function(handler) {
+               return this.bind("result", handler);
+       },
+       search: function(handler) {
+               return this.trigger("search", [handler]);
+       },
+       flushCache: function() {
+               return this.trigger("flushCache");
+       },
+       setOptions: function(options){
+               return this.trigger("setOptions", [options]);
+       },
+       unautocomplete: function() {
+               return this.trigger("unautocomplete");
+       }
+});
+
+$.Autocompleter = function(input, options) {
+
+       var KEY = {
+               UP: 38,
+               DOWN: 40,
+               DEL: 46,
+               TAB: 9,
+               RETURN: 13,
+               ESC: 27,
+               COMMA: 188,
+               PAGEUP: 33,
+               PAGEDOWN: 34,
+               BACKSPACE: 8
+       };
+
+       // Create $ object for input element
+       var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
+
+       var timeout;
+       var previousValue = "";
+       var cache = $.Autocompleter.Cache(options);
+       var hasFocus = 0;
+       var lastKeyPressCode;
+       var config = {
+               mouseDownOnSelect: false
+       };
+       var select = $.Autocompleter.Select(options, input, selectCurrent, config);
+       
+       var blockSubmit;
+       
+       // prevent form submit in opera when selecting with return key
+       $.browser.opera && $(input.form).bind("submit.autocomplete", function() {
+               if (blockSubmit) {
+                       blockSubmit = false;
+                       return false;
+               }
+       });
+       
+       // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
+       $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
+               // track last key pressed
+               lastKeyPressCode = event.keyCode;
+               switch(event.keyCode) {
+               
+                       case KEY.UP:
+                               event.preventDefault();
+                               if ( select.visible() ) {
+                                       select.prev();
+                               } else {
+                                       onChange(0, true);
+                               }
+                               break;
+                               
+                       case KEY.DOWN:
+                               event.preventDefault();
+                               if ( select.visible() ) {
+                                       select.next();
+                               } else {
+                                       onChange(0, true);
+                               }
+                               break;
+                               
+                       case KEY.PAGEUP:
+                               event.preventDefault();
+                               if ( select.visible() ) {
+                                       select.pageUp();
+                               } else {
+                                       onChange(0, true);
+                               }
+                               break;
+                               
+                       case KEY.PAGEDOWN:
+                               event.preventDefault();
+                               if ( select.visible() ) {
+                                       select.pageDown();
+                               } else {
+                                       onChange(0, true);
+                               }
+                               break;
+                       
+                       // matches also semicolon
+                       case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
+                       case KEY.TAB:
+                       case KEY.RETURN:
+                               if( selectCurrent() ) {
+                                       // stop default to prevent a form submit, Opera needs special handling
+                                       event.preventDefault();
+                                       blockSubmit = true;
+                                       return false;
+                               }
+                               break;
+                               
+                       case KEY.ESC:
+                               select.hide();
+                               break;
+                               
+                       default:
+                               clearTimeout(timeout);
+                               timeout = setTimeout(onChange, options.delay);
+                               break;
+               }
+       }).focus(function(){
+               // track whether the field has focus, we shouldn't process any
+               // results if the field no longer has focus
+               hasFocus++;
+       }).blur(function() {
+               hasFocus = 0;
+               if (!config.mouseDownOnSelect) {
+                       hideResults();
+               }
+       }).click(function() {
+               // show select when clicking in a focused field
+               if ( hasFocus++ > 1 && !select.visible() ) {
+                       onChange(0, true);
+               }
+       }).bind("search", function() {
+               // TODO why not just specifying both arguments?
+               var fn = (arguments.length > 1) ? arguments[1] : null;
+               function findValueCallback(q, data) {
+                       var result;
+                       if( data && data.length ) {
+                               for (var i=0; i < data.length; i++) {
+                                       if( data[i].result.toLowerCase() == q.toLowerCase() ) {
+                                               result = data[i];
+                                               break;
+                                       }
+                               }
+                       }
+                       if( typeof fn == "function" ) fn(result);
+                       else $input.trigger("result", result && [result.data, result.value]);
+               }
+               $.each(trimWords($input.val()), function(i, value) {
+                       request(value, findValueCallback, findValueCallback);
+               });
+       }).bind("flushCache", function() {
+               cache.flush();
+       }).bind("setOptions", function() {
+               $.extend(options, arguments[1]);
+               // if we've updated the data, repopulate
+               if ( "data" in arguments[1] )
+                       cache.populate();
+       }).bind("unautocomplete", function() {
+               select.unbind();
+               $input.unbind();
+               $(input.form).unbind(".autocomplete");
+       });
+       
+       
+       function selectCurrent() {
+               var selected = select.selected();
+               if( !selected )
+                       return false;
+               
+               var v = selected.result;
+               previousValue = v;
+               
+               if ( options.multiple ) {
+                       var words = trimWords($input.val());
+                       if ( words.length > 1 ) {
+                               v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
+                       }
+                       v += options.multipleSeparator;
+               }
+               
+               $input.val(v);
+               hideResultsNow();
+               $input.trigger("result", [selected.data, selected.value]);
+               return true;
+       }
+       
+       function onChange(crap, skipPrevCheck) {
+               if( lastKeyPressCode == KEY.DEL ) {
+                       select.hide();
+                       return;
+               }
+               
+               var currentValue = $input.val();
+               
+               if ( !skipPrevCheck && currentValue == previousValue )
+                       return;
+               
+               previousValue = currentValue;
+               
+               currentValue = lastWord(currentValue);
+               if ( currentValue.length >= options.minChars) {
+                       $input.addClass(options.loadingClass);
+                       if (!options.matchCase)
+                               currentValue = currentValue.toLowerCase();
+                       request(currentValue, receiveData, hideResultsNow);
+               } else {
+                       stopLoading();
+                       select.hide();
+               }
+       };
+       
+       function trimWords(value) {
+               if ( !value ) {
+                       return [""];
+               }
+               var words = value.split( options.multipleSeparator );
+               var result = [];
+               $.each(words, function(i, value) {
+                       if ( $.trim(value) )
+                               result[i] = $.trim(value);
+               });
+               return result;
+       }
+       
+       function lastWord(value) {
+               if ( !options.multiple )
+                       return value;
+               var words = trimWords(value);
+               return words[words.length - 1];
+       }
+       
+       // fills in the input box w/the first match (assumed to be the best match)
+       // q: the term entered
+       // sValue: the first matching result
+       function autoFill(q, sValue){
+               // autofill in the complete box w/the first match as long as the user hasn't entered in more data
+               // if the last user key pressed was backspace, don't autofill
+               if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
+                       // fill in the value (keep the case the user has typed)
+                       $input.val($input.val() + sValue.substring(lastWord(previousValue).length));
+                       // select the portion of the value not typed by the user (so the next character will erase)
+                       $.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
+               }
+       };
+
+       function hideResults() {
+               clearTimeout(timeout);
+               timeout = setTimeout(hideResultsNow, 200);
+       };
+
+       function hideResultsNow() {
+               var wasVisible = select.visible();
+               select.hide();
+               clearTimeout(timeout);
+               stopLoading();
+               if (options.mustMatch) {
+                       // call search and run callback
+                       $input.search(
+                               function (result){
+                                       // if no value found, clear the input box
+                                       if( !result ) {
+                                               if (options.multiple) {
+                                                       var words = trimWords($input.val()).slice(0, -1);
+                                                       $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
+                                               }
+                                               else
+                                                       $input.val( "" );
+                                       }
+                               }
+                       );
+               }
+               if (wasVisible)
+                       // position cursor at end of input field
+                       $.Autocompleter.Selection(input, input.value.length, input.value.length);
+       };
+
+       function receiveData(q, data) {
+               if ( data && data.length && hasFocus ) {
+                       stopLoading();
+                       select.display(data, q);
+                       autoFill(q, data[0].value);
+                       select.show();
+               } else {
+                       hideResultsNow();
+               }
+       };
+
+       function request(term, success, failure) {
+               if (!options.matchCase)
+                       term = term.toLowerCase();
+               var data = cache.load(term);
+               // recieve the cached data
+               if (data && data.length) {
+                       success(term, data);
+               // if an AJAX url has been supplied, try loading the data now
+               } else if( (typeof options.url == "string") && (options.url.length > 0) ){
+                       
+                       var extraParams = {
+                               timestamp: +new Date()
+                       };
+                       $.each(options.extraParams, function(key, param) {
+                               extraParams[key] = typeof param == "function" ? param() : param;
+                       });
+                       
+                       $.ajax({
+                               // try to leverage ajaxQueue plugin to abort previous requests
+                               mode: "abort",
+                               // limit abortion to this input
+                               port: "autocomplete" + input.name,
+                               dataType: options.dataType,
+                               url: options.url,
+                               data: $.extend({
+                                       q: lastWord(term),
+                                       limit: options.max
+                               }, extraParams),
+                               success: function(data) {
+                                       var parsed = options.parse && options.parse(data) || parse(data);
+                                       cache.add(term, parsed);
+                                       success(term, parsed);
+                               }
+                       });
+               } else {
+                       // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
+                       select.emptyList();
+                       failure(term);
+               }
+       };
+       
+       function parse(data) {
+               var parsed = [];
+               var rows = data.split("\n");
+               for (var i=0; i < rows.length; i++) {
+                       var row = $.trim(rows[i]);
+                       if (row) {
+                               row = row.split("|");
+                               parsed[parsed.length] = {
+                                       data: row,
+                                       value: row[0],
+                                       result: options.formatResult && options.formatResult(row, row[0]) || row[0]
+                               };
+                       }
+               }
+               return parsed;
+       };
+
+       function stopLoading() {
+               $input.removeClass(options.loadingClass);
+       };
+
+};
+
+$.Autocompleter.defaults = {
+       inputClass: "ac_input",
+       resultsClass: "ac_results",
+       loadingClass: "ac_loading",
+       minChars: 1,
+       delay: 400,
+       matchCase: false,
+       matchSubset: true,
+       matchContains: false,
+       cacheLength: 10,
+       max: 100,
+       mustMatch: false,
+       extraParams: {},
+       selectFirst: true,
+       formatItem: function(row) { return row[0]; },
+       formatMatch: null,
+       autoFill: false,
+       width: 0,
+       multiple: false,
+       multipleSeparator: ", ",
+       highlight: function(value, term) {
+               return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
+       },
+    scroll: true,
+    scrollHeight: 180
+};
+
+$.Autocompleter.Cache = function(options) {
+
+       var data = {};
+       var length = 0;
+       
+       function matchSubset(s, sub) {
+               if (!options.matchCase) 
+                       s = s.toLowerCase();
+               var i = s.indexOf(sub);
+               if (i == -1) return false;
+               return i == 0 || options.matchContains;
+       };
+       
+       function add(q, value) {
+               if (length > options.cacheLength){
+                       flush();
+               }
+               if (!data[q]){ 
+                       length++;
+               }
+               data[q] = value;
+       }
+       
+       function populate(){
+               if( !options.data ) return false;
+               // track the matches
+               var stMatchSets = {},
+                       nullData = 0;
+
+               // no url was specified, we need to adjust the cache length to make sure it fits the local data store
+               if( !options.url ) options.cacheLength = 1;
+               
+               // track all options for minChars = 0
+               stMatchSets[""] = [];
+               
+               // loop through the array and create a lookup structure
+               for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
+                       var rawValue = options.data[i];
+                       // if rawValue is a string, make an array otherwise just reference the array
+                       rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
+                       
+                       var value = options.formatMatch(rawValue, i+1, options.data.length);
+                       if ( value === false )
+                               continue;
+                               
+                       var firstChar = value.charAt(0).toLowerCase();
+                       // if no lookup array for this character exists, look it up now
+                       if( !stMatchSets[firstChar] ) 
+                               stMatchSets[firstChar] = [];
+
+                       // if the match is a string
+                       var row = {
+                               value: value,
+                               data: rawValue,
+                               result: options.formatResult && options.formatResult(rawValue) || value
+                       };
+                       
+                       // push the current match into the set list
+                       stMatchSets[firstChar].push(row);
+
+                       // keep track of minChars zero items
+                       if ( nullData++ < options.max ) {
+                               stMatchSets[""].push(row);
+                       }
+               };
+
+               // add the data items to the cache
+               $.each(stMatchSets, function(i, value) {
+                       // increase the cache size
+                       options.cacheLength++;
+                       // add to the cache
+                       add(i, value);
+               });
+       }
+       
+       // populate any existing data
+       setTimeout(populate, 25);
+       
+       function flush(){
+               data = {};
+               length = 0;
+       }
+       
+       return {
+               flush: flush,
+               add: add,
+               populate: populate,
+               load: function(q) {
+                       if (!options.cacheLength || !length)
+                               return null;
+                       /* 
+                        * if dealing w/local data and matchContains than we must make sure
+                        * to loop through all the data collections looking for matches
+                        */
+                       if( !options.url && options.matchContains ){
+                               // track all matches
+                               var csub = [];
+                               // loop through all the data grids for matches
+                               for( var k in data ){
+                                       // don't search through the stMatchSets[""] (minChars: 0) cache
+                                       // this prevents duplicates
+                                       if( k.length > 0 ){
+                                               var c = data[k];
+                                               $.each(c, function(i, x) {
+                                                       // if we've got a match, add it to the array
+                                                       if (matchSubset(x.value, q)) {
+                                                               csub.push(x);
+                                                       }
+                                               });
+                                       }
+                               }                               
+                               return csub;
+                       } else 
+                       // if the exact item exists, use it
+                       if (data[q]){
+                               return data[q];
+                       } else
+                       if (options.matchSubset) {
+                               for (var i = q.length - 1; i >= options.minChars; i--) {
+                                       var c = data[q.substr(0, i)];
+                                       if (c) {
+                                               var csub = [];
+                                               $.each(c, function(i, x) {
+                                                       if (matchSubset(x.value, q)) {
+                                                               csub[csub.length] = x;
+                                                       }
+                                               });
+                                               return csub;
+                                       }
+                               }
+                       }
+                       return null;
+               }
+       };
+};
+
+$.Autocompleter.Select = function (options, input, select, config) {
+       var CLASSES = {
+               ACTIVE: "ac_over"
+       };
+       
+       var listItems,
+               active = -1,
+               data,
+               term = "",
+               needsInit = true,
+               element,
+               list;
+       
+       // Create results
+       function init() {
+               if (!needsInit)
+                       return;
+               element = $("<div/>")
+               .hide()
+               .addClass(options.resultsClass)
+               .css("position", "absolute")
+               .appendTo(document.body);
+       
+               list = $("<ul/>").appendTo(element).mouseover( function(event) {
+                       if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
+                   active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
+                           $(target(event)).addClass(CLASSES.ACTIVE);            
+               }
+               }).click(function(event) {
+                       $(target(event)).addClass(CLASSES.ACTIVE);
+                       select();
+                       // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
+                       input.focus();
+                       return false;
+               }).mousedown(function() {
+                       config.mouseDownOnSelect = true;
+               }).mouseup(function() {
+                       config.mouseDownOnSelect = false;
+               });
+               
+               if( options.width > 0 )
+                       element.css("width", options.width);
+                       
+               needsInit = false;
+       } 
+       
+       function target(event) {
+               var element = event.target;
+               while(element && element.tagName != "LI")
+                       element = element.parentNode;
+               // more fun with IE, sometimes event.target is empty, just ignore it then
+               if(!element)
+                       return [];
+               return element;
+       }
+
+       function moveSelect(step) {
+               listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
+               movePosition(step);
+        var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
+        if(options.scroll) {
+            var offset = 0;
+            listItems.slice(0, active).each(function() {
+                               offset += this.offsetHeight;
+                       });
+            if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
+                list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
+            } else if(offset < list.scrollTop()) {
+                list.scrollTop(offset);
+            }
+        }
+       };
+       
+       function movePosition(step) {
+               active += step;
+               if (active < 0) {
+                       active = listItems.size() - 1;
+               } else if (active >= listItems.size()) {
+                       active = 0;
+               }
+       }
+       
+       function limitNumberOfItems(available) {
+               return options.max && options.max < available
+                       ? options.max
+                       : available;
+       }
+       
+       function fillList() {
+               list.empty();
+               var max = limitNumberOfItems(data.length);
+               for (var i=0; i < max; i++) {
+                       if (!data[i])
+                               continue;
+                       var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
+                       if ( formatted === false )
+                               continue;
+                       var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
+                       $.data(li, "ac_data", data[i]);
+               }
+               listItems = list.find("li");
+               if ( options.selectFirst ) {
+                       listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
+                       active = 0;
+               }
+               // apply bgiframe if available
+               if ( $.fn.bgiframe )
+                       list.bgiframe();
+       }
+       
+       return {
+               display: function(d, q) {
+                       init();
+                       data = d;
+                       term = q;
+                       fillList();
+               },
+               next: function() {
+                       moveSelect(1);
+               },
+               prev: function() {
+                       moveSelect(-1);
+               },
+               pageUp: function() {
+                       if (active != 0 && active - 8 < 0) {
+                               moveSelect( -active );
+                       } else {
+                               moveSelect(-8);
+                       }
+               },
+               pageDown: function() {
+                       if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
+                               moveSelect( listItems.size() - 1 - active );
+                       } else {
+                               moveSelect(8);
+                       }
+               },
+               hide: function() {
+                       element && element.hide();
+                       listItems && listItems.removeClass(CLASSES.ACTIVE);
+                       active = -1;
+               },
+               visible : function() {
+                       return element && element.is(":visible");
+               },
+               current: function() {
+                       return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
+               },
+               show: function() {
+                       var offset = $(input).offset();
+                       element.css({
+                               width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
+                               top: offset.top + input.offsetHeight,
+                               left: offset.left
+                       }).show();
+            if(options.scroll) {
+                list.scrollTop(0);
+                list.css({
+                                       maxHeight: options.scrollHeight,
+                                       overflow: 'auto'
+                               });
+                               
+                if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
+                                       var listHeight = 0;
+                                       listItems.each(function() {
+                                               listHeight += this.offsetHeight;
+                                       });
+                                       var scrollbarsVisible = listHeight > options.scrollHeight;
+                    list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
+                                       if (!scrollbarsVisible) {
+                                               // IE doesn't recalculate width when scrollbar disappears
+                                               listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
+                                       }
+                }
+                
+            }
+               },
+               selected: function() {
+                       var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
+                       return selected && selected.length && $.data(selected[0], "ac_data");
+               },
+               emptyList: function (){
+                       list && list.empty();
+               },
+               unbind: function() {
+                       element && element.remove();
+               }
+       };
+};
+
+$.Autocompleter.Selection = function(field, start, end) {
+       if( field.createTextRange ){
+               var selRange = field.createTextRange();
+               selRange.collapse(true);
+               selRange.moveStart("character", start);
+               selRange.moveEnd("character", end);
+               selRange.select();
+       } else if( field.setSelectionRange ){
+               field.setSelectionRange(start, end);
+       } else {
+               if( field.selectionStart ){
+                       field.selectionStart = start;
+                       field.selectionEnd = end;
+               }
+       }
+       field.focus();
+};
+
+})(jQuery);
\ No newline at end of file
diff --git a/plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.min.js b/plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.min.js
new file mode 100644 (file)
index 0000000..c9ddfb2
--- /dev/null
@@ -0,0 +1,15 @@
+/*
+ * Autocomplete - jQuery plugin 1.0.2
+ *
+ * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
+ *
+ * Dual licensed under the MIT and GPL licenses:
+ *   http://www.opensource.org/licenses/mit-license.php
+ *   http://www.gnu.org/licenses/gpl.html
+ *
+ * Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $
+ *
+ */;(function($){$.fn.extend({autocomplete:function(urlOrData,options){var isUrl=typeof urlOrData=="string";options=$.extend({},$.Autocompleter.defaults,{url:isUrl?urlOrData:null,data:isUrl?null:urlOrData,delay:isUrl?$.Autocompleter.defaults.delay:10,max:options&&!options.scroll?10:150},options);options.highlight=options.highlight||function(value){return value;};options.formatMatch=options.formatMatch||options.formatItem;return this.each(function(){new $.Autocompleter(this,options);});},result:function(handler){return this.bind("result",handler);},search:function(handler){return this.trigger("search",[handler]);},flushCache:function(){return this.trigger("flushCache");},setOptions:function(options){return this.trigger("setOptions",[options]);},unautocomplete:function(){return this.trigger("unautocomplete");}});$.Autocompleter=function(input,options){var KEY={UP:38,DOWN:40,DEL:46,TAB:9,RETURN:13,ESC:27,COMMA:188,PAGEUP:33,PAGEDOWN:34,BACKSPACE:8};var $input=$(input).attr("autocomplete","off").addClass(options.inputClass);var timeout;var previousValue="";var cache=$.Autocompleter.Cache(options);var hasFocus=0;var lastKeyPressCode;var config={mouseDownOnSelect:false};var select=$.Autocompleter.Select(options,input,selectCurrent,config);var blockSubmit;$.browser.opera&&$(input.form).bind("submit.autocomplete",function(){if(blockSubmit){blockSubmit=false;return false;}});$input.bind(($.browser.opera?"keypress":"keydown")+".autocomplete",function(event){lastKeyPressCode=event.keyCode;switch(event.keyCode){case KEY.UP:event.preventDefault();if(select.visible()){select.prev();}else{onChange(0,true);}break;case KEY.DOWN:event.preventDefault();if(select.visible()){select.next();}else{onChange(0,true);}break;case KEY.PAGEUP:event.preventDefault();if(select.visible()){select.pageUp();}else{onChange(0,true);}break;case KEY.PAGEDOWN:event.preventDefault();if(select.visible()){select.pageDown();}else{onChange(0,true);}break;case options.multiple&&$.trim(options.multipleSeparator)==","&&KEY.COMMA:case KEY.TAB:case KEY.RETURN:if(selectCurrent()){event.preventDefault();blockSubmit=true;return false;}break;case KEY.ESC:select.hide();break;default:clearTimeout(timeout);timeout=setTimeout(onChange,options.delay);break;}}).focus(function(){hasFocus++;}).blur(function(){hasFocus=0;if(!config.mouseDownOnSelect){hideResults();}}).click(function(){if(hasFocus++>1&&!select.visible()){onChange(0,true);}}).bind("search",function(){var fn=(arguments.length>1)?arguments[1]:null;function findValueCallback(q,data){var result;if(data&&data.length){for(var i=0;i<data.length;i++){if(data[i].result.toLowerCase()==q.toLowerCase()){result=data[i];break;}}}if(typeof fn=="function")fn(result);else $input.trigger("result",result&&[result.data,result.value]);}$.each(trimWords($input.val()),function(i,value){request(value,findValueCallback,findValueCallback);});}).bind("flushCache",function(){cache.flush();}).bind("setOptions",function(){$.extend(options,arguments[1]);if("data"in arguments[1])cache.populate();}).bind("unautocomplete",function(){select.unbind();$input.unbind();$(input.form).unbind(".autocomplete");});function selectCurrent(){var selected=select.selected();if(!selected)return false;var v=selected.result;previousValue=v;if(options.multiple){var words=trimWords($input.val());if(words.length>1){v=words.slice(0,words.length-1).join(options.multipleSeparator)+options.multipleSeparator+v;}v+=options.multipleSeparator;}$input.val(v);hideResultsNow();$input.trigger("result",[selected.data,selected.value]);return true;}function onChange(crap,skipPrevCheck){if(lastKeyPressCode==KEY.DEL){select.hide();return;}var currentValue=$input.val();if(!skipPrevCheck&&currentValue==previousValue)return;previousValue=currentValue;currentValue=lastWord(currentValue);if(currentValue.length>=options.minChars){$input.addClass(options.loadingClass);if(!options.matchCase)currentValue=currentValue.toLowerCase();request(currentValue,receiveData,hideResultsNow);}else{stopLoading();select.hide();}};function trimWords(value){if(!value){return[""];}var words=value.split(options.multipleSeparator);var result=[];$.each(words,function(i,value){if($.trim(value))result[i]=$.trim(value);});return result;}function lastWord(value){if(!options.multiple)return value;var words=trimWords(value);return words[words.length-1];}function autoFill(q,sValue){if(options.autoFill&&(lastWord($input.val()).toLowerCase()==q.toLowerCase())&&lastKeyPressCode!=KEY.BACKSPACE){$input.val($input.val()+sValue.substring(lastWord(previousValue).length));$.Autocompleter.Selection(input,previousValue.length,previousValue.length+sValue.length);}};function hideResults(){clearTimeout(timeout);timeout=setTimeout(hideResultsNow,200);};function hideResultsNow(){var wasVisible=select.visible();select.hide();clearTimeout(timeout);stopLoading();if(options.mustMatch){$input.search(function(result){if(!result){if(options.multiple){var words=trimWords($input.val()).slice(0,-1);$input.val(words.join(options.multipleSeparator)+(words.length?options.multipleSeparator:""));}else
+$input.val("");}});}if(wasVisible)$.Autocompleter.Selection(input,input.value.length,input.value.length);};function receiveData(q,data){if(data&&data.length&&hasFocus){stopLoading();select.display(data,q);autoFill(q,data[0].value);select.show();}else{hideResultsNow();}};function request(term,success,failure){if(!options.matchCase)term=term.toLowerCase();var data=cache.load(term);if(data&&data.length){success(term,data);}else if((typeof options.url=="string")&&(options.url.length>0)){var extraParams={timestamp:+new Date()};$.each(options.extraParams,function(key,param){extraParams[key]=typeof param=="function"?param():param;});$.ajax({mode:"abort",port:"autocomplete"+input.name,dataType:options.dataType,url:options.url,data:$.extend({q:lastWord(term),limit:options.max},extraParams),success:function(data){var parsed=options.parse&&options.parse(data)||parse(data);cache.add(term,parsed);success(term,parsed);}});}else{select.emptyList();failure(term);}};function parse(data){var parsed=[];var rows=data.split("\n");for(var i=0;i<rows.length;i++){var row=$.trim(rows[i]);if(row){row=row.split("|");parsed[parsed.length]={data:row,value:row[0],result:options.formatResult&&options.formatResult(row,row[0])||row[0]};}}return parsed;};function stopLoading(){$input.removeClass(options.loadingClass);};};$.Autocompleter.defaults={inputClass:"ac_input",resultsClass:"ac_results",loadingClass:"ac_loading",minChars:1,delay:400,matchCase:false,matchSubset:true,matchContains:false,cacheLength:10,max:100,mustMatch:false,extraParams:{},selectFirst:true,formatItem:function(row){return row[0];},formatMatch:null,autoFill:false,width:0,multiple:false,multipleSeparator:", ",highlight:function(value,term){return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)("+term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi,"\\$1")+")(?![^<>]*>)(?![^&;]+;)","gi"),"<strong>$1</strong>");},scroll:true,scrollHeight:180};$.Autocompleter.Cache=function(options){var data={};var length=0;function matchSubset(s,sub){if(!options.matchCase)s=s.toLowerCase();var i=s.indexOf(sub);if(i==-1)return false;return i==0||options.matchContains;};function add(q,value){if(length>options.cacheLength){flush();}if(!data[q]){length++;}data[q]=value;}function populate(){if(!options.data)return false;var stMatchSets={},nullData=0;if(!options.url)options.cacheLength=1;stMatchSets[""]=[];for(var i=0,ol=options.data.length;i<ol;i++){var rawValue=options.data[i];rawValue=(typeof rawValue=="string")?[rawValue]:rawValue;var value=options.formatMatch(rawValue,i+1,options.data.length);if(value===false)continue;var firstChar=value.charAt(0).toLowerCase();if(!stMatchSets[firstChar])stMatchSets[firstChar]=[];var row={value:value,data:rawValue,result:options.formatResult&&options.formatResult(rawValue)||value};stMatchSets[firstChar].push(row);if(nullData++<options.max){stMatchSets[""].push(row);}};$.each(stMatchSets,function(i,value){options.cacheLength++;add(i,value);});}setTimeout(populate,25);function flush(){data={};length=0;}return{flush:flush,add:add,populate:populate,load:function(q){if(!options.cacheLength||!length)return null;if(!options.url&&options.matchContains){var csub=[];for(var k in data){if(k.length>0){var c=data[k];$.each(c,function(i,x){if(matchSubset(x.value,q)){csub.push(x);}});}}return csub;}else
+if(data[q]){return data[q];}else
+if(options.matchSubset){for(var i=q.length-1;i>=options.minChars;i--){var c=data[q.substr(0,i)];if(c){var csub=[];$.each(c,function(i,x){if(matchSubset(x.value,q)){csub[csub.length]=x;}});return csub;}}}return null;}};};$.Autocompleter.Select=function(options,input,select,config){var CLASSES={ACTIVE:"ac_over"};var listItems,active=-1,data,term="",needsInit=true,element,list;function init(){if(!needsInit)return;element=$("<div/>").hide().addClass(options.resultsClass).css("position","absolute").appendTo(document.body);list=$("<ul/>").appendTo(element).mouseover(function(event){if(target(event).nodeName&&target(event).nodeName.toUpperCase()=='LI'){active=$("li",list).removeClass(CLASSES.ACTIVE).index(target(event));$(target(event)).addClass(CLASSES.ACTIVE);}}).click(function(event){$(target(event)).addClass(CLASSES.ACTIVE);select();input.focus();return false;}).mousedown(function(){config.mouseDownOnSelect=true;}).mouseup(function(){config.mouseDownOnSelect=false;});if(options.width>0)element.css("width",options.width);needsInit=false;}function target(event){var element=event.target;while(element&&element.tagName!="LI")element=element.parentNode;if(!element)return[];return element;}function moveSelect(step){listItems.slice(active,active+1).removeClass(CLASSES.ACTIVE);movePosition(step);var activeItem=listItems.slice(active,active+1).addClass(CLASSES.ACTIVE);if(options.scroll){var offset=0;listItems.slice(0,active).each(function(){offset+=this.offsetHeight;});if((offset+activeItem[0].offsetHeight-list.scrollTop())>list[0].clientHeight){list.scrollTop(offset+activeItem[0].offsetHeight-list.innerHeight());}else if(offset<list.scrollTop()){list.scrollTop(offset);}}};function movePosition(step){active+=step;if(active<0){active=listItems.size()-1;}else if(active>=listItems.size()){active=0;}}function limitNumberOfItems(available){return options.max&&options.max<available?options.max:available;}function fillList(){list.empty();var max=limitNumberOfItems(data.length);for(var i=0;i<max;i++){if(!data[i])continue;var formatted=options.formatItem(data[i].data,i+1,max,data[i].value,term);if(formatted===false)continue;var li=$("<li/>").html(options.highlight(formatted,term)).addClass(i%2==0?"ac_even":"ac_odd").appendTo(list)[0];$.data(li,"ac_data",data[i]);}listItems=list.find("li");if(options.selectFirst){listItems.slice(0,1).addClass(CLASSES.ACTIVE);active=0;}if($.fn.bgiframe)list.bgiframe();}return{display:function(d,q){init();data=d;term=q;fillList();},next:function(){moveSelect(1);},prev:function(){moveSelect(-1);},pageUp:function(){if(active!=0&&active-8<0){moveSelect(-active);}else{moveSelect(-8);}},pageDown:function(){if(active!=listItems.size()-1&&active+8>listItems.size()){moveSelect(listItems.size()-1-active);}else{moveSelect(8);}},hide:function(){element&&element.hide();listItems&&listItems.removeClass(CLASSES.ACTIVE);active=-1;},visible:function(){return element&&element.is(":visible");},current:function(){return this.visible()&&(listItems.filter("."+CLASSES.ACTIVE)[0]||options.selectFirst&&listItems[0]);},show:function(){var offset=$(input).offset();element.css({width:typeof options.width=="string"||options.width>0?options.width:$(input).width(),top:offset.top+input.offsetHeight,left:offset.left}).show();if(options.scroll){list.scrollTop(0);list.css({maxHeight:options.scrollHeight,overflow:'auto'});if($.browser.msie&&typeof document.body.style.maxHeight==="undefined"){var listHeight=0;listItems.each(function(){listHeight+=this.offsetHeight;});var scrollbarsVisible=listHeight>options.scrollHeight;list.css('height',scrollbarsVisible?options.scrollHeight:listHeight);if(!scrollbarsVisible){listItems.width(list.width()-parseInt(listItems.css("padding-left"))-parseInt(listItems.css("padding-right")));}}}},selected:function(){var selected=listItems&&listItems.filter("."+CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);return selected&&selected.length&&$.data(selected[0],"ac_data");},emptyList:function(){list&&list.empty();},unbind:function(){element&&element.remove();}};};$.Autocompleter.Selection=function(field,start,end){if(field.createTextRange){var selRange=field.createTextRange();selRange.collapse(true);selRange.moveStart("character",start);selRange.moveEnd("character",end);selRange.select();}else if(field.setSelectionRange){field.setSelectionRange(start,end);}else{if(field.selectionStart){field.selectionStart=start;field.selectionEnd=end;}}field.focus();};})(jQuery);
\ No newline at end of file
diff --git a/plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.pack.js b/plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.pack.js
new file mode 100644 (file)
index 0000000..271014a
--- /dev/null
@@ -0,0 +1,13 @@
+/*
+ * Autocomplete - jQuery plugin 1.0.2
+ *
+ * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
+ *
+ * Dual licensed under the MIT and GPL licenses:
+ *   http://www.opensource.org/licenses/mit-license.php
+ *   http://www.gnu.org/licenses/gpl.html
+ *
+ * Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $
+ *
+ */
+eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}(';(3($){$.31.1o({12:3(b,d){5 c=Y b=="1w";d=$.1o({},$.D.1L,{11:c?b:14,w:c?14:b,1D:c?$.D.1L.1D:10,Z:d&&!d.1x?10:3U},d);d.1t=d.1t||3(a){6 a};d.1q=d.1q||d.1K;6 I.K(3(){1E $.D(I,d)})},M:3(a){6 I.X("M",a)},1y:3(a){6 I.15("1y",[a])},20:3(){6 I.15("20")},1Y:3(a){6 I.15("1Y",[a])},1X:3(){6 I.15("1X")}});$.D=3(o,r){5 t={2N:38,2I:40,2D:46,2x:9,2v:13,2q:27,2d:3x,2j:33,2o:34,2e:8};5 u=$(o).3f("12","3c").P(r.24);5 p;5 m="";5 n=$.D.2W(r);5 s=0;5 k;5 h={1z:B};5 l=$.D.2Q(r,o,1U,h);5 j;$.1T.2L&&$(o.2K).X("3S.12",3(){4(j){j=B;6 B}});u.X(($.1T.2L?"3Q":"3N")+".12",3(a){k=a.2F;3L(a.2F){Q t.2N:a.1d();4(l.L()){l.2y()}A{W(0,C)}N;Q t.2I:a.1d();4(l.L()){l.2u()}A{W(0,C)}N;Q t.2j:a.1d();4(l.L()){l.2t()}A{W(0,C)}N;Q t.2o:a.1d();4(l.L()){l.2s()}A{W(0,C)}N;Q r.19&&$.1p(r.R)==","&&t.2d:Q t.2x:Q t.2v:4(1U()){a.1d();j=C;6 B}N;Q t.2q:l.U();N;3A:1I(p);p=1H(W,r.1D);N}}).1G(3(){s++}).3v(3(){s=0;4(!h.1z){2k()}}).2i(3(){4(s++>1&&!l.L()){W(0,C)}}).X("1y",3(){5 c=(1n.7>1)?1n[1]:14;3 23(q,a){5 b;4(a&&a.7){16(5 i=0;i<a.7;i++){4(a[i].M.O()==q.O()){b=a[i];N}}}4(Y c=="3")c(b);A u.15("M",b&&[b.w,b.H])}$.K(1g(u.J()),3(i,a){1R(a,23,23)})}).X("20",3(){n.18()}).X("1Y",3(){$.1o(r,1n[1]);4("w"2G 1n[1])n.1f()}).X("1X",3(){l.1u();u.1u();$(o.2K).1u(".12")});3 1U(){5 b=l.26();4(!b)6 B;5 v=b.M;m=v;4(r.19){5 a=1g(u.J());4(a.7>1){v=a.17(0,a.7-1).2Z(r.R)+r.R+v}v+=r.R}u.J(v);1l();u.15("M",[b.w,b.H]);6 C}3 W(b,c){4(k==t.2D){l.U();6}5 a=u.J();4(!c&&a==m)6;m=a;a=1k(a);4(a.7>=r.22){u.P(r.21);4(!r.1C)a=a.O();1R(a,2V,1l)}A{1B();l.U()}};3 1g(b){4(!b){6[""]}5 d=b.1Z(r.R);5 c=[];$.K(d,3(i,a){4($.1p(a))c[i]=$.1p(a)});6 c}3 1k(a){4(!r.19)6 a;5 b=1g(a);6 b[b.7-1]}3 1A(q,a){4(r.1A&&(1k(u.J()).O()==q.O())&&k!=t.2e){u.J(u.J()+a.48(1k(m).7));$.D.1N(o,m.7,m.7+a.7)}};3 2k(){1I(p);p=1H(1l,47)};3 1l(){5 c=l.L();l.U();1I(p);1B();4(r.2U){u.1y(3(a){4(!a){4(r.19){5 b=1g(u.J()).17(0,-1);u.J(b.2Z(r.R)+(b.7?r.R:""))}A u.J("")}})}4(c)$.D.1N(o,o.H.7,o.H.7)};3 2V(q,a){4(a&&a.7&&s){1B();l.2T(a,q);1A(q,a[0].H);l.1W()}A{1l()}};3 1R(f,d,g){4(!r.1C)f=f.O();5 e=n.2S(f);4(e&&e.7){d(f,e)}A 4((Y r.11=="1w")&&(r.11.7>0)){5 c={45:+1E 44()};$.K(r.2R,3(a,b){c[a]=Y b=="3"?b():b});$.43({42:"41",3Z:"12"+o.3Y,2M:r.2M,11:r.11,w:$.1o({q:1k(f),3X:r.Z},c),3W:3(a){5 b=r.1r&&r.1r(a)||1r(a);n.1h(f,b);d(f,b)}})}A{l.2J();g(f)}};3 1r(c){5 d=[];5 b=c.1Z("\\n");16(5 i=0;i<b.7;i++){5 a=$.1p(b[i]);4(a){a=a.1Z("|");d[d.7]={w:a,H:a[0],M:r.1v&&r.1v(a,a[0])||a[0]}}}6 d};3 1B(){u.1e(r.21)}};$.D.1L={24:"3R",2H:"3P",21:"3O",22:1,1D:3M,1C:B,1a:C,1V:B,1j:10,Z:3K,2U:B,2R:{},1S:C,1K:3(a){6 a[0]},1q:14,1A:B,E:0,19:B,R:", ",1t:3(b,a){6 b.2C(1E 3J("(?![^&;]+;)(?!<[^<>]*)("+a.2C(/([\\^\\$\\(\\)\\[\\]\\{\\}\\*\\.\\+\\?\\|\\\\])/2A,"\\\\$1")+")(?![^<>]*>)(?![^&;]+;)","2A"),"<2z>$1</2z>")},1x:C,1s:3I};$.D.2W=3(g){5 h={};5 j=0;3 1a(s,a){4(!g.1C)s=s.O();5 i=s.3H(a);4(i==-1)6 B;6 i==0||g.1V};3 1h(q,a){4(j>g.1j){18()}4(!h[q]){j++}h[q]=a}3 1f(){4(!g.w)6 B;5 f={},2w=0;4(!g.11)g.1j=1;f[""]=[];16(5 i=0,30=g.w.7;i<30;i++){5 c=g.w[i];c=(Y c=="1w")?[c]:c;5 d=g.1q(c,i+1,g.w.7);4(d===B)1P;5 e=d.3G(0).O();4(!f[e])f[e]=[];5 b={H:d,w:c,M:g.1v&&g.1v(c)||d};f[e].1O(b);4(2w++<g.Z){f[""].1O(b)}};$.K(f,3(i,a){g.1j++;1h(i,a)})}1H(1f,25);3 18(){h={};j=0}6{18:18,1h:1h,1f:1f,2S:3(q){4(!g.1j||!j)6 14;4(!g.11&&g.1V){5 a=[];16(5 k 2G h){4(k.7>0){5 c=h[k];$.K(c,3(i,x){4(1a(x.H,q)){a.1O(x)}})}}6 a}A 4(h[q]){6 h[q]}A 4(g.1a){16(5 i=q.7-1;i>=g.22;i--){5 c=h[q.3F(0,i)];4(c){5 a=[];$.K(c,3(i,x){4(1a(x.H,q)){a[a.7]=x}});6 a}}}6 14}}};$.D.2Q=3(e,g,f,k){5 h={G:"3E"};5 j,y=-1,w,1m="",1M=C,F,z;3 2r(){4(!1M)6;F=$("<3D/>").U().P(e.2H).T("3C","3B").1J(2p.2n);z=$("<3z/>").1J(F).3y(3(a){4(V(a).2m&&V(a).2m.3w()==\'2l\'){y=$("1F",z).1e(h.G).3u(V(a));$(V(a)).P(h.G)}}).2i(3(a){$(V(a)).P(h.G);f();g.1G();6 B}).3t(3(){k.1z=C}).3s(3(){k.1z=B});4(e.E>0)F.T("E",e.E);1M=B}3 V(a){5 b=a.V;3r(b&&b.3q!="2l")b=b.3p;4(!b)6[];6 b}3 S(b){j.17(y,y+1).1e(h.G);2h(b);5 a=j.17(y,y+1).P(h.G);4(e.1x){5 c=0;j.17(0,y).K(3(){c+=I.1i});4((c+a[0].1i-z.1c())>z[0].3o){z.1c(c+a[0].1i-z.3n())}A 4(c<z.1c()){z.1c(c)}}};3 2h(a){y+=a;4(y<0){y=j.1b()-1}A 4(y>=j.1b()){y=0}}3 2g(a){6 e.Z&&e.Z<a?e.Z:a}3 2f(){z.2B();5 b=2g(w.7);16(5 i=0;i<b;i++){4(!w[i])1P;5 a=e.1K(w[i].w,i+1,b,w[i].H,1m);4(a===B)1P;5 c=$("<1F/>").3m(e.1t(a,1m)).P(i%2==0?"3l":"3k").1J(z)[0];$.w(c,"2c",w[i])}j=z.3j("1F");4(e.1S){j.17(0,1).P(h.G);y=0}4($.31.2b)z.2b()}6{2T:3(d,q){2r();w=d;1m=q;2f()},2u:3(){S(1)},2y:3(){S(-1)},2t:3(){4(y!=0&&y-8<0){S(-y)}A{S(-8)}},2s:3(){4(y!=j.1b()-1&&y+8>j.1b()){S(j.1b()-1-y)}A{S(8)}},U:3(){F&&F.U();j&&j.1e(h.G);y=-1},L:3(){6 F&&F.3i(":L")},3h:3(){6 I.L()&&(j.2a("."+h.G)[0]||e.1S&&j[0])},1W:3(){5 a=$(g).3g();F.T({E:Y e.E=="1w"||e.E>0?e.E:$(g).E(),2E:a.2E+g.1i,1Q:a.1Q}).1W();4(e.1x){z.1c(0);z.T({29:e.1s,3e:\'3d\'});4($.1T.3b&&Y 2p.2n.3T.29==="3a"){5 c=0;j.K(3(){c+=I.1i});5 b=c>e.1s;z.T(\'3V\',b?e.1s:c);4(!b){j.E(z.E()-28(j.T("32-1Q"))-28(j.T("32-39")))}}}},26:3(){5 a=j&&j.2a("."+h.G).1e(h.G);6 a&&a.7&&$.w(a[0],"2c")},2J:3(){z&&z.2B()},1u:3(){F&&F.37()}}};$.D.1N=3(b,a,c){4(b.2O){5 d=b.2O();d.36(C);d.35("2P",a);d.4c("2P",c);d.4b()}A 4(b.2Y){b.2Y(a,c)}A{4(b.2X){b.2X=a;b.4a=c}}b.1G()}})(49);',62,261,'|||function|if|var|return|length|||||||||||||||||||||||||data||active|list|else|false|true|Autocompleter|width|element|ACTIVE|value|this|val|each|visible|result|break|toLowerCase|addClass|case|multipleSeparator|moveSelect|css|hide|target|onChange|bind|typeof|max||url|autocomplete||null|trigger|for|slice|flush|multiple|matchSubset|size|scrollTop|preventDefault|removeClass|populate|trimWords|add|offsetHeight|cacheLength|lastWord|hideResultsNow|term|arguments|extend|trim|formatMatch|parse|scrollHeight|highlight|unbind|formatResult|string|scroll|search|mouseDownOnSelect|autoFill|stopLoading|matchCase|delay|new|li|focus|setTimeout|clearTimeout|appendTo|formatItem|defaults|needsInit|Selection|push|continue|left|request|selectFirst|browser|selectCurrent|matchContains|show|unautocomplete|setOptions|split|flushCache|loadingClass|minChars|findValueCallback|inputClass||selected||parseInt|maxHeight|filter|bgiframe|ac_data|COMMA|BACKSPACE|fillList|limitNumberOfItems|movePosition|click|PAGEUP|hideResults|LI|nodeName|body|PAGEDOWN|document|ESC|init|pageDown|pageUp|next|RETURN|nullData|TAB|prev|strong|gi|empty|replace|DEL|top|keyCode|in|resultsClass|DOWN|emptyList|form|opera|dataType|UP|createTextRange|character|Select|extraParams|load|display|mustMatch|receiveData|Cache|selectionStart|setSelectionRange|join|ol|fn|padding|||moveStart|collapse|remove||right|undefined|msie|off|auto|overflow|attr|offset|current|is|find|ac_odd|ac_even|html|innerHeight|clientHeight|parentNode|tagName|while|mouseup|mousedown|index|blur|toUpperCase|188|mouseover|ul|default|absolute|position|div|ac_over|substr|charAt|indexOf|180|RegExp|100|switch|400|keydown|ac_loading|ac_results|keypress|ac_input|submit|style|150|height|success|limit|name|port||abort|mode|ajax|Date|timestamp||200|substring|jQuery|selectionEnd|select|moveEnd'.split('|'),0,{}))
\ No newline at end of file
diff --git a/plugins/Autocomplete/jquery-autocomplete/lib/jquery.ajaxQueue.js b/plugins/Autocomplete/jquery-autocomplete/lib/jquery.ajaxQueue.js
new file mode 100644 (file)
index 0000000..bdd2e4f
--- /dev/null
@@ -0,0 +1,116 @@
+/**\r
+ * Ajax Queue Plugin\r
+ * \r
+ * Homepage: http://jquery.com/plugins/project/ajaxqueue\r
+ * Documentation: http://docs.jquery.com/AjaxQueue\r
+ */\r
+\r
+/**\r
+\r
+<script>\r
+$(function(){\r
+       jQuery.ajaxQueue({\r
+               url: "test.php",\r
+               success: function(html){ jQuery("ul").append(html); }\r
+       });\r
+       jQuery.ajaxQueue({\r
+               url: "test.php",\r
+               success: function(html){ jQuery("ul").append(html); }\r
+       });\r
+       jQuery.ajaxSync({\r
+               url: "test.php",\r
+               success: function(html){ jQuery("ul").append("<b>"+html+"</b>"); }\r
+       });\r
+       jQuery.ajaxSync({\r
+               url: "test.php",\r
+               success: function(html){ jQuery("ul").append("<b>"+html+"</b>"); }\r
+       });\r
+});\r
+</script>\r
+<ul style="position: absolute; top: 5px; right: 5px;"></ul>\r
+\r
+ */\r
+/*\r
+ * Queued Ajax requests.\r
+ * A new Ajax request won't be started until the previous queued \r
+ * request has finished.\r
+ */\r
+\r
+/*\r
+ * Synced Ajax requests.\r
+ * The Ajax request will happen as soon as you call this method, but\r
+ * the callbacks (success/error/complete) won't fire until all previous\r
+ * synced requests have been completed.\r
+ */\r
+\r
+\r
+(function($) {\r
+       \r
+       var ajax = $.ajax;\r
+       \r
+       var pendingRequests = {};\r
+       \r
+       var synced = [];\r
+       var syncedData = [];\r
+       \r
+       $.ajax = function(settings) {\r
+               // create settings for compatibility with ajaxSetup\r
+               settings = jQuery.extend(settings, jQuery.extend({}, jQuery.ajaxSettings, settings));\r
+               \r
+               var port = settings.port;\r
+               \r
+               switch(settings.mode) {\r
+               case "abort": \r
+                       if ( pendingRequests[port] ) {\r
+                               pendingRequests[port].abort();\r
+                       }\r
+                       return pendingRequests[port] = ajax.apply(this, arguments);\r
+               case "queue": \r
+                       var _old = settings.complete;\r
+                       settings.complete = function(){\r
+                               if ( _old )\r
+                                       _old.apply( this, arguments );\r
+                               jQuery([ajax]).dequeue("ajax" + port );;\r
+                       };\r
+               \r
+                       jQuery([ ajax ]).queue("ajax" + port, function(){\r
+                               ajax( settings );\r
+                       });\r
+                       return;\r
+               case "sync":\r
+                       var pos = synced.length;\r
+       \r
+                       synced[ pos ] = {\r
+                               error: settings.error,\r
+                               success: settings.success,\r
+                               complete: settings.complete,\r
+                               done: false\r
+                       };\r
+               \r
+                       syncedData[ pos ] = {\r
+                               error: [],\r
+                               success: [],\r
+                               complete: []\r
+                       };\r
+               \r
+                       settings.error = function(){ syncedData[ pos ].error = arguments; };\r
+                       settings.success = function(){ syncedData[ pos ].success = arguments; };\r
+                       settings.complete = function(){\r
+                               syncedData[ pos ].complete = arguments;\r
+                               synced[ pos ].done = true;\r
+               \r
+                               if ( pos == 0 || !synced[ pos-1 ] )\r
+                                       for ( var i = pos; i < synced.length && synced[i].done; i++ ) {\r
+                                               if ( synced[i].error ) synced[i].error.apply( jQuery, syncedData[i].error );\r
+                                               if ( synced[i].success ) synced[i].success.apply( jQuery, syncedData[i].success );\r
+                                               if ( synced[i].complete ) synced[i].complete.apply( jQuery, syncedData[i].complete );\r
+               \r
+                                               synced[i] = null;\r
+                                               syncedData[i] = null;\r
+                                       }\r
+                       };\r
+               }\r
+               return ajax.apply(this, arguments);\r
+       };\r
+       \r
+})(jQuery);
\ No newline at end of file
diff --git a/plugins/Autocomplete/jquery-autocomplete/lib/jquery.bgiframe.min.js b/plugins/Autocomplete/jquery-autocomplete/lib/jquery.bgiframe.min.js
new file mode 100644 (file)
index 0000000..7faef4b
--- /dev/null
@@ -0,0 +1,10 @@
+/* Copyright (c) 2006 Brandon Aaron (http://brandonaaron.net)
+ * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 
+ * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
+ *
+ * $LastChangedDate: 2007-07-22 01:45:56 +0200 (Son, 22 Jul 2007) $
+ * $Rev: 2447 $
+ *
+ * Version 2.1.1
+ */
+(function($){$.fn.bgIframe=$.fn.bgiframe=function(s){if($.browser.msie&&/6.0/.test(navigator.userAgent)){s=$.extend({top:'auto',left:'auto',width:'auto',height:'auto',opacity:true,src:'javascript:false;'},s||{});var prop=function(n){return n&&n.constructor==Number?n+'px':n;},html='<iframe class="bgiframe"frameborder="0"tabindex="-1"src="'+s.src+'"'+'style="display:block;position:absolute;z-index:-1;'+(s.opacity!==false?'filter:Alpha(Opacity=\'0\');':'')+'top:'+(s.top=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderTopWidth)||0)*-1)+\'px\')':prop(s.top))+';'+'left:'+(s.left=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderLeftWidth)||0)*-1)+\'px\')':prop(s.left))+';'+'width:'+(s.width=='auto'?'expression(this.parentNode.offsetWidth+\'px\')':prop(s.width))+';'+'height:'+(s.height=='auto'?'expression(this.parentNode.offsetHeight+\'px\')':prop(s.height))+';'+'"/>';return this.each(function(){if($('> iframe.bgiframe',this).length==0)this.insertBefore(document.createElement(html),this.firstChild);});}return this;};})(jQuery);
\ No newline at end of file
diff --git a/plugins/Autocomplete/jquery-autocomplete/lib/jquery.js b/plugins/Autocomplete/jquery-autocomplete/lib/jquery.js
new file mode 100644 (file)
index 0000000..400531a
--- /dev/null
@@ -0,0 +1,3558 @@
+(function(){
+/*
+ * jQuery 1.2.6 - New Wave Javascript
+ *
+ * Copyright (c) 2008 John Resig (jquery.com)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2008-05-27 21:17:26 +0200 (Di, 27 Mai 2008) $
+ * $Rev: 5700 $
+ */
+
+// Map over jQuery in case of overwrite
+var _jQuery = window.jQuery,
+// Map over the $ in case of overwrite
+       _$ = window.$;
+
+var jQuery = window.jQuery = window.$ = function( selector, context ) {
+       // The jQuery object is actually just the init constructor 'enhanced'
+       return new jQuery.fn.init( selector, context );
+};
+
+// A simple way to check for HTML strings or ID strings
+// (both of which we optimize for)
+var quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/,
+
+// Is it a simple selector
+       isSimple = /^.[^:#\[\.]*$/,
+
+// Will speed up references to undefined, and allows munging its name.
+       undefined;
+
+jQuery.fn = jQuery.prototype = {
+       init: function( selector, context ) {
+               // Make sure that a selection was provided
+               selector = selector || document;
+
+               // Handle $(DOMElement)
+               if ( selector.nodeType ) {
+                       this[0] = selector;
+                       this.length = 1;
+                       return this;
+               }
+               // Handle HTML strings
+               if ( typeof selector == "string" ) {
+                       // Are we dealing with HTML string or an ID?
+                       var match = quickExpr.exec( selector );
+
+                       // Verify a match, and that no context was specified for #id
+                       if ( match && (match[1] || !context) ) {
+
+                               // HANDLE: $(html) -> $(array)
+                               if ( match[1] )
+                                       selector = jQuery.clean( [ match[1] ], context );
+
+                               // HANDLE: $("#id")
+                               else {
+                                       var elem = document.getElementById( match[3] );
+
+                                       // Make sure an element was located
+                                       if ( elem ){
+                                               // Handle the case where IE and Opera return items
+                                               // by name instead of ID
+                                               if ( elem.id != match[3] )
+                                                       return jQuery().find( selector );
+
+                                               // Otherwise, we inject the element directly into the jQuery object
+                                               return jQuery( elem );
+                                       }
+                                       selector = [];
+                               }
+
+                       // HANDLE: $(expr, [context])
+                       // (which is just equivalent to: $(content).find(expr)
+                       } else
+                               return jQuery( context ).find( selector );
+
+               // HANDLE: $(function)
+               // Shortcut for document ready
+               } else if ( jQuery.isFunction( selector ) )
+                       return jQuery( document )[ jQuery.fn.ready ? "ready" : "load" ]( selector );
+
+               return this.setArray(jQuery.makeArray(selector));
+       },
+
+       // The current version of jQuery being used
+       jquery: "1.2.6",
+
+       // The number of elements contained in the matched element set
+       size: function() {
+               return this.length;
+       },
+
+       // The number of elements contained in the matched element set
+       length: 0,
+
+       // Get the Nth element in the matched element set OR
+       // Get the whole matched element set as a clean array
+       get: function( num ) {
+               return num == undefined ?
+
+                       // Return a 'clean' array
+                       jQuery.makeArray( this ) :
+
+                       // Return just the object
+                       this[ num ];
+       },
+
+       // Take an array of elements and push it onto the stack
+       // (returning the new matched element set)
+       pushStack: function( elems ) {
+               // Build a new jQuery matched element set
+               var ret = jQuery( elems );
+
+               // Add the old object onto the stack (as a reference)
+               ret.prevObject = this;
+
+               // Return the newly-formed element set
+               return ret;
+       },
+
+       // Force the current matched set of elements to become
+       // the specified array of elements (destroying the stack in the process)
+       // You should use pushStack() in order to do this, but maintain the stack
+       setArray: function( elems ) {
+               // Resetting the length to 0, then using the native Array push
+               // is a super-fast way to populate an object with array-like properties
+               this.length = 0;
+               Array.prototype.push.apply( this, elems );
+
+               return this;
+       },
+
+       // Execute a callback for every element in the matched set.
+       // (You can seed the arguments with an array of args, but this is
+       // only used internally.)
+       each: function( callback, args ) {
+               return jQuery.each( this, callback, args );
+       },
+
+       // Determine the position of an element within
+       // the matched set of elements
+       index: function( elem ) {
+               var ret = -1;
+
+               // Locate the position of the desired element
+               return jQuery.inArray(
+                       // If it receives a jQuery object, the first element is used
+                       elem && elem.jquery ? elem[0] : elem
+               , this );
+       },
+
+       attr: function( name, value, type ) {
+               var options = name;
+
+               // Look for the case where we're accessing a style value
+               if ( name.constructor == String )
+                       if ( value === undefined )
+                               return this[0] && jQuery[ type || "attr" ]( this[0], name );
+
+                       else {
+                               options = {};
+                               options[ name ] = value;
+                       }
+
+               // Check to see if we're setting style values
+               return this.each(function(i){
+                       // Set all the styles
+                       for ( name in options )
+                               jQuery.attr(
+                                       type ?
+                                               this.style :
+                                               this,
+                                       name, jQuery.prop( this, options[ name ], type, i, name )
+                               );
+               });
+       },
+
+       css: function( key, value ) {
+               // ignore negative width and height values
+               if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 )
+                       value = undefined;
+               return this.attr( key, value, "curCSS" );
+       },
+
+       text: function( text ) {
+               if ( typeof text != "object" && text != null )
+                       return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
+
+               var ret = "";
+
+               jQuery.each( text || this, function(){
+                       jQuery.each( this.childNodes, function(){
+                               if ( this.nodeType != 8 )
+                                       ret += this.nodeType != 1 ?
+                                               this.nodeValue :
+                                               jQuery.fn.text( [ this ] );
+                       });
+               });
+
+               return ret;
+       },
+
+       wrapAll: function( html ) {
+               if ( this[0] )
+                       // The elements to wrap the target around
+                       jQuery( html, this[0].ownerDocument )
+                               .clone()
+                               .insertBefore( this[0] )
+                               .map(function(){
+                                       var elem = this;
+
+                                       while ( elem.firstChild )
+                                               elem = elem.firstChild;
+
+                                       return elem;
+                               })
+                               .append(this);
+
+               return this;
+       },
+
+       wrapInner: function( html ) {
+               return this.each(function(){
+                       jQuery( this ).contents().wrapAll( html );
+               });
+       },
+
+       wrap: function( html ) {
+               return this.each(function(){
+                       jQuery( this ).wrapAll( html );
+               });
+       },
+
+       append: function() {
+               return this.domManip(arguments, true, false, function(elem){
+                       if (this.nodeType == 1)
+                               this.appendChild( elem );
+               });
+       },
+
+       prepend: function() {
+               return this.domManip(arguments, true, true, function(elem){
+                       if (this.nodeType == 1)
+                               this.insertBefore( elem, this.firstChild );
+               });
+       },
+
+       before: function() {
+               return this.domManip(arguments, false, false, function(elem){
+                       this.parentNode.insertBefore( elem, this );
+               });
+       },
+
+       after: function() {
+               return this.domManip(arguments, false, true, function(elem){
+                       this.parentNode.insertBefore( elem, this.nextSibling );
+               });
+       },
+
+       end: function() {
+               return this.prevObject || jQuery( [] );
+       },
+
+       find: function( selector ) {
+               var elems = jQuery.map(this, function(elem){
+                       return jQuery.find( selector, elem );
+               });
+
+               return this.pushStack( /[^+>] [^+>]/.test( selector ) || selector.indexOf("..") > -1 ?
+                       jQuery.unique( elems ) :
+                       elems );
+       },
+
+       clone: function( events ) {
+               // Do the clone
+               var ret = this.map(function(){
+                       if ( jQuery.browser.msie && !jQuery.isXMLDoc(this) ) {
+                               // IE copies events bound via attachEvent when
+                               // using cloneNode. Calling detachEvent on the
+                               // clone will also remove the events from the orignal
+                               // In order to get around this, we use innerHTML.
+                               // Unfortunately, this means some modifications to
+                               // attributes in IE that are actually only stored
+                               // as properties will not be copied (such as the
+                               // the name attribute on an input).
+                               var clone = this.cloneNode(true),
+                                       container = document.createElement("div");
+                               container.appendChild(clone);
+                               return jQuery.clean([container.innerHTML])[0];
+                       } else
+                               return this.cloneNode(true);
+               });
+
+               // Need to set the expando to null on the cloned set if it exists
+               // removeData doesn't work here, IE removes it from the original as well
+               // this is primarily for IE but the data expando shouldn't be copied over in any browser
+               var clone = ret.find("*").andSelf().each(function(){
+                       if ( this[ expando ] != undefined )
+                               this[ expando ] = null;
+               });
+
+               // Copy the events from the original to the clone
+               if ( events === true )
+                       this.find("*").andSelf().each(function(i){
+                               if (this.nodeType == 3)
+                                       return;
+                               var events = jQuery.data( this, "events" );
+
+                               for ( var type in events )
+                                       for ( var handler in events[ type ] )
+                                               jQuery.event.add( clone[ i ], type, events[ type ][ handler ], events[ type ][ handler ].data );
+                       });
+
+               // Return the cloned set
+               return ret;
+       },
+
+       filter: function( selector ) {
+               return this.pushStack(
+                       jQuery.isFunction( selector ) &&
+                       jQuery.grep(this, function(elem, i){
+                               return selector.call( elem, i );
+                       }) ||
+
+                       jQuery.multiFilter( selector, this ) );
+       },
+
+       not: function( selector ) {
+               if ( selector.constructor == String )
+                       // test special case where just one selector is passed in
+                       if ( isSimple.test( selector ) )
+                               return this.pushStack( jQuery.multiFilter( selector, this, true ) );
+                       else
+                               selector = jQuery.multiFilter( selector, this );
+
+               var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType;
+               return this.filter(function() {
+                       return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector;
+               });
+       },
+
+       add: function( selector ) {
+               return this.pushStack( jQuery.unique( jQuery.merge(
+                       this.get(),
+                       typeof selector == 'string' ?
+                               jQuery( selector ) :
+                               jQuery.makeArray( selector )
+               )));
+       },
+
+       is: function( selector ) {
+               return !!selector && jQuery.multiFilter( selector, this ).length > 0;
+       },
+
+       hasClass: function( selector ) {
+               return this.is( "." + selector );
+       },
+
+       val: function( value ) {
+               if ( value == undefined ) {
+
+                       if ( this.length ) {
+                               var elem = this[0];
+
+                               // We need to handle select boxes special
+                               if ( jQuery.nodeName( elem, "select" ) ) {
+                                       var index = elem.selectedIndex,
+                                               values = [],
+                                               options = elem.options,
+                                               one = elem.type == "select-one";
+
+                                       // Nothing was selected
+                                       if ( index < 0 )
+                                               return null;
+
+                                       // Loop through all the selected options
+                                       for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) {
+                                               var option = options[ i ];
+
+                                               if ( option.selected ) {
+                                                       // Get the specifc value for the option
+                                                       value = jQuery.browser.msie && !option.attributes.value.specified ? option.text : option.value;
+
+                                                       // We don't need an array for one selects
+                                                       if ( one )
+                                                               return value;
+
+                                                       // Multi-Selects return an array
+                                                       values.push( value );
+                                               }
+                                       }
+
+                                       return values;
+
+                               // Everything else, we just grab the value
+                               } else
+                                       return (this[0].value || "").replace(/\r/g, "");
+
+                       }
+
+                       return undefined;
+               }
+
+               if( value.constructor == Number )
+                       value += '';
+
+               return this.each(function(){
+                       if ( this.nodeType != 1 )
+                               return;
+
+                       if ( value.constructor == Array && /radio|checkbox/.test( this.type ) )
+                               this.checked = (jQuery.inArray(this.value, value) >= 0 ||
+                                       jQuery.inArray(this.name, value) >= 0);
+
+                       else if ( jQuery.nodeName( this, "select" ) ) {
+                               var values = jQuery.makeArray(value);
+
+                               jQuery( "option", this ).each(function(){
+                                       this.selected = (jQuery.inArray( this.value, values ) >= 0 ||
+                                               jQuery.inArray( this.text, values ) >= 0);
+                               });
+
+                               if ( !values.length )
+                                       this.selectedIndex = -1;
+
+                       } else
+                               this.value = value;
+               });
+       },
+
+       html: function( value ) {
+               return value == undefined ?
+                       (this[0] ?
+                               this[0].innerHTML :
+                               null) :
+                       this.empty().append( value );
+       },
+
+       replaceWith: function( value ) {
+               return this.after( value ).remove();
+       },
+
+       eq: function( i ) {
+               return this.slice( i, i + 1 );
+       },
+
+       slice: function() {
+               return this.pushStack( Array.prototype.slice.apply( this, arguments ) );
+       },
+
+       map: function( callback ) {
+               return this.pushStack( jQuery.map(this, function(elem, i){
+                       return callback.call( elem, i, elem );
+               }));
+       },
+
+       andSelf: function() {
+               return this.add( this.prevObject );
+       },
+
+       data: function( key, value ){
+               var parts = key.split(".");
+               parts[1] = parts[1] ? "." + parts[1] : "";
+
+               if ( value === undefined ) {
+                       var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);
+
+                       if ( data === undefined && this.length )
+                               data = jQuery.data( this[0], key );
+
+                       return data === undefined && parts[1] ?
+                               this.data( parts[0] ) :
+                               data;
+               } else
+                       return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function(){
+                               jQuery.data( this, key, value );
+                       });
+       },
+
+       removeData: function( key ){
+               return this.each(function(){
+                       jQuery.removeData( this, key );
+               });
+       },
+
+       domManip: function( args, table, reverse, callback ) {
+               var clone = this.length > 1, elems;
+
+               return this.each(function(){
+                       if ( !elems ) {
+                               elems = jQuery.clean( args, this.ownerDocument );
+
+                               if ( reverse )
+                                       elems.reverse();
+                       }
+
+                       var obj = this;
+
+                       if ( table && jQuery.nodeName( this, "table" ) && jQuery.nodeName( elems[0], "tr" ) )
+                               obj = this.getElementsByTagName("tbody")[0] || this.appendChild( this.ownerDocument.createElement("tbody") );
+
+                       var scripts = jQuery( [] );
+
+                       jQuery.each(elems, function(){
+                               var elem = clone ?
+                                       jQuery( this ).clone( true )[0] :
+                                       this;
+
+                               // execute all scripts after the elements have been injected
+                               if ( jQuery.nodeName( elem, "script" ) )
+                                       scripts = scripts.add( elem );
+                               else {
+                                       // Remove any inner scripts for later evaluation
+                                       if ( elem.nodeType == 1 )
+                                               scripts = scripts.add( jQuery( "script", elem ).remove() );
+
+                                       // Inject the elements into the document
+                                       callback.call( obj, elem );
+                               }
+                       });
+
+                       scripts.each( evalScript );
+               });
+       }
+};
+
+// Give the init function the jQuery prototype for later instantiation
+jQuery.fn.init.prototype = jQuery.fn;
+
+function evalScript( i, elem ) {
+       if ( elem.src )
+               jQuery.ajax({
+                       url: elem.src,
+                       async: false,
+                       dataType: "script"
+               });
+
+       else
+               jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" );
+
+       if ( elem.parentNode )
+               elem.parentNode.removeChild( elem );
+}
+
+function now(){
+       return +new Date;
+}
+
+jQuery.extend = jQuery.fn.extend = function() {
+       // copy reference to target object
+       var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options;
+
+       // Handle a deep copy situation
+       if ( target.constructor == Boolean ) {
+               deep = target;
+               target = arguments[1] || {};
+               // skip the boolean and the target
+               i = 2;
+       }
+
+       // Handle case when target is a string or something (possible in deep copy)
+       if ( typeof target != "object" && typeof target != "function" )
+               target = {};
+
+       // extend jQuery itself if only one argument is passed
+       if ( length == i ) {
+               target = this;
+               --i;
+       }
+
+       for ( ; i < length; i++ )
+               // Only deal with non-null/undefined values
+               if ( (options = arguments[ i ]) != null )
+                       // Extend the base object
+                       for ( var name in options ) {
+                               var src = target[ name ], copy = options[ name ];
+
+                               // Prevent never-ending loop
+                               if ( target === copy )
+                                       continue;
+
+                               // Recurse if we're merging object values
+                               if ( deep && copy && typeof copy == "object" && !copy.nodeType )
+                                       target[ name ] = jQuery.extend( deep, 
+                                               // Never move original objects, clone them
+                                               src || ( copy.length != null ? [ ] : { } )
+                                       , copy );
+
+                               // Don't bring in undefined values
+                               else if ( copy !== undefined )
+                                       target[ name ] = copy;
+
+                       }
+
+       // Return the modified object
+       return target;
+};
+
+var expando = "jQuery" + now(), uuid = 0, windowData = {},
+       // exclude the following css properties to add px
+       exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i,
+       // cache defaultView
+       defaultView = document.defaultView || {};
+
+jQuery.extend({
+       noConflict: function( deep ) {
+               window.$ = _$;
+
+               if ( deep )
+                       window.jQuery = _jQuery;
+
+               return jQuery;
+       },
+
+       // See test/unit/core.js for details concerning this function.
+       isFunction: function( fn ) {
+               return !!fn && typeof fn != "string" && !fn.nodeName &&
+                       fn.constructor != Array && /^[\s[]?function/.test( fn + "" );
+       },
+
+       // check if an element is in a (or is an) XML document
+       isXMLDoc: function( elem ) {
+               return elem.documentElement && !elem.body ||
+                       elem.tagName && elem.ownerDocument && !elem.ownerDocument.body;
+       },
+
+       // Evalulates a script in a global context
+       globalEval: function( data ) {
+               data = jQuery.trim( data );
+
+               if ( data ) {
+                       // Inspired by code by Andrea Giammarchi
+                       // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html
+                       var head = document.getElementsByTagName("head")[0] || document.documentElement,
+                               script = document.createElement("script");
+
+                       script.type = "text/javascript";
+                       if ( jQuery.browser.msie )
+                               script.text = data;
+                       else
+                               script.appendChild( document.createTextNode( data ) );
+
+                       // Use insertBefore instead of appendChild  to circumvent an IE6 bug.
+                       // This arises when a base node is used (#2709).
+                       head.insertBefore( script, head.firstChild );
+                       head.removeChild( script );
+               }
+       },
+
+       nodeName: function( elem, name ) {
+               return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase();
+       },
+
+       cache: {},
+
+       data: function( elem, name, data ) {
+               elem = elem == window ?
+                       windowData :
+                       elem;
+
+               var id = elem[ expando ];
+
+               // Compute a unique ID for the element
+               if ( !id )
+                       id = elem[ expando ] = ++uuid;
+
+               // Only generate the data cache if we're
+               // trying to access or manipulate it
+               if ( name && !jQuery.cache[ id ] )
+                       jQuery.cache[ id ] = {};
+
+               // Prevent overriding the named cache with undefined values
+               if ( data !== undefined )
+                       jQuery.cache[ id ][ name ] = data;
+
+               // Return the named cache data, or the ID for the element
+               return name ?
+                       jQuery.cache[ id ][ name ] :
+                       id;
+       },
+
+       removeData: function( elem, name ) {
+               elem = elem == window ?
+                       windowData :
+                       elem;
+
+               var id = elem[ expando ];
+
+               // If we want to remove a specific section of the element's data
+               if ( name ) {
+                       if ( jQuery.cache[ id ] ) {
+                               // Remove the section of cache data
+                               delete jQuery.cache[ id ][ name ];
+
+                               // If we've removed all the data, remove the element's cache
+                               name = "";
+
+                               for ( name in jQuery.cache[ id ] )
+                                       break;
+
+                               if ( !name )
+                                       jQuery.removeData( elem );
+                       }
+
+               // Otherwise, we want to remove all of the element's data
+               } else {
+                       // Clean up the element expando
+                       try {
+                               delete elem[ expando ];
+                       } catch(e){
+                               // IE has trouble directly removing the expando
+                               // but it's ok with using removeAttribute
+                               if ( elem.removeAttribute )
+                                       elem.removeAttribute( expando );
+                       }
+
+                       // Completely remove the data cache
+                       delete jQuery.cache[ id ];
+               }
+       },
+
+       // args is for internal usage only
+       each: function( object, callback, args ) {
+               var name, i = 0, length = object.length;
+
+               if ( args ) {
+                       if ( length == undefined ) {
+                               for ( name in object )
+                                       if ( callback.apply( object[ name ], args ) === false )
+                                               break;
+                       } else
+                               for ( ; i < length; )
+                                       if ( callback.apply( object[ i++ ], args ) === false )
+                                               break;
+
+               // A special, fast, case for the most common use of each
+               } else {
+                       if ( length == undefined ) {
+                               for ( name in object )
+                                       if ( callback.call( object[ name ], name, object[ name ] ) === false )
+                                               break;
+                       } else
+                               for ( var value = object[0];
+                                       i < length && callback.call( value, i, value ) !== false; value = object[++i] ){}
+               }
+
+               return object;
+       },
+
+       prop: function( elem, value, type, i, name ) {
+               // Handle executable functions
+               if ( jQuery.isFunction( value ) )
+                       value = value.call( elem, i );
+
+               // Handle passing in a number to a CSS property
+               return value && value.constructor == Number && type == "curCSS" && !exclude.test( name ) ?
+                       value + "px" :
+                       value;
+       },
+
+       className: {
+               // internal only, use addClass("class")
+               add: function( elem, classNames ) {
+                       jQuery.each((classNames || "").split(/\s+/), function(i, className){
+                               if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) )
+                                       elem.className += (elem.className ? " " : "") + className;
+                       });
+               },
+
+               // internal only, use removeClass("class")
+               remove: function( elem, classNames ) {
+                       if (elem.nodeType == 1)
+                               elem.className = classNames != undefined ?
+                                       jQuery.grep(elem.className.split(/\s+/), function(className){
+                                               return !jQuery.className.has( classNames, className );
+                                       }).join(" ") :
+                                       "";
+               },
+
+               // internal only, use hasClass("class")
+               has: function( elem, className ) {
+                       return jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1;
+               }
+       },
+
+       // A method for quickly swapping in/out CSS properties to get correct calculations
+       swap: function( elem, options, callback ) {
+               var old = {};
+               // Remember the old values, and insert the new ones
+               for ( var name in options ) {
+                       old[ name ] = elem.style[ name ];
+                       elem.style[ name ] = options[ name ];
+               }
+
+               callback.call( elem );
+
+               // Revert the old values
+               for ( var name in options )
+                       elem.style[ name ] = old[ name ];
+       },
+
+       css: function( elem, name, force ) {
+               if ( name == "width" || name == "height" ) {
+                       var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ];
+
+                       function getWH() {
+                               val = name == "width" ? elem.offsetWidth : elem.offsetHeight;
+                               var padding = 0, border = 0;
+                               jQuery.each( which, function() {
+                                       padding += parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0;
+                                       border += parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0;
+                               });
+                               val -= Math.round(padding + border);
+                       }
+
+                       if ( jQuery(elem).is(":visible") )
+                               getWH();
+                       else
+                               jQuery.swap( elem, props, getWH );
+
+                       return Math.max(0, val);
+               }
+
+               return jQuery.curCSS( elem, name, force );
+       },
+
+       curCSS: function( elem, name, force ) {
+               var ret, style = elem.style;
+
+               // A helper method for determining if an element's values are broken
+               function color( elem ) {
+                       if ( !jQuery.browser.safari )
+                               return false;
+
+                       // defaultView is cached
+                       var ret = defaultView.getComputedStyle( elem, null );
+                       return !ret || ret.getPropertyValue("color") == "";
+               }
+
+               // We need to handle opacity special in IE
+               if ( name == "opacity" && jQuery.browser.msie ) {
+                       ret = jQuery.attr( style, "opacity" );
+
+                       return ret == "" ?
+                               "1" :
+                               ret;
+               }
+               // Opera sometimes will give the wrong display answer, this fixes it, see #2037
+               if ( jQuery.browser.opera && name == "display" ) {
+                       var save = style.outline;
+                       style.outline = "0 solid black";
+                       style.outline = save;
+               }
+
+               // Make sure we're using the right name for getting the float value
+               if ( name.match( /float/i ) )
+                       name = styleFloat;
+
+               if ( !force && style && style[ name ] )
+                       ret = style[ name ];
+
+               else if ( defaultView.getComputedStyle ) {
+
+                       // Only "float" is needed here
+                       if ( name.match( /float/i ) )
+                               name = "float";
+
+                       name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase();
+
+                       var computedStyle = defaultView.getComputedStyle( elem, null );
+
+                       if ( computedStyle && !color( elem ) )
+                               ret = computedStyle.getPropertyValue( name );
+
+                       // If the element isn't reporting its values properly in Safari
+                       // then some display: none elements are involved
+                       else {
+                               var swap = [], stack = [], a = elem, i = 0;
+
+                               // Locate all of the parent display: none elements
+                               for ( ; a && color(a); a = a.parentNode )
+                                       stack.unshift(a);
+
+                               // Go through and make them visible, but in reverse
+                               // (It would be better if we knew the exact display type that they had)
+                               for ( ; i < stack.length; i++ )
+                                       if ( color( stack[ i ] ) ) {
+                                               swap[ i ] = stack[ i ].style.display;
+                                               stack[ i ].style.display = "block";
+                                       }
+
+                               // Since we flip the display style, we have to handle that
+                               // one special, otherwise get the value
+                               ret = name == "display" && swap[ stack.length - 1 ] != null ?
+                                       "none" :
+                                       ( computedStyle && computedStyle.getPropertyValue( name ) ) || "";
+
+                               // Finally, revert the display styles back
+                               for ( i = 0; i < swap.length; i++ )
+                                       if ( swap[ i ] != null )
+                                               stack[ i ].style.display = swap[ i ];
+                       }
+
+                       // We should always get a number back from opacity
+                       if ( name == "opacity" && ret == "" )
+                               ret = "1";
+
+               } else if ( elem.currentStyle ) {
+                       var camelCase = name.replace(/\-(\w)/g, function(all, letter){
+                               return letter.toUpperCase();
+                       });
+
+                       ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ];
+
+                       // From the awesome hack by Dean Edwards
+                       // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
+
+                       // If we're not dealing with a regular pixel number
+                       // but a number that has a weird ending, we need to convert it to pixels
+                       if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) {
+                               // Remember the original values
+                               var left = style.left, rsLeft = elem.runtimeStyle.left;
+
+                               // Put in the new values to get a computed value out
+                               elem.runtimeStyle.left = elem.currentStyle.left;
+                               style.left = ret || 0;
+                               ret = style.pixelLeft + "px";
+
+                               // Revert the changed values
+                               style.left = left;
+                               elem.runtimeStyle.left = rsLeft;
+                       }
+               }
+
+               return ret;
+       },
+
+       clean: function( elems, context ) {
+               var ret = [];
+               context = context || document;
+               // !context.createElement fails in IE with an error but returns typeof 'object'
+               if (typeof context.createElement == 'undefined')
+                       context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
+
+               jQuery.each(elems, function(i, elem){
+                       if ( !elem )
+                               return;
+
+                       if ( elem.constructor == Number )
+                               elem += '';
+
+                       // Convert html string into DOM nodes
+                       if ( typeof elem == "string" ) {
+                               // Fix "XHTML"-style tags in all browsers
+                               elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){
+                                       return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ?
+                                               all :
+                                               front + "></" + tag + ">";
+                               });
+
+                               // Trim whitespace, otherwise indexOf won't work as expected
+                               var tags = jQuery.trim( elem ).toLowerCase(), div = context.createElement("div");
+
+                               var wrap =
+                                       // option or optgroup
+                                       !tags.indexOf("<opt") &&
+                                       [ 1, "<select multiple='multiple'>", "</select>" ] ||
+
+                                       !tags.indexOf("<leg") &&
+                                       [ 1, "<fieldset>", "</fieldset>" ] ||
+
+                                       tags.match(/^<(thead|tbody|tfoot|colg|cap)/) &&
+                                       [ 1, "<table>", "</table>" ] ||
+
+                                       !tags.indexOf("<tr") &&
+                                       [ 2, "<table><tbody>", "</tbody></table>" ] ||
+
+                                       // <thead> matched above
+                                       (!tags.indexOf("<td") || !tags.indexOf("<th")) &&
+                                       [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ] ||
+
+                                       !tags.indexOf("<col") &&
+                                       [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ] ||
+
+                                       // IE can't serialize <link> and <script> tags normally
+                                       jQuery.browser.msie &&
+                                       [ 1, "div<div>", "</div>" ] ||
+
+                                       [ 0, "", "" ];
+
+                               // Go to html and back, then peel off extra wrappers
+                               div.innerHTML = wrap[1] + elem + wrap[2];
+
+                               // Move to the right depth
+                               while ( wrap[0]-- )
+                                       div = div.lastChild;
+
+                               // Remove IE's autoinserted <tbody> from table fragments
+                               if ( jQuery.browser.msie ) {
+
+                                       // String was a <table>, *may* have spurious <tbody>
+                                       var tbody = !tags.indexOf("<table") && tags.indexOf("<tbody") < 0 ?
+                                               div.firstChild && div.firstChild.childNodes :
+
+                                               // String was a bare <thead> or <tfoot>
+                                               wrap[1] == "<table>" && tags.indexOf("<tbody") < 0 ?
+                                                       div.childNodes :
+                                                       [];
+
+                                       for ( var j = tbody.length - 1; j >= 0 ; --j )
+                                               if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length )
+                                                       tbody[ j ].parentNode.removeChild( tbody[ j ] );
+
+                                       // IE completely kills leading whitespace when innerHTML is used
+                                       if ( /^\s/.test( elem ) )
+                                               div.insertBefore( context.createTextNode( elem.match(/^\s*/)[0] ), div.firstChild );
+
+                               }
+
+                               elem = jQuery.makeArray( div.childNodes );
+                       }
+
+                       if ( elem.length === 0 && (!jQuery.nodeName( elem, "form" ) && !jQuery.nodeName( elem, "select" )) )
+                               return;
+
+                       if ( elem[0] == undefined || jQuery.nodeName( elem, "form" ) || elem.options )
+                               ret.push( elem );
+
+                       else
+                               ret = jQuery.merge( ret, elem );
+
+               });
+
+               return ret;
+       },
+
+       attr: function( elem, name, value ) {
+               // don't set attributes on text and comment nodes
+               if (!elem || elem.nodeType == 3 || elem.nodeType == 8)
+                       return undefined;
+
+               var notxml = !jQuery.isXMLDoc( elem ),
+                       // Whether we are setting (or getting)
+                       set = value !== undefined,
+                       msie = jQuery.browser.msie;
+
+               // Try to normalize/fix the name
+               name = notxml && jQuery.props[ name ] || name;
+
+               // Only do all the following if this is a node (faster for style)
+               // IE elem.getAttribute passes even for style
+               if ( elem.tagName ) {
+
+                       // These attributes require special treatment
+                       var special = /href|src|style/.test( name );
+
+                       // Safari mis-reports the default selected property of a hidden option
+                       // Accessing the parent's selectedIndex property fixes it
+                       if ( name == "selected" && jQuery.browser.safari )
+                               elem.parentNode.selectedIndex;
+
+                       // If applicable, access the attribute via the DOM 0 way
+                       if ( name in elem && notxml && !special ) {
+                               if ( set ){
+                                       // We can't allow the type property to be changed (since it causes problems in IE)
+                                       if ( name == "type" && jQuery.nodeName( elem, "input" ) && elem.parentNode )
+                                               throw "type property can't be changed";
+
+                                       elem[ name ] = value;
+                               }
+
+                               // browsers index elements by id/name on forms, give priority to attributes.
+                               if( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) )
+                                       return elem.getAttributeNode( name ).nodeValue;
+
+                               return elem[ name ];
+                       }
+
+                       if ( msie && notxml &&  name == "style" )
+                               return jQuery.attr( elem.style, "cssText", value );
+
+                       if ( set )
+                               // convert the value to a string (all browsers do this but IE) see #1070
+                               elem.setAttribute( name, "" + value );
+
+                       var attr = msie && notxml && special
+                                       // Some attributes require a special call on IE
+                                       ? elem.getAttribute( name, 2 )
+                                       : elem.getAttribute( name );
+
+                       // Non-existent attributes return null, we normalize to undefined
+                       return attr === null ? undefined : attr;
+               }
+
+               // elem is actually elem.style ... set the style
+
+               // IE uses filters for opacity
+               if ( msie && name == "opacity" ) {
+                       if ( set ) {
+                               // IE has trouble with opacity if it does not have layout
+                               // Force it by setting the zoom level
+                               elem.zoom = 1;
+
+                               // Set the alpha filter to set the opacity
+                               elem.filter = (elem.filter || "").replace( /alpha\([^)]*\)/, "" ) +
+                                       (parseInt( value ) + '' == "NaN" ? "" : "alpha(opacity=" + value * 100 + ")");
+                       }
+
+                       return elem.filter && elem.filter.indexOf("opacity=") >= 0 ?
+                               (parseFloat( elem.filter.match(/opacity=([^)]*)/)[1] ) / 100) + '':
+                               "";
+               }
+
+               name = name.replace(/-([a-z])/ig, function(all, letter){
+                       return letter.toUpperCase();
+               });
+
+               if ( set )
+                       elem[ name ] = value;
+
+               return elem[ name ];
+       },
+
+       trim: function( text ) {
+               return (text || "").replace( /^\s+|\s+$/g, "" );
+       },
+
+       makeArray: function( array ) {
+               var ret = [];
+
+               if( array != null ){
+                       var i = array.length;
+                       //the window, strings and functions also have 'length'
+                       if( i == null || array.split || array.setInterval || array.call )
+                               ret[0] = array;
+                       else
+                               while( i )
+                                       ret[--i] = array[i];
+               }
+
+               return ret;
+       },
+
+       inArray: function( elem, array ) {
+               for ( var i = 0, length = array.length; i < length; i++ )
+               // Use === because on IE, window == document
+                       if ( array[ i ] === elem )
+                               return i;
+
+               return -1;
+       },
+
+       merge: function( first, second ) {
+               // We have to loop this way because IE & Opera overwrite the length
+               // expando of getElementsByTagName
+               var i = 0, elem, pos = first.length;
+               // Also, we need to make sure that the correct elements are being returned
+               // (IE returns comment nodes in a '*' query)
+               if ( jQuery.browser.msie ) {
+                       while ( elem = second[ i++ ] )
+                               if ( elem.nodeType != 8 )
+                                       first[ pos++ ] = elem;
+
+               } else
+                       while ( elem = second[ i++ ] )
+                               first[ pos++ ] = elem;
+
+               return first;
+       },
+
+       unique: function( array ) {
+               var ret = [], done = {};
+
+               try {
+
+                       for ( var i = 0, length = array.length; i < length; i++ ) {
+                               var id = jQuery.data( array[ i ] );
+
+                               if ( !done[ id ] ) {
+                                       done[ id ] = true;
+                                       ret.push( array[ i ] );
+                               }
+                       }
+
+               } catch( e ) {
+                       ret = array;
+               }
+
+               return ret;
+       },
+
+       grep: function( elems, callback, inv ) {
+               var ret = [];
+
+               // Go through the array, only saving the items
+               // that pass the validator function
+               for ( var i = 0, length = elems.length; i < length; i++ )
+                       if ( !inv != !callback( elems[ i ], i ) )
+                               ret.push( elems[ i ] );
+
+               return ret;
+       },
+
+       map: function( elems, callback ) {
+               var ret = [];
+
+               // Go through the array, translating each of the items to their
+               // new value (or values).
+               for ( var i = 0, length = elems.length; i < length; i++ ) {
+                       var value = callback( elems[ i ], i );
+
+                       if ( value != null )
+                               ret[ ret.length ] = value;
+               }
+
+               return ret.concat.apply( [], ret );
+       }
+});
+
+var userAgent = navigator.userAgent.toLowerCase();
+
+// Figure out what browser is being used
+jQuery.browser = {
+       version: (userAgent.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [])[1],
+       safari: /webkit/.test( userAgent ),
+       opera: /opera/.test( userAgent ),
+       msie: /msie/.test( userAgent ) && !/opera/.test( userAgent ),
+       mozilla: /mozilla/.test( userAgent ) && !/(compatible|webkit)/.test( userAgent )
+};
+
+var styleFloat = jQuery.browser.msie ?
+       "styleFloat" :
+       "cssFloat";
+
+jQuery.extend({
+       // Check to see if the W3C box model is being used
+       boxModel: !jQuery.browser.msie || document.compatMode == "CSS1Compat",
+
+       props: {
+               "for": "htmlFor",
+               "class": "className",
+               "float": styleFloat,
+               cssFloat: styleFloat,
+               styleFloat: styleFloat,
+               readonly: "readOnly",
+               maxlength: "maxLength",
+               cellspacing: "cellSpacing",
+               rowspan: "rowSpan"
+       }
+});
+
+jQuery.each({
+       parent: function(elem){return elem.parentNode;},
+       parents: function(elem){return jQuery.dir(elem,"parentNode");},
+       next: function(elem){return jQuery.nth(elem,2,"nextSibling");},
+       prev: function(elem){return jQuery.nth(elem,2,"previousSibling");},
+       nextAll: function(elem){return jQuery.dir(elem,"nextSibling");},
+       prevAll: function(elem){return jQuery.dir(elem,"previousSibling");},
+       siblings: function(elem){return jQuery.sibling(elem.parentNode.firstChild,elem);},
+       children: function(elem){return jQuery.sibling(elem.firstChild);},
+       contents: function(elem){return jQuery.nodeName(elem,"iframe")?elem.contentDocument||elem.contentWindow.document:jQuery.makeArray(elem.childNodes);}
+}, function(name, fn){
+       jQuery.fn[ name ] = function( selector ) {
+               var ret = jQuery.map( this, fn );
+
+               if ( selector && typeof selector == "string" )
+                       ret = jQuery.multiFilter( selector, ret );
+
+               return this.pushStack( jQuery.unique( ret ) );
+       };
+});
+
+jQuery.each({
+       appendTo: "append",
+       prependTo: "prepend",
+       insertBefore: "before",
+       insertAfter: "after",
+       replaceAll: "replaceWith"
+}, function(name, original){
+       jQuery.fn[ name ] = function() {
+               var args = arguments;
+
+               return this.each(function(){
+                       for ( var i = 0, length = args.length; i < length; i++ )
+                               jQuery( args[ i ] )[ original ]( this );
+               });
+       };
+});
+
+jQuery.each({
+       removeAttr: function( name ) {
+               jQuery.attr( this, name, "" );
+               if (this.nodeType == 1)
+                       this.removeAttribute( name );
+       },
+
+       addClass: function( classNames ) {
+               jQuery.className.add( this, classNames );
+       },
+
+       removeClass: function( classNames ) {
+               jQuery.className.remove( this, classNames );
+       },
+
+       toggleClass: function( classNames ) {
+               jQuery.className[ jQuery.className.has( this, classNames ) ? "remove" : "add" ]( this, classNames );
+       },
+
+       remove: function( selector ) {
+               if ( !selector || jQuery.filter( selector, [ this ] ).r.length ) {
+                       // Prevent memory leaks
+                       jQuery( "*", this ).add(this).each(function(){
+                               jQuery.event.remove(this);
+                               jQuery.removeData(this);
+                       });
+                       if (this.parentNode)
+                               this.parentNode.removeChild( this );
+               }
+       },
+
+       empty: function() {
+               // Remove element nodes and prevent memory leaks
+               jQuery( ">*", this ).remove();
+
+               // Remove any remaining nodes
+               while ( this.firstChild )
+                       this.removeChild( this.firstChild );
+       }
+}, function(name, fn){
+       jQuery.fn[ name ] = function(){
+               return this.each( fn, arguments );
+       };
+});
+
+jQuery.each([ "Height", "Width" ], function(i, name){
+       var type = name.toLowerCase();
+
+       jQuery.fn[ type ] = function( size ) {
+               // Get window width or height
+               return this[0] == window ?
+                       // Opera reports document.body.client[Width/Height] properly in both quirks and standards
+                       jQuery.browser.opera && document.body[ "client" + name ] ||
+
+                       // Safari reports inner[Width/Height] just fine (Mozilla and Opera include scroll bar widths)
+                       jQuery.browser.safari && window[ "inner" + name ] ||
+
+                       // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode
+                       document.compatMode == "CSS1Compat" && document.documentElement[ "client" + name ] || document.body[ "client" + name ] :
+
+                       // Get document width or height
+                       this[0] == document ?
+                               // Either scroll[Width/Height] or offset[Width/Height], whichever is greater
+                               Math.max(
+                                       Math.max(document.body["scroll" + name], document.documentElement["scroll" + name]),
+                                       Math.max(document.body["offset" + name], document.documentElement["offset" + name])
+                               ) :
+
+                               // Get or set width or height on the element
+                               size == undefined ?
+                                       // Get width or height on the element
+                                       (this.length ? jQuery.css( this[0], type ) : null) :
+
+                                       // Set the width or height on the element (default to pixels if value is unitless)
+                                       this.css( type, size.constructor == String ? size : size + "px" );
+       };
+});
+
+// Helper function used by the dimensions and offset modules
+function num(elem, prop) {
+       return elem[0] && parseInt( jQuery.curCSS(elem[0], prop, true), 10 ) || 0;
+}var chars = jQuery.browser.safari && parseInt(jQuery.browser.version) < 417 ?
+               "(?:[\\w*_-]|\\\\.)" :
+               "(?:[\\w\u0128-\uFFFF*_-]|\\\\.)",
+       quickChild = new RegExp("^>\\s*(" + chars + "+)"),
+       quickID = new RegExp("^(" + chars + "+)(#)(" + chars + "+)"),
+       quickClass = new RegExp("^([#.]?)(" + chars + "*)");
+
+jQuery.extend({
+       expr: {
+               "": function(a,i,m){return m[2]=="*"||jQuery.nodeName(a,m[2]);},
+               "#": function(a,i,m){return a.getAttribute("id")==m[2];},
+               ":": {
+                       // Position Checks
+                       lt: function(a,i,m){return i<m[3]-0;},
+                       gt: function(a,i,m){return i>m[3]-0;},
+                       nth: function(a,i,m){return m[3]-0==i;},
+                       eq: function(a,i,m){return m[3]-0==i;},
+                       first: function(a,i){return i==0;},
+                       last: function(a,i,m,r){return i==r.length-1;},
+                       even: function(a,i){return i%2==0;},
+                       odd: function(a,i){return i%2;},
+
+                       // Child Checks
+                       "first-child": function(a){return a.parentNode.getElementsByTagName("*")[0]==a;},
+                       "last-child": function(a){return jQuery.nth(a.parentNode.lastChild,1,"previousSibling")==a;},
+                       "only-child": function(a){return !jQuery.nth(a.parentNode.lastChild,2,"previousSibling");},
+
+                       // Parent Checks
+                       parent: function(a){return a.firstChild;},
+                       empty: function(a){return !a.firstChild;},
+
+                       // Text Check
+                       contains: function(a,i,m){return (a.textContent||a.innerText||jQuery(a).text()||"").indexOf(m[3])>=0;},
+
+                       // Visibility
+                       visible: function(a){return "hidden"!=a.type&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden";},
+                       hidden: function(a){return "hidden"==a.type||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden";},
+
+                       // Form attributes
+                       enabled: function(a){return !a.disabled;},
+                       disabled: function(a){return a.disabled;},
+                       checked: function(a){return a.checked;},
+                       selected: function(a){return a.selected||jQuery.attr(a,"selected");},
+
+                       // Form elements
+                       text: function(a){return "text"==a.type;},
+                       radio: function(a){return "radio"==a.type;},
+                       checkbox: function(a){return "checkbox"==a.type;},
+                       file: function(a){return "file"==a.type;},
+                       password: function(a){return "password"==a.type;},
+                       submit: function(a){return "submit"==a.type;},
+                       image: function(a){return "image"==a.type;},
+                       reset: function(a){return "reset"==a.type;},
+                       button: function(a){return "button"==a.type||jQuery.nodeName(a,"button");},
+                       input: function(a){return /input|select|textarea|button/i.test(a.nodeName);},
+
+                       // :has()
+                       has: function(a,i,m){return jQuery.find(m[3],a).length;},
+
+                       // :header
+                       header: function(a){return /h\d/i.test(a.nodeName);},
+
+                       // :animated
+                       animated: function(a){return jQuery.grep(jQuery.timers,function(fn){return a==fn.elem;}).length;}
+               }
+       },
+
+       // The regular expressions that power the parsing engine
+       parse: [
+               // Match: [@value='test'], [@foo]
+               /^(\[) *@?([\w-]+) *([!*$^~=]*) *('?"?)(.*?)\4 *\]/,
+
+               // Match: :contains('foo')
+               /^(:)([\w-]+)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/,
+
+               // Match: :even, :last-child, #id, .class
+               new RegExp("^([:.#]*)(" + chars + "+)")
+       ],
+
+       multiFilter: function( expr, elems, not ) {
+               var old, cur = [];
+
+               while ( expr && expr != old ) {
+                       old = expr;
+                       var f = jQuery.filter( expr, elems, not );
+                       expr = f.t.replace(/^\s*,\s*/, "" );
+                       cur = not ? elems = f.r : jQuery.merge( cur, f.r );
+               }
+
+               return cur;
+       },
+
+       find: function( t, context ) {
+               // Quickly handle non-string expressions
+               if ( typeof t != "string" )
+                       return [ t ];
+
+               // check to make sure context is a DOM element or a document
+               if ( context && context.nodeType != 1 && context.nodeType != 9)
+                       return [ ];
+
+               // Set the correct context (if none is provided)
+               context = context || document;
+
+               // Initialize the search
+               var ret = [context], done = [], last, nodeName;
+
+               // Continue while a selector expression exists, and while
+               // we're no longer looping upon ourselves
+               while ( t && last != t ) {
+                       var r = [];
+                       last = t;
+
+                       t = jQuery.trim(t);
+
+                       var foundToken = false,
+
+                       // An attempt at speeding up child selectors that
+                       // point to a specific element tag
+                               re = quickChild,
+
+                               m = re.exec(t);
+
+                       if ( m ) {
+                               nodeName = m[1].toUpperCase();
+
+                               // Perform our own iteration and filter
+                               for ( var i = 0; ret[i]; i++ )
+                                       for ( var c = ret[i].firstChild; c; c = c.nextSibling )
+                                               if ( c.nodeType == 1 && (nodeName == "*" || c.nodeName.toUpperCase() == nodeName) )
+                                                       r.push( c );
+
+                               ret = r;
+                               t = t.replace( re, "" );
+                               if ( t.indexOf(" ") == 0 ) continue;
+                               foundToken = true;
+                       } else {
+                               re = /^([>+~])\s*(\w*)/i;
+
+                               if ( (m = re.exec(t)) != null ) {
+                                       r = [];
+
+                                       var merge = {};
+                                       nodeName = m[2].toUpperCase();
+                                       m = m[1];
+
+                                       for ( var j = 0, rl = ret.length; j < rl; j++ ) {
+                                               var n = m == "~" || m == "+" ? ret[j].nextSibling : ret[j].firstChild;
+                                               for ( ; n; n = n.nextSibling )
+                                                       if ( n.nodeType == 1 ) {
+                                                               var id = jQuery.data(n);
+
+                                                               if ( m == "~" && merge[id] ) break;
+
+                                                               if (!nodeName || n.nodeName.toUpperCase() == nodeName ) {
+                                                                       if ( m == "~" ) merge[id] = true;
+                                                                       r.push( n );
+                                                               }
+
+                                                               if ( m == "+" ) break;
+                                                       }
+                                       }
+
+                                       ret = r;
+
+                                       // And remove the token
+                                       t = jQuery.trim( t.replace( re, "" ) );
+                                       foundToken = true;
+                               }
+                       }
+
+                       // See if there's still an expression, and that we haven't already
+                       // matched a token
+                       if ( t && !foundToken ) {
+                               // Handle multiple expressions
+                               if ( !t.indexOf(",") ) {
+                                       // Clean the result set
+                                       if ( context == ret[0] ) ret.shift();
+
+                                       // Merge the result sets
+                                       done = jQuery.merge( done, ret );
+
+                                       // Reset the context
+                                       r = ret = [context];
+
+                                       // Touch up the selector string
+                                       t = " " + t.substr(1,t.length);
+
+                               } else {
+                                       // Optimize for the case nodeName#idName
+                                       var re2 = quickID;
+                                       var m = re2.exec(t);
+
+                                       // Re-organize the results, so that they're consistent
+                                       if ( m ) {
+                                               m = [ 0, m[2], m[3], m[1] ];
+
+                                       } else {
+                                               // Otherwise, do a traditional filter check for
+                                               // ID, class, and element selectors
+                                               re2 = quickClass;
+                                               m = re2.exec(t);
+                                       }
+
+                                       m[2] = m[2].replace(/\\/g, "");
+
+                                       var elem = ret[ret.length-1];
+
+                                       // Try to do a global search by ID, where we can
+                                       if ( m[1] == "#" && elem && elem.getElementById && !jQuery.isXMLDoc(elem) ) {
+                                               // Optimization for HTML document case
+                                               var oid = elem.getElementById(m[2]);
+
+                                               // Do a quick check for the existence of the actual ID attribute
+                                               // to avoid selecting by the name attribute in IE
+                                               // also check to insure id is a string to avoid selecting an element with the name of 'id' inside a form
+                                               if ( (jQuery.browser.msie||jQuery.browser.opera) && oid && typeof oid.id == "string" && oid.id != m[2] )
+                                                       oid = jQuery('[@id="'+m[2]+'"]', elem)[0];
+
+                                               // Do a quick check for node name (where applicable) so
+                                               // that div#foo searches will be really fast
+                                               ret = r = oid && (!m[3] || jQuery.nodeName(oid, m[3])) ? [oid] : [];
+                                       } else {
+                                               // We need to find all descendant elements
+                                               for ( var i = 0; ret[i]; i++ ) {
+                                                       // Grab the tag name being searched for
+                                                       var tag = m[1] == "#" && m[3] ? m[3] : m[1] != "" || m[0] == "" ? "*" : m[2];
+
+                                                       // Handle IE7 being really dumb about <object>s
+                                                       if ( tag == "*" && ret[i].nodeName.toLowerCase() == "object" )
+                                                               tag = "param";
+
+                                                       r = jQuery.merge( r, ret[i].getElementsByTagName( tag ));
+                                               }
+
+                                               // It's faster to filter by class and be done with it
+                                               if ( m[1] == "." )
+                                                       r = jQuery.classFilter( r, m[2] );
+
+                                               // Same with ID filtering
+                                               if ( m[1] == "#" ) {
+                                                       var tmp = [];
+
+                                                       // Try to find the element with the ID
+                                                       for ( var i = 0; r[i]; i++ )
+                                                               if ( r[i].getAttribute("id") == m[2] ) {
+                                                                       tmp = [ r[i] ];
+                                                                       break;
+                                                               }
+
+                                                       r = tmp;
+                                               }
+
+                                               ret = r;
+                                       }
+
+                                       t = t.replace( re2, "" );
+                               }
+
+                       }
+
+                       // If a selector string still exists
+                       if ( t ) {
+                               // Attempt to filter it
+                               var val = jQuery.filter(t,r);
+                               ret = r = val.r;
+                               t = jQuery.trim(val.t);
+                       }
+               }
+
+               // An error occurred with the selector;
+               // just return an empty set instead
+               if ( t )
+                       ret = [];
+
+               // Remove the root context
+               if ( ret && context == ret[0] )
+                       ret.shift();
+
+               // And combine the results
+               done = jQuery.merge( done, ret );
+
+               return done;
+       },
+
+       classFilter: function(r,m,not){
+               m = " " + m + " ";
+               var tmp = [];
+               for ( var i = 0; r[i]; i++ ) {
+                       var pass = (" " + r[i].className + " ").indexOf( m ) >= 0;
+                       if ( !not && pass || not && !pass )
+                               tmp.push( r[i] );
+               }
+               return tmp;
+       },
+
+       filter: function(t,r,not) {
+               var last;
+
+               // Look for common filter expressions
+               while ( t && t != last ) {
+                       last = t;
+
+                       var p = jQuery.parse, m;
+
+                       for ( var i = 0; p[i]; i++ ) {
+                               m = p[i].exec( t );
+
+                               if ( m ) {
+                                       // Remove what we just matched
+                                       t = t.substring( m[0].length );
+
+                                       m[2] = m[2].replace(/\\/g, "");
+                                       break;
+                               }
+                       }
+
+                       if ( !m )
+                               break;
+
+                       // :not() is a special case that can be optimized by
+                       // keeping it out of the expression list
+                       if ( m[1] == ":" && m[2] == "not" )
+                               // optimize if only one selector found (most common case)
+                               r = isSimple.test( m[3] ) ?
+                                       jQuery.filter(m[3], r, true).r :
+                                       jQuery( r ).not( m[3] );
+
+                       // We can get a big speed boost by filtering by class here
+                       else if ( m[1] == "." )
+                               r = jQuery.classFilter(r, m[2], not);
+
+                       else if ( m[1] == "[" ) {
+                               var tmp = [], type = m[3];
+
+                               for ( var i = 0, rl = r.length; i < rl; i++ ) {
+                                       var a = r[i], z = a[ jQuery.props[m[2]] || m[2] ];
+
+                                       if ( z == null || /href|src|selected/.test(m[2]) )
+                                               z = jQuery.attr(a,m[2]) || '';
+
+                                       if ( (type == "" && !!z ||
+                                                type == "=" && z == m[5] ||
+                                                type == "!=" && z != m[5] ||
+                                                type == "^=" && z && !z.indexOf(m[5]) ||
+                                                type == "$=" && z.substr(z.length - m[5].length) == m[5] ||
+                                                (type == "*=" || type == "~=") && z.indexOf(m[5]) >= 0) ^ not )
+                                                       tmp.push( a );
+                               }
+
+                               r = tmp;
+
+                       // We can get a speed boost by handling nth-child here
+                       } else if ( m[1] == ":" && m[2] == "nth-child" ) {
+                               var merge = {}, tmp = [],
+                                       // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
+                                       test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec(
+                                               m[3] == "even" && "2n" || m[3] == "odd" && "2n+1" ||
+                                               !/\D/.test(m[3]) && "0n+" + m[3] || m[3]),
+                                       // calculate the numbers (first)n+(last) including if they are negative
+                                       first = (test[1] + (test[2] || 1)) - 0, last = test[3] - 0;
+
+                               // loop through all the elements left in the jQuery object
+                               for ( var i = 0, rl = r.length; i < rl; i++ ) {
+                                       var node = r[i], parentNode = node.parentNode, id = jQuery.data(parentNode);
+
+                                       if ( !merge[id] ) {
+                                               var c = 1;
+
+                                               for ( var n = parentNode.firstChild; n; n = n.nextSibling )
+                                                       if ( n.nodeType == 1 )
+                                                               n.nodeIndex = c++;
+
+                                               merge[id] = true;
+                                       }
+
+                                       var add = false;
+
+                                       if ( first == 0 ) {
+                                               if ( node.nodeIndex == last )
+                                                       add = true;
+                                       } else if ( (node.nodeIndex - last) % first == 0 && (node.nodeIndex - last) / first >= 0 )
+                                               add = true;
+
+                                       if ( add ^ not )
+                                               tmp.push( node );
+                               }
+
+                               r = tmp;
+
+                       // Otherwise, find the expression to execute
+                       } else {
+                               var fn = jQuery.expr[ m[1] ];
+                               if ( typeof fn == "object" )
+                                       fn = fn[ m[2] ];
+
+                               if ( typeof fn == "string" )
+                                       fn = eval("false||function(a,i){return " + fn + ";}");
+
+                               // Execute it against the current filter
+                               r = jQuery.grep( r, function(elem, i){
+                                       return fn(elem, i, m, r);
+                               }, not );
+                       }
+               }
+
+               // Return an array of filtered elements (r)
+               // and the modified expression string (t)
+               return { r: r, t: t };
+       },
+
+       dir: function( elem, dir ){
+               var matched = [],
+                       cur = elem[dir];
+               while ( cur && cur != document ) {
+                       if ( cur.nodeType == 1 )
+                               matched.push( cur );
+                       cur = cur[dir];
+               }
+               return matched;
+       },
+
+       nth: function(cur,result,dir,elem){
+               result = result || 1;
+               var num = 0;
+
+               for ( ; cur; cur = cur[dir] )
+                       if ( cur.nodeType == 1 && ++num == result )
+                               break;
+
+               return cur;
+       },
+
+       sibling: function( n, elem ) {
+               var r = [];
+
+               for ( ; n; n = n.nextSibling ) {
+                       if ( n.nodeType == 1 && n != elem )
+                               r.push( n );
+               }
+
+               return r;
+       }
+});
+/*
+ * A number of helper functions used for managing events.
+ * Many of the ideas behind this code orignated from
+ * Dean Edwards' addEvent library.
+ */
+jQuery.event = {
+
+       // Bind an event to an element
+       // Original by Dean Edwards
+       add: function(elem, types, handler, data) {
+               if ( elem.nodeType == 3 || elem.nodeType == 8 )
+                       return;
+
+               // For whatever reason, IE has trouble passing the window object
+               // around, causing it to be cloned in the process
+               if ( jQuery.browser.msie && elem.setInterval )
+                       elem = window;
+
+               // Make sure that the function being executed has a unique ID
+               if ( !handler.guid )
+                       handler.guid = this.guid++;
+
+               // if data is passed, bind to handler
+               if( data != undefined ) {
+                       // Create temporary function pointer to original handler
+                       var fn = handler;
+
+                       // Create unique handler function, wrapped around original handler
+                       handler = this.proxy( fn, function() {
+                               // Pass arguments and context to original handler
+                               return fn.apply(this, arguments);
+                       });
+
+                       // Store data in unique handler
+                       handler.data = data;
+               }
+
+               // Init the element's event structure
+               var events = jQuery.data(elem, "events") || jQuery.data(elem, "events", {}),
+                       handle = jQuery.data(elem, "handle") || jQuery.data(elem, "handle", function(){
+                               // Handle the second event of a trigger and when
+                               // an event is called after a page has unloaded
+                               if ( typeof jQuery != "undefined" && !jQuery.event.triggered )
+                                       return jQuery.event.handle.apply(arguments.callee.elem, arguments);
+                       });
+               // Add elem as a property of the handle function
+               // This is to prevent a memory leak with non-native
+               // event in IE.
+               handle.elem = elem;
+
+               // Handle multiple events separated by a space
+               // jQuery(...).bind("mouseover mouseout", fn);
+               jQuery.each(types.split(/\s+/), function(index, type) {
+                       // Namespaced event handlers
+                       var parts = type.split(".");
+                       type = parts[0];
+                       handler.type = parts[1];
+
+                       // Get the current list of functions bound to this event
+                       var handlers = events[type];
+
+                       // Init the event handler queue
+                       if (!handlers) {
+                               handlers = events[type] = {};
+
+                               // Check for a special event handler
+                               // Only use addEventListener/attachEvent if the special
+                               // events handler returns false
+                               if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem) === false ) {
+                                       // Bind the global event handler to the element
+                                       if (elem.addEventListener)
+                                               elem.addEventListener(type, handle, false);
+                                       else if (elem.attachEvent)
+                                               elem.attachEvent("on" + type, handle);
+                               }
+                       }
+
+                       // Add the function to the element's handler list
+                       handlers[handler.guid] = handler;
+
+                       // Keep track of which events have been used, for global triggering
+                       jQuery.event.global[type] = true;
+               });
+
+               // Nullify elem to prevent memory leaks in IE
+               elem = null;
+       },
+
+       guid: 1,
+       global: {},
+
+       // Detach an event or set of events from an element
+       remove: function(elem, types, handler) {
+               // don't do events on text and comment nodes
+               if ( elem.nodeType == 3 || elem.nodeType == 8 )
+                       return;
+
+               var events = jQuery.data(elem, "events"), ret, index;
+
+               if ( events ) {
+                       // Unbind all events for the element
+                       if ( types == undefined || (typeof types == "string" && types.charAt(0) == ".") )
+                               for ( var type in events )
+                                       this.remove( elem, type + (types || "") );
+                       else {
+                               // types is actually an event object here
+                               if ( types.type ) {
+                                       handler = types.handler;
+                                       types = types.type;
+                               }
+
+                               // Handle multiple events seperated by a space
+                               // jQuery(...).unbind("mouseover mouseout", fn);
+                               jQuery.each(types.split(/\s+/), function(index, type){
+                                       // Namespaced event handlers
+                                       var parts = type.split(".");
+                                       type = parts[0];
+
+                                       if ( events[type] ) {
+                                               // remove the given handler for the given type
+                                               if ( handler )
+                                                       delete events[type][handler.guid];
+
+                                               // remove all handlers for the given type
+                                               else
+                                                       for ( handler in events[type] )
+                                                               // Handle the removal of namespaced events
+                                                               if ( !parts[1] || events[type][handler].type == parts[1] )
+                                                                       delete events[type][handler];
+
+                                               // remove generic event handler if no more handlers exist
+                                               for ( ret in events[type] ) break;
+                                               if ( !ret ) {
+                                                       if ( !jQuery.event.special[type] || jQuery.event.special[type].teardown.call(elem) === false ) {
+                                                               if (elem.removeEventListener)
+                                                                       elem.removeEventListener(type, jQuery.data(elem, "handle"), false);
+                                                               else if (elem.detachEvent)
+                                                                       elem.detachEvent("on" + type, jQuery.data(elem, "handle"));
+                                                       }
+                                                       ret = null;
+                                                       delete events[type];
+                                               }
+                                       }
+                               });
+                       }
+
+                       // Remove the expando if it's no longer used
+                       for ( ret in events ) break;
+                       if ( !ret ) {
+                               var handle = jQuery.data( elem, "handle" );
+                               if ( handle ) handle.elem = null;
+                               jQuery.removeData( elem, "events" );
+                               jQuery.removeData( elem, "handle" );
+                       }
+               }
+       },
+
+       trigger: function(type, data, elem, donative, extra) {
+               // Clone the incoming data, if any
+               data = jQuery.makeArray(data);
+
+               if ( type.indexOf("!") >= 0 ) {
+                       type = type.slice(0, -1);
+                       var exclusive = true;
+               }
+
+               // Handle a global trigger
+               if ( !elem ) {
+                       // Only trigger if we've ever bound an event for it
+                       if ( this.global[type] )
+                               jQuery("*").add([window, document]).trigger(type, data);
+
+               // Handle triggering a single element
+               } else {
+                       // don't do events on text and comment nodes
+                       if ( elem.nodeType == 3 || elem.nodeType == 8 )
+                               return undefined;
+
+                       var val, ret, fn = jQuery.isFunction( elem[ type ] || null ),
+                               // Check to see if we need to provide a fake event, or not
+                               event = !data[0] || !data[0].preventDefault;
+
+                       // Pass along a fake event
+                       if ( event ) {
+                               data.unshift({
+                                       type: type,
+                                       target: elem,
+                                       preventDefault: function(){},
+                                       stopPropagation: function(){},
+                                       timeStamp: now()
+                               });
+                               data[0][expando] = true; // no need to fix fake event
+                       }
+
+                       // Enforce the right trigger type
+                       data[0].type = type;
+                       if ( exclusive )
+                               data[0].exclusive = true;
+
+                       // Trigger the event, it is assumed that "handle" is a function
+                       var handle = jQuery.data(elem, "handle");
+                       if ( handle )
+                               val = handle.apply( elem, data );
+
+                       // Handle triggering native .onfoo handlers (and on links since we don't call .click() for links)
+                       if ( (!fn || (jQuery.nodeName(elem, 'a') && type == "click")) && elem["on"+type] && elem["on"+type].apply( elem, data ) === false )
+                               val = false;
+
+                       // Extra functions don't get the custom event object
+                       if ( event )
+                               data.shift();
+
+                       // Handle triggering of extra function
+                       if ( extra && jQuery.isFunction( extra ) ) {
+                               // call the extra function and tack the current return value on the end for possible inspection
+                               ret = extra.apply( elem, val == null ? data : data.concat( val ) );
+                               // if anything is returned, give it precedence and have it overwrite the previous value
+                               if (ret !== undefined)
+                                       val = ret;
+                       }
+
+                       // Trigger the native events (except for clicks on links)
+                       if ( fn && donative !== false && val !== false && !(jQuery.nodeName(elem, 'a') && type == "click") ) {
+                               this.triggered = true;
+                               try {
+                                       elem[ type ]();
+                               // prevent IE from throwing an error for some hidden elements
+                               } catch (e) {}
+                       }
+
+                       this.triggered = false;
+               }
+
+               return val;
+       },
+
+       handle: function(event) {
+               // returned undefined or false
+               var val, ret, namespace, all, handlers;
+
+               event = arguments[0] = jQuery.event.fix( event || window.event );
+
+               // Namespaced event handlers
+               namespace = event.type.split(".");
+               event.type = namespace[0];
+               namespace = namespace[1];
+               // Cache this now, all = true means, any handler
+               all = !namespace && !event.exclusive;
+
+               handlers = ( jQuery.data(this, "events") || {} )[event.type];
+
+               for ( var j in handlers ) {
+                       var handler = handlers[j];
+
+                       // Filter the functions by class
+                       if ( all || handler.type == namespace ) {
+                               // Pass in a reference to the handler function itself
+                               // So that we can later remove it
+                               event.handler = handler;
+                               event.data = handler.data;
+
+                               ret = handler.apply( this, arguments );
+
+                               if ( val !== false )
+                                       val = ret;
+
+                               if ( ret === false ) {
+                                       event.preventDefault();
+                                       event.stopPropagation();
+                               }
+                       }
+               }
+
+               return val;
+       },
+
+       props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target timeStamp toElement type view wheelDelta which".split(" "),
+
+       fix: function(event) {
+               if ( event[expando] == true )
+                       return event;
+
+               // store a copy of the original event object
+               // and "clone" to set read-only properties
+               var originalEvent = event;
+               event = { originalEvent: originalEvent };
+
+               for ( var i = this.props.length, prop; i; ){
+                       prop = this.props[ --i ];
+                       event[ prop ] = originalEvent[ prop ];
+               }
+
+               // Mark it as fixed
+               event[expando] = true;
+
+               // add preventDefault and stopPropagation since
+               // they will not work on the clone
+               event.preventDefault = function() {
+                       // if preventDefault exists run it on the original event
+                       if (originalEvent.preventDefault)
+                               originalEvent.preventDefault();
+                       // otherwise set the returnValue property of the original event to false (IE)
+                       originalEvent.returnValue = false;
+               };
+               event.stopPropagation = function() {
+                       // if stopPropagation exists run it on the original event
+                       if (originalEvent.stopPropagation)
+                               originalEvent.stopPropagation();
+                       // otherwise set the cancelBubble property of the original event to true (IE)
+                       originalEvent.cancelBubble = true;
+               };
+
+               // Fix timeStamp
+               event.timeStamp = event.timeStamp || now();
+
+               // Fix target property, if necessary
+               if ( !event.target )
+                       event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either
+
+               // check if target is a textnode (safari)
+               if ( event.target.nodeType == 3 )
+                       event.target = event.target.parentNode;
+
+               // Add relatedTarget, if necessary
+               if ( !event.relatedTarget && event.fromElement )
+                       event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;
+
+               // Calculate pageX/Y if missing and clientX/Y available
+               if ( event.pageX == null && event.clientX != null ) {
+                       var doc = document.documentElement, body = document.body;
+                       event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0);
+                       event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0);
+               }
+
+               // Add which for key events
+               if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) )
+                       event.which = event.charCode || event.keyCode;
+
+               // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
+               if ( !event.metaKey && event.ctrlKey )
+                       event.metaKey = event.ctrlKey;
+
+               // Add which for click: 1 == left; 2 == middle; 3 == right
+               // Note: button is not normalized, so don't use it
+               if ( !event.which && event.button )
+                       event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
+
+               return event;
+       },
+
+       proxy: function( fn, proxy ){
+               // Set the guid of unique handler to the same of original handler, so it can be removed
+               proxy.guid = fn.guid = fn.guid || proxy.guid || this.guid++;
+               // So proxy can be declared as an argument
+               return proxy;
+       },
+
+       special: {
+               ready: {
+                       setup: function() {
+                               // Make sure the ready event is setup
+                               bindReady();
+                               return;
+                       },
+
+                       teardown: function() { return; }
+               },
+
+               mouseenter: {
+                       setup: function() {
+                               if ( jQuery.browser.msie ) return false;
+                               jQuery(this).bind("mouseover", jQuery.event.special.mouseenter.handler);
+                               return true;
+                       },
+
+                       teardown: function() {
+                               if ( jQuery.browser.msie ) return false;
+                               jQuery(this).unbind("mouseover", jQuery.event.special.mouseenter.handler);
+                               return true;
+                       },
+
+                       handler: function(event) {
+                               // If we actually just moused on to a sub-element, ignore it
+                               if ( withinElement(event, this) ) return true;
+                               // Execute the right handlers by setting the event type to mouseenter
+                               event.type = "mouseenter";
+                               return jQuery.event.handle.apply(this, arguments);
+                       }
+               },
+
+               mouseleave: {
+                       setup: function() {
+                               if ( jQuery.browser.msie ) return false;
+                               jQuery(this).bind("mouseout", jQuery.event.special.mouseleave.handler);
+                               return true;
+                       },
+
+                       teardown: function() {
+                               if ( jQuery.browser.msie ) return false;
+                               jQuery(this).unbind("mouseout", jQuery.event.special.mouseleave.handler);
+                               return true;
+                       },
+
+                       handler: function(event) {
+                               // If we actually just moused on to a sub-element, ignore it
+                               if ( withinElement(event, this) ) return true;
+                               // Execute the right handlers by setting the event type to mouseleave
+                               event.type = "mouseleave";
+                               return jQuery.event.handle.apply(this, arguments);
+                       }
+               }
+       }
+};
+
+jQuery.fn.extend({
+       bind: function( type, data, fn ) {
+               return type == "unload" ? this.one(type, data, fn) : this.each(function(){
+                       jQuery.event.add( this, type, fn || data, fn && data );
+               });
+       },
+
+       one: function( type, data, fn ) {
+               var one = jQuery.event.proxy( fn || data, function(event) {
+                       jQuery(this).unbind(event, one);
+                       return (fn || data).apply( this, arguments );
+               });
+               return this.each(function(){
+                       jQuery.event.add( this, type, one, fn && data);
+               });
+       },
+
+       unbind: function( type, fn ) {
+               return this.each(function(){
+                       jQuery.event.remove( this, type, fn );
+               });
+       },
+
+       trigger: function( type, data, fn ) {
+               return this.each(function(){
+                       jQuery.event.trigger( type, data, this, true, fn );
+               });
+       },
+
+       triggerHandler: function( type, data, fn ) {
+               return this[0] && jQuery.event.trigger( type, data, this[0], false, fn );
+       },
+
+       toggle: function( fn ) {
+               // Save reference to arguments for access in closure
+               var args = arguments, i = 1;
+
+               // link all the functions, so any of them can unbind this click handler
+               while( i < args.length )
+                       jQuery.event.proxy( fn, args[i++] );
+
+               return this.click( jQuery.event.proxy( fn, function(event) {
+                       // Figure out which function to execute
+                       this.lastToggle = ( this.lastToggle || 0 ) % i;
+
+                       // Make sure that clicks stop
+                       event.preventDefault();
+
+                       // and execute the function
+                       return args[ this.lastToggle++ ].apply( this, arguments ) || false;
+               }));
+       },
+
+       hover: function(fnOver, fnOut) {
+               return this.bind('mouseenter', fnOver).bind('mouseleave', fnOut);
+       },
+
+       ready: function(fn) {
+               // Attach the listeners
+               bindReady();
+
+               // If the DOM is already ready
+               if ( jQuery.isReady )
+                       // Execute the function immediately
+                       fn.call( document, jQuery );
+
+               // Otherwise, remember the function for later
+               else
+                       // Add the function to the wait list
+                       jQuery.readyList.push( function() { return fn.call(this, jQuery); } );
+
+               return this;
+       }
+});
+
+jQuery.extend({
+       isReady: false,
+       readyList: [],
+       // Handle when the DOM is ready
+       ready: function() {
+               // Make sure that the DOM is not already loaded
+               if ( !jQuery.isReady ) {
+                       // Remember that the DOM is ready
+                       jQuery.isReady = true;
+
+                       // If there are functions bound, to execute
+                       if ( jQuery.readyList ) {
+                               // Execute all of them
+                               jQuery.each( jQuery.readyList, function(){
+                                       this.call( document );
+                               });
+
+                               // Reset the list of functions
+                               jQuery.readyList = null;
+                       }
+
+                       // Trigger any bound ready events
+                       jQuery(document).triggerHandler("ready");
+               }
+       }
+});
+
+var readyBound = false;
+
+function bindReady(){
+       if ( readyBound ) return;
+       readyBound = true;
+
+       // Mozilla, Opera (see further below for it) and webkit nightlies currently support this event
+       if ( document.addEventListener && !jQuery.browser.opera)
+               // Use the handy event callback
+               document.addEventListener( "DOMContentLoaded", jQuery.ready, false );
+
+       // If IE is used and is not in a frame
+       // Continually check to see if the document is ready
+       if ( jQuery.browser.msie && window == top ) (function(){
+               if (jQuery.isReady) return;
+               try {
+                       // If IE is used, use the trick by Diego Perini
+                       // http://javascript.nwbox.com/IEContentLoaded/
+                       document.documentElement.doScroll("left");
+               } catch( error ) {
+                       setTimeout( arguments.callee, 0 );
+                       return;
+               }
+               // and execute any waiting functions
+               jQuery.ready();
+       })();
+
+       if ( jQuery.browser.opera )
+               document.addEventListener( "DOMContentLoaded", function () {
+                       if (jQuery.isReady) return;
+                       for (var i = 0; i < document.styleSheets.length; i++)
+                               if (document.styleSheets[i].disabled) {
+                                       setTimeout( arguments.callee, 0 );
+                                       return;
+                               }
+                       // and execute any waiting functions
+                       jQuery.ready();
+               }, false);
+
+       if ( jQuery.browser.safari ) {
+               var numStyles;
+               (function(){
+                       if (jQuery.isReady) return;
+                       if ( document.readyState != "loaded" && document.readyState != "complete" ) {
+                               setTimeout( arguments.callee, 0 );
+                               return;
+                       }
+                       if ( numStyles === undefined )
+                               numStyles = jQuery("style, link[rel=stylesheet]").length;
+                       if ( document.styleSheets.length != numStyles ) {
+                               setTimeout( arguments.callee, 0 );
+                               return;
+                       }
+                       // and execute any waiting functions
+                       jQuery.ready();
+               })();
+       }
+
+       // A fallback to window.onload, that will always work
+       jQuery.event.add( window, "load", jQuery.ready );
+}
+
+jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
+       "mousedown,mouseup,mousemove,mouseover,mouseout,change,select," +
+       "submit,keydown,keypress,keyup,error").split(","), function(i, name){
+
+       // Handle event binding
+       jQuery.fn[name] = function(fn){
+               return fn ? this.bind(name, fn) : this.trigger(name);
+       };
+});
+
+// Checks if an event happened on an element within another element
+// Used in jQuery.event.special.mouseenter and mouseleave handlers
+var withinElement = function(event, elem) {
+       // Check if mouse(over|out) are still within the same parent element
+       var parent = event.relatedTarget;
+       // Traverse up the tree
+       while ( parent && parent != elem ) try { parent = parent.parentNode; } catch(error) { parent = elem; }
+       // Return true if we actually just moused on to a sub-element
+       return parent == elem;
+};
+
+// Prevent memory leaks in IE
+// And prevent errors on refresh with events like mouseover in other browsers
+// Window isn't included so as not to unbind existing unload events
+jQuery(window).bind("unload", function() {
+       jQuery("*").add(document).unbind();
+});
+jQuery.fn.extend({
+       // Keep a copy of the old load
+       _load: jQuery.fn.load,
+
+       load: function( url, params, callback ) {
+               if ( typeof url != 'string' )
+                       return this._load( url );
+
+               var off = url.indexOf(" ");
+               if ( off >= 0 ) {
+                       var selector = url.slice(off, url.length);
+                       url = url.slice(0, off);
+               }
+
+               callback = callback || function(){};
+
+               // Default to a GET request
+               var type = "GET";
+
+               // If the second parameter was provided
+               if ( params )
+                       // If it's a function
+                       if ( jQuery.isFunction( params ) ) {
+                               // We assume that it's the callback
+                               callback = params;
+                               params = null;
+
+                       // Otherwise, build a param string
+                       } else if( typeof params == 'object' ) {
+                               params = jQuery.param( params );
+                               type = "POST";
+                       }
+
+               var self = this;
+
+               // Request the remote document
+               jQuery.ajax({
+                       url: url,
+                       type: type,
+                       dataType: "html",
+                       data: params,
+                       complete: function(res, status){
+                               // If successful, inject the HTML into all the matched elements
+                               if ( status == "success" || status == "notmodified" )
+                                       // See if a selector was specified
+                                       self.html( selector ?
+                                               // Create a dummy div to hold the results
+                                               jQuery("<div/>")
+                                                       // inject the contents of the document in, removing the scripts
+                                                       // to avoid any 'Permission Denied' errors in IE
+                                                       .append(res.responseText.replace(/<script(.|\s)*?\/script>/g, ""))
+
+                                                       // Locate the specified elements
+                                                       .find(selector) :
+
+                                               // If not, just inject the full result
+                                               res.responseText );
+
+                               self.each( callback, [res.responseText, status, res] );
+                       }
+               });
+               return this;
+       },
+
+       serialize: function() {
+               return jQuery.param(this.serializeArray());
+       },
+       serializeArray: function() {
+               return this.map(function(){
+                       return jQuery.nodeName(this, "form") ?
+                               jQuery.makeArray(this.elements) : this;
+               })
+               .filter(function(){
+                       return this.name && !this.disabled &&
+                               (this.checked || /select|textarea/i.test(this.nodeName) ||
+                                       /text|hidden|password/i.test(this.type));
+               })
+               .map(function(i, elem){
+                       var val = jQuery(this).val();
+                       return val == null ? null :
+                               val.constructor == Array ?
+                                       jQuery.map( val, function(val, i){
+                                               return {name: elem.name, value: val};
+                                       }) :
+                                       {name: elem.name, value: val};
+               }).get();
+       }
+});
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( "ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","), function(i,o){
+       jQuery.fn[o] = function(f){
+               return this.bind(o, f);
+       };
+});
+
+var jsc = now();
+
+jQuery.extend({
+       get: function( url, data, callback, type ) {
+               // shift arguments if data argument was ommited
+               if ( jQuery.isFunction( data ) ) {
+                       callback = data;
+                       data = null;
+               }
+
+               return jQuery.ajax({
+                       type: "GET",
+                       url: url,
+                       data: data,
+                       success: callback,
+                       dataType: type
+               });
+       },
+
+       getScript: function( url, callback ) {
+               return jQuery.get(url, null, callback, "script");
+       },
+
+       getJSON: function( url, data, callback ) {
+               return jQuery.get(url, data, callback, "json");
+       },
+
+       post: function( url, data, callback, type ) {
+               if ( jQuery.isFunction( data ) ) {
+                       callback = data;
+                       data = {};
+               }
+
+               return jQuery.ajax({
+                       type: "POST",
+                       url: url,
+                       data: data,
+                       success: callback,
+                       dataType: type
+               });
+       },
+
+       ajaxSetup: function( settings ) {
+               jQuery.extend( jQuery.ajaxSettings, settings );
+       },
+
+       ajaxSettings: {
+               url: location.href,
+               global: true,
+               type: "GET",
+               timeout: 0,
+               contentType: "application/x-www-form-urlencoded",
+               processData: true,
+               async: true,
+               data: null,
+               username: null,
+               password: null,
+               accepts: {
+                       xml: "application/xml, text/xml",
+                       html: "text/html",
+                       script: "text/javascript, application/javascript",
+                       json: "application/json, text/javascript",
+                       text: "text/plain",
+                       _default: "*/*"
+               }
+       },
+
+       // Last-Modified header cache for next request
+       lastModified: {},
+
+       ajax: function( s ) {
+               // Extend the settings, but re-extend 's' so that it can be
+               // checked again later (in the test suite, specifically)
+               s = jQuery.extend(true, s, jQuery.extend(true, {}, jQuery.ajaxSettings, s));
+
+               var jsonp, jsre = /=\?(&|$)/g, status, data,
+                       type = s.type.toUpperCase();
+
+               // convert data if not already a string
+               if ( s.data && s.processData && typeof s.data != "string" )
+                       s.data = jQuery.param(s.data);
+
+               // Handle JSONP Parameter Callbacks
+               if ( s.dataType == "jsonp" ) {
+                       if ( type == "GET" ) {
+                               if ( !s.url.match(jsre) )
+                                       s.url += (s.url.match(/\?/) ? "&" : "?") + (s.jsonp || "callback") + "=?";
+                       } else if ( !s.data || !s.data.match(jsre) )
+                               s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?";
+                       s.dataType = "json";
+               }
+
+               // Build temporary JSONP function
+               if ( s.dataType == "json" && (s.data && s.data.match(jsre) || s.url.match(jsre)) ) {
+                       jsonp = "jsonp" + jsc++;
+
+                       // Replace the =? sequence both in the query string and the data
+                       if ( s.data )
+                               s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
+                       s.url = s.url.replace(jsre, "=" + jsonp + "$1");
+
+                       // We need to make sure
+                       // that a JSONP style response is executed properly
+                       s.dataType = "script";
+
+                       // Handle JSONP-style loading
+                       window[ jsonp ] = function(tmp){
+                               data = tmp;
+                               success();
+                               complete();
+                               // Garbage collect
+                               window[ jsonp ] = undefined;
+                               try{ delete window[ jsonp ]; } catch(e){}
+                               if ( head )
+                                       head.removeChild( script );
+                       };
+               }
+
+               if ( s.dataType == "script" && s.cache == null )
+                       s.cache = false;
+
+               if ( s.cache === false && type == "GET" ) {
+                       var ts = now();
+                       // try replacing _= if it is there
+                       var ret = s.url.replace(/(\?|&)_=.*?(&|$)/, "$1_=" + ts + "$2");
+                       // if nothing was replaced, add timestamp to the end
+                       s.url = ret + ((ret == s.url) ? (s.url.match(/\?/) ? "&" : "?") + "_=" + ts : "");
+               }
+
+               // If data is available, append data to url for get requests
+               if ( s.data && type == "GET" ) {
+                       s.url += (s.url.match(/\?/) ? "&" : "?") + s.data;
+
+                       // IE likes to send both get and post data, prevent this
+                       s.data = null;
+               }
+
+               // Watch for a new set of requests
+               if ( s.global && ! jQuery.active++ )
+                       jQuery.event.trigger( "ajaxStart" );
+
+               // Matches an absolute URL, and saves the domain
+               var remote = /^(?:\w+:)?\/\/([^\/?#]+)/;
+
+               // If we're requesting a remote document
+               // and trying to load JSON or Script with a GET
+               if ( s.dataType == "script" && type == "GET"
+                               && remote.test(s.url) && remote.exec(s.url)[1] != location.host ){
+                       var head = document.getElementsByTagName("head")[0];
+                       var script = document.createElement("script");
+                       script.src = s.url;
+                       if (s.scriptCharset)
+                               script.charset = s.scriptCharset;
+
+                       // Handle Script loading
+                       if ( !jsonp ) {
+                               var done = false;
+
+                               // Attach handlers for all browsers
+                               script.onload = script.onreadystatechange = function(){
+                                       if ( !done && (!this.readyState ||
+                                                       this.readyState == "loaded" || this.readyState == "complete") ) {
+                                               done = true;
+                                               success();
+                                               complete();
+                                               head.removeChild( script );
+                                       }
+                               };
+                       }
+
+                       head.appendChild(script);
+
+                       // We handle everything using the script element injection
+                       return undefined;
+               }
+
+               var requestDone = false;
+
+               // Create the request object; Microsoft failed to properly
+               // implement the XMLHttpRequest in IE7, so we use the ActiveXObject when it is available
+               var xhr = window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();
+
+               // Open the socket
+               // Passing null username, generates a login popup on Opera (#2865)
+               if( s.username )
+                       xhr.open(type, s.url, s.async, s.username, s.password);
+               else
+                       xhr.open(type, s.url, s.async);
+
+               // Need an extra try/catch for cross domain requests in Firefox 3
+               try {
+                       // Set the correct header, if data is being sent
+                       if ( s.data )
+                               xhr.setRequestHeader("Content-Type", s.contentType);
+
+                       // Set the If-Modified-Since header, if ifModified mode.
+                       if ( s.ifModified )
+                               xhr.setRequestHeader("If-Modified-Since",
+                                       jQuery.lastModified[s.url] || "Thu, 01 Jan 1970 00:00:00 GMT" );
+
+                       // Set header so the called script knows that it's an XMLHttpRequest
+                       xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+
+                       // Set the Accepts header for the server, depending on the dataType
+                       xhr.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ?
+                               s.accepts[ s.dataType ] + ", */*" :
+                               s.accepts._default );
+               } catch(e){}
+
+               // Allow custom headers/mimetypes
+               if ( s.beforeSend && s.beforeSend(xhr, s) === false ) {
+                       // cleanup active request counter
+                       s.global && jQuery.active--;
+                       // close opended socket
+                       xhr.abort();
+                       return false;
+               }
+
+               if ( s.global )
+                       jQuery.event.trigger("ajaxSend", [xhr, s]);
+
+               // Wait for a response to come back
+               var onreadystatechange = function(isTimeout){
+                       // The transfer is complete and the data is available, or the request timed out
+                       if ( !requestDone && xhr && (xhr.readyState == 4 || isTimeout == "timeout") ) {
+                               requestDone = true;
+
+                               // clear poll interval
+                               if (ival) {
+                                       clearInterval(ival);
+                                       ival = null;
+                               }
+
+                               status = isTimeout == "timeout" ? "timeout" :
+                                       !jQuery.httpSuccess( xhr ) ? "error" :
+                                       s.ifModified && jQuery.httpNotModified( xhr, s.url ) ? "notmodified" :
+                                       "success";
+
+                               if ( status == "success" ) {
+                                       // Watch for, and catch, XML document parse errors
+                                       try {
+                                               // process the data (runs the xml through httpData regardless of callback)
+                                               data = jQuery.httpData( xhr, s.dataType, s.dataFilter );
+                                       } catch(e) {
+                                               status = "parsererror";
+                                       }
+                               }
+
+                               // Make sure that the request was successful or notmodified
+                               if ( status == "success" ) {
+                                       // Cache Last-Modified header, if ifModified mode.
+                                       var modRes;
+                                       try {
+                                               modRes = xhr.getResponseHeader("Last-Modified");
+                                       } catch(e) {} // swallow exception thrown by FF if header is not available
+
+                                       if ( s.ifModified && modRes )
+                                               jQuery.lastModified[s.url] = modRes;
+
+                                       // JSONP handles its own success callback
+                                       if ( !jsonp )
+                                               success();
+                               } else
+                                       jQuery.handleError(s, xhr, status);
+
+                               // Fire the complete handlers
+                               complete();
+
+                               // Stop memory leaks
+                               if ( s.async )
+                                       xhr = null;
+                       }
+               };
+
+               if ( s.async ) {
+                       // don't attach the handler to the request, just poll it instead
+                       var ival = setInterval(onreadystatechange, 13);
+
+                       // Timeout checker
+                       if ( s.timeout > 0 )
+                               setTimeout(function(){
+                                       // Check to see if the request is still happening
+                                       if ( xhr ) {
+                                               // Cancel the request
+                                               xhr.abort();
+
+                                               if( !requestDone )
+                                                       onreadystatechange( "timeout" );
+                                       }
+                               }, s.timeout);
+               }
+
+               // Send the data
+               try {
+                       xhr.send(s.data);
+               } catch(e) {
+                       jQuery.handleError(s, xhr, null, e);
+               }
+
+               // firefox 1.5 doesn't fire statechange for sync requests
+               if ( !s.async )
+                       onreadystatechange();
+
+               function success(){
+                       // If a local callback was specified, fire it and pass it the data
+                       if ( s.success )
+                               s.success( data, status );
+
+                       // Fire the global callback
+                       if ( s.global )
+                               jQuery.event.trigger( "ajaxSuccess", [xhr, s] );
+               }
+
+               function complete(){
+                       // Process result
+                       if ( s.complete )
+                               s.complete(xhr, status);
+
+                       // The request was completed
+                       if ( s.global )
+                               jQuery.event.trigger( "ajaxComplete", [xhr, s] );
+
+                       // Handle the global AJAX counter
+                       if ( s.global && ! --jQuery.active )
+                               jQuery.event.trigger( "ajaxStop" );
+               }
+
+               // return XMLHttpRequest to allow aborting the request etc.
+               return xhr;
+       },
+
+       handleError: function( s, xhr, status, e ) {
+               // If a local callback was specified, fire it
+               if ( s.error ) s.error( xhr, status, e );
+
+               // Fire the global callback
+               if ( s.global )
+                       jQuery.event.trigger( "ajaxError", [xhr, s, e] );
+       },
+
+       // Counter for holding the number of active queries
+       active: 0,
+
+       // Determines if an XMLHttpRequest was successful or not
+       httpSuccess: function( xhr ) {
+               try {
+                       // IE error sometimes returns 1223 when it should be 204 so treat it as success, see #1450
+                       return !xhr.status && location.protocol == "file:" ||
+                               ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status == 304 || xhr.status == 1223 ||
+                               jQuery.browser.safari && xhr.status == undefined;
+               } catch(e){}
+               return false;
+       },
+
+       // Determines if an XMLHttpRequest returns NotModified
+       httpNotModified: function( xhr, url ) {
+               try {
+                       var xhrRes = xhr.getResponseHeader("Last-Modified");
+
+                       // Firefox always returns 200. check Last-Modified date
+                       return xhr.status == 304 || xhrRes == jQuery.lastModified[url] ||
+                               jQuery.browser.safari && xhr.status == undefined;
+               } catch(e){}
+               return false;
+       },
+
+       httpData: function( xhr, type, filter ) {
+               var ct = xhr.getResponseHeader("content-type"),
+                       xml = type == "xml" || !type && ct && ct.indexOf("xml") >= 0,
+                       data = xml ? xhr.responseXML : xhr.responseText;
+
+               if ( xml && data.documentElement.tagName == "parsererror" )
+                       throw "parsererror";
+                       
+               // Allow a pre-filtering function to sanitize the response
+               if( filter )
+                       data = filter( data, type );
+
+               // If the type is "script", eval it in global context
+               if ( type == "script" )
+                       jQuery.globalEval( data );
+
+               // Get the JavaScript object, if JSON is used.
+               if ( type == "json" )
+                       data = eval("(" + data + ")");
+
+               return data;
+       },
+
+       // Serialize an array of form elements or a set of
+       // key/values into a query string
+       param: function( a ) {
+               var s = [ ];
+
+               function add( key, value ){
+                       s[ s.length ] = encodeURIComponent(key) + '=' + encodeURIComponent(value);
+               };
+
+               // If an array was passed in, assume that it is an array
+               // of form elements
+               if ( a.constructor == Array || a.jquery )
+                       // Serialize the form elements
+                       jQuery.each( a, function(){
+                               add( this.name, this.value );
+                       });
+
+               // Otherwise, assume that it's an object of key/value pairs
+               else
+                       // Serialize the key/values
+                       for ( var j in a )
+                               // If the value is an array then the key names need to be repeated
+                               if ( a[j] && a[j].constructor == Array )
+                                       jQuery.each( a[j], function(){
+                                               add( j, this );
+                                       });
+                               else
+                                       add( j, jQuery.isFunction(a[j]) ? a[j]() : a[j] );
+
+               // Return the resulting serialization
+               return s.join("&").replace(/%20/g, "+");
+       }
+
+});
+jQuery.fn.extend({
+       show: function(speed,callback){
+               return speed ?
+                       this.animate({
+                               height: "show", width: "show", opacity: "show"
+                       }, speed, callback) :
+
+                       this.filter(":hidden").each(function(){
+                               this.style.display = this.oldblock || "";
+                               if ( jQuery.css(this,"display") == "none" ) {
+                                       var elem = jQuery("<" + this.tagName + " />").appendTo("body");
+                                       this.style.display = elem.css("display");
+                                       // handle an edge condition where css is - div { display:none; } or similar
+                                       if (this.style.display == "none")
+                                               this.style.display = "block";
+                                       elem.remove();
+                               }
+                       }).end();
+       },
+
+       hide: function(speed,callback){
+               return speed ?
+                       this.animate({
+                               height: "hide", width: "hide", opacity: "hide"
+                       }, speed, callback) :
+
+                       this.filter(":visible").each(function(){
+                               this.oldblock = this.oldblock || jQuery.css(this,"display");
+                               this.style.display = "none";
+                       }).end();
+       },
+
+       // Save the old toggle function
+       _toggle: jQuery.fn.toggle,
+
+       toggle: function( fn, fn2 ){
+               return jQuery.isFunction(fn) && jQuery.isFunction(fn2) ?
+                       this._toggle.apply( this, arguments ) :
+                       fn ?
+                               this.animate({
+                                       height: "toggle", width: "toggle", opacity: "toggle"
+                               }, fn, fn2) :
+                               this.each(function(){
+                                       jQuery(this)[ jQuery(this).is(":hidden") ? "show" : "hide" ]();
+                               });
+       },
+
+       slideDown: function(speed,callback){
+               return this.animate({height: "show"}, speed, callback);
+       },
+
+       slideUp: function(speed,callback){
+               return this.animate({height: "hide"}, speed, callback);
+       },
+
+       slideToggle: function(speed, callback){
+               return this.animate({height: "toggle"}, speed, callback);
+       },
+
+       fadeIn: function(speed, callback){
+               return this.animate({opacity: "show"}, speed, callback);
+       },
+
+       fadeOut: function(speed, callback){
+               return this.animate({opacity: "hide"}, speed, callback);
+       },
+
+       fadeTo: function(speed,to,callback){
+               return this.animate({opacity: to}, speed, callback);
+       },
+
+       animate: function( prop, speed, easing, callback ) {
+               var optall = jQuery.speed(speed, easing, callback);
+
+               return this[ optall.queue === false ? "each" : "queue" ](function(){
+                       if ( this.nodeType != 1)
+                               return false;
+
+                       var opt = jQuery.extend({}, optall), p,
+                               hidden = jQuery(this).is(":hidden"), self = this;
+
+                       for ( p in prop ) {
+                               if ( prop[p] == "hide" && hidden || prop[p] == "show" && !hidden )
+                                       return opt.complete.call(this);
+
+                               if ( p == "height" || p == "width" ) {
+                                       // Store display property
+                                       opt.display = jQuery.css(this, "display");
+
+                                       // Make sure that nothing sneaks out
+                                       opt.overflow = this.style.overflow;
+                               }
+                       }
+
+                       if ( opt.overflow != null )
+                               this.style.overflow = "hidden";
+
+                       opt.curAnim = jQuery.extend({}, prop);
+
+                       jQuery.each( prop, function(name, val){
+                               var e = new jQuery.fx( self, opt, name );
+
+                               if ( /toggle|show|hide/.test(val) )
+                                       e[ val == "toggle" ? hidden ? "show" : "hide" : val ]( prop );
+                               else {
+                                       var parts = val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),
+                                               start = e.cur(true) || 0;
+
+                                       if ( parts ) {
+                                               var end = parseFloat(parts[2]),
+                                                       unit = parts[3] || "px";
+
+                                               // We need to compute starting value
+                                               if ( unit != "px" ) {
+                                                       self.style[ name ] = (end || 1) + unit;
+                                                       start = ((end || 1) / e.cur(true)) * start;
+                                                       self.style[ name ] = start + unit;
+                                               }
+
+                                               // If a +=/-= token was provided, we're doing a relative animation
+                                               if ( parts[1] )
+                                                       end = ((parts[1] == "-=" ? -1 : 1) * end) + start;
+
+                                               e.custom( start, end, unit );
+                                       } else
+                                               e.custom( start, val, "" );
+                               }
+                       });
+
+                       // For JS strict compliance
+                       return true;
+               });
+       },
+
+       queue: function(type, fn){
+               if ( jQuery.isFunction(type) || ( type && type.constructor == Array )) {
+                       fn = type;
+                       type = "fx";
+               }
+
+               if ( !type || (typeof type == "string" && !fn) )
+                       return queue( this[0], type );
+
+               return this.each(function(){
+                       if ( fn.constructor == Array )
+                               queue(this, type, fn);
+                       else {
+                               queue(this, type).push( fn );
+
+                               if ( queue(this, type).length == 1 )
+                                       fn.call(this);
+                       }
+               });
+       },
+
+       stop: function(clearQueue, gotoEnd){
+               var timers = jQuery.timers;
+
+               if (clearQueue)
+                       this.queue([]);
+
+               this.each(function(){
+                       // go in reverse order so anything added to the queue during the loop is ignored
+                       for ( var i = timers.length - 1; i >= 0; i-- )
+                               if ( timers[i].elem == this ) {
+                                       if (gotoEnd)
+                                               // force the next step to be the last
+                                               timers[i](true);
+                                       timers.splice(i, 1);
+                               }
+               });
+
+               // start the next in the queue if the last step wasn't forced
+               if (!gotoEnd)
+                       this.dequeue();
+
+               return this;
+       }
+
+});
+
+var queue = function( elem, type, array ) {
+       if ( elem ){
+
+               type = type || "fx";
+
+               var q = jQuery.data( elem, type + "queue" );
+
+               if ( !q || array )
+                       q = jQuery.data( elem, type + "queue", jQuery.makeArray(array) );
+
+       }
+       return q;
+};
+
+jQuery.fn.dequeue = function(type){
+       type = type || "fx";
+
+       return this.each(function(){
+               var q = queue(this, type);
+
+               q.shift();
+
+               if ( q.length )
+                       q[0].call( this );
+       });
+};
+
+jQuery.extend({
+
+       speed: function(speed, easing, fn) {
+               var opt = speed && speed.constructor == Object ? speed : {
+                       complete: fn || !fn && easing ||
+                               jQuery.isFunction( speed ) && speed,
+                       duration: speed,
+                       easing: fn && easing || easing && easing.constructor != Function && easing
+               };
+
+               opt.duration = (opt.duration && opt.duration.constructor == Number ?
+                       opt.duration :
+                       jQuery.fx.speeds[opt.duration]) || jQuery.fx.speeds.def;
+
+               // Queueing
+               opt.old = opt.complete;
+               opt.complete = function(){
+                       if ( opt.queue !== false )
+                               jQuery(this).dequeue();
+                       if ( jQuery.isFunction( opt.old ) )
+                               opt.old.call( this );
+               };
+
+               return opt;
+       },
+
+       easing: {
+               linear: function( p, n, firstNum, diff ) {
+                       return firstNum + diff * p;
+               },
+               swing: function( p, n, firstNum, diff ) {
+                       return ((-Math.cos(p*Math.PI)/2) + 0.5) * diff + firstNum;
+               }
+       },
+
+       timers: [],
+       timerId: null,
+
+       fx: function( elem, options, prop ){
+               this.options = options;
+               this.elem = elem;
+               this.prop = prop;
+
+               if ( !options.orig )
+                       options.orig = {};
+       }
+
+});
+
+jQuery.fx.prototype = {
+
+       // Simple function for setting a style value
+       update: function(){
+               if ( this.options.step )
+                       this.options.step.call( this.elem, this.now, this );
+
+               (jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this );
+
+               // Set display property to block for height/width animations
+               if ( this.prop == "height" || this.prop == "width" )
+                       this.elem.style.display = "block";
+       },
+
+       // Get the current size
+       cur: function(force){
+               if ( this.elem[this.prop] != null && this.elem.style[this.prop] == null )
+                       return this.elem[ this.prop ];
+
+               var r = parseFloat(jQuery.css(this.elem, this.prop, force));
+               return r && r > -10000 ? r : parseFloat(jQuery.curCSS(this.elem, this.prop)) || 0;
+       },
+
+       // Start an animation from one number to another
+       custom: function(from, to, unit){
+               this.startTime = now();
+               this.start = from;
+               this.end = to;
+               this.unit = unit || this.unit || "px";
+               this.now = this.start;
+               this.pos = this.state = 0;
+               this.update();
+
+               var self = this;
+               function t(gotoEnd){
+                       return self.step(gotoEnd);
+               }
+
+               t.elem = this.elem;
+
+               jQuery.timers.push(t);
+
+               if ( jQuery.timerId == null ) {
+                       jQuery.timerId = setInterval(function(){
+                               var timers = jQuery.timers;
+
+                               for ( var i = 0; i < timers.length; i++ )
+                                       if ( !timers[i]() )
+                                               timers.splice(i--, 1);
+
+                               if ( !timers.length ) {
+                                       clearInterval( jQuery.timerId );
+                                       jQuery.timerId = null;
+                               }
+                       }, 13);
+               }
+       },
+
+       // Simple 'show' function
+       show: function(){
+               // Remember where we started, so that we can go back to it later
+               this.options.orig[this.prop] = jQuery.attr( this.elem.style, this.prop );
+               this.options.show = true;
+
+               // Begin the animation
+               this.custom(0, this.cur());
+
+               // Make sure that we start at a small width/height to avoid any
+               // flash of content
+               if ( this.prop == "width" || this.prop == "height" )
+                       this.elem.style[this.prop] = "1px";
+
+               // Start by showing the element
+               jQuery(this.elem).show();
+       },
+
+       // Simple 'hide' function
+       hide: function(){
+               // Remember where we started, so that we can go back to it later
+               this.options.orig[this.prop] = jQuery.attr( this.elem.style, this.prop );
+               this.options.hide = true;
+
+               // Begin the animation
+               this.custom(this.cur(), 0);
+       },
+
+       // Each step of an animation
+       step: function(gotoEnd){
+               var t = now();
+
+               if ( gotoEnd || t > this.options.duration + this.startTime ) {
+                       this.now = this.end;
+                       this.pos = this.state = 1;
+                       this.update();
+
+                       this.options.curAnim[ this.prop ] = true;
+
+                       var done = true;
+                       for ( var i in this.options.curAnim )
+                               if ( this.options.curAnim[i] !== true )
+                                       done = false;
+
+                       if ( done ) {
+                               if ( this.options.display != null ) {
+                                       // Reset the overflow
+                                       this.elem.style.overflow = this.options.overflow;
+
+                                       // Reset the display
+                                       this.elem.style.display = this.options.display;
+                                       if ( jQuery.css(this.elem, "display") == "none" )
+                                               this.elem.style.display = "block";
+                               }
+
+                               // Hide the element if the "hide" operation was done
+                               if ( this.options.hide )
+                                       this.elem.style.display = "none";
+
+                               // Reset the properties, if the item has been hidden or shown
+                               if ( this.options.hide || this.options.show )
+                                       for ( var p in this.options.curAnim )
+                                               jQuery.attr(this.elem.style, p, this.options.orig[p]);
+                       }
+
+                       if ( done )
+                               // Execute the complete function
+                               this.options.complete.call( this.elem );
+
+                       return false;
+               } else {
+                       var n = t - this.startTime;
+                       this.state = n / this.options.duration;
+
+                       // Perform the easing function, defaults to swing
+                       this.pos = jQuery.easing[this.options.easing || (jQuery.easing.swing ? "swing" : "linear")](this.state, n, 0, 1, this.options.duration);
+                       this.now = this.start + ((this.end - this.start) * this.pos);
+
+                       // Perform the next step of the animation
+                       this.update();
+               }
+
+               return true;
+       }
+
+};
+
+jQuery.extend( jQuery.fx, {
+       speeds:{
+               slow: 600,
+               fast: 200,
+               // Default speed
+               def: 400
+       },
+       step: {
+               scrollLeft: function(fx){
+                       fx.elem.scrollLeft = fx.now;
+               },
+
+               scrollTop: function(fx){
+                       fx.elem.scrollTop = fx.now;
+               },
+
+               opacity: function(fx){
+                       jQuery.attr(fx.elem.style, "opacity", fx.now);
+               },
+
+               _default: function(fx){
+                       fx.elem.style[ fx.prop ] = fx.now + fx.unit;
+               }
+       }
+});
+// The Offset Method
+// Originally By Brandon Aaron, part of the Dimension Plugin
+// http://jquery.com/plugins/project/dimensions
+jQuery.fn.offset = function() {
+       var left = 0, top = 0, elem = this[0], results;
+
+       if ( elem ) with ( jQuery.browser ) {
+               var parent       = elem.parentNode,
+                   offsetChild  = elem,
+                   offsetParent = elem.offsetParent,
+                   doc          = elem.ownerDocument,
+                   safari2      = safari && parseInt(version) < 522 && !/adobeair/i.test(userAgent),
+                   css          = jQuery.curCSS,
+                   fixed        = css(elem, "position") == "fixed";
+
+               // Use getBoundingClientRect if available
+               if ( !(mozilla && elem == document.body) && elem.getBoundingClientRect ) {
+                       var box = elem.getBoundingClientRect();
+
+                       // Add the document scroll offsets
+                       add(box.left + Math.max(doc.documentElement.scrollLeft, doc.body.scrollLeft),
+                               box.top  + Math.max(doc.documentElement.scrollTop,  doc.body.scrollTop));
+
+                       // IE adds the HTML element's border, by default it is medium which is 2px
+                       // IE 6 and 7 quirks mode the border width is overwritable by the following css html { border: 0; }
+                       // IE 7 standards mode, the border is always 2px
+                       // This border/offset is typically represented by the clientLeft and clientTop properties
+                       // However, in IE6 and 7 quirks mode the clientLeft and clientTop properties are not updated when overwriting it via CSS
+                       // Therefore this method will be off by 2px in IE while in quirksmode
+                       add( -doc.documentElement.clientLeft, -doc.documentElement.clientTop );
+
+               // Otherwise loop through the offsetParents and parentNodes
+               } else {
+
+                       // Initial element offsets
+                       add( elem.offsetLeft, elem.offsetTop );
+
+                       // Get parent offsets
+                       while ( offsetParent ) {
+                               // Add offsetParent offsets
+                               add( offsetParent.offsetLeft, offsetParent.offsetTop );
+
+                               // Mozilla and Safari > 2 does not include the border on offset parents
+                               // However Mozilla adds the border for table or table cells
+                               if ( mozilla && !/^t(able|d|h)$/i.test(offsetParent.tagName) || safari && !safari2 )
+                                       border( offsetParent );
+
+                               // Add the document scroll offsets if position is fixed on any offsetParent
+                               if ( !fixed && css(offsetParent, "position") == "fixed" )
+                                       fixed = true;
+
+                               // Set offsetChild to previous offsetParent unless it is the body element
+                               offsetChild  = /^body$/i.test(offsetParent.tagName) ? offsetChild : offsetParent;
+                               // Get next offsetParent
+                               offsetParent = offsetParent.offsetParent;
+                       }
+
+                       // Get parent scroll offsets
+                       while ( parent && parent.tagName && !/^body|html$/i.test(parent.tagName) ) {
+                               // Remove parent scroll UNLESS that parent is inline or a table to work around Opera inline/table scrollLeft/Top bug
+                               if ( !/^inline|table.*$/i.test(css(parent, "display")) )
+                                       // Subtract parent scroll offsets
+                                       add( -parent.scrollLeft, -parent.scrollTop );
+
+                               // Mozilla does not add the border for a parent that has overflow != visible
+                               if ( mozilla && css(parent, "overflow") != "visible" )
+                                       border( parent );
+
+                               // Get next parent
+                               parent = parent.parentNode;
+                       }
+
+                       // Safari <= 2 doubles body offsets with a fixed position element/offsetParent or absolutely positioned offsetChild
+                       // Mozilla doubles body offsets with a non-absolutely positioned offsetChild
+                       if ( (safari2 && (fixed || css(offsetChild, "position") == "absolute")) ||
+                               (mozilla && css(offsetChild, "position") != "absolute") )
+                                       add( -doc.body.offsetLeft, -doc.body.offsetTop );
+
+                       // Add the document scroll offsets if position is fixed
+                       if ( fixed )
+                               add(Math.max(doc.documentElement.scrollLeft, doc.body.scrollLeft),
+                                       Math.max(doc.documentElement.scrollTop,  doc.body.scrollTop));
+               }
+
+               // Return an object with top and left properties
+               results = { top: top, left: left };
+       }
+
+       function border(elem) {
+               add( jQuery.curCSS(elem, "borderLeftWidth", true), jQuery.curCSS(elem, "borderTopWidth", true) );
+       }
+
+       function add(l, t) {
+               left += parseInt(l, 10) || 0;
+               top += parseInt(t, 10) || 0;
+       }
+
+       return results;
+};
+
+
+jQuery.fn.extend({
+       position: function() {
+               var left = 0, top = 0, results;
+
+               if ( this[0] ) {
+                       // Get *real* offsetParent
+                       var offsetParent = this.offsetParent(),
+
+                       // Get correct offsets
+                       offset       = this.offset(),
+                       parentOffset = /^body|html$/i.test(offsetParent[0].tagName) ? { top: 0, left: 0 } : offsetParent.offset();
+
+                       // Subtract element margins
+                       // note: when an element has margin: auto the offsetLeft and marginLeft 
+                       // are the same in Safari causing offset.left to incorrectly be 0
+                       offset.top  -= num( this, 'marginTop' );
+                       offset.left -= num( this, 'marginLeft' );
+
+                       // Add offsetParent borders
+                       parentOffset.top  += num( offsetParent, 'borderTopWidth' );
+                       parentOffset.left += num( offsetParent, 'borderLeftWidth' );
+
+                       // Subtract the two offsets
+                       results = {
+                               top:  offset.top  - parentOffset.top,
+                               left: offset.left - parentOffset.left
+                       };
+               }
+
+               return results;
+       },
+
+       offsetParent: function() {
+               var offsetParent = this[0].offsetParent;
+               while ( offsetParent && (!/^body|html$/i.test(offsetParent.tagName) && jQuery.css(offsetParent, 'position') == 'static') )
+                       offsetParent = offsetParent.offsetParent;
+               return jQuery(offsetParent);
+       }
+});
+
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( ['Left', 'Top'], function(i, name) {
+       var method = 'scroll' + name;
+       
+       jQuery.fn[ method ] = function(val) {
+               if (!this[0]) return;
+
+               return val != undefined ?
+
+                       // Set the scroll offset
+                       this.each(function() {
+                               this == window || this == document ?
+                                       window.scrollTo(
+                                               !i ? val : jQuery(window).scrollLeft(),
+                                                i ? val : jQuery(window).scrollTop()
+                                       ) :
+                                       this[ method ] = val;
+                       }) :
+
+                       // Return the scroll offset
+                       this[0] == window || this[0] == document ?
+                               self[ i ? 'pageYOffset' : 'pageXOffset' ] ||
+                                       jQuery.boxModel && document.documentElement[ method ] ||
+                                       document.body[ method ] :
+                               this[0][ method ];
+       };
+});
+// Create innerHeight, innerWidth, outerHeight and outerWidth methods
+jQuery.each([ "Height", "Width" ], function(i, name){
+
+       var tl = i ? "Left"  : "Top",  // top or left
+               br = i ? "Right" : "Bottom"; // bottom or right
+
+       // innerHeight and innerWidth
+       jQuery.fn["inner" + name] = function(){
+               return this[ name.toLowerCase() ]() +
+                       num(this, "padding" + tl) +
+                       num(this, "padding" + br);
+       };
+
+       // outerHeight and outerWidth
+       jQuery.fn["outer" + name] = function(margin) {
+               return this["inner" + name]() +
+                       num(this, "border" + tl + "Width") +
+                       num(this, "border" + br + "Width") +
+                       (margin ?
+                               num(this, "margin" + tl) + num(this, "margin" + br) : 0);
+       };
+
+});})();
diff --git a/plugins/Autocomplete/jquery-autocomplete/lib/thickbox-compressed.js b/plugins/Autocomplete/jquery-autocomplete/lib/thickbox-compressed.js
new file mode 100644 (file)
index 0000000..28364be
--- /dev/null
@@ -0,0 +1,10 @@
+/*
+ * Thickbox 3 - One Box To Rule Them All.
+ * By Cody Lindley (http://www.codylindley.com)
+ * Copyright (c) 2007 cody lindley
+ * Licensed under the MIT License: http://www.opensource.org/licenses/mit-license.php
+*/
+
+var tb_pathToImage = "images/loadingAnimation.gif";
+
+eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('$(o).2S(9(){1u(\'a.18, 3n.18, 3i.18\');1w=1p 1t();1w.L=2H});9 1u(b){$(b).s(9(){6 t=X.Q||X.1v||M;6 a=X.u||X.23;6 g=X.1N||P;19(t,a,g);X.2E();H P})}9 19(d,f,g){3m{3(2t o.v.J.2i==="2g"){$("v","11").r({A:"28%",z:"28%"});$("11").r("22","2Z");3(o.1Y("1F")===M){$("v").q("<U 5=\'1F\'></U><4 5=\'B\'></4><4 5=\'8\'></4>");$("#B").s(G)}}n{3(o.1Y("B")===M){$("v").q("<4 5=\'B\'></4><4 5=\'8\'></4>");$("#B").s(G)}}3(1K()){$("#B").1J("2B")}n{$("#B").1J("2z")}3(d===M){d=""}$("v").q("<4 5=\'K\'><1I L=\'"+1w.L+"\' /></4>");$(\'#K\').2y();6 h;3(f.O("?")!==-1){h=f.3l(0,f.O("?"))}n{h=f}6 i=/\\.2s$|\\.2q$|\\.2m$|\\.2l$|\\.2k$/;6 j=h.1C().2h(i);3(j==\'.2s\'||j==\'.2q\'||j==\'.2m\'||j==\'.2l\'||j==\'.2k\'){1D="";1G="";14="";1z="";1x="";R="";1n="";1r=P;3(g){E=$("a[@1N="+g+"]").36();25(D=0;((D<E.1c)&&(R===""));D++){6 k=E[D].u.1C().2h(i);3(!(E[D].u==f)){3(1r){1z=E[D].Q;1x=E[D].u;R="<1e 5=\'1X\'>&1d;&1d;<a u=\'#\'>2T &2R;</a></1e>"}n{1D=E[D].Q;1G=E[D].u;14="<1e 5=\'1U\'>&1d;&1d;<a u=\'#\'>&2O; 2N</a></1e>"}}n{1r=1b;1n="1t "+(D+1)+" 2L "+(E.1c)}}}S=1p 1t();S.1g=9(){S.1g=M;6 a=2x();6 x=a[0]-1M;6 y=a[1]-1M;6 b=S.z;6 c=S.A;3(b>x){c=c*(x/b);b=x;3(c>y){b=b*(y/c);c=y}}n 3(c>y){b=b*(y/c);c=y;3(b>x){c=c*(x/b);b=x}}13=b+30;1a=c+2G;$("#8").q("<a u=\'\' 5=\'1L\' Q=\'1o\'><1I 5=\'2F\' L=\'"+f+"\' z=\'"+b+"\' A=\'"+c+"\' 23=\'"+d+"\'/></a>"+"<4 5=\'2D\'>"+d+"<4 5=\'2C\'>"+1n+14+R+"</4></4><4 5=\'2A\'><a u=\'#\' 5=\'Z\' Q=\'1o\'>1l</a> 1k 1j 1s</4>");$("#Z").s(G);3(!(14==="")){9 12(){3($(o).N("s",12)){$(o).N("s",12)}$("#8").C();$("v").q("<4 5=\'8\'></4>");19(1D,1G,g);H P}$("#1U").s(12)}3(!(R==="")){9 1i(){$("#8").C();$("v").q("<4 5=\'8\'></4>");19(1z,1x,g);H P}$("#1X").s(1i)}o.1h=9(e){3(e==M){I=2w.2v}n{I=e.2u}3(I==27){G()}n 3(I==3k){3(!(R=="")){o.1h="";1i()}}n 3(I==3j){3(!(14=="")){o.1h="";12()}}};16();$("#K").C();$("#1L").s(G);$("#8").r({Y:"T"})};S.L=f}n{6 l=f.2r(/^[^\\?]+\\??/,\'\');6 m=2p(l);13=(m[\'z\']*1)+30||3h;1a=(m[\'A\']*1)+3g||3f;W=13-30;V=1a-3e;3(f.O(\'2j\')!=-1){1E=f.1B(\'3d\');$("#15").C();3(m[\'1A\']!="1b"){$("#8").q("<4 5=\'2f\'><4 5=\'1H\'>"+d+"</4><4 5=\'2e\'><a u=\'#\' 5=\'Z\' Q=\'1o\'>1l</a> 1k 1j 1s</4></4><U 1W=\'0\' 2d=\'0\' L=\'"+1E[0]+"\' 5=\'15\' 1v=\'15"+1f.2c(1f.1y()*2b)+"\' 1g=\'1m()\' J=\'z:"+(W+29)+"p;A:"+(V+17)+"p;\' > </U>")}n{$("#B").N();$("#8").q("<U 1W=\'0\' 2d=\'0\' L=\'"+1E[0]+"\' 5=\'15\' 1v=\'15"+1f.2c(1f.1y()*2b)+"\' 1g=\'1m()\' J=\'z:"+(W+29)+"p;A:"+(V+17)+"p;\'> </U>")}}n{3($("#8").r("Y")!="T"){3(m[\'1A\']!="1b"){$("#8").q("<4 5=\'2f\'><4 5=\'1H\'>"+d+"</4><4 5=\'2e\'><a u=\'#\' 5=\'Z\'>1l</a> 1k 1j 1s</4></4><4 5=\'F\' J=\'z:"+W+"p;A:"+V+"p\'></4>")}n{$("#B").N();$("#8").q("<4 5=\'F\' 3c=\'3b\' J=\'z:"+W+"p;A:"+V+"p;\'></4>")}}n{$("#F")[0].J.z=W+"p";$("#F")[0].J.A=V+"p";$("#F")[0].3a=0;$("#1H").11(d)}}$("#Z").s(G);3(f.O(\'37\')!=-1){$("#F").q($(\'#\'+m[\'26\']).1T());$("#8").24(9(){$(\'#\'+m[\'26\']).q($("#F").1T())});16();$("#K").C();$("#8").r({Y:"T"})}n 3(f.O(\'2j\')!=-1){16();3($.1q.35){$("#K").C();$("#8").r({Y:"T"})}}n{$("#F").34(f+="&1y="+(1p 33().32()),9(){16();$("#K").C();1u("#F a.18");$("#8").r({Y:"T"})})}}3(!m[\'1A\']){o.21=9(e){3(e==M){I=2w.2v}n{I=e.2u}3(I==27){G()}}}}31(e){}}9 1m(){$("#K").C();$("#8").r({Y:"T"})}9 G(){$("#2Y").N("s");$("#Z").N("s");$("#8").2X("2W",9(){$(\'#8,#B,#1F\').2V("24").N().C()});$("#K").C();3(2t o.v.J.2i=="2g"){$("v","11").r({A:"1Z",z:"1Z"});$("11").r("22","")}o.1h="";o.21="";H P}9 16(){$("#8").r({2U:\'-\'+20((13/2),10)+\'p\',z:13+\'p\'});3(!(1V.1q.2Q&&1V.1q.2P<7)){$("#8").r({38:\'-\'+20((1a/2),10)+\'p\'})}}9 2p(a){6 b={};3(!a){H b}6 c=a.1B(/[;&]/);25(6 i=0;i<c.1c;i++){6 d=c[i].1B(\'=\');3(!d||d.1c!=2){39}6 e=2a(d[0]);6 f=2a(d[1]);f=f.2r(/\\+/g,\' \');b[e]=f}H b}9 2x(){6 a=o.2M;6 w=1S.2o||1R.2o||(a&&a.1Q)||o.v.1Q;6 h=1S.1P||1R.1P||(a&&a.2n)||o.v.2n;1O=[w,h];H 1O}9 1K(){6 a=2K.2J.1C();3(a.O(\'2I\')!=-1&&a.O(\'3o\')!=-1){H 1b}}',62,211,'|||if|div|id|var||TB_window|function||||||||||||||else|document|px|append|css|click||href|body||||width|height|TB_overlay|remove|TB_Counter|TB_TempArray|TB_ajaxContent|tb_remove|return|keycode|style|TB_load|src|null|unbind|indexOf|false|title|TB_NextHTML|imgPreloader|block|iframe|ajaxContentH|ajaxContentW|this|display|TB_closeWindowButton||html|goPrev|TB_WIDTH|TB_PrevHTML|TB_iframeContent|tb_position||thickbox|tb_show|TB_HEIGHT|true|length|nbsp|span|Math|onload|onkeydown|goNext|Esc|or|close|tb_showIframe|TB_imageCount|Close|new|browser|TB_FoundURL|Key|Image|tb_init|name|imgLoader|TB_NextURL|random|TB_NextCaption|modal|split|toLowerCase|TB_PrevCaption|urlNoQuery|TB_HideSelect|TB_PrevURL|TB_ajaxWindowTitle|img|addClass|tb_detectMacXFF|TB_ImageOff|150|rel|arrayPageSize|innerHeight|clientWidth|self|window|children|TB_prev|jQuery|frameborder|TB_next|getElementById|auto|parseInt|onkeyup|overflow|alt|unload|for|inlineId||100||unescape|1000|round|hspace|TB_closeAjaxWindow|TB_title|undefined|match|maxHeight|TB_iframe|bmp|gif|png|clientHeight|innerWidth|tb_parseQuery|jpeg|replace|jpg|typeof|which|keyCode|event|tb_getPageSize|show|TB_overlayBG|TB_closeWindow|TB_overlayMacFFBGHack|TB_secondLine|TB_caption|blur|TB_Image|60|tb_pathToImage|mac|userAgent|navigator|of|documentElement|Prev|lt|version|msie|gt|ready|Next|marginLeft|trigger|fast|fadeOut|TB_imageOff|hidden||catch|getTime|Date|load|safari|get|TB_inline|marginTop|continue|scrollTop|TB_modal|class|TB_|45|440|40|630|input|188|190|substr|try|area|firefox'.split('|'),0,{}))
\ No newline at end of file
diff --git a/plugins/Autocomplete/jquery-autocomplete/lib/thickbox.css b/plugins/Autocomplete/jquery-autocomplete/lib/thickbox.css
new file mode 100644 (file)
index 0000000..d24b9be
--- /dev/null
@@ -0,0 +1,163 @@
+/* ----------------------------------------------------------------------------------------------------------------*/
+/* ---------->>> global settings needed for thickbox <<<-----------------------------------------------------------*/
+/* ----------------------------------------------------------------------------------------------------------------*/
+*{padding: 0; margin: 0;}
+
+/* ----------------------------------------------------------------------------------------------------------------*/
+/* ---------->>> thickbox specific link and font settings <<<------------------------------------------------------*/
+/* ----------------------------------------------------------------------------------------------------------------*/
+#TB_window {
+       font: 12px Arial, Helvetica, sans-serif;
+       color: #333333;
+}
+
+#TB_secondLine {
+       font: 10px Arial, Helvetica, sans-serif;
+       color:#666666;
+}
+
+#TB_window a:link {color: #666666;}
+#TB_window a:visited {color: #666666;}
+#TB_window a:hover {color: #000;}
+#TB_window a:active {color: #666666;}
+#TB_window a:focus{color: #666666;}
+
+/* ----------------------------------------------------------------------------------------------------------------*/
+/* ---------->>> thickbox settings <<<-----------------------------------------------------------------------------*/
+/* ----------------------------------------------------------------------------------------------------------------*/
+#TB_overlay {
+       position: fixed;
+       z-index:100;
+       top: 0px;
+       left: 0px;
+       height:100%;
+       width:100%;
+}
+
+.TB_overlayMacFFBGHack {background: url(macFFBgHack.png) repeat;}
+.TB_overlayBG {
+       background-color:#000;
+       filter:alpha(opacity=75);
+       -moz-opacity: 0.75;
+       opacity: 0.75;
+}
+
+* html #TB_overlay { /* ie6 hack */
+     position: absolute;
+     height: expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px');
+}
+
+#TB_window {
+       position: fixed;
+       background: #ffffff;
+       z-index: 102;
+       color:#000000;
+       display:none;
+       border: 4px solid #525252;
+       text-align:left;
+       top:50%;
+       left:50%;
+}
+
+* html #TB_window { /* ie6 hack */
+position: absolute;
+margin-top: expression(0 - parseInt(this.offsetHeight / 2) + (TBWindowMargin = document.documentElement && document.documentElement.scrollTop || document.body.scrollTop) + 'px');
+}
+
+#TB_window img#TB_Image {
+       display:block;
+       margin: 15px 0 0 15px;
+       border-right: 1px solid #ccc;
+       border-bottom: 1px solid #ccc;
+       border-top: 1px solid #666;
+       border-left: 1px solid #666;
+}
+
+#TB_caption{
+       height:25px;
+       padding:7px 30px 10px 25px;
+       float:left;
+}
+
+#TB_closeWindow{
+       height:25px;
+       padding:11px 25px 10px 0;
+       float:right;
+}
+
+#TB_closeAjaxWindow{
+       padding:7px 10px 5px 0;
+       margin-bottom:1px;
+       text-align:right;
+       float:right;
+}
+
+#TB_ajaxWindowTitle{
+       float:left;
+       padding:7px 0 5px 10px;
+       margin-bottom:1px;
+}
+
+#TB_title{
+       background-color:#e8e8e8;
+       height:27px;
+}
+
+#TB_ajaxContent{
+       clear:both;
+       padding:2px 15px 15px 15px;
+       overflow:auto;
+       text-align:left;
+       line-height:1.4em;
+}
+
+#TB_ajaxContent.TB_modal{
+       padding:15px;
+}
+
+#TB_ajaxContent p{
+       padding:5px 0px 5px 0px;
+}
+
+#TB_load{
+       position: fixed;
+       display:none;
+       height:13px;
+       width:208px;
+       z-index:103;
+       top: 50%;
+       left: 50%;
+       margin: -6px 0 0 -104px; /* -height/2 0 0 -width/2 */
+}
+
+* html #TB_load { /* ie6 hack */
+position: absolute;
+margin-top: expression(0 - parseInt(this.offsetHeight / 2) + (TBWindowMargin = document.documentElement && document.documentElement.scrollTop || document.body.scrollTop) + 'px');
+}
+
+#TB_HideSelect{
+       z-index:99;
+       position:fixed;
+       top: 0;
+       left: 0;
+       background-color:#fff;
+       border:none;
+       filter:alpha(opacity=0);
+       -moz-opacity: 0;
+       opacity: 0;
+       height:100%;
+       width:100%;
+}
+
+* html #TB_HideSelect { /* ie6 hack */
+     position: absolute;
+     height: expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px');
+}
+
+#TB_iframeContent{
+       clear:both;
+       border:none;
+       margin-bottom:-1px;
+       margin-top:1px;
+       _margin-bottom:1px;
+}
diff --git a/plugins/Autocomplete/jquery-autocomplete/todo b/plugins/Autocomplete/jquery-autocomplete/todo
new file mode 100644 (file)
index 0000000..a8f03af
--- /dev/null
@@ -0,0 +1,166 @@
+TODO\r
+\r
+- test formatItem implementation that returns (clickable) anchors\r
+- bug: handle del key; eg. type a letter, remove it using del, type same letter again: nothing happens\r
+- handle up/down keys in textarea (prevent default while select is open?)\r
+- docs: max:0 works, too, "removing" it(??)\r
+- fix ac_loading/options.loadingClass\r
+- support/enable request urls like foo/bar/10 instead of foo/q=10\r
+- urlencode request term before passing to $.ajax/data; evaluate why $.ajax doesn't handle that itself, if at all; try with umlauts, russian/danish/chinese characeters (see validate)\r
+- test what happens when an element gets focused programmatically (maybe even before then autcomplete is applied)\r
+- check if blur on selecting can be removed\r
+- fix keyhandling to ignore metakeys, eg. shift; especially important for chinese characters that need more then one key\r
+- enhance mustMatch: provide event/callback when a value gets deleted\r
+- handle tab key different then enter, eg. don't blur field or prevent default, just let it move on; in any case, no need to blur the field when selecting a value via tab, unlike return\r
+- prevent redundant requests on\r
+  - superstring returned no result, no need to query again for substring, eg. pete returned nothing, peter won't either\r
+  - previous query mustn't be requested again, eg. pete returns 10 lines, peter nothing, backspace to pete should get the 10 lines from cache (may need TimeToLive setting for cache to invalidate it)\r
+- incorporate improvements and suggestions by Hector: http://beta.winserver.com/public/test/MultiSuggestTest.wct\r
+- json support: An optional JSON format, that assumes a certain JSON format as default and just looks for a dataType "json" to be activated; [records], where each record is { id:String, label:String, moreOptionalValues... } \r
+- accept callback as first argument to let users implement their own dynamic data (no caching) - consider async API\r
+- allow users to keep their incomplete value when pressing tab, just mimic the default-browser-autocomplete: tab doesn't select any proposed value -> tab closes the select and works normal otherwise\r
+- small bug in your autocomplete,  When setting autoFill:true I would expect formatResult to be called on autofill, it seems not to be the case.\r
+- add a callback to allow decoding the response\r
+- allow modification of not-last value in multiple-fields\r
+@option Number size Limit the number of items to show at once. Default: \r
+@option Function parse - TEST AND DOCUMENT ME\r
+- add option to display selectbox on focus\r
+\r
+$input.bind("show", function() {\r
+       if ( !select.visible() ) {\r
+               onChange(0, true);\r
+       }\r
+});\r
+\r
+- reference: http://capxous.com/\r
+  - add "try ..." hints to demo\r
+  - check out demos\r
+- reference: http://createwebapp.com/demo/\r
+  \r
+- add option to hide selectbox when no match is found - see comment by Ian on plugin page (14. Juli 2007 04:31)\r
+- add example for reinitializing an autocomplete using unbind()\r
+\r
+- Add option to pass through additional arguments to $.ajax, like type to use POST instead of GET\r
+\r
+ - I found out that the problem with UTF-8 not being correctly sent can be solved on the server side by applying (PHP) rawurldecode() function, which decodes the Unicode characters sent by GET method and therefore URL-encoded.\r
+-> add that hint to docs and examples\r
+\r
+But I am trying this with these three values: “foo bar”, “foo foo”, and “foo far”, and if I enter “b” (or “ba”) nothing matches, if I enter “f” all three do match, and if I enter “fa” the last one matches.\r
+The problem seems to be that the cache is implemented with a first-character hashtable, so only after matching the first character, the latter ones are searched for.\r
+\r
+xml example:\r
+<script type="text/javascript">\r
+      function parseXML(data) {\r
+        var results = [];\r
+        var branches = $(data).find('item');\r
+        $(branches).each(function() {\r
+          var text = $.trim($(this).find('text').text());\r
+          var value = $.trim($(this).find('value').text());\r
+          //console.log(text);\r
+          //console.log(value);\r
+          results[results.length] = {'data': this, 'result': value, 'value': text};\r
+        });\r
+        $(results).each(function() {\r
+          //console.log('value', this.value);\r
+          //console.log('text', this.text);\r
+        });\r
+        //console.log(results);\r
+        return results;\r
+      };\r
+    $(YourOojHere).autocomplete(SERVER_AJAX_URL, {parse: parseXML});\r
+  </script>\r
+<?xml version="1.0"?>\r
+<ajaxresponse>\r
+  <item>\r
+    <text>\r
+      <![CDATA[<b>FreeNode:</b> irc.freenode.net:6667]]>\r
+    </text>\r
+    <value><![CDATA[irc.freenode.net:6667]]></value>\r
+  </item><item>\r
+    <text>\r
+      <![CDATA[<b>irc.oftc.net</b>:6667]]>\r
+    </text>\r
+    <value><![CDATA[irc.oftc.net:6667]]></value>\r
+  </item><item>\r
+    <text>\r
+      <![CDATA[<b>irc.undernet.org</b>:6667]]>\r
+    </text>\r
+    <value><![CDATA[irc.undernet.org:6667]]></value>\r
+  </item>\r
+</ajaxresponse>\r
+  \r
+  \r
+\r
+Hi all,\r
+\r
+I use Autocomplete 1.0 Alpha mostly for form inputs bound to foreign\r
+key columns. For instance I have a user_position table with two\r
+columns: user_id and position_id. On new appointment form I have two\r
+autocomplete text inputs with the following code:\r
+\r
+   <input type="text" id="user_id" class="ac_input"  tabindex="1" />\r
+   <input type="text" id="position_id" class="ac_input" tabindex="2" />\r
+\r
+As you can see the inputs do not have a name attribute, and when the\r
+form is submitted their values are not sent, which is all right since\r
+they will contain strings like:\r
+\r
+   'John Doe'\r
+   'Sales Manager'\r
+\r
+whereas our backend expects something like:\r
+\r
+   23\r
+   14\r
+\r
+which are the user_id for John Doe and position_id for Sales Manager.\r
+To send these values I have two hidden inputs in the form like this:\r
+\r
+   <input type="hidden" name="user_id" value="">\r
+   <input type="hidden" name="position_id" value="">\r
+\r
+Also I have the following code in the $().ready function:\r
+\r
+   $("#user_id").result(function(event, data, formatted) {\r
+     $("input[@name=user_id]").val(data[1]);\r
+   });\r
+   $("#position_id").result(function(event, data, formatted) {\r
+     $("input[@name=position_id]").val(data[1]);\r
+   });\r
+\r
+As could be seen these functions stuff user_id and position_id values\r
+(in our example 23 and 14) into the hidden inputs, and when the form\r
+is submitted these values are sent:\r
+\r
+   user_id = 23\r
+   position_id = 14\r
+\r
+The backend script then takes care of adding a record to our\r
+user_position table containing those values.\r
+\r
+I wonder how could the plugin code be modified to simplify the setup\r
+by taking care of adding hidden inputs and updating the value of\r
+hidden inputs as default behavior. I have successfully attempted a\r
+simpler solution - writing a wrapper to perform these additional tasks\r
+and invoke autocomplete as well. I hope my intention is clear enough,\r
+if not, this is exactly the expected outcome:\r
+\r
+Before:\r
+\r
+   <script type="text/javascript"\r
+   src="jquery.autocomplete-modified.js"></script>\r
+   <input type="text" name="user_id" class="ac_input" tabindex="1" />\r
+\r
+After:\r
+\r
+   <input type="text" id="user_id" class="ac_input" tabindex="1" />\r
+   <input type="hidden" name="user_id" value="23">\r
+\r
+\r
+Last word, I know this looks like a tall order, and I do not hope\r
+someone will make a complete working mod for me, but rather would very\r
+much appreciate helpful  advise and directions.\r
+\r
+Many thanks in advance\r
+Majid\r
+\r
diff --git a/plugins/Autocomplete/readme.txt b/plugins/Autocomplete/readme.txt
new file mode 100644 (file)
index 0000000..3272aa1
--- /dev/null
@@ -0,0 +1,6 @@
+Autocomplete allows users to autocomplete screen names in @ replies. When an "@" is typed into the notice text area, an autocomplete box is displayed populated with the user's friends' screen names.
+
+Installation
+============
+Add "addPlugin('Autocomplete');" to the bottom of your config.php
+That's it!
index 57c98b4f14370e24df0e4fd93963295d936a6dae..19251cca4d0dd6b74897bacf80d7e40b6fdedbe6 100644 (file)
@@ -47,9 +47,7 @@ class FBC_XDReceiverAction extends Action
         header('Expires:');
         header('Pragma:');
 
-        $this->startXML('html',
-                        '-//W3C//DTD XHTML 1.0 Strict//EN',
-                        'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
+        $this->startXML('html');
 
         $language = $this->getLanguage();
 
@@ -58,10 +56,7 @@ class FBC_XDReceiverAction extends Action
                                           'lang' => $language));
         $this->elementStart('head');
         $this->element('title', null, 'cross domain receiver page');
-        $this->element('script',
-            array('src' =>
-                'http://static.ak.connect.facebook.com/js/api_lib/v0.4/XdCommReceiver.debug.js',
-                'type' => 'text/javascript'), '');
+        $this->script('http://static.ak.connect.facebook.com/js/api_lib/v0.4/XdCommReceiver.debug.js');
         $this->elementEnd('head');
         $this->elementStart('body');
         $this->elementEnd('body');
index 3cf9fefc1380fa5bb958040bb2642334184a40a3..6191d9ea64e6bb486ddf789303efb86fefc79902 100644 (file)
@@ -31,27 +31,25 @@ require_once INSTALLDIR . '/plugins/FBConnect/FBConnectPlugin.php';
 
 class FBConnectauthAction extends Action
 {
-
     var $fbuid      = null;
     var $fb_fields  = null;
 
     function prepare($args) {
         parent::prepare($args);
 
-        try {
+        $this->fbuid = getFacebook()->get_loggedin_user();
 
-            $this->fbuid = getFacebook()->get_loggedin_user();
+        if ($this->fbuid > 0) {
+            $this->fb_fields = $this->getFacebookFields($this->fbuid,
+                                                        array('first_name', 'last_name', 'name'));
+        } else {
+            list($proxy, $ip) = common_client_ip();
 
-            if ($this->fbuid > 0) {
-                $this->fb_fields = $this->getFacebookFields($this->fbuid,
-                    array('first_name', 'last_name', 'name'));
-            } else {
-                common_debug("No Facebook User found.");
-            }
+            common_log(LOG_WARNING, 'Facebook Connect Plugin - ' .
+                       "Failed auth attempt, proxy = $proxy, ip = $ip.");
 
-        } catch (Exception $e) {
-            common_log(LOG_WARNING, 'Problem getting Facebook uid: ' .
-                $e->getMessage());
+            $this->clientError(_('You must be logged into Facebook to ' .
+                                 'use Facebook Connect.'));
         }
 
         return true;
@@ -69,8 +67,9 @@ class FBConnectauthAction extends Action
             if (!empty($flink)) {
 
                 // User already has a linked Facebook account and shouldn't be here
-                common_debug('There is already a local user (' . $flink->user_id .
-                    ') linked with this Facebook (' . $this->fbuid . ').');
+                common_debug('Facebook Connect Plugin - ' .
+                             'There is already a local user (' . $flink->user_id .
+                             ') linked with this Facebook (' . $this->fbuid . ').');
 
                 // We don't want these cookies
                 getFacebook()->clear_cookie_state();
@@ -101,7 +100,8 @@ class FBConnectauthAction extends Action
             } else if ($this->arg('connect')) {
                 $this->connectNewUser();
             } else {
-                common_debug(print_r($this->args, true), __FILE__);
+                common_debug('Facebook Connect Plugin - ' .
+                             print_r($this->args, true));
                 $this->showForm(_('Something weird happened.'),
                                 $this->trimmed('newname'));
             }
@@ -211,7 +211,6 @@ class FBConnectauthAction extends Action
 
     function createNewUser()
     {
-
         if (common_config('site', 'closed')) {
             $this->clientError(_('Registration not allowed.'));
             return;
@@ -238,7 +237,7 @@ class FBConnectauthAction extends Action
 
         if (!Validate::string($nickname, array('min_length' => 1,
                                                'max_length' => 64,
-                                               'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
+                                               'format' => NICKNAME_FMT))) {
             $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.'));
             return;
         }
@@ -274,7 +273,8 @@ class FBConnectauthAction extends Action
         common_set_user($user);
         common_real_login(true);
 
-        common_debug("Registered new user $user->id from Facebook user $this->fbuid");
+        common_debug('Facebook Connect Plugin - ' .
+                     "Registered new user $user->id from Facebook user $this->fbuid");
 
         common_redirect(common_local_url('showstream', array('nickname' => $user->nickname)),
                         303);
@@ -292,8 +292,9 @@ class FBConnectauthAction extends Action
 
         $user = User::staticGet('nickname', $nickname);
 
-        if ($user) {
-            common_debug("Legit user to connect to Facebook: $nickname");
+        if (!empty($user)) {
+            common_debug('Facebook Connect Plugin - ' .
+                         "Legit user to connect to Facebook: $nickname");
         }
 
         $result = $this->flinkUser($user->id, $this->fbuid);
@@ -303,7 +304,8 @@ class FBConnectauthAction extends Action
             return;
         }
 
-        common_debug("Connected Facebook user $this->fbuid to local user $user->id");
+        common_debug('Facebook Connnect Plugin - ' .
+                     "Connected Facebook user $this->fbuid to local user $user->id");
 
         common_set_user($user);
         common_real_login(true);
@@ -317,12 +319,13 @@ class FBConnectauthAction extends Action
 
         $result = $this->flinkUser($user->id, $this->fbuid);
 
-        if (!$result) {
+        if (empty($result)) {
             $this->serverError(_('Error connecting user to Facebook.'));
             return;
         }
 
-        common_debug("Connected Facebook user $this->fbuid to local user $user->id");
+        common_debug('Facebook Connect Plugin - ' .
+                     "Connected Facebook user $this->fbuid to local user $user->id");
 
         // Return to Facebook connection settings tab
         common_redirect(common_local_url('FBConnectSettings'), 303);
@@ -330,16 +333,18 @@ class FBConnectauthAction extends Action
 
     function tryLogin()
     {
-        common_debug("Trying Facebook Login...");
+        common_debug('Facebook Connect Plugin - ' .
+                     "Trying login for Facebook user $this->fbuid.");
 
         $flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_CONNECT_SERVICE);
 
-        if ($flink) {
+        if (!empty($flink)) {
             $user = $flink->getUser();
 
             if (!empty($user)) {
 
-                common_debug("Logged in Facebook user $flink->foreign_id as user $user->id ($user->nickname)");
+                common_debug('Facebook Connect Plugin - ' .
+                             "Logged in Facebook user $flink->foreign_id as user $user->id ($user->nickname)");
 
                 common_set_user($user);
                 common_real_login(true);
@@ -348,7 +353,8 @@ class FBConnectauthAction extends Action
 
         } else {
 
-            common_debug("No flink found for fbuid: $this->fbuid");
+            common_debug('Facebook Connect Plugin - ' .
+                         "No flink found for fbuid: $this->fbuid - new user");
 
             $this->showForm(null, $this->bestNewNickname());
         }
@@ -418,7 +424,7 @@ class FBConnectauthAction extends Action
     {
         if (!Validate::string($str, array('min_length' => 1,
                                           'max_length' => 64,
-                                          'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
+                                          'format' => NICKNAME_FMT))) {
             return false;
         }
         if (!User::allowed_nickname($str)) {
@@ -444,7 +450,8 @@ class FBConnectauthAction extends Action
             return reset($infos);
 
         } catch (Exception $e) {
-            common_log(LOG_WARNING, "Facebook client failure when requesting " .
+            common_log(LOG_WARNING, 'Facebook Connect Plugin - ' .
+                       "Facebook client failure when requesting " .
                 join(",", $fields) . " on uid " . $fb_uid .
                     " : ". $e->getMessage());
             return null;
index 6788793b25c6bd4ddd066802007a8db33a7d1a23..c1bd1c0944673d63f1a7577c5addb91e700048ce 100644 (file)
@@ -82,9 +82,7 @@ class FBConnectPlugin extends Plugin
 
             $action->extraHeaders();
 
-            $action->startXML('html',
-                '-//W3C//DTD XHTML 1.0 Strict//EN',
-                'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
+            $action->startXML('html');
 
             $language = $action->getLanguage();
 
@@ -118,13 +116,13 @@ class FBConnectPlugin extends Plugin
             // but we actually do, for IE and Safari. Gar.
 
             $html = sprintf('<script type="text/javascript">
-                                window.onload = function () {
+                                $(document).ready(function () {
                                     FB_RequireFeatures(
                                         ["XFBML"],
                                             function() {
                                                 FB.init("%s", "../xd_receiver.html");
                                             }
-                                        ); }
+                                        ); });
 
                                 function goto_login() {
                                     window.location = "%s";
@@ -146,11 +144,7 @@ class FBConnectPlugin extends Plugin
     function onEndShowFooter($action)
     {
         if ($this->reqFbScripts($action)) {
-
-            $action->element('script',
-                array('type' => 'text/javascript',
-                      'src'  => 'http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php'),
-                      '');
+            $action->script('http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php');
         }
     }
 
@@ -158,10 +152,7 @@ class FBConnectPlugin extends Plugin
     {
 
         if ($this->reqFbScripts($action)) {
-
-            $action->element('link', array('rel' => 'stylesheet',
-                'type' => 'text/css',
-                'href' => common_path('plugins/FBConnect/FBConnectPlugin.css')));
+            $action->cssLink('plugins/FBConnect/FBConnectPlugin.css');
         }
     }
 
@@ -223,7 +214,7 @@ class FBConnectPlugin extends Plugin
                     $fbuid    = $facebook->get_loggedin_user();
 
                 } catch (Exception $e) {
-                    common_log(LOG_WARNING,
+                    common_log(LOG_WARNING, 'Facebook Connect Plugin - ' .
                         'Problem getting Facebook user: ' .
                             $e->getMessage());
                 }
@@ -351,7 +342,7 @@ class FBConnectPlugin extends Plugin
     }
 
     function onStartLogout($action)
-    {
+{
         $action->logout();
         $fbuid = $this->loggedIn();
 
@@ -360,8 +351,9 @@ class FBConnectPlugin extends Plugin
                 $facebook = getFacebook();
                 $facebook->expire_session();
             } catch (Exception $e) {
-                common_log(LOG_WARNING, 'Could\'t logout of Facebook: ' .
-                    $e->getMessage());
+                common_log(LOG_WARNING, 'Facebook Connect Plugin - ' .
+                           'Could\'t logout of Facebook: ' .
+                           $e->getMessage());
             }
         }
 
@@ -385,7 +377,8 @@ class FBConnectPlugin extends Plugin
             }
 
         } catch (Exception $e) {
-            common_log(LOG_WARNING, "Facebook client failure requesting profile pic!");
+            common_log(LOG_WARNING, 'Facebook Connect Plugin - ' .
+                       "Facebook client failure requesting profile pic!");
         }
 
        return $url;
index 034ecebae21b74df88c349435dd14e208d000ee2..d1bea0854ea464619f8e04f6223f317175d10db7 100644 (file)
@@ -186,9 +186,9 @@ class FBConnectSettingsAction extends ConnectSettingsAction
                 $facebook->clear_cookie_state();
 
             } catch (Exception $e) {
-                common_log(LOG_WARNING,
-                    'Couldn\'t clear Facebook cookies: ' .
-                        $e->getMessage());
+                common_log(LOG_WARNING, 'Facebook Connect Plugin - ' .
+                           'Couldn\'t clear Facebook cookies: ' .
+                           $e->getMessage());
             }
 
             $this->showForm(_('You have disconnected from Facebook.'), true);
index 914b774cb77c4d721c1e02b4d899453546c69dcb..11e5ce45371f03a3943ba33954cbcfb4ed85f6fc 100644 (file)
@@ -43,8 +43,7 @@ API key and secret to your Laconica config.php file:
 Finally, to enable the plugin, add the following stanza to your
 config.php:
 
-    require_once(INSTALLDIR.'/plugins/FBConnect/FBConnectPlugin.php');
-    $fbc = new FBConnectPlugin();
+    addPlugin('FBConnect');
 
 To try out the plugin, fire up your browser and connect to:
 
diff --git a/plugins/InfiniteScroll/InfiniteScrollPlugin.php b/plugins/InfiniteScroll/InfiniteScrollPlugin.php
new file mode 100644 (file)
index 0000000..1e4a03e
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Plugin to enable Infinite Scrolling
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Plugin
+ * @package   Laconica
+ * @author    Craig Andrews <candrews@integralblue.com>
+ * @copyright 2009 Craig Andrews http://candrews.integralblue.com
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+class InfiniteScrollPlugin extends Plugin
+{
+    function __construct()
+    {
+        parent::__construct();
+    }
+
+    function onEndShowScripts($action)
+    {
+        $action->script('plugins/InfiniteScroll/jquery.infinitescroll.min.js');
+        $action->script('plugins/InfiniteScroll/infinitescroll.js');
+    }
+}
diff --git a/plugins/InfiniteScroll/ajax-loader.gif b/plugins/InfiniteScroll/ajax-loader.gif
new file mode 100644 (file)
index 0000000..a576ecd
Binary files /dev/null and b/plugins/InfiniteScroll/ajax-loader.gif differ
diff --git a/plugins/InfiniteScroll/infinitescroll.js b/plugins/InfiniteScroll/infinitescroll.js
new file mode 100644 (file)
index 0000000..6513072
--- /dev/null
@@ -0,0 +1,15 @@
+jQuery(document).ready(function($){
+  $('notices_primary').infinitescroll({
+    debug: true,
+    nextSelector    : "li.nav_next a",
+    loadingImg      : $('address .url')[0].href+'plugins/InfiniteScroll/ajax-loader.gif',
+    text            : "<em>Loading the next set of posts...</em>",
+    donetext        : "<em>Congratulations, you\'ve reached the end of the Internet.</em>",
+    navSelector     : "div.pagination",
+    contentSelector : "#notices_primary ol.notices",
+    itemSelector    : "#notices_primary ol.notices li"
+    },function(){
+        NoticeAttachments();
+    });
+});
+
diff --git a/plugins/InfiniteScroll/jquery.infinitescroll.js b/plugins/InfiniteScroll/jquery.infinitescroll.js
new file mode 100644 (file)
index 0000000..670686b
--- /dev/null
@@ -0,0 +1,251 @@
+
+/*!
+// Infinite Scroll jQuery plugin
+// copyright Paul Irish, licensed GPL & MIT
+// version 1.2.090804
+
+// home and docs: http://www.infinite-scroll.com
+*/
+
+// todo: add preloading option.
+;(function($){
+    
+  $.fn.infinitescroll = function(options,callback){
+    
+    // console log wrapper.
+    function debug(){
+      if (opts.debug) { window.console && console.log.call(console,arguments)}
+    }
+    
+    // grab each selector option and see if any fail.
+    function areSelectorsValid(opts){
+      for (var key in opts){
+        if (key.indexOf && key.indexOf('Selector') && $(opts[key]).length === 0){
+            debug('Your ' + key + ' found no elements.');    
+            return false;
+        } 
+        return true;
+      }
+    }
+
+
+    // find the number to increment in the path.
+    function determinePath(path){
+      
+      path.match(relurl) ? path.match(relurl)[2] : path; 
+
+      // there is a 2 in the url surrounded by slashes, e.g. /page/2/
+      if ( path.match(/^(.*?)\b2\b(.*?$)/) ){  
+          path = path.match(/^(.*?)\b2\b(.*?$)/).slice(1);
+      } else 
+        // if there is any 2 in the url at all.
+        if (path.match(/^(.*?)2(.*?$)/)){
+          debug('Trying backup next selector parse technique. Treacherous waters here, matey.');
+          path = path.match(/^(.*?)2(.*?$)/).slice(1);
+      } else {
+        debug('Sorry, we couldn\'t parse your Next (Previous Posts) URL. Verify your the css selector points to the correct A tag. If you still get this error: yell, scream, and kindly ask for help at infinite-scroll.com.');    
+        props.isInvalidPage = true;  //prevent it from running on this page.
+      }
+      
+      return path;
+    }
+
+
+    // 'document' means the full document usually, but sometimes the content of the overflow'd div in local mode
+    function getDocumentHeight(){
+      // weird doubletouch of scrollheight because http://soulpass.com/2006/07/24/ie-and-scrollheight/
+      return opts.localMode ? ($(props.container)[0].scrollHeight && $(props.container)[0].scrollHeight) 
+                                // needs to be document's height. (not props.container's) html's height is wrong in IE.
+                                : $(document).height()
+    }
+    
+    
+        
+    function isNearBottom(opts,props){
+      
+      // distance remaining in the scroll
+      // computed as: document height - distance already scroll - viewport height - buffer
+      var pixelsFromWindowBottomToBottom = getDocumentHeight()  -
+                                            (opts.localMode ? $(props.container).scrollTop() : 
+                                              // have to do this bs because safari doesnt report a scrollTop on the html element
+                                              ($(props.container).scrollTop() || $(props.container.ownerDocument.body).scrollTop())) - 
+                                            $(opts.localMode ? props.container : window).height();
+      
+      debug('math:',pixelsFromWindowBottomToBottom, props.pixelsFromNavToBottom);
+      
+      // if distance remaining in the scroll (including buffer) is less than the orignal nav to bottom....
+      return (pixelsFromWindowBottomToBottom  - opts.bufferPx < props.pixelsFromNavToBottom);    
+    }    
+    
+    function showDoneMsg(){
+      props.loadingMsg
+        .find('img').hide()
+        .parent()
+          .find('div').html(opts.donetext).animate({opacity: 1},2000).fadeOut('normal');
+      
+      // user provided callback when done    
+      opts.errorCallback();
+    }
+    
+    function infscrSetup(path,opts,props,callback){
+    
+        if (props.isDuringAjax || props.isInvalidPage || props.isDone) return; 
+    
+               if ( !isNearBottom(opts,props) ) return; 
+                 
+               // we dont want to fire the ajax multiple times
+               props.isDuringAjax = true; 
+               
+               // show the loading message and hide the previous/next links
+               props.loadingMsg.appendTo( opts.contentSelector ).show();
+               $( opts.navSelector ).hide(); 
+               
+               // increment the URL bit. e.g. /page/3/
+               props.currPage++;
+               
+               debug('heading into ajax',path);
+               
+               // if we're dealing with a table we can't use DIVs
+               var box = $(opts.contentSelector).is('table') ? $('<tbody/>') : $('<div/>');  
+               
+               box
+                 .attr('id','infscr-page-'+props.currPage)
+                 .addClass('infscr-pages')
+                 .appendTo( opts.contentSelector )
+                 .load( path.join( props.currPage ) + ' ' + opts.itemSelector,null,function(){
+                   
+                       // if we've hit the last page...
+                       if (props.isDone){ 
+                    showDoneMsg();
+                                     return false;    
+                                     
+                   } else {
+                     
+                       // if it didn't return anything
+                       if (box.children().length == 0){
+                         // fake an ajaxError so we can quit.
+                         $.event.trigger( "ajaxError", [{status:404}] ); 
+                       } 
+                       
+                       // fadeout currently makes the <em>'d text ugly in IE6
+                           props.loadingMsg.fadeOut('normal' ); 
+  
+                           // smooth scroll to ease in the new content
+                           if (opts.animate){ 
+                           var scrollTo = $(window).scrollTop() + $('#infscr-loading').height() + opts.extraScrollPx + 'px';
+                      $('html,body').animate({scrollTop: scrollTo}, 800,function(){ props.isDuringAjax = false; }); 
+                           }
+                    
+                    // pass in the new DOM element as context for the callback
+                    callback.call( box[0] );
+                    
+                           if (!opts.animate) props.isDuringAjax = false; // once the call is done, we can allow it again.
+                   }
+                   }); // end of load()
+                       
+                   
+      }  // end of infscrSetup()
+          
+  
+    
+      
+    // lets get started.
+    
+    var opts    = $.extend({}, $.infinitescroll.defaults, options);
+    var props   = $.infinitescroll; // shorthand
+    callback    = callback || function(){};
+    
+    if (!areSelectorsValid(opts)){ return false;  }
+    
+     // we doing this on an overflow:auto div?
+    props.container   =  opts.localMode ? this : document.documentElement;
+                          
+    // contentSelector we'll use for our .load()
+    opts.contentSelector = opts.contentSelector || this; 
+    
+    
+    // get the relative URL - everything past the domain name.
+    var relurl        = /(.*?\/\/).*?(\/.*)/;
+    var path          = $(opts.nextSelector).attr('href');
+    
+    
+    if (!path) { debug('Navigation selector not found'); return; }
+    
+    // set the path to be a relative URL from root.
+    path          = determinePath(path);
+    
+
+    // reset scrollTop in case of page refresh:
+    if (opts.localMode) $(props.container)[0].scrollTop = 0;
+
+    // distance from nav links to bottom
+    // computed as: height of the document + top offset of container - top offset of nav link
+    props.pixelsFromNavToBottom =  getDocumentHeight()  +
+                                     $(props.container).offset().top - 
+                                     $(opts.navSelector).offset().top;
+    
+    // define loading msg
+    props.loadingMsg = $('<div id="infscr-loading" style="text-align: center;"><img alt="Loading..." src="'+
+                                  opts.loadingImg+'" /><div>'+opts.loadingText+'</div></div>');    
+     // preload the image
+    (new Image()).src    = opts.loadingImg;
+                     
+
+  
+    // set up our bindings
+    $(document).ajaxError(function(e,xhr,opt){
+      debug('Page not found. Self-destructing...');    
+      
+      // die if we're out of pages.
+      if (xhr.status == 404){ 
+        showDoneMsg();
+        props.isDone = true; 
+        $(opts.localMode ? this : window).unbind('scroll.infscr');
+      } 
+    });
+    
+    // bind scroll handler to element (if its a local scroll) or window  
+    $(opts.localMode ? this : window)
+      .bind('scroll.infscr', function(){ infscrSetup(path,opts,props,callback); } )
+      .trigger('scroll.infscr'); // trigger the event, in case it's a short page
+    
+    
+    return this;
+  
+  }  // end of $.fn.infinitescroll()
+  
+
+  
+  // options and read-only properties object
+  
+  $.infinitescroll = {     
+        defaults      : {
+                          debug           : false,
+                          preload         : false,
+                          nextSelector    : "div.navigation a:first",
+                          loadingImg      : "http://www.infinite-scroll.com/loading.gif",
+                          loadingText     : "<em>Loading the next set of posts...</em>",
+                          donetext        : "<em>Congratulations, you've reached the end of the internet.</em>",
+                          navSelector     : "div.navigation",
+                          contentSelector : null,           // not really a selector. :) it's whatever the method was called on..
+                          extraScrollPx   : 150,
+                          itemSelector    : "div.post",
+                          animate         : false,
+                          localMode      : false,
+                          bufferPx        : 40,
+                          errorCallback   : function(){}
+                        }, 
+        loadingImg    : undefined,
+        loadingMsg    : undefined,
+        container     : undefined,
+        currPage      : 1,
+        currDOMChunk  : null,  // defined in setup()'s load()
+        isDuringAjax  : false,
+        isInvalidPage : false,
+        isDone        : false  // for when it goes all the way through the archive.
+  };
+  
+
+
+})(jQuery);
diff --git a/plugins/InfiniteScroll/jquery.infinitescroll.min.js b/plugins/InfiniteScroll/jquery.infinitescroll.min.js
new file mode 100644 (file)
index 0000000..04c75c4
--- /dev/null
@@ -0,0 +1,8 @@
+/*
+// Infinite Scroll jQuery plugin
+// copyright Paul Irish, licensed GPL & MIT
+// version 1.2.090804
+
+// home and docs: http://www.infinite-scroll.com
+*/
+(function(A){A.fn.infinitescroll=function(N,L){function E(){if(B.debug){window.console&&console.log.call(console,arguments)}}function G(P){for(var O in P){if(O.indexOf&&O.indexOf("Selector")&&A(P[O]).length===0){E("Your "+O+" found no elements.");return false}return true}}function K(O){O.match(C)?O.match(C)[2]:O;if(O.match(/^(.*?)\b2\b(.*?$)/)){O=O.match(/^(.*?)\b2\b(.*?$)/).slice(1)}else{if(O.match(/^(.*?)2(.*?$)/)){E("Trying backup next selector parse technique. Treacherous waters here, matey.");O=O.match(/^(.*?)2(.*?$)/).slice(1)}else{E("Sorry, we couldn't parse your Next (Previous Posts) URL. Verify your the css selector points to the correct A tag. If you still get this error: yell, scream, and kindly ask for help at infinite-scroll.com.");H.isInvalidPage=true}}return O}function I(){return B.localMode?(A(H.container)[0].scrollHeight&&A(H.container)[0].scrollHeight):A(document).height()}function F(Q,P){var O=I()-(Q.localMode?A(P.container).scrollTop():(A(P.container).scrollTop()||A(P.container.ownerDocument.body).scrollTop()))-A(Q.localMode?P.container:window).height();E("math:",O,P.pixelsFromNavToBottom);return(O-Q.bufferPx<P.pixelsFromNavToBottom)}function J(){H.loadingMsg.find("img").hide().parent().find("div").html(B.donetext).animate({opacity:1},2000).fadeOut("normal");B.errorCallback()}function D(R,Q,O,S){if(O.isDuringAjax||O.isInvalidPage||O.isDone){return }if(!F(Q,O)){return }O.isDuringAjax=true;O.loadingMsg.appendTo(Q.contentSelector).show();A(Q.navSelector).hide();O.currPage++;E("heading into ajax",R);var P=A(Q.contentSelector).is("table")?A("<tbody/>"):A("<div/>");P.attr("id","infscr-page-"+O.currPage).addClass("infscr-pages").appendTo(Q.contentSelector).load(R.join(O.currPage)+" "+Q.itemSelector,null,function(){if(O.isDone){J();return false}else{if(P.children().length==0){A.event.trigger("ajaxError",[{status:404}])}O.loadingMsg.fadeOut("normal");if(Q.animate){var T=A(window).scrollTop()+A("#infscr-loading").height()+Q.extraScrollPx+"px";A("html,body").animate({scrollTop:T},800,function(){O.isDuringAjax=false})}S.call(P[0]);if(!Q.animate){O.isDuringAjax=false}}})}var B=A.extend({},A.infinitescroll.defaults,N);var H=A.infinitescroll;L=L||function(){};if(!G(B)){return false}H.container=B.localMode?this:document.documentElement;B.contentSelector=B.contentSelector||this;var C=/(.*?\/\/).*?(\/.*)/;var M=A(B.nextSelector).attr("href");if(!M){E("Navigation selector not found");return }M=K(M);if(B.localMode){A(H.container)[0].scrollTop=0}H.pixelsFromNavToBottom=I()+A(H.container).offset().top-A(B.navSelector).offset().top;H.loadingMsg=A('<div id="infscr-loading" style="text-align: center;"><img alt="Loading..." src="'+B.loadingImg+'" /><div>'+B.loadingText+"</div></div>");(new Image()).src=B.loadingImg;A(document).ajaxError(function(P,Q,O){E("Page not found. Self-destructing...");if(Q.status==404){J();H.isDone=true;A(B.localMode?this:window).unbind("scroll.infscr")}});A(B.localMode?this:window).bind("scroll.infscr",function(){D(M,B,H,L)}).trigger("scroll.infscr");return this};A.infinitescroll={defaults:{debug:false,preload:false,nextSelector:"div.navigation a:first",loadingImg:"http://www.infinite-scroll.com/loading.gif",loadingText:"<em>Loading the next set of posts...</em>",donetext:"<em>Congratulations, you've reached the end of the internet.</em>",navSelector:"div.navigation",contentSelector:null,extraScrollPx:150,itemSelector:"div.post",animate:false,localMode:false,bufferPx:40,errorCallback:function(){}},loadingImg:undefined,loadingMsg:undefined,container:undefined,currPage:1,currDOMChunk:null,isDuringAjax:false,isInvalidPage:false,isDone:false}})(jQuery);
\ No newline at end of file
diff --git a/plugins/InfiniteScroll/readme.txt b/plugins/InfiniteScroll/readme.txt
new file mode 100644 (file)
index 0000000..3ce3b7f
--- /dev/null
@@ -0,0 +1,6 @@
+Infinite Scroll adds the following functionality to your Laconica installation: When a user scrolls towards the bottom of the page, the next page of notices is automatically retrieved and appended. This means they never need to click "Next Page", which dramatically increases stickiness.
+
+Installation
+============
+Add "addPlugin('InfiniteScroll');" to the bottom of your config.php
+That's it!
index 516625e41ab691bb299d4745bba4696694995b82..87fc3881e2835c51f168a0e13ddc449b2ea9beb3 100644 (file)
@@ -30,7 +30,9 @@ class FinishopenidloginAction extends Action
     function handle($args)
     {
         parent::handle($args);
-        if (common_is_real_login()) {
+        if (!common_config('openid', 'enabled')) {
+            common_redirect(common_local_url('login'));
+        } else if (common_is_real_login()) {
             $this->clientError(_('Already logged in.'));
         } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
             $token = $this->trimmed('token');
@@ -217,7 +219,7 @@ class FinishopenidloginAction extends Action
 
         if (!Validate::string($nickname, array('min_length' => 1,
                                                'max_length' => 64,
-                                               'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
+                                               'format' => NICKNAME_FMT))) {
             $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.'));
             return;
         }
@@ -389,7 +391,7 @@ class FinishopenidloginAction extends Action
     {
         if (!Validate::string($str, array('min_length' => 1,
                                           'max_length' => 64,
-                                          'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
+                                          'format' => NICKNAME_FMT))) {
             return false;
         }
         if (!User::allowed_nickname($str)) {
index 3d968c56e21404e622fa923fb781334f20097803..76f573b9f52f8429d7d6972aa4101bec4619c1c9 100644 (file)
@@ -26,7 +26,9 @@ class OpenidloginAction extends Action
     function handle($args)
     {
         parent::handle($args);
-        if (common_is_real_login()) {
+        if (!common_config('openid', 'enabled')) {
+            common_redirect(common_local_url('login'));
+        } else if (common_is_real_login()) {
             $this->clientError(_('Already logged in.'));
         } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
             $openid_url = $this->trimmed('openid_url');
index 26bf6483660823175c87367fe44ad9d08aa0f16f..57389903d1230864fbf1eee9e9cf174723c9be54 100644 (file)
@@ -82,6 +82,12 @@ class OpenidsettingsAction extends AccountSettingsAction
 
     function showContent()
     {
+        if (!common_config('openid', 'enabled')) {
+            $this->element('div', array('class' => 'error'),
+                           _('OpenID is not available.'));
+            return;
+        }
+
         $user = common_current_user();
 
         $this->elementStart('form', array('method' => 'post',
index 507f0194d7a3c726eb05112f8272b23e435b4da8..75bb8a91e9250a58132bdf40bda40e10d80151ef 100644 (file)
@@ -84,9 +84,7 @@ class RealtimePlugin extends Plugin
         $scripts = $this->_getScripts();
 
         foreach ($scripts as $script) {
-            $action->element('script', array('type' => 'text/javascript',
-                                             'src' => $script),
-                         ' ');
+            $action->script($script);
         }
 
         $user = common_current_user();
@@ -201,8 +199,8 @@ class RealtimePlugin extends Plugin
 
     function _getScripts()
     {
-        return array(common_path('plugins/Realtime/realtimeupdate.js'),
-                     common_path('plugins/Realtime/json2.js'));
+        return array('plugins/Realtime/realtimeupdate.js',
+                     'plugins/Realtime/json2.js');
     }
 
     function _updateInitialize($timeline, $user_id)
index 5ef8352d18ab1881aaa139030a7207330c1389bd..38a860fc75e9b43b890ce1e0a643e20b1e88d0dd 100644 (file)
@@ -65,9 +65,7 @@ class recaptcha extends Plugin
 
         $action->extraHeaders();
 
-        $action->startXML('html',
-            '-//W3C//DTD XHTML 1.0 Strict//EN',
-            'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
+        $action->startXML('html');
 
         $action->raw('<style type="text/css">#recaptcha_area{float:left;}</style>');
         return false;
old mode 100644 (file)
new mode 100755 (executable)
index 8c9a912..f952af8
@@ -42,7 +42,7 @@ class UTF8FixerUpper
     {
         $this->args = $args;
 
-        if (array_key_exists('max_date', $args)) {
+        if (!empty($args['max_date'])) {
             $this->max_date = strftime('%Y-%m-%d %H:%M:%S', strtotime($args['max_date']));
         } else {
             $this->max_date = strftime('%Y-%m-%d %H:%M:%S', time());
index 1e4546dff1261704f3afc35fe864c473b1ce2ddb..f1d8d8116d0deb6f1d60ec46b56d2dba721732b9 100755 (executable)
@@ -43,7 +43,12 @@ if(common_config('twitterbridge','enabled')) {
     echo "twitterstatusfetcher.php ";
 }
 echo "ombqueuehandler.php ";
-echo "twitterqueuehandler.php ";
+if (common_config('twitter', 'enabled')) {
+    echo "twitterqueuehandler.php ";
+    echo "synctwitterfriends.php ";
+}
 echo "facebookqueuehandler.php ";
 echo "pingqueuehandler.php ";
-echo "smsqueuehandler.php ";
+if (common_config('sms', 'enabled')) {
+    echo "smsqueuehandler.php ";
+}
index 3ef4d06383f99cba74214c5f170d5ececcbd20a9..67636d34263572099c674307ea6beb5cfdefc795 100755 (executable)
@@ -66,9 +66,10 @@ class MailerDaemon
         }
         $msg = $this->cleanup_msg($msg);
         $msg = common_shorten_links($msg);
-        if (mb_strlen($msg) > 140) {
-            $this->error($from,_('That\'s too long. '.
-                'Max notice size is 140 chars.'));
+        if (Notice::contentTooLong($msg)) {
+            $this->error($from, sprintf(_('That\'s too long. '.
+                                          'Max notice size is %d chars.'),
+                                        Notice::maxContent()));
         }
         $fileRecords = array();
         foreach($attachments as $attachment){
@@ -78,9 +79,9 @@ class MailerDaemon
                 die('error() should trigger an exception before reaching here.');
             }
             $filename = $this->saveFile($user, $attachment,$mimetype);
-            
+
             fclose($attachment);
-            
+
             if (empty($filename)) {
                 $this->error($from,_('Couldn\'t save file.'));
             }
@@ -96,9 +97,10 @@ class MailerDaemon
             $short_fileurl = common_shorten_url($fileurl);
             $msg .= ' ' . $short_fileurl;
 
-            if (mb_strlen($msg) > 140) {
+            if (Notice::contentTooLong($msg)) {
                 $this->deleteFile($filename);
-                $this->error($from,_('Max notice size is 140 chars, including attachment URL.'));
+                $this->error($from, sprintf(_('Max notice size is %d chars, including attachment URL.'),
+                                            Notice::maxContent()));
             }
 
             // Also, not sure this is necessary -- Zach
@@ -123,7 +125,7 @@ class MailerDaemon
         $stream  = stream_get_meta_data($attachment);
         if (copy($stream['uri'], $filepath) && chmod($filepath,0664)) {
             return $filename;
-        } else {   
+        } else {
             $this->error(null,_('File could not be moved to destination directory.' . $stream['uri'] . ' ' . $filepath));
         }
     }
@@ -152,7 +154,7 @@ class MailerDaemon
     }
 
     function maybeAddRedir($file_id, $url)
-    {   
+    {
         $file_redir = File_redirection::staticGet('url', $url);
 
         if (empty($file_redir)) {
@@ -273,7 +275,7 @@ class MailerDaemon
     }
 
     function attachFile($notice, $filerec)
-    {   
+    {
         File_to_post::processNew($filerec->id, $notice->id);
 
         $this->maybeAddRedir($filerec->id,
@@ -385,5 +387,7 @@ class MailerDaemon
     }
 }
 
-$md = new MailerDaemon();
-$md->handle_message('php://stdin');
+if (common_config('emailpost', 'enabled')) {
+    $md = new MailerDaemon();
+    $md->handle_message('php://stdin');
+}
index 1587192b6fb5b9c1532e6af18411fa76dbc44ba4..cc5263ae23e8aeb20ea763e65b5fb33d33925e9f 100755 (executable)
@@ -57,7 +57,7 @@ class OmbQueueHandler extends QueueHandler
             $this->log(LOG_DEBUG, 'Ignoring remote notice ' . $notice->id);
             return true;
         } else {
-            return omb_broadcast_remote_subscribers($notice);
+            return omb_broadcast_notice($notice);
         }
     }
 
index 60ffd83ad1e45d8991aa4115a3e8c305acba5ddb..894e5aaffe74a3dd900160d436ad17c3981b6aa9 100755 (executable)
@@ -25,7 +25,7 @@ DIR=`php $SDIR/getpiddir.php`
 
 for f in jabberhandler ombhandler publichandler smshandler pinghandler \
         xmppconfirmhandler xmppdaemon twitterhandler facebookhandler \
-        twitterstatusfetcher; do
+        twitterstatusfetcher synctwitterfriends; do
 
        FILES="$DIR/$f.*.pid"
        for ff in "$FILES" ; do
index fe53ff44d634fa92895dd00c34ccc804fa245295..2de464bccc72625171afc6cd3ae38064f1329f1d 100755 (executable)
 
 define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
 
-// Uncomment this to get useful console output
+$shortoptions = 'di::';
+$longoptions = array('id::', 'debug');
+
+$helptext = <<<END_OF_TRIM_HELP
+Batch script for synching local friends with Twitter friends.
+  -i --id              Identity (default 'generic')
+  -d --debug           Debug (lots of log output)
+
+END_OF_TRIM_HELP;
+
+require_once INSTALLDIR . '/scripts/commandline.inc';
+require_once INSTALLDIR . '/lib/parallelizingdaemon.php';
+
+/**
+ * Daemon to sync local friends with Twitter friends
+ *
+ * @category Twitter
+ * @package  Laconica
+ * @author   Zach Copley <zach@controlyourself.ca>
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
 
 $helptext = <<<END_OF_TWITTER_HELP
 Batch script for synching local friends with Twitter friends.
 
 END_OF_TWITTER_HELP;
 
-require_once INSTALLDIR.'/scripts/commandline.inc';
+require_once INSTALLDIR . '/scripts/commandline.inc';
+require_once INSTALLDIR . '/lib/parallelizingdaemon.php';
 
-// Make a lockfile
-$lockfilename = lockFilename();
-if (!($lockfile = @fopen($lockfilename, "w"))) {
-    print "Already running... exiting.\n";
-    exit(1);
-}
+class SyncTwitterFriendsDaemon extends ParallelizingDaemon
+{
+    /**
+     *  Constructor
+     *
+     * @param string  $id           the name/id of this daemon
+     * @param int     $interval     sleep this long before doing everything again
+     * @param int     $max_children maximum number of child processes at a time
+     * @param boolean $debug        debug output flag
+     *
+     * @return void
+     *
+     **/
 
-// Obtain an exlcusive lock on file (will fail if script is already going)
-if (!@flock( $lockfile, LOCK_EX | LOCK_NB, &$wouldblock) || $wouldblock) {
-    // Script already running - abort
-    @fclose($lockfile);
-    print "Already running... exiting.\n";
-    exit(1);
-}
+    function __construct($id = null, $interval = 60,
+                         $max_children = 2, $debug = null)
+    {
+        parent::__construct($id, $interval, $max_children, $debug);
+    }
 
-$flink = new Foreign_link();
-$flink->service = 1; // Twitter
-$flink->orderBy('last_friendsync');
-$flink->limit(25);  // sync this many users during this run
-$cnt = $flink->find();
+    /**
+     * Name of this daemon
+     *
+     * @return string Name of the daemon.
+     */
 
-print "Updating Twitter friends subscriptions for $cnt users.\n";
+    function name()
+    {
+        return ('synctwitterfriends.' . $this->_id);
+    }
 
-while ($flink->fetch()) {
+    /**
+     * Find all the Twitter foreign links for users who have requested
+     * automatically subscribing to their Twitter friends locally.
+     *
+     * @return array flinks an array of Foreign_link objects
+     */
+    function getObjects()
+    {
+        $flinks = array();
+        $flink = new Foreign_link();
 
-    if (($flink->friendsync & FOREIGN_FRIEND_RECV) == FOREIGN_FRIEND_RECV) {
+        $conn = &$flink->getDatabaseConnection();
 
-        $user = User::staticGet($flink->user_id);
+        $flink->service = TWITTER_SERVICE;
+        $flink->orderBy('last_friendsync');
+        $flink->limit(25);  // sync this many users during this run
+        $flink->find();
 
-        if (empty($user)) {
-            common_log(LOG_WARNING, "Unmatched user for ID " . $flink->user_id);
-            print "Unmatched user for ID $flink->user_id\n";
-            continue;
+        while ($flink->fetch()) {
+            if (($flink->friendsync & FOREIGN_FRIEND_RECV) == FOREIGN_FRIEND_RECV) {
+                $flinks[] = clone($flink);
+            }
         }
 
-        print "Updating Twitter friends for $user->nickname (Laconica ID: $user->id)... ";
+        $conn->disconnect();
 
-        $fuser = $flink->getForeignUser();
+        global $_DB_DATAOBJECT;
+        unset($_DB_DATAOBJECT['CONNECTIONS']);
 
-        if (empty($fuser)) {
-            common_log(LOG_WARNING, "Unmatched user for ID " . $flink->user_id);
-            print "Unmatched user for ID $flink->user_id\n";
-            continue;
-        }
+        return $flinks;
+    }
+
+    function childTask($flink) {
 
-        save_twitter_friends($user, $fuser->id, $fuser->nickname, $flink->credentials);
+        // Each child ps needs its own DB connection
+
+        // Note: DataObject::getDatabaseConnection() creates
+        // a new connection if there isn't one already
+
+        $conn = &$flink->getDatabaseConnection();
+
+        $this->subscribeTwitterFriends($flink);
 
         $flink->last_friendsync = common_sql_now();
         $flink->update();
 
-        if (defined('SCRIPT_DEBUG')) {
-            print "\nDONE\n";
-        } else {
-            print "DONE\n";
+        $conn->disconnect();
+
+        // XXX: Couldn't find a less brutal way to blow
+        // away a cached connection
+
+        global $_DB_DATAOBJECT;
+        unset($_DB_DATAOBJECT['CONNECTIONS']);
+    }
+
+    function fetchTwitterFriends($flink)
+    {
+        $friends = array();
+
+        $token = TwitterOAuthClient::unpackToken($flink->credentials);
+
+        $client = new TwitterOAuthClient($token->key, $token->secret);
+
+        try {
+            $friends_ids = $client->friendsIds();
+        } catch (OAuthCurlException $e) {
+            common_log(LOG_WARNING, $this->name() .
+                       ' - cURL error getting friend ids ' .
+                       $e->getCode() . ' - ' . $e->getMessage());
+            return $friends;
+        }
+
+        if (empty($friends_ids)) {
+            common_debug($this->name() .
+                         " - Twitter user $flink->foreign_id " .
+                         'doesn\'t have any friends!');
+            return $friends;
+        }
+
+        common_debug($this->name() . ' - Twitter\'s API says Twitter user id ' .
+                     "$flink->foreign_id has " .
+                     count($friends_ids) . ' friends.');
+
+        // Calculate how many pages to get...
+        $pages = ceil(count($friends_ids) / 100);
+
+        if ($pages == 0) {
+            common_debug($this->name() . " - $user seems to have no friends.");
+        }
+
+        for ($i = 1; $i <= $pages; $i++) {
+
+        try {
+            $more_friends = $client->statusesFriends(null, null, null, $i);
+        } catch (OAuthCurlException $e) {
+            common_log(LOG_WARNING, $this->name() .
+                       ' - cURL error getting Twitter statuses/friends ' .
+                       "page $i - " . $e->getCode() . ' - ' .
+                       $e->getMessage());
         }
+
+            if (empty($more_friends)) {
+                common_log(LOG_WARNING, $this->name() .
+                           " - Couldn't retrieve page $i " .
+                           "of Twitter user $flink->foreign_id friends.");
+                continue;
+            } else {
+                $friends = array_merge($friends, $more_friends);
+            }
+        }
+
+        return $friends;
     }
-}
 
-function lockFilename()
-{
-    $piddir = common_config('daemon', 'piddir');
-    if (!$piddir) {
-        $piddir = '/var/run';
+    function subscribeTwitterFriends($flink)
+    {
+        $friends = $this->fetchTwitterFriends($flink);
+
+        if (empty($friends)) {
+            common_debug($this->name() .
+                         ' - Couldn\'t get friends from Twitter for ' .
+                         "Twitter user $flink->foreign_id.");
+            return false;
+        }
+
+        $user = $flink->getUser();
+
+        foreach ($friends as $friend) {
+
+            $friend_name = $friend->screen_name;
+            $friend_id = (int) $friend->id;
+
+            // Update or create the Foreign_user record for each
+            // Twitter friend
+
+            if (!save_twitter_user($friend_id, $friend_name)) {
+                common_log(LOG_WARNING, $this-name() .
+                           " - Couldn't save $screen_name's friend, $friend_name.");
+                continue;
+            }
+
+            // Check to see if there's a related local user
+
+            $friend_flink = Foreign_link::getByForeignID($friend_id,
+                                                         TWITTER_SERVICE);
+
+            if (!empty($friend_flink)) {
+
+                // Get associated user and subscribe her
+
+                $friend_user = User::staticGet('id', $friend_flink->user_id);
+
+                if (!empty($friend_user)) {
+                    $result = subs_subscribe_to($user, $friend_user);
+
+                    if ($result === true) {
+                        common_log(LOG_INFO,
+                                   $this->name() . ' - Subscribed ' .
+                                   "$friend_user->nickname to $user->nickname.");
+                    } else {
+                        common_debug($this->name() .
+                                     ' - Tried subscribing ' .
+                                     "$friend_user->nickname to $user->nickname - " .
+                                     $result);
+                    }
+                }
+            }
+        }
+
+        return true;
     }
 
-    return $piddir . '/synctwitterfriends.lock';
 }
 
-// Cleanup
-fclose($lockfile);
-unlink($lockfilename);
+$id    = null;
+$debug = null;
+
+if (have_option('i')) {
+    $id = get_option_value('i');
+} else if (have_option('--id')) {
+    $id = get_option_value('--id');
+} else if (count($args) > 0) {
+    $id = $args[0];
+} else {
+    $id = null;
+}
+
+if (have_option('d') || have_option('debug')) {
+    $debug = true;
+}
+
+$syncer = new SyncTwitterFriendsDaemon($id, 60, 2, $debug);
+$syncer->runOnce();
 
-exit(0);
index e1745cfc089614f41aa8a5766f179e72a924f836..f5289c5f4b7384a1daeaa7de0c8e6119cbbb820d 100755 (executable)
@@ -56,17 +56,23 @@ require_once INSTALLDIR . '/lib/daemon.php';
 // NOTE: an Avatar path MUST be set in config.php for this
 // script to work: e.g.: $config['avatar']['path'] = '/laconica/avatar';
 
-class TwitterStatusFetcher extends Daemon
+class TwitterStatusFetcher extends ParallelizingDaemon
 {
-    private $_children = array();
-
-    function __construct($id=null, $daemonize=true)
+    /**
+     *  Constructor
+     *
+     * @param string  $id           the name/id of this daemon
+     * @param int     $interval     sleep this long before doing everything again
+     * @param int     $max_children maximum number of child processes at a time
+     * @param boolean $debug        debug output flag
+     *
+     * @return void
+     *
+     **/
+    function __construct($id = null, $interval = 60,
+                         $max_children = 2, $debug = null)
     {
-        parent::__construct($daemonize);
-
-        if ($id) {
-            $this->set_id($id);
-        }
+        parent::__construct($id, $interval, $max_children, $debug);
     }
 
     /**
@@ -81,126 +87,22 @@ class TwitterStatusFetcher extends Daemon
     }
 
     /**
-     * Run the daemon
+     * Find all the Twitter foreign links for users who have requested
+     * importing of their friends' timelines
      *
-     * @return void
+     * @return array flinks an array of Foreign_link objects
      */
 
-    function run()
+    function getObjects()
     {
-        if (defined('SCRIPT_DEBUG')) {
-            common_debug($this->name() .
-                ': debugging log output enabled.');
-        }
-
-        do {
-
-            $flinks = $this->refreshFlinks();
-
-            foreach ($flinks as $f) {
-
-                // We have to disconnect from the DB before forking so
-                // each sub-process will open its own connection and
-                // avoid stomping on the others
-
-                $conn = &$f->getDatabaseConnection();
-                $conn->disconnect();
-
-                $pid = pcntl_fork();
-
-                if ($pid == -1) {
-                    die ("Couldn't fork!");
-                }
-
-                if ($pid) {
-
-                    // Parent
-                    if (defined('SCRIPT_DEBUG')) {
-                        common_debug("Parent: forked new status ".
-                                     " fetcher process " . $pid);
-                    }
-
-                    $this->_children[] = $pid;
-
-                } else {
-
-                    // Child
-                    $this->getTimeline($f);
-                    exit();
-                }
-
-                // Remove child from ps list as it finishes
-                while (($c = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) {
-
-                    if (defined('SCRIPT_DEBUG')) {
-                        common_debug("Child $c finished.");
-                    }
-
-                    $this->removePs($this->_children, $c);
-                }
-
-                // Wait! We have too many damn kids.
-                if (sizeof($this->_children) > MAXCHILDREN) {
-
-                    if (defined('SCRIPT_DEBUG')) {
-                        common_debug('Too many children. Waiting...');
-                    }
-
-                    if (($c = pcntl_wait($status, WUNTRACED)) > 0) {
-
-                        if (defined('SCRIPT_DEBUG')) {
-                            common_debug("Finished waiting for $c");
-                        }
-
-                        $this->removePs($this->_children, $c);
-                    }
-                }
-            }
-
-            // Remove all children from the process list before restarting
-            while (($c = pcntl_wait($status, WUNTRACED)) > 0) {
-
-                if (defined('SCRIPT_DEBUG')) {
-                    common_debug("Child $c finished.");
-                }
-
-                $this->removePs($this->_children, $c);
-            }
-
-            // Rest for a bit before we fetch more statuses
-
-            if (defined('SCRIPT_DEBUG')) {
-                common_debug('Waiting ' . POLL_INTERVAL .
-                    ' secs before hitting Twitter again.');
-            }
-
-            if (POLL_INTERVAL > 0) {
-                sleep(POLL_INTERVAL);
-            }
-
-        } while (true);
-    }
-
-    /**
-     * Refresh the foreign links for this user
-     *
-     * @return void
-     */
+        global $_DB_DATAOBJECT;
 
-    function refreshFlinks()
-    {
         $flink = new Foreign_link();
+        $conn = &$flink->getDatabaseConnection();
 
-        $flink->service = 1; // Twitter
-
+        $flink->service = TWITTER_SERVICE;
         $flink->orderBy('last_noticesync');
-
-        $cnt = $flink->find();
-
-        if (defined('SCRIPT_DEBUG')) {
-            common_debug('Updating Twitter friends subscriptions' .
-                " for $cnt users.");
-        }
+        $flink->find();
 
         $flinks = array();
 
@@ -215,78 +117,81 @@ class TwitterStatusFetcher extends Daemon
         $flink->free();
         unset($flink);
 
+        $conn->disconnect();
+        unset($_DB_DATAOBJECT['CONNECTIONS']);
+
         return $flinks;
     }
 
-    /**
-     * Unknown
-     *
-     * @param array  &$plist unknown.
-     * @param string $ps     unknown.
-     *
-     * @return unknown
-     * @todo document
-     */
+    function childTask($flink) {
 
-    function removePs(&$plist, $ps)
-    {
-        for ($i = 0; $i < sizeof($plist); $i++) {
-            if ($plist[$i] == $ps) {
-                unset($plist[$i]);
-                $plist = array_values($plist);
-                break;
-            }
-        }
+        // Each child ps needs its own DB connection
+
+        // Note: DataObject::getDatabaseConnection() creates
+        // a new connection if there isn't one already
+
+        $conn = &$flink->getDatabaseConnection();
+
+        $this->getTimeline($flink);
+
+        $flink->last_friendsync = common_sql_now();
+        $flink->update();
+
+        $conn->disconnect();
+
+        // XXX: Couldn't find a less brutal way to blow
+        // away a cached connection
+
+        global $_DB_DATAOBJECT;
+        unset($_DB_DATAOBJECT['CONNECTIONS']);
     }
 
     function getTimeline($flink)
     {
-        if (empty($flink)) {
-            common_log(LOG_WARNING,
-                "Can't retrieve Foreign_link for foreign ID $fid");
-            return;
-        }
-
-        $fuser = $flink->getForeignUser();
-
-        if (empty($fuser)) {
-            common_log(LOG_WARNING, "Unmatched user for ID " .
-                $flink->user_id);
+         if (empty($flink)) {
+            common_log(LOG_WARNING, $this->name() .
+                " - Can't retrieve Foreign_link for foreign ID $fid");
             return;
         }
 
-        if (defined('SCRIPT_DEBUG')) {
-            common_debug('Trying to get timeline for Twitter user ' .
-                "$fuser->nickname ($flink->foreign_id).");
-        }
+        common_debug($this->name() . ' - Trying to get timeline for Twitter user ' .
+                     $flink->foreign_id);
 
         // XXX: Biggest remaining issue - How do we know at which status
         // to start importing?  How many statuses?  Right now I'm going
         // with the default last 20.
 
-        $url = 'http://twitter.com/statuses/friends_timeline.json';
+        $token = TwitterOAuthClient::unpackToken($flink->credentials);
+
+        $client = new TwitterOAuthClient($token->key, $token->secret);
 
-        $timeline_json = get_twitter_data($url, $fuser->nickname,
-            $flink->credentials);
+        $timeline = null;
 
-        $timeline = json_decode($timeline_json);
+        try {
+            $timeline = $client->statusesFriendsTimeline();
+        } catch (OAuthClientCurlException $e) {
+            common_log(LOG_WARNING, $this->name() .
+                       ' - OAuth client unable to get friends timeline for user ' .
+                       $flink->user_id . ' - code: ' .
+                       $e->getCode() . 'msg: ' . $e->getMessage());
+        }
 
         if (empty($timeline)) {
-            common_log(LOG_WARNING, "Empty timeline.");
+            common_log(LOG_WARNING, $this->name() .  " - Empty timeline.");
             return;
         }
 
         // Reverse to preserve order
+
         foreach (array_reverse($timeline) as $status) {
 
             // Hacktastic: filter out stuff coming from this Laconica
+
             $source = mb_strtolower(common_config('integration', 'source'));
 
             if (preg_match("/$source/", mb_strtolower($status->source))) {
-                if (defined('SCRIPT_DEBUG')) {
-                    common_debug('Skipping import of status ' . $status->id .
-                        ' with source ' . $source);
-                }
+                common_debug($this->name() . ' - Skipping import of status ' .
+                             $status->id . ' with source ' . $source);
                 continue;
             }
 
@@ -294,6 +199,7 @@ class TwitterStatusFetcher extends Daemon
         }
 
         // Okay, record the time we synced with Twitter for posterity
+
         $flink->last_noticesync = common_sql_now();
         $flink->update();
     }
@@ -301,11 +207,12 @@ class TwitterStatusFetcher extends Daemon
     function saveStatus($status, $flink)
     {
         $id = $this->ensureProfile($status->user);
+
         $profile = Profile::staticGet($id);
 
-        if (!$profile) {
-            common_log(LOG_ERR,
-                'Problem saving notice. No associated Profile.');
+        if (empty($profile)) {
+            common_log(LOG_ERR, $this->name() .
+                ' - Problem saving notice. No associated Profile.');
             return null;
         }
 
@@ -318,7 +225,7 @@ class TwitterStatusFetcher extends Daemon
 
         // check to see if we've already imported the status
 
-        if (!$notice) {
+        if (empty($notice)) {
 
             $notice = new Notice();
 
@@ -329,7 +236,7 @@ class TwitterStatusFetcher extends Daemon
             $notice->content    = common_shorten_links($status->text); // XXX
             $notice->rendered   = common_render_content($notice->content, $notice);
             $notice->source     = 'twitter';
-            $notice->reply_to   = null; // XXX lookup reply
+            $notice->reply_to   = null; // XXX: lookup reply
             $notice->is_local   = Notice::GATEWAY;
 
             if (Event::handle('StartNoticeSave', array(&$notice))) {
@@ -355,24 +262,22 @@ class TwitterStatusFetcher extends Daemon
     function ensureProfile($user)
     {
         // check to see if there's already a profile for this user
+
         $profileurl = 'http://twitter.com/' . $user->screen_name;
         $profile = Profile::staticGet('profileurl', $profileurl);
 
-        if ($profile) {
-            if (defined('SCRIPT_DEBUG')) {
-                common_debug("Profile for $profile->nickname found.");
-            }
+        if (!empty($profile)) {
+            common_debug($this->name() .
+                         " - Profile for $profile->nickname found.");
 
             // Check to see if the user's Avatar has changed
-            $this->checkAvatar($user, $profile);
 
+            $this->checkAvatar($user, $profile);
             return $profile->id;
 
         } else {
-            if (defined('SCRIPT_DEBUG')) {
-                common_debug('Adding profile and remote profile ' .
-                    "for Twitter user: $profileurl");
-            }
+            common_debug($this->name() . ' - Adding profile and remote profile ' .
+                         "for Twitter user: $profileurl.");
 
             $profile = new Profile();
             $profile->query("BEGIN");
@@ -394,9 +299,10 @@ class TwitterStatusFetcher extends Daemon
             }
 
             // check for remote profile
+
             $remote_pro = Remote_profile::staticGet('uri', $profileurl);
 
-            if (!$remote_pro) {
+            if (empty($remote_pro)) {
 
                 $remote_pro = new Remote_profile();
 
@@ -433,23 +339,18 @@ class TwitterStatusFetcher extends Daemon
         $oldname = $profile->getAvatar(48)->filename;
 
         if ($newname != $oldname) {
-
-            if (defined('SCRIPT_DEBUG')) {
-                common_debug('Avatar for Twitter user ' .
-                    "$profile->nickname has changed.");
-                common_debug("old: $oldname new: $newname");
-            }
+            common_debug($this->name() . ' - Avatar for Twitter user ' .
+                         "$profile->nickname has changed.");
+            common_debug($this->name() . " - old: $oldname new: $newname");
 
             $this->updateAvatars($twitter_user, $profile);
         }
 
         if ($this->missingAvatarFile($profile)) {
-
-            if (defined('SCRIPT_DEBUG')) {
-                common_debug('Twitter user ' . $profile->nickname .
-                    ' is missing one or more local avatars.');
-                common_debug("old: $oldname new: $newname");
-            }
+            common_debug($this->name() . ' - Twitter user ' .
+                         $profile->nickname .
+                         ' is missing one or more local avatars.');
+            common_debug($this->name() ." - old: $oldname new: $newname");
 
             $this->updateAvatars($twitter_user, $profile);
         }
@@ -529,23 +430,20 @@ class TwitterStatusFetcher extends Daemon
             if ($this->fetchAvatar($url, $filename)) {
                 $this->newAvatar($id, $size, $mediatype, $filename);
             } else {
-                common_log(LOG_WARNING, "Problem fetching Avatar: $url", __FILE__);
+                common_log(LOG_WARNING, $this->id() .
+                           " - Problem fetching Avatar: $url");
             }
         }
     }
 
     function updateAvatar($profile_id, $size, $mediatype, $filename) {
 
-        if (defined('SCRIPT_DEBUG')) {
-            common_debug("Updating avatar: $size");
-        }
+        common_debug($this->name() . " - Updating avatar: $size");
 
         $profile = Profile::staticGet($profile_id);
 
         if (empty($profile)) {
-            if (defined('SCRIPT_DEBUG')) {
-                common_debug("Couldn't get profile: $profile_id!");
-            }
+            common_debug($this->name() . " - Couldn't get profile: $profile_id!");
             return;
         }
 
@@ -553,6 +451,7 @@ class TwitterStatusFetcher extends Daemon
         $avatar = $profile->getAvatar($sizes[$size]);
 
         // Delete the avatar, if present
+
         if ($avatar) {
             $avatar->delete();
         }
@@ -590,9 +489,7 @@ class TwitterStatusFetcher extends Daemon
         $avatar->filename = $filename;
         $avatar->url = Avatar::url($filename);
 
-        if (defined('SCRIPT_DEBUG')) {
-            common_debug("new filename: $avatar->url");
-        }
+        common_debug($this->name() . " - New filename: $avatar->url");
 
         $avatar->created = common_sql_now();
 
@@ -603,9 +500,8 @@ class TwitterStatusFetcher extends Daemon
             return null;
         }
 
-        if (defined('SCRIPT_DEBUG')) {
-            common_debug("Saved new $size avatar for $profile_id.");
-        }
+        common_debug($this->name() .
+                     " - Saved new $size avatar for $profile_id.");
 
         return $id;
     }
@@ -618,13 +514,12 @@ class TwitterStatusFetcher extends Daemon
 
         $out = fopen($avatarfile, 'wb');
         if (!$out) {
-            common_log(LOG_WARNING, "Couldn't open file $filename", __FILE__);
+            common_log(LOG_WARNING, $this->name() .
+                       " - Couldn't open file $filename");
             return false;
         }
 
-        if (defined('SCRIPT_DEBUG')) {
-            common_debug("Fetching avatar: $url");
-        }
+        common_debug($this->name() . " - Fetching Twitter avatar: $url");
 
         $ch = curl_init();
         curl_setopt($ch, CURLOPT_URL, $url);
@@ -641,7 +536,8 @@ class TwitterStatusFetcher extends Daemon
     }
 }
 
-declare(ticks = 1);
+$id    = null;
+$debug = null;
 
 if (have_option('i')) {
     $id = get_option_value('i');
@@ -654,9 +550,9 @@ if (have_option('i')) {
 }
 
 if (have_option('d') || have_option('debug')) {
-    define('SCRIPT_DEBUG', true);
+    $debug = true;
 }
 
-$fetcher = new TwitterStatusFetcher($id);
+$fetcher = new TwitterStatusFetcher($id, 60, 2, $debug);
 $fetcher->runOnce();
 
index 69512f243895e6d6d2a9c023a5408638f141e1a1..f033025f8376f6a6086e3840555feb4fcfce6f4e 100755 (executable)
@@ -316,9 +316,11 @@ class XMPPDaemon extends Daemon
     {
         $body = trim($pl['body']);
         $content_shortened = common_shorten_links($body);
-        if (mb_strlen($content_shortened) > 140) {
+        if (Notice::contentTooLong($content_shortened)) {
           $from = jabber_normalize_jid($pl['from']);
-          $this->from_site($from, "Message too long - maximum is 140 characters, you sent ".mb_strlen($content_shortened));
+          $this->from_site($from, sprintf(_("Message too long - maximum is %d characters, you sent %d"),
+                                          Notice::maxContent(),
+                                          mb_strlen($content_shortened)));
           return;
         }
         $notice = Notice::saveNew($user->id, $content_shortened, 'xmpp');
index 6c6dfb503e987f62892c8fa78716cd885a982139..b35f332aaeb2ca5bf6f1e057448abde639fdb618 100644 (file)
@@ -1,18 +1,11 @@
 /* Fixes issue here http://code.google.com/p/jcrop/issues/detail?id=1 */
-.jcrop-holder
-{
-       text-align: left;
-}
+.jcrop-holder { text-align: left; }
 
 .jcrop-vline, .jcrop-hline
 {
        font-size: 0;
        position: absolute;
-       background: #fff url(../images/illustrations/illu_jcrop.gif) top left repeat;
-       /*
-       opacity: .5;
-       *filter:alpha(opacity=50);
-       */
+       background: white url(../images/illustrations/illu_jcrop.gif) top left repeat;
 }
 .jcrop-vline { height: 100%; width: 1px !important; }
 .jcrop-hline { width: 100%; height: 1px !important; }
        height: 7px !important;
        border: 1px #eee solid;
        background-color: #333;
-       /*width: 9px;
-       height: 9px;*/
+       *width: 9px;
+       *height: 9px;
 }
 
-.jcrop-tracker {
-       /*background-color: gray;*/
-       width: 100%; height: 100%;
-}
+.jcrop-tracker { width: 100%; height: 100%; }
 
 .custom .jcrop-vline,
 .custom .jcrop-hline
index 6a001f11aa7d0ea8d7bccdd97c85adaa42457f8b..de63f1577fe4abfa91c16be78b58b2a789c46bd3 100644 (file)
Binary files a/theme/base/images/icons/icon_atom.png and b/theme/base/images/icons/icon_atom.png differ
index 0ccd1ce254370ad278a4c4cc9d0484b72a564bf5..e75778a9e1434e1c3bbce79c6d1efbf77e2cf016 100644 (file)
Binary files a/theme/base/images/icons/icon_rss.png and b/theme/base/images/icons/icon_rss.png differ
index b7c86ae07bc1d515701e35510248993d48f4d02e..6a4b87df1aa4a288e01d44bb4e50fa969024ff3a 100644 (file)
@@ -66,7 +66,7 @@ div.notice-options input,
 .entity_nudge p,
 .form_settings input.form_action-primary,
 .form_make_admin input.submit {
-color:#002E6E;
+color:#002FA7;
 }
 
 .notice,
@@ -223,6 +223,10 @@ background:transparent url(../../base/images/icons/twotone/green/favourite.gif)
 .notice-options form.form_disfavor input.submit {
 background:transparent url(../../base/images/icons/twotone/green/disfavourite.gif) no-repeat 0 45%;
 }
+.notice-options form.form_favor.processing input.submit,
+.notice-options form.form_disfavor.processing input.submit {
+background:transparent url(../../base/images/icons/icon_processing.gif) no-repeat 0 45%;
+}
 .notice-options .notice_delete {
 background:transparent url(../../base/images/icons/twotone/green/trash.gif) no-repeat 0 45%;
 }
@@ -235,9 +239,6 @@ opacity:0.4;
 .notices li:hover div.notice-options {
 opacity:1;
 }
-div.entry-content {
-color:#333333;
-}
 div.notice-options a,
 div.notice-options input {
 font-family:sans-serif;
index 38b8692b4a2f71c8de3d6a12b715df33aada5f7c..6c7808616a0ed512a66845527ea0b18a703865f6 100644 (file)
Binary files a/theme/default/default-avatar-mini.png and b/theme/default/default-avatar-mini.png differ
index f8357d4fc296271b837b3d9911cfbc133918ef2f..08ce4e48ee5baf4d5eea4860c720db57f7b7b4dd 100644 (file)
Binary files a/theme/default/default-avatar-profile.png and b/theme/default/default-avatar-profile.png differ
index 6b63baa70ffede89af964363c384971346d9a7a6..f18994d75223ce90bb966570424a400f33e5d208 100644 (file)
Binary files a/theme/default/default-avatar-stream.png and b/theme/default/default-avatar-stream.png differ
index fdead6c4a0a23614552329ee68bb0860b0200a0c..322cbe903404e4348bc4473758f457b6e9703f5c 100644 (file)
Binary files a/theme/default/logo.png and b/theme/default/logo.png differ
index 6a882049539cc1cbc0136ac4ad7e607116455706..0688db42566a199041293c9feb37f0443588637c 100644 (file)
@@ -66,7 +66,7 @@ div.notice-options input,
 .entity_nudge p,
 .form_settings input.form_action-primary,
 .form_make_admin input.submit {
-color:#002E6E;
+color:#002FA7;
 }
 
 .notice,
@@ -235,9 +235,6 @@ opacity:0.4;
 .notices li:hover div.notice-options {
 opacity:1;
 }
-div.entry-content {
-color:#333333;
-}
 div.notice-options a,
 div.notice-options input {
 font-family:sans-serif;
index 38b8692b4a2f71c8de3d6a12b715df33aada5f7c..6c7808616a0ed512a66845527ea0b18a703865f6 100644 (file)
Binary files a/theme/identica/default-avatar-mini.png and b/theme/identica/default-avatar-mini.png differ
index f8357d4fc296271b837b3d9911cfbc133918ef2f..08ce4e48ee5baf4d5eea4860c720db57f7b7b4dd 100644 (file)
Binary files a/theme/identica/default-avatar-profile.png and b/theme/identica/default-avatar-profile.png differ
index 6b63baa70ffede89af964363c384971346d9a7a6..f18994d75223ce90bb966570424a400f33e5d208 100644 (file)
Binary files a/theme/identica/default-avatar-stream.png and b/theme/identica/default-avatar-stream.png differ
index 7c68b34f61008753d12394806c3cf46f7b04effd..b32c6f951c626f044ae7a8bdb46c69ca1e416cd6 100644 (file)
Binary files a/theme/identica/logo.png and b/theme/identica/logo.png differ
index 5f1ed843942ee286c05c4b883f43601b910eb874..be375e75a6397837db477599c163ab22a7f34315 100644 (file)
@@ -1,6 +1,4 @@
-<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html
-PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html>
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
        <head>
                <title><?php echo section('title'); ?></title>
@@ -44,4 +42,4 @@ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
                        </div>
                        </div>
                </body>
-       </html>
\ No newline at end of file
+       </html>