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