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