]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch '0.8.x' into testing
authorEvan Prodromou <evan@controlyourself.ca>
Mon, 24 Aug 2009 20:55:49 +0000 (16:55 -0400)
committerEvan Prodromou <evan@controlyourself.ca>
Mon, 24 Aug 2009 20:55:49 +0000 (16:55 -0400)
Conflicts:
actions/twitterauthorization.php
lib/oauthclient.php
lib/twitter.php
lib/twitterapi.php
lib/twitteroauthclient.php
scripts/twitterstatusfetcher.php

140 files changed:
README
actions/all.php
actions/allrss.php
actions/api.php
actions/attachment.php
actions/avatarsettings.php
actions/confirmaddress.php
actions/emailsettings.php
actions/favorited.php
actions/favoritesrss.php
actions/finishopenidlogin.php
actions/grouplogo.php
actions/grouprss.php
actions/groupsearch.php
actions/imsettings.php
actions/invite.php
actions/login.php
actions/newnotice.php
actions/noticesearch.php
actions/noticesearchrss.php
actions/oembed.php [new file with mode: 0644]
actions/openidlogin.php
actions/openidsettings.php
actions/opensearch.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/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/twitapigroups.php
actions/twitapioembed.php [deleted file]
actions/twitapistatuses.php
actions/twitterauthorization.php
actions/twittersettings.php
actions/unsubscribe.php
actions/updateprofile.php
actions/userauthorization.php
actions/userrss.php
classes/Design.php
classes/File.php
classes/Foreign_link.php
classes/Notice.php
classes/User_group.php
config.php.sample
db/notice_source.sql
doc-src/sms
extlib/php-gettext/AUTHORS [new file with mode: 0644]
extlib/php-gettext/COPYING [new file with mode: 0644]
extlib/php-gettext/ChangeLog [new file with mode: 0644]
extlib/php-gettext/README [new file with mode: 0644]
extlib/php-gettext/gettext.inc [new file with mode: 0644]
extlib/php-gettext/gettext.php [new file with mode: 0644]
extlib/php-gettext/streams.php [new file with mode: 0644]
index.php
install.php
js/jcrop/jquery.Jcrop.min.js [new file with mode: 0644]
js/jcrop/jquery.Jcrop.pack.js [deleted file]
js/util.js
lib/accountsettingsaction.php
lib/action.php
lib/arraywrapper.php
lib/common.php
lib/connectsettingsaction.php
lib/designsettings.php
lib/error.php
lib/facebookaction.php
lib/htmloutputter.php
lib/jsonsearchresultslist.php
lib/logingroupnav.php
lib/mail.php
lib/noticelist.php
lib/oauthclient.php
lib/parallelizingdaemon.php [new file with mode: 0644]
lib/router.php
lib/search_engines.php
lib/twitter.php
lib/twitterapi.php
lib/twitteroauthclient.php
lib/unqueuemanager.php
lib/util.php
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/Realtime/RealtimePlugin.php
plugins/recaptcha/recaptcha.php
scripts/fixup_utf8.php [changed mode: 0644->0755]
scripts/getvaliddaemons.php
scripts/maildaemon.php
scripts/stopdaemons.sh
scripts/synctwitterfriends.php
scripts/twitterstatusfetcher.php
tests/URLDetectionTest.php [new file with mode: 0644]
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..c13e28791d98f904e3db4459be6b2ef1d284af3e 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".
 
-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.
+* Importing statuses from Twitter
 
-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.
+To allow your users to import their friends' Twitter statuses, you will
+need to enable the bidirectional Twitter bridge in config.php:
 
-Sample cron job:
+      $config['twitterbridge']['enabled'] = true;
 
-# Update Twitter friends subscriptions every half hour
-0,30 * * * * /path/to/php /path/to/laconica/scripts/synctwitterfriends.php>&/dev/null
+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.:
+
+      $config['integration']['source'] = 'YourApp';
+
+* 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
@@ -1167,6 +1197,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 +1266,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
 -----------
 
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 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 ff0b35218f5ccb0ecd38208e6771f3cb7970f813..a291958261eff2c9222da5a0099a2074de4bf88e 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 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 50de83f6fb2574dc2627c2c3b87da9785110ba3a..6f1b4777e5ddb2f8960ab4a80c4e8e61480922de 100644 (file)
@@ -65,6 +65,8 @@ class LoginAction extends Action
      *
      * Switches on request method; either shows the form or handles its input.
      *
+     * Checks if only OpenID is allowed and redirects to openidlogin if so.
+     *
      * @param array $args $_REQUEST data
      *
      * @return void
@@ -73,7 +75,9 @@ class LoginAction extends Action
     function handle($args)
     {
         parent::handle($args);
-        if (common_is_real_login()) {
+        if (common_config('site', 'openidonly')) {
+            common_redirect(common_local_url('openidlogin'));
+        } else if (common_is_real_login()) {
             $this->clientError(_('Already logged in.'));
         } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
             $this->checkLogin();
@@ -247,11 +251,15 @@ 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, or ' .
                      'try [OpenID](%%action.openidlogin%%). ');
+        } else {
+            return _('Login with your username and password. ' .
+                     'Don\'t have a username yet? ' .
+                     '[Register](%%action.register%%) a new account.');
         }
     }
 
index e254eac4999852389b8623b36db62d10826f5bb3..c120b256a97068b9fb20275cbc7df83cb6eb76f3 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) {
@@ -190,37 +190,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 +233,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);
@@ -367,7 +367,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 a8d052096ca470712c43b78e0a2988c6546a5605..744aae7136a851d2604a6525706d5568898f5e29 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 5f59ebc014d57bfe9ff4a1279dc3a4714e98fb72..40a480dc43e9182e8222e5274bc32413778b1ece 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 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 fb847680b9c158e85dc319d89029933edddabc2d..d8fb2c9bb1cdd2adc8a6344a0ba5f4788a28f3eb 100644 (file)
@@ -189,7 +189,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)) {
index d0317ac70660c7dcf5a3f1a02fc59c65b2d7fe97..2cf2e96e685c606f5214f9b57a17be80d67be9da 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;
     }
 
@@ -183,7 +196,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');
             }
        }
 
@@ -203,15 +217,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();
 
@@ -238,9 +244,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 dcbbbdb6a6de9afb238715b7ff1cac3d78f40b7d..683d21af82c1b22ed102e4b2781e288da80298cf 100644 (file)
@@ -116,6 +116,8 @@ class RegisterAction extends Action
      *
      * Checks if registration is closed and shows an error if so.
      *
+     * Checks if only OpenID is allowed and redirects to openidlogin if so.
+     *
      * @param array $args $_REQUEST data
      *
      * @return void
@@ -127,6 +129,8 @@ class RegisterAction extends Action
 
         if (common_config('site', 'closed')) {
             $this->clientError(_('Registration not allowed.'));
+        } else if (common_config('site', 'openidonly')) {
+            common_redirect(common_local_url('openidlogin'));
         } else if (common_logged_in()) {
             $this->clientError(_('Already logged in.'));
         } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
@@ -325,14 +329,22 @@ class RegisterAction extends Action
         } else if ($this->error) {
             $this->element('p', 'error', $this->error);
         } else {
-            $instr =
-              common_markup_to_html(_('With this form you can create '.
-                                      ' a new account. ' .
-                                      'You can then post notices and '.
-                                      'link up to friends and colleagues. '.
-                                      '(Have an [OpenID](http://openid.net/)? ' .
-                                      'Try our [OpenID registration]'.
-                                      '(%%action.openidlogin%%)!)'));
+            if (common_config('openid', 'enabled')) {
+                $instr =
+                  common_markup_to_html(_('With this form you can create '.
+                                          ' a new account. ' .
+                                          'You can then post notices and '.
+                                          'link up to friends and colleagues. '.
+                                          '(Have an [OpenID](http://openid.net/)? ' .
+                                          'Try our [OpenID registration]'.
+                                          '(%%action.openidlogin%%)!)'));
+            } else {
+                $instr =
+                  common_markup_to_html(_('With this form you can create '.
+                                          ' a new account. ' .
+                                          'You can then post notices and '.
+                                          'link up to friends and colleagues.'));
+            }
 
             $this->elementStart('div', 'instructions');
             $this->raw($instr);
index e658f8d3748ed290159222c5ed13867187e3be3e..7323103fc5c0d5bb289ede1815c5979e1dde8c7f 100644 (file)
@@ -71,11 +71,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);
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 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 8f73dc824af6d6180fab885c571b7c265d8ebf0e..fb15dddcfdcbeaec022120664e0910d552ef15a9 100644 (file)
@@ -97,8 +97,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;
         }
 
@@ -190,7 +190,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)) {
@@ -278,16 +278,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 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 c9943698dc2bc06510c2d5bafdf506e7e6b6ba43..185129d5eee91f2634a8d613f29da8f80066ad70 100644 (file)
@@ -449,7 +449,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)
@@ -461,7 +462,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)
@@ -470,7 +472,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);
@@ -526,26 +528,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;
index 2390034cde6226f538a6513457b14a0b625b5bd7..866e3a1e7ae63439f3b3a02be0946ad99f5044e0 100644 (file)
@@ -43,6 +43,13 @@ class TwitterauthorizationAction extends Action
         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);
@@ -51,7 +58,7 @@ class TwitterauthorizationAction extends Action
             $this->clientError(_('Not logged in.'), 403);
         }
 
-        $user = common_current_user();
+        $user  = common_current_user();
         $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
 
         // If there's already a foreign link record, it means we already
@@ -66,88 +73,128 @@ class TwitterauthorizationAction extends Action
         // process
 
         if (empty($this->oauth_token)) {
+            $this->authorizeRequestToken();
+        } else {
+            $this->saveAccessToken();
+        }
+    }
 
