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