]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch '0.7.x' into 0.8.x
authorSarven Capadisli <csarven@controlyourself.ca>
Fri, 1 May 2009 00:16:25 +0000 (00:16 +0000)
committerSarven Capadisli <csarven@controlyourself.ca>
Fri, 1 May 2009 00:16:25 +0000 (00:16 +0000)
Resolved Conflicts:
lib/queuehandler.php
lib/util.php
theme/base/css/display.css

36 files changed:
actions/accesstoken.php
actions/all.php
actions/api.php
actions/newmessage.php
actions/twitapifriendships.php
actions/twitapistatuses.php
actions/twitapiusers.php
classes/Notice.php
classes/Notice_inbox.php
classes/User.php
db/notice_source.sql
js/util.js
lib/common.php
lib/galleryaction.php
lib/router.php
lib/subpeopletagcloudsection.php
lib/subscriberspeopleselftagcloudsection.php
lib/subscriberspeopletagcloudsection.php
lib/subscriptionspeopleselftagcloudsection.php
lib/subscriptionspeopletagcloudsection.php
lib/twitterapi.php
lib/util.php
plugins/Comet/CometPlugin.php [new file with mode: 0644]
plugins/Comet/README [new file with mode: 0644]
plugins/Comet/bayeux.class.inc.php [new file with mode: 0644]
plugins/Comet/jquery.comet.js [new file with mode: 0644]
plugins/Comet/json2.js [new file with mode: 0644]
plugins/Comet/updatetimeline.js [new file with mode: 0644]
plugins/LinkbackPlugin.php
scripts/getvaliddaemons.php [new file with mode: 0755]
scripts/inboxqueuehandler.php [new file with mode: 0755]
scripts/memcachedqueuehandler.php [new file with mode: 0755]
scripts/startdaemons.sh
scripts/stopdaemons.sh
scripts/xmppdaemon.php
theme/base/css/display.css

index bb68d3314d49efea773f0cc6f304d308ec2636a2..46b43c70216e106a5722434c38f365d56c0d3cc5 100644 (file)
@@ -59,7 +59,7 @@ class AccesstokenAction extends Action
         try {
             common_debug('getting request from env variables', __FILE__);
             common_remove_magic_from_request();
-            $req = OAuthRequest::from_request('POST', common_locale_url('accesstoken'));
+            $req = OAuthRequest::from_request('POST', common_local_url('accesstoken'));
             common_debug('getting a server', __FILE__);
             $server = omb_oauth_server();
             common_debug('fetching the access token', __FILE__);
index a92e554623136dbdbea88f11788d1ad6ea3b2af6..a53bbea07b4c0f541e91fcab6a4e761651d6b02c 100644 (file)
@@ -93,7 +93,7 @@ class AllAction extends ProfileAction
         if (common_logged_in()) {
             $current_user = common_current_user();
             if ($this->user->id === $current_user->id) {
-                $message .= _('Try subscribing to more people, [join a group](%%action.groups) or post something yourself.');
+                $message .= _('Try subscribing to more people, [join a group](%%action.groups%%) or post something yourself.');
             } else {
                 $message .= sprintf(_('You can try to [nudge %s](../%s) from his profile or [post something to his or her attention](%%%%action.newnotice%%%%?status_textarea=%s).'), $this->user->nickname, $this->user->nickname, '@' . $this->user->nickname);
             }
index d2f0a2eff0f7a47c1038939970d1fe0fd9a2cd32..8762b4bcd3efc974831252987f72b8bb33bd4041 100644 (file)
@@ -130,6 +130,7 @@ class ApiAction extends Action
                                  'statuses/friends_timeline',
                                  'statuses/friends',
                                  'statuses/replies',
+                                 'statuses/mentions',
                                  'statuses/followers',
                                  'favorites/favorites');
 
index 82276ff341c6e6d8ea54d033fe2588aba48e21c3..52d4899ba2d453d8012465eaa60def69458df4a2 100644 (file)
@@ -172,15 +172,54 @@ class NewmessageAction extends Action
 
         $this->notify($user, $this->other, $message);
 
-        $url = common_local_url('outbox', array('nickname' => $user->nickname));
+        if ($this->boolean('ajax')) {
+            $this->startHTML('text/xml;charset=utf-8');
+            $this->elementStart('head');
+            $this->element('title', null, _('Message sent'));
+            $this->elementEnd('head');
+            $this->elementStart('body');
+            $this->element('p', array('id' => 'command_result'),
+                sprintf(_('Direct message to %s sent'),
+                    $this->other->nickname));
+            $this->elementEnd('body');
+            $this->elementEnd('html');
+        } else {
+            $url = common_local_url('outbox',
+                array('nickname' => $user->nickname));
+            common_redirect($url, 303);
+        }
+    }
 
-        common_redirect($url, 303);
+    /**
+     * Show an Ajax-y error message
+     *
+     * Goes back to the browser, where it's shown in a popup.
+     *
+     * @param string $msg Message to show
+     *
+     * @return void
+     */
+
+    function ajaxErrorMsg($msg)
+    {
+        $this->startHTML('text/xml;charset=utf-8', true);
+        $this->elementStart('head');
+        $this->element('title', null, _('Ajax Error'));
+        $this->elementEnd('head');
+        $this->elementStart('body');
+        $this->element('p', array('id' => 'error'), $msg);
+        $this->elementEnd('body');
+        $this->elementEnd('html');
     }
 
     function showForm($msg = null)
     {
-        $this->msg = $msg;
+        if ($msg && $this->boolean('ajax')) {
+            $this->ajaxErrorMsg($msg);
+            return;
+        }
 
+        $this->msg = $msg;
         $this->showPage();
     }
 
index c50c5e84a9dcc948682e80d55c0a26641a726dd6..2f8250e0dc9dfa56b55893b436a15838c8a9b39a 100644 (file)
@@ -133,11 +133,7 @@ class TwitapifriendshipsAction extends TwitterapiAction
             return;
         }
 
-        if ($user_a->isSubscribed($user_b)) {
-            $result = 'true';
-        } else {
-            $result = 'false';
-        }
+        $result = $user_a->isSubscribed($user_b);
 
         switch ($apidata['content-type']) {
          case 'xml':
index 323c4f1f8824c0d197d5ec6b1b788f25efc35515..3abeba36727d2f8ae4ee6708f8d3aab95d00a64d 100644 (file)
@@ -144,10 +144,10 @@ class TwitapistatusesAction extends TwitterapiAction
             break;
          case 'atom':
             if (isset($apidata['api_arg'])) {
-                $selfuri = $selfuri = common_root_url() .
+                $selfuri = common_root_url() .
                     'api/statuses/friends_timeline/' . $apidata['api_arg'] . '.atom';
             } else {
-                $selfuri = $selfuri = common_root_url() .
+                $selfuri = common_root_url() .
                     'api/statuses/friends_timeline.atom';
             }
             $this->show_atom_timeline($notice, $title, $id, $link, $subtitle, null, $selfuri);
@@ -231,10 +231,10 @@ class TwitapistatusesAction extends TwitterapiAction
             break;
          case 'atom':
             if (isset($apidata['api_arg'])) {
-                $selfuri = $selfuri = common_root_url() .
+                $selfuri = common_root_url() .
                     'api/statuses/user_timeline/' . $apidata['api_arg'] . '.atom';
             } else {
-                $selfuri = $selfuri = common_root_url() .
+                $selfuri = common_root_url() .
                  'api/statuses/user_timeline.atom';
             }
             $this->show_atom_timeline($notice, $title, $id, $link, $subtitle, $suplink, $selfuri);
@@ -344,7 +344,7 @@ class TwitapistatusesAction extends TwitterapiAction
         $this->show($args, $apidata);
     }
 
-    function replies($args, $apidata)
+    function mentions($args, $apidata)
     {
 
         parent::handle($args);
@@ -360,11 +360,13 @@ class TwitapistatusesAction extends TwitterapiAction
         $profile = $user->getProfile();
 
         $sitename = common_config('site', 'name');
-        $title = sprintf(_('%1$s / Updates replying to %2$s'), $sitename, $user->nickname);
+        $title = sprintf(_('%1$s / Updates mentioning %2$s'),
+            $sitename, $user->nickname);
         $taguribase = common_config('integration', 'taguri');
-        $id = "tag:$taguribase:Replies:".$user->id;
+        $id = "tag:$taguribase:Mentions:".$user->id;
         $link = common_local_url('replies', array('nickname' => $user->nickname));
-        $subtitle = sprintf(_('%1$s updates that reply to updates from %2$s / %3$s.'), $sitename, $user->nickname, $profile->getBestName());
+        $subtitle = sprintf(_('%1$s updates that reply to updates from %2$s / %3$s.'),
+            $sitename, $user->nickname, $profile->getBestName());
 
         if (!$page) {
             $page = 1;
@@ -385,7 +387,8 @@ class TwitapistatusesAction extends TwitterapiAction
 
         $since = strtotime($this->arg('since'));
 
-        $notice = $user->getReplies((($page-1)*20), $count, $since_id, $before_id, $since);
+        $notice = $user->getReplies((($page-1)*20),
+            $count, $since_id, $before_id, $since);
         $notices = array();
 
         while ($notice->fetch()) {
@@ -400,14 +403,10 @@ class TwitapistatusesAction extends TwitterapiAction
             $this->show_rss_timeline($notices, $title, $link, $subtitle);
             break;
          case 'atom':
-             if (isset($apidata['api_arg'])) {
-                 $selfuri = $selfuri = common_root_url() .
-                     'api/statuses/replies/' . $apidata['api_arg'] . '.atom';
-             } else {
-                 $selfuri = $selfuri = common_root_url() .
-                  'api/statuses/replies.atom';
-             }
-            $this->show_atom_timeline($notices, $title, $id, $link, $subtitle, null, $selfuri);
+            $selfuri = common_root_url() .
+                ltrim($_SERVER['QUERY_STRING'], 'p=');
+            $this->show_atom_timeline($notices, $title, $id, $link, $subtitle,
+                null, $selfuri);
             break;
          case 'json':
             $this->show_json_timeline($notices);
@@ -418,6 +417,11 @@ class TwitapistatusesAction extends TwitterapiAction
 
     }
 
+    function replies($args, $apidata)
+    {
+        call_user_func(array($this, 'mentions'), $args, $apidata);
+    }
+
     function show($args, $apidata)
     {
         parent::handle($args);
index 2894b7486dd31372fe63ca76ca41ed99821922b4..1542cfb33e3d064130093272e014ac4fa6262d81 100644 (file)
@@ -82,8 +82,8 @@ class TwitapiusersAction extends TwitterapiAction
                $twitter_user['profile_text_color'] = '';
                $twitter_user['profile_link_color'] = '';
                $twitter_user['profile_sidebar_fill_color'] = '';
-        $twitter_user['profile_sidebar_border_color'] = '';
-        $twitter_user['profile_background_tile'] = 'false';
+               $twitter_user['profile_sidebar_border_color'] = '';
+               $twitter_user['profile_background_tile'] = false;
 
                $faves = DB_DataObject::factory('fave');
                $faves->user_id = $user->id;
@@ -103,24 +103,16 @@ class TwitapiusersAction extends TwitterapiAction
 
                if (isset($apidata['user'])) {
 
-                       if ($apidata['user']->isSubscribed($profile)) {
-                               $twitter_user['following'] = 'true';
-                       } else {
-                               $twitter_user['following'] = 'false';
-                       }
+                       $twitter_user['following'] = $apidata['user']->isSubscribed($profile);
             
-            // Notifications on?
-                   $sub = Subscription::pkeyGet(array('subscriber' =>
-                       $apidata['user']->id, 'subscribed' => $profile->id));
+                       // Notifications on?
+                       $sub = Subscription::pkeyGet(array('subscriber' =>
+                               $apidata['user']->id, 'subscribed' => $profile->id));
             
-            if ($sub) {
-                if ($sub->jabber || $sub->sms) {
-                    $twitter_user['notifications'] = 'true';
-                } else {
-                    $twitter_user['notifications'] = 'false';
-                }
-            }
-        }
+                       if ($sub) {
+                               $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
+                       }
+               }
         
                if ($apidata['content-type'] == 'xml') {
                        $this->init_document('xml');
index a399d97e0cd0f47783c65d0363bfb7d51ca84bc1..19ae8313cdd731ec8c9b9b03bea2e9b8de47d53b 100644 (file)
@@ -206,7 +206,12 @@ class Notice extends Memcached_DataObject
             $notice->saveTags();
             $notice->saveGroups();
 
-            $notice->addToInboxes();
+            if (common_config('queue', 'enabled')) {
+                $notice->addToAuthorInbox();
+            } else {
+                $notice->addToInboxes();
+            }
+
             $notice->query('COMMIT');
 
             Event::handle('EndNoticeSave', array($notice));
@@ -216,7 +221,11 @@ class Notice extends Memcached_DataObject
         # XXX: someone clever could prepend instead of clearing the cache
 
         if (common_config('memcached', 'enabled')) {
-            $notice->blowCaches();
+            if (common_config('queue', 'enabled')) {
+                $notice->blowAuthorCaches();
+            } else {
+                $notice->blowCaches();
+            }
         }
 
         return $notice;
@@ -280,6 +289,17 @@ class Notice extends Memcached_DataObject
         $this->blowGroupCache($blowLast);
     }
 
+    function blowAuthorCaches($blowLast=false)
+    {
+        // Clear the user's cache
+        $cache = common_memcache();
+        if (!empty($cache)) {
+            $cache->delete(common_cache_key('notice_inbox:by_user:'.$this->profile_id));
+        }
+        $this->blowNoticeCache($blowLast);
+        $this->blowPublicCache($blowLast);
+    }
+
     function blowGroupCache($blowLast=false)
     {
         $cache = common_memcache();
@@ -296,9 +316,9 @@ class Notice extends Memcached_DataObject
                     $member->group_id = $group_inbox->group_id;
                     if ($member->find()) {
                         while ($member->fetch()) {
-                            $cache->delete(common_cache_key('user:notices_with_friends:' . $member->profile_id));
+                            $cache->delete(common_cache_key('notice_inbox:by_user:' . $member->profile_id));
                             if ($blowLast) {
-                                $cache->delete(common_cache_key('user:notices_with_friends:' . $member->profile_id . ';last'));
+                                $cache->delete(common_cache_key('notice_inbox:by_user:' . $member->profile_id . ';last'));
                             }
                         }
                     }
@@ -341,9 +361,9 @@ class Notice extends Memcached_DataObject
                          'WHERE subscription.subscribed = ' . $this->profile_id);
 
             while ($user->fetch()) {
-                $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id));
+                $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id));
                 if ($blowLast) {
-                    $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id . ';last'));
+                    $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id.';last'));
                 }
             }
             $user->free();
