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