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