@@ -602,27 +622,80 @@ class Notice extends Memcached_DataObject
         return $wrapper;
     }
 
+    function getStreamByIds($ids)
+    {
+        $cache = common_memcache();
+
+        if (!empty($cache)) {
+            $notices = array();
+            foreach ($ids as $id) {
+                $notices[] = Notice::staticGet('id', $id);
+            }
+            return new ArrayWrapper($notices);
+        } else {
+            $notice = new Notice();
+            $notice->whereAdd('id in (' . implode(', ', $ids) . ')');
+            $notice->orderBy('id DESC');
+
+            $notice->find();
+            return $notice;
+        }
+    }
+
     function publicStream($offset=0, $limit=20, $since_id=0, $before_id=0, $since=null)
     {
+        $ids = Notice::stream(array('Notice', '_publicStreamDirect'),
+                              array(),
+                              'public',
+                              $offset, $limit, $since_id, $before_id, $since);
 
-        $parts = array();
+        return Notice::getStreamByIds($ids);
+    }
 
-        $qry = 'SELECT * FROM notice ';
+    function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $before_id=0, $since=null)
+    {
+        $notice = new Notice();
+
+        $notice->selectAdd(); // clears it
+        $notice->selectAdd('id');
+
+        $notice->orderBy('id DESC');
+
+        if (!is_null($offset)) {
+            $notice->limit($offset, $limit);
+        }
 
         if (common_config('public', 'localonly')) {
-            $parts[] = 'is_local = 1';
+            $notice->whereAdd('is_local = 1');
         } else {
             # -1 == blacklisted
-            $parts[] = 'is_local != -1';
+            $notice->whereAdd('is_local != -1');
+        }
+
+        if ($since_id != 0) {
+            $notice->whereAdd('id > ' . $since_id);
+        }
+
+        if ($before_id != 0) {
+            $notice->whereAdd('id < ' . $before_id);
+        }
+
+        if (!is_null($since)) {
+            $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
         }
 
-        if ($parts) {
-            $qry .= ' WHERE ' . implode(' AND ', $parts);
+        $ids = array();
+
+        if ($notice->find()) {
+            while ($notice->fetch()) {
+                $ids[] = $notice->id;
+            }
         }
 
-        return Notice::getStream($qry,
-                                 'public',
-                                 $offset, $limit, $since_id, $before_id, null, $since);
+        $notice->free();
+        $notice = NULL;
+
+        return $ids;
     }
 
     function addToInboxes()
@@ -648,6 +721,33 @@ class Notice extends Memcached_DataObject
         return;
     }
 
+    function addToAuthorInbox()
+    {
+        $enabled = common_config('inboxes', 'enabled');
+
+        if ($enabled === true || $enabled === 'transitional') {
+            $user = User::staticGet('id', $this->profile_id);
+            if (empty($user)) {
+                return;
+            }
+            $inbox = new Notice_inbox();
+            $UT = common_config('db','type')=='pgsql'?'"user"':'user';
+            $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created) ' .
+              "SELECT $UT.id, " . $this->id . ", '" . $this->created . "' " .
+              "FROM $UT " .
+              "WHERE $UT.id = " . $this->profile_id . ' ' .
+              'AND NOT EXISTS (SELECT user_id, notice_id ' .
+              'FROM notice_inbox ' .
+              "WHERE user_id = " . $this->profile_id . ' '.
+              'AND notice_id = ' . $this->id . ' )';
+            if ($enabled === 'transitional') {
+                $qry .= " AND $UT.inboxed = 1";
+            }
+            $inbox->query($qry);
+        }
+        return;
+    }
+
     function saveGroups()
     {
         $enabled = common_config('inboxes', 'enabled');
@@ -700,24 +800,29 @@ class Notice extends Memcached_DataObject
 
                 // FIXME: do this in an offline daemon
 
-                $inbox = new Notice_inbox();
-                $UT = common_config('db','type')=='pgsql'?'"user"':'user';
-                $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created, source) ' .
-                  "SELECT $UT.id, " . $this->id . ", '" . $this->created . "', 2 " .
-                  "FROM $UT JOIN group_member ON $UT.id = group_member.profile_id " .
-                  'WHERE group_member.group_id = ' . $group->id . ' ' .
-                  'AND NOT EXISTS (SELECT user_id, notice_id ' .
-                  'FROM notice_inbox ' .
-                  "WHERE user_id = $UT.id " .
-                  'AND notice_id = ' . $this->id . ' )';
-                if ($enabled === 'transitional') {
-                    $qry .= " AND $UT.inboxed = 1";
-                }
-                $result = $inbox->query($qry);
+                $this->addToGroupInboxes($group);
             }
         }
     }
 
+    function addToGroupInboxes($group)
+    {
+        $inbox = new Notice_inbox();
+        $UT = common_config('db','type')=='pgsql'?'"user"':'user';
+        $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created, source) ' .
+          "SELECT $UT.id, " . $this->id . ", '" . $this->created . "', 2 " .
+          "FROM $UT JOIN group_member ON $UT.id = group_member.profile_id " .
+          'WHERE group_member.group_id = ' . $group->id . ' ' .
+          'AND NOT EXISTS (SELECT user_id, notice_id ' .
+          'FROM notice_inbox ' .
+          "WHERE user_id = $UT.id " .
+          'AND notice_id = ' . $this->id . ' )';
+        if ($enabled === 'transitional') {
+            $qry .= " AND $UT.inboxed = 1";
+        }
+        $result = $inbox->query($qry);
+    }
+
     function saveReplies()
     {
         // Alternative reply format
@@ -913,4 +1018,59 @@ class Notice extends Memcached_DataObject
                                     array('notice' => $this->id));
         }
     }
+
+    function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $before_id=0, $since=null)
+    {
+        $cache = common_memcache();
+
+        if (empty($cache) ||
+            $since_id != 0 || $before_id != 0 || !is_null($since) ||
+            ($offset + $limit) > NOTICE_CACHE_WINDOW) {
+            return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id,
+                                                                      $before_id, $since)));
+        }
+
+        $idkey = common_cache_key($cachekey);
+
+        $idstr = $cache->get($idkey);
+
+        if (!empty($idstr)) {
+            // Cache hit! Woohoo!
+            $window = explode(',', $idstr);
+            $ids = array_slice($window, $offset, $limit);
+            return $ids;
+        }
+
+        $laststr = $cache->get($idkey.';last');
+
+        if (!empty($laststr)) {
+            $window = explode(',', $laststr);
+            $last_id = $window[0];
+            $new_ids = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW,
+                                                                          $last_id, 0, null)));
+
+            $new_window = array_merge($new_ids, $window);
+
+            $new_windowstr = implode(',', $new_window);
+
+            $result = $cache->set($idkey, $new_windowstr);
+            $result = $cache->set($idkey . ';last', $new_windowstr);
+
+            $ids = array_slice($new_window, $offset, $limit);
+
+            return $ids;
+        }
+
+        $window = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW,
+                                                                     0, 0, null)));
+
+        $windowstr = implode(',', $window);
+
+        $result = $cache->set($idkey, $windowstr);
+        $result = $cache->set($idkey . ';last', $windowstr);
+
+        $ids = array_slice($window, $offset, $limit);
+
+        return $ids;
+    }
 }
index 81ddb453859a4721b1c1b7531d618fdc84de263a..dec14b0d18897cf0fcb40ddf8e44305929ca5103 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 /*
  * Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, Inc.
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
@@ -21,7 +21,11 @@ if (!defined('LACONICA')) { exit(1); }
 
 require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
 
-class Notice_inbox extends Memcached_DataObject 
+// We keep 5 pages of inbox notices in memcache, +1 for pagination check
+
+define('INBOX_CACHE_WINDOW', 101);
+
+class Notice_inbox extends Memcached_DataObject
 {
     ###START_AUTOCODE
     /* the code below is auto generated do not remove the above tag */
@@ -38,4 +42,47 @@ class Notice_inbox extends Memcached_DataObject
 
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
+
+    function stream($user_id, $offset, $limit, $since_id, $before_id, $since)
+    {
+        return Notice::stream(array('Notice_inbox', '_streamDirect'),
+                              array($user_id),
+                              'notice_inbox:by_user:'.$user_id,
+                              $offset, $limit, $since_id, $before_id, $since);
+    }
+
+    function _streamDirect($user_id, $offset, $limit, $since_id, $before_id, $since)
+    {
+        $inbox = new Notice_inbox();
+
+        $inbox->user_id = $user_id;
+
+        if ($since_id != 0) {
+            $inbox->whereAdd('notice_id > ' . $since_id);
+        }
+
+        if ($before_id != 0) {
+            $inbox->whereAdd('notice_id < ' . $before_id);
+        }
+
+        if (!is_null($since)) {
+            $inbox->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
+        }
+
+        $inbox->orderBy('notice_id DESC');
+
+        if (!is_null($offset)) {
+            $inbox->limit($offset, $limit);
+        }
+
+        $ids = array();
+
+        if ($inbox->find()) {
+            while ($inbox->fetch()) {
+                $ids[] = $inbox->notice_id;
+            }
+        }
+
+        return $ids;
+    }
 }
index 3b9b5cd839e4b605f985d8cbb280dd50082bcc60..ce7ea1464f8356a19fa3d3b1e06c052e50431ac9 100644 (file)
@@ -444,21 +444,17 @@ class User extends Memcached_DataObject
               'SELECT notice.* ' .
               'FROM notice JOIN subscription ON notice.profile_id = subscription.subscribed ' .
               'WHERE subscription.subscriber = %d ';
-            $order = null;
+            return Notice::getStream(sprintf($qry, $this->id),
+                                     'user:notices_with_friends:' . $this->id,
+                                     $offset, $limit, $since_id, $before_id,
+                                     $order, $since);
         } else if ($enabled === true ||
                    ($enabled == 'transitional' && $this->inboxed == 1)) {
 
-            $qry =
-              'SELECT notice.* ' .
-              'FROM notice JOIN notice_inbox ON notice.id = notice_inbox.notice_id ' .
-              'WHERE notice_inbox.user_id = %d ';
-            // NOTE: we override ORDER
-            $order = null;
+            $ids = Notice_inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since);
+
+            return Notice::getStreamByIds($ids);
         }
-        return Notice::getStream(sprintf($qry, $this->id),
-                                 'user:notices_with_friends:' . $this->id,
-                                 $offset, $limit, $since_id, $before_id,
-                                 $order, $since);
     }
 
     function blowFavesCache()
index d5124e223a8d5b0e695e35d4063ff16b8771e840..ce44f32354f2718a45dbb6f4588c2d0bd0a6fd23 100644 (file)
@@ -8,6 +8,7 @@ VALUES
     ('deskbar','Deskbar-Applet','http://www.gnome.org/projects/deskbar-applet/', now()),
     ('Do','Gnome Do','http://do.davebsd.com/wiki/index.php?title=Microblog_Plugin', now()),
     ('Facebook','Facebook','http://apps.facebook.com/identica/', now()),
+    ('feed2omb','feed2omb','http://projects.ciarang.com/p/feed2omb/', now()),
     ('Gwibber','Gwibber','http://launchpad.net/gwibber', now()),
     ('HelloTxt','HelloTxt','http://hellotxt.com/', now()),
     ('identicatools','Laconica Tools','http://bitbucketlabs.net/laconica-tools/', now()),
