From 51ac34e80c5a99008b1a945b2c00b6dbfdde1529 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 26 Jul 2009 13:06:38 -0600 Subject: first version of deleting users --- classes/Profile.php | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++ classes/User.php | 43 ++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/classes/Profile.php b/classes/Profile.php index f926b2cef..0ee6fa657 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -461,4 +461,79 @@ class Profile extends Memcached_DataObject $c->delete(common_cache_key('profile:notice_count:'.$this->id)); } } + + function delete() + { + $this->_deleteNotices(); + $this->_deleteSubscriptions(); + $this->_deleteMessages(); + $this->_deleteTags(); + $this->_deleteBlocks(); + + $related = array('Avatar', + 'Reply', + 'Group_member', + ); + + foreach ($related as $cls) { + $inst = new $cls(); + $inst->profile_id = $this->id; + $inst->delete(); + } + + parent::delete(); + } + + function _deleteNotices() + { + $notice = new Notice(); + $notice->profile_id = $this->id; + + if ($notice->find()) { + while ($notice->fetch()) { + $other = clone($notice); + $other->delete(); + } + } + } + + function _deleteSubscriptions() + { + $sub = new Subscription(); + $sub->subscriber = $this->id; + $sub->delete(); + + $subd = new Subscription(); + $subd->subscribed = $this->id; + $subd->delete(); + } + + function _deleteMessages() + { + $msg = new Message(); + $msg->from_profile = $this->id; + $msg->delete(); + + $msg = new Message(); + $msg->to_profile = $this->id; + $msg->delete(); + } + + function _deleteTags() + { + $tag = new Profile_tag(); + $tag->tagged = $this->id; + $msg->delete(); + } + + function _deleteBlocks() + { + $block = new Profile_block(); + $block->blocked = $this->id; + $block->delete(); + + $block = new Group_block(); + $block->blocked = $this->id; + $block->delete(); + } } diff --git a/classes/User.php b/classes/User.php index bea47a3b0..991e9c18f 100644 --- a/classes/User.php +++ b/classes/User.php @@ -685,4 +685,47 @@ class User extends Memcached_DataObject { return Design::staticGet('id', $this->design_id); } + + function delete() + { + $profile = $this->getProfile(); + $profile->delete(); + + $related = array('Fave', + 'User_openid', + 'Confirm_address', + 'Remember_me', + 'Foreign_link', + 'Invitation', + ); + + if (common_config('inboxes', 'enabled')) { + $related[] = 'Notice_inbox'; + } + + foreach ($related as $cls) { + $inst = new $cls(); + $inst->user_id = $this->id; + $inst->delete(); + } + + $this->_deleteTags(); + + parent::delete(); + } + + function _deleteTags() + { + $tag = new Profile_tag(); + $tag->tagger = $this->id; + $tag->delete(); + } + + function _deleteBlocks() + { + $block = new Profile_block(); + $block->blocker = $this->id; + $block->delete(); + // XXX delete group block? Reset blocker? + } } -- cgit v1.2.3-54-g00ecf From 94e3f6bb092486df99034064c0e7d553bcf7d180 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 2 Oct 2009 15:29:57 -0400 Subject: also delete blocks --- classes/User.php | 1 + 1 file changed, 1 insertion(+) diff --git a/classes/User.php b/classes/User.php index ef8434292..007662131 100644 --- a/classes/User.php +++ b/classes/User.php @@ -714,6 +714,7 @@ class User extends Memcached_DataObject } $this->_deleteTags(); + $this->_deleteBlocks(); parent::delete(); } -- cgit v1.2.3-54-g00ecf From be513db013b6079d3ff32fdb0bb332cdbace1798 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 2 Oct 2009 15:42:34 -0400 Subject: copy-and-paste typo in profile deletion --- classes/Profile.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/Profile.php b/classes/Profile.php index 463802b4e..8385ebf88 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -523,7 +523,7 @@ class Profile extends Memcached_DataObject { $tag = new Profile_tag(); $tag->tagged = $this->id; - $msg->delete(); + $tag->delete(); } function _deleteBlocks() -- cgit v1.2.3-54-g00ecf From 54b22c0c0bb1e7f5fbbcf9035219b5ed07e1b80c Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 2 Oct 2009 15:42:58 -0400 Subject: script to permanently delete a user --- scripts/deleteuser.php | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 scripts/deleteuser.php diff --git a/scripts/deleteuser.php b/scripts/deleteuser.php new file mode 100644 index 000000000..67aea7921 --- /dev/null +++ b/scripts/deleteuser.php @@ -0,0 +1,68 @@ +#!/usr/bin/env php +. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); + +$shortoptions = 'i::n::y'; +$longoptions = array('id::nickname::yes'); + +$helptext = <<nickname}' ({$user->id}). Are you sure? [y/N] "; + $response = fgets(STDIN); + if (strtolower($response) != 'y') { + print "Aborting.\n"; + exit(0); + } +} + +print "Deleting..."; +$user->delete(); +print "DONE.\n"; -- cgit v1.2.3-54-g00ecf From 2d85d619074b69ff0d31249c45715af686b13986 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 2 Oct 2009 15:46:00 -0400 Subject: better handling of y response in deleteuser.php --- scripts/deleteuser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deleteuser.php b/scripts/deleteuser.php index 67aea7921..52389123c 100644 --- a/scripts/deleteuser.php +++ b/scripts/deleteuser.php @@ -57,7 +57,7 @@ if (have_option('i', 'id')) { if (!have_option('y', 'yes')) { print "About to PERMANENTLY delete user '{$user->nickname}' ({$user->id}). Are you sure? [y/N] "; $response = fgets(STDIN); - if (strtolower($response) != 'y') { + if (strtolower(trim($response)) != 'y') { print "Aborting.\n"; exit(0); } -- cgit v1.2.3-54-g00ecf From 0743ca125902ab2d40f324063b5a0552b1e28f72 Mon Sep 17 00:00:00 2001 From: Brenda Wallace Date: Sat, 3 Oct 2009 17:36:53 +1300 Subject: indent fix --- install.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/install.php b/install.php index c2a5bb29e..69f025a6a 100644 --- a/install.php +++ b/install.php @@ -1,3 +1,4 @@ + * @author Tom Adams * @license GNU Affero General Public License http://www.gnu.org/licenses/ - * @version 0.9 + * @version 0.9.x * @link http://status.net */ @@ -340,13 +341,13 @@ function checkExtension($name) if (extension_loaded($name)) { return true; } elseif (function_exists('dl') && ini_get('enable_dl') && !ini_get('safe_mode')) { - // dl will throw a fatal error if it's disabled or we're in safe mode. - // More fun, it may not even exist under some SAPIs in 5.3.0 or later... - $soname = $name . '.' . PHP_SHLIB_SUFFIX; - if (PHP_SHLIB_SUFFIX == 'dll') { - $soname = "php_" . $soname; - } - return @dl($soname); + // dl will throw a fatal error if it's disabled or we're in safe mode. + // More fun, it may not even exist under some SAPIs in 5.3.0 or later... + $soname = $name . '.' . PHP_SHLIB_SUFFIX; + if (PHP_SHLIB_SUFFIX == 'dll') { + $soname = "php_" . $soname; + } + return @dl($soname); } else { return false; } -- cgit v1.2.3-54-g00ecf From 852f0c59a448b3c0dc07a928924cce6ce3399e70 Mon Sep 17 00:00:00 2001 From: Brenda Wallace Date: Sat, 3 Oct 2009 17:40:53 +1300 Subject: added doxygen --- index.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/index.php b/index.php index 51e30f578..a4d1fcf4d 100644 --- a/index.php +++ b/index.php @@ -61,6 +61,9 @@ function getPath($req) } } +/** + * logs and then displays error messages + */ function handleError($error) { if ($error->getCode() == DB_DATAOBJECT_ERROR_NODATA) { -- cgit v1.2.3-54-g00ecf From 00c0becd56c1185f26524828be4722ffd8f05712 Mon Sep 17 00:00:00 2001 From: Brenda Wallace Date: Sat, 3 Oct 2009 17:48:38 +1300 Subject: more doxygen --- index.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.php b/index.php index a4d1fcf4d..644812bd5 100644 --- a/index.php +++ b/index.php @@ -63,6 +63,8 @@ function getPath($req) /** * logs and then displays error messages + * + * @return void */ function handleError($error) { -- cgit v1.2.3-54-g00ecf From 5528c0cd3d66c1acd7bf25b26833e8a107641f35 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Sat, 3 Oct 2009 22:06:51 +0000 Subject: Using CDATA for autofocus script --- lib/htmloutputter.php | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php index 64be745be..2ff9380cc 100644 --- a/lib/htmloutputter.php +++ b/lib/htmloutputter.php @@ -427,16 +427,12 @@ class HTMLOutputter extends XMLOutputter function autofocus($id) { $this->elementStart('script', array('type' => 'text/javascript')); - $this->raw(' - - '); + $this->raw('/**/'); $this->elementEnd('script'); } } -- cgit v1.2.3-54-g00ecf From bcdf31c639d7a50a7eefd16706fa0d9d6f33601f Mon Sep 17 00:00:00 2001 From: Brenda Wallace Date: Sun, 4 Oct 2009 11:14:26 +1300 Subject: added all them doxygens --- classes/Notice.php | 825 +++++++++++++++++++++++++++-------------------------- 1 file changed, 421 insertions(+), 404 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index 93d5de790..c68098c65 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1,5 +1,5 @@ . + * + * @category Notices + * @package StatusNet + * @author Brenda Wallace + * @author Christopher Vollick + * @author CiaranG + * @author Craig Andrews + * @author Evan Prodromou + * @author Gina Haeussge + * @author Jeffery To + * @author Mike Cochrane + * @author Robin Millette + * @author Sarven Capadisli + * @author Tom Adams + * @license GNU Affero General Public License http://www.gnu.org/licenses/ */ -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} /** - * Table Definition for notice - */ +* Table Definition for notice +*/ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; /* We keep the first three 20-notice pages, plus one for pagination check, @@ -35,7 +52,7 @@ class Notice extends Memcached_DataObject { ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ - + public $__table = 'notice'; // table name public $id; // int(4) primary_key not_null public $profile_id; // int(4) not_null @@ -49,45 +66,45 @@ class Notice extends Memcached_DataObject public $is_local; // tinyint(1) public $source; // varchar(32) public $conversation; // int(4) - + /* Static get */ function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Notice',$k,$v); } - + /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - + /* Notice types */ const LOCAL_PUBLIC = 1; const REMOTE_OMB = 0; const LOCAL_NONPUBLIC = -1; const GATEWAY = -2; - + function getProfile() { return Profile::staticGet('id', $this->profile_id); } - + function delete() { $this->blowCaches(true); $this->blowFavesCache(true); $this->blowSubsCache(true); - + // For auditing purposes, save a record that the notice // was deleted. - + $deleted = new Deleted_notice(); - + $deleted->id = $this->id; $deleted->profile_id = $this->profile_id; $deleted->uri = $this->uri; $deleted->created = $this->created; $deleted->deleted = common_sql_now(); - + $this->query('BEGIN'); - + $deleted->insert(); - + //Null any notices that are replies to this notice $this->query(sprintf("UPDATE notice set reply_to = null WHERE reply_to = %d", $this->id)); $related = array('Reply', @@ -106,7 +123,7 @@ class Notice extends Memcached_DataObject $result = parent::delete(); $this->query('COMMIT'); } - + function saveTags() { /* extract all #hastags */ @@ -114,14 +131,14 @@ class Notice extends Memcached_DataObject if (!$count) { return true; } - + //turn each into their canonical tag //this is needed to remove dupes before saving e.g. #hash.tag = #hashtag $hashtags = array(); for($i=0; $itag = $hashtag; $tag->created = $this->created; $id = $tag->insert(); - + if (!$id) { throw new ServerException(sprintf(_('DB error inserting hashtag: %s'), $last_error->message)); return; } } - + static function saveNew($profile_id, $content, $source=null, $is_local=Notice::LOCAL_PUBLIC, $reply_to=null, $uri=null, $created=null) { - - $profile = Profile::staticGet($profile_id); - - $final = common_shorten_links($content); - - if (Notice::contentTooLong($final)) { - throw new ClientException(_('Problem saving notice. Too long.')); - } - - if (!$profile) { - throw new ClientException(_('Problem saving notice. Unknown user.')); - } - - if (common_config('throttle', 'enabled') && !Notice::checkEditThrottle($profile_id)) { - common_log(LOG_WARNING, 'Excessive posting by profile #' . $profile_id . '; throttled.'); - throw new ClientException(_('Too many notices too fast; take a breather '. + + $profile = Profile::staticGet($profile_id); + + $final = common_shorten_links($content); + + if (Notice::contentTooLong($final)) { + throw new ClientException(_('Problem saving notice. Too long.')); + } + + if (!$profile) { + throw new ClientException(_('Problem saving notice. Unknown user.')); + } + + if (common_config('throttle', 'enabled') && !Notice::checkEditThrottle($profile_id)) { + common_log(LOG_WARNING, 'Excessive posting by profile #' . $profile_id . '; throttled.'); + throw new ClientException(_('Too many notices too fast; take a breather '. 'and post again in a few minutes.')); - } - - if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) { - common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.'); - throw new ClientException(_('Too many duplicate messages too quickly;'. + } + + if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) { + common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.'); + throw new ClientException(_('Too many duplicate messages too quickly;'. ' take a breather and post again in a few minutes.')); - } - - $banned = common_config('profile', 'banned'); - - if ( in_array($profile_id, $banned) || in_array($profile->nickname, $banned)) { - common_log(LOG_WARNING, "Attempted post from banned user: $profile->nickname (user id = $profile_id)."); - throw new ClientException(_('You are banned from posting notices on this site.')); - } - - $notice = new Notice(); - $notice->profile_id = $profile_id; - - $blacklist = common_config('public', 'blacklist'); - $autosource = common_config('public', 'autosource'); - + } + + $banned = common_config('profile', 'banned'); + + if ( in_array($profile_id, $banned) || in_array($profile->nickname, $banned)) { + common_log(LOG_WARNING, "Attempted post from banned user: $profile->nickname (user id = $profile_id)."); + throw new ClientException(_('You are banned from posting notices on this site.')); + } + + $notice = new Notice(); + $notice->profile_id = $profile_id; + + $blacklist = common_config('public', 'blacklist'); + $autosource = common_config('public', 'autosource'); + # Blacklisted are non-false, but not 1, either - - if (($blacklist && in_array($profile_id, $blacklist)) || - ($source && $autosource && in_array($source, $autosource))) { - $notice->is_local = Notice::LOCAL_NONPUBLIC; - } else { - $notice->is_local = $is_local; - } - - if (!empty($created)) { - $notice->created = $created; - } else { - $notice->created = common_sql_now(); - } - - $notice->content = $final; - $notice->rendered = common_render_content($final, $notice); - $notice->source = $source; - $notice->uri = $uri; - - $notice->reply_to = self::getReplyTo($reply_to, $profile_id, $source, $final); - - if (!empty($notice->reply_to)) { - $reply = Notice::staticGet('id', $notice->reply_to); - $notice->conversation = $reply->conversation; - } - - if (Event::handle('StartNoticeSave', array(&$notice))) { - + + if (($blacklist && in_array($profile_id, $blacklist)) || + ($source && $autosource && in_array($source, $autosource))) { + $notice->is_local = Notice::LOCAL_NONPUBLIC; + } else { + $notice->is_local = $is_local; + } + + if (!empty($created)) { + $notice->created = $created; + } else { + $notice->created = common_sql_now(); + } + + $notice->content = $final; + $notice->rendered = common_render_content($final, $notice); + $notice->source = $source; + $notice->uri = $uri; + + $notice->reply_to = self::getReplyTo($reply_to, $profile_id, $source, $final); + + if (!empty($notice->reply_to)) { + $reply = Notice::staticGet('id', $notice->reply_to); + $notice->conversation = $reply->conversation; + } + + if (Event::handle('StartNoticeSave', array(&$notice))) { + // XXX: some of these functions write to the DB - - $notice->query('BEGIN'); - - $id = $notice->insert(); - - if (!$id) { - common_log_db_error($notice, 'INSERT', __FILE__); - throw new ServerException(_('Problem saving notice.')); - } - + + $notice->query('BEGIN'); + + $id = $notice->insert(); + + if (!$id) { + common_log_db_error($notice, 'INSERT', __FILE__); + throw new ServerException(_('Problem saving notice.')); + } + // Update ID-dependent columns: URI, conversation - - $orig = clone($notice); - - $changed = false; - - if (empty($uri)) { - $notice->uri = common_notice_uri($notice); - $changed = true; - } - + + $orig = clone($notice); + + $changed = false; + + if (empty($uri)) { + $notice->uri = common_notice_uri($notice); + $changed = true; + } + // If it's not part of a conversation, it's // the beginning of a new conversation. - - if (empty($notice->conversation)) { - $notice->conversation = $notice->id; - $changed = true; - } - - if ($changed) { - if (!$notice->update($orig)) { - common_log_db_error($notice, 'UPDATE', __FILE__); - throw new ServerException(_('Problem saving notice.')); - } - } - + + if (empty($notice->conversation)) { + $notice->conversation = $notice->id; + $changed = true; + } + + if ($changed) { + if (!$notice->update($orig)) { + common_log_db_error($notice, 'UPDATE', __FILE__); + throw new ServerException(_('Problem saving notice.')); + } + } + // XXX: do we need to change this for remote users? - - $notice->saveReplies(); - $notice->saveTags(); - - $notice->addToInboxes(); - - $notice->saveUrls(); - - $notice->query('COMMIT'); - - Event::handle('EndNoticeSave', array($notice)); - } - + + $notice->saveReplies(); + $notice->saveTags(); + + $notice->addToInboxes(); + + $notice->saveUrls(); + + $notice->query('COMMIT'); + + Event::handle('EndNoticeSave', array($notice)); + } + # Clear the cache for subscribed users, so they'll update at next request # XXX: someone clever could prepend instead of clearing the cache - - $notice->blowCaches(); - - return $notice; - } - + + $notice->blowCaches(); + + return $notice; + } + /** save all urls in the notice to the db - * - * follow redirects and save all available file information - * (mimetype, date, size, oembed, etc.) - * - * @return void - */ - function saveUrls() { - common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id); - } - + * + * follow redirects and save all available file information + * (mimetype, date, size, oembed, etc.) + * + * @return void + */ + function saveUrls() { + common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id); + } + function saveUrl($data) { list($url, $notice_id) = $data; File::processNew($url, $notice_id); } - + static function checkDupes($profile_id, $content) { $profile = Profile::staticGet($profile_id); if (!$profile) { @@ -311,14 +328,14 @@ class Notice extends Memcached_DataObject $notice->profile_id = $profile_id; $notice->content = $content; if (common_config('db','type') == 'pgsql') - $notice->whereAdd('extract(epoch from now() - created) < ' . common_config('site', 'dupelimit')); + $notice->whereAdd('extract(epoch from now() - created) < ' . common_config('site', 'dupelimit')); else - $notice->whereAdd('now() - created < ' . common_config('site', 'dupelimit')); - + $notice->whereAdd('now() - created < ' . common_config('site', 'dupelimit')); + $cnt = $notice->count(); return ($cnt == 0); } - + static function checkEditThrottle($profile_id) { $profile = Profile::staticGet($profile_id); if (!$profile) { @@ -336,7 +353,7 @@ class Notice extends Memcached_DataObject # Either not N notices in the stream, OR the Nth was not posted within timespan seconds return true; } - + function getUploadedAttachment() { $post = clone $this; $query = 'select file.url as up, file.id as i from file join file_to_post on file.id = file_id where post_id=' . $post->escape($post->id) . ' and url like "%/notice/%/file"'; @@ -350,7 +367,7 @@ class Notice extends Memcached_DataObject $post->free(); return $ret; } - + function hasAttachments() { $post = clone $this; $query = "select count(file_id) as n_attachments from file join file_to_post on (file_id = file.id) join notice on (post_id = notice.id) where post_id = " . $post->escape($post->id); @@ -360,7 +377,7 @@ class Notice extends Memcached_DataObject $post->free(); return $n_attachments; } - + function attachments() { // XXX: cache this $att = array(); @@ -374,7 +391,7 @@ class Notice extends Memcached_DataObject } return $att; } - + function blowCaches($blowLast=false) { $this->blowSubsCache($blowLast); @@ -387,7 +404,7 @@ class Notice extends Memcached_DataObject $profile = Profile::staticGet($this->profile_id); $profile->blowNoticeCount(); } - + function blowConversationCache($blowLast=false) { $cache = common_memcache(); @@ -399,7 +416,7 @@ class Notice extends Memcached_DataObject } } } - + function blowGroupCache($blowLast=false) { $cache = common_memcache(); @@ -428,7 +445,7 @@ class Notice extends Memcached_DataObject unset($group_inbox); } } - + function blowTagCache($blowLast=false) { $cache = common_memcache(); @@ -439,7 +456,7 @@ class Notice extends Memcached_DataObject while ($tag->fetch()) { $tag->blowCache($blowLast); $ck = 'profile:notice_ids_tagged:' . $this->profile_id . ':' . $tag->tag; - + $cache->delete($ck); if ($blowLast) { $cache->delete($ck . ';last'); @@ -450,19 +467,19 @@ class Notice extends Memcached_DataObject unset($tag); } } - + function blowSubsCache($blowLast=false) { $cache = common_memcache(); if ($cache) { $user = new User(); - + $UT = common_config('db','type')=='pgsql'?'"user"':'user'; $user->query('SELECT id ' . - + "FROM $UT JOIN subscription ON $UT.id = subscription.subscriber " . 'WHERE subscription.subscribed = ' . $this->profile_id); - + while ($user->fetch()) { $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id)); $cache->delete(common_cache_key('notice_inbox:by_user_own:'.$user->id)); @@ -475,7 +492,7 @@ class Notice extends Memcached_DataObject unset($user); } } - + function blowNoticeCache($blowLast=false) { if ($this->is_local) { @@ -488,7 +505,7 @@ class Notice extends Memcached_DataObject } } } - + function blowRepliesCache($blowLast=false) { $cache = common_memcache(); @@ -507,7 +524,7 @@ class Notice extends Memcached_DataObject unset($reply); } } - + function blowPublicCache($blowLast=false) { if ($this->is_local == Notice::LOCAL_PUBLIC) { @@ -520,7 +537,7 @@ class Notice extends Memcached_DataObject } } } - + function blowFavesCache($blowLast=false) { $cache = common_memcache(); @@ -541,14 +558,14 @@ class Notice extends Memcached_DataObject unset($fave); } } - + # XXX: too many args; we need to move to named params or even a separate # class for notice streams - + static function getStream($qry, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0, $order=null, $since=null) { - + if (common_config('memcached', 'enabled')) { - + # Skip the cache if this is a since, since_id or max_id qry if ($since_id > 0 || $max_id > 0 || $since) { return Notice::getStreamDirect($qry, $offset, $limit, $since_id, $max_id, $order, $since); @@ -556,127 +573,127 @@ class Notice extends Memcached_DataObject return Notice::getCachedStream($qry, $cachekey, $offset, $limit, $order); } } - + return Notice::getStreamDirect($qry, $offset, $limit, $since_id, $max_id, $order, $since); } - + static function getStreamDirect($qry, $offset, $limit, $since_id, $max_id, $order, $since) { - + $needAnd = false; $needWhere = true; - + if (preg_match('/\bWHERE\b/i', $qry)) { $needWhere = false; $needAnd = true; } - + if ($since_id > 0) { - + if ($needWhere) { $qry .= ' WHERE '; $needWhere = false; } else { $qry .= ' AND '; } - + $qry .= ' notice.id > ' . $since_id; } - + if ($max_id > 0) { - + if ($needWhere) { $qry .= ' WHERE '; $needWhere = false; } else { $qry .= ' AND '; } - + $qry .= ' notice.id <= ' . $max_id; } - + if ($since) { - + if ($needWhere) { $qry .= ' WHERE '; $needWhere = false; } else { $qry .= ' AND '; } - + $qry .= ' notice.created > \'' . date('Y-m-d H:i:s', $since) . '\''; } - + # Allow ORDER override - + if ($order) { $qry .= $order; } else { $qry .= ' ORDER BY notice.created DESC, notice.id DESC '; } - + if (common_config('db','type') == 'pgsql') { $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset; } else { $qry .= ' LIMIT ' . $offset . ', ' . $limit; } - + $notice = new Notice(); - + $notice->query($qry); - + return $notice; } - + # XXX: this is pretty long and should probably be broken up into # some helper functions - + static function getCachedStream($qry, $cachekey, $offset, $limit, $order) { - + # If outside our cache window, just go to the DB - + if ($offset + $limit > NOTICE_CACHE_WINDOW) { return Notice::getStreamDirect($qry, $offset, $limit, null, null, $order, null); } - + # Get the cache; if we can't, just go to the DB - + $cache = common_memcache(); - + if (!$cache) { return Notice::getStreamDirect($qry, $offset, $limit, null, null, $order, null); } - + # Get the notices out of the cache - + $notices = $cache->get(common_cache_key($cachekey)); - + # On a cache hit, return a DB-object-like wrapper - + if ($notices !== false) { $wrapper = new ArrayWrapper(array_slice($notices, $offset, $limit)); return $wrapper; } - + # If the cache was invalidated because of new data being # added, we can try and just get the new stuff. We keep an additional # copy of the data at the key + ';last' - + # No cache hit. Try to get the *last* cached version - + $last_notices = $cache->get(common_cache_key($cachekey) . ';last'); - + if ($last_notices) { - + # Reverse-chron order, so last ID is last. - + $last_id = $last_notices[0]->id; - + # XXX: this assumes monotonically increasing IDs; a fair # bet with our DB. - + $new_notice = Notice::getStreamDirect($qry, 0, NOTICE_CACHE_WINDOW, - $last_id, null, $order, null); - + $last_id, null, $order, null); + if ($new_notice) { $new_notices = array(); while ($new_notice->fetch()) { @@ -685,54 +702,54 @@ class Notice extends Memcached_DataObject $new_notice->free(); $notices = array_slice(array_merge($new_notices, $last_notices), 0, NOTICE_CACHE_WINDOW); - + # Store the array in the cache for next time - + $result = $cache->set(common_cache_key($cachekey), $notices); $result = $cache->set(common_cache_key($cachekey) . ';last', $notices); - + # return a wrapper of the array for use now - + return new ArrayWrapper(array_slice($notices, $offset, $limit)); } } - + # Otherwise, get the full cache window out of the DB - + $notice = Notice::getStreamDirect($qry, 0, NOTICE_CACHE_WINDOW, null, null, $order, null); - + # If there are no hits, just return the value - + if (!$notice) { return $notice; } - + # Pack results into an array - + $notices = array(); - + while ($notice->fetch()) { $notices[] = clone($notice); } - + $notice->free(); - + # Store the array in the cache for next time - + $result = $cache->set(common_cache_key($cachekey), $notices); $result = $cache->set(common_cache_key($cachekey) . ';last', $notices); - + # return a wrapper of the array for use now - + $wrapper = new ArrayWrapper(array_slice($notices, $offset, $limit)); - + return $wrapper; } - + function getStreamByIds($ids) { $cache = common_memcache(); - + if (!empty($cache)) { $notices = array(); foreach ($ids as $id) { @@ -750,35 +767,35 @@ class Notice extends Memcached_DataObject } $notice->whereAdd('id in (' . implode(', ', $ids) . ')'); $notice->orderBy('id DESC'); - + $notice->find(); return $notice; } } - + function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0, $since=null) { $ids = Notice::stream(array('Notice', '_publicStreamDirect'), array(), 'public', $offset, $limit, $since_id, $max_id, $since); - + return Notice::getStreamByIds($ids); } - + function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $max_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')) { $notice->whereAdd('is_local = ' . Notice::LOCAL_PUBLIC); } else { @@ -786,108 +803,108 @@ class Notice extends Memcached_DataObject $notice->whereAdd('is_local !='. Notice::LOCAL_NONPUBLIC); $notice->whereAdd('is_local !='. Notice::GATEWAY); } - + if ($since_id != 0) { $notice->whereAdd('id > ' . $since_id); } - + if ($max_id != 0) { $notice->whereAdd('id <= ' . $max_id); } - + if (!is_null($since)) { $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); } - + $ids = array(); - + if ($notice->find()) { while ($notice->fetch()) { $ids[] = $notice->id; } } - + $notice->free(); $notice = NULL; - + return $ids; } - + function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null) { $ids = Notice::stream(array('Notice', '_conversationStreamDirect'), array($id), 'notice:conversation_ids:'.$id, $offset, $limit, $since_id, $max_id, $since); - + return Notice::getStreamByIds($ids); } - + function _conversationStreamDirect($id, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null) { $notice = new Notice(); - + $notice->selectAdd(); // clears it $notice->selectAdd('id'); - + $notice->conversation = $id; - + $notice->orderBy('id DESC'); - + if (!is_null($offset)) { $notice->limit($offset, $limit); } - + if ($since_id != 0) { $notice->whereAdd('id > ' . $since_id); } - + if ($max_id != 0) { $notice->whereAdd('id <= ' . $max_id); } - + if (!is_null($since)) { $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); } - + $ids = array(); - + if ($notice->find()) { while ($notice->fetch()) { $ids[] = $notice->id; } } - + $notice->free(); $notice = NULL; - + return $ids; } - + function addToInboxes() { $enabled = common_config('inboxes', 'enabled'); - + if ($enabled === true || $enabled === 'transitional') { - + // XXX: loads constants - + $inbox = new Notice_inbox(); - + $users = $this->getSubscribedUsers(); - + // FIXME: kind of ignoring 'transitional'... // we'll probably stop supporting inboxless mode // in 0.9.x - + $ni = array(); - + foreach ($users as $id) { $ni[$id] = NOTICE_INBOX_SOURCE_SUB; } - + $groups = $this->saveGroups(); - + foreach ($groups as $group) { $users = $group->getUserMembers(); foreach ($users as $id) { @@ -896,12 +913,12 @@ class Notice extends Memcached_DataObject } } } - + $cnt = 0; - + $qryhdr = 'INSERT INTO notice_inbox (user_id, notice_id, source, created) VALUES '; $qry = $qryhdr; - + foreach ($ni as $id => $source) { if ($cnt > 0) { $qry .= ', '; @@ -919,52 +936,52 @@ class Notice extends Memcached_DataObject $cnt = 0; } } - + if ($cnt > 0) { $inbox = new Notice_inbox(); $inbox->query($qry); } } - + return; } - + function getSubscribedUsers() { $user = new User(); - + if(common_config('db','quote_identifiers')) - $user_table = '"user"'; + $user_table = '"user"'; else $user_table = 'user'; - + $qry = - 'SELECT id ' . - 'FROM '. $user_table .' JOIN subscription '. - 'ON '. $user_table .'.id = subscription.subscriber ' . - 'WHERE subscription.subscribed = %d '; - + 'SELECT id ' . + 'FROM '. $user_table .' JOIN subscription '. + 'ON '. $user_table .'.id = subscription.subscriber ' . + 'WHERE subscription.subscribed = %d '; + $user->query(sprintf($qry, $this->profile_id)); - + $ids = array(); - + while ($user->fetch()) { $ids[] = $user->id; } - + $user->free(); - + return $ids; } - + function saveGroups() { $groups = array(); - + $enabled = common_config('inboxes', 'enabled'); if ($enabled !== true && $enabled !== 'transitional') { return $groups; } - + /* extract all !group */ $count = preg_match_all('/(?:^|\s)!([A-Za-z0-9]{1,64})/', strtolower($this->content), @@ -972,62 +989,62 @@ class Notice extends Memcached_DataObject if (!$count) { return $groups; } - + $profile = $this->getProfile(); - + /* Add them to the database */ - + foreach (array_unique($match[1]) as $nickname) { /* XXX: remote groups. */ $group = User_group::getForNickname($nickname); - + if (empty($group)) { continue; } - + // we automatically add a tag for every group name, too - + $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($nickname), 'notice_id' => $this->id)); - + if (is_null($tag)) { $this->saveTag($nickname); } - + if ($profile->isMember($group)) { - + $result = $this->addToGroupInbox($group); - + if (!$result) { common_log_db_error($gi, 'INSERT', __FILE__); } - + $groups[] = clone($group); } } - + return $groups; } - + function addToGroupInbox($group) { $gi = Group_inbox::pkeyGet(array('group_id' => $group->id, 'notice_id' => $this->id)); - + if (empty($gi)) { - + $gi = new Group_inbox(); - + $gi->group_id = $group->id; $gi->notice_id = $this->id; $gi->created = $this->created; - + return $gi->insert(); } - + return true; } - + function saveReplies() { // Alternative reply format @@ -1037,21 +1054,21 @@ class Notice extends Memcached_DataObject } // extract all @messages $cnt = preg_match_all('/(?:^|\s)@([a-z0-9]{1,64})/', $this->content, $match); - + $names = array(); - + if ($cnt || $tname) { // XXX: is there another way to make an array copy? $names = ($tname) ? array_unique(array_merge(array(strtolower($tname)), $match[1])) : array_unique($match[1]); } - + $sender = Profile::staticGet($this->profile_id); - + $replied = array(); - + // store replied only for first @ (what user/notice what the reply directed, // we assume first @ is it) - + for ($i=0; $icreated); @@ -1076,7 +1093,7 @@ class Notice extends Memcached_DataObject $replied[$recipient->id] = 1; } } - + // Hash format replies, too $cnt = preg_match_all('/(?:^|\s)@#([a-z0-9]{1,64})/', $this->content, $match); if ($cnt) { @@ -1103,7 +1120,7 @@ class Notice extends Memcached_DataObject } } } - + foreach (array_keys($replied) as $recipient) { $user = User::staticGet('id', $recipient); if ($user) { @@ -1111,22 +1128,22 @@ class Notice extends Memcached_DataObject } } } - + function asAtomEntry($namespace=false, $source=false) { $profile = $this->getProfile(); - + $xs = new XMLStringer(true); - + if ($namespace) { $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom', 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'); } else { $attrs = array(); } - + $xs->elementStart('entry', $attrs); - + if ($source) { $xs->elementStart('source'); $xs->element('title', null, $profile->nickname . " - " . common_config('site', 'name')); @@ -1135,38 +1152,38 @@ class Notice extends Memcached_DataObject if (!empty($user)) { $atom_feed = common_local_url('api', array('apiaction' => 'statuses', - 'method' => 'user_timeline', - 'argument' => $profile->nickname.'.atom')); + 'method' => 'user_timeline', + 'argument' => $profile->nickname.'.atom')); $xs->element('link', array('rel' => 'self', 'type' => 'application/atom+xml', 'href' => $profile->profileurl)); $xs->element('link', array('rel' => 'license', 'href' => common_config('license', 'url'))); } - + $xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE)); } - + $xs->elementStart('author'); $xs->element('name', null, $profile->nickname); $xs->element('uri', null, $profile->profileurl); $xs->elementEnd('author'); - + if ($source) { $xs->elementEnd('source'); } - + $xs->element('title', null, $this->content); $xs->element('summary', null, $this->content); - + $xs->element('link', array('rel' => 'alternate', 'href' => $this->bestUrl())); - + $xs->element('id', null, $this->uri); - + $xs->element('published', null, common_date_w3dtf($this->created)); $xs->element('updated', null, common_date_w3dtf($this->modified)); - + if ($this->reply_to) { $reply_notice = Notice::staticGet('id', $this->reply_to); if (!empty($reply_notice)) { @@ -1177,9 +1194,9 @@ class Notice extends Memcached_DataObject 'href' => $reply_notice->bestUrl())); } } - + $xs->element('content', array('type' => 'html'), $this->rendered); - + $tag = new Notice_tag(); $tag->notice_id = $this->id; if ($tag->find()) { @@ -1188,7 +1205,7 @@ class Notice extends Memcached_DataObject } } $tag->free(); - + # Enclosures $attachments = $this->attachments(); if($attachments){ @@ -1203,12 +1220,12 @@ class Notice extends Memcached_DataObject } } } - + $xs->elementEnd('entry'); - + return $xs->getString(); } - + function bestUrl() { if (!empty($this->url)) { @@ -1220,135 +1237,135 @@ class Notice extends Memcached_DataObject array('notice' => $this->id)); } } - + function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null) { $cache = common_memcache(); - + if (empty($cache) || $since_id != 0 || $max_id != 0 || (!is_null($since) && $since > 0) || is_null($limit) || ($offset + $limit) > NOTICE_CACHE_WINDOW) { - return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id, - $max_id, $since))); - } - + return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id, + $max_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))); - + $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))); - + 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; } - + /** - * Determine which notice, if any, a new notice is in reply to. - * - * For conversation tracking, we try to see where this notice fits - * in the tree. Rough algorithm is: - * - * if (reply_to is set and valid) { - * return reply_to; - * } else if ((source not API or Web) and (content starts with "T NAME" or "@name ")) { - * return ID of last notice by initial @name in content; - * } - * - * Note that all @nickname instances will still be used to save "reply" records, - * so the notice shows up in the mentioned users' "replies" tab. - * - * @param integer $reply_to ID passed in by Web or API - * @param integer $profile_id ID of author - * @param string $source Source tag, like 'web' or 'gwibber' - * @param string $content Final notice content - * - * @return integer ID of replied-to notice, or null for not a reply. - */ - - static function getReplyTo($reply_to, $profile_id, $source, $content) + * Determine which notice, if any, a new notice is in reply to. + * + * For conversation tracking, we try to see where this notice fits + * in the tree. Rough algorithm is: + * + * if (reply_to is set and valid) { + * return reply_to; + * } else if ((source not API or Web) and (content starts with "T NAME" or "@name ")) { + * return ID of last notice by initial @name in content; + * } + * + * Note that all @nickname instances will still be used to save "reply" records, + * so the notice shows up in the mentioned users' "replies" tab. + * + * @param integer $reply_to ID passed in by Web or API + * @param integer $profile_id ID of author + * @param string $source Source tag, like 'web' or 'gwibber' + * @param string $content Final notice content + * + * @return integer ID of replied-to notice, or null for not a reply. + */ + + static function getReplyTo($reply_to, $profile_id, $source, $content) { static $lb = array('xmpp', 'mail', 'sms', 'omb'); - + // If $reply_to is specified, we check that it exists, and then // return it if it does - + if (!empty($reply_to)) { $reply_notice = Notice::staticGet('id', $reply_to); if (!empty($reply_notice)) { return $reply_to; } } - + // If it's not a "low bandwidth" source (one where you can't set // a reply_to argument), we return. This is mostly web and API // clients. - + if (!in_array($source, $lb)) { return null; } - + // Is there an initial @ or T? - + if (preg_match('/^T ([A-Z0-9]{1,64}) /', $content, $match) || preg_match('/^@([a-z0-9]{1,64})\s+/', $content, $match)) { - $nickname = common_canonical_nickname($match[1]); - } else { - return null; - } - + $nickname = common_canonical_nickname($match[1]); + } else { + return null; + } + // Figure out who that is. - + $sender = Profile::staticGet('id', $profile_id); $recipient = common_relative_profile($sender, $nickname, common_sql_now()); - + if (empty($recipient)) { return null; } - + // Get their last notice - + $last = $recipient->getCurrentNotice(); - + if (!empty($last)) { return $last->id; } } - + static function maxContent() { $contentlimit = common_config('notice', 'contentlimit'); @@ -1358,7 +1375,7 @@ class Notice extends Memcached_DataObject } return $contentlimit; } - + static function contentTooLong($content) { $contentlimit = self::maxContent(); -- cgit v1.2.3-54-g00ecf From 18f4a7eaea3c78cb55db843d77b43db86ac70308 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sat, 3 Oct 2009 20:34:40 -0400 Subject: reformat Notice.php --- classes/Notice.php | 810 ++++++++++++++++++++++++++--------------------------- 1 file changed, 405 insertions(+), 405 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index c68098c65..ba2227c0a 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -15,7 +15,7 @@ * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - * + * * @category Notices * @package StatusNet * @author Brenda Wallace @@ -32,13 +32,13 @@ * @license GNU Affero General Public License http://www.gnu.org/licenses/ */ -if (!defined('STATUSNET') && !defined('LACONICA')) { - exit(1); +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); } /** -* Table Definition for notice -*/ + * Table Definition for notice + */ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; /* We keep the first three 20-notice pages, plus one for pagination check, @@ -52,7 +52,7 @@ class Notice extends Memcached_DataObject { ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ - + public $__table = 'notice'; // table name public $id; // int(4) primary_key not_null public $profile_id; // int(4) not_null @@ -66,45 +66,45 @@ class Notice extends Memcached_DataObject public $is_local; // tinyint(1) public $source; // varchar(32) public $conversation; // int(4) - + /* Static get */ function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Notice',$k,$v); } - + /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - + /* Notice types */ const LOCAL_PUBLIC = 1; const REMOTE_OMB = 0; const LOCAL_NONPUBLIC = -1; const GATEWAY = -2; - + function getProfile() { return Profile::staticGet('id', $this->profile_id); } - + function delete() { $this->blowCaches(true); $this->blowFavesCache(true); $this->blowSubsCache(true); - + // For auditing purposes, save a record that the notice // was deleted. - + $deleted = new Deleted_notice(); - + $deleted->id = $this->id; $deleted->profile_id = $this->profile_id; $deleted->uri = $this->uri; $deleted->created = $this->created; $deleted->deleted = common_sql_now(); - + $this->query('BEGIN'); - + $deleted->insert(); - + //Null any notices that are replies to this notice $this->query(sprintf("UPDATE notice set reply_to = null WHERE reply_to = %d", $this->id)); $related = array('Reply', @@ -123,7 +123,7 @@ class Notice extends Memcached_DataObject $result = parent::delete(); $this->query('COMMIT'); } - + function saveTags() { /* extract all #hastags */ @@ -131,14 +131,14 @@ class Notice extends Memcached_DataObject if (!$count) { return true; } - + //turn each into their canonical tag //this is needed to remove dupes before saving e.g. #hash.tag = #hashtag $hashtags = array(); for($i=0; $itag = $hashtag; $tag->created = $this->created; $id = $tag->insert(); - + if (!$id) { throw new ServerException(sprintf(_('DB error inserting hashtag: %s'), $last_error->message)); return; } } - + static function saveNew($profile_id, $content, $source=null, $is_local=Notice::LOCAL_PUBLIC, $reply_to=null, $uri=null, $created=null) { - - $profile = Profile::staticGet($profile_id); - - $final = common_shorten_links($content); - - if (Notice::contentTooLong($final)) { - throw new ClientException(_('Problem saving notice. Too long.')); - } - - if (!$profile) { - throw new ClientException(_('Problem saving notice. Unknown user.')); - } - - if (common_config('throttle', 'enabled') && !Notice::checkEditThrottle($profile_id)) { - common_log(LOG_WARNING, 'Excessive posting by profile #' . $profile_id . '; throttled.'); - throw new ClientException(_('Too many notices too fast; take a breather '. + + $profile = Profile::staticGet($profile_id); + + $final = common_shorten_links($content); + + if (Notice::contentTooLong($final)) { + throw new ClientException(_('Problem saving notice. Too long.')); + } + + if (!$profile) { + throw new ClientException(_('Problem saving notice. Unknown user.')); + } + + if (common_config('throttle', 'enabled') && !Notice::checkEditThrottle($profile_id)) { + common_log(LOG_WARNING, 'Excessive posting by profile #' . $profile_id . '; throttled.'); + throw new ClientException(_('Too many notices too fast; take a breather '. 'and post again in a few minutes.')); - } - - if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) { - common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.'); - throw new ClientException(_('Too many duplicate messages too quickly;'. + } + + if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) { + common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.'); + throw new ClientException(_('Too many duplicate messages too quickly;'. ' take a breather and post again in a few minutes.')); - } - - $banned = common_config('profile', 'banned'); - - if ( in_array($profile_id, $banned) || in_array($profile->nickname, $banned)) { - common_log(LOG_WARNING, "Attempted post from banned user: $profile->nickname (user id = $profile_id)."); - throw new ClientException(_('You are banned from posting notices on this site.')); - } - - $notice = new Notice(); - $notice->profile_id = $profile_id; - - $blacklist = common_config('public', 'blacklist'); - $autosource = common_config('public', 'autosource'); - + } + + $banned = common_config('profile', 'banned'); + + if ( in_array($profile_id, $banned) || in_array($profile->nickname, $banned)) { + common_log(LOG_WARNING, "Attempted post from banned user: $profile->nickname (user id = $profile_id)."); + throw new ClientException(_('You are banned from posting notices on this site.')); + } + + $notice = new Notice(); + $notice->profile_id = $profile_id; + + $blacklist = common_config('public', 'blacklist'); + $autosource = common_config('public', 'autosource'); + # Blacklisted are non-false, but not 1, either - - if (($blacklist && in_array($profile_id, $blacklist)) || - ($source && $autosource && in_array($source, $autosource))) { - $notice->is_local = Notice::LOCAL_NONPUBLIC; - } else { - $notice->is_local = $is_local; - } - - if (!empty($created)) { - $notice->created = $created; - } else { - $notice->created = common_sql_now(); - } - - $notice->content = $final; - $notice->rendered = common_render_content($final, $notice); - $notice->source = $source; - $notice->uri = $uri; - - $notice->reply_to = self::getReplyTo($reply_to, $profile_id, $source, $final); - - if (!empty($notice->reply_to)) { - $reply = Notice::staticGet('id', $notice->reply_to); - $notice->conversation = $reply->conversation; - } - - if (Event::handle('StartNoticeSave', array(&$notice))) { - + + if (($blacklist && in_array($profile_id, $blacklist)) || + ($source && $autosource && in_array($source, $autosource))) { + $notice->is_local = Notice::LOCAL_NONPUBLIC; + } else { + $notice->is_local = $is_local; + } + + if (!empty($created)) { + $notice->created = $created; + } else { + $notice->created = common_sql_now(); + } + + $notice->content = $final; + $notice->rendered = common_render_content($final, $notice); + $notice->source = $source; + $notice->uri = $uri; + + $notice->reply_to = self::getReplyTo($reply_to, $profile_id, $source, $final); + + if (!empty($notice->reply_to)) { + $reply = Notice::staticGet('id', $notice->reply_to); + $notice->conversation = $reply->conversation; + } + + if (Event::handle('StartNoticeSave', array(&$notice))) { + // XXX: some of these functions write to the DB - - $notice->query('BEGIN'); - - $id = $notice->insert(); - - if (!$id) { - common_log_db_error($notice, 'INSERT', __FILE__); - throw new ServerException(_('Problem saving notice.')); - } - + + $notice->query('BEGIN'); + + $id = $notice->insert(); + + if (!$id) { + common_log_db_error($notice, 'INSERT', __FILE__); + throw new ServerException(_('Problem saving notice.')); + } + // Update ID-dependent columns: URI, conversation - - $orig = clone($notice); - - $changed = false; - - if (empty($uri)) { - $notice->uri = common_notice_uri($notice); - $changed = true; - } - + + $orig = clone($notice); + + $changed = false; + + if (empty($uri)) { + $notice->uri = common_notice_uri($notice); + $changed = true; + } + // If it's not part of a conversation, it's // the beginning of a new conversation. - - if (empty($notice->conversation)) { - $notice->conversation = $notice->id; - $changed = true; - } - - if ($changed) { - if (!$notice->update($orig)) { - common_log_db_error($notice, 'UPDATE', __FILE__); - throw new ServerException(_('Problem saving notice.')); - } - } - + + if (empty($notice->conversation)) { + $notice->conversation = $notice->id; + $changed = true; + } + + if ($changed) { + if (!$notice->update($orig)) { + common_log_db_error($notice, 'UPDATE', __FILE__); + throw new ServerException(_('Problem saving notice.')); + } + } + // XXX: do we need to change this for remote users? - - $notice->saveReplies(); - $notice->saveTags(); - - $notice->addToInboxes(); - - $notice->saveUrls(); - - $notice->query('COMMIT'); - - Event::handle('EndNoticeSave', array($notice)); - } - + + $notice->saveReplies(); + $notice->saveTags(); + + $notice->addToInboxes(); + + $notice->saveUrls(); + + $notice->query('COMMIT'); + + Event::handle('EndNoticeSave', array($notice)); + } + # Clear the cache for subscribed users, so they'll update at next request # XXX: someone clever could prepend instead of clearing the cache - - $notice->blowCaches(); - - return $notice; - } - + + $notice->blowCaches(); + + return $notice; + } + /** save all urls in the notice to the db - * - * follow redirects and save all available file information - * (mimetype, date, size, oembed, etc.) - * - * @return void - */ - function saveUrls() { - common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id); - } - + * + * follow redirects and save all available file information + * (mimetype, date, size, oembed, etc.) + * + * @return void + */ + function saveUrls() { + common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id); + } + function saveUrl($data) { list($url, $notice_id) = $data; File::processNew($url, $notice_id); } - + static function checkDupes($profile_id, $content) { $profile = Profile::staticGet($profile_id); if (!$profile) { @@ -328,14 +328,14 @@ class Notice extends Memcached_DataObject $notice->profile_id = $profile_id; $notice->content = $content; if (common_config('db','type') == 'pgsql') - $notice->whereAdd('extract(epoch from now() - created) < ' . common_config('site', 'dupelimit')); + $notice->whereAdd('extract(epoch from now() - created) < ' . common_config('site', 'dupelimit')); else - $notice->whereAdd('now() - created < ' . common_config('site', 'dupelimit')); - + $notice->whereAdd('now() - created < ' . common_config('site', 'dupelimit')); + $cnt = $notice->count(); return ($cnt == 0); } - + static function checkEditThrottle($profile_id) { $profile = Profile::staticGet($profile_id); if (!$profile) { @@ -353,7 +353,7 @@ class Notice extends Memcached_DataObject # Either not N notices in the stream, OR the Nth was not posted within timespan seconds return true; } - + function getUploadedAttachment() { $post = clone $this; $query = 'select file.url as up, file.id as i from file join file_to_post on file.id = file_id where post_id=' . $post->escape($post->id) . ' and url like "%/notice/%/file"'; @@ -367,7 +367,7 @@ class Notice extends Memcached_DataObject $post->free(); return $ret; } - + function hasAttachments() { $post = clone $this; $query = "select count(file_id) as n_attachments from file join file_to_post on (file_id = file.id) join notice on (post_id = notice.id) where post_id = " . $post->escape($post->id); @@ -377,7 +377,7 @@ class Notice extends Memcached_DataObject $post->free(); return $n_attachments; } - + function attachments() { // XXX: cache this $att = array(); @@ -391,7 +391,7 @@ class Notice extends Memcached_DataObject } return $att; } - + function blowCaches($blowLast=false) { $this->blowSubsCache($blowLast); @@ -404,7 +404,7 @@ class Notice extends Memcached_DataObject $profile = Profile::staticGet($this->profile_id); $profile->blowNoticeCount(); } - + function blowConversationCache($blowLast=false) { $cache = common_memcache(); @@ -416,7 +416,7 @@ class Notice extends Memcached_DataObject } } } - + function blowGroupCache($blowLast=false) { $cache = common_memcache(); @@ -445,7 +445,7 @@ class Notice extends Memcached_DataObject unset($group_inbox); } } - + function blowTagCache($blowLast=false) { $cache = common_memcache(); @@ -456,7 +456,7 @@ class Notice extends Memcached_DataObject while ($tag->fetch()) { $tag->blowCache($blowLast); $ck = 'profile:notice_ids_tagged:' . $this->profile_id . ':' . $tag->tag; - + $cache->delete($ck); if ($blowLast) { $cache->delete($ck . ';last'); @@ -467,19 +467,19 @@ class Notice extends Memcached_DataObject unset($tag); } } - + function blowSubsCache($blowLast=false) { $cache = common_memcache(); if ($cache) { $user = new User(); - + $UT = common_config('db','type')=='pgsql'?'"user"':'user'; $user->query('SELECT id ' . - + "FROM $UT JOIN subscription ON $UT.id = subscription.subscriber " . 'WHERE subscription.subscribed = ' . $this->profile_id); - + while ($user->fetch()) { $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id)); $cache->delete(common_cache_key('notice_inbox:by_user_own:'.$user->id)); @@ -492,7 +492,7 @@ class Notice extends Memcached_DataObject unset($user); } } - + function blowNoticeCache($blowLast=false) { if ($this->is_local) { @@ -505,7 +505,7 @@ class Notice extends Memcached_DataObject } } } - + function blowRepliesCache($blowLast=false) { $cache = common_memcache(); @@ -524,7 +524,7 @@ class Notice extends Memcached_DataObject unset($reply); } } - + function blowPublicCache($blowLast=false) { if ($this->is_local == Notice::LOCAL_PUBLIC) { @@ -537,7 +537,7 @@ class Notice extends Memcached_DataObject } } } - + function blowFavesCache($blowLast=false) { $cache = common_memcache(); @@ -558,14 +558,14 @@ class Notice extends Memcached_DataObject unset($fave); } } - + # XXX: too many args; we need to move to named params or even a separate # class for notice streams - + static function getStream($qry, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0, $order=null, $since=null) { - + if (common_config('memcached', 'enabled')) { - + # Skip the cache if this is a since, since_id or max_id qry if ($since_id > 0 || $max_id > 0 || $since) { return Notice::getStreamDirect($qry, $offset, $limit, $since_id, $max_id, $order, $since); @@ -573,127 +573,127 @@ class Notice extends Memcached_DataObject return Notice::getCachedStream($qry, $cachekey, $offset, $limit, $order); } } - + return Notice::getStreamDirect($qry, $offset, $limit, $since_id, $max_id, $order, $since); } - + static function getStreamDirect($qry, $offset, $limit, $since_id, $max_id, $order, $since) { - + $needAnd = false; $needWhere = true; - + if (preg_match('/\bWHERE\b/i', $qry)) { $needWhere = false; $needAnd = true; } - + if ($since_id > 0) { - + if ($needWhere) { $qry .= ' WHERE '; $needWhere = false; } else { $qry .= ' AND '; } - + $qry .= ' notice.id > ' . $since_id; } - + if ($max_id > 0) { - + if ($needWhere) { $qry .= ' WHERE '; $needWhere = false; } else { $qry .= ' AND '; } - + $qry .= ' notice.id <= ' . $max_id; } - + if ($since) { - + if ($needWhere) { $qry .= ' WHERE '; $needWhere = false; } else { $qry .= ' AND '; } - + $qry .= ' notice.created > \'' . date('Y-m-d H:i:s', $since) . '\''; } - + # Allow ORDER override - + if ($order) { $qry .= $order; } else { $qry .= ' ORDER BY notice.created DESC, notice.id DESC '; } - + if (common_config('db','type') == 'pgsql') { $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset; } else { $qry .= ' LIMIT ' . $offset . ', ' . $limit; } - + $notice = new Notice(); - + $notice->query($qry); - + return $notice; } - + # XXX: this is pretty long and should probably be broken up into # some helper functions - + static function getCachedStream($qry, $cachekey, $offset, $limit, $order) { - + # If outside our cache window, just go to the DB - + if ($offset + $limit > NOTICE_CACHE_WINDOW) { return Notice::getStreamDirect($qry, $offset, $limit, null, null, $order, null); } - + # Get the cache; if we can't, just go to the DB - + $cache = common_memcache(); - + if (!$cache) { return Notice::getStreamDirect($qry, $offset, $limit, null, null, $order, null); } - + # Get the notices out of the cache - + $notices = $cache->get(common_cache_key($cachekey)); - + # On a cache hit, return a DB-object-like wrapper - + if ($notices !== false) { $wrapper = new ArrayWrapper(array_slice($notices, $offset, $limit)); return $wrapper; } - + # If the cache was invalidated because of new data being # added, we can try and just get the new stuff. We keep an additional # copy of the data at the key + ';last' - + # No cache hit. Try to get the *last* cached version - + $last_notices = $cache->get(common_cache_key($cachekey) . ';last'); - + if ($last_notices) { - + # Reverse-chron order, so last ID is last. - + $last_id = $last_notices[0]->id; - + # XXX: this assumes monotonically increasing IDs; a fair # bet with our DB. - + $new_notice = Notice::getStreamDirect($qry, 0, NOTICE_CACHE_WINDOW, - $last_id, null, $order, null); - + $last_id, null, $order, null); + if ($new_notice) { $new_notices = array(); while ($new_notice->fetch()) { @@ -702,54 +702,54 @@ class Notice extends Memcached_DataObject $new_notice->free(); $notices = array_slice(array_merge($new_notices, $last_notices), 0, NOTICE_CACHE_WINDOW); - + # Store the array in the cache for next time - + $result = $cache->set(common_cache_key($cachekey), $notices); $result = $cache->set(common_cache_key($cachekey) . ';last', $notices); - + # return a wrapper of the array for use now - + return new ArrayWrapper(array_slice($notices, $offset, $limit)); } } - + # Otherwise, get the full cache window out of the DB - + $notice = Notice::getStreamDirect($qry, 0, NOTICE_CACHE_WINDOW, null, null, $order, null); - + # If there are no hits, just return the value - + if (!$notice) { return $notice; } - + # Pack results into an array - + $notices = array(); - + while ($notice->fetch()) { $notices[] = clone($notice); } - + $notice->free(); - + # Store the array in the cache for next time - + $result = $cache->set(common_cache_key($cachekey), $notices); $result = $cache->set(common_cache_key($cachekey) . ';last', $notices); - + # return a wrapper of the array for use now - + $wrapper = new ArrayWrapper(array_slice($notices, $offset, $limit)); - + return $wrapper; } - + function getStreamByIds($ids) { $cache = common_memcache(); - + if (!empty($cache)) { $notices = array(); foreach ($ids as $id) { @@ -767,35 +767,35 @@ class Notice extends Memcached_DataObject } $notice->whereAdd('id in (' . implode(', ', $ids) . ')'); $notice->orderBy('id DESC'); - + $notice->find(); return $notice; } } - + function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0, $since=null) { $ids = Notice::stream(array('Notice', '_publicStreamDirect'), array(), 'public', $offset, $limit, $since_id, $max_id, $since); - + return Notice::getStreamByIds($ids); } - + function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $max_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')) { $notice->whereAdd('is_local = ' . Notice::LOCAL_PUBLIC); } else { @@ -803,108 +803,108 @@ class Notice extends Memcached_DataObject $notice->whereAdd('is_local !='. Notice::LOCAL_NONPUBLIC); $notice->whereAdd('is_local !='. Notice::GATEWAY); } - + if ($since_id != 0) { $notice->whereAdd('id > ' . $since_id); } - + if ($max_id != 0) { $notice->whereAdd('id <= ' . $max_id); } - + if (!is_null($since)) { $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); } - + $ids = array(); - + if ($notice->find()) { while ($notice->fetch()) { $ids[] = $notice->id; } } - + $notice->free(); $notice = NULL; - + return $ids; } - + function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null) { $ids = Notice::stream(array('Notice', '_conversationStreamDirect'), array($id), 'notice:conversation_ids:'.$id, $offset, $limit, $since_id, $max_id, $since); - + return Notice::getStreamByIds($ids); } - + function _conversationStreamDirect($id, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null) { $notice = new Notice(); - + $notice->selectAdd(); // clears it $notice->selectAdd('id'); - + $notice->conversation = $id; - + $notice->orderBy('id DESC'); - + if (!is_null($offset)) { $notice->limit($offset, $limit); } - + if ($since_id != 0) { $notice->whereAdd('id > ' . $since_id); } - + if ($max_id != 0) { $notice->whereAdd('id <= ' . $max_id); } - + if (!is_null($since)) { $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); } - + $ids = array(); - + if ($notice->find()) { while ($notice->fetch()) { $ids[] = $notice->id; } } - + $notice->free(); $notice = NULL; - + return $ids; } - + function addToInboxes() { $enabled = common_config('inboxes', 'enabled'); - + if ($enabled === true || $enabled === 'transitional') { - + // XXX: loads constants - + $inbox = new Notice_inbox(); - + $users = $this->getSubscribedUsers(); - + // FIXME: kind of ignoring 'transitional'... // we'll probably stop supporting inboxless mode // in 0.9.x - + $ni = array(); - + foreach ($users as $id) { $ni[$id] = NOTICE_INBOX_SOURCE_SUB; } - + $groups = $this->saveGroups(); - + foreach ($groups as $group) { $users = $group->getUserMembers(); foreach ($users as $id) { @@ -913,12 +913,12 @@ class Notice extends Memcached_DataObject } } } - + $cnt = 0; - + $qryhdr = 'INSERT INTO notice_inbox (user_id, notice_id, source, created) VALUES '; $qry = $qryhdr; - + foreach ($ni as $id => $source) { if ($cnt > 0) { $qry .= ', '; @@ -936,52 +936,52 @@ class Notice extends Memcached_DataObject $cnt = 0; } } - + if ($cnt > 0) { $inbox = new Notice_inbox(); $inbox->query($qry); } } - + return; } - + function getSubscribedUsers() { $user = new User(); - + if(common_config('db','quote_identifiers')) - $user_table = '"user"'; + $user_table = '"user"'; else $user_table = 'user'; - + $qry = - 'SELECT id ' . - 'FROM '. $user_table .' JOIN subscription '. - 'ON '. $user_table .'.id = subscription.subscriber ' . - 'WHERE subscription.subscribed = %d '; - + 'SELECT id ' . + 'FROM '. $user_table .' JOIN subscription '. + 'ON '. $user_table .'.id = subscription.subscriber ' . + 'WHERE subscription.subscribed = %d '; + $user->query(sprintf($qry, $this->profile_id)); - + $ids = array(); - + while ($user->fetch()) { $ids[] = $user->id; } - + $user->free(); - + return $ids; } - + function saveGroups() { $groups = array(); - + $enabled = common_config('inboxes', 'enabled'); if ($enabled !== true && $enabled !== 'transitional') { return $groups; } - + /* extract all !group */ $count = preg_match_all('/(?:^|\s)!([A-Za-z0-9]{1,64})/', strtolower($this->content), @@ -989,62 +989,62 @@ class Notice extends Memcached_DataObject if (!$count) { return $groups; } - + $profile = $this->getProfile(); - + /* Add them to the database */ - + foreach (array_unique($match[1]) as $nickname) { /* XXX: remote groups. */ $group = User_group::getForNickname($nickname); - + if (empty($group)) { continue; } - + // we automatically add a tag for every group name, too - + $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($nickname), 'notice_id' => $this->id)); - + if (is_null($tag)) { $this->saveTag($nickname); } - + if ($profile->isMember($group)) { - + $result = $this->addToGroupInbox($group); - + if (!$result) { common_log_db_error($gi, 'INSERT', __FILE__); } - + $groups[] = clone($group); } } - + return $groups; } - + function addToGroupInbox($group) { $gi = Group_inbox::pkeyGet(array('group_id' => $group->id, 'notice_id' => $this->id)); - + if (empty($gi)) { - + $gi = new Group_inbox(); - + $gi->group_id = $group->id; $gi->notice_id = $this->id; $gi->created = $this->created; - + return $gi->insert(); } - + return true; } - + function saveReplies() { // Alternative reply format @@ -1054,21 +1054,21 @@ class Notice extends Memcached_DataObject } // extract all @messages $cnt = preg_match_all('/(?:^|\s)@([a-z0-9]{1,64})/', $this->content, $match); - + $names = array(); - + if ($cnt || $tname) { // XXX: is there another way to make an array copy? $names = ($tname) ? array_unique(array_merge(array(strtolower($tname)), $match[1])) : array_unique($match[1]); } - + $sender = Profile::staticGet($this->profile_id); - + $replied = array(); - + // store replied only for first @ (what user/notice what the reply directed, // we assume first @ is it) - + for ($i=0; $icreated); @@ -1093,7 +1093,7 @@ class Notice extends Memcached_DataObject $replied[$recipient->id] = 1; } } - + // Hash format replies, too $cnt = preg_match_all('/(?:^|\s)@#([a-z0-9]{1,64})/', $this->content, $match); if ($cnt) { @@ -1120,7 +1120,7 @@ class Notice extends Memcached_DataObject } } } - + foreach (array_keys($replied) as $recipient) { $user = User::staticGet('id', $recipient); if ($user) { @@ -1128,22 +1128,22 @@ class Notice extends Memcached_DataObject } } } - + function asAtomEntry($namespace=false, $source=false) { $profile = $this->getProfile(); - + $xs = new XMLStringer(true); - + if ($namespace) { $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom', 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'); } else { $attrs = array(); } - + $xs->elementStart('entry', $attrs); - + if ($source) { $xs->elementStart('source'); $xs->element('title', null, $profile->nickname . " - " . common_config('site', 'name')); @@ -1152,38 +1152,38 @@ class Notice extends Memcached_DataObject if (!empty($user)) { $atom_feed = common_local_url('api', array('apiaction' => 'statuses', - 'method' => 'user_timeline', - 'argument' => $profile->nickname.'.atom')); + 'method' => 'user_timeline', + 'argument' => $profile->nickname.'.atom')); $xs->element('link', array('rel' => 'self', 'type' => 'application/atom+xml', 'href' => $profile->profileurl)); $xs->element('link', array('rel' => 'license', 'href' => common_config('license', 'url'))); } - + $xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE)); } - + $xs->elementStart('author'); $xs->element('name', null, $profile->nickname); $xs->element('uri', null, $profile->profileurl); $xs->elementEnd('author'); - + if ($source) { $xs->elementEnd('source'); } - + $xs->element('title', null, $this->content); $xs->element('summary', null, $this->content); - + $xs->element('link', array('rel' => 'alternate', 'href' => $this->bestUrl())); - + $xs->element('id', null, $this->uri); - + $xs->element('published', null, common_date_w3dtf($this->created)); $xs->element('updated', null, common_date_w3dtf($this->modified)); - + if ($this->reply_to) { $reply_notice = Notice::staticGet('id', $this->reply_to); if (!empty($reply_notice)) { @@ -1194,9 +1194,9 @@ class Notice extends Memcached_DataObject 'href' => $reply_notice->bestUrl())); } } - + $xs->element('content', array('type' => 'html'), $this->rendered); - + $tag = new Notice_tag(); $tag->notice_id = $this->id; if ($tag->find()) { @@ -1205,7 +1205,7 @@ class Notice extends Memcached_DataObject } } $tag->free(); - + # Enclosures $attachments = $this->attachments(); if($attachments){ @@ -1220,12 +1220,12 @@ class Notice extends Memcached_DataObject } } } - + $xs->elementEnd('entry'); - + return $xs->getString(); } - + function bestUrl() { if (!empty($this->url)) { @@ -1237,135 +1237,135 @@ class Notice extends Memcached_DataObject array('notice' => $this->id)); } } - + function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null) { $cache = common_memcache(); - + if (empty($cache) || $since_id != 0 || $max_id != 0 || (!is_null($since) && $since > 0) || is_null($limit) || ($offset + $limit) > NOTICE_CACHE_WINDOW) { - return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id, - $max_id, $since))); - } - + return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id, + $max_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))); - + $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))); - + 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; } - + /** - * Determine which notice, if any, a new notice is in reply to. - * - * For conversation tracking, we try to see where this notice fits - * in the tree. Rough algorithm is: - * - * if (reply_to is set and valid) { - * return reply_to; - * } else if ((source not API or Web) and (content starts with "T NAME" or "@name ")) { - * return ID of last notice by initial @name in content; - * } - * - * Note that all @nickname instances will still be used to save "reply" records, - * so the notice shows up in the mentioned users' "replies" tab. - * - * @param integer $reply_to ID passed in by Web or API - * @param integer $profile_id ID of author - * @param string $source Source tag, like 'web' or 'gwibber' - * @param string $content Final notice content - * - * @return integer ID of replied-to notice, or null for not a reply. - */ - - static function getReplyTo($reply_to, $profile_id, $source, $content) + * Determine which notice, if any, a new notice is in reply to. + * + * For conversation tracking, we try to see where this notice fits + * in the tree. Rough algorithm is: + * + * if (reply_to is set and valid) { + * return reply_to; + * } else if ((source not API or Web) and (content starts with "T NAME" or "@name ")) { + * return ID of last notice by initial @name in content; + * } + * + * Note that all @nickname instances will still be used to save "reply" records, + * so the notice shows up in the mentioned users' "replies" tab. + * + * @param integer $reply_to ID passed in by Web or API + * @param integer $profile_id ID of author + * @param string $source Source tag, like 'web' or 'gwibber' + * @param string $content Final notice content + * + * @return integer ID of replied-to notice, or null for not a reply. + */ + + static function getReplyTo($reply_to, $profile_id, $source, $content) { static $lb = array('xmpp', 'mail', 'sms', 'omb'); - + // If $reply_to is specified, we check that it exists, and then // return it if it does - + if (!empty($reply_to)) { $reply_notice = Notice::staticGet('id', $reply_to); if (!empty($reply_notice)) { return $reply_to; } } - + // If it's not a "low bandwidth" source (one where you can't set // a reply_to argument), we return. This is mostly web and API // clients. - + if (!in_array($source, $lb)) { return null; } - + // Is there an initial @ or T? - + if (preg_match('/^T ([A-Z0-9]{1,64}) /', $content, $match) || preg_match('/^@([a-z0-9]{1,64})\s+/', $content, $match)) { - $nickname = common_canonical_nickname($match[1]); - } else { - return null; - } - + $nickname = common_canonical_nickname($match[1]); + } else { + return null; + } + // Figure out who that is. - + $sender = Profile::staticGet('id', $profile_id); $recipient = common_relative_profile($sender, $nickname, common_sql_now()); - + if (empty($recipient)) { return null; } - + // Get their last notice - + $last = $recipient->getCurrentNotice(); - + if (!empty($last)) { return $last->id; } } - + static function maxContent() { $contentlimit = common_config('notice', 'contentlimit'); @@ -1375,7 +1375,7 @@ class Notice extends Memcached_DataObject } return $contentlimit; } - + static function contentTooLong($content) { $contentlimit = self::maxContent(); -- cgit v1.2.3-54-g00ecf From 9c460d591e1e11f7aa3970ffd0b3e320dd11d33e Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 23 Sep 2009 23:23:13 -0400 Subject: move scripts to just before , add event for scripts that need to be in --- lib/action.php | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/action.php b/lib/action.php index 71ceffe20..1b2f73752 100644 --- a/lib/action.php +++ b/lib/action.php @@ -120,14 +120,16 @@ class Action extends HTMLOutputter // lawsuit { // XXX: attributes (profile?) $this->elementStart('head'); - $this->showTitle(); - $this->showShortcutIcon(); - $this->showStylesheets(); - $this->showScripts(); - $this->showOpenSearch(); - $this->showFeeds(); - $this->showDescription(); - $this->extraHead(); + if (Event::handle('StartShowHeadElements', array($this))) { + $this->showTitle(); + $this->showShortcutIcon(); + $this->showStylesheets(); + $this->showOpenSearch(); + $this->showFeeds(); + $this->showDescription(); + $this->extraHead(); + Event::handle('EndShowHeadElements', array($this)); + } $this->elementEnd('head'); } @@ -352,6 +354,7 @@ class Action extends HTMLOutputter // lawsuit Event::handle('EndShowFooter', array($this)); } $this->elementEnd('div'); + $this->showScripts(); $this->elementEnd('body'); } -- cgit v1.2.3-54-g00ecf From 22b4a66de3441bcb54b748f98be5b53e3be4bece Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 30 Jun 2009 23:33:47 -0400 Subject: copy Comet plugin to Orbited --- plugins/Orbited/CometPlugin.php | 205 +++++ plugins/Orbited/README | 26 + plugins/Orbited/bayeux.class.inc.php | 134 ++++ plugins/Orbited/jquery.comet.js | 1451 ++++++++++++++++++++++++++++++++++ plugins/Orbited/json2.js | 478 +++++++++++ plugins/Orbited/updatetimeline.js | 154 ++++ 6 files changed, 2448 insertions(+) create mode 100644 plugins/Orbited/CometPlugin.php create mode 100644 plugins/Orbited/README create mode 100644 plugins/Orbited/bayeux.class.inc.php create mode 100644 plugins/Orbited/jquery.comet.js create mode 100644 plugins/Orbited/json2.js create mode 100644 plugins/Orbited/updatetimeline.js diff --git a/plugins/Orbited/CometPlugin.php b/plugins/Orbited/CometPlugin.php new file mode 100644 index 000000000..45251c66f --- /dev/null +++ b/plugins/Orbited/CometPlugin.php @@ -0,0 +1,205 @@ +. + * + * @category Plugin + * @package Laconica + * @author Evan Prodromou + * @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 + * @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, $username=null, $password=null) + { + $this->server = $server; + $this->username = $username; + $this->password = $password; + + 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)), + ' '); + } + + $user = common_current_user(); + + if (!empty($user->id)) { + $user_id = $user->id; + } else { + $user_id = 0; + } + + $replyurl = common_local_url('newnotice'); + $favorurl = common_local_url('favor'); + // FIXME: need to find a better way to pass this pattern in + $deleteurl = common_local_url('deletenotice', + array('notice' => '0000000000')); + + $action->elementStart('script', array('type' => 'text/javascript')); + $action->raw("$(document).ready(function() { updater.init(\"$this->server\", \"$timeline\", $user_id, \"$replyurl\", \"$favorurl\", \"$deleteurl\"); });"); + $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, $this->user, $this->password); + + 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); + $arr['source'] = htmlspecialchars($arr['source']); + + if (!empty($notice->reply_to)) { + $reply_to = Notice::staticGet('id', $notice->reply_to); + if (!empty($reply_to)) { + $arr['in_reply_to_status_url'] = $reply_to->bestUrl(); + } + $reply_to = null; + } + + $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/Orbited/README b/plugins/Orbited/README new file mode 100644 index 000000000..4abd40af7 --- /dev/null +++ b/plugins/Orbited/README @@ -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/Orbited/bayeux.class.inc.php b/plugins/Orbited/bayeux.class.inc.php new file mode 100644 index 000000000..39ad8a8fc --- /dev/null +++ b/plugins/Orbited/bayeux.class.inc.php @@ -0,0 +1,134 @@ + 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; + + private $sUser = ''; + private $sPassword = ''; + + public $sUrl = ''; + + function __construct($sUrl, $sUser='', $sPassword='') + { + $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); + + if (!is_null($sUser) && mb_strlen($sUser) > 0) { + curl_setopt($this->oCurl, CURLOPT_USERPWD,"$sUser:$sPassword"); + } + + $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/Orbited/jquery.comet.js b/plugins/Orbited/jquery.comet.js new file mode 100644 index 000000000..6de437fa8 --- /dev/null +++ b/plugins/Orbited/jquery.comet.js @@ -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 $.cometd, + * 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: + *
+     * var url2 = ...;
+     * var cometd2 = new $.Cometd();
+     * cometd2.init(url2);
+     * 
+ */ + $.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, 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: + *
+         * {
+         *     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.
+         * 
+ * @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/Orbited/json2.js b/plugins/Orbited/json2.js new file mode 100644 index 000000000..7e27df518 --- /dev/null +++ b/plugins/Orbited/json2.js @@ -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 ' '), + 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/Orbited/updatetimeline.js b/plugins/Orbited/updatetimeline.js new file mode 100644 index 000000000..170949e9b --- /dev/null +++ b/plugins/Orbited/updatetimeline.js @@ -0,0 +1,154 @@ +// update the local timeline from a Comet server +// + +var updater = function() +{ + var _server; + var _timeline; + var _userid; + var _replyurl; + var _favorurl; + var _deleteurl; + var _cometd; + + return { + init: function(server, timeline, userid, replyurl, favorurl, deleteurl) + { + _cometd = $.cometd; // Uses the default Comet object + _cometd.setLogLevel('debug'); + _cometd.init(server); + _server = server; + _timeline = timeline; + _userid = userid; + _favorurl = favorurl; + _replyurl = replyurl; + _deleteurl = deleteurl; + _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(1000); + NoticeHover(); + NoticeReply(); + } + + function makeNoticeItem(data) + { + user = data['user']; + html = data['html'].replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); + source = data['source'].replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); + + ni = "
  • "+ + "
    "+ + ""+ + ""+ + "\""+user['screen_name']+"\"/"+ + ""+user['screen_name']+""+ + ""+ + ""+ + "

    "+html+"

    "+ + "
    "+ + "
    "+ + "
    "+ + "
    Published
    "+ + "
    "+ + ""+ + "a few seconds ago"+ + " "+ + "
    "+ + "
    "+ + "
    "+ + "
    From
    "+ + "
    "+source+"
    "+ // may have a link, I think + "
    "; + + if (data['in_reply_to_status_id']) { + ni = ni+"
    "+ + "
    To
    "+ + "
    "+ + "in reply to"+ + "
    "+ + "
    "; + } + + ni = ni+"
    "+ + "
    "; + + if (_userid != 0) { + var input = $("form#form_notice fieldset input#token"); + var session_key = input.val(); + ni = ni+makeFavoriteForm(data['id'], session_key); + ni = ni+makeReplyLink(data['id'], data['user']['screen_name']); + if (_userid == data['user']['id']) { + ni = ni+makeDeleteLink(data['id']); + } + } + + ni = ni+"
    "+ + "
  • "; + return ni; + } + + function makeFavoriteForm(id, session_key) + { + var ff; + + ff = "
    "+ + "
    "+ + "Favor this notice"+ // XXX: i18n + ""+ + ""+ + ""+ + "
    "+ + "
    "; + return ff; + } + + function makeReplyLink(id, nickname) + { + var rl; + rl = "
    "+ + "
    Reply to this notice
    "+ + "
    "+ + "Reply "+id+""+ + ""+ + "
    "+ + "
    "; + return rl; + } + + function makeDeleteLink(id) + { + var dl, delurl; + delurl = _deleteurl.replace("0000000000", id); + + dl = "
    "+ + "
    Delete this notice
    "+ + "
    "+ + "Delete"+ + "
    "+ + "
    "; + + return dl; + } +}(); + -- cgit v1.2.3-54-g00ecf From 0d6314052fed12fd626bfa5519853f1d6ecb6814 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 1 Jul 2009 00:58:44 -0400 Subject: rename plugin --- plugins/Orbited/CometPlugin.php | 205 -------------------------------------- plugins/Orbited/OrbitedPlugin.php | 205 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 205 deletions(-) delete mode 100644 plugins/Orbited/CometPlugin.php create mode 100644 plugins/Orbited/OrbitedPlugin.php diff --git a/plugins/Orbited/CometPlugin.php b/plugins/Orbited/CometPlugin.php deleted file mode 100644 index 45251c66f..000000000 --- a/plugins/Orbited/CometPlugin.php +++ /dev/null @@ -1,205 +0,0 @@ -. - * - * @category Plugin - * @package Laconica - * @author Evan Prodromou - * @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 - * @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, $username=null, $password=null) - { - $this->server = $server; - $this->username = $username; - $this->password = $password; - - 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)), - ' '); - } - - $user = common_current_user(); - - if (!empty($user->id)) { - $user_id = $user->id; - } else { - $user_id = 0; - } - - $replyurl = common_local_url('newnotice'); - $favorurl = common_local_url('favor'); - // FIXME: need to find a better way to pass this pattern in - $deleteurl = common_local_url('deletenotice', - array('notice' => '0000000000')); - - $action->elementStart('script', array('type' => 'text/javascript')); - $action->raw("$(document).ready(function() { updater.init(\"$this->server\", \"$timeline\", $user_id, \"$replyurl\", \"$favorurl\", \"$deleteurl\"); });"); - $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, $this->user, $this->password); - - 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); - $arr['source'] = htmlspecialchars($arr['source']); - - if (!empty($notice->reply_to)) { - $reply_to = Notice::staticGet('id', $notice->reply_to); - if (!empty($reply_to)) { - $arr['in_reply_to_status_url'] = $reply_to->bestUrl(); - } - $reply_to = null; - } - - $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/Orbited/OrbitedPlugin.php b/plugins/Orbited/OrbitedPlugin.php new file mode 100644 index 000000000..45251c66f --- /dev/null +++ b/plugins/Orbited/OrbitedPlugin.php @@ -0,0 +1,205 @@ +. + * + * @category Plugin + * @package Laconica + * @author Evan Prodromou + * @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 + * @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, $username=null, $password=null) + { + $this->server = $server; + $this->username = $username; + $this->password = $password; + + 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)), + ' '); + } + + $user = common_current_user(); + + if (!empty($user->id)) { + $user_id = $user->id; + } else { + $user_id = 0; + } + + $replyurl = common_local_url('newnotice'); + $favorurl = common_local_url('favor'); + // FIXME: need to find a better way to pass this pattern in + $deleteurl = common_local_url('deletenotice', + array('notice' => '0000000000')); + + $action->elementStart('script', array('type' => 'text/javascript')); + $action->raw("$(document).ready(function() { updater.init(\"$this->server\", \"$timeline\", $user_id, \"$replyurl\", \"$favorurl\", \"$deleteurl\"); });"); + $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, $this->user, $this->password); + + 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); + $arr['source'] = htmlspecialchars($arr['source']); + + if (!empty($notice->reply_to)) { + $reply_to = Notice::staticGet('id', $notice->reply_to); + if (!empty($reply_to)) { + $arr['in_reply_to_status_url'] = $reply_to->bestUrl(); + } + $reply_to = null; + } + + $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); + } +} -- cgit v1.2.3-54-g00ecf From 630dcda56fa056389bd715e3d1df704addf2a8ca Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 20 Jul 2009 19:17:39 -0400 Subject: remove unused files for OrbitedPlugin --- plugins/Orbited/bayeux.class.inc.php | 134 ---- plugins/Orbited/jquery.comet.js | 1451 ---------------------------------- plugins/Orbited/json2.js | 478 ----------- 3 files changed, 2063 deletions(-) delete mode 100644 plugins/Orbited/bayeux.class.inc.php delete mode 100644 plugins/Orbited/jquery.comet.js delete mode 100644 plugins/Orbited/json2.js diff --git a/plugins/Orbited/bayeux.class.inc.php b/plugins/Orbited/bayeux.class.inc.php deleted file mode 100644 index 39ad8a8fc..000000000 --- a/plugins/Orbited/bayeux.class.inc.php +++ /dev/null @@ -1,134 +0,0 @@ - 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; - - private $sUser = ''; - private $sPassword = ''; - - public $sUrl = ''; - - function __construct($sUrl, $sUser='', $sPassword='') - { - $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); - - if (!is_null($sUser) && mb_strlen($sUser) > 0) { - curl_setopt($this->oCurl, CURLOPT_USERPWD,"$sUser:$sPassword"); - } - - $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/Orbited/jquery.comet.js b/plugins/Orbited/jquery.comet.js deleted file mode 100644 index 6de437fa8..000000000 --- a/plugins/Orbited/jquery.comet.js +++ /dev/null @@ -1,1451 +0,0 @@ -/** - * 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 $.cometd, - * 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: - *
    -     * var url2 = ...;
    -     * var cometd2 = new $.Cometd();
    -     * cometd2.init(url2);
    -     * 
    - */ - $.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, 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: - *
    -         * {
    -         *     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.
    -         * 
    - * @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/Orbited/json2.js b/plugins/Orbited/json2.js deleted file mode 100644 index 7e27df518..000000000 --- a/plugins/Orbited/json2.js +++ /dev/null @@ -1,478 +0,0 @@ -/* - 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 ' '), - 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'); - }; - } -}()); -- cgit v1.2.3-54-g00ecf From 9c2d0879e9064fe3b9cf9af2c7eabe869966bd92 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 21 Jul 2009 11:00:03 -0700 Subject: updated OrbitedPlugin to use RealtimePlugin --- plugins/Orbited/OrbitedPlugin.php | 186 ++++++++++---------------------------- 1 file changed, 47 insertions(+), 139 deletions(-) diff --git a/plugins/Orbited/OrbitedPlugin.php b/plugins/Orbited/OrbitedPlugin.php index 45251c66f..cf6e5de13 100644 --- a/plugins/Orbited/OrbitedPlugin.php +++ b/plugins/Orbited/OrbitedPlugin.php @@ -2,7 +2,7 @@ /** * Laconica, the distributed open-source microblogging tool * - * Plugin to do "real time" updates using Comet/Bayeux + * Plugin to do "real time" updates using Orbited + STOMP * * PHP version 5 * @@ -31,8 +31,13 @@ if (!defined('LACONICA')) { exit(1); } +require_once INSTALLDIR.'/plugins/Realtime/RealtimePlugin.php'; + /** - * Plugin to do realtime updates using Comet + * Plugin to do realtime updates using Orbited + STOMP + * + * This plugin pushes data to a STOMP server which is then served to the + * browser by the Orbited server. * * @category Plugin * @package Laconica @@ -41,165 +46,68 @@ if (!defined('LACONICA')) { * @link http://laconi.ca/ */ -class CometPlugin extends Plugin +class OrbitedPlugin extends RealtimePlugin { - var $server = null; + public $webserver = null; + public $webport = null; + public $channelbase = null; + public $stompserver = null; + public $username = null; + public $password = null; - function __construct($server=null, $username=null, $password=null) - { - $this->server = $server; - $this->username = $username; - $this->password = $password; + protected $con = null; - parent::__construct(); + function _getScripts() + { + $scripts = parent::_getScripts(); + $root = 'http://'.$this->webserver.(($this->webport == 80) ? '':':'.$this->webport); + $scripts[] = $root.'/static/Orbited.js'; + $scripts[] = $root.'/static/protocols/stomp/stomp.js'; + $scripts[] = common_path('plugins/Orbited/orbitedupdater.js'); + return $scripts; } - function onEndShowScripts($action) + function _updateInitialize($timeline, $user_id) { - $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)), - ' '); - } - - $user = common_current_user(); - - if (!empty($user->id)) { - $user_id = $user->id; - } else { - $user_id = 0; - } - - $replyurl = common_local_url('newnotice'); - $favorurl = common_local_url('favor'); - // FIXME: need to find a better way to pass this pattern in - $deleteurl = common_local_url('deletenotice', - array('notice' => '0000000000')); - - $action->elementStart('script', array('type' => 'text/javascript')); - $action->raw("$(document).ready(function() { updater.init(\"$this->server\", \"$timeline\", $user_id, \"$replyurl\", \"$favorurl\", \"$deleteurl\"); });"); - $action->elementEnd('script'); - - return true; + $script = parent::_updateInitialize($timeline, $user_id); + return $script." OrbitedUpdater.init(\"$this->stompserver\", $this->stompport, \"{$timeline}\");"; } - function onEndNoticeSave($notice) + function _connect() { - $this->log(LOG_INFO, "Called for save notice."); - - $timelines = array(); + require_once(INSTALLDIR.'/extlibs/Stomp.php'); - // XXX: Add other timelines; this is just for the public one + $stompserver = (empty($this->stompserver)) ? "tcp://{$this->webserver}:61613/" : $this->stompserver; - if ($notice->is_local || - ($notice->is_local == 0 && !common_config('public', 'localonly'))) { - $timelines[] = '/timelines/public'; - } + $this->con = new Stomp($stompserver); - $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, $this->user, $this->password); - - foreach ($timelines as $timeline) { - $this->log(LOG_INFO, "Posting notice $notice->id to '$timeline'."); - $bay->publish($timeline, $json); - } - - $bay = NULL; + if ($this->con->connect($this->username, $this->password)) { + $this->_log(LOG_INFO, "Connected."); + } else { + $this->_log(LOG_ERR, 'Failed to connect to queue server'); + throw new ServerException('Failed to connect to queue server'); } - - return true; } - function noticeAsJson($notice) + function _publish($channel, $message) { - // 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); - $arr['source'] = htmlspecialchars($arr['source']); + $result = $this->con->send($channel, + json_encode($message)); - if (!empty($notice->reply_to)) { - $reply_to = Notice::staticGet('id', $notice->reply_to); - if (!empty($reply_to)) { - $arr['in_reply_to_status_url'] = $reply_to->bestUrl(); - } - $reply_to = null; - } - - $profile = $notice->getProfile(); - $arr['user']['profile_url'] = $profile->profileurl; - - return $arr; + return $result; + // TODO: parse and deal with result } - function getNoticeTags($notice) + function _disconnect() { - $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; + $this->con->disconnect(); } - // Push this up to Plugin - - function log($level, $msg) + function _pathToChannel($path) { - common_log($level, get_class($this) . ': '.$msg); + if (!empty($this->channelbase)) { + array_unshift($path, $this->channelbase); + } + return '/' . implode('/', $path); } } -- cgit v1.2.3-54-g00ecf From 035978270d609b650b8e32f252366e0e75b12507 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 4 Oct 2009 03:02:04 -0400 Subject: Update OrbitedPlugin to work with RealtimePlugin framework --- plugins/Orbited/OrbitedPlugin.php | 43 ++++++++++- plugins/Orbited/orbitedupdater.js | 21 ++++++ plugins/Orbited/updatetimeline.js | 154 -------------------------------------- 3 files changed, 60 insertions(+), 158 deletions(-) create mode 100644 plugins/Orbited/orbitedupdater.js delete mode 100644 plugins/Orbited/updatetimeline.js diff --git a/plugins/Orbited/OrbitedPlugin.php b/plugins/Orbited/OrbitedPlugin.php index cf6e5de13..ad7d1d276 100644 --- a/plugins/Orbited/OrbitedPlugin.php +++ b/plugins/Orbited/OrbitedPlugin.php @@ -52,34 +52,49 @@ class OrbitedPlugin extends RealtimePlugin public $webport = null; public $channelbase = null; public $stompserver = null; + public $stompport = null; public $username = null; public $password = null; + public $webuser = null; + public $webpass = null; protected $con = null; function _getScripts() { $scripts = parent::_getScripts(); - $root = 'http://'.$this->webserver.(($this->webport == 80) ? '':':'.$this->webport); + + $port = (is_null($this->webport)) ? 8000 : $this->webport; + + $server = (is_null($this->webserver)) ? common_config('site', 'server') : $this->webserver; + + $root = 'http://'.$server.(($port == 80) ? '':':'.$port); + $scripts[] = $root.'/static/Orbited.js'; $scripts[] = $root.'/static/protocols/stomp/stomp.js'; $scripts[] = common_path('plugins/Orbited/orbitedupdater.js'); + return $scripts; } function _updateInitialize($timeline, $user_id) { $script = parent::_updateInitialize($timeline, $user_id); - return $script." OrbitedUpdater.init(\"$this->stompserver\", $this->stompport, \"{$timeline}\");"; + + $server = $this->_getStompServer(); + $port = $this->_getStompPort(); + + return $script." OrbitedUpdater.init(\"$server\", $port, ". + "\"{$timeline}\", \"{$this->webuser}\", \"{$this->webpass}\");"; } function _connect() { require_once(INSTALLDIR.'/extlibs/Stomp.php'); - $stompserver = (empty($this->stompserver)) ? "tcp://{$this->webserver}:61613/" : $this->stompserver; + $url = $this->_getStompUrl(); - $this->con = new Stomp($stompserver); + $this->con = new Stomp($url); if ($this->con->connect($this->username, $this->password)) { $this->_log(LOG_INFO, "Connected."); @@ -110,4 +125,24 @@ class OrbitedPlugin extends RealtimePlugin } return '/' . implode('/', $path); } + + function _getStompServer() + { + $server = (!is_null($this->stompserver)) ? $this->stompserver : + (!is_null($this->webserver)) ? $this->webserver : + common_config('site', 'server'); + return $server; + } + + function _getStompPort() + { + $port = (!is_null($this->stompport)) ? $this->stompport : 61613; + } + + function _getStompUrl() + { + $server = $this->_getStompServer(); + $port = $this->_getStompPort(); + return "tcp://$server:$port/"; + } } diff --git a/plugins/Orbited/orbitedupdater.js b/plugins/Orbited/orbitedupdater.js new file mode 100644 index 000000000..d70f4a4fd --- /dev/null +++ b/plugins/Orbited/orbitedupdater.js @@ -0,0 +1,21 @@ +// Update the local timeline from a Orbited server + +var OrbitedUpdater = function() +{ + return { + + init: function(server, port, timeline, username, password) + { + // set up stomp client. + stomp = new STOMPClient(); + + stomp.connect(server, port, username, password); + stomp.subscribe(timeline); + + stomp.onmessageframe = function(frame) { + RealtimeUpdate.receive(JSON.parse(frame.body)); + }; + }; + } +}(); + diff --git a/plugins/Orbited/updatetimeline.js b/plugins/Orbited/updatetimeline.js deleted file mode 100644 index 170949e9b..000000000 --- a/plugins/Orbited/updatetimeline.js +++ /dev/null @@ -1,154 +0,0 @@ -// update the local timeline from a Comet server -// - -var updater = function() -{ - var _server; - var _timeline; - var _userid; - var _replyurl; - var _favorurl; - var _deleteurl; - var _cometd; - - return { - init: function(server, timeline, userid, replyurl, favorurl, deleteurl) - { - _cometd = $.cometd; // Uses the default Comet object - _cometd.setLogLevel('debug'); - _cometd.init(server); - _server = server; - _timeline = timeline; - _userid = userid; - _favorurl = favorurl; - _replyurl = replyurl; - _deleteurl = deleteurl; - _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(1000); - NoticeHover(); - NoticeReply(); - } - - function makeNoticeItem(data) - { - user = data['user']; - html = data['html'].replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); - source = data['source'].replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); - - ni = "
  • "+ - "
    "+ - ""+ - ""+ - "\""+user['screen_name']+"\"/"+ - ""+user['screen_name']+""+ - ""+ - ""+ - "

    "+html+"

    "+ - "
    "+ - "
    "+ - "
    "+ - "
    Published
    "+ - "
    "+ - ""+ - "a few seconds ago"+ - " "+ - "
    "+ - "
    "+ - "
    "+ - "
    From
    "+ - "
    "+source+"
    "+ // may have a link, I think - "
    "; - - if (data['in_reply_to_status_id']) { - ni = ni+"
    "+ - "
    To
    "+ - "
    "+ - "in reply to"+ - "
    "+ - "
    "; - } - - ni = ni+"
    "+ - "
    "; - - if (_userid != 0) { - var input = $("form#form_notice fieldset input#token"); - var session_key = input.val(); - ni = ni+makeFavoriteForm(data['id'], session_key); - ni = ni+makeReplyLink(data['id'], data['user']['screen_name']); - if (_userid == data['user']['id']) { - ni = ni+makeDeleteLink(data['id']); - } - } - - ni = ni+"
    "+ - "
  • "; - return ni; - } - - function makeFavoriteForm(id, session_key) - { - var ff; - - ff = "
    "+ - "
    "+ - "Favor this notice"+ // XXX: i18n - ""+ - ""+ - ""+ - "
    "+ - "
    "; - return ff; - } - - function makeReplyLink(id, nickname) - { - var rl; - rl = "
    "+ - "
    Reply to this notice
    "+ - "
    "+ - "Reply "+id+""+ - ""+ - "
    "+ - "
    "; - return rl; - } - - function makeDeleteLink(id) - { - var dl, delurl; - delurl = _deleteurl.replace("0000000000", id); - - dl = "
    "+ - "
    Delete this notice
    "+ - "
    "+ - "Delete"+ - "
    "+ - "
    "; - - return dl; - } -}(); - -- cgit v1.2.3-54-g00ecf From 27ff66c9de64a60cb3219f6d823cb290d4f18c01 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 4 Oct 2009 04:05:40 -0400 Subject: Some changes required from Orbited debugging --- plugins/Orbited/OrbitedPlugin.php | 18 ++++++++++++------ plugins/Orbited/orbitedextra.js | 2 ++ plugins/Orbited/orbitedupdater.js | 11 +++++++---- 3 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 plugins/Orbited/orbitedextra.js diff --git a/plugins/Orbited/OrbitedPlugin.php b/plugins/Orbited/OrbitedPlugin.php index ad7d1d276..ba87b266a 100644 --- a/plugins/Orbited/OrbitedPlugin.php +++ b/plugins/Orbited/OrbitedPlugin.php @@ -60,6 +60,12 @@ class OrbitedPlugin extends RealtimePlugin protected $con = null; + function onStartShowHeadElements($action) + { + // See http://orbited.org/wiki/Deployment#Cross-SubdomainDeployment + $action->element('script', null, ' document.domain = document.domain; '); + } + function _getScripts() { $scripts = parent::_getScripts(); @@ -71,6 +77,7 @@ class OrbitedPlugin extends RealtimePlugin $root = 'http://'.$server.(($port == 80) ? '':':'.$port); $scripts[] = $root.'/static/Orbited.js'; + $scripts[] = common_path('plugins/Orbited/orbitedextra.js'); $scripts[] = $root.'/static/protocols/stomp/stomp.js'; $scripts[] = common_path('plugins/Orbited/orbitedupdater.js'); @@ -90,16 +97,16 @@ class OrbitedPlugin extends RealtimePlugin function _connect() { - require_once(INSTALLDIR.'/extlibs/Stomp.php'); + require_once(INSTALLDIR.'/extlib/Stomp.php'); $url = $this->_getStompUrl(); $this->con = new Stomp($url); if ($this->con->connect($this->username, $this->password)) { - $this->_log(LOG_INFO, "Connected."); + $this->log(LOG_INFO, "Connected."); } else { - $this->_log(LOG_ERR, 'Failed to connect to queue server'); + $this->log(LOG_ERR, 'Failed to connect to queue server'); throw new ServerException('Failed to connect to queue server'); } } @@ -128,15 +135,14 @@ class OrbitedPlugin extends RealtimePlugin function _getStompServer() { - $server = (!is_null($this->stompserver)) ? $this->stompserver : + return (!is_null($this->stompserver)) ? $this->stompserver : (!is_null($this->webserver)) ? $this->webserver : common_config('site', 'server'); - return $server; } function _getStompPort() { - $port = (!is_null($this->stompport)) ? $this->stompport : 61613; + return (!is_null($this->stompport)) ? $this->stompport : 61613; } function _getStompUrl() diff --git a/plugins/Orbited/orbitedextra.js b/plugins/Orbited/orbitedextra.js new file mode 100644 index 000000000..47e5c0c80 --- /dev/null +++ b/plugins/Orbited/orbitedextra.js @@ -0,0 +1,2 @@ +TCPSocket = Orbited.TCPSocket; + diff --git a/plugins/Orbited/orbitedupdater.js b/plugins/Orbited/orbitedupdater.js index d70f4a4fd..8c5ab3b73 100644 --- a/plugins/Orbited/orbitedupdater.js +++ b/plugins/Orbited/orbitedupdater.js @@ -9,13 +9,16 @@ var OrbitedUpdater = function() // set up stomp client. stomp = new STOMPClient(); - stomp.connect(server, port, username, password); - stomp.subscribe(timeline); - stomp.onmessageframe = function(frame) { RealtimeUpdate.receive(JSON.parse(frame.body)); }; - }; + + stomp.onconnectedframe = function() { + stomp.subscribe(timeline); + } + + stomp.connect(server, port, username, password); + } } }(); -- cgit v1.2.3-54-g00ecf From 091e7b908befb7d24404ea653f59b38063e04f69 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 4 Oct 2009 04:10:15 -0400 Subject: need to show scripts at end of body in RealtimePlugin --- plugins/Realtime/RealtimePlugin.php | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/Realtime/RealtimePlugin.php b/plugins/Realtime/RealtimePlugin.php index 0f0d0f9f4..181927968 100644 --- a/plugins/Realtime/RealtimePlugin.php +++ b/plugins/Realtime/RealtimePlugin.php @@ -230,6 +230,7 @@ class RealtimePlugin extends Plugin } $action->showContentBlock(); + $action->showScripts(); $action->elementEnd('body'); return false; // No default processing } -- cgit v1.2.3-54-g00ecf From affe00276a008038e8a8b37b3812215e382fe98e Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 4 Oct 2009 04:11:10 -0400 Subject: remove spurious readme from Orbited --- plugins/Orbited/README | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 plugins/Orbited/README diff --git a/plugins/Orbited/README b/plugins/Orbited/README deleted file mode 100644 index 4abd40af7..000000000 --- a/plugins/Orbited/README +++ /dev/null @@ -1,26 +0,0 @@ -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. -- cgit v1.2.3-54-g00ecf From df8cce27887a3a7cd813ade61b9bd47e945d9ddc Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Sun, 4 Oct 2009 16:51:43 +0000 Subject: Removed obsolete iphone theme (pre v0.7) --- theme/iphone/bg-body.gif | Bin 136 -> 0 bytes theme/iphone/bg-header.gif | Bin 2469 -> 0 bytes theme/iphone/default-avatar-mini.png | Bin 646 -> 0 bytes theme/iphone/default-avatar-profile.png | Bin 2853 -> 0 bytes theme/iphone/default-avatar-stream.png | Bin 1487 -> 0 bytes theme/iphone/display.css | 698 -------------------------------- theme/iphone/display.css.dist | 686 ------------------------------- theme/iphone/ie6.css.dist | 63 --- theme/iphone/ie7.css.dist | 20 - theme/iphone/login-bg.gif | Bin 328 -> 0 bytes theme/iphone/logo.png | Bin 818 -> 0 bytes 11 files changed, 1467 deletions(-) delete mode 100644 theme/iphone/bg-body.gif delete mode 100644 theme/iphone/bg-header.gif delete mode 100644 theme/iphone/default-avatar-mini.png delete mode 100644 theme/iphone/default-avatar-profile.png delete mode 100644 theme/iphone/default-avatar-stream.png delete mode 100644 theme/iphone/display.css delete mode 100644 theme/iphone/display.css.dist delete mode 100644 theme/iphone/ie6.css.dist delete mode 100644 theme/iphone/ie7.css.dist delete mode 100644 theme/iphone/login-bg.gif delete mode 100644 theme/iphone/logo.png diff --git a/theme/iphone/bg-body.gif b/theme/iphone/bg-body.gif deleted file mode 100644 index d87e2e8d7..000000000 Binary files a/theme/iphone/bg-body.gif and /dev/null differ diff --git a/theme/iphone/bg-header.gif b/theme/iphone/bg-header.gif deleted file mode 100644 index 5154b2e5e..000000000 Binary files a/theme/iphone/bg-header.gif and /dev/null differ diff --git a/theme/iphone/default-avatar-mini.png b/theme/iphone/default-avatar-mini.png deleted file mode 100644 index 38b8692b4..000000000 Binary files a/theme/iphone/default-avatar-mini.png and /dev/null differ diff --git a/theme/iphone/default-avatar-profile.png b/theme/iphone/default-avatar-profile.png deleted file mode 100644 index f8357d4fc..000000000 Binary files a/theme/iphone/default-avatar-profile.png and /dev/null differ diff --git a/theme/iphone/default-avatar-stream.png b/theme/iphone/default-avatar-stream.png deleted file mode 100644 index 6b63baa70..000000000 Binary files a/theme/iphone/default-avatar-stream.png and /dev/null differ diff --git a/theme/iphone/display.css b/theme/iphone/display.css deleted file mode 100644 index 1838a8e86..000000000 --- a/theme/iphone/display.css +++ /dev/null @@ -1,698 +0,0 @@ -/* CSS Document */ -/* Design & CSS by Marie-Claude Doyon http://www.marieclaudedoyon.com */ -/* Simplified for mobile by Ken Sheppardson http://identi.ca/kshep */ - -@import url(../../base/css/display.css); - -html {} -body { - width: 100%; - padding: 0; - margin: 0; - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 24px; - min-height: 100%; - height: 100%; - color: #193441; -} - -a { - color: #C15D42; - text-decoration: none; -} -a:hover { - text-decoration: underline; -} -img, img a { - border: 0; -} -h1 { - font-size: 1.2em; -} - -#wrap { - margin: 0; -} - -#header { - width: 100%; - float: left; - background-color: #193441; - margin: 0 0 20px 0; - padding: 0; -} -#logo { - float: left; - margin: 10px 0px 0px 10px; -} -p#branding { - margin: 0; - padding: 6px 0 3px 0; - color: #fbf2d7; - font-size: 2em; - font-weight: bold; - line-height: 2.5em; -} -p#branding a { - color: #dab134; -} - -#header h1.pagetitle { - display: none; - margin: 0; - padding: 0; - font-size: 1.2em; - line-height: 2em; - color: #d8e2d7; -} - -#header h2.sitename { - display: none; - margin: 0; - padding: 0; - color: #FCFFF5; -} - -/* ===== Begin Navigation Styling ===== */ - -/* ----- Navigation ------ */ -#nav { - float: right; - margin: 0; - padding: 0; - list-style-type: none; - font-size: 1.2em; -} -#nav li { - display: block; - float: left; -} -#nav li a { - display: block; - padding: 9px 15px 12px 0px; - color: #91AA9D; -} -#nav li a:hover { - text-decoration: underline; -} - -/* ----- Tabs ----- */ -#nav_views { - clear: both; - float: left; - margin: 10px 0px 0px 5px; - padding: 0; - bottom: 0; - list-style-type: none; - font-size: 1.1em; - font-weight: bold; -} -#nav_views li { - display: block; - float: left; - line-height: 1.3em; -} -#nav_views li a { - display: block; - margin: 0; - padding: 4px 12px 3px 12px; - color: #FCFFF5; - background-color: #91AA9D; - border-right: 1px solid #6A8787; -} -#nav_views li a:hover { - text-decoration: none; -} -#nav_views li.current a, #nav_views li.current a:hover { - color: #3F606F; - background-color: #FCFFF5; - border-right: 1px solid #6A8787; -} -#nav_views li.current a:hover { - color: #193441; -} -#nav_views li a:hover { - color: #FCFFF5; - background-color: #3F606F; - border-right: 1px solid #6A8787; -} - -/* ----- Nav Footer ----- */ -#nav_sub { - clear: both; - margin: 18px 10px 0 10px; - padding: 0; - list-style-type: none; - font-size: 1.1em; - font-weight: bold; - line-height: 2em; - border-top: 1px solid #D8E2D7; -} -#nav_sub li { - display: block; - float: left; -} -#nav_sub li a { - padding: 6px 24px 6px 0; -} -#nav_sub li a:hover { - text-decoration: underline; -} -/* ===== End Navigation Styling ===== */ - -#content { - clear: left; - margin: 10px; - font-family: Georgia, "Times New Roman", Times, serif; - font-size: 1em; - line-height: 1.1em; -} -#content h2 { - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 1.1em; -} -#content label { - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 1.1em; -} - -.instructions { - clear: both; - float: left; - margin: 5px 5px 10px 5px; -} -.instructions p, .success, .error { - font-weight: normal; - margin: 0; - padding: 10px; - font-family: Georgia, "Times New Roman", Times, serif; - font-size: 1.1em; - line-height: 1.2em; - border: 1px solid #91AA9D; - color: #FCFFF5; -} -.instructions a, .success a, .error a { - color: #d8e2d7; - text-decoration: underline; -} -.instructions a:hover, .success a:hover, .error a:hover { - color: #FCFFF5; -} -.success { - clear: both; - float: left; - margin: 5px 5px 10px 5px; - background-color: #48705b; -} -.error { - clear: both; - float: left; - margin: 5px 5px 10px 5px; - background-color: #ce3728; -} - -/* ----- Stream -----*/ - -#notices { - clear: both; - margin: 0 auto; - padding: 0; - list-style-type: none; - border-top: 1px solid #D8E2D7; -} -#notices a:hover { - text-decoration: underline; -} -.notice_single { - clear: both; - display: block; - margin: 0; - padding: 5px 5px 5px 0; - min-height: 48px; - font-family: Georgia, "Times New Roman", Times, serif; - font-size: 1em; - line-height: 1.4em; - border-bottom: 1px solid #D8E2D7; -} -.notice_single:hover { - background-color: #F3F8EA; -} -.notice_single p { - display: inline; - margin: 0; - padding: 0; -} -#notice_delete_form #confirmation_text { - display: block; - font-size: 1.1em; - font-weight: bold; -} -input#submit_yes, input#submit_no { - margin: 18px 10px 0px 0px; - padding: 4px; - font-weight: bold; - color: #FCFFF5; - background-color: #C15D42; - cursor: pointer; - border: 0; - width: 40px; -} -input#submit_yes:hover, input#submit_no:hover { - background-color: #904632; -} -.avatar.stream { - float: left; - margin: 0 10px 0 0; -} -p.time { - display: block; - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 0.9em; - line-height: 2em; -} -p.time a { - color: #91AA9D; -} - -/* ----- Profile -----*/ -#profile { - clear: both; - float: left; - padding: 10px 0 0 0; - border-top: 1px solid #D8E2D7; - font-family: Georgia, "Times New Roman", Times, serif; -} -#profile h1 { - clear: both; - float: left; - margin: 0; - padding: 0; - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 1.2em; -} -#profile h2 { - clear: both; - float: left; - margin: 0; - padding: 1em 0 0.2em 0; - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 1.1em; - text-transform: uppercase; - color: #91AA9D; -} -#profile p { - clear: both; - float: left; - margin: 0 10px 0 0; - font-size: 1em; - line-height: 1.4em; -} -#profile p.location { - margin: 0 10px 12px 0; - font-style: italic; -} -#profile p.notice_current { - font-size: 1.2em; - line-height: 1.3em; -} -#profile_avatar { - float: left; - margin-right: 4px; -} -#profile_avatar img { - margin-bottom: 5px; -} -.avatar.profile { - clear: left; - margin: 0 10px 5px 0; -} -.avatar.original { - float: left; - margin: 0 10px 18px 0; -} -a.nickname { - font-family: Verdana, Arial, Helvetica, sans-serif; - font-weight: bold; - font-size: 1.1em; - padding-right: 3px; -} -#profile_information { - float: left; -} - -.statistics { - clear: both; - float: left; -} -.statistics h2 { - clear: both; - float: left; - margin: 12px 0 3px 0; -} -dl.statistics { - margin: 0; -} -.statistics dt { - clear: left; - float: left; - width: 200px; -} -.statistics dd { - float: left; -} -.statistics dt:after { - content: ":"; -} -#subscriptions { - clear: both; - float: left; - margin: 18px 0 30px 0; -} -#subscriptions_avatars { - clear: both; - float: left; - margin: 6px 0 0 0; - padding: 0; - list-style-type: none; -} -#subscriptions_avatars li .avatar.mini { - float: left; - margin: 0 3px 3px 0; - padding: 0; - line-height: 0; -} -#subscriptions_viewall { - clear: left; -} -/* ----- End Profile -----*/ - -/* ----- Begin Subscriptions & Subscribers -----*/ - -ul.subscriptions, ul.subscribers { - float: none; - margin: 0; - padding: 0; - list-style-type: none; - overflow: auto; -} -ul.subscriptions li, ul.subscribers li { - display: block; - float: left; - padding: 0; -} -/* ----- End Subscriptions & Subscribers -----*/ - -#pagination { - margin: 18px auto; -} -#nav_pagination { - margin: 0 0 36px 0; - padding: 0; - float: right; - list-style-type: none; - font-size: 12px; - font-weight: bold; -} -#nav_pagination li { - display: block; - float: left; - background-color: #91AA9D; -} -#nav_pagination li.before { - margin-right: 1px; -} -#nav_pagination li a { - padding: 6px 15px; - line-height: 2em; - background-color: #91AA9D; - color: #FCFFF5; -} -#nav_pagination li a:hover { - background-color: #3F606F; - color: #FCFFF5; - text-decoration: none; -} - -#footer { - clear: both; - margin: 10px; - border-top: 1px solid #D8E2D7; -} -#footer p { - font-size: 0.8em; - margin-top: 1em; - line-height: 1.2em; -} -#cc { - float: left; - margin: 3px 10px 0 0; -} - -/* ===== Begin Forms Styling ===== */ - -/* ----- Forms General Style ----- */ -form { - margin: 0 auto; - padding: 0; -} -form { - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 1em; -} -form label { - display: block; - font-size: 1em; - font-weight: bold; - line-height: 1.5em; -} -form input { - border: 1px solid #D8E2D7; - width: 264px; -} -input#submit, input.submit { - display: block; - margin: 18px 0; - padding: 4px; - font-weight: bold; - color: #FCFFF5; - background-color: #C15D42; - cursor: pointer; - border: 0; - width: auto; -} -input#submit:hover, input.submit:hover { - background-color: #904632; -} -input.checkbox { - width: auto; - border: 0; -} -textarea, input { - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 1em; - color: #193441; - padding: 3px; -} -textarea:focus, input:focus { - background-color: #f0f6eb; -} -textarea { - width: 270px; - border: 1px solid #D8E2D7; -} -.input_instructions { - margin-top: 3px; - display: block; - font-size: 1em; - line-height: 1.2em; - color: #91aa9d; - font-family: Verdana, Arial, Helvetica, sans-serif; -} - -/* ----- Status Form ----- */ -#status_form { - width: 100%; - margin: 0px 0px 10px 5px; -} -#status_form p { - margin: 0; - padding: 0; -} -#status_label { - display: none; - clear: both; - margin: 0; - padding: 0 0 3px 0; - font-size: 1.5em; - font-weight: bold; - line-height: 2em; - color: #91AA9D; -} -#status_textarea { - display: block; - float: left; - width: 70%; - height: 3em; - margin: 0 0 10px 0; - padding: 0; - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 1.1em; - color: #193441; - border: 0; -} -#status_submit { - display: block; - float: left; - margin: 0 0 0 4px; - padding: 1em 10px 1em 10px; - line-height: 1em; - width: 10%; - background-color: #C15D42; - font-family: Verdana, Arial, Helvetica, sans-serif; - font-weight: bold; - font-size: 1em; - color: #FCFFF5; - cursor: pointer; - border: 0; -} -#status_submit:hover { - background-color: #904632; -} -#counter { - padding: 1em .5em 1em 5px; - color: #fff; - clear: both; - float: left; - font-weight: bold; - text-align: right; -} -/* ----- Subscribe Form ----- */ -#subscribe .submit, #unsubscribe .submit, #remotesubscribe .button, #remotesubscribe { - clear: left; - margin: 0; - width: 96px; - height: 27px; - font-family: verdana, arial, helvetica, sans-serif; - font-weight: bold; - font-size: 12px; - text-transform: uppercase; - background-color: #c15d42; - color: #fcfff5; - border: 0; -} -#remotesubscribe { - width: 96px; - height: 22px; - padding: 5px 0 0 0; - text-align: center; -} -#subscribe .button:hover, #unsubscribe .button:hover { - background-color: #904632; - cursor: pointer; -} - -a#remotesubscribe { - display: block; -} - -/* ----- Login Form -----*/ -input#license { - width: auto; - border: 0; -} -/* ----- Avatar Form -----*/ -form { - clear: left; -} - -/* ----- OpenID Form -----*/ - -input#openid_url { - background: url(login-bg.gif) no-repeat; - background-color: #fff; - background-position: 4px 50%; - color: #000; - padding-left: 24px; -} - -/* People lists (search results, maybe subscribers) */ - -#profiles { - clear: both; - margin: 0 auto; - padding: 0; - list-style-type: none; - border-top: 1px solid #D8E2D7; -} -#profiles a:hover { - text-decoration: underline; -} - -.profile_single { - clear: both; - display: block; - margin: 0; - padding: 5px 5px 5px 0; - min-height: 48px; - font-family: Georgia, "Times New Roman", Times, serif; - font-size: 1.2em; - line-height: 1.4em; - border-bottom: 1px solid #D8E2D7; -} -.profile_single:hover { - background-color: #F3F8EA; -} - -/* ----- IM Settings Form -----*/ - -#imsettings p { - margin: 0; - padding: 0; - line-height: 1.3em; -} - -/* ===== End Forms Styling ===== */ - -/* ===== Tag Cloud Styling ===== */ - -p.tagcloud { -text-align: center; -} - -p.tagcloud a { -line-height:1em; -vertical-align:middle; -} - -p.tagcloud a.largest { -font-size: 4em; -} -p.tagcloud a.verylarge { -font-size: 3em; -} - -p.tagcloud a.large { -font-size: 2em; -} - -p.tagcloud a.medium { -font-size: 1.5em; -} - -p.tagcloud a.small { -font-size: 1em; -} - -p.tagcloud a.verysmall { -font-size: 80%; -} - -p.tagcloud a.smallest { -font-size: 60%; -} - -a.replybutton { - border: 1px solid #D8E2D7; - padding: 0px 10px 0px 10px; - line-height: 0.8em; -} diff --git a/theme/iphone/display.css.dist b/theme/iphone/display.css.dist deleted file mode 100644 index 395da2e2f..000000000 --- a/theme/iphone/display.css.dist +++ /dev/null @@ -1,686 +0,0 @@ -/* CSS Document */ -/* Design & CSS by Marie-Claude Doyon http://www.marieclaudedoyon.com */ - -html { - background: url(bg-body.gif) repeat-y top center #d8e2d7; - } -body { - position: absolute; - width: 100%; - margin: 0; - padding: 0; - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 10px; - line-height: 12px; - min-height: 100%; - height: 100%; - color: #193441; - } -a { - color: #C15D42; - text-decoration: none; - } -a:hover { - text-decoration: underline; - } -img, img a { - border: 0; - } -h1 { - font-size: 14px; - } - - -#wrap { - margin: 0 auto; - padding: 0 20px; - width: 760px; - background: url(bg-header.gif) repeat-x #FCFFF5; - } -#header { - position: relative; - margin: 0 auto; - width: 540px; - height: 216px; - } -#logo { - margin-top: 9px; - } -p#branding { - margin: 0; - padding: 6px 0 3px 0; - color: #fbf2d7; - font-size: 21px; - font-weight: bold; - line-height: 27px; - } -p#branding a { - color: #dab134; - } - -#header h1.pagetitle { - margin: 0; - padding: 0; - font-size: 15px; - line-height: 24px; - color: #d8e2d7; -} - -#header h2.sitename { - display: none; - margin: 0; - padding: 0; - color: #FCFFF5; -} - -/* ===== Begin Navigation Styling ===== */ - -/* ----- Navigation ------ */ -#nav { - float: right; - margin: 0; - padding: 0; - list-style-type: none; - font-size: 12px; - } -#nav li { - display: block; - float: left; - } -#nav li a { - display: block; - padding: 9px 9px 12px 9px; - color: #91AA9D; - } -#nav li a:hover { - text-decoration: underline; - } - -/* ----- Tabs ----- */ -#nav_views { - margin: 0 auto; - padding: 0; - position: absolute; - bottom: 0; - list-style-type: none; - font-size: 14px; - font-weight: bold; - width: 540px; - /*height: 30px;*/ - } -#nav_views li { - display: block; - float: left; - line-height: 21px; - } -#nav_views li a { - display: block; - margin: 0; - padding: 4px 12px 3px 12px; - color: #FCFFF5; - background-color: #91AA9D; - border-right: 1px solid #6A8787; - } -#nav_views li a:hover { - text-decoration: none; - } -#nav_views li.current a, #nav_views li.current a:hover { - color: #3F606F; - background-color: #FCFFF5; - border-right: 1px solid #6A8787; - } -#nav_views li.current a:hover { - color: #193441; - } -#nav_views li a:hover { - color: #FCFFF5; - background-color: #3F606F; - border-right: 1px solid #6A8787; - } - -/* ----- Nav Footer ----- */ -#nav_sub { - clear: both; - margin: 18px auto 0 auto; - padding: 0; - list-style-type: none; - font-size: 11px; - font-weight: bold; - line-height: 21px; - border-top: 1px solid #D8E2D7; - width: 540px; - } -#nav_sub li { - display: block; - float: left; - } -#nav_sub li a { - padding: 6px 24px 6px 0; - } -#nav_sub li a:hover { - text-decoration: underline; - } -/* ===== End Navigation Styling ===== */ - -#content { - clear: left; - margin: 40px 0 45px 0; - padding: 0 110px; - font-family: Georgia, "Times New Roman", Times, serif; - font-size: 14px; - line-height: 18px; - } -#content h2 { - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 15px; - } -#content label { - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 12px; - } -.instructions p, .success, .error { - font-weight: normal; - margin: 36px 0 0 0; - padding: 10px; - font-family: Georgia, "Times New Roman", Times, serif; - font-size: 13px; - line-height: 15px; - border: 1px solid #91AA9D; - color: #FCFFF5; - } -.instructions a, .success a, .error a { - color: #d8e2d7; - text-decoration: underline; - } -.instructions a:hover, .success a:hover, .error a:hover { - color: #FCFFF5; - } -.success { - background-color: #48705b; - } -.error { - background-color: #ce3728; - } - - -/* ----- Stream -----*/ - -#notices { - clear: both; - margin: 0 auto; - padding: 0; - list-style-type: none; - width: 540px; - border-top: 1px solid #D8E2D7; - } -#notices a:hover { - text-decoration: underline; - } -.notice_single { - clear: both; - display: block; - margin: 0; - padding: 5px 5px 5px 0; - min-height: 48px; - font-family: Georgia, "Times New Roman", Times, serif; - font-size: 13px; - line-height: 16px; - border-bottom: 1px solid #D8E2D7; - } -.notice_single:hover { - background-color: #F3F8EA; - } -.notice_single p { - display: inline; - margin: 0; - padding: 0; - } -#notice_delete_form #confirmation_text { - display: block; - font-size: 14px; - font-weight: bold; - } -input#submit_yes, input#submit_no { - margin: 18px 10px 0px 0px; - padding: 4px; - font-weight: bold; - color: #FCFFF5; - background-color: #C15D42; - cursor: pointer; - border: 0; - width: 40px; - } -input#submit_yes:hover, input#submit_no:hover { - background-color: #904632; - } -.avatar.stream { - float: left; - margin: 0 10px 0 0; - } -p.time { - display: block; - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 10px; - line-height: 15px; - } -p.time a { - color: #91AA9D; - } - -/* ----- Profile -----*/ -#profile { - clear: left; - margin: 0 -110px; - padding: 10px 0 0 0; - min-height: 170px; - border-top: 1px solid #D8E2D7; - font-family: Georgia, "Times New Roman", Times, serif; - } -#profile h1 { - margin: 0; - padding: 0; - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 14px; - } -#profile h2 { - margin: 0; - padding: 0; - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 11px; - text-transform: uppercase; - color: #91AA9D; - } -#profile p { - margin: 0 10px 0 0; - font-size: 12px; - line-height: 14px; - } -#profile p.location { - margin: 0 10px 12px 0; - font-style: italic; - } -#profile p.notice_current { - font-size: 18px; - line-height: 21px; - } -#profile_avatar { - float: left; - margin-right: 4px; - } -#profile_avatar img { - margin-bottom: 5px; - } -.avatar.profile { - clear: left; - margin: 0 10px 5px 0; - } -.avatar.original { - float: left; - margin: 0 10px 18px 0; - } -a.nickname { - font-family: Verdana, Arial, Helvetica, sans-serif; - font-weight: bold; - font-size: 12px; - padding-right: 3px; - } -#profile_information { - float: left; - position: relative; - width: 270px; - height: 200px; - } -.statistics { - margin-top: 18px; - } -.statistics h2 { - margin: 12px 0 3px 0; - } -dl.statistics { - margin: 0; - font-size: 12px; - line-height: 14px; - } -.statistics dt { - float: left; - width: 96px; -} -.statistics dd { - margin-left: 100px; -} -.statistics dt:after { - content: ":"; - } -#subscriptions { - float: left; - margin: 18px 0 30px 0; - } -#subscriptions_avatars { - float: left; - margin: 6px 0 0 0; - padding: 0; - list-style-type: none; - width: 270px; - } -#subscriptions_avatars li .avatar.mini { - float: left; - margin: 0 3px 3px 0; - padding: 0; - line-height: 0; - /* border: 1px solid #f00; */ - } -#subscriptions_viewall { - clear: left; - } -/* ----- End Profile -----*/ - -/* ----- Begin Subscriptions & Subscribers -----*/ - -ul.subscriptions, ul.subscribers { - float: none; - margin: 0; - padding: 0; - list-style-type: none; - overflow: auto; - } -ul.subscriptions li, ul.subscribers li { - display: block; - float: left; - padding: 0; - } -/* ----- End Subscriptions & Subscribers -----*/ - - - -#pagination { - margin: 18px auto; - width: 540px; - } -#nav_pagination { - margin: 0 0 36px 0; - padding: 0; - float: right; - list-style-type: none; - font-size: 12px; - font-weight: bold; - } -#nav_pagination li { - display: block; - float: left; - background-color: #91AA9D; - } -#nav_pagination li.before { - margin-right: 1px; - } -#nav_pagination li a { - padding: 6px 15px; - line-height: 21px; - background-color: #91AA9D; - color: #FCFFF5; - } -#nav_pagination li a:hover { - background-color: #3F606F; - color: #FCFFF5; - text-decoration: none; - } - -#footer { - clear: both; - margin: 0 auto; - padding: 0 0 36px 0; - width: 540px; - border-top: 1px solid #D8E2D7; - } -#footer p { - margin-top: 9px; - line-height: 12px; - } -#cc { - float: left; - margin: 3px 10px 0 0; - } - -/* ===== Begin Forms Styling ===== */ - -/* ----- Forms General Style ----- */ -form { - margin: 0 auto; - padding: 0; - } -form { - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 12px; - } -form label { - display: block; - font-size: 12px; - font-weight: bold; - line-height: 18px; - } -form input { - border: 1px solid #D8E2D7; - width: 264px; - } -input#submit, input.submit { - display: block; - margin: 18px 0; - padding: 4px; - font-weight: bold; - color: #FCFFF5; - background-color: #C15D42; - cursor: pointer; - border: 0; - width: auto; - } -input#submit:hover, input.submit:hover { - background-color: #904632; - } -input.checkbox { - /*width: 14px; - height: 14px;*/ - width: auto; - border: 0; - } -textarea, input { - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 12px; - color: #193441; - padding: 3px; - } -textarea:focus, input:focus { - background-color: #f0f6eb; - } -textarea { - width: 270px; - border: 1px solid #D8E2D7; - } -.input_instructions { - margin-top: 3px; - display: block; - font-size: 11px; - line-height: 15px; - color: #91aa9d; - font-family: Verdana, Arial, Helvetica, sans-serif; - } - -/* ----- Status Form ----- */ -#status_form { - height: 96px; - /*background-color: #F00;*/ - } -#status_form p { - margin: 36px 0 0 0; - padding: 0; - } -#status_label { - display: block; - clear: both; - margin: 0; - padding: 0 0 3px 0; - font-size: 18px; - font-weight: bold; - line-height: 24px; - color: #91AA9D; - } -#status_textarea { - display: block; - float: left; - width: 463px; - height: 35px; - padding: 5px; - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 12px; - color: #193441; - border: 0; - } -#status_submit { - display: block; - float: left; - margin: 1px 0 0 4px; - width: 63px; - height: 45px; - background-color: #C15D42; - font-family: Verdana, Arial, Helvetica, sans-serif; - font-weight: bold; - font-size: 14px; - color: #FCFFF5; - cursor: pointer; - border: 0; - } -#status_submit:hover { - background-color: #904632; - } -#counter { - position: absolute; - top: 140px; - left: -64px; - width: 50px; - font-weight: bold; - text-align: right; -} -/* ----- Subscribe Form ----- */ -#subscribe .submit, #unsubscribe .submit, #remotesubscribe .button, #remotesubscribe { - clear: left; - margin: 0; - width: 96px; - height: 27px; - font-family: verdana, arial, helvetica, sans-serif; - font-weight: bold; - font-size: 10px; - text-transform: uppercase; - background-color: #c15d42; - color: #fcfff5; - border: 0; - } -#remotesubscribe { - width: 96px; - height: 22px; - padding: 5px 0 0 0; - text-align: center; - } -#subscribe .button:hover, #unsubscribe .button:hover { - background-color: #904632; - cursor: pointer; - } - -a#remotesubscribe { - display: block; -} - -/* ----- Login Form -----*/ -input#license { - width: auto; - border: 0; - } -/* ----- Avatar Form -----*/ -form { - clear: left; -} - -/* ----- OpenID Form -----*/ - -input#openid_url { - background: url(login-bg.gif) no-repeat; - background-color: #fff; - background-position: 4px 50%; - color: #000; - padding-left: 24px; -} - -/* People lists (search results, maybe subscribers) */ - -#profiles { - clear: both; - margin: 0 auto; - padding: 0; - list-style-type: none; - width: 540px; - border-top: 1px solid #D8E2D7; - /*border: 1px solid #F00;*/ - } -#profiles a:hover { - text-decoration: underline; - } - -.profile_single { - clear: both; - display: block; - margin: 0; - padding: 5px 5px 5px 0; - min-height: 48px; - font-family: Georgia, "Times New Roman", Times, serif; - font-size: 13px; - line-height: 16px; - border-bottom: 1px solid #D8E2D7; - } -.profile_single:hover { - background-color: #F3F8EA; - } - -/* ----- IM Settings Form -----*/ - -#imsettings p { - margin: 0; - padding: 0; - line-height: 15px; -} - -/* ===== End Forms Styling ===== */ - -/* ===== Tag Cloud Styling ===== */ - -p.tagcloud { -text-align: center; -} - -p.tagcloud a { -line-height:100%; -vertical-align:middle; -} - -p.tagcloud a.largest { -font-size: 400%; -} -p.tagcloud a.verylarge { -font-size: 300%; -} - -p.tagcloud a.large { -font-size: 200%; -} - -p.tagcloud a.medium { -font-size: 150%; -} - -p.tagcloud a.small { -font-size: 100%; -} - -p.tagcloud a.verysmall { -font-size: 80%; -} - -p.tagcloud a.smallest { -font-size: 60%; -} diff --git a/theme/iphone/ie6.css.dist b/theme/iphone/ie6.css.dist deleted file mode 100644 index 97d9fee3f..000000000 --- a/theme/iphone/ie6.css.dist +++ /dev/null @@ -1,63 +0,0 @@ -@charset "UTF-8"; -/* CSS Document */ -body { - text-align: center; -} -input { - height: 24px; -} -#wrap { - margin: 0 auto; - padding: 0 20px; - width: 800px; - text-align: left; - background: url(bg-header.gif) repeat-x #FCFFF5; - } -#header { - position: relative; - margin-left: 108px; - } -#nav_views { - margin: 0; - } -#nav_views li { - line-height: 19px; - } -.statistics dd { - margin-top: -15px; - clear: both; - } -#notices { - margin: 0; - } -.notice_single { - height: 48px; - } -#profile p.notice_current { - height: 96px; - } - -#subscriptions_avatars li { - float: left; - margin: 0; - padding: 0; - } -img.avatar.original, img.avatar.profile { - clear: none; - float: left; -} -#status_textarea { - height: 46px; - } - -#nav_pagination li a { - padding: 6px 15px; - line-height: 27px; - } -#nav_sub { - position: relative; - margin-left: 108px; - } -#footer { - margin-left: 108px; -} diff --git a/theme/iphone/ie7.css.dist b/theme/iphone/ie7.css.dist deleted file mode 100644 index bbf52d5cf..000000000 --- a/theme/iphone/ie7.css.dist +++ /dev/null @@ -1,20 +0,0 @@ -@charset "UTF-8"; -/* CSS Document */ - -#statistics dd { - clear: both; - } - - -#subscriptions_avatars li { - float: left; - } -img.avatar.original, img.avatar.profile { - clear: none; - float: left; -} - -#nav_pagination li a { - padding: 6px 15px; - line-height: 27px; - } \ No newline at end of file diff --git a/theme/iphone/login-bg.gif b/theme/iphone/login-bg.gif deleted file mode 100644 index e2d8377db..000000000 Binary files a/theme/iphone/login-bg.gif and /dev/null differ diff --git a/theme/iphone/logo.png b/theme/iphone/logo.png deleted file mode 100644 index 3b271814d..000000000 Binary files a/theme/iphone/logo.png and /dev/null differ -- cgit v1.2.3-54-g00ecf From 2f065d687dc76bfbac3e1b7b3dcccf544f4b4987 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Sun, 4 Oct 2009 23:53:02 +0200 Subject: Unnecessary selector bit --- theme/base/css/display.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 7706fba48..76709113f 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -251,7 +251,7 @@ margin-right:18px; margin-bottom:11px; margin-left:18px; } -#site_nav_global_primary ul li { +#site_nav_global_primary li { display:inline; margin-left:11px; } -- cgit v1.2.3-54-g00ecf From 7a33125a6c05a885a7e4c6d80be4e0fe26499565 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Mon, 5 Oct 2009 14:17:55 +0000 Subject: Added hook to wrap the notice form data block --- EVENTS.txt | 6 +++++ lib/noticeform.php | 70 +++++++++++++++++++++++++++++------------------------- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/EVENTS.txt b/EVENTS.txt index fbb2f36a7..02b11a8a6 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -93,6 +93,12 @@ StartShowAside: Showing before the Aside container EndShowAside: Showing after the Aside container - $action: the current action +StartShowNoticeFormData: Showing before the notice form data +- $action: the current action + +EndShowNoticeFormData: Showing after the notice form data +- $action: the current action + StartNoticeSave: before inserting a notice (good place for content filters) - $notice: notice being saved (no ID or URI) diff --git a/lib/noticeform.php b/lib/noticeform.php index 186330bf1..9864d15eb 100644 --- a/lib/noticeform.php +++ b/lib/noticeform.php @@ -142,40 +142,44 @@ class NoticeForm extends Form function formData() { - $this->out->element('label', array('for' => 'notice_data-text'), - sprintf(_('What\'s up, %s?'), $this->user->nickname)); - // XXX: vary by defined max size - $this->out->element('textarea', array('id' => 'notice_data-text', - 'cols' => 35, - 'rows' => 4, - 'name' => 'status_textarea'), - ($this->content) ? $this->content : ''); - - $contentLimit = Notice::maxContent(); - - $this->out->element('script', array('type' => 'text/javascript'), - 'maxLength = ' . $contentLimit . ';'); - - if ($contentLimit > 0) { - $this->out->elementStart('dl', 'form_note'); - $this->out->element('dt', null, _('Available characters')); - $this->out->element('dd', array('id' => 'notice_text-count'), - $contentLimit); - $this->out->elementEnd('dl'); + if (Event::handle('StartShowNoticeFormData', array($this))) { + $this->out->element('label', array('for' => 'notice_data-text'), + sprintf(_('What\'s up, %s?'), $this->user->nickname)); + // XXX: vary by defined max size + $this->out->element('textarea', array('id' => 'notice_data-text', + 'cols' => 35, + 'rows' => 4, + 'name' => 'status_textarea'), + ($this->content) ? $this->content : ''); + + $contentLimit = Notice::maxContent(); + + $this->out->element('script', array('type' => 'text/javascript'), + 'maxLength = ' . $contentLimit . ';'); + + if ($contentLimit > 0) { + $this->out->elementStart('dl', 'form_note'); + $this->out->element('dt', null, _('Available characters')); + $this->out->element('dd', array('id' => 'notice_text-count'), + $contentLimit); + $this->out->elementEnd('dl'); + } + + if (common_config('attachments', 'uploads')) { + $this->out->element('label', array('for' => 'notice_data-attach'),_('Attach')); + $this->out->element('input', array('id' => 'notice_data-attach', + 'type' => 'file', + 'name' => 'attach', + 'title' => _('Attach a file'))); + $this->out->hidden('MAX_FILE_SIZE', common_config('attachments', 'file_quota')); + } + if ($this->action) { + $this->out->hidden('notice_return-to', $this->action, 'returnto'); + } + $this->out->hidden('notice_in-reply-to', $this->inreplyto, 'inreplyto'); + + Event::handle('StartShowNoticeFormData', array($this)); } - - if (common_config('attachments', 'uploads')) { - $this->out->element('label', array('for' => 'notice_data-attach'),_('Attach')); - $this->out->element('input', array('id' => 'notice_data-attach', - 'type' => 'file', - 'name' => 'attach', - 'title' => _('Attach a file'))); - $this->out->hidden('MAX_FILE_SIZE', common_config('attachments', 'file_quota')); - } - if ($this->action) { - $this->out->hidden('notice_return-to', $this->action, 'returnto'); - } - $this->out->hidden('notice_in-reply-to', $this->inreplyto, 'inreplyto'); } /** -- cgit v1.2.3-54-g00ecf