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