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