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