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