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