]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/apiaction.php
Fix ticket #2700: some numeric IDs were misinterpreted as hex numbers instead of...
[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 show_single_json_status($notice)
730     {
731         $this->initDocument('json');
732         $status = $this->twitterStatusArray($notice);
733         $this->showJsonObjects($status);
734         $this->endDocument('json');
735     }
736
737     function showXmlTimeline($notice)
738     {
739         $this->initDocument('xml');
740         $this->elementStart('statuses', array('type' => 'array',
741                                               'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
742
743         if (is_array($notice)) {
744             $notice = new ArrayWrapper($notice);
745         }
746
747         while ($notice->fetch()) {
748             try {
749                 $twitter_status = $this->twitterStatusArray($notice);
750                 $this->showTwitterXmlStatus($twitter_status);
751             } catch (Exception $e) {
752                 common_log(LOG_ERR, $e->getMessage());
753                 continue;
754             }
755         }
756
757         $this->elementEnd('statuses');
758         $this->endDocument('xml');
759     }
760
761     function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
762     {
763         $this->initDocument('rss');
764
765         $this->element('title', null, $title);
766         $this->element('link', null, $link);
767
768         if (!is_null($self)) {
769             $this->element(
770                 'atom:link',
771                 array(
772                     'type' => 'application/rss+xml',
773                     'href' => $self,
774                     'rel'  => 'self'
775                 )
776            );
777         }
778
779         if (!is_null($suplink)) {
780             // For FriendFeed's SUP protocol
781             $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
782                                          'rel' => 'http://api.friendfeed.com/2008/03#sup',
783                                          'href' => $suplink,
784                                          'type' => 'application/json'));
785         }
786
787         if (!is_null($logo)) {
788             $this->elementStart('image');
789             $this->element('link', null, $link);
790             $this->element('title', null, $title);
791             $this->element('url', null, $logo);
792             $this->elementEnd('image');
793         }
794
795         $this->element('description', null, $subtitle);
796         $this->element('language', null, 'en-us');
797         $this->element('ttl', null, '40');
798
799         if (is_array($notice)) {
800             $notice = new ArrayWrapper($notice);
801         }
802
803         while ($notice->fetch()) {
804             try {
805                 $entry = $this->twitterRssEntryArray($notice);
806                 $this->showTwitterRssItem($entry);
807             } catch (Exception $e) {
808                 common_log(LOG_ERR, $e->getMessage());
809                 // continue on exceptions
810             }
811         }
812
813         $this->endTwitterRss();
814     }
815
816     function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
817     {
818         $this->initDocument('atom');
819
820         $this->element('title', null, $title);
821         $this->element('id', null, $id);
822         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
823
824         if (!is_null($logo)) {
825             $this->element('logo',null,$logo);
826         }
827
828         if (!is_null($suplink)) {
829             # For FriendFeed's SUP protocol
830             $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
831                                          'href' => $suplink,
832                                          'type' => 'application/json'));
833         }
834
835         if (!is_null($selfuri)) {
836             $this->element('link', array('href' => $selfuri,
837                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
838         }
839
840         $this->element('updated', null, common_date_iso8601('now'));
841         $this->element('subtitle', null, $subtitle);
842
843         if (is_array($notice)) {
844             $notice = new ArrayWrapper($notice);
845         }
846
847         while ($notice->fetch()) {
848             try {
849                 $this->raw($notice->asAtomEntry());
850             } catch (Exception $e) {
851                 common_log(LOG_ERR, $e->getMessage());
852                 continue;
853             }
854         }
855
856         $this->endDocument('atom');
857     }
858
859     function showRssGroups($group, $title, $link, $subtitle)
860     {
861         $this->initDocument('rss');
862
863         $this->element('title', null, $title);
864         $this->element('link', null, $link);
865         $this->element('description', null, $subtitle);
866         $this->element('language', null, 'en-us');
867         $this->element('ttl', null, '40');
868
869         if (is_array($group)) {
870             foreach ($group as $g) {
871                 $twitter_group = $this->twitterRssGroupArray($g);
872                 $this->showTwitterRssItem($twitter_group);
873             }
874         } else {
875             while ($group->fetch()) {
876                 $twitter_group = $this->twitterRssGroupArray($group);
877                 $this->showTwitterRssItem($twitter_group);
878             }
879         }
880
881         $this->endTwitterRss();
882     }
883
884     function showTwitterAtomEntry($entry)
885     {
886         $this->elementStart('entry');
887         $this->element('title', null, common_xml_safe_str($entry['title']));
888         $this->element(
889             'content',
890             array('type' => 'html'),
891             common_xml_safe_str($entry['content'])
892         );
893         $this->element('id', null, $entry['id']);
894         $this->element('published', null, $entry['published']);
895         $this->element('updated', null, $entry['updated']);
896         $this->element('link', array('type' => 'text/html',
897                                      'href' => $entry['link'],
898                                      'rel' => 'alternate'));
899         $this->element('link', array('type' => $entry['avatar-type'],
900                                      'href' => $entry['avatar'],
901                                      'rel' => 'image'));
902         $this->elementStart('author');
903
904         $this->element('name', null, $entry['author-name']);
905         $this->element('uri', null, $entry['author-uri']);
906
907         $this->elementEnd('author');
908         $this->elementEnd('entry');
909     }
910
911     function showXmlDirectMessage($dm, $namespaces=false)
912     {
913         $attrs = array();
914         if ($namespaces) {
915             $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
916         }
917         $this->elementStart('direct_message', $attrs);
918         foreach($dm as $element => $value) {
919             switch ($element) {
920             case 'sender':
921             case 'recipient':
922                 $this->showTwitterXmlUser($value, $element);
923                 break;
924             case 'text':
925                 $this->element($element, null, common_xml_safe_str($value));
926                 break;
927             default:
928                 $this->element($element, null, $value);
929                 break;
930             }
931         }
932         $this->elementEnd('direct_message');
933     }
934
935     function directMessageArray($message)
936     {
937         $dmsg = array();
938
939         $from_profile = $message->getFrom();
940         $to_profile = $message->getTo();
941
942         $dmsg['id'] = $message->id;
943         $dmsg['sender_id'] = $message->from_profile;
944         $dmsg['text'] = trim($message->content);
945         $dmsg['recipient_id'] = $message->to_profile;
946         $dmsg['created_at'] = $this->dateTwitter($message->created);
947         $dmsg['sender_screen_name'] = $from_profile->nickname;
948         $dmsg['recipient_screen_name'] = $to_profile->nickname;
949         $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
950         $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
951
952         return $dmsg;
953     }
954
955     function rssDirectMessageArray($message)
956     {
957         $entry = array();
958
959         $from = $message->getFrom();
960
961         $entry['title'] = sprintf('Message from %1$s to %2$s',
962             $from->nickname, $message->getTo()->nickname);
963
964         $entry['content'] = common_xml_safe_str($message->rendered);
965         $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
966         $entry['published'] = common_date_iso8601($message->created);
967
968         $taguribase = TagURI::base();
969
970         $entry['id'] = "tag:$taguribase:$entry[link]";
971         $entry['updated'] = $entry['published'];
972
973         $entry['author-name'] = $from->getBestName();
974         $entry['author-uri'] = $from->homepage;
975
976         $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
977
978         $entry['avatar']      = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
979         $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
980
981         // RSS item specific
982
983         $entry['description'] = $entry['content'];
984         $entry['pubDate'] = common_date_rfc2822($message->created);
985         $entry['guid'] = $entry['link'];
986
987         return $entry;
988     }
989
990     function showSingleXmlDirectMessage($message)
991     {
992         $this->initDocument('xml');
993         $dmsg = $this->directMessageArray($message);
994         $this->showXmlDirectMessage($dmsg, true);
995         $this->endDocument('xml');
996     }
997
998     function showSingleJsonDirectMessage($message)
999     {
1000         $this->initDocument('json');
1001         $dmsg = $this->directMessageArray($message);
1002         $this->showJsonObjects($dmsg);
1003         $this->endDocument('json');
1004     }
1005
1006     function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1007     {
1008         $this->initDocument('atom');
1009
1010         $this->element('title', null, common_xml_safe_str($title));
1011         $this->element('id', null, $id);
1012         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1013
1014         if (!is_null($selfuri)) {
1015             $this->element('link', array('href' => $selfuri,
1016                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1017         }
1018
1019         $this->element('updated', null, common_date_iso8601('now'));
1020         $this->element('subtitle', null, common_xml_safe_str($subtitle));
1021
1022         if (is_array($group)) {
1023             foreach ($group as $g) {
1024                 $this->raw($g->asAtomEntry());
1025             }
1026         } else {
1027             while ($group->fetch()) {
1028                 $this->raw($group->asAtomEntry());
1029             }
1030         }
1031
1032         $this->endDocument('atom');
1033
1034     }
1035
1036     function showJsonTimeline($notice)
1037     {
1038         $this->initDocument('json');
1039
1040         $statuses = array();
1041
1042         if (is_array($notice)) {
1043             $notice = new ArrayWrapper($notice);
1044         }
1045
1046         while ($notice->fetch()) {
1047             try {
1048                 $twitter_status = $this->twitterStatusArray($notice);
1049                 array_push($statuses, $twitter_status);
1050             } catch (Exception $e) {
1051                 common_log(LOG_ERR, $e->getMessage());
1052                 continue;
1053             }
1054         }
1055
1056         $this->showJsonObjects($statuses);
1057
1058         $this->endDocument('json');
1059     }
1060
1061     function showJsonGroups($group)
1062     {
1063         $this->initDocument('json');
1064
1065         $groups = array();
1066
1067         if (is_array($group)) {
1068             foreach ($group as $g) {
1069                 $twitter_group = $this->twitterGroupArray($g);
1070                 array_push($groups, $twitter_group);
1071             }
1072         } else {
1073             while ($group->fetch()) {
1074                 $twitter_group = $this->twitterGroupArray($group);
1075                 array_push($groups, $twitter_group);
1076             }
1077         }
1078
1079         $this->showJsonObjects($groups);
1080
1081         $this->endDocument('json');
1082     }
1083
1084     function showXmlGroups($group)
1085     {
1086
1087         $this->initDocument('xml');
1088         $this->elementStart('groups', array('type' => 'array'));
1089
1090         if (is_array($group)) {
1091             foreach ($group as $g) {
1092                 $twitter_group = $this->twitterGroupArray($g);
1093                 $this->showTwitterXmlGroup($twitter_group);
1094             }
1095         } else {
1096             while ($group->fetch()) {
1097                 $twitter_group = $this->twitterGroupArray($group);
1098                 $this->showTwitterXmlGroup($twitter_group);
1099             }
1100         }
1101
1102         $this->elementEnd('groups');
1103         $this->endDocument('xml');
1104     }
1105
1106     function showTwitterXmlUsers($user)
1107     {
1108         $this->initDocument('xml');
1109         $this->elementStart('users', array('type' => 'array',
1110                                            'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1111
1112         if (is_array($user)) {
1113             foreach ($user as $u) {
1114                 $twitter_user = $this->twitterUserArray($u);
1115                 $this->showTwitterXmlUser($twitter_user);
1116             }
1117         } else {
1118             while ($user->fetch()) {
1119                 $twitter_user = $this->twitterUserArray($user);
1120                 $this->showTwitterXmlUser($twitter_user);
1121             }
1122         }
1123
1124         $this->elementEnd('users');
1125         $this->endDocument('xml');
1126     }
1127
1128     function showJsonUsers($user)
1129     {
1130         $this->initDocument('json');
1131
1132         $users = array();
1133
1134         if (is_array($user)) {
1135             foreach ($user as $u) {
1136                 $twitter_user = $this->twitterUserArray($u);
1137                 array_push($users, $twitter_user);
1138             }
1139         } else {
1140             while ($user->fetch()) {
1141                 $twitter_user = $this->twitterUserArray($user);
1142                 array_push($users, $twitter_user);
1143             }
1144         }
1145
1146         $this->showJsonObjects($users);
1147
1148         $this->endDocument('json');
1149     }
1150
1151     function showSingleJsonGroup($group)
1152     {
1153         $this->initDocument('json');
1154         $twitter_group = $this->twitterGroupArray($group);
1155         $this->showJsonObjects($twitter_group);
1156         $this->endDocument('json');
1157     }
1158
1159     function showSingleXmlGroup($group)
1160     {
1161         $this->initDocument('xml');
1162         $twitter_group = $this->twitterGroupArray($group);
1163         $this->showTwitterXmlGroup($twitter_group);
1164         $this->endDocument('xml');
1165     }
1166
1167     function dateTwitter($dt)
1168     {
1169         $dateStr = date('d F Y H:i:s', strtotime($dt));
1170         $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1171         $d->setTimezone(new DateTimeZone(common_timezone()));
1172         return $d->format('D M d H:i:s O Y');
1173     }
1174
1175     function initDocument($type='xml')
1176     {
1177         switch ($type) {
1178         case 'xml':
1179             header('Content-Type: application/xml; charset=utf-8');
1180             $this->startXML();
1181             break;
1182         case 'json':
1183             header('Content-Type: application/json; charset=utf-8');
1184
1185             // Check for JSONP callback
1186             if (isset($this->callback)) {
1187                 print $this->callback . '(';
1188             }
1189             break;
1190         case 'rss':
1191             header("Content-Type: application/rss+xml; charset=utf-8");
1192             $this->initTwitterRss();
1193             break;
1194         case 'atom':
1195             header('Content-Type: application/atom+xml; charset=utf-8');
1196             $this->initTwitterAtom();
1197             break;
1198         default:
1199             // TRANS: Client error on an API request with an unsupported data format.
1200             $this->clientError(_('Not a supported data format.'));
1201             break;
1202         }
1203
1204         return;
1205     }
1206
1207     function endDocument($type='xml')
1208     {
1209         switch ($type) {
1210         case 'xml':
1211             $this->endXML();
1212             break;
1213         case 'json':
1214             // Check for JSONP callback
1215             if (isset($this->callback)) {
1216                 print ')';
1217             }
1218             break;
1219         case 'rss':
1220             $this->endTwitterRss();
1221             break;
1222         case 'atom':
1223             $this->endTwitterRss();
1224             break;
1225         default:
1226             // TRANS: Client error on an API request with an unsupported data format.
1227             $this->clientError(_('Not a supported data format.'));
1228             break;
1229         }
1230         return;
1231     }
1232
1233     function clientError($msg, $code = 400, $format = 'xml')
1234     {
1235         $action = $this->trimmed('action');
1236
1237         common_debug("User error '$code' on '$action': $msg", __FILE__);
1238
1239         if (!array_key_exists($code, ClientErrorAction::$status)) {
1240             $code = 400;
1241         }
1242
1243         $status_string = ClientErrorAction::$status[$code];
1244
1245         // Do not emit error header for JSONP
1246         if (!isset($this->callback)) {
1247             header('HTTP/1.1 ' . $code . ' ' . $status_string);
1248         }
1249
1250         switch($format) {
1251         case 'xml':
1252             $this->initDocument('xml');
1253             $this->elementStart('hash');
1254             $this->element('error', null, $msg);
1255             $this->element('request', null, $_SERVER['REQUEST_URI']);
1256             $this->elementEnd('hash');
1257             $this->endDocument('xml');
1258             break;
1259         case 'json':
1260             $this->initDocument('json');
1261             $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1262             print(json_encode($error_array));
1263             $this->endDocument('json');
1264             break;
1265         case 'text':
1266             header('Content-Type: text/plain; charset=utf-8');
1267             print $msg;
1268             break;
1269         default:
1270             // If user didn't request a useful format, throw a regular client error
1271             throw new ClientException($msg, $code);
1272         }
1273     }
1274
1275     function serverError($msg, $code = 500, $content_type = 'xml')
1276     {
1277         $action = $this->trimmed('action');
1278
1279         common_debug("Server error '$code' on '$action': $msg", __FILE__);
1280
1281         if (!array_key_exists($code, ServerErrorAction::$status)) {
1282             $code = 400;
1283         }
1284
1285         $status_string = ServerErrorAction::$status[$code];
1286
1287         // Do not emit error header for JSONP
1288         if (!isset($this->callback)) {
1289             header('HTTP/1.1 '.$code.' '.$status_string);
1290         }
1291
1292         if ($content_type == 'xml') {
1293             $this->initDocument('xml');
1294             $this->elementStart('hash');
1295             $this->element('error', null, $msg);
1296             $this->element('request', null, $_SERVER['REQUEST_URI']);
1297             $this->elementEnd('hash');
1298             $this->endDocument('xml');
1299         } else {
1300             $this->initDocument('json');
1301             $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1302             print(json_encode($error_array));
1303             $this->endDocument('json');
1304         }
1305     }
1306
1307     function initTwitterRss()
1308     {
1309         $this->startXML();
1310         $this->elementStart(
1311             'rss',
1312             array(
1313                 'version'      => '2.0',
1314                 'xmlns:atom'   => 'http://www.w3.org/2005/Atom',
1315                 'xmlns:georss' => 'http://www.georss.org/georss'
1316             )
1317         );
1318         $this->elementStart('channel');
1319         Event::handle('StartApiRss', array($this));
1320     }
1321
1322     function endTwitterRss()
1323     {
1324         $this->elementEnd('channel');
1325         $this->elementEnd('rss');
1326         $this->endXML();
1327     }
1328
1329     function initTwitterAtom()
1330     {
1331         $this->startXML();
1332         // FIXME: don't hardcode the language here!
1333         $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1334                                           'xml:lang' => 'en-US',
1335                                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1336     }
1337
1338     function endTwitterAtom()
1339     {
1340         $this->elementEnd('feed');
1341         $this->endXML();
1342     }
1343
1344     function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1345     {
1346         $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1347         switch ($content_type) {
1348         case 'xml':
1349             $this->showTwitterXmlUser($profile_array);
1350             break;
1351         case 'json':
1352             $this->showJsonObjects($profile_array);
1353             break;
1354         default:
1355             // TRANS: Client error on an API request with an unsupported data format.
1356             $this->clientError(_('Not a supported data format.'));
1357             return;
1358         }
1359         return;
1360     }
1361
1362     private static function is_decimal($str)
1363     {
1364         return preg_match('/^[0-9]+$/', $str);
1365     }
1366
1367     function getTargetUser($id)
1368     {
1369         if (empty($id)) {
1370             // Twitter supports these other ways of passing the user ID
1371             if (self::is_decimal($this->arg('id'))) {
1372                 return User::staticGet($this->arg('id'));
1373             } else if ($this->arg('id')) {
1374                 $nickname = common_canonical_nickname($this->arg('id'));
1375                 return User::staticGet('nickname', $nickname);
1376             } else if ($this->arg('user_id')) {
1377                 // This is to ensure that a non-numeric user_id still
1378                 // overrides screen_name even if it doesn't get used
1379                 if (self::is_decimal($this->arg('user_id'))) {
1380                     return User::staticGet('id', $this->arg('user_id'));
1381                 }
1382             } else if ($this->arg('screen_name')) {
1383                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1384                 return User::staticGet('nickname', $nickname);
1385             } else {
1386                 // Fall back to trying the currently authenticated user
1387                 return $this->auth_user;
1388             }
1389
1390         } else if (self::is_decimal($id)) {
1391             return User::staticGet($id);
1392         } else {
1393             $nickname = common_canonical_nickname($id);
1394             return User::staticGet('nickname', $nickname);
1395         }
1396     }
1397
1398     function getTargetProfile($id)
1399     {
1400         if (empty($id)) {
1401
1402             // Twitter supports these other ways of passing the user ID
1403             if (self::is_decimal($this->arg('id'))) {
1404                 return Profile::staticGet($this->arg('id'));
1405             } else if ($this->arg('id')) {
1406                 // Screen names currently can only uniquely identify a local user.
1407                 $nickname = common_canonical_nickname($this->arg('id'));
1408                 $user = User::staticGet('nickname', $nickname);
1409                 return $user ? $user->getProfile() : null;
1410             } else if ($this->arg('user_id')) {
1411                 // This is to ensure that a non-numeric user_id still
1412                 // overrides screen_name even if it doesn't get used
1413                 if (self::is_decimal($this->arg('user_id'))) {
1414                     return Profile::staticGet('id', $this->arg('user_id'));
1415                 }
1416             } else if ($this->arg('screen_name')) {
1417                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1418                 $user = User::staticGet('nickname', $nickname);
1419                 return $user ? $user->getProfile() : null;
1420             }
1421         } else if (self::is_decimal($id)) {
1422             return Profile::staticGet($id);
1423         } else {
1424             $nickname = common_canonical_nickname($id);
1425             $user = User::staticGet('nickname', $nickname);
1426             return $user ? $user->getProfile() : null;
1427         }
1428     }
1429
1430     function getTargetGroup($id)
1431     {
1432         if (empty($id)) {
1433             if (self::is_decimal($this->arg('id'))) {
1434                 return User_group::staticGet($this->arg('id'));
1435             } else if ($this->arg('id')) {
1436                 $nickname = common_canonical_nickname($this->arg('id'));
1437                 $local = Local_group::staticGet('nickname', $nickname);
1438                 if (empty($local)) {
1439                     return null;
1440                 } else {
1441                     return User_group::staticGet('id', $local->id);
1442                 }
1443             } else if ($this->arg('group_id')) {
1444                 // This is to ensure that a non-numeric user_id still
1445                 // overrides screen_name even if it doesn't get used
1446                 if (self::is_decimal($this->arg('group_id'))) {
1447                     return User_group::staticGet('id', $this->arg('group_id'));
1448                 }
1449             } else if ($this->arg('group_name')) {
1450                 $nickname = common_canonical_nickname($this->arg('group_name'));
1451                 $local = Local_group::staticGet('nickname', $nickname);
1452                 if (empty($local)) {
1453                     return null;
1454                 } else {
1455                     return User_group::staticGet('id', $local->group_id);
1456                 }
1457             }
1458
1459         } else if (self::is_decimal($id)) {
1460             return User_group::staticGet($id);
1461         } else {
1462             $nickname = common_canonical_nickname($id);
1463             $local = Local_group::staticGet('nickname', $nickname);
1464             if (empty($local)) {
1465                 return null;
1466             } else {
1467                 return User_group::staticGet('id', $local->group_id);
1468             }
1469         }
1470     }
1471
1472     /**
1473      * Returns query argument or default value if not found. Certain
1474      * parameters used throughout the API are lightly scrubbed and
1475      * bounds checked.  This overrides Action::arg().
1476      *
1477      * @param string $key requested argument
1478      * @param string $def default value to return if $key is not provided
1479      *
1480      * @return var $var
1481      */
1482     function arg($key, $def=null)
1483     {
1484         // XXX: Do even more input validation/scrubbing?
1485
1486         if (array_key_exists($key, $this->args)) {
1487             switch($key) {
1488             case 'page':
1489                 $page = (int)$this->args['page'];
1490                 return ($page < 1) ? 1 : $page;
1491             case 'count':
1492                 $count = (int)$this->args['count'];
1493                 if ($count < 1) {
1494                     return 20;
1495                 } elseif ($count > 200) {
1496                     return 200;
1497                 } else {
1498                     return $count;
1499                 }
1500             case 'since_id':
1501                 $since_id = (int)$this->args['since_id'];
1502                 return ($since_id < 1) ? 0 : $since_id;
1503             case 'max_id':
1504                 $max_id = (int)$this->args['max_id'];
1505                 return ($max_id < 1) ? 0 : $max_id;
1506             default:
1507                 return parent::arg($key, $def);
1508             }
1509         } else {
1510             return $def;
1511         }
1512     }
1513
1514     /**
1515      * Calculate the complete URI that called up this action.  Used for
1516      * Atom rel="self" links.  Warning: this is funky.
1517      *
1518      * @return string URL    a URL suitable for rel="self" Atom links
1519      */
1520     function getSelfUri()
1521     {
1522         $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1523
1524         $id = $this->arg('id');
1525         $aargs = array('format' => $this->format);
1526         if (!empty($id)) {
1527             $aargs['id'] = $id;
1528         }
1529
1530         $tag = $this->arg('tag');
1531         if (!empty($tag)) {
1532             $aargs['tag'] = $tag;
1533         }
1534
1535         parse_str($_SERVER['QUERY_STRING'], $params);
1536         $pstring = '';
1537         if (!empty($params)) {
1538             unset($params['p']);
1539             $pstring = http_build_query($params);
1540         }
1541
1542         $uri = common_local_url($action, $aargs);
1543
1544         if (!empty($pstring)) {
1545             $uri .= '?' . $pstring;
1546         }
1547
1548         return $uri;
1549     }
1550 }