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