index 53e6eb7923c9da5734ed27846aa45ef839e1b848..f15c4f2bbf1e92f94e9f67af17ba247fb277906f 100644 (file)
@@ -166,6 +166,16 @@ $(document).ready(function(){
                                                                                                                                                   $("#notice_action-submit").addClass("disabled");
                                                                                                                                                   return true;
                                                                                                                                                 },
+                                          error: function (xhr, textStatus, errorThrown) {     $("#form_notice").removeClass("processing");
+                                                                                                                                               $("#notice_action-submit").removeAttr("disabled");
+                                                                                                                                               $("#notice_action-submit").removeClass("disabled");
+                                                                                                                                               if ($(".error", xhr.responseXML).length > 0) {
+                                                                                                                                                       $('#form_notice').append(document._importNode($(".error", xhr.responseXML).get(0), true));
+                                                                                                                                               }
+                                                                                                                                               else {
+                                                                                                                                                       alert("Sorry! We had trouble sending your notice ("+xhr.status+" "+xhr.statusText+"). Please report the problem to the site administrator if this happens again.");
+                                                                                                                                               }
+                                                                                                                                         },
                                           success: function(xml) {     if ($("#error", xml).length > 0) {
                                                                                                        var result = document._importNode($("p", xml).get(0), true);
                                                                                                        result = result.textContent || result.innerHTML;
@@ -178,11 +188,15 @@ $(document).ready(function(){
                                                                                                            alert(result);
                                                     }
                                                     else {
-                                                                                                           $("#notices_primary .notices").prepend(document._importNode($("li", xml).get(0), true));
-                                                                                                           $("#notices_primary .notice:first").css({display:"none"});
-                                                                                                           $("#notices_primary .notice:first").fadeIn(2500);
-                                                                                                           NoticeHover();
-                                                                                                           NoticeReply();
+                                                         li = $("li", xml).get(0);
+                                                         id = li.id;
+                                                         if ($("#"+li.id).length == 0) {
+                                                              $("#notices_primary .notices").prepend(document._importNode(li, true));
+                                                              $("#notices_primary .notice:first").css({display:"none"});
+                                                              $("#notices_primary .notice:first").fadeIn(2500);
+                                                              NoticeHover();
+                                                              NoticeReply();
+                                                         }
                                                                                                        }
                                                                                                        $("#notice_data-text").val("");
                                                     counter();
@@ -192,10 +206,8 @@ $(document).ready(function(){
                                                                                                $("#notice_action-submit").removeClass("disabled");
                                                                                         }
                                           };
-    if (document.body.id  != 'inbox' && document.body.id != 'outbox') {
-           $("#form_notice").ajaxForm(PostNotice);
-           $("#form_notice").each(addAjaxHidden);
-    }
+       $("#form_notice").ajaxForm(PostNotice);
+       $("#form_notice").each(addAjaxHidden);
     NoticeHover();
     NoticeReply();
 });
index dd2570e7518f5570d97e3072e49c6c478a63584e..00e5b0bc29c07db7f7e0e36bdd03125fc97a94a4 100644 (file)
@@ -193,7 +193,7 @@ $_config_files[] = INSTALLDIR.'/config.php';
 $_have_a_config = false;
 
 foreach ($_config_files as $_config_file) {
-    if (file_exists($_config_file)) {
+    if (@file_exists($_config_file)) {
         include_once($_config_file);
         $_have_a_config = true;
     }
index 0484918ce22d59e35d0a4af3e56f47a1d6778ad2..8fa11a7562752f217edb65bb206f797b5732c40a 100644 (file)
@@ -134,9 +134,11 @@ class GalleryAction extends Action
             $this->elementStart('li', array('id'=>'filter_tags_item'));
             $this->elementStart('form', array('name' => 'bytag',
                                                'id' => 'bytag',
+                                               'action' => common_path('?action=' . $this->trimmed('action')),
                                                'method' => 'post'));
             $this->dropdown('tag', _('Tag'), $content,
                             _('Choose a tag to narrow list'), false, $tag);
+            $this->hidden('nickname', $this->user->nickname);
             $this->submit('submit', _('Go'));
             $this->elementEnd('form');
             $this->elementEnd('li');
@@ -169,4 +171,4 @@ class GalleryAction extends Action
     {
         return array();
     }
-}
\ No newline at end of file
+}
index 5e16f34191d6466671acca84d2b9df6a1de9f3f9..9308c818a4bf2e0f65824dcdd5203ad1d4ede2af 100644 (file)
@@ -237,12 +237,12 @@ class Router
         $m->connect('api/statuses/:method',
                     array('action' => 'api',
                           'apiaction' => 'statuses'),
-                    array('method' => '(public_timeline|friends_timeline|user_timeline|update|replies|friends|followers|featured)(\.(atom|rss|xml|json))?'));
+                    array('method' => '(public_timeline|friends_timeline|user_timeline|update|replies|mentions|friends|followers|featured)(\.(atom|rss|xml|json))?'));
 
         $m->connect('api/statuses/:method/:argument',
                     array('action' => 'api',
                           'apiaction' => 'statuses'),
-                    array('method' => '(user_timeline|friends_timeline|replies|show|destroy|friends|followers)'));
+                    array('method' => '(user_timeline|friends_timeline|replies|mentions|show|destroy|friends|followers)'));
 
         // users
 
index d98f28afa7ad44ed033de53cb274cfa02b70b1fb..9f6948dc92711ddee0286fcc2284bf47921d2a92 100644 (file)
@@ -74,3 +74,4 @@ class SubPeopleTagCloudSection extends TagCloudSection
         $this->out->elementEnd('li');
     }
 }
+
index b5a39c6de63793947437d2c3faec18ada39a6b5f..115241a53fce73efde1f8553ceacaf18fc0f4894 100644 (file)
@@ -49,6 +49,14 @@ class SubscribersPeopleSelfTagCloudSection extends SubPeopleTagCloudSection
     }
 
     function query() {
-        return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscribed != subscriber and tagger = tagged group by tag order by weight desc';
+//        return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscribed != subscriber and tagger = tagged group by tag order by weight desc';
+
+
+        return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscribed != subscriber and tagger = tagged and tag is not null group by tag order by weight desc';
+
+//        return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscribed where subscriber=%d and subscribed != subscriber and tagger = tagged and tag is not null group by tag order by weight desc';
+
+
     }
 }
+
index 23011efdd3d3c550d4b39857b9ed9de0677b2892..4dafbc1c7df0554dc5c80f5fbbde0939f8df649a 100644 (file)
@@ -53,8 +53,9 @@ class SubscribersPeopleTagCloudSection extends SubPeopleTagCloudSection
         return common_local_url('subscribers', array('nickname' => $nickname, 'tag' => $tag));
     }
 
-
     function query() {
-        return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagged and subscribed=tagger where subscribed=%d and subscriber != subscribed group by tag order by weight desc';
+//        return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagged and subscribed=tagger where subscribed=%d and subscriber != subscribed group by tag order by weight desc';
+        return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagged and subscribed=tagger where subscribed=%d and subscriber != subscribed and tag is not null group by tag order by weight desc';
     }
 }
+
index 8ac65adb0524005a73e7fec3b2a52c1d296bc9b9..3896294d28f008655bb9de8e80a13ac5ab56f293 100644 (file)
@@ -49,6 +49,13 @@ class SubscriptionsPeopleSelfTagCloudSection extends SubPeopleTagCloudSection
     }
 
     function query() {
-        return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscriber != subscribed and tagger = tagged group by tag order by weight desc';
+//        return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscriber != subscribed and tagger = tagged group by tag order by weight desc';
+
+
+
+        return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscribed where subscriber=%d and subscribed != subscriber and tagger = tagged and tag is not null group by tag order by weight desc';
+
+//        return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscribed != subscriber and tagger = tagged and tag is not null group by tag order by weight desc';
     }
 }
+
index c3f7d1763ed84e96c3dc5b086e6847b3dc001727..f9c8672e36832c147e2e7b8c09f784134aa236dc 100644 (file)
@@ -54,6 +54,8 @@ class SubscriptionsPeopleTagCloudSection extends SubPeopleTagCloudSection
     }
 
     function query() {
-        return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagger and subscribed=tagged where subscriber=%d and subscriber != subscribed group by tag order by weight desc';
+//        return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagger and subscribed=tagged where subscriber=%d and subscriber != subscribed group by tag order by weight desc';
+        return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagger and subscribed=tagged where subscriber=%d and subscriber != subscribed and tag is not null group by tag order by weight desc';
     }
 }
+
index 6a90b4e2885701d83c99cfe93d08ac0afdf48161..caf8c071639212a4ec8c7eb19dc37509fddd197e 100644 (file)
@@ -51,6 +51,26 @@ class TwitterapiAction extends Action
         parent::handle($args);
     }
 
+    /**
+     * Overrides XMLOutputter::element to write booleans as strings (true|false).
+     * See that method's documentation for more info.
+     * 
+     * @param string $tag     Element type or tagname
+     * @param array  $attrs   Array of element attributes, as
+     *                        key-value pairs
+     * @param string $content string content of the element
+     *
+     * @return void
+     */
+    function element($tag, $attrs=null, $content=null)
+    {
+        if (is_bool($content)) {
+            $content = ($content ? 'true' : 'false');
+        }
+
+        return parent::element($tag, $attrs, $content);
+    }
+    
     function twitter_user_array($profile, $get_notice=false)
     {
 
@@ -66,7 +86,7 @@ class TwitterapiAction extends Action
         $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
 
         $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_STREAM_SIZE);
