]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/apiaction.php
Assigning my copyrights to the Free Software Foundation
[quix0rs-gnu-social.git] / lib / apiaction.php
1 <?php
2 /**
3  * StatusNet, the distributed open-source microblogging tool
4  *
5  * Base API action
6  *
7  * PHP version 5
8  *
9  * LICENCE: This program is free software: you can redistribute it and/or modify
10  * it under the terms of the GNU Affero General Public License as published by
11  * the Free Software Foundation, either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU Affero General Public License for more details.
18  *
19  * You should have received a copy of the GNU Affero General Public License
20  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21  *
22  * @category  API
23  * @package   StatusNet
24  * @author    Craig Andrews <candrews@integralblue.com>
25  * @author    Dan Moore <dan@moore.cx>
26  * @author    Evan Prodromou <evan@status.net>
27  * @author    Jeffery To <jeffery.to@gmail.com>
28  * @author    Toby Inkster <mail@tobyinkster.co.uk>
29  * @author    Zach Copley <zach@status.net>
30  * @copyright 2009 StatusNet, Inc.
31  * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
32  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
33  * @link      http://status.net/
34  */
35
36 /* External API usage documentation. Please update when you change how the API works. */
37
38 /*! @mainpage StatusNet REST API
39
40     @section Introduction
41
42     Some explanatory text about the API would be nice.
43
44     @section API Methods
45
46     @subsection timelinesmethods_sec Timeline Methods
47
48     @li @ref publictimeline
49     @li @ref friendstimeline
50
51     @subsection statusmethods_sec Status Methods
52
53     @li @ref statusesupdate
54
55     @subsection usermethods_sec User Methods
56
57     @subsection directmessagemethods_sec Direct Message Methods
58
59     @subsection friendshipmethods_sec Friendship Methods
60
61     @subsection socialgraphmethods_sec Social Graph Methods
62
63     @subsection accountmethods_sec Account Methods
64
65     @subsection favoritesmethods_sec Favorites Methods
66
67     @subsection blockmethods_sec Block Methods
68
69     @subsection oauthmethods_sec OAuth Methods
70
71     @subsection helpmethods_sec Help Methods
72
73     @subsection groupmethods_sec Group Methods
74
75     @page apiroot API Root
76
77     The URLs for methods referred to in this API documentation are
78     relative to the StatusNet API root. The API root is determined by the
79     site's @b server and @b path variables, which are generally specified
80     in config.php. For example:
81
82     @code
83     $config['site']['server'] = 'example.org';
84     $config['site']['path'] = 'statusnet'
85     @endcode
86
87     The pattern for a site's API root is: @c protocol://server/path/api E.g:
88
89     @c http://example.org/statusnet/api
90
91     The @b path can be empty.  In that case the API root would simply be:
92
93     @c http://example.org/api
94
95 */
96
97 if (!defined('STATUSNET')) {
98     exit(1);
99 }
100
101 /**
102  * Contains most of the Twitter-compatible API output functions.
103  *
104  * @category API
105  * @package  StatusNet
106  * @author   Craig Andrews <candrews@integralblue.com>
107  * @author   Dan Moore <dan@moore.cx>
108  * @author   Evan Prodromou <evan@status.net>
109  * @author   Jeffery To <jeffery.to@gmail.com>
110  * @author   Toby Inkster <mail@tobyinkster.co.uk>
111  * @author   Zach Copley <zach@status.net>
112  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
113  * @link     http://status.net/
114  */
115
116 class ApiAction extends Action
117 {
118     const READ_ONLY  = 1;
119     const READ_WRITE = 2;
120
121     var $format    = null;
122     var $user      = null;
123     var $auth_user = null;
124     var $page      = null;
125     var $count     = null;
126     var $max_id    = null;
127     var $since_id  = null;
128     var $source    = null;
129
130     var $access    = self::READ_ONLY;  // read (default) or read-write
131
132     static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
133
134     /**
135      * Initialization.
136      *
137      * @param array $args Web and URL arguments
138      *
139      * @return boolean false if user doesn't exist
140      */
141
142     function prepare($args)
143     {
144         StatusNet::setApi(true); // reduce exception reports to aid in debugging
145         parent::prepare($args);
146
147         $this->format   = $this->arg('format');
148         $this->page     = (int)$this->arg('page', 1);
149         $this->count    = (int)$this->arg('count', 20);
150         $this->max_id   = (int)$this->arg('max_id', 0);
151         $this->since_id = (int)$this->arg('since_id', 0);
152
153         if ($this->arg('since')) {
154             header('X-StatusNet-Warning: since parameter is disabled; use since_id');
155         }
156
157         $this->source = $this->trimmed('source');
158
159         if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
160             $this->source = 'api';
161         }
162
163         return true;
164     }
165
166     /**
167      * Handle a request
168      *
169      * @param array $args Arguments from $_REQUEST
170      *
171      * @return void
172      */
173
174     function handle($args)
175     {
176         header('Access-Control-Allow-Origin: *');
177         parent::handle($args);
178     }
179
180     /**
181      * Overrides XMLOutputter::element to write booleans as strings (true|false).
182      * See that method's documentation for more info.
183      *
184      * @param string $tag     Element type or tagname
185      * @param array  $attrs   Array of element attributes, as
186      *                        key-value pairs
187      * @param string $content string content of the element
188      *
189      * @return void
190      */
191     function element($tag, $attrs=null, $content=null)
192     {
193         if (is_bool($content)) {
194             $content = ($content ? 'true' : 'false');
195         }
196
197         return parent::element($tag, $attrs, $content);
198     }
199
200     function twitterUserArray($profile, $get_notice=false)
201     {
202         $twitter_user = array();
203
204         $twitter_user['id'] = intval($profile->id);
205         $twitter_user['name'] = $profile->getBestName();
206         $twitter_user['screen_name'] = $profile->nickname;
207         $twitter_user['location'] = ($profile->location) ? $profile->location : null;
208         $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
209
210         $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
211         $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
212             Avatar::defaultImage(AVATAR_STREAM_SIZE);
213
214         $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
215         $twitter_user['protected'] = false; # not supported by StatusNet yet
216         $twitter_user['followers_count'] = $profile->subscriberCount();
217
218         $design        = null;
219         $user          = $profile->getUser();
220
221         // Note: some profiles don't have an associated user
222
223         $defaultDesign = Design::siteDesign();
224
225         if (!empty($user)) {
226             $design = $user->getDesign();
227         }
228
229         if (empty($design)) {
230             $design = $defaultDesign;
231         }
232
233         $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
234         $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
235         $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
236         $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
237         $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
238         $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
239         $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
240         $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
241         $twitter_user['profile_sidebar_border_color'] = '';
242
243         $twitter_user['friends_count'] = $profile->subscriptionCount();
244
245         $twitter_user['created_at'] = $this->dateTwitter($profile->created);
246
247         $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
248
249         $timezone = 'UTC';
250
251         if (!empty($user) && $user->timezone) {
252             $timezone = $user->timezone;
253         }
254
255         $t = new DateTime;
256         $t->setTimezone(new DateTimeZone($timezone));
257
258         $twitter_user['utc_offset'] = $t->format('Z');
259         $twitter_user['time_zone'] = $timezone;
260
261         $twitter_user['profile_background_image_url']
262             = empty($design->backgroundimage)
263             ? '' : ($design->disposition & BACKGROUND_ON)
264             ? Design::url($design->backgroundimage) : '';
265
266         $twitter_user['profile_background_tile']
267             = empty($design->disposition)
268             ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
269
270         $twitter_user['statuses_count'] = $profile->noticeCount();
271
272         // Is the requesting user following this user?
273         $twitter_user['following'] = false;
274         $twitter_user['notifications'] = false;
275
276         if (isset($this->auth_user)) {
277
278             $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
279
280             // Notifications on?
281             $sub = Subscription::pkeyGet(array('subscriber' =>
282                                                $this->auth_user->id,
283                                                'subscribed' => $profile->id));
284
285             if ($sub) {
286                 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
287             }
288         }
289
290         if ($get_notice) {
291             $notice = $profile->getCurrentNotice();
292             if ($notice) {
293                 # don't get user!
294                 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
295             }
296         }
297
298         return $twitter_user;
299     }
300
301     function twitterStatusArray($notice, $include_user=true)
302     {
303         $base = $this->twitterSimpleStatusArray($notice, $include_user);
304
305         if (!empty($notice->repeat_of)) {
306             $original = Notice::staticGet('id', $notice->repeat_of);
307             if (!empty($original)) {
308                 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
309                 $base['retweeted_status'] = $original_array;
310             }
311         }
312
313         return $base;
314     }
315
316     function twitterSimpleStatusArray($notice, $include_user=true)
317     {
318         $profile = $notice->getProfile();
319
320         $twitter_status = array();
321         $twitter_status['text'] = $notice->content;
322         $twitter_status['truncated'] = false; # Not possible on StatusNet
323         $twitter_status['created_at'] = $this->dateTwitter($notice->created);
324         $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
325             intval($notice->reply_to) : null;
326
327         $source = null;
328
329         $ns = $notice->getSource();
330         if ($ns) {
331             if (!empty($ns->name) && !empty($ns->url)) {
332                 $source = '<a href="'
333                     . htmlspecialchars($ns->url)
334                     . '" rel="nofollow">'
335                     . htmlspecialchars($ns->name)
336                     . '</a>';
337             } else {
338                 $source = $ns->code;
339             }
340         }
341
342         $twitter_status['source'] = $source;
343         $twitter_status['id'] = intval($notice->id);
344
345         $replier_profile = null;
346
347         if ($notice->reply_to) {
348             $reply = Notice::staticGet(intval($notice->reply_to));
349             if ($reply) {
350                 $replier_profile = $reply->getProfile();
351             }
352         }
353
354         $twitter_status['in_reply_to_user_id'] =
355             ($replier_profile) ? intval($replier_profile->id) : null;
356         $twitter_status['in_reply_to_screen_name'] =
357             ($replier_profile) ? $replier_profile->nickname : null;
358
359         if (isset($notice->lat) && isset($notice->lon)) {
360             // This is the format that GeoJSON expects stuff to be in
361             $twitter_status['geo'] = array('type' => 'Point',
362                                            'coordinates' => array((float) $notice->lat,
363                                                                   (float) $notice->lon));
364         } else {
365             $twitter_status['geo'] = null;
366         }
367
368         if (isset($this->auth_user)) {
369             $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
370         } else {
371             $twitter_status['favorited'] = false;
372         }
373
374         // Enclosures
375         $attachments = $notice->attachments();
376
377         if (!empty($attachments)) {
378
379             $twitter_status['attachments'] = array();
380
381             foreach ($attachments as $attachment) {
382                 $enclosure_o=$attachment->getEnclosure();
383                 if ($enclosure_o) {
384                     $enclosure = array();
385                     $enclosure['url'] = $enclosure_o->url;
386                     $enclosure['mimetype'] = $enclosure_o->mimetype;
387                     $enclosure['size'] = $enclosure_o->size;
388                     $twitter_status['attachments'][] = $enclosure;
389                 }
390             }
391         }
392
393         if ($include_user && $profile) {
394             # Don't get notice (recursive!)
395             $twitter_user = $this->twitterUserArray($profile, false);
396             $twitter_status['user'] = $twitter_user;
397         }
398
399         return $twitter_status;
400     }
401
402     function twitterGroupArray($group)
403     {
404         $twitter_group=array();
405         $twitter_group['id']=$group->id;
406         $twitter_group['url']=$group->permalink();
407         $twitter_group['nickname']=$group->nickname;
408         $twitter_group['fullname']=$group->fullname;
409         $twitter_group['original_logo']=$group->original_logo;
410         $twitter_group['homepage_logo']=$group->homepage_logo;
411         $twitter_group['stream_logo']=$group->stream_logo;
412         $twitter_group['mini_logo']=$group->mini_logo;
413         $twitter_group['homepage']=$group->homepage;
414         $twitter_group['description']=$group->description;
415         $twitter_group['location']=$group->location;
416         $twitter_group['created']=$this->dateTwitter($group->created);
417         $twitter_group['modified']=$this->dateTwitter($group->modified);
418         return $twitter_group;
419     }
420
421     function twitterRssGroupArray($group)
422     {
423         $entry = array();
424         $entry['content']=$group->description;
425         $entry['title']=$group->nickname;
426         $entry['link']=$group->permalink();
427         $entry['published']=common_date_iso8601($group->created);
428         $entry['updated']==common_date_iso8601($group->modified);
429         $taguribase = common_config('integration', 'groupuri');
430         $entry['id'] = "group:$groupuribase:$entry[link]";
431
432         $entry['description'] = $entry['content'];
433         $entry['pubDate'] = common_date_rfc2822($group->created);
434         $entry['guid'] = $entry['link'];
435
436         return $entry;
437     }
438
439     function twitterRssEntryArray($notice)
440     {
441         $profile = $notice->getProfile();
442         $entry = array();
443
444         // We trim() to avoid extraneous whitespace in the output
445
446         $entry['content'] = common_xml_safe_str(trim($notice->rendered));
447         $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
448         $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
449         $entry['published'] = common_date_iso8601($notice->created);
450
451         $taguribase = TagURI::base();
452         $entry['id'] = "tag:$taguribase:$entry[link]";
453
454         $entry['updated'] = $entry['published'];
455         $entry['author'] = $profile->getBestName();
456
457         // Enclosures
458         $attachments = $notice->attachments();
459         $enclosures = array();
460
461         foreach ($attachments as $attachment) {
462             $enclosure_o=$attachment->getEnclosure();
463             if ($enclosure_o) {
464                  $enclosure = array();
465                  $enclosure['url'] = $enclosure_o->url;
466                  $enclosure['mimetype'] = $enclosure_o->mimetype;
467                  $enclosure['size'] = $enclosure_o->size;
468                  $enclosures[] = $enclosure;
469             }
470         }
471
472         if (!empty($enclosures)) {
473             $entry['enclosures'] = $enclosures;
474         }
475
476         // Tags/Categories
477         $tag = new Notice_tag();
478         $tag->notice_id = $notice->id;
479         if ($tag->find()) {
480             $entry['tags']=array();
481             while ($tag->fetch()) {
482                 $entry['tags'][]=$tag->tag;
483             }
484         }
485         $tag->free();
486
487         // RSS Item specific
488         $entry['description'] = $entry['content'];
489         $entry['pubDate'] = common_date_rfc2822($notice->created);
490         $entry['guid'] = $entry['link'];
491
492         if (isset($notice->lat) && isset($notice->lon)) {
493             // This is the format that GeoJSON expects stuff to be in.
494             // showGeoRSS() below uses it for XML output, so we reuse it
495             $entry['geo'] = array('type' => 'Point',
496                                   'coordinates' => array((float) $notice->lat,
497                                                          (float) $notice->lon));
498         } else {
499             $entry['geo'] = null;
500         }
501
502         return $entry;
503     }
504
505     function twitterRelationshipArray($source, $target)
506     {
507         $relationship = array();
508
509         $relationship['source'] =
510             $this->relationshipDetailsArray($source, $target);
511         $relationship['target'] =
512             $this->relationshipDetailsArray($target, $source);
513
514         return array('relationship' => $relationship);
515     }
516
517     function relationshipDetailsArray($source, $target)
518     {
519         $details = array();
520
521         $details['screen_name'] = $source->nickname;
522         $details['followed_by'] = $target->isSubscribed($source);
523         $details['following'] = $source->isSubscribed($target);
524
525         $notifications = false;
526
527         if ($source->isSubscribed($target)) {
528
529             $sub = Subscription::pkeyGet(array('subscriber' =>
530                 $source->id, 'subscribed' => $target->id));
531
532             if (!empty($sub)) {
533                 $notifications = ($sub->jabber || $sub->sms);
534             }
535         }
536
537         $details['notifications_enabled'] = $notifications;
538         $details['blocking'] = $source->hasBlocked($target);
539         $details['id'] = $source->id;
540
541         return $details;
542     }
543
544     function showTwitterXmlRelationship($relationship)
545     {
546         $this->elementStart('relationship');
547
548         foreach($relationship as $element => $value) {
549             if ($element == 'source' || $element == 'target') {
550                 $this->elementStart($element);
551                 $this->showXmlRelationshipDetails($value);
552                 $this->elementEnd($element);
553             }
554         }
555
556         $this->elementEnd('relationship');
557     }
558
559     function showXmlRelationshipDetails($details)
560     {
561         foreach($details as $element => $value) {
562             $this->element($element, null, $value);
563         }
564     }
565
566     function showTwitterXmlStatus($twitter_status, $tag='status')
567     {
568         $this->elementStart($tag);
569         foreach($twitter_status as $element => $value) {
570             switch ($element) {
571             case 'user':
572                 $this->showTwitterXmlUser($twitter_status['user']);
573                 break;
574             case 'text':
575                 $this->element($element, null, common_xml_safe_str($value));
576                 break;
577             case 'attachments':
578                 $this->showXmlAttachments($twitter_status['attachments']);
579                 break;
580             case 'geo':
581                 $this->showGeoXML($value);
582                 break;
583             case 'retweeted_status':
584                 $this->showTwitterXmlStatus($value, 'retweeted_status');
585                 break;
586             default:
587                 $this->element($element, null, $value);
588             }
589         }
590         $this->elementEnd($tag);
591     }
592
593     function showTwitterXmlGroup($twitter_group)
594     {
595         $this->elementStart('group');
596         foreach($twitter_group as $element => $value) {
597             $this->element($element, null, $value);
598         }
599         $this->elementEnd('group');
600     }
601
602     function showTwitterXmlUser($twitter_user, $role='user')
603     {
604         $this->elementStart($role);
605         foreach($twitter_user as $element => $value) {
606             if ($element == 'status') {
607                 $this->showTwitterXmlStatus($twitter_user['status']);
608             } else {
609                 $this->element($element, null, $value);
610             }
611         }
612         $this->elementEnd($role);
613     }
614
615     function showXmlAttachments($attachments) {
616         if (!empty($attachments)) {
617             $this->elementStart('attachments', array('type' => 'array'));
618             foreach ($attachments as $attachment) {
619                 $attrs = array();
620                 $attrs['url'] = $attachment['url'];
621                 $attrs['mimetype'] = $attachment['mimetype'];
622                 $attrs['size'] = $attachment['size'];
623                 $this->element('enclosure', $attrs, '');
624             }
625             $this->elementEnd('attachments');
626         }
627     }
628
629     function showGeoXML($geo)
630     {
631         if (empty($geo)) {
632             // empty geo element
633             $this->element('geo');
634         } else {
635             $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
636             $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
637             $this->elementEnd('geo');
638         }
639     }
640
641     function showGeoRSS($geo)
642     {
643         if (!empty($geo)) {
644             $this->element(
645                 'georss:point',
646                 null,
647                 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
648             );
649         }
650     }
651
652     function showTwitterRssItem($entry)
653     {
654         $this->elementStart('item');
655         $this->element('title', null, $entry['title']);
656         $this->element('description', null, $entry['description']);
657         $this->element('pubDate', null, $entry['pubDate']);
658         $this->element('guid', null, $entry['guid']);
659         $this->element('link', null, $entry['link']);
660
661         # RSS only supports 1 enclosure per item
662         if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
663             $enclosure = $entry['enclosures'][0];
664             $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
665         }
666
667         if(array_key_exists('tags', $entry)){
668             foreach($entry['tags'] as $tag){
669                 $this->element('category', null,$tag);
670             }
671         }
672
673         $this->showGeoRSS($entry['geo']);
674         $this->elementEnd('item');
675     }
676
677     function showJsonObjects($objects)
678     {
679         print(json_encode($objects));
680     }
681
682     function showSingleXmlStatus($notice)
683     {
684         $this->initDocument('xml');
685         $twitter_status = $this->twitterStatusArray($notice);
686         $this->showTwitterXmlStatus($twitter_status);
687         $this->endDocument('xml');
688     }
689
690     function show_single_json_status($notice)
691     {
692         $this->initDocument('json');
693         $status = $this->twitterStatusArray($notice);
694         $this->showJsonObjects($status);
695         $this->endDocument('json');
696     }
697
698     function showXmlTimeline($notice)
699     {
700
701         $this->initDocument('xml');
702         $this->elementStart('statuses', array('type' => 'array'));
703
704         if (is_array($notice)) {
705             foreach ($notice as $n) {
706                 $twitter_status = $this->twitterStatusArray($n);
707                 $this->showTwitterXmlStatus($twitter_status);
708             }
709         } else {
710             while ($notice->fetch()) {
711                 $twitter_status = $this->twitterStatusArray($notice);
712                 $this->showTwitterXmlStatus($twitter_status);
713             }
714         }
715
716         $this->elementEnd('statuses');
717         $this->endDocument('xml');
718     }
719
720     function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
721     {
722
723         $this->initDocument('rss');
724
725         $this->element('title', null, $title);
726         $this->element('link', null, $link);
727
728         if (!is_null($self)) {
729             $this->element(
730                 'atom:link',
731                 array(
732                     'type' => 'application/rss+xml',
733                     'href' => $self,
734                     'rel'  => 'self'
735                 )
736            );
737         }
738
739         if (!is_null($suplink)) {
740             // For FriendFeed's SUP protocol
741             $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
742                                          'rel' => 'http://api.friendfeed.com/2008/03#sup',
743                                          'href' => $suplink,
744                                          'type' => 'application/json'));
745         }
746
747         if (!is_null($logo)) {
748             $this->elementStart('image');
749             $this->element('link', null, $link);
750             $this->element('title', null, $title);
751             $this->element('url', null, $logo);
752             $this->elementEnd('image');
753         }
754
755         $this->element('description', null, $subtitle);
756         $this->element('language', null, 'en-us');
757         $this->element('ttl', null, '40');
758
759         if (is_array($notice)) {
760             foreach ($notice as $n) {
761                 $entry = $this->twitterRssEntryArray($n);
762                 $this->showTwitterRssItem($entry);
763             }
764         } else {
765             while ($notice->fetch()) {
766                 $entry = $this->twitterRssEntryArray($notice);
767                 $this->showTwitterRssItem($entry);
768             }
769         }
770
771         $this->endTwitterRss();
772     }
773
774     function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
775     {
776
777         $this->initDocument('atom');
778
779         $this->element('title', null, $title);
780         $this->element('id', null, $id);
781         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
782
783         if (!is_null($logo)) {
784             $this->element('logo',null,$logo);
785         }
786
787         if (!is_null($suplink)) {
788             # For FriendFeed's SUP protocol
789             $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
790                                          'href' => $suplink,
791                                          'type' => 'application/json'));
792         }
793
794         if (!is_null($selfuri)) {
795             $this->element('link', array('href' => $selfuri,
796                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
797         }
798
799         $this->element('updated', null, common_date_iso8601('now'));
800         $this->element('subtitle', null, $subtitle);
801
802         if (is_array($notice)) {
803             foreach ($notice as $n) {
804                 $this->raw($n->asAtomEntry());
805             }
806         } else {
807             while ($notice->fetch()) {
808                 $this->raw($notice->asAtomEntry());
809             }
810         }
811
812         $this->endDocument('atom');
813
814     }
815
816     function showRssGroups($group, $title, $link, $subtitle)
817     {
818
819         $this->initDocument('rss');
820
821         $this->element('title', null, $title);
822         $this->element('link', null, $link);
823         $this->element('description', null, $subtitle);
824         $this->element('language', null, 'en-us');
825         $this->element('ttl', null, '40');
826
827         if (is_array($group)) {
828             foreach ($group as $g) {
829                 $twitter_group = $this->twitterRssGroupArray($g);
830                 $this->showTwitterRssItem($twitter_group);
831             }
832         } else {
833             while ($group->fetch()) {
834                 $twitter_group = $this->twitterRssGroupArray($group);
835                 $this->showTwitterRssItem($twitter_group);
836             }
837         }
838
839         $this->endTwitterRss();
840     }
841
842     function showTwitterAtomEntry($entry)
843     {
844         $this->elementStart('entry');
845         $this->element('title', null, common_xml_safe_str($entry['title']));
846         $this->element(
847             'content',
848             array('type' => 'html'),
849             common_xml_safe_str($entry['content'])
850         );
851         $this->element('id', null, $entry['id']);
852         $this->element('published', null, $entry['published']);
853         $this->element('updated', null, $entry['updated']);
854         $this->element('link', array('type' => 'text/html',
855                                      'href' => $entry['link'],
856                                      'rel' => 'alternate'));
857         $this->element('link', array('type' => $entry['avatar-type'],
858                                      'href' => $entry['avatar'],
859                                      'rel' => 'image'));
860         $this->elementStart('author');
861
862         $this->element('name', null, $entry['author-name']);
863         $this->element('uri', null, $entry['author-uri']);
864
865         $this->elementEnd('author');
866         $this->elementEnd('entry');
867     }
868
869     function showXmlDirectMessage($dm)
870     {
871         $this->elementStart('direct_message');
872         foreach($dm as $element => $value) {
873             switch ($element) {
874             case 'sender':
875             case 'recipient':
876                 $this->showTwitterXmlUser($value, $element);
877                 break;
878             case 'text':
879                 $this->element($element, null, common_xml_safe_str($value));
880                 break;
881             default:
882                 $this->element($element, null, $value);
883                 break;
884             }
885         }
886         $this->elementEnd('direct_message');
887     }
888
889     function directMessageArray($message)
890     {
891         $dmsg = array();
892
893         $from_profile = $message->getFrom();
894         $to_profile = $message->getTo();
895
896         $dmsg['id'] = $message->id;
897         $dmsg['sender_id'] = $message->from_profile;
898         $dmsg['text'] = trim($message->content);
899         $dmsg['recipient_id'] = $message->to_profile;
900         $dmsg['created_at'] = $this->dateTwitter($message->created);
901         $dmsg['sender_screen_name'] = $from_profile->nickname;
902         $dmsg['recipient_screen_name'] = $to_profile->nickname;
903         $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
904         $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
905
906         return $dmsg;
907     }
908
909     function rssDirectMessageArray($message)
910     {
911         $entry = array();
912
913         $from = $message->getFrom();
914
915         $entry['title'] = sprintf('Message from %1$s to %2$s',
916             $from->nickname, $message->getTo()->nickname);
917
918         $entry['content'] = common_xml_safe_str($message->rendered);
919         $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
920         $entry['published'] = common_date_iso8601($message->created);
921
922         $taguribase = TagURI::base();
923
924         $entry['id'] = "tag:$taguribase:$entry[link]";
925         $entry['updated'] = $entry['published'];
926
927         $entry['author-name'] = $from->getBestName();
928         $entry['author-uri'] = $from->homepage;
929
930         $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
931
932         $entry['avatar']      = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
933         $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
934
935         // RSS item specific
936
937         $entry['description'] = $entry['content'];
938         $entry['pubDate'] = common_date_rfc2822($message->created);
939         $entry['guid'] = $entry['link'];
940
941         return $entry;
942     }
943
944     function showSingleXmlDirectMessage($message)
945     {
946         $this->initDocument('xml');
947         $dmsg = $this->directMessageArray($message);
948         $this->showXmlDirectMessage($dmsg);
949         $this->endDocument('xml');
950     }
951
952     function showSingleJsonDirectMessage($message)
953     {
954         $this->initDocument('json');
955         $dmsg = $this->directMessageArray($message);
956         $this->showJsonObjects($dmsg);
957         $this->endDocument('json');
958     }
959
960     function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
961     {
962
963         $this->initDocument('atom');
964
965         $this->element('title', null, common_xml_safe_str($title));
966         $this->element('id', null, $id);
967         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
968
969         if (!is_null($selfuri)) {
970             $this->element('link', array('href' => $selfuri,
971                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
972         }
973
974         $this->element('updated', null, common_date_iso8601('now'));
975         $this->element('subtitle', null, common_xml_safe_str($subtitle));
976
977         if (is_array($group)) {
978             foreach ($group as $g) {
979                 $this->raw($g->asAtomEntry());
980             }
981         } else {
982             while ($group->fetch()) {
983                 $this->raw($group->asAtomEntry());
984             }
985         }
986
987         $this->endDocument('atom');
988
989     }
990
991     function showJsonTimeline($notice)
992     {
993
994         $this->initDocument('json');
995
996         $statuses = array();
997
998         if (is_array($notice)) {
999             foreach ($notice as $n) {
1000                 $twitter_status = $this->twitterStatusArray($n);
1001                 array_push($statuses, $twitter_status);
1002             }
1003         } else {
1004             while ($notice->fetch()) {
1005                 $twitter_status = $this->twitterStatusArray($notice);
1006                 array_push($statuses, $twitter_status);
1007             }
1008         }
1009
1010         $this->showJsonObjects($statuses);
1011
1012         $this->endDocument('json');
1013     }
1014
1015     function showJsonGroups($group)
1016     {
1017
1018         $this->initDocument('json');
1019
1020         $groups = array();
1021
1022         if (is_array($group)) {
1023             foreach ($group as $g) {
1024                 $twitter_group = $this->twitterGroupArray($g);
1025                 array_push($groups, $twitter_group);
1026             }
1027         } else {
1028             while ($group->fetch()) {
1029                 $twitter_group = $this->twitterGroupArray($group);
1030                 array_push($groups, $twitter_group);
1031             }
1032         }
1033
1034         $this->showJsonObjects($groups);
1035
1036         $this->endDocument('json');
1037     }
1038
1039     function showXmlGroups($group)
1040     {
1041
1042         $this->initDocument('xml');
1043         $this->elementStart('groups', array('type' => 'array'));
1044
1045         if (is_array($group)) {
1046             foreach ($group as $g) {
1047                 $twitter_group = $this->twitterGroupArray($g);
1048                 $this->showTwitterXmlGroup($twitter_group);
1049             }
1050         } else {
1051             while ($group->fetch()) {
1052                 $twitter_group = $this->twitterGroupArray($group);
1053                 $this->showTwitterXmlGroup($twitter_group);
1054             }
1055         }
1056
1057         $this->elementEnd('groups');
1058         $this->endDocument('xml');
1059     }
1060
1061     function showTwitterXmlUsers($user)
1062     {
1063
1064         $this->initDocument('xml');
1065         $this->elementStart('users', array('type' => 'array'));
1066
1067         if (is_array($user)) {
1068             foreach ($user as $u) {
1069                 $twitter_user = $this->twitterUserArray($u);
1070                 $this->showTwitterXmlUser($twitter_user);
1071             }
1072         } else {
1073             while ($user->fetch()) {
1074                 $twitter_user = $this->twitterUserArray($user);
1075                 $this->showTwitterXmlUser($twitter_user);
1076             }
1077         }
1078
1079         $this->elementEnd('users');
1080         $this->endDocument('xml');
1081     }
1082
1083     function showJsonUsers($user)
1084     {
1085
1086         $this->initDocument('json');
1087
1088         $users = array();
1089
1090         if (is_array($user)) {
1091             foreach ($user as $u) {
1092                 $twitter_user = $this->twitterUserArray($u);
1093                 array_push($users, $twitter_user);
1094             }
1095         } else {
1096             while ($user->fetch()) {
1097                 $twitter_user = $this->twitterUserArray($user);
1098                 array_push($users, $twitter_user);
1099             }
1100         }
1101
1102         $this->showJsonObjects($users);
1103
1104         $this->endDocument('json');
1105     }
1106
1107     function showSingleJsonGroup($group)
1108     {
1109         $this->initDocument('json');
1110         $twitter_group = $this->twitterGroupArray($group);
1111         $this->showJsonObjects($twitter_group);
1112         $this->endDocument('json');
1113     }
1114
1115     function showSingleXmlGroup($group)
1116     {
1117         $this->initDocument('xml');
1118         $twitter_group = $this->twitterGroupArray($group);
1119         $this->showTwitterXmlGroup($twitter_group);
1120         $this->endDocument('xml');
1121     }
1122
1123     function dateTwitter($dt)
1124     {
1125         $dateStr = date('d F Y H:i:s', strtotime($dt));
1126         $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1127         $d->setTimezone(new DateTimeZone(common_timezone()));
1128         return $d->format('D M d H:i:s O Y');
1129     }
1130
1131     function initDocument($type='xml')
1132     {
1133         switch ($type) {
1134         case 'xml':
1135             header('Content-Type: application/xml; charset=utf-8');
1136             $this->startXML();
1137             break;
1138         case 'json':
1139             header('Content-Type: application/json; charset=utf-8');
1140
1141             // Check for JSONP callback
1142             $callback = $this->arg('callback');
1143             if ($callback) {
1144                 print $callback . '(';
1145             }
1146             break;
1147         case 'rss':
1148             header("Content-Type: application/rss+xml; charset=utf-8");
1149             $this->initTwitterRss();
1150             break;
1151         case 'atom':
1152             header('Content-Type: application/atom+xml; charset=utf-8');
1153             $this->initTwitterAtom();
1154             break;
1155         default:
1156             // TRANS: Client error on an API request with an unsupported data format.
1157             $this->clientError(_('Not a supported data format.'));
1158             break;
1159         }
1160
1161         return;
1162     }
1163
1164     function endDocument($type='xml')
1165     {
1166         switch ($type) {
1167         case 'xml':
1168             $this->endXML();
1169             break;
1170         case 'json':
1171
1172             // Check for JSONP callback
1173             $callback = $this->arg('callback');
1174             if ($callback) {
1175                 print ')';
1176             }
1177             break;
1178         case 'rss':
1179             $this->endTwitterRss();
1180             break;
1181         case 'atom':
1182             $this->endTwitterRss();
1183             break;
1184         default:
1185             // TRANS: Client error on an API request with an unsupported data format.
1186             $this->clientError(_('Not a supported data format.'));
1187             break;
1188         }
1189         return;
1190     }
1191
1192     function clientError($msg, $code = 400, $format = 'xml')
1193     {
1194         $action = $this->trimmed('action');
1195
1196         common_debug("User error '$code' on '$action': $msg", __FILE__);
1197
1198         if (!array_key_exists($code, ClientErrorAction::$status)) {
1199             $code = 400;
1200         }
1201
1202         $status_string = ClientErrorAction::$status[$code];
1203
1204         header('HTTP/1.1 '.$code.' '.$status_string);
1205
1206         if ($format == 'xml') {
1207             $this->initDocument('xml');
1208             $this->elementStart('hash');
1209             $this->element('error', null, $msg);
1210             $this->element('request', null, $_SERVER['REQUEST_URI']);
1211             $this->elementEnd('hash');
1212             $this->endDocument('xml');
1213         } elseif ($format == 'json'){
1214             $this->initDocument('json');
1215             $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1216             print(json_encode($error_array));
1217             $this->endDocument('json');
1218         } else {
1219
1220             // If user didn't request a useful format, throw a regular client error
1221             throw new ClientException($msg, $code);
1222         }
1223     }
1224
1225     function serverError($msg, $code = 500, $content_type = 'xml')
1226     {
1227         $action = $this->trimmed('action');
1228
1229         common_debug("Server error '$code' on '$action': $msg", __FILE__);
1230
1231         if (!array_key_exists($code, ServerErrorAction::$status)) {
1232             $code = 400;
1233         }
1234
1235         $status_string = ServerErrorAction::$status[$code];
1236
1237         header('HTTP/1.1 '.$code.' '.$status_string);
1238
1239         if ($content_type == 'xml') {
1240             $this->initDocument('xml');
1241             $this->elementStart('hash');
1242             $this->element('error', null, $msg);
1243             $this->element('request', null, $_SERVER['REQUEST_URI']);
1244             $this->elementEnd('hash');
1245             $this->endDocument('xml');
1246         } else {
1247             $this->initDocument('json');
1248             $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1249             print(json_encode($error_array));
1250             $this->endDocument('json');
1251         }
1252     }
1253
1254     function initTwitterRss()
1255     {
1256         $this->startXML();
1257         $this->elementStart(
1258             'rss',
1259             array(
1260                 'version'      => '2.0',
1261                 'xmlns:atom'   => 'http://www.w3.org/2005/Atom',
1262                 'xmlns:georss' => 'http://www.georss.org/georss'
1263             )
1264         );
1265         $this->elementStart('channel');
1266         Event::handle('StartApiRss', array($this));
1267     }
1268
1269     function endTwitterRss()
1270     {
1271         $this->elementEnd('channel');
1272         $this->elementEnd('rss');
1273         $this->endXML();
1274     }
1275
1276     function initTwitterAtom()
1277     {
1278         $this->startXML();
1279         // FIXME: don't hardcode the language here!
1280         $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1281                                           'xml:lang' => 'en-US',
1282                                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1283     }
1284
1285     function endTwitterAtom()
1286     {
1287         $this->elementEnd('feed');
1288         $this->endXML();
1289     }
1290
1291     function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1292     {
1293         $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1294         switch ($content_type) {
1295         case 'xml':
1296             $this->showTwitterXmlUser($profile_array);
1297             break;
1298         case 'json':
1299             $this->showJsonObjects($profile_array);
1300             break;
1301         default:
1302             // TRANS: Client error on an API request with an unsupported data format.
1303             $this->clientError(_('Not a supported data format.'));
1304             return;
1305         }
1306         return;
1307     }
1308
1309     function getTargetUser($id)
1310     {
1311         if (empty($id)) {
1312
1313             // Twitter supports these other ways of passing the user ID
1314             if (is_numeric($this->arg('id'))) {
1315                 return User::staticGet($this->arg('id'));
1316             } else if ($this->arg('id')) {
1317                 $nickname = common_canonical_nickname($this->arg('id'));
1318                 return User::staticGet('nickname', $nickname);
1319             } else if ($this->arg('user_id')) {
1320                 // This is to ensure that a non-numeric user_id still
1321                 // overrides screen_name even if it doesn't get used
1322                 if (is_numeric($this->arg('user_id'))) {
1323                     return User::staticGet('id', $this->arg('user_id'));
1324                 }
1325             } else if ($this->arg('screen_name')) {
1326                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1327                 return User::staticGet('nickname', $nickname);
1328             } else {
1329                 // Fall back to trying the currently authenticated user
1330                 return $this->auth_user;
1331             }
1332
1333         } else if (is_numeric($id)) {
1334             return User::staticGet($id);
1335         } else {
1336             $nickname = common_canonical_nickname($id);
1337             return User::staticGet('nickname', $nickname);
1338         }
1339     }
1340
1341     function getTargetGroup($id)
1342     {
1343         if (empty($id)) {
1344             if (is_numeric($this->arg('id'))) {
1345                 return User_group::staticGet($this->arg('id'));
1346             } else if ($this->arg('id')) {
1347                 $nickname = common_canonical_nickname($this->arg('id'));
1348                 $local = Local_group::staticGet('nickname', $nickname);
1349                 if (empty($local)) {
1350                     return null;
1351                 } else {
1352                     return User_group::staticGet('id', $local->id);
1353                 }
1354             } else if ($this->arg('group_id')) {
1355                 // This is to ensure that a non-numeric user_id still
1356                 // overrides screen_name even if it doesn't get used
1357                 if (is_numeric($this->arg('group_id'))) {
1358                     return User_group::staticGet('id', $this->arg('group_id'));
1359                 }
1360             } else if ($this->arg('group_name')) {
1361                 $nickname = common_canonical_nickname($this->arg('group_name'));
1362                 $local = Local_group::staticGet('nickname', $nickname);
1363                 if (empty($local)) {
1364                     return null;
1365                 } else {
1366                     return User_group::staticGet('id', $local->group_id);
1367                 }
1368             }
1369
1370         } else if (is_numeric($id)) {
1371             return User_group::staticGet($id);
1372         } else {
1373             $nickname = common_canonical_nickname($id);
1374             $local = Local_group::staticGet('nickname', $nickname);
1375             if (empty($local)) {
1376                 return null;
1377             } else {
1378                 return User_group::staticGet('id', $local->group_id);
1379             }
1380         }
1381     }
1382
1383     /**
1384      * Returns query argument or default value if not found. Certain
1385      * parameters used throughout the API are lightly scrubbed and
1386      * bounds checked.  This overrides Action::arg().
1387      *
1388      * @param string $key requested argument
1389      * @param string $def default value to return if $key is not provided
1390      *
1391      * @return var $var
1392      */
1393     function arg($key, $def=null)
1394     {
1395
1396         // XXX: Do even more input validation/scrubbing?
1397
1398         if (array_key_exists($key, $this->args)) {
1399             switch($key) {
1400             case 'page':
1401                 $page = (int)$this->args['page'];
1402                 return ($page < 1) ? 1 : $page;
1403             case 'count':
1404                 $count = (int)$this->args['count'];
1405                 if ($count < 1) {
1406                     return 20;
1407                 } elseif ($count > 200) {
1408                     return 200;
1409                 } else {
1410                     return $count;
1411                 }
1412             case 'since_id':
1413                 $since_id = (int)$this->args['since_id'];
1414                 return ($since_id < 1) ? 0 : $since_id;
1415             case 'max_id':
1416                 $max_id = (int)$this->args['max_id'];
1417                 return ($max_id < 1) ? 0 : $max_id;
1418             default:
1419                 return parent::arg($key, $def);
1420             }
1421         } else {
1422             return $def;
1423         }
1424     }
1425
1426     /**
1427      * Calculate the complete URI that called up this action.  Used for
1428      * Atom rel="self" links.  Warning: this is funky.
1429      *
1430      * @return string URL    a URL suitable for rel="self" Atom links
1431      */
1432     function getSelfUri()
1433     {
1434         $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1435
1436         $id = $this->arg('id');
1437         $aargs = array('format' => $this->format);
1438         if (!empty($id)) {
1439             $aargs['id'] = $id;
1440         }
1441
1442         $tag = $this->arg('tag');
1443         if (!empty($tag)) {
1444             $aargs['tag'] = $tag;
1445         }
1446
1447         parse_str($_SERVER['QUERY_STRING'], $params);
1448         $pstring = '';
1449         if (!empty($params)) {
1450             unset($params['p']);
1451             $pstring = http_build_query($params);
1452         }
1453
1454         $uri = common_local_url($action, $aargs);
1455
1456         if (!empty($pstring)) {
1457             $uri .= '?' . $pstring;
1458         }
1459
1460         return $uri;
1461     }
1462
1463 }