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