From 497f232ec288cb051c1d9bd4d8a5513fce3d6197 Mon Sep 17 00:00:00 2001 From: Michael J Rubinsky Date: Fri, 22 Nov 2013 15:30:30 -0500 Subject: [PATCH] Refactor Twitter block to use HordeCore. --- horde/js/twitterclient.js | 141 +++------ horde/lib/Ajax/Application.php | 4 + horde/lib/Ajax/Application/TwitterHandler.php | 293 ++++++++++++++++++ horde/package.xml | 2 + horde/services/twitter/index.php | 157 ---------- 5 files changed, 339 insertions(+), 258 deletions(-) create mode 100644 horde/lib/Ajax/Application/TwitterHandler.php diff --git a/horde/js/twitterclient.js b/horde/js/twitterclient.js index 7f80eee10ba..855794f9710 100644 --- a/horde/js/twitterclient.js +++ b/horde/js/twitterclient.js @@ -94,17 +94,10 @@ var Horde_Twitter = Class.create({ inReplyTo: this.inReplyTo }; - new Ajax.Request(this.opts.endpoint, { - method: 'post', - parameters: params, - onSuccess: function(response) { - this.updateCallback(response.responseJSON); - }.bind(this), - onFailure: function() { - $(this.opts.spinner).toggle(); - this.inReplyTo = ''; - }.bind(this) - }); + HordeCore.doAction('updateStatus', + params, + { callback: this.updateCallback.bind(this) } + ); }, /** @@ -117,17 +110,10 @@ var Horde_Twitter = Class.create({ actionID: 'retweet', tweetId: id }; - new Ajax.Request(this.opts.endpoint, { - method: 'post', - parameters: params, - onSuccess: function(response) { - this.updateCallback(response.responseJSON); - }.bind(this), - onFailure: function() { - $(this.opts.spinner).toggle(); - this.inReplyTo = ''; - }.bind(this) - }); + HordeCore.doAction('retweet', + params, + { callback: this.updateCallback.bind(this) } + ); }, /** @@ -136,20 +122,11 @@ var Horde_Twitter = Class.create({ favorite: function(id) { $(this.opts.spinner).toggle(); - var params = { - actionID: 'favorite', - tweetId: id - }; - new Ajax.Request(this.opts.endpoint, { - method: 'post', - parameters: params, - onSuccess: function(response) { - this.favoriteCallback(response.responseJSON); - }.bind(this), - onFailure: function() { - $(this.opts.spinner).toggle(); - }.bind(this) - }); + + HordeCore.doAction('favorite', + { tweetId: id }, + { callback: this.favoriteCallback.bind(this) } + ); }, unfavorite: function(id) @@ -159,16 +136,10 @@ var Horde_Twitter = Class.create({ actionID: 'unfavorite', tweetId: id }; - new Ajax.Request(this.opts.endpoint, { - method: 'post', - parameters: params, - onSuccess: function(response) { - this.unfavoriteCallback(response.responseJSON); - }.bind(this), - onFailure: function() { - $(this.opts.spinner).toggle(); - }.bind(this) - }); + HordeCore.doAction('unfavorite', + { tweetId: id }, + { callback: this.unfavoriteCallback.bind(this) } + ); }, favoriteCallback: function(r) @@ -213,14 +184,10 @@ var Horde_Twitter = Class.create({ params.mentions = 1; break; } - new Ajax.Request(this.opts.endpoint, { - method: 'post', - parameters: params, - onSuccess: callback, - onFailure: function() { - $(this.opts.spinner).toggle(); - }.bind(this) - }); + HordeCore.doAction('twitterUpdate', + params, + { callback: callback } + ); }, /** @@ -248,15 +215,10 @@ var Horde_Twitter = Class.create({ } callback = this._getNewEntriesCallback.bind(this); } - - new Ajax.Request(this.opts.endpoint, { - method: 'post', - parameters: params, - onSuccess: callback, - onFailure: function() { - $(this.opts.spinner).toggle(); - }.bind(this) - }); + HordeCore.doAction('twitterUpdate', + params, + { callback: callback } + ); }, showPreview: function(url) @@ -286,9 +248,9 @@ var Horde_Twitter = Class.create({ * @param object response The response object from the Ajax request. */ _getOlderEntriesCallback: function(response) { - var h, content = response.responseJSON.c; - if (response.responseJSON.o) { - this.oldestId = response.responseJSON.o; + var h, content = response.c; + if (response.o) { + this.oldestId = response.o; h = $(this.opts.content).scrollHeight $(this.opts.content).insert(content); $(this.opts.content).scrollTop = h; @@ -302,10 +264,10 @@ var Horde_Twitter = Class.create({ * @param object response The response object from the Ajax request. */ _getOlderMentionsCallback: function(response) { - var h, content = response.responseJSON.c; + var h, content = response.c; // If no more available, the oldest id will be null - if (response.responseJSON.o) { - this.oldestMention = response.responseJSON.o; + if (response.o) { + this.oldestMention = response.o; h = $(this.opts.mentions).scrollHeight $(this.opts.mentions).insert(content); $(this.opts.mentions).scrollTop = h; @@ -318,9 +280,9 @@ var Horde_Twitter = Class.create({ * */ _getNewEntriesCallback: function(response) { - var h, content = response.responseJSON.c; + var h, content = response.c; - if (response.responseJSON.n != this.newestId) { + if (response.n != this.newestId) { h = $(this.opts.content).scrollHeight; $(this.opts.content).insert({ 'top': content }); if (this.activeTab != 'stream') { @@ -334,11 +296,11 @@ var Horde_Twitter = Class.create({ } } - this.newestId = response.responseJSON.n; + this.newestId = response.n; // First time we've been called, record the oldest one as well.' if (!this.oldestId) { - this.oldestId = response.responseJSON.o; + this.oldestId = response.o; } } new PeriodicalExecuter(function(pe) { this.getNewEntries(); pe.stop(); }.bind(this), this.opts.refreshrate ); @@ -349,9 +311,9 @@ var Horde_Twitter = Class.create({ * */ _getNewMentionsCallback: function(response) { - var h, content = response.responseJSON.c; + var h, content = response.c; - if (response.responseJSON.n != this.newestMention) { + if (response.n != this.newestMention) { h = $(this.opts.mentions).scrollHeight; $(this.opts.mentions).insert({ 'top': content }); if (this.activeTab != 'mentions') { @@ -365,11 +327,11 @@ var Horde_Twitter = Class.create({ } } - this.newestMention = response.responseJSON.n; + this.newestMention = response.n; // First time we've been called, record the oldest one as well. if (!this.oldestMention) { - this.oldestMention = response.responseJSON.o; + this.oldestMention = response.o; } } new PeriodicalExecuter(function(pe) { this.getNewEntries('mentions'); pe.stop(); }.bind(this), this.opts.refreshrate ); @@ -389,36 +351,13 @@ var Horde_Twitter = Class.create({ * Callback for after a new tweet is posted. */ updateCallback: function(response) { - if (response.error) - this.buildNewTweet(response); + $(this.opts.content).insert({ top: response }); $(this.opts.input).value = this.opts.strings.defaultText; $(this.opts.spinner).toggle(); this.inReplyTo = ''; $(this.opts.inreplyto).update(''); }, - /** - * Build and display the node for a new tweet. - */ - buildNewTweet: function(response) { - var tweet = new Element('div', {'class':'hordeSmStreamstory'}), - tPic = new Element('div', {'class':'solidbox hordeSmAvatar'}).update( - new Element('a', {'href': 'http://twitter.com/' + response.user.screen_name}).update( - new Element('img', {'src':response.user.profile_image_url}) - ) - ); - tPic.appendChild( - new Element('div', { 'style': {'overflow': 'hidden' }}).update( - new Element('a', {'href': 'http://twitter.com/' + response.user.screen_name}).update(response.user.screen_name) - ) - ); - var tBody = new Element('div', {'class':'hordeSmStreambody'}).update(response.text); - tBody.appendChild(new Element('div', {'class':'hordeSmStreaminfo'}).update(this.opts.strings.justnow + '

')); - tweet.appendChild(tPic); - tweet.appendChild(tBody); - $(this.opts.content).insert({top:tweet}); - }, - showMentions: function() { if (this.activeTab != 'mentions') { diff --git a/horde/lib/Ajax/Application.php b/horde/lib/Ajax/Application.php index a736d79a946..dd021654024 100644 --- a/horde/lib/Ajax/Application.php +++ b/horde/lib/Ajax/Application.php @@ -21,6 +21,10 @@ protected function _init() $this->addHandler('Horde_Ajax_Application_Handler'); // Needed because Core contains Imples $this->addHandler('Horde_Core_Ajax_Application_Handler_Imple'); + + if (!empty($GLOBALS['conf']['twitter']['enabled'])) { + $this->addHandler('Horde_Ajax_Application_TwitterHandler'); + } } } diff --git a/horde/lib/Ajax/Application/TwitterHandler.php b/horde/lib/Ajax/Application/TwitterHandler.php new file mode 100644 index 00000000000..6211ce197da --- /dev/null +++ b/horde/lib/Ajax/Application/TwitterHandler.php @@ -0,0 +1,293 @@ + + * @category Horde + * @license http://www.horde.org/licenses/lgpl LGPL-2 + * @package Horde + */ + +/** + * Defines the AJAX actions used in the Twitter client. + * + * @author Michael J Rubinsky + * @category Horde + * @license http://www.horde.org/licenses/lgpl LGPL-2 + * @package Horde + */ +class Horde_Ajax_Application_TwitterHandler extends Horde_Core_Ajax_Application_Handler +{ + /** + * Update the twitter timeline. + * + * @return array An hash containing the following keys: + * - o: The id of the oldest tweet + * - n: The id of the newest tweet + * - c: The HTML content + */ + public function twitterUpdate() + { + global $injector; + + if (empty($GLOBALS['conf']['twitter']['enabled'])) { + return _("Twitter not enabled."); + } + + switch ($this->vars->actionID) { + case 'getPage': + return $this->_doTwitterGetPage(); + } + + } + + /** + * Retweet a tweet. Expects the following in $this->vars: + * - tweetId: The tweet id to retweet. + * - i: + * + * @return string The HTML to render the newly retweeted tweet. + */ + public function retweet() + { + $twitter = $this->_getTwitterObject(); + try { + $tweet = json_decode($twitter->statuses->retweet($this->vars->tweetId)); + $view = $this->_buildTweet($tweet); + $html = $this->_buildTweet($tweet)->render('twitter_tweet'); + return $html; + } catch (Horde_Service_Twitter_Exception $e) { + $this->_twitterError($e); + } + } + + /** + * Favorite a tweet. Expects: + * - tweetId: + * + * @return stdClass + */ + public function favorite() + { + $twitter = $this->_getTwitterObject(); + try { + return json_decode($twitter->favorites->create($this->vars->tweetId)); + } catch (Horde_Service_Twitter_Exception $e) { + $this->_twitterError($e); + } + } + + /** + * Unfavorite a tweet. Expects: + * - tweetId: + */ + public function unfavorite() + { + $twitter = $this->_getTwitterObject(); + try { + return json_decode($twitter->favorites->destroy($this->vars->tweetId)); + } catch (Horde_Service_Twitter_Exception $e) { + $this->_twitterError($e); + } + } + + /** + * Update twitter status. Expects: + * - inReplyTo: + * - statusText: + * + * @return string The HTML text of the new tweet. + */ + public function updateStatus() + { + $twitter = $this->_getTwitterObject(); + if ($inreplyTo = $this->vars->inReplyTo) { + $params = array('in_reply_to_status_id', $inreplyTo); + } else { + $params = array(); + } + try { + $tweet = json_decode($twitter->statuses->update($this->vars->statusText, $params)); + return $this->_buildTweet($tweet)->render('twitter_tweet'); + } catch (Horde_Service_Twitter_Exception $e) { + $this->_twitterError($e); + } + } + + /** + * + * @return Horde_Service_Twitter + */ + protected function _getTwitterObject() + { + $twitter = $GLOBALS['injector']->getInstance('Horde_Service_Twitter'); + $token = unserialize($GLOBALS['prefs']->getValue('twitter')); + if (!empty($token['key']) && !empty($token['secret'])) { + $auth_token = new Horde_Oauth_Token($token['key'], $token['secret']); + $twitter->auth->setToken($auth_token); + } + + return $twitter; + } + + /** + * Helper method to build a view object for a tweet. + * + * @param stdClass $tweet The tweet object. + * + * @return Horde_View The view object, populated with tweet data. + */ + protected function _buildTweet($tweet) + { + global $injector, $registry; + + $view = new Horde_View(array('templatePath' => HORDE_TEMPLATES . '/block')); + $view->addHelper('Tag'); + $view->ajax_uri = $registry->getServiceLink('ajax', $registry->getApp()); + $filter = $injector->getInstance('Horde_Core_Factory_TextFilter'); + $instance = $this->vars->i; + + // Links and media + $map = $previews = array(); + foreach ($tweet->entities->urls as $link) { + $replace = '' . htmlspecialchars($link->display_url) . ''; + $map[$link->indices[0]] = array($link->indices[1], $replace); + } + if (!empty($tweet->entities->media)) { + foreach ($tweet->entities->media as $picture) { + $replace = '' . htmlentities($picture->display_url, ENT_COMPAT, 'UTF-8') . ''; + $map[$picture->indices[0]] = array($picture->indices[1], $replace); + $previews[] = ' '; + } + } + if (!empty($tweet->entities->user_mentions)) { + foreach ($tweet->entities->user_mentions as $user) { + $replace = ' @' . htmlentities($user->screen_name, ENT_COMPAT, 'UTF-8') . ''; + $map[$user->indices[0]] = array($user->indices[1], $replace); + } + } + if (!empty($tweet->entities->hashtags)) { + foreach ($tweet->entities->hashtags as $hashtag) { + $replace = ' #' . htmlentities($hashtag->text, ENT_COMPAT, 'UTF-8') . ''; + $map[$hashtag->indices[0]] = array($hashtag->indices[1], $replace); + } + } + $body = ''; + $pos = 0; + while ($pos <= Horde_String::length($tweet->text) - 1) { + if (!empty($map[$pos])) { + $entity = $map[$pos]; + $body .= $entity[1]; + $pos = $entity[0]; + } else { + $body .= Horde_String::substr($tweet->text, $pos, 1); + ++$pos; + } + } + foreach ($previews as $preview) { + $body .= $preview; + } + $view->body = $body; + + /* If this is a retweet, use the original author's profile info */ + if (!empty($tweet->retweeted_status)) { + $tweetObj = $tweet->retweeted_status; + } else { + $tweetObj = $tweet; + } + + /* These are all referencing the *original* tweet */ + $view->profileLink = Horde::externalUrl('http://twitter.com/' . htmlspecialchars($tweetObj->user->screen_name), true); + $view->profileImg = $GLOBALS['browser']->usingSSLConnection() ? $tweetObj->user->profile_image_url_https : $tweetObj->user->profile_image_url; + $view->authorName = '@' . htmlspecialchars($tweetObj->user->screen_name); + $view->authorFullname = htmlspecialchars($tweetObj->user->name); + $view->createdAt = $tweetObj->created_at; + $view->clientText = $filter->filter($tweet->source, 'xss'); + $view->tweet = $tweet; + $view->instanceid = $instance; + + return $view; + } + + /** + * Helper method for getting a slice of tweets. + * + * Expects the following in $this->vars: + * - max_id: + * - since_id: + * - i: + * - mentions: + * + * @return [type] [description] + */ + protected function _doTwitterGetPage() + { + global $injector; + + $twitter = $this->_getTwitterObject(); + try { + $params = array('include_entities' => 1); + if ($max = $this->vars->max_id) { + $params['max_id'] = $max; + } elseif ($since = $this->vars->since_id) { + $params['since_id'] = $since; + } + $instance = $this->vars->i; + if ($this->vars->mentions) { + $stream = Horde_Serialize::unserialize($twitter->statuses->mentions($params), Horde_Serialize::JSON); + } else { + $stream = Horde_Serialize::unserialize($twitter->statuses->homeTimeline($params), Horde_Serialize::JSON); + } + } catch (Horde_Service_Twitter_Exception $e) { + $this->_twitterError($e); + } + if (count($stream)) { + $newest = $stream[0]->id_str; + } else { + $newest = $params['since_id']; + $oldest = 0; + } + + $view = new Horde_View(array('templatePath' => HORDE_TEMPLATES . '/block')); + $view->addHelper('Tag'); + $html = ''; + foreach ($stream as $tweet) { + /* Don't return the max_id tweet, since we already have it */ + if (!empty($params['max_id']) && $params['max_id'] == $tweet->id_str) { + continue; + } + $view = $this->_buildTweet($tweet); + $oldest = $tweet->id_str; + $html .= $view->render('twitter_tweet'); + } + + $result = array( + 'o' => $oldest, + 'n' => $newest, + 'c' => $html + ); + + return $result; + } + + public function _twitterError($e) + { + global $notification, $page_output; + + Horde::log($e, 'INFO'); + $body = ($e instanceof Exception) ? $e->getMessage() : $e; + if (($errors = json_decode($body, true)) && isset($errors['errors'])) { + $errors = $errors['errors']; + } else { + $errors = array(array('message' => $body)); + } + $notification->push(_("Error connecting to Twitter. Details have been logged for the administrator."), 'horde.error', array('sticky')); + foreach ($errors as $error) { + $notification->push($error['message'], 'horde.error', array('sticky')); + } + } + +} \ No newline at end of file diff --git a/horde/package.xml b/horde/package.xml index 676925900ea..5c95c0a7fe9 100644 --- a/horde/package.xml +++ b/horde/package.xml @@ -247,6 +247,7 @@ + @@ -2044,6 +2045,7 @@ + diff --git a/horde/services/twitter/index.php b/horde/services/twitter/index.php index 387b8b6784a..0b33c5b9c36 100644 --- a/horde/services/twitter/index.php +++ b/horde/services/twitter/index.php @@ -52,163 +52,6 @@ function _outputError($e) $twitter->auth->setToken($auth_token); } -/* See if we are here for any actions */ -$action = Horde_Util::getPost('actionID'); -switch ($action) { - -case 'updateStatus': - if ($inreplyTo = Horde_Util::getPost('inReplyTo')) { - $params = array('in_reply_to_status_id', $inreplyTo); - } else { - $params = array(); - } - try { - $result = $twitter->statuses->update(Horde_Util::getPost('statusText'), $params); - header('Content-Type: application/json'); - echo $result; - } catch (Horde_Service_Twitter_Exception $e) { - Horde::log($e->getMessage(), 'ERR'); - header('HTTP/1.1: 500'); - } - exit; -case 'favorite': - try { - $result = $twitter->favorites->create(Horde_Util::getPost('tweetId')); - header('Content-Type: application/json'); - echo $result; - } catch (Horde_Service_Twitter_Exception $e) { - Horde::log($e->getMessage(), 'ERR'); - header('HTTP/1.1: 500'); - } - exit; -case 'unfavorite': - try { - $result = $twitter->favorites->destroy(Horde_Util::getPost('tweetId')); - header('Content-Type: application/json'); - echo $result; - } catch (Horde_Service_Twitter_Exception $e) { - header('HTTP/1.1: 500'); - } - exit; -case 'retweet': - try { - $result = $twitter->statuses->retweet(Horde_Util::getPost('tweetId')); - header('Content-Type: application/json'); - echo $result; - } catch (Horde_Service_Twitter_Exception $e) { - Horde::log($e->getMessage(), 'ERR'); - header('HTTP/1.1: 500'); - } - exit; - -case 'getPage': - try { - $params = array('include_entities' => 1); - if ($max = Horde_Util::getPost('max_id')) { - $params['max_id'] = $max; - } elseif ($since = Horde_Util::getPost('since_id')) { - $params['since_id'] = $since; - } - $instance = Horde_Util::getPost('i'); - if (Horde_Util::getPost('mentions', null)) { - $stream = Horde_Serialize::unserialize($twitter->statuses->mentions($params), Horde_Serialize::JSON); - } else { - $stream = Horde_Serialize::unserialize($twitter->statuses->homeTimeline($params), Horde_Serialize::JSON); - } - } catch (Horde_Service_Twitter_Exception $e) { - _outputError($e); - } - $html = ''; - if (count($stream)) { - $newest = $stream[0]->id_str; - } else { - $newest = $params['since_id']; - $oldest = 0; - } - - $view = new Horde_View(array('templatePath' => HORDE_TEMPLATES . '/block')); - $view->addHelper('Tag'); - foreach ($stream as $tweet) { - /* Don't return the max_id tweet, since we already have it */ - if (!empty($params['max_id']) && $params['max_id'] == $tweet->id_str) { - continue; - } - - $filter = $injector->getInstance('Horde_Core_Factory_TextFilter'); - - // Links and media - $map = $previews = array(); - - foreach ($tweet->entities->urls as $link) { - $replace = '' . htmlspecialchars($link->display_url) . ''; - $map[$link->indices[0]] = array($link->indices[1], $replace); - } - if (!empty($tweet->entities->media)) { - foreach ($tweet->entities->media as $picture) { - $replace = '' . htmlentities($picture->display_url, ENT_COMPAT, 'UTF-8') . ''; - $map[$picture->indices[0]] = array($picture->indices[1], $replace); - $previews[] = ' '; - } - } - if (!empty($tweet->entities->user_mentions)) { - foreach ($tweet->entities->user_mentions as $user) { - $replace = ' @' . htmlentities($user->screen_name, ENT_COMPAT, 'UTF-8') . ''; - $map[$user->indices[0]] = array($user->indices[1], $replace); - } - } - if (!empty($tweet->entities->hashtags)) { - foreach ($tweet->entities->hashtags as $hashtag) { - $replace = ' #' . htmlentities($hashtag->text, ENT_COMPAT, 'UTF-8') . ''; - $map[$hashtag->indices[0]] = array($hashtag->indices[1], $replace); - } - } - $body = ''; - $pos = 0; - while ($pos <= Horde_String::length($tweet->text) - 1) { - if (!empty($map[$pos])) { - $entity = $map[$pos]; - $body .= $entity[1]; - $pos = $entity[0]; - } else { - $body .= Horde_String::substr($tweet->text, $pos, 1); - ++$pos; - } - } - foreach ($previews as $preview) { - $body .= $preview; - } - $view->body = $body; - - /* If this is a retweet, use the original author's profile info */ - if (!empty($tweet->retweeted_status)) { - $tweetObj = $tweet->retweeted_status; - } else { - $tweetObj = $tweet; - } - - /* These are all referencing the *original* tweet */ - $view->profileLink = Horde::externalUrl('http://twitter.com/' . htmlspecialchars($tweetObj->user->screen_name), true); - $view->profileImg = $GLOBALS['browser']->usingSSLConnection() ? $tweetObj->user->profile_image_url_https : $tweetObj->user->profile_image_url; - $view->authorName = '@' . htmlspecialchars($tweetObj->user->screen_name); - $view->authorFullname = htmlspecialchars($tweetObj->user->name); - $view->createdAt = $tweetObj->created_at; - $view->clientText = $filter->filter($tweet->source, 'xss'); - $view->tweet = $tweet; - $view->instanceid = $instance; - $oldest = $tweet->id_str; - $html .= $view->render('twitter_tweet'); - } - - $result = array( - 'o' => $oldest, - 'n' => $newest, - 'c' => $html - ); - header('Content-Type: application/json'); - echo Horde_Serialize::serialize($result, Horde_Serialize::JSON); - exit; -} - $page_output->topbar = $page_output->sidebar = false; /* No requested action, check to see if we have a valid token */