1: <?php
  2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  20:  21:  22:  23:  24:  25:  26:  27:  28:  29:  30:  31:  32:  33:  34:  35: 
 36: 
 37: Yii::import ('application.components.TwitterAPI.TwitterAPIExchange');
 38: 
 39:  40:  41:  42:  43: 
 44: class  extends SortableWidget {
 45: 
 46:     public $viewFile = '_twitterFeedWidget';
 47: 
 48:     public $model;
 49: 
 50:     public $sortableWidgetJSClass = 'TwitterFeedWidget';
 51: 
 52:     public $template = '<div class="submenu-title-bar widget-title-bar">{twitterLogo}{widgetLabel}{screenNameSelector}{closeButton}{minimizeButton}</div>{widgetContents}';
 53: 
 54:     private static $_JSONPropertiesStructure;
 55: 
 56:     private $_username;
 57: 
 58:     public static function getJSONPropertiesStructure () {
 59:         if (!isset (self::$_JSONPropertiesStructure)) {
 60:             self::$_JSONPropertiesStructure = array_merge (
 61:                 parent::getJSONPropertiesStructure (),
 62:                 array (
 63:                     'label' => 'Twitter Feed',
 64:                     'hidden' => false,
 65:                 )
 66:             );
 67:         }
 68:         return self::$_JSONPropertiesStructure;
 69:     }
 70: 
 71:     private ;
 72:     public function  () {
 73:         if (!isset ($this->_modelTwitterAliases)) {
 74:             $this->_modelTwitterAliases = RecordAliases::getAliases ($this->model, 'twitter');
 75:         }
 76:         return $this->_modelTwitterAliases;
 77:     }
 78: 
 79:     public function  () {
 80:         echo '<span id="twitter-widget-top-bar-logo"></span>';
 81:     }
 82: 
 83:     public function renderScreenNameSelector () {
 84:         $options = array ();
 85:         foreach ($this->getModelTwitterAliases () as $alias) {
 86:             $options[$alias->alias] = $alias->alias;
 87:         }
 88:         echo CHtml::dropDownList ('screenName', null, $options, array (
 89:             'class' => 'x2-minimal-select',
 90:             'id' => 'screen-name-selector',
 91:         ));
 92:     }
 93: 
 94:      95:  96: 
 97:     public function getPackages () {
 98:         if (!isset ($this->_packages)) {
 99:             $this->_packages = array_merge (
100:                 parent::getPackages (),
101:                 array (
102:                     'TwitterFeedWidgetJS' => array(
103:                         'baseUrl' => Yii::app()->request->baseUrl,
104:                         'js' => array (
105:                             'js/sortableWidgets/TwitterFeedWidget.js',
106:                         ),
107:                         'depends' => array ('SortableWidgetJS')
108:                     ),
109:                 )
110:             );
111:         }
112:         return $this->_packages;
113:     }
114: 
115:     public function getViewFileParams () {
116:         if (!isset ($this->_viewFileParams)) {
117:             $this->_viewFileParams = array_merge (
118:                 parent::getViewFileParams (),
119:                 array (
120:                     'username' => $this->_username,
121:                 )
122:             );
123:         }
124:         return $this->_viewFileParams;
125:     } 
126: 
127:     128: 129: 
130:     public function  () {
131:         $tweetDP = $this->getTweetDataProvider ();
132:         if (!$tweetDP) return null;
133:         $data = $tweetDP->getData ();
134:         $lastTweetId = null;
135:         if (count ($data)) {
136:             $lastTweetId = $data[count ($data) - 1]['id_str'];
137:         }
138:         return $lastTweetId;
139:     }
140: 
141:     142: 143: 
144:     public function getCacheKey () {
145:         $username = $this->_username;
146:         return 'TwitterFeedWidget'.$username;
147:     }
148: 
149:     public function run () {
150:         $credentials = $this->getTwitterCredentials ();
151:         if (!$credentials) return '';
152:         if (!extension_loaded('curl')) {
153:             $this->addError (Yii::t('app', 'The Twitter widget requires the PHP curl extension.'));
154:             return parent::run (); 
155:         } else {
156:             $aliases = $this->getModelTwitterAliases ();
157:             if (!count ($aliases)) return '';
158:             if (isset ($_GET['twitterScreenName'])) {
159:                 $this->_username = $_GET['twitterScreenName'];
160:             } else {
161:                 $this->_username = $aliases[0]->alias;
162:             }
163:             try {
164:                 $this->getTweetDataProvider ();
165:             } catch (TwitterFeedWidgetException $e) {
166:                 $errorMessage = $e->getMessage ();
167:                 if (isset ($_GET['twitterFeedAjax'])) {
168:                     throw new CHttpException (429, $errorMessage);
169:                 } else {
170:                     $this->addError ($errorMessage);
171:                 }
172:             }
173:         }
174:         return parent::run ();
175:     }
176:     
177:     private $_credentials;
178:     public function  () {
179:         if (!isset ($this->_credentials)) {
180:             $credId = Yii::app()->settings->twitterCredentialsId;
181:             if ($credId && ($credentials = Credentials::model ()->findByPk ($credId))) {
182:                 $this->_credentials = array(
183:                     'oauth_access_token' => $credentials->auth->oauthAccessToken,
184:                     'oauth_access_token_secret' => $credentials->auth->oauthAccessTokenSecret,
185:                     'consumer_key' => $credentials->auth->consumerKey,
186:                     'consumer_secret' => $credentials->auth->consumerSecret,
187:                 );
188:             }
189:         }
190:         return $this->_credentials;
191:     }
192: 
193:     194: 195: 
196:     public function renderTimestamp (array $tweet) {
197:         $timestamp = strtotime ($tweet['created_at']);
198:         $date = getDate ($timestamp);
199:         $nowTs = time ();
200:         $now = getDate ($nowTs);
201:         $formattedTimestamp = '';
202:         if ($date['year'] !== $now['year']) { 
203:             $formattedTimestamp = Yii::app()->dateFormatter->format(
204:                 'd MMM yy', $timestamp);
205:         } else if ($date['yday'] !== $now['yday']) { 
206:             $formattedTimestamp = Yii::app()->dateFormatter->format(
207:                 'd MMM', $timestamp);
208:         } else if ($now['hours'] - $date['hours'] > 1) { 
209:             $diffTs = $nowTs - $timestamp;
210:             $formattedTimestamp = floor ($diffTs / 60 / 60).'h';
211:         } else if ($now['minutes'] - $date['minutes'] > 1) { 
212:             $diffTs = $nowTs - $timestamp;
213:             $formattedTimestamp = floor ($diffTs / 60).'m';
214:         } else { 
215:             $diffTs = $nowTs - $timestamp;
216:             $formattedTimestamp = $diffTs.'s';
217:         }
218:         return '<a href="https://www.twitter.com/'.urlencode ($this->_username).'/status/'.
219:             $tweet['id_str'].'">'.
220:                 CHtml::encode ($formattedTimestamp).
221:             '</a>';
222:     }
223: 
224:     225: 226: 
227:     public function replaceTextEntities (array &$tweet) {
228:         if (isset ($tweet['retweeted_status'])) {
229:             $matches = array ();
230:             $name = $tweet['user']['name'];
231:             $retweetedByText = 
232:                 '<div class="retweeted-by-text-container">
233:                     <span class="retweet-icon-small"></span>'.
234:                     CHtml::encode (Yii::t('app', 'Retweeted by')).
235:                     ' <a href="https://twitter.com/'.urlencode ($name).'">'.
236:                         $name.
237:                     '</a>'.
238:                 '</div>';
239:             $tweet = $tweet['retweeted_status'];
240:         }
241: 
242: 
243:         if (!isset ($tweet['entities'])) return $tweet['text'];
244:         $text = $tweet['text'];
245:         $entities = $tweet['entities'];
246:         $orderedEntities = array ();
247: 
248:         
249:         foreach ($entities as $type => $entitiesOfType) {
250:             foreach ($entitiesOfType as $entity) {
251:                 $orderedEntities[] = array_merge (array ('type' => $type), $entity);
252:             }
253:         }
254:         
255:         usort ($orderedEntities, function ($a, $b) {
256:             return $b['indices'][0] - $a['indices'][0];
257:         });
258: 
259:         
260:         foreach ($orderedEntities as $entity) {
261:             switch ($entity['type']) {
262:                 case 'hashtags':
263:                     $link = "<a href='https://twitter.com/hashtag/".
264:                         urlencode ($entity['text'])."'?src=hash>
265:                         #".CHtml::encode ($entity['text'])."</a>";
266:                     break;
267:                 case 'symbols':
268:                     $link = "<a href='https://twitter.com/search?1=".
269:                         urlencode ($entity['text'])."'?src=ctag>
270:                         $".CHtml::encode ($entity['text'])."</a>";
271:                     break;
272:                 case 'urls':
273:                     $link = "<a 
274:                         title='".$entity['expanded_url']."'
275:                         href='".$entity['url']."'>
276:                         ".CHtml::encode ($entity['display_url'])."</a>";
277:                     break;
278:                 case 'user_mentions':
279:                     $link = "<a href='https://twitter.com/".urlencode ($entity['screen_name'])."'>
280:                         @".CHtml::encode ($entity['screen_name'])."</a>";
281:                     break;
282:                 default: 
283:                     continue 2;
284:             }
285:             $text = mb_substr ($text, 0, $entity['indices'][0], 'UTF-8').$link.
286:                 mb_substr ($text, $entity['indices'][1] + 1, null, 'UTF-8');
287:         }
288: 
289:         if (isset ($retweetedByText)) $text .= $retweetedByText;
290:         $tweet['text'] = $text;
291:     }
292: 
293:     294: 295: 296: 
297:     public function remainingRequests ($resourceName, $value=null) {
298:         $resourceName = preg_replace ('/\.json$/', '', $resourceName);
299:         $rateLimitStatus = $this->getRateLimitStatus ();
300: 
301: 
302: 
303: 
304:         if (!$rateLimitStatus) {
305:             return false; 
306:         }
307:         $matches = array ();
308:         preg_match ('/^\/([^\/]+)\//', $resourceName, $matches);
309:         $resourceCategory = $matches[1];
310: 
311: 
312: 
313: 
314: 
315:         if (!isset ($rateLimitStatus['resources'][$resourceCategory][$resourceName])) {
316:             return false; 
317:         }
318:         if ($value !== null) {
319:             $rateLimitStatus['resources'][$resourceCategory][$resourceName][
320:                 'remaining'] = $value;
321:             $this->setRateLimitStatus ($rateLimitStatus);
322:         } 
323: 
324:         $entry = $rateLimitStatus['resources'][$resourceCategory][$resourceName];
325:         $remaining = (int) $entry['remaining'];
326: 
327: 
328: 
329:         return $remaining;
330:     }
331: 
332:     333: 334: 
335:     private function setRateLimitStatus (array $rateLimitStatus) {
336:         $this->_rateLimits = $rateLimitStatus;
337:         Yii::app()->settings->twitterRateLimits = $rateLimitStatus;
338:         Yii::app()->settings->save ();
339:     }
340: 
341:     342: 343: 344: 
345:     private $_rateLimits;
346:     private function getRateLimitStatus () {
347:         if (isset ($this->_rateLimits)) return $this->_rateLimits;
348: 
349:         $rateLimitWindow = 60 * 15;
350: 
351:         
352:         $rateLimits = Yii::app()->settings->twitterRateLimits;
353:         if (ctype_digit ($rateLimits)) { 
354:             if ((int) $rateLimits >= time ()) { 
355:                 return false;
356:             }
357:         } elseif (is_array ($rateLimits)) { 
358:             
359:             
360:             
361:             
362:             if (!isset (
363:                     $rateLimits['resources']['application']['/application/rate_limit_status'])) {
364: 
365:                 Yii::app()->settings->twitterRateLimits = time () + $rateLimitWindow;
366:                 Yii::app()->settings->save ();
367:                 return false; 
368:             }
369: 
370:             $entry = $rateLimits['resources']['application']['/application/rate_limit_status'];
371:             if ($entry['reset'] > time ()) { 
372:                 if ((int) $entry['remaining'] < 1) {
373:                     
374:                     return false;
375:                 } else {
376:                     
377:                     
378:                     return $rateLimits;
379:                 }
380:             }
381: 
382:         } else if ($rateLimits !== null) {
383:             
384:             Yii::app()->settings->twitterRateLimits = time () + $rateLimitWindow;
385:             Yii::app()->settings->save ();
386:             return false; 
387:         }
388: 
389:         
390: 
391:         
392:         $credentials = $this->getTwitterCredentials ();
393:         $url = 'https://api.twitter.com/1.1/application/rate_limit_status.json';
394:         $requestMethod = 'GET';
395:         $twitter = new TwitterAPIExchange ($credentials);
396:         $rateLimitStatus = CJSON::decode (
397:             $twitter
398:             ->buildOauth ($url, $requestMethod)
399:             ->performRequest ()); 
400:         if (($statusCode = $twitter->getLastStatusCode ()) != 200) {
401:             $this->throwApiException ($rateLimitStatus, $statusCode);
402:         }
403:         Yii::app()->settings->twitterRateLimits = $rateLimitStatus;
404:         Yii::app()->settings->save ();
405: 
406:         $this->_rateLimits = $rateLimitStatus;
407:         return $rateLimitStatus;
408:     }
409: 
410:     411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 
423:     private ;
424:     public function  ($append=false) {
425:         $this->getRateLimitStatus ();
426:         $maxId = isset ($_GET['maxTweetId']) ? $_GET['maxTweetId'] : -1;
427: 
428:         if (!isset ($this->_tweets) || $append) {
429:             $username = $this->_username;
430:             $cache = Yii::app()->cache2;
431:             $cacheKey = $this->getCacheKey ();
432:             $pageSize = 5;
433:             $tweets = $cache->get ($cacheKey);
434: 
435:             if ($append && !$tweets) {
436:                 
437:                 
438:                 
439:                 $append = false;
440:                 $maxId = -1;
441:             }
442: 
443:             if (!$tweets || $append) { 
444:                 $tweetCount = 100;
445:                 $credentials = $this->getTwitterCredentials ();
446:                 $resourceName = '/statuses/user_timeline.json';
447:                 $remainingRequests = $this->remainingRequests ($resourceName);
448: 
449:                 if ($remainingRequests < 1) {
450:                     
451:                     throw new TwitterFeedWidgetException (Yii::t(
452:                         'app', 'Twitter feed could not be retrieved. Please try again later.'));
453:                 }
454: 
455:                 $url = 'https://api.twitter.com/1.1'.$resourceName;;
456:                 $getfield = '?screen_name='.$username.'&count='.$tweetCount;
457:                 if ($append) { 
458:                     $maxId = $tweets[count ($tweets) - 1]['id_str'];
459:                     $getfield .= '&max_id='.$maxId;
460:                 }
461: 
462:                 $requestMethod = 'GET';
463:                 $twitter = new TwitterAPIExchange ($credentials);
464:                 $oldTweets = $tweets;
465: 
466:                 $tweets = CJSON::decode ($twitter->setGetfield ($getfield)
467:                     ->buildOauth ($url, $requestMethod)
468:                     ->performRequest ()); 
469:                 if (($statusCode = $twitter->getLastStatusCode ()) != 200) {
470:                     $this->throwApiException ($tweets, $statusCode);
471:                 }
472:                 $this->remainingRequests ($resourceName, $remainingRequests - 1);
473:                 if ($append) {
474:                     $tweets = array_merge ($oldTweets, $tweets);
475:                 } 
476:                 $cache->set ($cacheKey, $tweets, 60 * 5);
477:                 
478:             } else {
479:                 
480:             }
481: 
482:             if ($maxId === -1) { 
483:                 $this->_tweets = array_slice ($tweets, 0, $pageSize);
484:             } else { 
485:                 $tweetCount = count ($tweets);
486:                 $found = false;
487:                 for ($i = 0; $i < $tweetCount; $i++) { 
488:                     $tweet = $tweets[$i];
489:                     if ($tweet['id_str'] == $maxId) {
490:                         $found = true; 
491:                         break;
492:                     }
493:                 }
494:                 if ($found && $i + $pageSize < $tweetCount) {
495:                     $this->_tweets = array_slice ($tweets, 0, $i + $pageSize + 1);
496:                 } else if (!$append) { 
497:                     return $this->requestTweets (true);
498:                 } else { 
499:                     $this->_tweets = array_slice ($tweets, 0, $pageSize);
500:                 }
501:             }
502:         }
503:         return $this->_tweets;
504:     }
505: 
506:     private ;
507:     public function  () {
508:         if (!isset ($this->_tweetDataProvider)) {
509:             $tweets = $this->requestTweets ();
510:             $this->_tweetDataProvider = new CArrayDataProvider ($tweets, array (
511:                 'pagination' => array (
512:                     'pageSize' => PHP_INT_MAX,
513:                 ),
514:             ));
515:         }
516:         return $this->_tweetDataProvider;
517:     }
518: 
519:     public function getTimeline () {
520:         if (isset ($_GET['twitterFeedAjax'])) {
521:             ob_clean ();
522:             ob_start (); 
523:         }
524:         $dataProvider = $this->getTweetDataProvider ();
525:         if (!$dataProvider) return;
526: 
527:         Yii::app()->controller->widget ('zii.widgets.CListView', array (
528:             'id' => 'twitter-feed',
529:             'ajaxVar' => 'twitterFeedAjax',
530:             'htmlOptions' => array (
531:                 'class' => 'list-view twitter-feed-list-view',
532:             ),
533:             'viewData' => array (
534:                 'twitterFeedWidget' => $this,
535:             ),
536:             'dataProvider' => $dataProvider,
537:             'itemView' => 'application.components.sortableWidget.views._tweet',
538:             'template' => '{items}',
539:         ));
540:         if (isset ($_GET['twitterFeedAjax'])) {
541:             echo '<script>x2.TwitterFeedWidget.lastTweetId = "'.
542:                 $this->getLastTweetId ().'";</script>';
543:             echo ob_get_clean (); 
544:             ob_flush ();
545:             Yii::app()->end ();
546:         }
547:     }
548: 
549:     protected function getJSSortableWidgetParams () {
550:         if (!isset ($this->_JSSortableWidgetParams)) {
551:             if (!$this->hasError ()) {
552:                 $lastTweetId = $this->getLastTweetId ();
553:             } else {
554:                 $lastTweetId = null;
555:             }
556:             $this->_JSSortableWidgetParams = array_merge (parent::getJSSortableWidgetParams (),
557:                 array (
558:                     'lastTweetId' => $lastTweetId,
559:                 )
560:             );
561:         }
562:         return $this->_JSSortableWidgetParams;
563:     }
564: 
565:     566: 567: 568: 
569:     private function throwApiException ($response, $code) {
570:         $error = isset ($response['error']) ? $response['error'] : '';
571:         switch ($code) {
572:             case 404: 
573:                 $message = Yii::t('app', 'Twitter username not found.');
574:                 break;
575:             case 401: 
576:                 $message = Yii::t(
577:                     'app', 'Twitter Integration credentials are missing or incorrect. Please '.
578:                         'contact an administrator.');
579:                 break;
580:             default:
581:                 $message = Yii::t('app', 'Twitter API {code} error{message}', array (
582:                     '{code}' => $code,
583:                     '{message}' => $error ?  ': '.$error : '',
584:                 ));
585:         }
586:         throw new TwitterFeedWidgetException ($message);
587:     }
588: }
589: 
590: class  extends CException {
591: }
592: 
593: ?>
594: