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