-            try {
+    /**
+     * 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
+            // Get a new request token and authorize it
 
-                $client = new TwitterOAuthClient();
-                $req_tok = $client->getRequestToken();
+            $client  = new TwitterOAuthClient();
+            $req_tok =
+              $client->getRequestToken(TwitterOAuthClient::$requestTokenURL);
 
-                // Sock the request token away in the session temporarily
+            // Sock the request token away in the session temporarily
 
-                $_SESSION['twitter_request_token'] = $req_tok->key;
-                $_SESSION['twitter_request_token_secret'] = $req_tok->key;
+            $_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.'));
-            }
+            $auth_link = $client->getAuthorizeLink($req_tok);
 
-            common_redirect($auth_link);
+        } 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.'));
+        }
 
-        } else {
+        common_redirect($auth_link);
+    }
 
-            // Check to make sure Twitter returned the same request
-            // token we sent them
+    /**
+     * Called when Twitter returns an authorized request token. Exchanges
+     * it for an access token and stores it.
+     *
+     * @return nothing
+     */
+    function saveAccessToken()
+    {
 
-            if ($_SESSION['twitter_request_token'] != $this->oauth_token) {
-                $this->serverError(_('Couldn\'t link your Twitter account.'));
-            }
+        // Check to make sure Twitter returned the same request
+        // token we sent them
 
-            try {
+        if ($_SESSION['twitter_request_token'] != $this->oauth_token) {
+            $this->serverError(_('Couldn\'t link your Twitter account.'));
+        }
 
-                $client = new TwitterOAuthClient($_SESSION['twitter_request_token'],
-                                 $_SESSION['twitter_request_token_secret']);
+        try {
 
-                // Exchange the request token for an access token
+            $client = new TwitterOAuthClient($_SESSION['twitter_request_token'],
+                $_SESSION['twitter_request_token_secret']);
 
-                $atok = $client->getAccessToken();
+            // Exchange the request token for an access token
 
-                // Save the access token and Twitter user info
+            $atok = $client->getAccessToken(TwitterOAuthClient::$accessTokenURL);
 
-                $client = new TwitterOAuthClient($atok->key, $atok->secret);
+            // Test the access token and get the user's Twitter info
 
-                $twitter_user = $client->verify_credentials();
+            $client       = new TwitterOAuthClient($atok->key, $atok->secret);
+            $twitter_user = $client->verifyCredentials();
 
-            } catch (OAuthClientException $e) {
-                $msg = sprintf('OAuth client cURL error - code: %1s, msg: %2s',
+        } 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.'));
-            }
+            $this->serverError(_('Couldn\'t link your Twitter account.'));
+        }
 
-            $user = common_current_user();
+        // Save the access token and Twitter user info
 
-            $flink = new Foreign_link();
+        $this->saveForeignLink($atok, $twitter_user);
 
-            $flink->user_id     = $user->id;
-            $flink->foreign_id  = $twitter_user->id;
-            $flink->service     = TWITTER_SERVICE;
-            $flink->token       = $atok->key;
-            $flink->credentials = $atok->secret;
-            $flink->created     = common_sql_now();
+        // Clean up the the mess we made in the session
 
-            $flink->set_flags(true, false, false, false);
+        unset($_SESSION['twitter_request_token']);
+        unset($_SESSION['twitter_request_token_secret']);
 
-            $flink_id = $flink->insert();
+        common_redirect(common_local_url('twittersettings'));
+    }
 
-            if (empty($flink_id)) {
-                common_log_db_error($flink, 'INSERT', __FILE__);
-                $this->serverError(_('Couldn\'t link your Twitter account.'));
-            }
+    /**
+     * 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();
 
-            save_twitter_user($twitter_user->id, $twitter_user->screen_name);
+        $flink = new Foreign_link();
 
-            // clean up the the mess we made in the session
+        $flink->user_id     = $user->id;
+        $flink->foreign_id  = $twitter_user->id;
+        $flink->service     = TWITTER_SERVICE;
 
-            unset($_SESSION['twitter_request_token']);
-            unset($_SESSION['twitter_request_token_secret']);
+        $creds = TwitterOAuthClient::packToken($access_token);
 
-            common_redirect(common_local_url('twittersettings'));
+        $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 7fffa0af018b8b5909d26fa51651f6701afc3382..0859ab9d34ef55c8b45bc9ad71f0e257f6aeb154 100644 (file)
@@ -82,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();
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..f6cb277aa7a6b98c1f11cbf6514b3dbe2b40a8fd 100644 (file)
@@ -79,7 +79,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;
         }
index 8dc2c808d6ff849f0da742e342de137203eeacd4..7e397e8888b216ed5512cb8f57b0dcb050e2bcf4 100644 (file)
@@ -47,7 +47,11 @@ class UserauthorizationAction extends Action
                 # 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;
             }
 
@@ -481,7 +485,7 @@ class UserauthorizationAction extends Action
         $nickname = $_GET['omb_listenee_nickname'];
         if (!Validate::string($nickname, array('min_length' => 1,
                                                'max_length' => 64,
-                                               'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
+                                               'format' => NICKNAME_FMT))) {
             throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
         }
         $profile = $_GET['omb_listenee_profile'];
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 9354bfcda8c6fab07b662521e28a8a0f5ba28218..19c9e0292494e9242702370f263b1a86df6e5ec0 100644 (file)
@@ -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 a3a159eb54a29d7e85bbb80413f643b78d0eb3d0..bc3ab006db43ea17f572f88828f1ed9707cf3de7 100644 (file)
@@ -30,34 +30,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)
@@ -67,7 +71,7 @@ class Foreign_link extends Memcached_DataObject
         } else {
             $this->noticesync &= ~FOREIGN_NOTICE_SEND;
         }
-        
+
         if ($noticerecv) {
             $this->noticesync |= FOREIGN_NOTICE_RECV;
         } else {
index ebd5e1efd5c9c1b00b7641ec43b3e59822ce6617..442eb00fdb2333de84025cee7f1c5e2241e30108 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
@@ -62,7 +58,11 @@ class Notice extends Memcached_DataObject
     /* 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()
     {
@@ -134,7 +134,7 @@ 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);
 
@@ -177,7 +177,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;
         }
@@ -509,7 +509,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'));
@@ -775,10 +775,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) {
index b1ab1c2d313c6537f4fc0db0f01a16b51dfc6c24..ea19cbb97acff0b2a752ebc1793d02e81a94903b 100644 (file)
@@ -297,4 +297,45 @@ class User_group extends Memcached_DataObject
 
         return $ids;
     }
+
+    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 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 71fa893446dfdb443fba26032d81cb1de67e5141..f590d1b97a7fa2cd83017b309688e372b37c99d3 100644 (file)
@@ -22,6 +22,7 @@ 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()),
@@ -35,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 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/php-gettext/AUTHORS b/extlib/php-gettext/AUTHORS
new file mode 100644 (file)
index 0000000..da6ade7
--- /dev/null
@@ -0,0 +1,3 @@
+Danilo Segan <danilo@kvota.net>
+Nico Kaiser <nico@siriux.net> (contributed most changes between 1.0.2 and 1.0.3, bugfix for 1.0.5)
+Steven Armstrong <sa@c-area.ch> (gettext.inc, leading to 1.0.6)
diff --git a/extlib/php-gettext/COPYING b/extlib/php-gettext/COPYING
new file mode 100644 (file)
index 0000000..5b6e7c6
--- /dev/null
@@ -0,0 +1,340 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                       59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+\f
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+\f
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+\f
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+\f
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+\f
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/extlib/php-gettext/ChangeLog b/extlib/php-gettext/ChangeLog
new file mode 100644 (file)
index 0000000..5e0949d
--- /dev/null
@@ -0,0 +1,144 @@
+2006-02-07  Danilo Šegan  <danilo@gnome.org>
+
+       * examples/pigs_dropin.php: comment-out bind_textdomain_codeset
+
+       * gettext.inc (T_bind_textdomain_codeset): bind_textdomain_codeset
+       is available only in PHP 4.2.0+ (thanks to Jens A. Tkotz).
+
+       * Makefile: Include gettext.inc in DIST_FILES, VERSION up to
+       1.0.7.
+
+2006-02-03  Danilo Šegan  <danilo@gnome.org>
+
+       Added setlocale() emulation as well.
+       
+       * examples/pigs_dropin.php: Use T_setlocale() and locale_emulation().
+       * examples/pigs_fallback.php: Use T_setlocale() and locale_emulation().
+
+       * gettext.inc:  Added globals $EMULATEGETTEXT and $CURRENTLOCALE.
+       (locale_emulation): Whether emulation is active.
+       (_check_locale): Rewrite.
+       (_setlocale): Added emulated setlocale function.
+       (T_setlocale): Wrapper around _setlocale.
+       (_get_reader): Use variables and _setlocale.
+
+2006-02-02  Danilo Šegan  <danilo@gnome.org>
+
+       Fix bug #12192.
+
+       * examples/locale/sr_CS/LC_MESSAGES/messages.po: Correct grammar.
+       * examples/locale/sr_CS/LC_MESSAGES/messages.mo: Rebuild.
+
+2006-02-02  Danilo Šegan  <danilo@gnome.org>
+
+       Fix bug #15419.
+       
+       * streams.php: Support for PHP 5.1.1 fread() which reads most 8kb.
+       (Fix by Piotr Szotkowski <shot@hot.pl>)
+
+2006-02-02  Danilo Šegan  <danilo@gnome.org>
+
+       Merge Steven Armstrong's changes, supporting standard gettext
+       interfaces: 
+
+       * examples/*: Restructured examples.
+       * gettext.inc: Added.
+       * AUTHORS: Added Steven.
+       * Makefile (VERSION): Up to 1.0.6.
+       
+2006-01-28  Nico Kaiser  <nico@siriux.net>
+
+       * gettext.php (select_string): Fix "true" <-> 1 difference of PHP
+
+2005-07-29  Danilo Šegan  <danilo@gnome.org>
+
+       * Makefile (VERSION): Up to 1.0.5.
+
+2005-07-29  Danilo Šegan  <danilo@gnome.org>
+
+       Fixes bug #13850.
+       
+       * gettext.php (gettext_reader): check $Reader->error as well.
+
+2005-07-29  Danilo Šegan  <danilo@gnome.org>
+
+       * Makefile (VERSION): Up to 1.0.4.
+
+2005-07-29  Danilo Šegan  <danilo@gnome.org>
+
+       Fixes bug #13771.
+
+       * gettext.php (gettext_reader->get_plural_forms): Plural forms
+       header extraction regex change.  Reported by Edgar Gonzales.
+       
+2005-02-28  Danilo Šegan  <dsegan@gmx.net>
+
+       * AUTHORS: Added Nico to the list.
+
+       * Makefile (VERSION): Up to 1.0.3.
+
+       * README: Updated.
+
+2005-02-28  Danilo Šegan  <dsegan@gmx.net>
+
+       * gettext.php: Added pre-loading, code documentation, and many
+       code clean-ups by Nico Kaiser <nico@siriux.net>.
+
+2005-02-28  Danilo Šegan  <dsegan@gmx.net>
+
+       * streams.php (FileReader.read): Handle read($bytes = 0).
+
+       * examples/pigs.php: Prefix gettext function names with T or T_.
+
+       * examples/update: Use the same keywords T_ and T_ngettext.
+
+       * streams.php: Added CachedFileReader.
+
+2003-11-11  Danilo Šegan  <dsegan@gmx.net>
+
+       * gettext.php: Added hashing to find_string.
+
+2003-11-01  Danilo Šegan  <dsegan@gmx.net>
+
+       * Makefile (DIST_FILES): Replaced LICENSE with COPYING.
+       (VERSION): Up to 1.0.2.
+
+       * AUTHORS: Minor edits.
+
+       * README: Minor edits.
+
+       * COPYING: Removed LICENSE, added this file.
+
+       * gettext.php: Added copyright notice and disclaimer.
+       * streams.php: Same.
+       * examples/pigs.php: Same.
+
+2003-10-23  Danilo Šegan  <dsegan@gmx.net>
+
+       * Makefile: Upped version to 1.0.1.
+
+       * gettext.php (gettext_reader): Remove a call to set_total_plurals.
+       (set_total_plurals): Removed unused function for some better days.
+
+2003-10-23  Danilo Šegan  <dsegan@gmx.net>
+
+       * Makefile: Added, version 1.0.0.
+
+       * examples/*: Added an example of usage.
+
+       * README: Described all the crap.
+       
+2003-10-22  Danilo Šegan  <dsegan@gmx.net>
+
+       * gettext.php: Plural forms implemented too.
+
+       * streams.php: Added FileReader for direct access to files (no
+       need to keep file in memory).
+
+       * gettext.php: It works, except for plural forms.
+
+       * streams.php: Created abstract class StreamReader.
+       Added StringReader class.
+
+       * gettext.php: Started writing gettext_reader.
+
diff --git a/extlib/php-gettext/README b/extlib/php-gettext/README
new file mode 100644 (file)
index 0000000..c7525e2
--- /dev/null
@@ -0,0 +1,189 @@
+PHP-gettext 1.0
+
+Copyright 2003, 2006 -- Danilo "angry with PHP[1]" Segan
+Licensed under GPLv2 (or any later version, see COPYING)
+
+[1] PHP is actually cyrillic, and translates roughly to 
+    "works-doesn't-work" (UTF-8: Ради-Не-Ради)
+
+
+Introduction
+
+    How many times did you look for a good translation tool, and
+    found out that gettext is best for the job? Many times.
+
+    How many times did you try to use gettext in PHP, but failed
+    miserably, because either your hosting provider didn't support
+    it, or the server didn't have adequate locale? Many times.
+
+    Well, this is a solution to your needs. It allows using gettext
+    tools for managing translations, yet it doesn't require gettext
+    library at all. It parses generated MO files directly, and thus
+    might be a bit slower than the (maybe provided) gettext library.
+
+    PHP-gettext is a simple reader for GNU gettext MO files. Those
+    are binary containers for translations, produced by GNU msgfmt.
+
+Why?
+
+    I got used to having gettext work even without gettext
+    library. It's there in my favourite language Python, so I was
+    surprised that I couldn't find it in PHP. I even Googled for it,
+    but to no avail.
+
+    So, I said, what the heck, I'm going to write it for this
+    disguisting language of PHP, because I'm often constrained to it.
+
+Features
+
+  o Support for simple translations
+    Just define a simple alias for translate() function (suggested
+    use of _() or gettext(); see provided example).
+
+  o Support for ngettext calls (plural forms, see a note under bugs)
+    You may also use plural forms. Translations in MO files need to
+    provide this, and they must also provide "plural-forms" header.
+    Please see 'info gettext' for more details.
+
+  o Support for reading straight files, or strings (!!!)
+    Since I can imagine many different backends for reading in the MO
+    file data, I used imaginary abstract class StreamReader to do all
+    the input (check streams.php). For your convenience, I've already
+    provided two classes for reading files: FileReader and
+    StringReader (CachedFileReader is a combination of the two: it 
+    loads entire file contents into a string, and then works on that). 
+    See example below for usage. You can for instance use StringReader 
+    when you read in data from a database, or you can create your own 
+    derivative of StreamReader for anything you like. 
+    
+
+Bugs
+
+    Plural-forms field in MO header (translation for empty string,
+    i.e. "") is treated according to PHP syntactic rules (it's
+    eval()ed). Since these should actually follow C syntax, there are
+    some problems.
+
+    For instance, I'm used to using this:
+      Plural-Forms: nplurals=3;    plural=n%10==1 && n%100!=11 ? 0 : \
+         n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;
+    but it fails with PHP (it sets $plural=2 instead of 0 for $n==1).
+
+    The fix is usually simple, but I'm lazy to go into the details of
+    PHP operator precedence, and maybe try to fix it. In here, I had
+    to put everything after the first ':' in parenthesis:
+      Plural-Forms: nplurals=3;    plural=n%10==1 && n%100!=11 ? 0 : \
+         (n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
+    That works, and I'm satisfied.
+
+    Besides this one, there are probably a bunch of other bugs, since
+    I hate PHP (did I mention it already? no? strange), and don't
+    know it very well. So, feel free to fix any of those and report
+    them back to me at <danilo@kvota.net>.
+
+Usage
+
+    Put files streams.php and gettext.php somewhere you can load them
+    from, and require 'em in where you want to use them.
+
+    Then, create one 'stream reader' (a class that provides functions
+    like read(), seekto(), currentpos() and length()) which will
+    provide data for the 'gettext_reader', with eg.
+      $streamer = new FileStream('data.mo');
+
+    Then, use that as a parameter to gettext_reader constructor:
+      $wohoo = new gettext_reader($streamer);
+
+    If you want to disable pre-loading of entire message catalog in 
+    memory (if, for example, you have a multi-thousand message catalog 
+    which you'll use only occasionally), use "false" for second 
+    parameter to gettext_reader constructor:
+      $wohoo = new gettext_reader($streamer, false);
+
+    From now on, you have all the benefits of gettext data at your
+    disposal, so may run: 
+      print $wohoo->translate("This is a test");
+      print $wohoo->ngettext("%d bird", "%d birds", $birds);
+
+    You might need to pass parameter "-k" to xgettext to make it
+    extract all the strings. In above example, try with 
+      xgettext -ktranslate -kngettext:1,2 file.php
+    what should create messages.po which contains two messages for
+    translation.
+
+    I suggest creating simple aliases for these functions (see
+    example/pigs.php for how do I do it, which means it's probably a
+    bad way).
+
+
+Usage with gettext.inc (standard gettext interfaces emulation)
+
+    Check example in examples/pig_dropin.php, basically you include 
+    gettext.inc and use all the standard gettext interfaces as 
+    documented on:
+
+       http://www.php.net/gettext
+
+    The only catch is that you can check return value of setlocale()
+    to see if your locale is system supported or not.
+
+
+Example
+
+    See in examples/ subdirectory. There are a couple of files.
+    pigs.php is an example, serbian.po is a translation to Serbian
+    language, and serbian.mo is generated with
+       msgfmt -o serbian.mo serbian.po
+    There is also simple "update" script that can be used to generate
+    POT file and to update the translation using msgmerge.
+
+Interesting TODO:
+
+  o Try to parse "plural-forms" header field, and to follow C syntax
+    rules. This won't be easy.
+
+Boring TODO:
+
+  o Learn PHP and fix bugs, slowness and other stuff resulting from
+    my lack of knowledge (but *maybe*, it's not my knowledge that is
+    bad, but PHP itself ;-).  
+
+    (This is mostly done thanks to Nico Kaiser.)
+
+  o Try to use hash tables in MO files: with pre-loading, would it 
+    be useful at all?
+
+Never-asked-questions:
+
+  o Why did you mark this as version 1.0 when this is the first code
+    release?
+
+    Well, it's quite simple. I consider that the first released thing
+    should be labeled "version 1" (first, right?). Zero is there to
+    indicate that there's zero improvement and/or change compared to 
+    "version 1".
+
+    I plan to use version numbers 1.0.* for small bugfixes, and to
+    release 1.1 as "first stable release of version 1".
+
+    This may trick someone that this is actually useful software, but
+    as with any other free software, I take NO RESPONSIBILITY for
+    creating such a masterpiece that will smoke crack, trash your
+    hard disk, and make lasers in your CD device dance to the tune of
+    Mozart's 40th Symphony (there is one like that, right?).
+
+  o Can I...?
+    
+    Yes, you can. This is free software (as in freedom, free speech),
+    and you might do whatever you wish with it, provided you do not
+    limit freedom of others (GPL).
+
+    I'm considering licensing this under LGPL, but I *do* want
+    *every* PHP-gettext user to contribute and respect ideas of free
+    software, so don't count on it happening anytime soon.
+
+    I'm sorry that I'm taking away your freedom of taking others'
+    freedom away, but I believe that's neglible as compared to what
+    freedoms you could take away. ;-)
+
+    Uhm, whatever.
diff --git a/extlib/php-gettext/gettext.inc b/extlib/php-gettext/gettext.inc
new file mode 100644 (file)
index 0000000..eb94b25
--- /dev/null
@@ -0,0 +1,318 @@
+<?php
+/*
+   Copyright (c) 2005 Steven Armstrong <sa at c-area dot ch>
+   
+   Drop in replacement for native gettext.
+   
+   This file is part of PHP-gettext.
+
+   PHP-gettext is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 2 of the License, or
+   (at your option) any later version.
+
+   PHP-gettext is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with PHP-gettext; if not, write to the Free Software
+   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+*/
+/*
+LC_CTYPE               0
+LC_NUMERIC     1
+LC_TIME                        2
+LC_COLLATE     3
+LC_MONETARY    4
+LC_MESSAGES    5
+LC_ALL                 6
+*/
+
+require('streams.php');
+require('gettext.php');
+
+
+// Variables
+
+global $text_domains, $default_domain, $LC_CATEGORIES, $EMULATEGETTEXT, $CURRENTLOCALE;
+$text_domains = array();
+$default_domain = 'messages';
+$LC_CATEGORIES = array('LC_CTYPE', 'LC_NUMERIC', 'LC_TIME', 'LC_COLLATE', 'LC_MONETARY', 'LC_MESSAGES', 'LC_ALL');
+$EMULATEGETTEXT = 0;
+$CURRENTLOCALE = '';
+
+
+// Utility functions
+
+/**
+ * Utility function to get a StreamReader for the given text domain.
+ */
+function _get_reader($domain=null, $category=5, $enable_cache=true) {
+       global $text_domains, $default_domain, $LC_CATEGORIES;
+       if (!isset($domain)) $domain = $default_domain;
+       if (!isset($text_domains[$domain]->l10n)) {
+               // get the current locale
+               $locale = _setlocale(LC_MESSAGES, 0);
+               $p = isset($text_domains[$domain]->path) ? $text_domains[$domain]->path : './';
+               $path = $p . "$locale/". $LC_CATEGORIES[$category] ."/$domain.mo";
+               if (file_exists($path)) {
+                       $input = new FileReader($path);
+               }
+               else {
+                       $input = null;
+               }
+               $text_domains[$domain]->l10n = new gettext_reader($input, $enable_cache);
+       }
+       return $text_domains[$domain]->l10n;
+}
+
+/**
+ * Returns whether we are using our emulated gettext API or PHP built-in one.
+ */
+function locale_emulation() {
+    global $EMULATEGETTEXT;
+    return $EMULATEGETTEXT;
+}
+
+/**
+ * Checks if the current locale is supported on this system.
+ */
+function _check_locale() {
+    global $EMULATEGETTEXT;
+    return !$EMULATEGETTEXT;
+}
+
+/**
+ * Get the codeset for the given domain.
+ */
+function _get_codeset($domain=null) {
+       global $text_domains, $default_domain, $LC_CATEGORIES;
+       if (!isset($domain)) $domain = $default_domain;
+       return (isset($text_domains[$domain]->codeset))? $text_domains[$domain]->codeset : ini_get('mbstring.internal_encoding');
+}
+
+/**
+ * Convert the given string to the encoding set by bind_textdomain_codeset.
+ */
+function _encode($text) {
+       $source_encoding = mb_detect_encoding($text);
+       $target_encoding = _get_codeset();
+       if ($source_encoding != $target_encoding) {
+               return mb_convert_encoding($text, $target_encoding, $source_encoding);
+       }
+       else {
+               return $text;
+       }
+}
+
+
+
+
+// Custom implementation of the standard gettext related functions
+
+/**
+ * Sets a requested locale, if needed emulates it.
+ */
+function _setlocale($category, $locale) {
+    global $CURRENTLOCALE, $EMULATEGETTEXT;
+    if ($locale === 0) { // use === to differentiate between string "0"
+        if ($CURRENTLOCALE != '') 
+            return $CURRENTLOCALE;
+        else 
+            // obey LANG variable, maybe extend to support all of LC_* vars
+            // even if we tried to read locale without setting it first
+            return _setlocale($category, $CURRENTLOCALE);
+    } else {
+        $ret = 0;
+        if (function_exists('setlocale')) // I don't know if this ever happens ;)
+           $ret = setlocale($category, $locale);
+        if (($ret and $locale == '') or ($ret == $locale)) {
+            $EMULATEGETTEXT = 0;
+            $CURRENTLOCALE = $ret;
+        } else {
+           if ($locale == '') // emulate variable support
+               $CURRENTLOCALE = getenv('LANG');
+           else
+               $CURRENTLOCALE = $locale;
+            $EMULATEGETTEXT = 1;
+        }
+        return $CURRENTLOCALE;
+    }
+}
+
+/**
+ * Sets the path for a domain.
+ */
+function _bindtextdomain($domain, $path) {
+       global $text_domains;
+       // ensure $path ends with a slash
+       if ($path[strlen($path) - 1] != '/') $path .= '/';
+       elseif ($path[strlen($path) - 1] != '\\') $path .= '\\';
+       $text_domains[$domain]->path = $path;
+}
+
+/**
+ * Specify the character encoding in which the messages from the DOMAIN message catalog will be returned.
+ */
+function _bind_textdomain_codeset($domain, $codeset) {
+       global $text_domains;
+       $text_domains[$domain]->codeset = $codeset;
+}
+
+/**
+ * Sets the default domain.
+ */
+function _textdomain($domain) {
+       global $default_domain;
+       $default_domain = $domain;
+}
+
+/**
+ * Lookup a message in the current domain.
+ */
+function _gettext($msgid) {
+       $l10n = _get_reader();
+       //return $l10n->translate($msgid);
+       return _encode($l10n->translate($msgid));
+}
+/**
+ * Alias for gettext.
+ */
+function __($msgid) {
+       return _gettext($msgid);
+}
+/**
+ * Plural version of gettext.
+ */
+function _ngettext($single, $plural, $number) {
+       $l10n = _get_reader();
+       //return $l10n->ngettext($single, $plural, $number);
+       return _encode($l10n->ngettext($single, $plural, $number));
+}
+
+/**
+ * Override the current domain.
+ */
+function _dgettext($domain, $msgid) {
+       $l10n = _get_reader($domain);
+       //return $l10n->translate($msgid);
+       return _encode($l10n->translate($msgid));
+}
+/**
+ * Plural version of dgettext.
+ */
+function _dngettext($domain, $single, $plural, $number) {
+       $l10n = _get_reader($domain);
+       //return $l10n->ngettext($single, $plural, $number);
+       return _encode($l10n->ngettext($single, $plural, $number));
+}
+
+/**
+ * Overrides the domain and category for a single lookup.
+ */
+function _dcgettext($domain, $msgid, $category) {
+       $l10n = _get_reader($domain, $category);
+       //return $l10n->translate($msgid);
+       return _encode($l10n->translate($msgid));
+}
+/**
+ * Plural version of dcgettext.
+ */
+function _dcngettext($domain, $single, $plural, $number, $category) {
+       $l10n = _get_reader($domain, $category);
+       //return $l10n->ngettext($single, $plural, $number);
+       return _encode($l10n->ngettext($single, $plural, $number));
+}
+
+
+
+// Wrappers to use if the standard gettext functions are available, but the current locale is not supported by the system.
+// Use the standard impl if the current locale is supported, use the custom impl otherwise.
+
+function T_setlocale($category, $locale) {
+    return _setlocale($category, $locale);
+}
+
+function T_bindtextdomain($domain, $path) {
+       if (_check_locale()) return bindtextdomain($domain, $path);
+       else return _bindtextdomain($domain, $path);
+}
+function T_bind_textdomain_codeset($domain, $codeset) {
+    // bind_textdomain_codeset is available only in PHP 4.2.0+
+       if (_check_locale() and function_exists('bind_textdomain_codeset')) return bind_textdomain_codeset($domain, $codeset);
+       else return _bind_textdomain_codeset($domain, $codeset);
+}
+function T_textdomain($domain) {
+       if (_check_locale()) return textdomain($domain);
+       else return _textdomain($domain);
+}
+function T_gettext($msgid) {
+       if (_check_locale()) return gettext($msgid);
+       else return _gettext($msgid);
+}
+function T_($msgid) {
+       if (_check_locale()) return _($msgid);
+       return __($msgid);
+}
+function T_ngettext($single, $plural, $number) {
+       if (_check_locale()) return ngettext($single, $plural, $number);
+       else return _ngettext($single, $plural, $number);
+}
+function T_dgettext($domain, $msgid) {
+       if (_check_locale()) return dgettext($domain, $msgid);
+       else return _dgettext($domain, $msgid);
+}
+function T_dngettext($domain, $single, $plural, $number) {
+       if (_check_locale()) return dngettext($domain, $single, $plural, $number);
+       else return _dngettext($domain, $single, $plural, $number);
+}
+function T_dcgettext($domain, $msgid, $category) {
+       if (_check_locale()) return dcgettext($domain, $msgid, $category);
+       else return _dcgettext($domain, $msgid, $category);
+}
+function T_dcngettext($domain, $single, $plural, $number, $category) {
+       if (_check_locale()) return dcngettext($domain, $single, $plural, $number, $category);
+       else return _dcngettext($domain, $single, $plural, $number, $category);
+}
+
+
+
+// Wrappers used as a drop in replacement for the standard gettext functions
+
+if (!function_exists('gettext')) {
+       function bindtextdomain($domain, $path) {
+               return _bindtextdomain($domain, $path);
+       }
+       function bind_textdomain_codeset($domain, $codeset) {
+               return _bind_textdomain_codeset($domain, $codeset);
+       }
+       function textdomain($domain) {
+               return _textdomain($domain);
+       }
+       function gettext($msgid) {
+               return _gettext($msgid);
+       }
+       function _($msgid) {
+               return __($msgid);
+       }
+       function ngettext($single, $plural, $number) {
+               return _ngettext($single, $plural, $number);
+       }
+       function dgettext($domain, $msgid) {
+               return _dgettext($domain, $msgid);
+       }
+       function dngettext($domain, $single, $plural, $number) {
+               return _dngettext($domain, $single, $plural, $number);
+       }
+       function dcgettext($domain, $msgid, $category) {
+               return _dcgettext($domain, $msgid, $category);
+       }
+       function dcngettext($domain, $single, $plural, $number, $category) {
+               return _dcngettext($domain, $single, $plural, $number, $category);
+       }
+}
+
+?>
\ No newline at end of file
diff --git a/extlib/php-gettext/gettext.php b/extlib/php-gettext/gettext.php
new file mode 100644 (file)
index 0000000..ad94a98
--- /dev/null
@@ -0,0 +1,358 @@
+<?php
+/*
+   Copyright (c) 2003 Danilo Segan <danilo@kvota.net>.
+   Copyright (c) 2005 Nico Kaiser <nico@siriux.net>
+   
+   This file is part of PHP-gettext.
+
+   PHP-gettext is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 2 of the License, or
+   (at your option) any later version.
+
+   PHP-gettext is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with PHP-gettext; if not, write to the Free Software
+   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+*/
+/**
+ * Provides a simple gettext replacement that works independently from
+ * the system's gettext abilities.
+ * It can read MO files and use them for translating strings.
+ * The files are passed to gettext_reader as a Stream (see streams.php)
+ * 
+ * This version has the ability to cache all strings and translations to
+ * speed up the string lookup.
+ * While the cache is enabled by default, it can be switched off with the
+ * second parameter in the constructor (e.g. whenusing very large MO files
+ * that you don't want to keep in memory)
+ */
+class gettext_reader {
+  //public:
+   var $error = 0; // public variable that holds error code (0 if no error)
+   
+   //private:
+  var $BYTEORDER = 0;        // 0: low endian, 1: big endian
+  var $STREAM = NULL;
+  var $short_circuit = false;
+  var $enable_cache = false;
+  var $originals = NULL;      // offset of original table
+  var $translations = NULL;    // offset of translation table
+  var $pluralheader = NULL;    // cache header field for plural forms
+  var $total = 0;          // total string count
+  var $table_originals = NULL;  // table for original strings (offsets)
+  var $table_translations = NULL;  // table for translated strings (offsets)
+  var $cache_translations = NULL;  // original -> translation mapping
+
+
+  /* Methods */
+  
+    
+  /**
+   * Reads a 32bit Integer from the Stream
+   * 
+   * @access private
+   * @return Integer from the Stream
+   */
+  function readint() {
+      if ($this->BYTEORDER == 0) {
+        // low endian
+        return array_shift(unpack('V', $this->STREAM->read(4)));
+      } else {
+        // big endian
+        return array_shift(unpack('N', $this->STREAM->read(4)));
+      }
+    }
+
+  /**
+   * Reads an array of Integers from the Stream
+   * 
+   * @param int count How many elements should be read
+   * @return Array of Integers
+   */
+  function readintarray($count) {
+    if ($this->BYTEORDER == 0) {
+        // low endian
+        return unpack('V'.$count, $this->STREAM->read(4 * $count));
+      } else {
+        // big endian
+        return unpack('N'.$count, $this->STREAM->read(4 * $count));
+      }
+  }
+  
+  /**
+   * Constructor
+   * 
+   * @param object Reader the StreamReader object
+   * @param boolean enable_cache Enable or disable caching of strings (default on)
+   */
+  function gettext_reader($Reader, $enable_cache = true) {
+    // If there isn't a StreamReader, turn on short circuit mode.
+    if (! $Reader || isset($Reader->error) ) {
+      $this->short_circuit = true;
+      return;
+    }
+    
+    // Caching can be turned off
+    $this->enable_cache = $enable_cache;
+
+    // $MAGIC1 = (int)0x950412de; //bug in PHP 5
+    $MAGIC1 = (int) - 1794895138;
+    // $MAGIC2 = (int)0xde120495; //bug
+    $MAGIC2 = (int) - 569244523;
+
+    $this->STREAM = $Reader;
+    $magic = $this->readint();
+    if ($magic == $MAGIC1) {
+      $this->BYTEORDER = 0;
+    } elseif ($magic == $MAGIC2) {
+      $this->BYTEORDER = 1;
+    } else {
+      $this->error = 1; // not MO file
+      return false;
+    }
+    
+    // FIXME: Do we care about revision? We should.
+    $revision = $this->readint();
+    
+    $this->total = $this->readint();
+    $this->originals = $this->readint();
+    $this->translations = $this->readint();
+  }
+  
+  /**
+   * Loads the translation tables from the MO file into the cache
+   * If caching is enabled, also loads all strings into a cache
+   * to speed up translation lookups
+   * 
+   * @access private
+   */
+  function load_tables() {
+    if (is_array($this->cache_translations) &&
+      is_array($this->table_originals) &&
+      is_array($this->table_translations))
+      return;
+    
+    /* get original and translations tables */
+    $this->STREAM->seekto($this->originals);
+    $this->table_originals = $this->readintarray($this->total * 2);
+    $this->STREAM->seekto($this->translations);
+    $this->table_translations = $this->readintarray($this->total * 2);
+    
+    if ($this->enable_cache) {
+      $this->cache_translations = array ();
+      /* read all strings in the cache */
+      for ($i = 0; $i < $this->total; $i++) {
+        $this->STREAM->seekto($this->table_originals[$i * 2 + 2]);
+        $original = $this->STREAM->read($this->table_originals[$i * 2 + 1]);
+        $this->STREAM->seekto($this->table_translations[$i * 2 + 2]);
+        $translation = $this->STREAM->read($this->table_translations[$i * 2 + 1]);
+        $this->cache_translations[$original] = $translation;
+      }
+    }
+  }
+  
+  /**
+   * Returns a string from the "originals" table
+   * 
+   * @access private
+   * @param int num Offset number of original string
+   * @return string Requested string if found, otherwise ''
+   */
+  function get_original_string($num) {
+    $length = $this->table_originals[$num * 2 + 1];
+    $offset = $this->table_originals[$num * 2 + 2];
+    if (! $length)
+      return '';
+    $this->STREAM->seekto($offset);
+    $data = $this->STREAM->read($length);
+    return (string)$data;
+  }
+  
+  /**
+   * Returns a string from the "translations" table
+   * 
+   * @access private
+   * @param int num Offset number of original string
+   * @return string Requested string if found, otherwise ''
+   */
+  function get_translation_string($num) {
+    $length = $this->table_translations[$num * 2 + 1];
+    $offset = $this->table_translations[$num * 2 + 2];
+    if (! $length)
+      return '';
+    $this->STREAM->seekto($offset);
+    $data = $this->STREAM->read($length);
+    return (string)$data;
+  }
+  
+  /**
+   * Binary search for string
+   * 
+   * @access private
+   * @param string string
+   * @param int start (internally used in recursive function)
+   * @param int end (internally used in recursive function)
+   * @return int string number (offset in originals table)
+   */
+  function find_string($string, $start = -1, $end = -1) {
+    if (($start == -1) or ($end == -1)) {
+      // find_string is called with only one parameter, set start end end
+      $start = 0;
+      $end = $this->total;
+    }
+    if (abs($start - $end) <= 1) {
+      // We're done, now we either found the string, or it doesn't exist
+      $txt = $this->get_original_string($start);
+      if ($string == $txt)
+        return $start;
+      else
+        return -1;
+    } else if ($start > $end) {
+      // start > end -> turn around and start over
+      return $this->find_string($string, $end, $start);
+    } else {
+      // Divide table in two parts
+      $half = (int)(($start + $end) / 2);
+      $cmp = strcmp($string, $this->get_original_string($half));
+      if ($cmp == 0)
+        // string is exactly in the middle => return it
+        return $half;
+      else if ($cmp < 0)
+        // The string is in the upper half
+        return $this->find_string($string, $start, $half);
+      else
+        // The string is in the lower half
+        return $this->find_string($string, $half, $end);
+    }
+  }
+  
+  /**
+   * Translates a string
+   * 
+   * @access public
+   * @param string string to be translated
+   * @return string translated string (or original, if not found)
+   */
+  function translate($string) {
+    if ($this->short_circuit)
+      return $string;
+    $this->load_tables();     
+    
+    if ($this->enable_cache) {
+      // Caching enabled, get translated string from cache
+      if (array_key_exists($string, $this->cache_translations))
+        return $this->cache_translations[$string];
+      else
+        return $string;
+    } else {
+      // Caching not enabled, try to find string
+      $num = $this->find_string($string);
+      if ($num == -1)
+        return $string;
+      else
+        return $this->get_translation_string($num);
+    }
+  }
+
+  /**
+   * Get possible plural forms from MO header
+   * 
+   * @access private
+   * @return string plural form header
+   */
+  function get_plural_forms() {
+    // lets assume message number 0 is header  
+    // this is true, right?
+    $this->load_tables();
+    
+    // cache header field for plural forms
+    if (! is_string($this->pluralheader)) {
+      if ($this->enable_cache) {
+        $header = $this->cache_translations[""];
+      } else {
+        $header = $this->get_translation_string(0);
+      }
+      if (eregi("plural-forms: ([^\n]*)\n", $header, $regs))
+        $expr = $regs[1];
+      else
+        $expr = "nplurals=2; plural=n == 1 ? 0 : 1;";
+      $this->pluralheader = $expr;
+    }
+    return $this->pluralheader;
+  }
+
+  /**
+   * Detects which plural form to take
+   * 
+   * @access private
+   * @param n count
+   * @return int array index of the right plural form
+   */
+  function select_string($n) {
+    $string = $this->get_plural_forms();
+    $string = str_replace('nplurals',"\$total",$string);
+    $string = str_replace("n",$n,$string);
+    $string = str_replace('plural',"\$plural",$string);
+    
+    $total = 0;
+    $plural = 0;
+
+    eval("$string");
+    if ($plural >= $total) $plural = $total - 1;
+    return $plural;
+  }
+
+  /**
+   * Plural version of gettext
+   * 
+   * @access public
+   * @param string single
+   * @param string plural
+   * @param string number
+   * @return translated plural form
+   */
+  function ngettext($single, $plural, $number) {
+    if ($this->short_circuit) {
+      if ($number != 1)
+        return $plural;
+      else
+        return $single;
+    }
+
+    // find out the appropriate form
+    $select = $this->select_string($number); 
+    
+    // this should contains all strings separated by NULLs
+    $key = $single.chr(0).$plural;
+    
+    
+    if ($this->enable_cache) {
+      if (! array_key_exists($key, $this->cache_translations)) {
+        return ($number != 1) ? $plural : $single;
+      } else {
+        $result = $this->cache_translations[$key];
+        $list = explode(chr(0), $result);
+        return $list[$select];
+      }
+    } else {
+      $num = $this->find_string($key);
+      if ($num == -1) {
+        return ($number != 1) ? $plural : $single;
+      } else {
+        $result = $this->get_translation_string($num);
+        $list = explode(chr(0), $result);
+        return $list[$select];
+      }
+    }
+  }
+
+}
+
+?>
diff --git a/extlib/php-gettext/streams.php b/extlib/php-gettext/streams.php
new file mode 100644 (file)
index 0000000..3eafa74
--- /dev/null
@@ -0,0 +1,167 @@
+<?php
+/*
+   Copyright (c) 2003, 2005 Danilo Segan <danilo@kvota.net>.
+
+   This file is part of PHP-gettext.
+
+   PHP-gettext is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 2 of the License, or
+   (at your option) any later version.
+
+   PHP-gettext is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with PHP-gettext; if not, write to the Free Software
+   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+*/
+
+
+// Simple class to wrap file streams, string streams, etc.
+// seek is essential, and it should be byte stream
+class StreamReader {
+  // should return a string [FIXME: perhaps return array of bytes?]
+  function read($bytes) {
+    return false;
+  }
+  
+  // should return new position
+  function seekto($position) {
+    return false;
+  }
+  
+  // returns current position
+  function currentpos() {
+    return false;
+  }
+  
+  // returns length of entire stream (limit for seekto()s)
+  function length() {
+    return false;
+  }
+}
+
+class StringReader {
+  var $_pos;
+  var $_str;
+
+  function StringReader($str='') {
+    $this->_str = $str;
+    $this->_pos = 0;
+  }
+
+  function read($bytes) {
+    $data = substr($this->_str, $this->_pos, $bytes);
+    $this->_pos += $bytes;
+    if (strlen($this->_str)<$this->_pos)
+      $this->_pos = strlen($this->_str);
+
+    return $data;
+  }
+
+  function seekto($pos) {
+    $this->_pos = $pos;
+    if (strlen($this->_str)<$this->_pos)
+      $this->_pos = strlen($this->_str);
+    return $this->_pos;
+  }
+
+  function currentpos() {
+    return $this->_pos;
+  }
+
+  function length() {
+    return strlen($this->_str);
+  }
+
+}
+
+
+class FileReader {
+  var $_pos;
+  var $_fd;
+  var $_length;
+
+  function FileReader($filename) {
+    if (file_exists($filename)) {
+
+      $this->_length=filesize($filename);
+      $this->_pos = 0;
+      $this->_fd = fopen($filename,'rb');
+      if (!$this->_fd) {
+       $this->error = 3; // Cannot read file, probably permissions
+       return false;
+      }
+    } else {
+      $this->error = 2; // File doesn't exist
+      return false;
+    }
+  }
+
+  function read($bytes) {
+    if ($bytes) {
+      fseek($this->_fd, $this->_pos);
+
+      // PHP 5.1.1 does not read more than 8192 bytes in one fread()
+      // the discussions at PHP Bugs suggest it's the intended behaviour
+      $data = '';
+      while ($bytes > 0) {
+        $chunk  = fread($this->_fd, $bytes);
+        $data  .= $chunk;
+        $bytes -= strlen($chunk);
+      }
+      $this->_pos = ftell($this->_fd);
+      
+      return $data;
+    } else return '';
+  }
+
+  function seekto($pos) {
+    fseek($this->_fd, $pos);
+    $this->_pos = ftell($this->_fd);
+    return $this->_pos;
+  }
+
+  function currentpos() {
+    return $this->_pos;
+  }
+
+  function length() {
+    return $this->_length;
+  }
+
+  function close() {
+    fclose($this->_fd);
+  }
+
+}
+
+// Preloads entire file in memory first, then creates a StringReader 
+// over it (it assumes knowledge of StringReader internals)
+class CachedFileReader extends StringReader {
+  function CachedFileReader($filename) {
+    if (file_exists($filename)) {
+
+      $length=filesize($filename);
+      $fd = fopen($filename,'rb');
+
+      if (!$fd) {
+       $this->error = 3; // Cannot read file, probably permissions
+       return false;
+      }
+      $this->_str = fread($fd, $length);
+      fclose($fd);
+
+    } else {
+      $this->error = 2; // File doesn't exist
+      return false;
+    }
+  }
+}
+
+
+?>
\ No newline at end of file
index 2e74d38fb0ac29d46e01ab115727e93100680f0e..be62fe1f344561f87893d6e46ea1bf6ff8d5bf1d 100644 (file)
--- a/index.php
+++ b/index.php
@@ -182,12 +182,36 @@ function main()
     // If the site is private, and they're not on one of the "public"
     // parts of the site, redirect to login
 
-    if (!$user && common_config('site', 'private') &&
-        !in_array($action, array('login', 'openidlogin', 'finishopenidlogin',
-                                 'recoverpassword', 'api', 'doc', 'register')) &&
-        !preg_match('/rss$/', $action)) {
-        common_redirect(common_local_url('login'));
-        return;
+    if (!$user && common_config('site', 'private')) {
+        $public_actions = array('openidlogin', 'finishopenidlogin',
+                                'recoverpassword', 'api', 'doc',
+                                'opensearch');
+        $login_action = 'openidlogin';
+        if (!common_config('site', 'openidonly')) {
+            $public_actions[] = 'login';
+            $public_actions[] = 'register';
+            $login_action = 'login';
+        }
+        if (!in_array($action, $public_actions) &&
+            !preg_match('/rss$/', $action)) {
+
+            // set returnto
+            $rargs =& common_copy_args($args);
+            unset($rargs['action']);
+            if (common_config('site', 'fancy')) {
+                unset($rargs['p']);
+            }
+            if (array_key_exists('submit', $rargs)) {
+                unset($rargs['submit']);
+            }
+            foreach (array_keys($_COOKIE) as $cookie) {
+                unset($rargs[$cookie]);
+            }
+            common_set_returnto(common_local_url($action, $rargs));
+
+            common_redirect(common_local_url($login_action));
+            return;
+        }
     }
 
     $action_class = ucfirst($action).'Action';
index 227f99789a7dee034b318a8e4cf247243535c540..c13f70272d7b3d4ce0ea3de9fd3ef7119412b8ea 100644 (file)
@@ -49,8 +49,7 @@ function checkPrereqs()
     }
 
     $reqs = array('gd', 'curl',
-                  'xmlwriter', 'mbstring',
-                  'gettext');
+                  'xmlwriter', 'mbstring');
 
     foreach ($reqs as $req) {
         if (!checkExtension($req)) {
@@ -180,6 +179,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 +221,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 +289,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 +300,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 +325,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 +350,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".
+            ($db['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 +396,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");
@@ -383,9 +416,7 @@ function runDbScript($filename, $conn, $type='mysql')
 
 ?>
 <?php echo"<?"; ?> xml version="1.0" encoding="UTF-8" <?php echo "?>"; ?>
-<!DOCTYPE html
-PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<!DOCTYPE html>
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en_US" lang="en_US">
     <head>
         <title>Install Laconica</title>
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 ef147bef4a79ac241da814b53ec62bd2abb6a738..f09ce838c4bb216ca404508d3b0c35003a29fd86 100644 (file)
  */
 
 $(document).ready(function(){
+       var counterBlackout = false;
+       
        // count character on keyup
        function counter(event){
                var maxLength = 140;
                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;
@@ -57,6 +82,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 +95,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');
@@ -255,10 +288,10 @@ function NoticeReply() {
 function NoticeReplySet(nick,id) {
        rgx_username = /^[0-9a-zA-Z\-_.]*$/;
        if (nick.match(rgx_username)) {
-               replyto = "@" + nick + " ";
                var text = $("#notice_data-text");
                if (text.length) {
-                       text.val(replyto + text.val());
+                       replyto = "@" + nick + " ";
+                       text.val(replyto + text.val().replace(RegExp(replyto, 'i'), ''));
                        $("#form_notice input#notice_in-reply-to").val(id);
                        if (text.get(0).setSelectionRange) {
                                var len = text.val().length;
index 4ab50abcef1134a50282bbdfb64a45a4696fa972..1a21d871ed592c5ea5926ab77b937e2e9ced8411 100644 (file)
@@ -126,6 +126,10 @@ class AccountSettingsNav extends Widget
         $this->action->elementStart('ul', array('class' => 'nav'));
 
         foreach ($menu as $menuaction => $menudesc) {
+            if ($menuaction == 'openidsettings' &&
+                !common_config('openid', 'enabled')) {
+                continue;
+            }
             $this->action->menuItem(common_local_url($menuaction),
                                    $menudesc[0],
                                    $menudesc[1],
index a5244371a5def60d0bf6635336044dba41a36185..1bdc4daea7cd73eef4ef56ca781eb3d802d9947b 100644 (file)
@@ -193,21 +193,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));
             }
 
@@ -253,26 +244,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; }');
@@ -423,6 +402,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'));
@@ -434,12 +421,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'),
@@ -452,17 +436,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 5d6956d9b232c982a5b33856af51449a75e2d038..8d79b266611fa168bd698dfd3e80091fc8c58933 100644 (file)
@@ -47,6 +47,9 @@ require_once('PEAR.php');
 require_once('DB/DataObject.php');
 require_once('DB/DataObject/Cast.php'); # for dates
 
+if (!function_exists('gettext')) {
+    require_once("php-gettext/gettext.inc");
+}
 require_once(INSTALLDIR.'/lib/language.php');
 
 // This gets included before the config file, so that admin code and plugins
@@ -82,7 +85,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,6 +112,7 @@ $config =
               'broughtbyurl' => null,
               'closed' => false,
               'inviteonly' => false,
+              'openidonly' => false,
               'private' => false,
               'ssl' => 'never',
               'sslserver' => null,
@@ -169,6 +173,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,6 +189,12 @@ $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' =>
@@ -364,6 +376,12 @@ 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;
+}
+
 // XXX: how many of these could be auto-loaded on use?
 
 require_once 'Validate.php';
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..289e702c6958d332fd6cd7b10900e692a9e543be 100644 (file)
@@ -95,34 +95,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');
     }
 
     /**
@@ -277,8 +256,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');
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 f23985f3ab3a1293bcb02a8ab98364b355628a81..2fb1828d6b079ef0b4c49e8305cc5f1ecc4f1ef3 100644 (file)
@@ -72,14 +72,18 @@ class LoginGroupNav extends Widget
         // action => array('prompt', 'title')
         $menu = array();
 
-        $menu['login'] = array(_('Login'),
-                         _('Login with a username and password'));
-        if (!(common_config('site','closed') || common_config('site','inviteonly'))) {
-            $menu['register'] = array(_('Register'),
-                                _('Sign up for a new account'));
+        if (!common_config('site','openidonly')) {
+            $menu['login'] = array(_('Login'),
+                             _('Login with a username and password'));
+            if (!(common_config('site','closed') || common_config('site','inviteonly'))) {
+                $menu['register'] = array(_('Register'),
+                                    _('Sign up for a new account'));
+            }
+        }
+        if (common_config('openid', 'enabled')) {
+            $menu['openidlogin'] = array(_('OpenID'),
+                                   _('Login or register with OpenID'));
         }
-        $menu['openidlogin'] = array(_('OpenID'),
-                               _('Login or register with OpenID'));
 
         $action_name = $this->action->trimmed('action');
         $this->action->elementStart('ul', array('class' => 'nav'));
index 16c1b0f30a74ddc326f9cb7617e7b418911aa736..33d1eb7549bf19815f524b40a41ed2c0324a4de2 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);
 }
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',
index 11de991c817d1ecf47b1b9cb8382767bd8206b0c..b66a24be44d53c491e4ccda3c49208ca2719e365 100644 (file)
 <?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');
-
-class OAuthClientCurlException extends Exception { }
+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;
+        $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);
         }
     }
 
-    function getRequestToken()
+    /**
+     * 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(TwitterOAuthClient::$requestTokenURL);
+        $response = $this->oAuthGet($url);
         parse_str($response);
         $token = new OAuthToken($oauth_token, $oauth_token_secret);
         return $token;
     }
 
-    function getAuthorizeLink($request_token, $oauth_callback = null)
+    /**
+     * 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)
     {
-        $url = TwitterOAuthClient::$authorizeURL . '?oauth_token=' .
+        $authorize_url = $url . '?oauth_token=' .
             $request_token->key;
 
         if (isset($oauth_callback)) {
-            $url .= '&oauth_callback=' . urlencode($oauth_callback);
+            $authorize_url .= '&oauth_callback=' . urlencode($oauth_callback);
         }
 
-        return $url;
+        return $authorize_url;
     }
 
-    function getAccessToken()
+    /**
+     * 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(TwitterOAuthClient::$accessTokenURL);
+        $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,
@@ -59,6 +157,14 @@ class OAuthClient
         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,
@@ -70,6 +176,14 @@ class OAuthClient
             $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(
@@ -89,7 +203,7 @@ class OAuthClient
         );
 
         if (isset($params)) {
-            $options[CURLOPT_POST] = true;
+            $options[CURLOPT_POST]       = true;
             $options[CURLOPT_POSTFIELDS] = $params;
         }
 
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 6651773c0d8e3c93355027e8ead89e1d8461a783..04c6dd41401dd0335179378dd457039120658802 100644 (file)
@@ -117,15 +117,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
 
@@ -413,6 +406,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 2369ac2678362a91ff6fab6098eeee5625e62ec5..11c1374283c2d820173302748cee1a7db725d9df 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;
+if (!defined('LACONICA')) {
+    exit(1);
 }
 
-function twitter_user_info($screen_name, $password)
-{
-    $uri = "http://twitter.com/users/show/$screen_name.json";
-    return twitter_json_data($uri, $screen_name, $password);
-}
-
-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;
         }
     }
 
@@ -361,7 +151,7 @@ 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)) {
 
@@ -370,62 +160,63 @@ function broadcast_twitter($notice)
         // XXX: Hack to get around PHP cURL's use of @ being a a meta character
         $statustxt = preg_replace('/^@/', ' @', $notice->content);
 
-        $client = new TwitterOAuthClient($flink->token, $flink->credentials);
+        $token = TwitterOAuthClient::unpackToken($flink->credentials);
+
+        $client = new TwitterOAuthClient($token->key, $token->secret);
 
         $status = null;
 
         try {
-            $status = $client->statuses_update($statustxt);
+            $status = $client->statusesUpdate($statustxt);
         } catch (OAuthClientCurlException $e) {
 
-        if ($e->getMessage() == 'The requested URL returned error: 401') {
+            if ($e->getMessage() == 'The requested URL returned error: 401') {
 
-            $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);
+                $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);
 
-            // Bad auth token! We need to delete the foreign_link
-            // to Twitter and inform the user.
+                // Bad auth token! We need to delete the foreign_link
+                // to Twitter and inform the user.
 
-            remove_twitter_link($flink);
-            return true;
+                remove_twitter_link($flink);
+                return true;
 
-        } else {
+            } else {
 
-            // Some other error happened, so we should probably
-            // try to send again later.
+                // Some other error happened, so we should probably
+                // try to send again later.
 
-            $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);
+                $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;
+                return false;
+            }
         }
-    }
-
-    if (empty($status)) {
 
-        // This could represent a failure posting,
-        // or the Twitter API might just be behaving flakey.
+        if (empty($status)) {
 
-        $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);
+            // This could represent a failure posting,
+            // or the Twitter API might just be behaving flakey.
 
-        return false;
-    }
+            $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);
 
-    // Notice crossed the great divide
+            return false;
+        }
 
-    $msg = sprintf('Twitter bridge posted notice %s to Twitter.',
-               $notice->id);
-    common_log(LOG_INFO, $msg);
+        // Notice crossed the great divide
 
+        $msg = sprintf('Twitter bridge posted notice %s to Twitter.',
+                       $notice->id);
+        common_log(LOG_INFO, $msg);
     }
 
     return true;
@@ -442,7 +233,7 @@ 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__);
     }
 
@@ -450,17 +241,77 @@ function remove_twitter_link($flink)
 
     if (isset($user->email)) {
 
-    $result = mail_twitter_bridge_removed($user);
+        $result = mail_twitter_bridge_removed($user);
+
+        if (!$result) {
+
+            $msg = 'Unable to send email to notify ' .
+              "$user->nickname (user id: $user->id) " .
+              'that their Twitter bridge link was ' .
+              'removed!';
+
+            common_log(LOG_WARNING, $msg);
+        }
+    }
+
+}
+
+        $result = mail_twitter_bridge_removed($user);
+
+        if (!$result) {
+
+            $msg = 'Unable to send email to notify ' .
+              "$user->nickname (user id: $user->id) " .
+              'that their Twitter bridge link was ' .
+              'removed!';
+
+            common_log(LOG_WARNING, $msg);
+        }
+    }
+
+}
+
+        $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);
+        }
     }
+
+}
+
+        $result = mail_twitter_bridge_removed($user);
+
+        if (!$result) {
+
+            $msg = 'Unable to send email to notify ' .
+              "$user->nickname (user id: $user->id) " .
+              'that their Twitter bridge link was ' .
+              'removed!';
+
+            common_log(LOG_WARNING, $msg);
+        }
+    }
+
+}
+
+        $result = mail_twitter_bridge_removed($user);
+
+        if (!$result) {
+
+            $msg = 'Unable to send email to notify ' .
+              "$user->nickname (user id: $user->id) " .
+              'that their Twitter bridge link was ' .
+              'removed!';
+
+            common_log(LOG_WARNING, $msg);
+        }
     }
 
 }
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);
index c5f114fb025f35842cf4d88718c420d1a8a1aaa3..b7dc4a80c3b81098a608ddc97f518a7a62944ebe 100644 (file)
@@ -1,11 +1,60 @@
 <?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');
@@ -15,39 +64,89 @@ class TwitterOAuthClient extends OAuthClient
                             $oauth_token, $oauth_token_secret);
     }
 
-    function getAuthorizeLink($request_token) {
-        return parent::getAuthorizeLink($request_token,
-                                        common_local_url('twitterauthorization'));
+    // 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));
     }
 
-    function verify_credentials()
+    static function unpackToken($str)
     {
-        $url = 'https://twitter.com/account/verify_credentials.json';
-        $response = $this->oAuthGet($url);
+        $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;
     }
 
-    function statuses_update($status, $in_reply_to_status_id = null)
+    /**
+     * 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,
+        $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);
+        $status   = json_decode($response);
         return $status;
     }
 
-    function statuses_friends_timeline($since_id = null, $max_id = null,
-                                       $cnt = null, $page = null) {
+    /**
+     * 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';
+        $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);
+        $qry    = http_build_query($params);
 
         if (!empty($qry)) {
             $url .= "?$qry";
@@ -58,4 +157,64 @@ class TwitterOAuthClient extends OAuthClient
         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..a10ca339a783014d4403b5605ef0ac3dc30cc113 100644 (file)
@@ -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 c8e318efec6e4491157e16d6444ab46de7db84f7..2be4213e794f74cae3457c994a7360eeeabfa305 100644 (file)
@@ -412,87 +412,62 @@ 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):'.
+            '(?:'. //Known protocols
+                '(?:'.
+                    '(?:https?|ftps?|mms|rtsp|gopher|news|nntp|telnet|wais|file|prospero|webcal|irc)://'.
+                    '|'.
+                    '(?:mailto|aim|tel|xmpp):'.
+                ')[^\s\/]+'.
+            ')'.
+            '|(?:(?: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
+            '|(?:'. //IPv6
+                '(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}(?:(?:[0-9A-Fa-f]{1,4})|:))|(?:(?:[0-9A-Fa-f]{1,4}:){6}(?::|(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})|(?::[0-9A-Fa-f]{1,4})))|(?:(?:[0-9A-Fa-f]{1,4}:){5}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:[0-9A-Fa-f]{1,4}:){4}(?::[0-9A-Fa-f]{1,4}){0,1}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:[0-9A-Fa-f]{1,4}:){3}(?::[0-9A-Fa-f]{1,4}){0,2}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:[0-9A-Fa-f]{1,4}:){2}(?::[0-9A-Fa-f]{1,4}){0,3}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:[0-9A-Fa-f]{1,4}:)(?::[0-9A-Fa-f]{1,4}){0,4}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?::(?::[0-9A-Fa-f]{1,4}){0,5}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})))'.
+            ')|(?:'. //DNS
+                '\S+\.(?:museum|travel|onion|local|[a-z]{2,4})'.
+            ')'.
+        ')'.
+        '(?:'.
+            '$|(?:'.
+                '/[^\s\(\)\[\]\{\}]*'.
+            ')'.
         ')'.
-        '[^.\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);
-
-        // Call user specified func
-        if (empty($notice_id)) {
-            $modified_url = call_user_func($callback, $url);
-        } else {
-            $modified_url = call_user_func($callback, array($url, $notice_id));
-        }
-
-        // Replace it!
-        $start = mb_strpos($text, $url, $offset);
-        $text = mb_substr($text, 0, $start).$modified_url.mb_substr($text, $start + mb_strlen($url), mb_strlen($text));
-        $offset = $start + mb_strlen($modified_url);
-    }
-
-    return $text;
+    return preg_replace_callback($regex, curry(callback_helper,$callback,$notice_id) ,$text);
+}
+
+function callback_helper($matches, $callback, $notice_id) {
+    $pos = strpos($matches[0],$matches[1]);
+    $left = substr($matches[0],0,$pos);
+    $right = substr($matches[0],$pos+strlen($matches[1]));
+    
+    if(empty($notice_id)){
+        $result = call_user_func_array($callback,$matches[1]);
+    }else{
+        $result = call_user_func_array($callback, array($matches[1],$notice_id) );
+    }
+    return $left . $result . $right;
+}
+
+function curry($fn) {
+    //TODO switch to a PHP 5.3 function closure based approach if PHP 5.3 is used
+    $args = func_get_args();
+    array_shift($args);
+    $id = uniqid('_partial');
+    $GLOBALS[$id] = array($fn, $args);
+    return create_function(
+        '',
+        '
+        $args = func_get_args();
+        return call_user_func_array(
+        $GLOBALS["'.$id.'"][0],
+        array_merge(
+            $args,
+            $GLOBALS["'.$id.'"][1]));
+    ');
 }
 
 function common_linkify($url) {
@@ -500,6 +475,11 @@ function common_linkify($url) {
     // functions
     $url = htmlspecialchars_decode($url);
 
+   if(strpos($url, '@')!==false && strpos($url, ':')===false){
+       //url is an email address without the mailto: protocol
+       return XMLStringer::estring('a', array('href' => "mailto:$url", 'rel' => 'external'), $url);
+   }
+
     $canon = File_redirection::_canonUrl($url);
 
     $longurl_data = File_redirection::where($url);
@@ -884,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';
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 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..91c257adb451fd468af1a5da305beee297580c27 100755 (executable)
@@ -385,5 +385,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 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 10aef9ca3e216dd471d146c70b8ee791889b5003..082bcc9622a5b929e85bc947a21d547ff08afa45 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,139 +87,26 @@ 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) {
-
-                $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
-
-                    // Each child ps needs its own DB connection
-
-                    // Note: DataObject::getDatabaseConnection() creates
-                    // a new connection if there isn't one already
-
-                    global $_DB_DATAOBJECT;
-                    $conn = &$f->getDatabaseConnection();
-
-                    $this->getTimeline($f);
-
-                    $conn->disconnect();
-
-                    // XXX: Couldn't find a less brutal way to blow
-                    // away a cached connection
-
-                    unset($_DB_DATAOBJECT['CONNECTIONS']);
-
-                    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()
-    {
+=======
         global $_DB_DATAOBJECT;
 
+>>>>>>> 0.8.x:scripts/twitterstatusfetcher.php
         $flink = new Foreign_link();
         $conn = &$flink->getDatabaseConnection();
 
         $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();
 
@@ -234,73 +127,75 @@ class TwitterStatusFetcher extends Daemon
         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");
+            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 ' .
-                $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.
 
-        $client = new TwitterOAuthClient($flink->token, $flink->credentials);
+        $token = TwitterOAuthClient::unpackToken($flink->credentials);
+
+        $client = new TwitterOAuthClient($token->key, $token->secret);
 
         $timeline = null;
 
         try {
-            $timeline = $client->statuses_friends_timeline();
+            $timeline = $client->statusesFriendsTimeline();
         } catch (OAuthClientCurlException $e) {
-            common_log(LOG_WARNING,
-                       'OAuth client unable to get friends timeline for user ' .
+            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;
             }
 
@@ -308,6 +203,7 @@ class TwitterStatusFetcher extends Daemon
         }
 
         // Okay, record the time we synced with Twitter for posterity
+
         $flink->last_noticesync = common_sql_now();
         $flink->update();
     }
@@ -319,8 +215,8 @@ class TwitterStatusFetcher extends Daemon
         $profile = Profile::staticGet($id);
 
         if (empty($profile)) {
-            common_log(LOG_ERR,
-                'Problem saving notice. No associated Profile.');
+            common_log(LOG_ERR, $this->name() .
+                ' - Problem saving notice. No associated Profile.');
             return null;
         }
 
@@ -344,7 +240,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))) {
@@ -370,24 +266,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 (!empty($profile)) {
-            if (defined('SCRIPT_DEBUG')) {
-                common_debug("Profile for $profile->nickname found.");
-            }
+            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");
@@ -409,9 +303,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();
 
@@ -448,23 +343,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);
         }
@@ -544,23 +434,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;
         }
 
@@ -568,6 +455,7 @@ class TwitterStatusFetcher extends Daemon
         $avatar = $profile->getAvatar($sizes[$size]);
 
         // Delete the avatar, if present
+
         if ($avatar) {
             $avatar->delete();
         }
@@ -605,9 +493,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();
 
@@ -618,9 +504,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;
     }
@@ -633,13 +518,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);
@@ -656,7 +540,8 @@ class TwitterStatusFetcher extends Daemon
     }
 }
 
-declare(ticks = 1);
+$id    = null;
+$debug = null;
 
 if (have_option('i')) {
     $id = get_option_value('i');
@@ -669,9 +554,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();
 
diff --git a/tests/URLDetectionTest.php b/tests/URLDetectionTest.php
new file mode 100644 (file)
index 0000000..f35b03e
--- /dev/null
@@ -0,0 +1,189 @@
+<?php
+
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+    print "This script must be run from the command line\n";
+    exit();
+}
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+define('LACONICA', true);
+
+require_once INSTALLDIR . '/lib/common.php';
+
+class URLDetectionTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * @dataProvider provider
+     *
+     */
+    public function testProduction($content, $expected)
+    {
+        $rendered = common_render_text($content);
+        $this->assertEquals($expected, $rendered);
+    }
+
+    static public function provider()
+    {
+        return array(
+                     array('example',
+                           'example'),
+                     array('http://example',
+                           'http://example'),
+                     array('http://example/',
+                           'http://example/'),
+                     array('http://example/path',
+                           'http://example/path'),
+                     array('http://example.com',
+                           '<a href="http://example.com" class="extlink">http://example.com</a>'),
+                     array('https://example.com',
+                           '<a href="https://example.com" class="extlink">https://example.com</a>'),
+                     array('ftp://example.com',
+                           '<a href="ftp://example.com" class="extlink">ftp://example.com</a>'),
+                     array('ftps://example.com',
+                           '<a href="ftps://example.com" class="extlink">ftps://example.com</a>'),
+                     array('http://user@example.com',
+                           '<a href="http://user@example.com" class="extlink">http://user@example.com</a>'),
+                     array('http://user:pass@example.com',
+                           '<a href="http://user:pass@example.com" class="extlink">http://user:pass@example.com</a>'),
+                     array('http://example.com:8080',
+                           '<a href="http://example.com:8080" class="extlink">http://example.com:8080</a>'),
+                     array('http://www.example.com',
+                           '<a href="http://www.example.com" class="extlink">http://www.example.com</a>'),
+                     array('http://example.com/',
+                           '<a href="http://example.com/" class="extlink">http://example.com/</a>'),
+                     array('http://example.com/path',
+                           '<a href="http://example.com/path" class="extlink">http://example.com/path</a>'),
+                     array('http://example.com/path.html',
+                           '<a href="http://example.com/path.html" class="extlink">http://example.com/path.html</a>'),
+                     array('http://example.com/path.html#fragment',
+                           '<a href="http://example.com/path.html#fragment" class="extlink">http://example.com/path.html#fragment</a>'),
+                     array('http://example.com/path.php?foo=bar&bar=foo',
+                           '<a href="http://example.com/path.php?foo=bar&bar=foo" class="extlink">http://example.com/path.php?foo=bar&bar=foo</a>'),
+                     array('http://müllärör.de',
+                           '<a href="http://müllärör.de" class="extlink">http://müllärör.de</a>'),
+                     array('http://ﺱﺲﺷ.com',
+                           '<a href="http://ﺱﺲﺷ.com" class="extlink">http://ﺱﺲﺷ.com</a>'),
+                     array('http://сделаткартинки.com',
+                           '<a href="http://сделаткартинки.com" class="extlink">http://сделаткартинки.com</a>'),
+                     array('http://tūdaliņ.lv',
+                           '<a href="http://tūdaliņ.lv" class="extlink">http://tūdaliņ.lv</a>'),
+                     array('http://brændendekærlighed.com',
+                           '<a href="http://brændendekærlighed.com" class="extlink">http://brændendekærlighed.com</a>'),
+                     array('http://あーるいん.com',
+                           '<a href="http://あーるいん.com" class="extlink">http://あーるいん.com</a>'),
+                     array('http://예비교사.com',
+                           '<a href="http://예비교사.com" class="extlink">http://예비교사.com</a>'),
+                     array('http://example.com.',
+                           '<a href="http://example.com" class="extlink">http://example.com</a>.'),
+                     array('http://example.com?',
+                           '<a href="http://example.com" class="extlink">http://example.com</a>?'),
+                     array('http://example.com!',
+                           '<a href="http://example.com" class="extlink">http://example.com</a>!'),
+                     array('http://example.com,',
+                           '<a href="http://example.com" class="extlink">http://example.com</a>,'),
+                     array('http://example.com;',
+                           '<a href="http://example.com" class="extlink">http://example.com</a>;'),
+                     array('http://example.com:',
+                           '<a href="http://example.com" class="extlink">http://example.com</a>:'),
+                     array('\'http://example.com\'',
+                           '\'<a href="http://example.com" class="extlink">http://example.com</a>\''),
+                     array('"http://example.com"',
+                           '"<a href="http://example.com" class="extlink">http://example.com</a>"'),
+                     array('http://example.com\r',
+                           '<a href="http://example.com" class="extlink">http://example.com</a>'),
+                     array('(http://example.com)',
+                           '(<a href="http://example.com" class="extlink">http://example.com</a>)'),
+                     array('[http://example.com]',
+                           '[<a href="http://example.com" class="extlink">http://example.com</a>]'),
+                     array('<http://example.com>',
+                           '<<a href="http://example.com" class="extlink">http://example.com</a>>'),
+                     array('http://example.com/path/(foo)/bar',
+                           '<a href="http://example.com/path/(foo)/bar" class="extlink">http://example.com/path/(foo)/bar</a>'),
+                     array('http://example.com/path/[foo]/bar',
+                           '<a href="http://example.com/path/[foo]/bar" class="extlink">http://example.com/path/[foo]/bar</a>'),
+                     array('http://example.com/path/foo/(bar)',
+                           '<a href="http://example.com/path/foo/(bar)" class="extlink">http://example.com/path/foo/(bar)</a>'),
+                     array('http://example.com/path/foo/[bar]',
+                           '<a href="http://example.com/path/foo/[bar]" class="extlink">http://example.com/path/foo/[bar]</a>'),
+                     array('Hey, check out my cool site http://example.com okay?',
+                           'Hey, check out my cool site <a href="http://example.com" class="extlink">http://example.com</a> okay?'),
+                     array('What about parens (e.g. http://example.com/path/foo/(bar))?',
+                           'What about parens (e.g. <a href="http://example.com/path/foo/(bar)" class="extlink">http://example.com/path/foo/(bar)</a>)?'),
+                     array('What about parens (e.g. http://example.com/path/foo/(bar)?',
+                           'What about parens (e.g. <a href="http://example.com/path/foo/(bar)" class="extlink">http://example.com/path/foo/(bar)</a>?'),
+                     array('What about parens (e.g. http://example.com/path/foo/(bar).)?',
+                           'What about parens (e.g. <a href="http://example.com/path/foo/(bar)" class="extlink">http://example.com/path/foo/(bar)</a>.)?'),
+                     array('What about parens (e.g. http://example.com/path/(foo,bar)?',
+                           'What about parens (e.g. <a href="http://example.com/path/(foo,bar)" class="extlink">http://example.com/path/(foo,bar)</a>?'),
+                     array('Unbalanced too (e.g. http://example.com/path/((((foo)/bar)?',
+                           'Unbalanced too (e.g. <a href="http://example.com/path/((((foo)/bar)" class="extlink">http://example.com/path/((((foo)/bar)</a>?'),
+                     array('Unbalanced too (e.g. http://example.com/path/(foo))))/bar)?',
+                           'Unbalanced too (e.g. <a href="http://example.com/path/(foo))))/bar" class="extlink">http://example.com/path/(foo))))/bar</a>)?'),
+                     array('Unbalanced too (e.g. http://example.com/path/foo/((((bar)?',
+                           'Unbalanced too (e.g. <a href="http://example.com/path/foo/((((bar)" class="extlink">http://example.com/path/foo/((((bar)</a>?'),
+                     array('Unbalanced too (e.g. http://example.com/path/foo/(bar))))?',
+                           'Unbalanced too (e.g. <a href="http://example.com/path/foo/(bar)" class="extlink">http://example.com/path/foo/(bar)</a>)))?'),
+                     array('example.com',
+                           '<a href="http://example.com" class="extlink">example.com</a>'),
+                     array('example.org',
+                           '<a href="http://example.org" class="extlink">example.org</a>'),
+                     array('example.co.uk',
+                           '<a href="http://example.co.uk" class="extlink">example.co.uk</a>'),
+                     array('www.example.co.uk',
+                           '<a href="http://www.example.co.uk" class="extlink">www.example.co.uk</a>'),
+                     array('farm1.images.example.co.uk',
+                           '<a href="http://farm1.images.example.co.uk" class="extlink">farm1.images.example.co.uk</a>'),
+                     array('example.museum',
+                           '<a href="http://example.museum" class="extlink">example.museum</a>'),
+                     array('example.travel',
+                           '<a href="http://example.travel" class="extlink">example.travel</a>'),
+                     array('example.com.',
+                           '<a href="http://example.com" class="extlink">example.com</a>.'),
+                     array('example.com?',
+                           '<a href="http://example.com" class="extlink">example.com</a>?'),
+                     array('example.com!',
+                           '<a href="http://example.com" class="extlink">example.com</a>!'),
+                     array('example.com,',
+                           '<a href="http://example.com" class="extlink">example.com</a>,'),
+                     array('example.com;',
+                           '<a href="http://example.com" class="extlink">example.com</a>;'),
+                     array('example.com:',
+                           '<a href="http://example.com" class="extlink">example.com</a>:'),
+                     array('\'example.com\'',
+                           '\'<a href="http://example.com" class="extlink">example.com</a>\''),
+                     array('"example.com"',
+                           '"<a href="http://example.com" class="extlink">example.com</a>"'),
+                     array('example.com\r',
+                           '<a href="http://example.com" class="extlink">example.com</a>'),
+                     array('(example.com)',
+                           '(<a href="http://example.com" class="extlink">example.com</a>)'),
+                     array('[example.com]',
+                           '[<a href="http://example.com" class="extlink">example.com</a>]'),
+                     array('<example.com>',
+                           '<<a href="http://example.com" class="extlink">example.com</a>>'),
+                     array('Hey, check out my cool site example.com okay?',
+                           'Hey, check out my cool site <a href="http://example.com" class="extlink">example.com</a> okay?'),
+                     array('Hey, check out my cool site example.com.I made it.',
+                           'Hey, check out my cool site <a href="http://example.com" class="extlink">example.com</a>.I made it.'),
+                     array('Hey, check out my cool site example.com.Funny thing...',
+                           'Hey, check out my cool site <a href="http://example.com" class="extlink">example.com</a>.Funny thing...'),
+                     array('Hey, check out my cool site example.com.You will love it.',
+                           'Hey, check out my cool site <a href="http://example.com" class="extlink">example.com</a>.You will love it.'),
+                     array('What about parens (e.g. example.com/path/foo/(bar))?',
+                           'What about parens (e.g. <a href="http://example.com/path/foo/(bar)" class="extlink">example.com/path/foo/(bar)</a>)?'),
+                     array('What about parens (e.g. example.com/path/foo/(bar)?',
+                           'What about parens (e.g. <a href="http://example.com/path/foo/(bar)" class="extlink">example.com/path/foo/(bar)</a>?'),
+                     array('What about parens (e.g. example.com/path/foo/(bar).)?',
+                           'What about parens (e.g. <a href="http://example.com/path/foo/(bar)" class="extlink">example.com/path/foo/(bar)</a>.)?'),
+                     array('What about parens (e.g. example.com/path/(foo,bar)?',
+                           'What about parens (e.g. <a href="http://example.com/path/(foo,bar)" class="extlink">example.com/path/(foo,bar)</a>?'),
+                     array('file.ext',
+                           'file.ext'),
+                     array('file.html',
+                           'file.html'),
+                     array('file.php',
+                           'file.php')
+                     );
+    }
+}
+
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 921a6b27b4c08e835185c6c0190e7b57096f7acd..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%;
 }
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 8af5644b6726cd8b43185ce7c305e44a1fb8025e..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,
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>