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