1<?php 2/** 3 * Copyright 2009-2017 Horde LLC (http://www.horde.org/) 4 * 5 * See the enclosed file COPYING for license information (LGPL-2). If you 6 * did not receive this file, see http://www.horde.org/licenses/lgpl. 7 * 8 * @author Michael J Rubinsky <mrubinsk.horde.org> 9 * @category Horde 10 * @license http://www.horde.org/licenses/lgpl LGPL-2 11 * @package Horde 12 */ 13 14/** 15 * Defines the AJAX actions used in the Twitter client. 16 * 17 * @author Michael J Rubinsky <mrubinsk.horde.org> 18 * @category Horde 19 * @license http://www.horde.org/licenses/lgpl LGPL-2 20 * @package Horde 21 */ 22class Horde_Ajax_Application_TwitterHandler extends Horde_Core_Ajax_Application_Handler 23{ 24 /** 25 * Update the twitter timeline. 26 * 27 * @return array An hash containing the following keys: 28 * - o: The id of the oldest tweet 29 * - n: The id of the newest tweet 30 * - c: The HTML content 31 */ 32 public function twitterUpdate() 33 { 34 global $conf; 35 36 if (empty($conf['twitter']['enabled'])) { 37 return _("Twitter not enabled."); 38 } 39 40 switch ($this->vars->actionID) { 41 case 'getPage': 42 return $this->_doTwitterGetPage(); 43 } 44 45 } 46 47 /** 48 * Retweet a tweet. Expects the following in $this->vars: 49 * - tweetId: The tweet id to retweet. 50 * - i: 51 * 52 * @return string The HTML to render the newly retweeted tweet. 53 */ 54 public function retweet() 55 { 56 $twitter = $this->_getTwitterObject(); 57 try { 58 $tweet = json_decode($twitter->statuses->retweet($this->vars->tweetId)); 59 $html = $this->_buildTweet($tweet)->render('twitter_tweet'); 60 return $html; 61 } catch (Horde_Service_Twitter_Exception $e) { 62 $this->_twitterError($e); 63 } 64 } 65 66 /** 67 * Favorite a tweet. Expects: 68 * - tweetId: 69 * 70 * @return stdClass 71 */ 72 public function favorite() 73 { 74 $twitter = $this->_getTwitterObject(); 75 try { 76 return json_decode($twitter->favorites->create($this->vars->tweetId)); 77 } catch (Horde_Service_Twitter_Exception $e) { 78 $this->_twitterError($e); 79 } 80 } 81 82 /** 83 * Unfavorite a tweet. Expects: 84 * - tweetId: 85 */ 86 public function unfavorite() 87 { 88 $twitter = $this->_getTwitterObject(); 89 try { 90 return json_decode($twitter->favorites->destroy($this->vars->tweetId)); 91 } catch (Horde_Service_Twitter_Exception $e) { 92 $this->_twitterError($e); 93 } 94 } 95 96 /** 97 * Update twitter status. Expects: 98 * - inReplyTo: 99 * - statusText: 100 * 101 * @return string The HTML text of the new tweet. 102 */ 103 public function updateStatus() 104 { 105 $twitter = $this->_getTwitterObject(); 106 if ($inreplyTo = $this->vars->inReplyTo) { 107 $params = array('in_reply_to_status_id', $inreplyTo); 108 } else { 109 $params = array(); 110 } 111 try { 112 $tweet = json_decode($twitter->statuses->update($this->vars->statusText, $params)); 113 return $this->_buildTweet($tweet)->render('twitter_tweet'); 114 } catch (Horde_Service_Twitter_Exception $e) { 115 $this->_twitterError($e); 116 } 117 } 118 119 /** 120 * 121 * @return Horde_Service_Twitter 122 */ 123 protected function _getTwitterObject() 124 { 125 $twitter = $GLOBALS['injector']->getInstance('Horde_Service_Twitter'); 126 $token = unserialize($GLOBALS['prefs']->getValue('twitter')); 127 if (!empty($token['key']) && !empty($token['secret'])) { 128 $auth_token = new Horde_Oauth_Token($token['key'], $token['secret']); 129 $twitter->auth->setToken($auth_token); 130 } 131 132 return $twitter; 133 } 134 135 /** 136 * Helper method to build a view object for a tweet. 137 * 138 * @param stdClass $tweet The tweet object. 139 * 140 * @return Horde_View The view object, populated with tweet data. 141 */ 142 protected function _buildTweet($tweet) 143 { 144 global $injector, $registry; 145 146 $view = new Horde_View(array('templatePath' => HORDE_TEMPLATES . '/block')); 147 $view->addHelper('Tag'); 148 $view->ajax_uri = $registry->getServiceLink('ajax', $registry->getApp()); 149 $filter = $injector->getInstance('Horde_Core_Factory_TextFilter'); 150 $instance = $this->vars->i; 151 152 // Links and media 153 $map = $previews = array(); 154 foreach ($tweet->entities->urls as $link) { 155 $replace = '<a target="_blank" href="' . $link->url . '" title="' . $link->expanded_url . '">' . htmlspecialchars($link->display_url) . '</a>'; 156 $map[$link->indices[0]] = array($link->indices[1], $replace); 157 } 158 if (!empty($tweet->entities->media)) { 159 foreach ($tweet->entities->media as $picture) { 160 $replace = '<a target="_blank" href="' . $picture->url . '" title="' . $picture->expanded_url . '">' . htmlentities($picture->display_url, ENT_COMPAT, 'UTF-8') . '</a>'; 161 $map[$picture->indices[0]] = array($picture->indices[1], $replace); 162 $previews[] = ' <a href="#" onclick="return Horde[\'twitter' . $instance . '\'].showPreview(\'' . $picture->media_url . ':small\');"><img src="' . Horde_Themes::img('mime/image.png') . '" /></a>'; 163 } 164 } 165 if (!empty($tweet->entities->user_mentions)) { 166 foreach ($tweet->entities->user_mentions as $user) { 167 $replace = ' <a target="_blank" title="' . $user->name . '" href="http://twitter.com/' . $user->screen_name . '">@' . htmlentities($user->screen_name, ENT_COMPAT, 'UTF-8') . '</a>'; 168 $map[$user->indices[0]] = array($user->indices[1], $replace); 169 } 170 } 171 if (!empty($tweet->entities->hashtags)) { 172 foreach ($tweet->entities->hashtags as $hashtag) { 173 $replace = ' <a target="_blank" href="http://twitter.com/search?q=#' . urlencode($hashtag->text) . '">#' . htmlentities($hashtag->text, ENT_COMPAT, 'UTF-8') . '</a>'; 174 $map[$hashtag->indices[0]] = array($hashtag->indices[1], $replace); 175 } 176 } 177 $body = ''; 178 $pos = 0; 179 while ($pos <= Horde_String::length($tweet->text) - 1) { 180 if (!empty($map[$pos])) { 181 $entity = $map[$pos]; 182 $body .= $entity[1]; 183 $pos = $entity[0]; 184 } else { 185 $body .= Horde_String::substr($tweet->text, $pos, 1); 186 ++$pos; 187 } 188 } 189 foreach ($previews as $preview) { 190 $body .= $preview; 191 } 192 $view->body = $body; 193 194 /* If this is a retweet, use the original author's profile info */ 195 if (!empty($tweet->retweeted_status)) { 196 $tweetObj = $tweet->retweeted_status; 197 } else { 198 $tweetObj = $tweet; 199 } 200 201 /* These are all referencing the *original* tweet */ 202 $view->profileLink = Horde::externalUrl('http://twitter.com/' . htmlspecialchars($tweetObj->user->screen_name), true); 203 $view->profileImg = $GLOBALS['browser']->usingSSLConnection() ? $tweetObj->user->profile_image_url_https : $tweetObj->user->profile_image_url; 204 $view->authorName = '@' . htmlspecialchars($tweetObj->user->screen_name); 205 $view->authorFullname = htmlspecialchars($tweetObj->user->name); 206 $view->createdAt = $tweetObj->created_at; 207 $view->clientText = $filter->filter($tweet->source, 'xss'); 208 $view->tweet = $tweet; 209 $view->instanceid = $instance; 210 211 return $view; 212 } 213 214 /** 215 * Helper method for getting a slice of tweets. 216 * 217 * Expects the following in $this->vars: 218 * - max_id: 219 * - since_id: 220 * - i: 221 * - mentions: 222 * 223 * @return [type] [description] 224 */ 225 protected function _doTwitterGetPage() 226 { 227 $twitter = $this->_getTwitterObject(); 228 try { 229 $params = array('include_entities' => 1); 230 if ($max = $this->vars->max_id) { 231 $params['max_id'] = $max; 232 } elseif ($since = $this->vars->since_id) { 233 $params['since_id'] = $since; 234 } 235 if ($this->vars->mentions) { 236 $stream = Horde_Serialize::unserialize($twitter->statuses->mentions($params), Horde_Serialize::JSON); 237 } else { 238 $stream = Horde_Serialize::unserialize($twitter->statuses->homeTimeline($params), Horde_Serialize::JSON); 239 } 240 } catch (Horde_Service_Twitter_Exception $e) { 241 $this->_twitterError($e); 242 return; 243 } 244 if (count($stream)) { 245 $newest = $stream[0]->id_str; 246 } else { 247 $newest = $params['since_id']; 248 $oldest = 0; 249 } 250 251 $view = new Horde_View(array('templatePath' => HORDE_TEMPLATES . '/block')); 252 $view->addHelper('Tag'); 253 $html = ''; 254 foreach ($stream as $tweet) { 255 /* Don't return the max_id tweet, since we already have it */ 256 if (!empty($params['max_id']) && $params['max_id'] == $tweet->id_str) { 257 continue; 258 } 259 $view = $this->_buildTweet($tweet); 260 $oldest = $tweet->id_str; 261 $html .= $view->render('twitter_tweet'); 262 } 263 264 $result = array( 265 'o' => $oldest, 266 'n' => $newest, 267 'c' => $html 268 ); 269 270 return $result; 271 } 272 273 protected function _twitterError($e) 274 { 275 global $notification; 276 277 Horde::log($e, 'INFO'); 278 $body = ($e instanceof Exception) ? $e->getMessage() : $e; 279 if (($errors = json_decode($body, true)) && isset($errors['errors'])) { 280 $errors = $errors['errors']; 281 } else { 282 $errors = array(array('message' => $body)); 283 } 284 $notification->push(_("Error connecting to Twitter. Details have been logged for the administrator."), 'horde.error', array('sticky')); 285 foreach ($errors as $error) { 286 $notification->push($error['message'], 'horde.error', array('sticky')); 287 } 288 } 289 290} 291