-        $twitter_user['protected'] = 'false'; # not supported by Laconica yet
+        $twitter_user['protected'] = false; # not supported by Laconica yet
         $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
 
         if ($get_notice) {
@@ -86,7 +106,7 @@ class TwitterapiAction extends Action
 
         $twitter_status = array();
         $twitter_status['text'] = $notice->content;
-        $twitter_status['truncated'] = 'false'; # Not possible on Laconica
+        $twitter_status['truncated'] = false; # Not possible on Laconica
         $twitter_status['created_at'] = $this->date_twitter($notice->created);
         $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
             intval($notice->reply_to) : null;
@@ -108,10 +128,9 @@ class TwitterapiAction extends Action
             ($replier_profile) ? $replier_profile->nickname : null;
 
         if (isset($this->auth_user)) {
-            $twitter_status['favorited'] =
-                ($this->auth_user->hasFave($notice)) ? 'true' : 'false';
+            $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
         } else {
-            $twitter_status['favorited'] = 'false';
+            $twitter_status['favorited'] = false;
         }
 
         if ($include_user) {
index da009682bbc865e6eaf1431b292b29160f4a49c9..198185338547e930cbae0269389ed3992d5ccb51 100644 (file)
@@ -519,11 +519,16 @@ function common_shorten_links($text)
 
 function common_shorten_link($url, $reverse = false)
 {
+
     static $url_cache = array();
     if ($reverse) return isset($url_cache[$url]) ? $url_cache[$url] : $url;
 
     $user = common_current_user();
-
+    if (!isset($user)) {
+      // common current user does not find a user when called from the XMPP daemon
+      // therefore we'll set one here fix, so that XMPP given URLs may be shortened
+      $user->urlshorteningservice = 'ur1.ca';
+    }
     $curlh = curl_init();
     curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 20); // # seconds to wait
     curl_setopt($curlh, CURLOPT_USERAGENT, 'Laconica');
@@ -1019,7 +1024,7 @@ function common_root_url($ssl=false)
 function common_good_rand($bytes)
 {
     // XXX: use random.org...?
-    if (file_exists('/dev/urandom')) {
+    if (@file_exists('/dev/urandom')) {
         return common_urandom($bytes);
     } else { // FIXME: this is probably not good enough
         return common_mtrand($bytes);
@@ -1385,11 +1390,10 @@ function common_compatible_license($from, $to)
  */
 function common_database_tablename($tablename)
 {
-  
+
   if(common_config('db','quote_identifiers')) {
       $tablename = '"'. $tablename .'"';
   }
   //table prefixes could be added here later
   return $tablename;
 }
-
diff --git a/plugins/Comet/CometPlugin.php b/plugins/Comet/CometPlugin.php
new file mode 100644 (file)
index 0000000..2e0bb40
--- /dev/null
@@ -0,0 +1,180 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Plugin to do "real time" updates using Comet/Bayeux
+ *
+ * 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    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);
+}
+
+/**
+ * Plugin to do realtime updates using Comet
+ *
+ * @category Plugin
+ * @package  Laconica
+ * @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 CometPlugin extends Plugin
+{
+    var $server = null;
+
+    function __construct($server=null)
+    {
+        $this->server = $server;
+
+        parent::__construct();
+    }
+
+    function onEndShowScripts($action)
+    {
+        $timeline = null;
+
+        $this->log(LOG_DEBUG, 'got action ' . $action->trimmed('action'));
+
+        switch ($action->trimmed('action')) {
+         case 'public':
+            $timeline = '/timelines/public';
+            break;
+         case 'tag':
+            $tag = $action->trimmed('tag');
+            if (!empty($tag)) {
+                $timeline = '/timelines/tag/'.$tag;
+            } else {
+                return true;
+            }
+            break;
+         default:
+            return true;
+        }
+
+        $scripts = array('jquery.comet.js', 'json2.js', 'updatetimeline.js');
+
+        foreach ($scripts as $script) {
+            $action->element('script', array('type' => 'text/javascript',
+                                             'src' => common_path('plugins/Comet/'.$script)),
+                         ' ');
+        }
+
+        $action->elementStart('script', array('type' => 'text/javascript'));
+        $action->raw("$(document).ready(function() { updater.init(\"$this->server\", \"$timeline\");});");
+        $action->elementEnd('script');
+
+        return true;
+    }
+
+    function onEndNoticeSave($notice)
+    {
+        $this->log(LOG_INFO, "Called for save notice.");
+
+        $timelines = array();
+
+        // XXX: Add other timelines; this is just for the public one
+
+        if ($notice->is_local ||
+            ($notice->is_local == 0 && !common_config('public', 'localonly'))) {
+            $timelines[] = '/timelines/public';
+        }
+
+        $tags = $this->getNoticeTags($notice);
+
+        if (!empty($tags)) {
+            foreach ($tags as $tag) {
+                $timelines[] = '/timelines/tag/' . $tag;
+            }
+        }
+
+        if (count($timelines) > 0) {
+            // Require this, since we need it
+            require_once(INSTALLDIR.'/plugins/Comet/bayeux.class.inc.php');
+
+            $json = $this->noticeAsJson($notice);
+
+            // Bayeux? Comet? Huh? These terms confuse me
+            $bay = new Bayeux($this->server);
+
+            foreach ($timelines as $timeline) {
+                $this->log(LOG_INFO, "Posting notice $notice->id to '$timeline'.");
+                $bay->publish($timeline, $json);
+            }
+
+            $bay = NULL;
+        }
+
+        return true;
+    }
+
+    function noticeAsJson($notice)
+    {
+        // FIXME: this code should be abstracted to a neutral third
+        // party, like Notice::asJson(). I'm not sure of the ethics
+        // of refactoring from within a plugin, so I'm just abusing
+        // the TwitterApiAction method. Don't do this unless you're me!
+
+        require_once(INSTALLDIR.'/lib/twitterapi.php');
+
+        $act = new TwitterApiAction('/dev/null');
+
+        $arr = $act->twitter_status_array($notice, true);
+        $arr['url'] = $notice->bestUrl();
+        $arr['html'] = htmlspecialchars($notice->rendered);
+
+        $profile = $notice->getProfile();
+        $arr['user']['profile_url'] = $profile->profileurl;
+
+        return $arr;
+    }
+
+    function getNoticeTags($notice)
+    {
+        $tags = null;
+
+        $nt = new Notice_tag();
+        $nt->notice_id = $notice->id;
+
+        if ($nt->find()) {
+            $tags = array();
+            while ($nt->fetch()) {
+                $tags[] = $nt->tag;
+            }
+        }
+
+        $nt->free();
+        $nt = null;
+
+        return $tags;
+    }
+
+    // Push this up to Plugin
+
+    function log($level, $msg)
+    {
+        common_log($level, get_class($this) . ': '.$msg);
+    }
+}
diff --git a/plugins/Comet/README b/plugins/Comet/README
new file mode 100644 (file)
index 0000000..4abd40a
--- /dev/null
@@ -0,0 +1,26 @@
+This is a plugin to automatically load notices in the browser no
+matter who creates them -- the kind of thing we see with
+search.twitter.com, rejaw.com, or FriendFeed's "real time" news.
+
+NOTE: this is an insecure version; don't roll it out on a production
+server.
+
+It requires a cometd server. I've only had the cometd-java server work
+correctly; something's wiggy with the Twisted-based server.
+
+After you have a cometd server installed, just add this code to your
+config.php:
+
+    require_once(INSTALLDIR.'/plugins/Comet/CometPlugin.php');
+    $cp = new CometPlugin('http://example.com:8080/cometd/');
+
+Change 'example.com:8080' to the name and port of the server you
+installed cometd on.
+
+TODO:
+
+* Needs to be tested with Ajax submission. Probably messes everything
+  up.
+* Add more timelines: personal inbox and tags would be great.
+* Add security. In particular, only let the PHP code publish notices
+  to the cometd server. Currently, it doesn't try to authenticate.
diff --git a/plugins/Comet/bayeux.class.inc.php b/plugins/Comet/bayeux.class.inc.php
new file mode 100644 (file)
index 0000000..785d3e3
--- /dev/null
@@ -0,0 +1,127 @@
+<?php
+/*
+ *
+ * Phomet: a php comet client
+ *
+ * Copyright (C) 2008 Morgan 'ARR!' Allen <morganrallen@gmail.com> http://morglog.alleycatracing.com
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+class Bayeux
+{
+    private $oCurl = '';
+    private $nNextId = 0;
+
+    public $sUrl = '';
+
+    function __construct($sUrl)
+    {
+        $this->sUrl = $sUrl;
+
+        $this->oCurl = curl_init();
+
+        $aHeaders = array();
+        $aHeaders[] = 'Connection: Keep-Alive';
+
+        curl_setopt($this->oCurl, CURLOPT_URL, $sUrl);
+        curl_setopt($this->oCurl, CURLOPT_HTTPHEADER, $aHeaders);
+        curl_setopt($this->oCurl, CURLOPT_HEADER, 0);
+        curl_setopt($this->oCurl, CURLOPT_POST, 1);
+        curl_setopt($this->oCurl, CURLOPT_RETURNTRANSFER,1);
+
+        $this->handShake();
+    }
+
+    function __destruct()
+    {
+        $this->disconnect();
+    }
+
+    function handShake()
+    {
+        $msgHandshake = array();
+        $msgHandshake['channel'] = '/meta/handshake';
+        $msgHandshake['version'] = "1.0";
+        $msgHandshake['minimumVersion'] = "0.9";
+        $msgHandshake['supportedConnectionTypes'] = array('long-polling');
+        $msgHandshake['id'] = $this->nNextId++;
+
+        curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($msgHandshake)))));
+
+        $data = curl_exec($this->oCurl);
+
+        if(curl_errno($this->oCurl))
+          die("Error: " . curl_error($this->oCurl));
+
+        $oReturn = json_decode($data);
+
+        if (is_array($oReturn)) {
+            $oReturn = $oReturn[0];
+        }
+
+        $bSuccessful = ($oReturn->successful) ? true : false;
+
+        if($bSuccessful)
+        {
+            $this->clientId = $oReturn->clientId;
+
+            $this->connect();
+        }
+    }
+
+    public function connect()
+    {
+        $aMsg['channel'] = '/meta/connect';
+        $aMsg['id'] = $this->nNextId++;
+        $aMsg['clientId'] = $this->clientId;
+        $aMsg['connectionType'] = 'long-polling';
+
+        curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($aMsg)))));
+
+        $data = curl_exec($this->oCurl);
+    }
+
+    function disconnect()
+    {
+        $msgHandshake = array();
+        $msgHandshake['channel'] = '/meta/disconnect';
+        $msgHandshake['id'] = $this->nNextId++;
+        $msgHandshake['clientId'] = $this->clientId;
+
+        curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($msgHandshake)))));
+
+        curl_exec($this->oCurl);
+    }
+
+    public function publish($sChannel, $oData)
+    {
+        if(!$sChannel || !$oData)
+          return;
+
+        $aMsg = array();
+
+        $aMsg['channel'] = $sChannel;
+        $aMsg['id'] = $this->nNextId++;
+        $aMsg['data'] = $oData;
+        $aMsg['clientId'] = $this->clientId;
+
+        curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($aMsg)))));
+
+        $data = curl_exec($this->oCurl);
+//        var_dump($data);
+    }
+}
diff --git a/plugins/Comet/jquery.comet.js b/plugins/Comet/jquery.comet.js
new file mode 100644 (file)
index 0000000..6de437f
--- /dev/null
@@ -0,0 +1,1451 @@
+/**
+ * Copyright 2008 Mort Bay Consulting Pty. Ltd.
+ * Dual licensed under the Apache License 2.0 and the MIT license.
+ * ----------------------------------------------------------------------------
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http: *www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ----------------------------------------------------------------------------
+ * Licensed under the MIT license;
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ * ----------------------------------------------------------------------------
+ * $Revision$ $Date$
+ */
+(function($)
+{
+    /**
+     * The constructor for a Comet object.
+     * There is a default Comet instance already created at the variable <code>$.cometd</code>,
+     * and hence that can be used to start a comet conversation with a server.
+     * In the rare case a page needs more than one comet conversation, a new instance can be
+     * created via:
+     * <pre>
+     * var url2 = ...;
+     * var cometd2 = new $.Cometd();
+     * cometd2.init(url2);
+     * </pre>
+     */
+    $.Cometd = function(name)
+    {
+        var _name = name || 'default';
+        var _logPriorities = { debug: 1, info: 2, warn: 3, error: 4 };
+        var _logLevel = 'info';
+        var _url;
+        var _xd = false;
+        var _transport;
+        var _status = 'disconnected';
+        var _messageId = 0;
+        var _clientId = null;
+        var _batch = 0;
+        var _messageQueue = [];
+        var _listeners = {};
+        var _backoff = 0;
+        var _backoffIncrement = 1000;
+        var _maxBackoff = 60000;
+        var _scheduledSend = null;
+        var _extensions = [];
+        var _advice = {};
+        var _handshakeProps;
+
+        /**
+         * Returns the name assigned to this Comet object, or the string 'default'
+         * if no name has been explicitely passed as parameter to the constructor.
+         */
+        this.getName = function()
+        {
+            return _name;
+        };
+
+        /**
+         * Configures the initial comet communication with the comet server.
+         * @param cometURL the URL of the comet server
+         */
+        this.configure = function(cometURL)
+        {
+            _configure(cometURL);
+        };
+
+        function _configure(cometURL)
+        {
+            _url = cometURL;
+            _debug('Initializing comet with url: {}', _url);
+
+            // Check immediately if we're cross domain
+            // If cross domain, the handshake must not send the long polling transport type
+            var urlParts = /(^https?:)?(\/\/(([^:\/\?#]+)(:(\d+))?))?([^\?#]*)/.exec(cometURL);
+            if (urlParts[3]) _xd = urlParts[3] != location.host;
+
+            // Temporary setup a transport to send the initial handshake
+            // The transport may be changed as a result of handshake
+            if (_xd)
+                _transport = newCallbackPollingTransport();
+            else
+                _transport = newLongPollingTransport();
+            _debug('Initial transport is {}', _transport.getType());
+        };
+
+        /**
+         * Configures and establishes the comet communication with the comet server
+         * via a handshake and a subsequent connect.
+         * @param cometURL the URL of the comet server
+         * @param handshakeProps an object to be merged with the handshake message
+         * @see #configure(cometURL)
+         * @see #handshake(handshakeProps)
+         */
+        this.init = function(cometURL, handshakeProps)
+        {
+            _configure(cometURL);
+            _handshake(handshakeProps);
+        };
+
+        /**
+         * Establishes the comet communication with the comet server
+         * via a handshake and a subsequent connect.
+         * @param handshakeProps an object to be merged with the handshake message
+         */
+        this.handshake = function(handshakeProps)
+        {
+            _handshake(handshakeProps);
+        };
+
+        /**
+         * Disconnects from the comet server.
+         * @param disconnectProps an object to be merged with the disconnect message
+         */
+        this.disconnect = function(disconnectProps)
+        {
+            var bayeuxMessage = {
+                channel: '/meta/disconnect'
+            };
+            var message = $.extend({}, disconnectProps, bayeuxMessage);
+            // Deliver immediately
+            // The handshake and connect mechanism make use of startBatch(), and in case
+            // of a failed handshake the disconnect would not be delivered if using _send().
+            _setStatus('disconnecting');
+            _deliver([message], false);
+        };
+
+        /**
+         * Marks the start of a batch of application messages to be sent to the server
+         * in a single request, obtaining a single response containing (possibly) many
+         * application reply messages.
+         * Messages are held in a queue and not sent until {@link #endBatch()} is called.
+         * If startBatch() is called multiple times, then an equal number of endBatch()
+         * calls must be made to close and send the batch of messages.
+         * @see #endBatch()
+         */
+        this.startBatch = function()
+        {
+            _startBatch();
+        };
+
+        /**
+         * Marks the end of a batch of application messages to be sent to the server
+         * in a single request.
+         * @see #startBatch()
+         */
+        this.endBatch = function()
+        {
+            _endBatch(true);
+        };
+
+        /**
+         * Subscribes to the given channel, performing the given callback in the given scope
+         * when a message for the channel arrives.
+         * @param channel the channel to subscribe to
+         * @param scope the scope of the callback
+         * @param callback the callback to call when a message is delivered to the channel
+         * @param subscribeProps an object to be merged with the subscribe message
+         * @return the subscription handle to be passed to {@link #unsubscribe(object)}
+         */
+        this.subscribe = function(channel, scope, callback, subscribeProps)
+        {
+            var subscription = this.addListener(channel, scope, callback);
+
+            // Send the subscription message after the subscription registration to avoid
+            // races where the server would deliver a message to the subscribers, but here
+            // on the client the subscription has not been added yet to the data structures
+            var bayeuxMessage = {
+                channel: '/meta/subscribe',
+                subscription: channel
+            };
+            var message = $.extend({}, subscribeProps, bayeuxMessage);
+            _send(message);
+
+            return subscription;
+        };
+
+        /**
+         * Unsubscribes the subscription obtained with a call to {@link #subscribe(string, object, function)}.
+         * @param subscription the subscription to unsubscribe.
+         */
+        this.unsubscribe = function(subscription, unsubscribeProps)
+        {
+            // Remove the local listener before sending the message
+            // This ensures that if the server fails, this client does not get notifications
+            this.removeListener(subscription);
+            var bayeuxMessage = {
+                channel: '/meta/unsubscribe',
+                subscription: subscription[0]
+            };
+            var message = $.extend({}, unsubscribeProps, bayeuxMessage);
+            _send(message);
+        };
+
+        /**
+         * Publishes a message on the given channel, containing the given content.
+         * @param channel the channel to publish the message to
+         * @param content the content of the message
+         * @param publishProps an object to be merged with the publish message
+         */
+        this.publish = function(channel, content, publishProps)
+        {
+            var bayeuxMessage = {
+                channel: channel,
+                data: content
+            };
+            var message = $.extend({}, publishProps, bayeuxMessage);
+            _send(message);
+        };
+
+        /**
+         * Adds a listener for bayeux messages, performing the given callback in the given scope
+         * when a message for the given channel arrives.
+         * @param channel the channel the listener is interested to
+         * @param scope the scope of the callback
+         * @param callback the callback to call when a message is delivered to the channel
+         * @returns the subscription handle to be passed to {@link #removeListener(object)}
+         * @see #removeListener(object)
+         */
+        this.addListener = function(channel, scope, callback)
+        {
+            // The data structure is a map<channel, subscription[]>, where each subscription
+            // holds the callback to be called and its scope.
+
+            // Normalize arguments
+            if (!callback)
+            {
+                callback = scope;
+                scope = undefined;
+            }
+
+            var subscription = {
+                scope: scope,
+                callback: callback
+            };
+
+            var subscriptions = _listeners[channel];
+            if (!subscriptions)
+            {
+                subscriptions = [];
+                _listeners[channel] = subscriptions;
+            }
+            // Pushing onto an array appends at the end and returns the id associated with the element increased by 1.
+            // Note that if:
+            // a.push('a'); var hb=a.push('b'); delete a[hb-1]; var hc=a.push('c');
+            // then:
+            // hc==3, a.join()=='a',,'c', a.length==3
+            var subscriptionIndex = subscriptions.push(subscription) - 1;
+            _debug('Added listener: channel \'{}\', callback \'{}\', index {}', channel, callback.name, subscriptionIndex);
+
+            // The subscription to allow removal of the listener is made of the channel and the index
+            return [channel, subscriptionIndex];
+        };
+
+        /**
+         * Removes the subscription obtained with a call to {@link #addListener(string, object, function)}.
+         * @param subscription the subscription to unsubscribe.
+         */
+        this.removeListener = function(subscription)
+        {
+            var subscriptions = _listeners[subscription[0]];
+            if (subscriptions)
+            {
+                delete subscriptions[subscription[1]];
+                _debug('Removed listener: channel \'{}\', index {}', subscription[0], subscription[1]);
+            }
+        };
+
+        /**
+         * Removes all listeners registered with {@link #addListener(channel, scope, callback)} or
+         * {@link #subscribe(channel, scope, callback)}.
+         */
+        this.clearListeners = function()
+        {
+            _listeners = {};
+        };
+
+        /**
+         * Returns a string representing the status of the bayeux communication with the comet server.
+         */
+        this.getStatus = function()
+        {
+            return _status;
+        };
+
+        /**
+         * Sets the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
+         * Default value is 1 second, which means if there is a persistent failure the retries will happen
+         * after 1 second, then after 2 seconds, then after 3 seconds, etc. So for example with 15 seconds of
+         * elapsed time, there will be 5 retries (at 1, 3, 6, 10 and 15 seconds elapsed).
+         * @param period the backoff period to set
+         * @see #getBackoffIncrement()
+         */
+        this.setBackoffIncrement = function(period)
+        {
+            _backoffIncrement = period;
+        };
+
+        /**
+         * Returns the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
+         * @see #setBackoffIncrement(period)
+         */
+        this.getBackoffIncrement = function()
+        {
+            return _backoffIncrement;
+        };
+
+        /**
+         * Returns the backoff period to wait before retrying an unsuccessful or failed message.
+         */
+        this.getBackoffPeriod = function()
+        {
+            return _backoff;
+        };
+
+        /**
+         * Sets the log level for console logging.
+         * Valid values are the strings 'error', 'warn', 'info' and 'debug', from
+         * less verbose to more verbose.
+         * @param level the log level string
+         */
+        this.setLogLevel = function(level)
+        {
+            _logLevel = level;
+        };
+
+        /**
+         * Registers an extension whose callbacks are called for every incoming message
+         * (that comes from the server to this client implementation) and for every
+         * outgoing message (that originates from this client implementation for the
+         * server).
+         * The format of the extension object is the following:
+         * <pre>
+         * {
+         *     incoming: function(message) { ... },
+         *     outgoing: function(message) { ... }
+         * }
+         * Both properties are optional, but if they are present they will be called
+         * respectively for each incoming message and for each outgoing message.
+         * </pre>
+         * @param name the name of the extension
+         * @param extension the extension to register
+         * @return true if the extension was registered, false otherwise
+         * @see #unregisterExtension(name)
+         */
+        this.registerExtension = function(name, extension)
+        {
+            var existing = false;
+            for (var i = 0; i < _extensions.length; ++i)
+            {
+                var existingExtension = _extensions[i];
+                if (existingExtension.name == name)
+                {
+                    existing = true;
+                    return false;
+                }
+            }
+            if (!existing)
+            {
+                _extensions.push({
+                    name: name,
+                    extension: extension
+                });
+                _debug('Registered extension \'{}\'', name);
+                return true;
+            }
+            else
+            {
+                _info('Could not register extension with name \'{}\': another extension with the same name already exists');
+                return false;
+            }
+        };
+
+        /**
+         * Unregister an extension previously registered with
+         * {@link #registerExtension(name, extension)}.
+         * @param name the name of the extension to unregister.
+         * @return true if the extension was unregistered, false otherwise
+         */
+        this.unregisterExtension = function(name)
+        {
+            var unregistered = false;
+            $.each(_extensions, function(index, extension)
+            {
+                if (extension.name == name)
+                {
+                    _extensions.splice(index, 1);
+                    unregistered = true;
+                    _debug('Unregistered extension \'{}\'', name);
+                    return false;
+                }
+            });
+            return unregistered;
+        };
+
+        /**
+         * Starts a the batch of messages to be sent in a single request.
+         * @see _endBatch(deliverMessages)
+         */
+        function _startBatch()
+        {
+            ++_batch;
+        };
+
+        /**
+         * Ends the batch of messages to be sent in a single request,
+         * optionally delivering messages present in the message queue depending
+         * on the given argument.
+         * @param deliverMessages whether to deliver the messages in the queue or not
+         * @see _startBatch()
+         */
+        function _endBatch(deliverMessages)
+        {
+            --_batch;
+            if (_batch < 0) _batch = 0;
+            if (deliverMessages && _batch == 0 && !_isDisconnected())
+            {
+                var messages = _messageQueue;
+                _messageQueue = [];
+                if (messages.length > 0) _deliver(messages, false);
+            }
+        };
+
+        function _nextMessageId()
+        {
+            return ++_messageId;
+        };
+
+        /**
+         * Converts the given response into an array of bayeux messages
+         * @param response the response to convert
+         * @return an array of bayeux messages obtained by converting the response
+         */
+        function _convertToMessages(response)
+        {
+            if (response === undefined) return [];
+            if (response instanceof Array) return response;
+            if (response instanceof String || typeof response == 'string') return eval('(' + response + ')');
+            if (response instanceof Object) return [response];
+            throw 'Conversion Error ' + response + ', typeof ' + (typeof response);
+        };
+
+        function _setStatus(newStatus)
+        {
+            _debug('{} -> {}', _status, newStatus);
+            _status = newStatus;
+        };
+
+        function _isDisconnected()
+        {
+            return _status == 'disconnecting' || _status == 'disconnected';
+        };
+
+        /**
+         * Sends the initial handshake message
+         */
+        function _handshake(handshakeProps)
+        {
+            _debug('Starting handshake');
+            _clientId = null;
+
+            // Start a batch.
+            // This is needed because handshake and connect are async.
+            // It may happen that the application calls init() then subscribe()
+            // and the subscribe message is sent before the connect message, if
+            // the subscribe message is not held until the connect message is sent.
+            // So here we start a batch to hold temporarly any message until
+            // the connection is fully established.
+            _batch = 0;
+            _startBatch();
+
+            // Save the original properties provided by the user
+            // Deep copy to avoid the user to be able to change them later
+            _handshakeProps = $.extend(true, {}, handshakeProps);
+
+            var bayeuxMessage = {
+                version: '1.0',
+                minimumVersion: '0.9',
+                channel: '/meta/handshake',
+                supportedConnectionTypes: _xd ? ['callback-polling'] : ['long-polling', 'callback-polling']
+            };
+            // Do not allow the user to mess with the required properties,
+            // so merge first the user properties and *then* the bayeux message
+            var message = $.extend({}, handshakeProps, bayeuxMessage);
+
+            // We started a batch to hold the application messages,
+            // so here we must bypass it and deliver immediately.
+            _setStatus('handshaking');
+            _deliver([message], false);
+        };
+
+        function _findTransport(handshakeResponse)
+        {
+            var transportTypes = handshakeResponse.supportedConnectionTypes;
+            if (_xd)
+            {
+                // If we are cross domain, check if the server supports it, that's the only option
+                if ($.inArray('callback-polling', transportTypes) >= 0) return _transport;
+            }
+            else
+            {
+                // Check if we can keep long-polling
+                if ($.inArray('long-polling', transportTypes) >= 0) return _transport;
+
+                // The server does not support long-polling
+                if ($.inArray('callback-polling', transportTypes) >= 0) return newCallbackPollingTransport();
+            }
+            return null;
+        };
+
+        function _delayedHandshake()
+        {
+            _setStatus('handshaking');
+            _delayedSend(function()
+            {
+                _handshake(_handshakeProps);
+            });
+        };
+
+        function _delayedConnect()
+        {
+            _setStatus('connecting');
+            _delayedSend(function()
+            {
+                _connect();
+            });
+        };
+
+        function _delayedSend(operation)
+        {
+            _cancelDelayedSend();
+            var delay = _backoff;
+            _debug("Delayed send: backoff {}, interval {}", _backoff, _advice.interval);
+            if (_advice.interval && _advice.interval > 0)
+                delay += _advice.interval;
+            _scheduledSend = _setTimeout(operation, delay);
+        };
+
+        function _cancelDelayedSend()
+        {
+            if (_scheduledSend !== null) clearTimeout(_scheduledSend);
+            _scheduledSend = null;
+        };
+
+        function _setTimeout(funktion, delay)
+        {
+            return setTimeout(function()
+            {
+                try
+                {
+                    funktion();
+                }
+                catch (x)
+                {
+                    _debug('Exception during scheduled execution of function \'{}\': {}', funktion.name, x);
+                }
+            }, delay);
+        };
+
+        /**
+         * Sends the connect message
+         */
+        function _connect()
+        {
+            _debug('Starting connect');
+            var message = {
+                channel: '/meta/connect',
+                connectionType: _transport.getType()
+            };
+            _setStatus('connecting');
+            _deliver([message], true);
+            _setStatus('connected');
+        };
+
+        function _send(message)
+        {
+            if (_batch > 0)
+                _messageQueue.push(message);
+            else
+                _deliver([message], false);
+        };
+
+        /**
+         * Delivers the messages to the comet server
+         * @param messages the array of messages to send
+         */
+        function _deliver(messages, comet)
+        {
+            // We must be sure that the messages have a clientId.
+            // This is not guaranteed since the handshake may take time to return
+            // (and hence the clientId is not known yet) and the application
+            // may create other messages.
+            $.each(messages, function(index, message)
+            {
+                message['id'] = _nextMessageId();
+                if (_clientId) message['clientId'] = _clientId;
+                messages[index] = _applyOutgoingExtensions(message);
+            });
+
+            var self = this;
+            var envelope = {
+                url: _url,
+                messages: messages,
+                onSuccess: function(request, response)
+                {
+                    try
+                    {
+                        _handleSuccess.call(self, request, response, comet);
+                    }
+                    catch (x)
+                    {
+                        _debug('Exception during execution of success callback: {}', x);
+                    }
+                },
+                onFailure: function(request, reason, exception)
+                {
+                    try
+                    {
+                        _handleFailure.call(self, request, messages, reason, exception, comet);
+                    }
+                    catch (x)
+                    {
+                        _debug('Exception during execution of failure callback: {}', x);
+                    }
+                }
+            };
+            _debug('Sending request to {}, message(s): {}', envelope.url, JSON.stringify(envelope.messages));
+            _transport.send(envelope, comet);
+        };
+
+        function _applyIncomingExtensions(message)
+        {
+            for (var i = 0; i < _extensions.length; ++i)
+            {
+                var extension = _extensions[i];
+                var callback = extension.extension.incoming;
+                if (callback && typeof callback === 'function')
+                {
+                    _debug('Calling incoming extension \'{}\', callback \'{}\'', extension.name, callback.name);
+                    message = _applyExtension(extension.name, callback, message) || message;
+                }
+            }
+            return message;
+        };
+
+        function _applyOutgoingExtensions(message)
+        {
+            for (var i = 0; i < _extensions.length; ++i)
+            {
+                var extension = _extensions[i];
+                var callback = extension.extension.outgoing;
+                if (callback && typeof callback === 'function')
+                {
+                    _debug('Calling outgoing extension \'{}\', callback \'{}\'', extension.name, callback.name);
+                    message = _applyExtension(extension.name, callback, message) || message;
+                }
+            }
+            return message;
+        };
+
+        function _applyExtension(name, callback, message)
+        {
+            try
+            {
+                return callback(message);
+            }
+            catch (x)
+            {
+                _debug('Exception during execution of extension \'{}\': {}', name, x);
+                return message;
+            }
+        };
+
+        function _handleSuccess(request, response, comet)
+        {
+            var messages = _convertToMessages(response);
+            _debug('Received response {}', JSON.stringify(messages));
+
+            // Signal the transport it can deliver other queued requests
+            _transport.complete(request, true, comet);
+
+            for (var i = 0; i < messages.length; ++i)
+            {
+                var message = messages[i];
+                message = _applyIncomingExtensions(message);
+
+                if (message.advice) _advice = message.advice;
+
+                var channel = message.channel;
+                switch (channel)
+                {
+                    case '/meta/handshake':
+                        _handshakeSuccess(message);
+                        break;
+                    case '/meta/connect':
+                        _connectSuccess(message);
+                        break;
+                    case '/meta/disconnect':
+                        _disconnectSuccess(message);
+                        break;
+                    case '/meta/subscribe':
+                        _subscribeSuccess(message);
+                        break;
+                    case '/meta/unsubscribe':
+                        _unsubscribeSuccess(message);
+                        break;
+                    default:
+                        _messageSuccess(message);
+                        break;
+                }
+            }
+        };
+
+        function _handleFailure(request, messages, reason, exception, comet)
+        {
+            var xhr = request.xhr;
+            _debug('Request failed, status: {}, reason: {}, exception: {}', xhr && xhr.status, reason, exception);
+
+            // Signal the transport it can deliver other queued requests
+            _transport.complete(request, false, comet);
+
+            for (var i = 0; i < messages.length; ++i)
+            {
+                var message = messages[i];
+                var channel = message.channel;
+                switch (channel)
+                {
+                    case '/meta/handshake':
+                        _handshakeFailure(xhr, message);
+                        break;
+                    case '/meta/connect':
+                        _connectFailure(xhr, message);
+                        break;
+                    case '/meta/disconnect':
+                        _disconnectFailure(xhr, message);
+                        break;
+                    case '/meta/subscribe':
+                        _subscribeFailure(xhr, message);
+                        break;
+                    case '/meta/unsubscribe':
+                        _unsubscribeFailure(xhr, message);
+                        break;
+                    default:
+                        _messageFailure(xhr, message);
+                        break;
+                }
+            }
+        };
+
+        function _handshakeSuccess(message)
+        {
+            if (message.successful)
+            {
+                _debug('Handshake successful');
+                // Save clientId, figure out transport, then follow the advice to connect
+                _clientId = message.clientId;
+
+                var newTransport = _findTransport(message);
+                if (newTransport === null)
+                {
+                    throw 'Could not agree on transport with server';
+                }
+                else
+                {
+                    if (_transport.getType() != newTransport.getType())
+                    {
+                        _debug('Changing transport from {} to {}', _transport.getType(), newTransport.getType());
+                        _transport = newTransport;
+                    }
+                }
+
+                // Notify the listeners
+                // Here the new transport is in place, as well as the clientId, so
+                // the listener can perform a publish() if it wants, and the listeners
+                // are notified before the connect below.
+                _notifyListeners('/meta/handshake', message);
+
+                var action = _advice.reconnect ? _advice.reconnect : 'retry';
+                switch (action)
+                {
+                    case 'retry':
+                        _delayedConnect();
+                        break;
+                    default:
+                        break;
+                }
+            }
+            else
+            {
+                _debug('Handshake unsuccessful');
+
+                var retry = !_isDisconnected() && _advice.reconnect != 'none';
+                if (!retry) _setStatus('disconnected');
+
+                _notifyListeners('/meta/handshake', message);
+                _notifyListeners('/meta/unsuccessful', message);
+
+                // Only try again if we haven't been disconnected and
+                // the advice permits us to retry the handshake
+                if (retry)
+                {
+                    _increaseBackoff();
+                    _debug('Handshake failure, backing off and retrying in {} ms', _backoff);
+                    _delayedHandshake();
+                }
+            }
+        };
+
+        function _handshakeFailure(xhr, message)
+        {
+            _debug('Handshake failure');
+
+            // Notify listeners
+            var failureMessage = {
+                successful: false,
+                failure: true,
+                channel: '/meta/handshake',
+                request: message,
+                xhr: xhr,
+                advice: {
+                    action: 'retry',
+                    interval: _backoff
+                }
+            };
+
+            var retry = !_isDisconnected() && _advice.reconnect != 'none';
+            if (!retry) _setStatus('disconnected');
+
+            _notifyListeners('/meta/handshake', failureMessage);
+            _notifyListeners('/meta/unsuccessful', failureMessage);
+
+            // Only try again if we haven't been disconnected and the
+            // advice permits us to try again
+            if (retry)
+            {
+                _increaseBackoff();
+                _debug('Handshake failure, backing off and retrying in {} ms', _backoff);
+                _delayedHandshake();
+            }
+        };
+
+        function _connectSuccess(message)
+        {
+            var action = _isDisconnected() ? 'none' : (_advice.reconnect ? _advice.reconnect : 'retry');
+            if (!_isDisconnected()) _setStatus(action == 'retry' ? 'connecting' : 'disconnecting');
+
+            if (message.successful)
+            {
+                _debug('Connect successful');
+
+                // End the batch and allow held messages from the application
+                // to go to the server (see _handshake() where we start the batch).
+                // The batch is ended before notifying the listeners, so that
+                // listeners can batch other cometd operations
+                _endBatch(true);
+
+                // Notify the listeners after the status change but before the next connect
+                _notifyListeners('/meta/connect', message);
+
+                // Connect was successful.
+                // Normally, the advice will say "reconnect: 'retry', interval: 0"
+                // and the server will hold the request, so when a response returns
+                // we immediately call the server again (long polling)
+                switch (action)
+                {
+                    case 'retry':
+                        _resetBackoff();
+                        _delayedConnect();
+                        break;
+                    default:
+                        _resetBackoff();
+                        _setStatus('disconnected');
+                        break;
+                }
+            }
+            else
+            {
+                _debug('Connect unsuccessful');
+
+                // Notify the listeners after the status change but before the next action
+                _notifyListeners('/meta/connect', message);
+                _notifyListeners('/meta/unsuccessful', message);
+
+                // Connect was not successful.
+                // This may happen when the server crashed, the current clientId
+                // will be invalid, and the server will ask to handshake again
+                switch (action)
+                {
+                    case 'retry':
+                        _increaseBackoff();
+                        _delayedConnect();
+                        break;
+                    case 'handshake':
+                        // End the batch but do not deliver the messages until we connect successfully
+                        _endBatch(false);
+                        _resetBackoff();
+                        _delayedHandshake();
+                        break;
+                    case 'none':
+                        _resetBackoff();
+                        _setStatus('disconnected');
+                        break;
+                }
+            }
+        };
+
+        function _connectFailure(xhr, message)
+        {
+            _debug('Connect failure');
+
+            // Notify listeners
+            var failureMessage = {
+                successful: false,
+                failure: true,
+                channel: '/meta/connect',
+                request: message,
+                xhr: xhr,
+                advice: {
+                    action: 'retry',
+                    interval: _backoff
+                }
+            };
+            _notifyListeners('/meta/connect', failureMessage);
+            _notifyListeners('/meta/unsuccessful', failureMessage);
+
+            if (!_isDisconnected())
+            {
+                var action = _advice.reconnect ? _advice.reconnect : 'retry';
+                switch (action)
+                {
+                    case 'retry':
+                        _increaseBackoff();
+                        _debug('Connect failure, backing off and retrying in {} ms', _backoff);
+                        _delayedConnect();
+                        break;
+                    case 'handshake':
+                        _resetBackoff();
+                        _delayedHandshake();
+                        break;
+                    case 'none':
+                        _resetBackoff();
+                        break;
+                    default:
+                        _debug('Unrecognized reconnect value: {}', action);
+                        break;
+                }
+            }
+        };
+
+        function _disconnectSuccess(message)
+        {
+            if (message.successful)
+            {
+                _debug('Disconnect successful');
+                _disconnect(false);
+                _notifyListeners('/meta/disconnect', message);
+            }
+            else
+            {
+                _debug('Disconnect unsuccessful');
+                _disconnect(true);
+                _notifyListeners('/meta/disconnect', message);
+                _notifyListeners('/meta/usuccessful', message);
+            }
+        };
+
+        function _disconnect(abort)
+        {
+            _cancelDelayedSend();
+            if (abort) _transport.abort();
+            _clientId = null;
+            _setStatus('disconnected');
+            _batch = 0;
+            _messageQueue = [];
+            _resetBackoff();
+        };
+
+        function _disconnectFailure(xhr, message)
+        {
+            _debug('Disconnect failure');
+            _disconnect(true);
+
+            var failureMessage = {
+                successful: false,
+                failure: true,
+                channel: '/meta/disconnect',
+                request: message,
+                xhr: xhr,
+                advice: {
+                    action: 'none',
+                    interval: 0
+                }
+            };
+            _notifyListeners('/meta/disconnect', failureMessage);
+            _notifyListeners('/meta/unsuccessful', failureMessage);
+        };
+
+        function _subscribeSuccess(message)
+        {
+            if (message.successful)
+            {
+                _debug('Subscribe successful');
+                _notifyListeners('/meta/subscribe', message);
+            }
+            else
+            {
+                _debug('Subscribe unsuccessful');
+                _notifyListeners('/meta/subscribe', message);
+                _notifyListeners('/meta/unsuccessful', message);
+            }
+        };
+
+        function _subscribeFailure(xhr, message)
+        {
+            _debug('Subscribe failure');
+
+            var failureMessage = {
+                successful: false,
+                failure: true,
+                channel: '/meta/subscribe',
+                request: message,
+                xhr: xhr,
+                advice: {
+                    action: 'none',
+                    interval: 0
+                }
+            };
+            _notifyListeners('/meta/subscribe', failureMessage);
+            _notifyListeners('/meta/unsuccessful', failureMessage);
+        };
+
+        function _unsubscribeSuccess(message)
+        {
+            if (message.successful)
+            {
+                _debug('Unsubscribe successful');
+                _notifyListeners('/meta/unsubscribe', message);
+            }
+            else
+            {
+                _debug('Unsubscribe unsuccessful');
+                _notifyListeners('/meta/unsubscribe', message);
+                _notifyListeners('/meta/unsuccessful', message);
+            }
+        };
+
+        function _unsubscribeFailure(xhr, message)
+        {
+            _debug('Unsubscribe failure');
+
+            var failureMessage = {
+                successful: false,
+                failure: true,
+                channel: '/meta/unsubscribe',
+                request: message,
+                xhr: xhr,
+                advice: {
+                    action: 'none',
+                    interval: 0
+                }
+            };
+            _notifyListeners('/meta/unsubscribe', failureMessage);
+            _notifyListeners('/meta/unsuccessful', failureMessage);
+        };
+
+        function _messageSuccess(message)
+        {
+            if (message.successful === undefined)
+            {
+                if (message.data)
+                {
+                    // It is a plain message, and not a bayeux meta message
+                    _notifyListeners(message.channel, message);
+                }
+                else
+                {
+                    _debug('Unknown message {}', JSON.stringify(message));
+                }
+            }
+            else
+            {
+                if (message.successful)
+                {
+                    _debug('Publish successful');
+                    _notifyListeners('/meta/publish', message);
+                }
+                else
+                {
+                    _debug('Publish unsuccessful');
+                    _notifyListeners('/meta/publish', message);
+                    _notifyListeners('/meta/unsuccessful', message);
+                }
+            }
+        };
+
+        function _messageFailure(xhr, message)
+        {
+            _debug('Publish failure');
+
+            var failureMessage = {
+                successful: false,
+                failure: true,
+                channel: message.channel,
+                request: message,
+                xhr: xhr,
+                advice: {
+                    action: 'none',
+                    interval: 0
+                }
+            };
+            _notifyListeners('/meta/publish', failureMessage);
+            _notifyListeners('/meta/unsuccessful', failureMessage);
+        };
+
+        function _notifyListeners(channel, message)
+        {
+            // Notify direct listeners
+            _notify(channel, message);
+
+            // Notify the globbing listeners
+            var channelParts = channel.split("/");
+            var last = channelParts.length - 1;
+            for (var i = last; i > 0; --i)
+            {
+                var channelPart = channelParts.slice(0, i).join('/') + '/*';
+                // We don't want to notify /foo/* if the channel is /foo/bar/baz,
+                // so we stop at the first non recursive globbing
+                if (i == last) _notify(channelPart, message);
+                // Add the recursive globber and notify
+                channelPart += '*';
+                _notify(channelPart, message);
+            }
+        };
+
+        function _notify(channel, message)
+        {
+            var subscriptions = _listeners[channel];
+            if (subscriptions && subscriptions.length > 0)
+            {
+                for (var i = 0; i < subscriptions.length; ++i)
+                {
+                    var subscription = subscriptions[i];
+                    // Subscriptions may come and go, so the array may have 'holes'
+                    if (subscription)
+                    {
+                        try
+                        {
+                            _debug('Notifying subscription: channel \'{}\', callback \'{}\'', channel, subscription.callback.name);
+                            subscription.callback.call(subscription.scope, message);
+                        }
+                        catch (x)
+                        {
+                            // Ignore exceptions from callbacks
+                            _warn('Exception during execution of callback \'{}\' on channel \'{}\' for message {}, exception: {}', subscription.callback.name, channel, JSON.stringify(message), x);
+                        }
+                    }
+                }
+            }
+        };
+
+        function _resetBackoff()
+        {
+            _backoff = 0;
+        };
+
+        function _increaseBackoff()
+        {
+            if (_backoff < _maxBackoff) _backoff += _backoffIncrement;
+        };
+
+        var _error = this._error = function(text, args)
+        {
+            _log('error', _format.apply(this, arguments));
+        };
+
+        var _warn = this._warn = function(text, args)
+        {
+            _log('warn', _format.apply(this, arguments));
+        };
+
+        var _info = this._info = function(text, args)
+        {
+            _log('info', _format.apply(this, arguments));
+        };
+
+        var _debug = this._debug = function(text, args)
+        {
+            _log('debug', _format.apply(this, arguments));
+        };
+
+        function _log(level, text)
+        {
+            var priority = _logPriorities[level];
+            var configPriority = _logPriorities[_logLevel];
+            if (!configPriority) configPriority = _logPriorities['info'];
+            if (priority >= configPriority)
+            {
+                if (window.console) window.console.log(text);
+            }
+        };
+
+        function _format(text)
+        {
+            var braces = /\{\}/g;
+            var result = '';
+            var start = 0;
+            var count = 0;
+            while (braces.test(text))
+            {
+                result += text.substr(start, braces.lastIndex - start - 2);
+                var arg = arguments[++count];
+                result += arg !== undefined ? arg : '{}';
+                start = braces.lastIndex;
+            }
+            result += text.substr(start, text.length - start);
+            return result;
+        };
+
+        function newLongPollingTransport()
+        {
+            return $.extend({}, new Transport('long-polling'), new LongPollingTransport());
+        };
+
+        function newCallbackPollingTransport()
+        {
+            return $.extend({}, new Transport('callback-polling'), new CallbackPollingTransport());
+        };
+
+        /**
+         * Base object with the common functionality for transports.
+         * The key responsibility is to allow at most 2 outstanding requests to the server,
+         * to avoid that requests are sent behind a long poll.
+         * To achieve this, we have one reserved request for the long poll, and all other
+         * requests are serialized one after the other.
+         */
+        var Transport = function(type)
+        {
+            var _maxRequests = 2;
+            var _requestIds = 0;
+            var _cometRequest = null;
+            var _requests = [];
+            var _packets = [];
+
+            this.getType = function()
+            {
+                return type;
+            };
+
+            this.send = function(packet, comet)
+            {
+                if (comet)
+                    _cometSend(this, packet);
+                else
+                    _send(this, packet);
+            };
+
+            function _cometSend(self, packet)
+            {
+                if (_cometRequest !== null) throw 'Concurrent comet requests not allowed, request ' + _cometRequest.id + ' not yet completed';
+
+                var requestId = ++_requestIds;
+                _debug('Beginning comet request {}', requestId);
+
+                var request = {id: requestId};
+                _debug('Delivering comet request {}', requestId);
+                self.deliver(packet, request);
+                _cometRequest = request;
+            };
+
+            function _send(self, packet)
+            {
+                var requestId = ++_requestIds;
+                _debug('Beginning request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length);
+
+                var request = {id: requestId};
+                // Consider the comet request which should always be present
+                if (_requests.length < _maxRequests - 1)
+                {
+                    _debug('Delivering request {}', requestId);
+                    self.deliver(packet, request);
+                    _requests.push(request);
+                }
+                else
+                {
+                    _packets.push([packet, request]);
+                    _debug('Queued request {}, {} queued requests', requestId, _packets.length);
+                }
+            };
+
+            this.complete = function(request, success, comet)
+            {
+                if (comet)
+                    _cometComplete(request);
+                else
+                    _complete(this, request, success);
+            };
+
+            function _cometComplete(request)
+            {
+                var requestId = request.id;
+                if (_cometRequest !== request) throw 'Comet request mismatch, completing request ' + requestId;
+
+                // Reset comet request
+                _cometRequest = null;
+                _debug('Ended comet request {}', requestId);
+            };
+
+            function _complete(self, request, success)
+            {
+                var requestId = request.id;
+                var index = $.inArray(request, _requests);
+                // The index can be negative the request has been aborted
+                if (index >= 0) _requests.splice(index, 1);
+                _debug('Ended request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length);
+
+                if (_packets.length > 0)
+                {
+                    var packet = _packets.shift();
+                    if (success)
+                    {
+                        _debug('Dequeueing and sending request {}, {} queued requests', packet[1].id, _packets.length);
+                        _send(self, packet[0]);
+                    }
+                    else
+                    {
+                        _debug('Dequeueing and failing request {}, {} queued requests', packet[1].id, _packets.length);
+                        // Keep the semantic of calling response callbacks asynchronously after the request
+                        setTimeout(function() { packet[0].onFailure(packet[1], 'error'); }, 0);
+                    }
+                }
+            };
+
+            this.abort = function()
+            {
+                for (var i = 0; i < _requests.length; ++i)
+                {
+                    var request = _requests[i];
+                    _debug('Aborting request {}', request.id);
+                    if (request.xhr) request.xhr.abort();
+                }
+                if (_cometRequest)
+                {
+                    _debug('Aborting comet request {}', _cometRequest.id);
+                    if (_cometRequest.xhr) _cometRequest.xhr.abort();
+                }
+                _cometRequest = null;
+                _requests = [];
+                _packets = [];
+            };
+        };
+
+        var LongPollingTransport = function()
+        {
+            this.deliver = function(packet, request)
+            {
+                request.xhr = $.ajax({
+                    url: packet.url,
+                    type: 'POST',
+                    contentType: 'text/json;charset=UTF-8',
+                    beforeSend: function(xhr)
+                    {
+                        xhr.setRequestHeader('Connection', 'Keep-Alive');
+                        return true;
+                    },
+                    data: JSON.stringify(packet.messages),
+                    success: function(response) { packet.onSuccess(request, response); },
+                    error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); }
+                });
+            };
+        };
+
+        var CallbackPollingTransport = function()
+        {
+            var _maxLength = 2000;
+            this.deliver = function(packet, request)
+            {
+                // Microsoft Internet Explorer has a 2083 URL max length
+                // We must ensure that we stay within that length
+                var messages = JSON.stringify(packet.messages);
+                // Encode the messages because all brackets, quotes, commas, colons, etc
+                // present in the JSON will be URL encoded, taking many more characters
+                var urlLength = packet.url.length + encodeURI(messages).length;
+                _debug('URL length: {}', urlLength);
+                // Let's stay on the safe side and use 2000 instead of 2083
+                // also because we did not count few characters among which
+                // the parameter name 'message' and the parameter 'jsonp',
+                // which sum up to about 50 chars
+                if (urlLength > _maxLength)
+                {
+                    var x = packet.messages.length > 1 ?
+                            'Too many bayeux messages in the same batch resulting in message too big ' +
+                            '(' + urlLength + ' bytes, max is ' + _maxLength + ') for transport ' + this.getType() :
+                            'Bayeux message too big (' + urlLength + ' bytes, max is ' + _maxLength + ') ' +
+                            'for transport ' + this.getType();
+                    // Keep the semantic of calling response callbacks asynchronously after the request
+                    _setTimeout(function() { packet.onFailure(request, 'error', x); }, 0);
+                }
+                else
+                {
+                    $.ajax({
+                        url: packet.url,
+                        type: 'GET',
+                        dataType: 'jsonp',
+                        jsonp: 'jsonp',
+                        beforeSend: function(xhr)
+                        {
+                            xhr.setRequestHeader('Connection', 'Keep-Alive');
+                            return true;
+                        },
+                        data:
+                        {
+                            // In callback-polling, the content must be sent via the 'message' parameter
+                            message: messages
+                        },
+                        success: function(response) { packet.onSuccess(request, response); },
+                        error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); }
+                    });
+                }
+            };
+        };
+    };
+
+    /**
+     * The JS object that exposes the comet API to applications
+     */
+    $.cometd = new $.Cometd(); // The default instance
+
+})(jQuery);
diff --git a/plugins/Comet/json2.js b/plugins/Comet/json2.js
new file mode 100644 (file)
index 0000000..7e27df5
--- /dev/null
@@ -0,0 +1,478 @@
+/*
+    http://www.JSON.org/json2.js
+    2009-04-16
+
+    Public Domain.
+
+    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
+
+    See http://www.JSON.org/js.html
+
+    This file creates a global JSON object containing two methods: stringify
+    and parse.
+
+        JSON.stringify(value, replacer, space)
+            value       any JavaScript value, usually an object or array.
+
+            replacer    an optional parameter that determines how object
+                        values are stringified for objects. It can be a
+                        function or an array of strings.
+
+            space       an optional parameter that specifies the indentation
+                        of nested structures. If it is omitted, the text will
+                        be packed without extra whitespace. If it is a number,
+                        it will specify the number of spaces to indent at each
+                        level. If it is a string (such as '\t' or '&nbsp;'),
+                        it contains the characters used to indent at each level.
+
+            This method produces a JSON text from a JavaScript value.
+
+            When an object value is found, if the object contains a toJSON
+            method, its toJSON method will be called and the result will be
+            stringified. A toJSON method does not serialize: it returns the
+            value represented by the name/value pair that should be serialized,
+            or undefined if nothing should be serialized. The toJSON method
+            will be passed the key associated with the value, and this will be
+            bound to the object holding the key.
+
+            For example, this would serialize Dates as ISO strings.
+
+                Date.prototype.toJSON = function (key) {
+                    function f(n) {
+                        // Format integers to have at least two digits.
+                        return n < 10 ? '0' + n : n;
+                    }
+
+                    return this.getUTCFullYear()   + '-' +
+                         f(this.getUTCMonth() + 1) + '-' +
+                         f(this.getUTCDate())      + 'T' +
+                         f(this.getUTCHours())     + ':' +
+                         f(this.getUTCMinutes())   + ':' +
+                         f(this.getUTCSeconds())   + 'Z';
+                };
+
+            You can provide an optional replacer method. It will be passed the
+            key and value of each member, with this bound to the containing
+            object. The value that is returned from your method will be
+            serialized. If your method returns undefined, then the member will
+            be excluded from the serialization.
+
+            If the replacer parameter is an array of strings, then it will be
+            used to select the members to be serialized. It filters the results
+            such that only members with keys listed in the replacer array are
+            stringified.
+
+            Values that do not have JSON representations, such as undefined or
+            functions, will not be serialized. Such values in objects will be
+            dropped; in arrays they will be replaced with null. You can use
+            a replacer function to replace those with JSON values.
+            JSON.stringify(undefined) returns undefined.
+
+            The optional space parameter produces a stringification of the
+            value that is filled with line breaks and indentation to make it
+            easier to read.
+
+            If the space parameter is a non-empty string, then that string will
+            be used for indentation. If the space parameter is a number, then
+            the indentation will be that many spaces.
+
+            Example:
+
+            text = JSON.stringify(['e', {pluribus: 'unum'}]);
+            // text is '["e",{"pluribus":"unum"}]'
+
+
+            text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
+            // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
+
+            text = JSON.stringify([new Date()], function (key, value) {
+                return this[key] instanceof Date ?
+                    'Date(' + this[key] + ')' : value;
+            });
+            // text is '["Date(---current time---)"]'
+
+
+        JSON.parse(text, reviver)
+            This method parses a JSON text to produce an object or array.
+            It can throw a SyntaxError exception.
+
+            The optional reviver parameter is a function that can filter and
+            transform the results. It receives each of the keys and values,
+            and its return value is used instead of the original value.
+            If it returns what it received, then the structure is not modified.
+            If it returns undefined then the member is deleted.
+
+            Example:
+
+            // Parse the text. Values that look like ISO date strings will
+            // be converted to Date objects.
+
+            myData = JSON.parse(text, function (key, value) {
+                var a;
+                if (typeof value === 'string') {
+                    a =
+/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
+                    if (a) {
+                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+                            +a[5], +a[6]));
+                    }
+                }
+                return value;
+            });
+
+            myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
+                var d;
+                if (typeof value === 'string' &&
+                        value.slice(0, 5) === 'Date(' &&
+                        value.slice(-1) === ')') {
+                    d = new Date(value.slice(5, -1));
+                    if (d) {
+                        return d;
+                    }
+                }
+                return value;
+            });
+
+
+    This is a reference implementation. You are free to copy, modify, or
+    redistribute.
+
+    This code should be minified before deployment.
+    See http://javascript.crockford.com/jsmin.html
+
+    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
+    NOT CONTROL.
+*/
+
+/*jslint evil: true */
+
+/*global JSON */
+
+/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
+    call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
+    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
+    lastIndex, length, parse, prototype, push, replace, slice, stringify,
+    test, toJSON, toString, valueOf
+*/
+
+// Create a JSON object only if one does not already exist. We create the
+// methods in a closure to avoid creating global variables.
+
+if (!this.JSON) {
+    JSON = {};
+}
+(function () {
+
+    function f(n) {
+        // Format integers to have at least two digits.
+        return n < 10 ? '0' + n : n;
+    }
+
+    if (typeof Date.prototype.toJSON !== 'function') {
+
+        Date.prototype.toJSON = function (key) {
+
+            return this.getUTCFullYear()   + '-' +
+                 f(this.getUTCMonth() + 1) + '-' +
+                 f(this.getUTCDate())      + 'T' +
+                 f(this.getUTCHours())     + ':' +
+                 f(this.getUTCMinutes())   + ':' +
+                 f(this.getUTCSeconds())   + 'Z';
+        };
+
+        String.prototype.toJSON =
+        Number.prototype.toJSON =
+        Boolean.prototype.toJSON = function (key) {
+            return this.valueOf();
+        };
+    }
+
+    var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+        escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+        gap,
+        indent,
+        meta = {    // table of character substitutions
+            '\b': '\\b',
+            '\t': '\\t',
+            '\n': '\\n',
+            '\f': '\\f',
+            '\r': '\\r',
+            '"' : '\\"',
+            '\\': '\\\\'
+        },
+        rep;
+
+
+    function quote(string) {
+
+// If the string contains no control characters, no quote characters, and no
+// backslash characters, then we can safely slap some quotes around it.
+// Otherwise we must also replace the offending characters with safe escape
+// sequences.
+
+        escapable.lastIndex = 0;
+        return escapable.test(string) ?
+            '"' + string.replace(escapable, function (a) {
+                var c = meta[a];
+                return typeof c === 'string' ? c :
+                    '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+            }) + '"' :
+            '"' + string + '"';
+    }
+
+
+    function str(key, holder) {
+
+// Produce a string from holder[key].
+
+        var i,          // The loop counter.
+            k,          // The member key.
+            v,          // The member value.
+            length,
+            mind = gap,
+            partial,
+            value = holder[key];
+
+// If the value has a toJSON method, call it to obtain a replacement value.
+
+        if (value && typeof value === 'object' &&
+                typeof value.toJSON === 'function') {
+            value = value.toJSON(key);
+        }
+
+// If we were called with a replacer function, then call the replacer to
+// obtain a replacement value.
+
+        if (typeof rep === 'function') {
+            value = rep.call(holder, key, value);
+        }
+
+// What happens next depends on the value's type.
+
+        switch (typeof value) {
+        case 'string':
+            return quote(value);
+
+        case 'number':
+
+// JSON numbers must be finite. Encode non-finite numbers as null.
+
+            return isFinite(value) ? String(value) : 'null';
+
+        case 'boolean':
+        case 'null':
+
+// If the value is a boolean or null, convert it to a string. Note:
+// typeof null does not produce 'null'. The case is included here in
+// the remote chance that this gets fixed someday.
+
+            return String(value);
+
+// If the type is 'object', we might be dealing with an object or an array or
+// null.
+
+        case 'object':
+
+// Due to a specification blunder in ECMAScript, typeof null is 'object',
+// so watch out for that case.
+
+            if (!value) {
+                return 'null';
+            }
+
+// Make an array to hold the partial results of stringifying this object value.
+
+            gap += indent;
+            partial = [];
+
+// Is the value an array?
+
+            if (Object.prototype.toString.apply(value) === '[object Array]') {
+
+// The value is an array. Stringify every element. Use null as a placeholder
+// for non-JSON values.
+
+                length = value.length;
+                for (i = 0; i < length; i += 1) {
+                    partial[i] = str(i, value) || 'null';
+                }
+
+// Join all of the elements together, separated with commas, and wrap them in
+// brackets.
+
+                v = partial.length === 0 ? '[]' :
+                    gap ? '[\n' + gap +
+                            partial.join(',\n' + gap) + '\n' +
+                                mind + ']' :
+                          '[' + partial.join(',') + ']';
+                gap = mind;
+                return v;
+            }
+
+// If the replacer is an array, use it to select the members to be stringified.
+
+            if (rep && typeof rep === 'object') {
+                length = rep.length;
+                for (i = 0; i < length; i += 1) {
+                    k = rep[i];
+                    if (typeof k === 'string') {
+                        v = str(k, value);
+                        if (v) {
+                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
+                        }
+                    }
+                }
+            } else {
+
+// Otherwise, iterate through all of the keys in the object.
+
+                for (k in value) {
+                    if (Object.hasOwnProperty.call(value, k)) {
+                        v = str(k, value);
+                        if (v) {
+                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
+                        }
+                    }
+                }
+            }
+
+// Join all of the member texts together, separated with commas,
+// and wrap them in braces.
+
+            v = partial.length === 0 ? '{}' :
+                gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
+                        mind + '}' : '{' + partial.join(',') + '}';
+            gap = mind;
+            return v;
+        }
+    }
+
+// If the JSON object does not yet have a stringify method, give it one.
+
+    if (typeof JSON.stringify !== 'function') {
+        JSON.stringify = function (value, replacer, space) {
+
+// The stringify method takes a value and an optional replacer, and an optional
+// space parameter, and returns a JSON text. The replacer can be a function
+// that can replace values, or an array of strings that will select the keys.
+// A default replacer method can be provided. Use of the space parameter can
+// produce text that is more easily readable.
+
+            var i;
+            gap = '';
+            indent = '';
+
+// If the space parameter is a number, make an indent string containing that
+// many spaces.
+
+            if (typeof space === 'number') {
+                for (i = 0; i < space; i += 1) {
+                    indent += ' ';
+                }
+
+// If the space parameter is a string, it will be used as the indent string.
+
+            } else if (typeof space === 'string') {
+                indent = space;
+            }
+
+// If there is a replacer, it must be a function or an array.
+// Otherwise, throw an error.
+
+            rep = replacer;
+            if (replacer && typeof replacer !== 'function' &&
+                    (typeof replacer !== 'object' ||
+                     typeof replacer.length !== 'number')) {
+                throw new Error('JSON.stringify');
+            }
+
+// Make a fake root object containing our value under the key of ''.
+// Return the result of stringifying the value.
+
+            return str('', {'': value});
+        };
+    }
+
+
+// If the JSON object does not yet have a parse method, give it one.
+
+    if (typeof JSON.parse !== 'function') {
+        JSON.parse = function (text, reviver) {
+
+// The parse method takes a text and an optional reviver function, and returns
+// a JavaScript value if the text is a valid JSON text.
+
+            var j;
+
+            function walk(holder, key) {
+
+// The walk method is used to recursively walk the resulting structure so
+// that modifications can be made.
+
+                var k, v, value = holder[key];
+                if (value && typeof value === 'object') {
+                    for (k in value) {
+                        if (Object.hasOwnProperty.call(value, k)) {
+                            v = walk(value, k);
+                            if (v !== undefined) {
+                                value[k] = v;
+                            } else {
+                                delete value[k];
+                            }
+                        }
+                    }
+                }
+                return reviver.call(holder, key, value);
+            }
+
+
+// Parsing happens in four stages. In the first stage, we replace certain
+// Unicode characters with escape sequences. JavaScript handles many characters
+// incorrectly, either silently deleting them, or treating them as line endings.
+
+            cx.lastIndex = 0;
+            if (cx.test(text)) {
+                text = text.replace(cx, function (a) {
+                    return '\\u' +
+                        ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+                });
+            }
+
+// In the second stage, we run the text against regular expressions that look
+// for non-JSON patterns. We are especially concerned with '()' and 'new'
+// because they can cause invocation, and '=' because it can cause mutation.
+// But just to be safe, we want to reject all unexpected forms.
+
+// We split the second stage into 4 regexp operations in order to work around
+// crippling inefficiencies in IE's and Safari's regexp engines. First we
+// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
+// replace all simple value tokens with ']' characters. Third, we delete all
+// open brackets that follow a colon or comma or that begin the text. Finally,
+// we look to see that the remaining characters are only whitespace or ']' or
+// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
+
+            if (/^[\],:{}\s]*$/.
+test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
+replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
+replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
+
+// In the third stage we use the eval function to compile the text into a
+// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
+// in JavaScript: it can begin a block or an object literal. We wrap the text
+// in parens to eliminate the ambiguity.
+
+                j = eval('(' + text + ')');
+
+// In the optional fourth stage, we recursively walk the new structure, passing
+// each name/value pair to a reviver function for possible transformation.
+
+                return typeof reviver === 'function' ?
+                    walk({'': j}, '') : j;
+            }
+
+// If the text is not JSON parseable, then a SyntaxError is thrown.
+
+            throw new SyntaxError('JSON.parse');
+        };
+    }
+}());
diff --git a/plugins/Comet/updatetimeline.js b/plugins/Comet/updatetimeline.js
new file mode 100644 (file)
index 0000000..de750ba
--- /dev/null
@@ -0,0 +1,77 @@
+// update the local timeline from a Comet server
+//
+
+var updater = function()
+{
+     var _cometd;
+
+     return {
+          init: function(server, timeline)
+          {
+               _cometd = $.cometd; // Uses the default Comet object
+               _cometd.setLogLevel('debug');
+               _cometd.init(server);
+               _cometd.subscribe(timeline, receive);
+               $(window).unload(leave);
+          }
+     }
+
+     function leave()
+     {
+          _cometd.disconnect();
+     }
+
+     function receive(message)
+     {
+          id = message.data.id;
+
+          // Don't add it if it already exists
+
+          if ($("#notice-"+id).length > 0) {
+               return;
+          }
+
+          var noticeItem = makeNoticeItem(message.data);
+          $("#notices_primary .notices").prepend(noticeItem, true);
+          $("#notices_primary .notice:first").css({display:"none"});
+          $("#notices_primary .notice:first").fadeIn(2500);
+          NoticeHover();
+          NoticeReply();
+     }
+
+     function makeNoticeItem(data)
+     {
+          user = data['user'];
+          html = data['html'].replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
+
+          ni = "<li class=\"hentry notice\" id=\"notice-"+data['id']+"\">"+
+               "<div class=\"entry-title\">"+
+               "<span class=\"vcard author\">"+
+               "<a href=\""+user['profile_url']+"\" class=\"url\">"+
+               "<img src=\""+user['profile_image_url']+"\" class=\"avatar photo\" width=\"48\" height=\"48\" alt=\""+user['screen_name']+"\"/>"+
+               "<span class=\"nickname fn\">"+user['screen_name']+"</span>"+
+               "</a>"+
+               "</span>"+
+               "<p class=\"entry-content\">"+html+"</p>"+
+               "</div>"+
+               "<div class=\"entry-content\">"+
+               "<dl class=\"timestamp\">"+
+               "<dt>Published</dt>"+
+               "<dd>"+
+               "<a rel=\"bookmark\" href=\""+data['url']+"\" >"+
+               "<abbr class=\"published\" title=\""+data['created_at']+"\">a few seconds ago</abbr>"+
+               "</a> "+
+               "</dd>"+
+               "</dl>"+
+               "<dl class=\"device\">"+
+               "<dt>From</dt> "+
+               "<dd>"+data['source']+"</dd>"+
+               "</dl>"+
+               "</div>"+
+               "<div class=\"notice-options\">"+
+               "</div>"+
+               "</li>";
+          return ni;
+     }
+}();
+
index 881ead99ec29d091d5a13729b9fa1d3eb8f87076..93a0294c4ca9cbd23cd720c4909089b667494136 100644 (file)
@@ -121,6 +121,12 @@ class LinkbackPlugin extends Plugin
     {
         $args = array($this->notice->uri, $url);
 
+        if (!extension_loaded('xmlrpc')) {
+            if (!dl('xmlrpc.so')) {
+                common_log(LOG_ERR, "Can't pingback; xmlrpc extension not available.");
+            }
+        }
+
         $request = xmlrpc_encode_request('pingback.ping', $args);
         $context = stream_context_create(array('http' => array('method' => "POST",
                                                                'header' =>
@@ -141,7 +147,7 @@ class LinkbackPlugin extends Plugin
     }
 
     // Largely cadged from trackback_cls.php by
-    // Ran Aroussi <ran@blogish.org>, GPL2
+    // Ran Aroussi <ran@blogish.org>, GPL2 or any later version
     // http://phptrackback.sourceforge.net/
 
     function getTrackback($text, $url)
diff --git a/scripts/getvaliddaemons.php b/scripts/getvaliddaemons.php
new file mode 100755 (executable)
index 0000000..482e63a
--- /dev/null
@@ -0,0 +1,52 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Utility script to get a list of daemons that should run, based on the
+ * current configuration. This is used by startdaemons.sh to determine what
+ * it should and shouldn't start up. The output is a list of space-separated
+ * daemon names.
+ */
+
+
+# Abort if called from a web server
+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');
+
+if(common_config('xmpp','enabled')) {
+    echo "xmppdaemon.php jabberqueuehandler.php publicqueuehandler.php ";
+    echo "xmppconfirmhandler.php ";
+}
+if(common_config('memcached','enabled')) {
+    echo "memcachedqueuehandler.php ";
+}
+echo "ombqueuehandler.php ";
+echo "twitterqueuehandler.php ";
+echo "facebookqueuehandler.php ";
+echo "pingqueuehandler.php ";
+echo "inboxqueuehandler.php ";
+echo "smsqueuehandler.php ";
diff --git a/scripts/inboxqueuehandler.php b/scripts/inboxqueuehandler.php
new file mode 100755 (executable)
index 0000000..73d31e8
--- /dev/null
@@ -0,0 +1,69 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008,2009 Control Yourself, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// Abort if called from a web server
+
+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');
+require_once(INSTALLDIR . '/lib/queuehandler.php');
+
+set_error_handler('common_error_handler');
+
+class InboxQueueHandler extends QueueHandler
+{
+    function transport()
+    {
+        return 'inbox';
+    }
+
+       function start() {
+               $this->log(LOG_INFO, "INITIALIZE");
+               return true;
+       }
+
+    function handle_notice($notice)
+    {
+        $this->log(LOG_INFO, "Distributing notice to inboxes for $notice->id");
+        $notice->addToInboxes();
+        $notice->blowSubsCache();
+        return true;
+    }
+
+       function finish() {
+       }
+}
+
+ini_set("max_execution_time", "0");
+ini_set("max_input_time", "0");
+set_time_limit(0);
+mb_internal_encoding('UTF-8');
+
+$id = ($argc > 1) ? $argv[1] : null;
+
+$handler = new InboxQueueHandler($id);
+
+$handler->runOnce();
diff --git a/scripts/memcachedqueuehandler.php b/scripts/memcachedqueuehandler.php
new file mode 100755 (executable)
index 0000000..185b781
--- /dev/null
@@ -0,0 +1,70 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008,2009 Control Yourself, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// Abort if called from a web server
+
+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');
+require_once(INSTALLDIR . '/lib/queuehandler.php');
+
+set_error_handler('common_error_handler');
+
+class MemcachedQueueHandler extends QueueHandler
+{
+    function transport()
+    {
+        return 'memcache';
+    }
+
+       function start() {
+               $this->log(LOG_INFO, "INITIALIZE");
+               return true;
+       }
+
+    function handle_notice($notice)
+    {
+        // XXX: fork here
+        $this->log(LOG_INFO, "Blowing memcached for $notice->id");
+        $notice->blowCaches();
+        return true;
+    }
+
+       function finish() {
+       }
+
+}
+
+ini_set("max_execution_time", "0");
+ini_set("max_input_time", "0");
+set_time_limit(0);
+mb_internal_encoding('UTF-8');
+
+$id = ($argc > 1) ? $argv[1] : null;
+
+$handler = new MemcachedQueueHandler($id);
+
+$handler->runOnce();
index c3729761d06a65f63455b76fc6df1d12e702d58a..3869e95c4c3cf5ae2735f54dbff600349af6f7d8 100755 (executable)
 # Note that the 'maildaemon' needs to run as a mail filter.
 
 DIR=`dirname $0`
+DAEMONS=`php $DIR/getvaliddaemons.php`
 
-for f in xmppdaemon.php jabberqueuehandler.php publicqueuehandler.php \
-         xmppconfirmhandler.php smsqueuehandler.php ombqueuehandler.php \
-         twitterqueuehandler.php facebookqueuehandler.php pingqueuehandler.php; do
+for f in $DAEMONS; do
 
          echo -n "Starting $f...";
         php $DIR/$f
index 2bb8f9ecb2a6d02c7b66b443c35778c17b95de72..f6d71eddfb466370bf6d0fd91850ff3667951f84 100755 (executable)
@@ -24,7 +24,8 @@ SDIR=`dirname $0`
 DIR=`php $SDIR/getpiddir.php`
 
 for f in jabberhandler ombhandler publichandler smshandler pinghandler \
-        xmppconfirmhandler xmppdaemon twitterhandler facebookhandler ; do
+        xmppconfirmhandler xmppdaemon twitterhandler facebookhandler \
+         memcachehandler inboxhandler; do
 
        FILES="$DIR/$f.*.pid"
        for ff in "$FILES" ; do
index 0ce2f2a28008397867de795afd36d1b0cf943cea..b79fa1b3ba0145101e167ddafea80fdd67ca3fc8 100755 (executable)
@@ -152,11 +152,6 @@ class XMPPDaemon extends Daemon
             $body = preg_replace('/d[\ ]*('. $to .')[\ ]*/', '', $pl['body']);
             $this->add_direct($user, $body, $to, $from);
         } else {
-            $len = mb_strlen($pl['body']);
-            if($len > 140) {
-                $this->from_site($from, 'Message too long - maximum is 140 characters, you sent ' . $len);
-                return;
-            }
             $this->add_notice($user, $pl);
         }
 
@@ -255,15 +250,13 @@ class XMPPDaemon extends Daemon
     function add_notice(&$user, &$pl)
     {
         $body = trim($pl['body']);
-        $content_shortened = common_shorten_link($body);
+        $content_shortened = common_shorten_links($body);
         if (mb_strlen($content_shortened) > 140) {
-            $content = trim(mb_substr($body, 0, 140));
-            $content_shortened = common_shorten_link($content);
-        }
-        else {
-            $content = $body;
+          $from = jabber_normalize_jid($pl['from']);
+          $this->from_site($from, "Message too long - maximum is 140 characters, you sent ".mb_strlen($content_shortened));
+          return;
         }
-        $notice = Notice::saveNew($user->id, $content, 'xmpp');
+        $notice = Notice::saveNew($user->id, $content_shortened, 'xmpp');
         if (is_string($notice)) {
             $this->log(LOG_ERR, $notice);
             return;
index a6fe8eb754f6c53e81636177e6132435a1830df7..efa5f4ac6969cc462b01969dc5758c12ffedd29b 100644 (file)
@@ -86,7 +86,7 @@ border:0;
 
 .error,
 .success {
-padding:4px 7px;
+padding:4px 1.55%;
 border-radius:4px;
 -moz-border-radius:4px;
 -webkit-border-radius:4px;
@@ -426,6 +426,7 @@ line-height:1;
 #form_notice fieldset {
 border:0;
 padding:0;
+position:relative;
 }
 #form_notice legend {
 display:none;
@@ -480,7 +481,13 @@ margin-bottom:7px;
 margin-left:18px;
 float:left;
 }
-
+#form_notice .error {
+float:left;
+clear:both;
+width:96.9%;
+margin-bottom:0;
+line-height:1.618;
+}
 
 /* entity_profile */
 .entity_profile {
@@ -835,23 +842,6 @@ text-transform:lowercase;
 }
 
 
-
-.notice-data {
-position:absolute;
-top:18px;
-right:0;
-min-height:50px;
-margin-bottom:4px;
-}
-.notice .entry-content .notice-data dt {
-display:none;
-}
-
-.notice-data a {
-display:block;
-outline:none;
-}
-
 .notice-options {
 padding-left:2%;
 float:left;
@@ -1029,6 +1019,8 @@ padding-right:30px;
 .hentry .entry-content p {
 margin-bottom:18px;
 }
+.system_notice ul,
+.instructions ul,
 .hentry entry-content ol,
 .hentry .entry-content ul {
 list-style-position:inside;