]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/apiaction.php
include protected flag for users in JSON or XML
[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         $user = $profile->getUser();
206
207         $twitter_user['id'] = intval($profile->id);
208         $twitter_user['name'] = $profile->getBestName();
209         $twitter_user['screen_name'] = $profile->nickname;
210         $twitter_user['location'] = ($profile->location) ? $profile->location : null;
211         $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
212
213         $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
214         $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
215             Avatar::defaultImage(AVATAR_STREAM_SIZE);
216
217         $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
218         $twitter_user['protected'] = ($user->private_stream) ? true : false;
219         $twitter_user['followers_count'] = $profile->subscriberCount();
220
221         $design = null;
222
223         // Note: some profiles don't have an associated user
224
225         $defaultDesign = Design::siteDesign();
226
227         if (!empty($user)) {
228             $design = $user->getDesign();
229         }
230
231         if (empty($design)) {
232             $design = $defaultDesign;
233         }
234
235         $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
236         $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
237         $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
238         $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
239         $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
240         $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
241         $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
242         $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
243         $twitter_user['profile_sidebar_border_color'] = '';
244
245         $twitter_user['friends_count'] = $profile->subscriptionCount();
246
247         $twitter_user['created_at'] = $this->dateTwitter($profile->created);
248
249         $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
250
251         $timezone = 'UTC';
252
253         if (!empty($user) && $user->timezone) {
254             $timezone = $user->timezone;
255         }
256
257         $t = new DateTime;
258         $t->setTimezone(new DateTimeZone($timezone));
259
260         $twitter_user['utc_offset'] = $t->format('Z');
261         $twitter_user['time_zone'] = $timezone;
262
263         $twitter_user['profile_background_image_url']
264             = empty($design->backgroundimage)
265             ? '' : ($design->disposition & BACKGROUND_ON)
266             ? Design::url($design->backgroundimage) : '';
267
268         $twitter_user['profile_background_tile']
269             = (bool)($design->disposition & BACKGROUND_TILE);
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'] = intval($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'] = intval($source->id);
567
568         return $details;
569     }
570
571     function showTwitterXmlRelationship($relationship)
572     {
573         $this->elementStart('relationship');
574
575         foreach($relationship as $element => $value) {
576             if ($element == 'source' || $element == 'target') {
577                 $this->elementStart($element);
578                 $this->showXmlRelationshipDetails($value);
579                 $this->elementEnd($element);
580             }
581         }
582
583         $this->elementEnd('relationship');
584     }
585
586     function showXmlRelationshipDetails($details)
587     {
588         foreach($details as $element => $value) {
589             $this->element($element, null, $value);
590         }
591     }
592
593     function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
594     {
595         $attrs = array();
596         if ($namespaces) {
597             $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
598         }
599         $this->elementStart($tag, $attrs);
600         foreach($twitter_status as $element => $value) {
601             switch ($element) {
602             case 'user':
603                 $this->showTwitterXmlUser($twitter_status['user']);
604                 break;
605             case 'text':
606                 $this->element($element, null, common_xml_safe_str($value));
607                 break;
608             case 'attachments':
609                 $this->showXmlAttachments($twitter_status['attachments']);
610                 break;
611             case 'geo':
612                 $this->showGeoXML($value);
613                 break;
614             case 'retweeted_status':
615                 $this->showTwitterXmlStatus($value, 'retweeted_status');
616                 break;
617             default:
618                 if (strncmp($element, 'statusnet_', 10) == 0) {
619                     $this->element('statusnet:'.substr($element, 10), null, $value);
620                 } else {
621                     $this->element($element, null, $value);
622                 }
623             }
624         }
625         $this->elementEnd($tag);
626     }
627
628     function showTwitterXmlGroup($twitter_group)
629     {
630         $this->elementStart('group');
631         foreach($twitter_group as $element => $value) {
632             $this->element($element, null, $value);
633         }
634         $this->elementEnd('group');
635     }
636
637     function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
638     {
639         $attrs = array();
640         if ($namespaces) {
641             $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
642         }
643         $this->elementStart($role, $attrs);
644         foreach($twitter_user as $element => $value) {
645             if ($element == 'status') {
646                 $this->showTwitterXmlStatus($twitter_user['status']);
647             } else if (strncmp($element, 'statusnet_', 10) == 0) {
648                 $this->element('statusnet:'.substr($element, 10), null, $value);
649             } else {
650                 $this->element($element, null, $value);
651             }
652         }
653         $this->elementEnd($role);
654     }
655
656     function showXmlAttachments($attachments) {
657         if (!empty($attachments)) {
658             $this->elementStart('attachments', array('type' => 'array'));
659             foreach ($attachments as $attachment) {
660                 $attrs = array();
661                 $attrs['url'] = $attachment['url'];
662                 $attrs['mimetype'] = $attachment['mimetype'];
663                 $attrs['size'] = $attachment['size'];
664                 $this->element('enclosure', $attrs, '');
665             }
666             $this->elementEnd('attachments');
667         }
668     }
669
670     function showGeoXML($geo)
671     {
672         if (empty($geo)) {
673             // empty geo element
674             $this->element('geo');
675         } else {
676             $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
677             $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
678             $this->elementEnd('geo');
679         }
680     }
681
682     function showGeoRSS($geo)
683     {
684         if (!empty($geo)) {
685             $this->element(
686                 'georss:point',
687                 null,
688                 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
689             );
690         }
691     }
692
693     function showTwitterRssItem($entry)
694     {
695         $this->elementStart('item');
696         $this->element('title', null, $entry['title']);
697         $this->element('description', null, $entry['description']);
698         $this->element('pubDate', null, $entry['pubDate']);
699         $this->element('guid', null, $entry['guid']);
700         $this->element('link', null, $entry['link']);
701
702         // RSS only supports 1 enclosure per item
703         if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
704             $enclosure = $entry['enclosures'][0];
705             $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
706         }
707
708         if(array_key_exists('tags', $entry)){
709             foreach($entry['tags'] as $tag){
710                 $this->element('category', null,$tag);
711             }
712         }
713
714         $this->showGeoRSS($entry['geo']);
715         $this->elementEnd('item');
716     }
717
718     function showJsonObjects($objects)
719     {
720         print(json_encode($objects));
721     }
722
723     function showSingleXmlStatus($notice)
724     {
725         $this->initDocument('xml');
726         $twitter_status = $this->twitterStatusArray($notice);
727         $this->showTwitterXmlStatus($twitter_status, 'status', true);
728         $this->endDocument('xml');
729     }
730
731     function showSingleAtomStatus($notice)
732     {
733         header('Content-Type: application/atom+xml; charset=utf-8');
734         print $notice->asAtomEntry(true, true, true, $this->auth_user);
735     }
736
737     function show_single_json_status($notice)
738     {
739         $this->initDocument('json');
740         $status = $this->twitterStatusArray($notice);
741         $this->showJsonObjects($status);
742         $this->endDocument('json');
743     }
744
745     function showXmlTimeline($notice)
746     {
747         $this->initDocument('xml');
748         $this->elementStart('statuses', array('type' => 'array',
749                                               'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
750
751         if (is_array($notice)) {
752             $notice = new ArrayWrapper($notice);
753         }
754
755         while ($notice->fetch()) {
756             try {
757                 $twitter_status = $this->twitterStatusArray($notice);
758                 $this->showTwitterXmlStatus($twitter_status);
759             } catch (Exception $e) {
760                 common_log(LOG_ERR, $e->getMessage());
761                 continue;
762             }
763         }
764
765         $this->elementEnd('statuses');
766         $this->endDocument('xml');
767     }
768
769     function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
770     {
771         $this->initDocument('rss');
772
773         $this->element('title', null, $title);
774         $this->element('link', null, $link);
775
776         if (!is_null($self)) {
777             $this->element(
778                 'atom:link',
779                 array(
780                     'type' => 'application/rss+xml',
781                     'href' => $self,
782                     'rel'  => 'self'
783                 )
784            );
785         }
786
787         if (!is_null($suplink)) {
788             // For FriendFeed's SUP protocol
789             $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
790                                          'rel' => 'http://api.friendfeed.com/2008/03#sup',
791                                          'href' => $suplink,
792                                          'type' => 'application/json'));
793         }
794
795         if (!is_null($logo)) {
796             $this->elementStart('image');
797             $this->element('link', null, $link);
798             $this->element('title', null, $title);
799             $this->element('url', null, $logo);
800             $this->elementEnd('image');
801         }
802
803         $this->element('description', null, $subtitle);
804         $this->element('language', null, 'en-us');
805         $this->element('ttl', null, '40');
806
807         if (is_array($notice)) {
808             $notice = new ArrayWrapper($notice);
809         }
810
811         while ($notice->fetch()) {
812             try {
813                 $entry = $this->twitterRssEntryArray($notice);
814                 $this->showTwitterRssItem($entry);
815             } catch (Exception $e) {
816                 common_log(LOG_ERR, $e->getMessage());
817                 // continue on exceptions
818             }
819         }
820
821         $this->endTwitterRss();
822     }
823
824     function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
825     {
826         $this->initDocument('atom');
827
828         $this->element('title', null, $title);
829         $this->element('id', null, $id);
830         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
831
832         if (!is_null($logo)) {
833             $this->element('logo',null,$logo);
834         }
835
836         if (!is_null($suplink)) {
837             // For FriendFeed's SUP protocol
838             $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
839                                          'href' => $suplink,
840                                          'type' => 'application/json'));
841         }
842
843         if (!is_null($selfuri)) {
844             $this->element('link', array('href' => $selfuri,
845                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
846         }
847
848         $this->element('updated', null, common_date_iso8601('now'));
849         $this->element('subtitle', null, $subtitle);
850
851         if (is_array($notice)) {
852             $notice = new ArrayWrapper($notice);
853         }
854
855         while ($notice->fetch()) {
856             try {
857                 $this->raw($notice->asAtomEntry());
858             } catch (Exception $e) {
859                 common_log(LOG_ERR, $e->getMessage());
860                 continue;
861             }
862         }
863
864         $this->endDocument('atom');
865     }
866
867     function showRssGroups($group, $title, $link, $subtitle)
868     {
869         $this->initDocument('rss');
870
871         $this->element('title', null, $title);
872         $this->element('link', null, $link);
873         $this->element('description', null, $subtitle);
874         $this->element('language', null, 'en-us');
875         $this->element('ttl', null, '40');
876
877         if (is_array($group)) {
878             foreach ($group as $g) {
879                 $twitter_group = $this->twitterRssGroupArray($g);
880                 $this->showTwitterRssItem($twitter_group);
881             }
882         } else {
883             while ($group->fetch()) {
884                 $twitter_group = $this->twitterRssGroupArray($group);
885                 $this->showTwitterRssItem($twitter_group);
886             }
887         }
888
889         $this->endTwitterRss();
890     }
891
892     function showTwitterAtomEntry($entry)
893     {
894         $this->elementStart('entry');
895         $this->element('title', null, common_xml_safe_str($entry['title']));
896         $this->element(
897             'content',
898             array('type' => 'html'),
899             common_xml_safe_str($entry['content'])
900         );
901         $this->element('id', null, $entry['id']);
902         $this->element('published', null, $entry['published']);
903         $this->element('updated', null, $entry['updated']);
904         $this->element('link', array('type' => 'text/html',
905                                      'href' => $entry['link'],
906                                      'rel' => 'alternate'));
907         $this->element('link', array('type' => $entry['avatar-type'],
908                                      'href' => $entry['avatar'],
909                                      'rel' => 'image'));
910         $this->elementStart('author');
911
912         $this->element('name', null, $entry['author-name']);
913         $this->element('uri', null, $entry['author-uri']);
914
915         $this->elementEnd('author');
916         $this->elementEnd('entry');
917     }
918
919     function showXmlDirectMessage($dm, $namespaces=false)
920     {
921         $attrs = array();
922         if ($namespaces) {
923             $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
924         }
925         $this->elementStart('direct_message', $attrs);
926         foreach($dm as $element => $value) {
927             switch ($element) {
928             case 'sender':
929             case 'recipient':
930                 $this->showTwitterXmlUser($value, $element);
931                 break;
932             case 'text':
933                 $this->element($element, null, common_xml_safe_str($value));
934                 break;
935             default:
936                 $this->element($element, null, $value);
937                 break;
938             }
939         }
940         $this->elementEnd('direct_message');
941     }
942
943     function directMessageArray($message)
944     {
945         $dmsg = array();
946
947         $from_profile = $message->getFrom();
948         $to_profile = $message->getTo();
949
950         $dmsg['id'] = intval($message->id);
951         $dmsg['sender_id'] = intval($from_profile);
952         $dmsg['text'] = trim($message->content);
953         $dmsg['recipient_id'] = intval($to_profile);
954         $dmsg['created_at'] = $this->dateTwitter($message->created);
955         $dmsg['sender_screen_name'] = $from_profile->nickname;
956         $dmsg['recipient_screen_name'] = $to_profile->nickname;
957         $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
958         $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
959
960         return $dmsg;
961     }
962
963     function rssDirectMessageArray($message)
964     {
965         $entry = array();
966
967         $from = $message->getFrom();
968
969         $entry['title'] = sprintf('Message from %1$s to %2$s',
970             $from->nickname, $message->getTo()->nickname);
971
972         $entry['content'] = common_xml_safe_str($message->rendered);
973         $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
974         $entry['published'] = common_date_iso8601($message->created);
975
976         $taguribase = TagURI::base();
977
978         $entry['id'] = "tag:$taguribase:$entry[link]";
979         $entry['updated'] = $entry['published'];
980
981         $entry['author-name'] = $from->getBestName();
982         $entry['author-uri'] = $from->homepage;
983
984         $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
985
986         $entry['avatar']      = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
987         $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
988
989         // RSS item specific
990
991         $entry['description'] = $entry['content'];
992         $entry['pubDate'] = common_date_rfc2822($message->created);
993         $entry['guid'] = $entry['link'];
994
995         return $entry;
996     }
997
998     function showSingleXmlDirectMessage($message)
999     {
1000         $this->initDocument('xml');
1001         $dmsg = $this->directMessageArray($message);
1002         $this->showXmlDirectMessage($dmsg, true);
1003         $this->endDocument('xml');
1004     }
1005
1006     function showSingleJsonDirectMessage($message)
1007     {
1008         $this->initDocument('json');
1009         $dmsg = $this->directMessageArray($message);
1010         $this->showJsonObjects($dmsg);
1011         $this->endDocument('json');
1012     }
1013
1014     function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1015     {
1016         $this->initDocument('atom');
1017
1018         $this->element('title', null, common_xml_safe_str($title));
1019         $this->element('id', null, $id);
1020         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1021
1022         if (!is_null($selfuri)) {
1023             $this->element('link', array('href' => $selfuri,
1024                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1025         }
1026
1027         $this->element('updated', null, common_date_iso8601('now'));
1028         $this->element('subtitle', null, common_xml_safe_str($subtitle));
1029
1030         if (is_array($group)) {
1031             foreach ($group as $g) {
1032                 $this->raw($g->asAtomEntry());
1033             }
1034         } else {
1035             while ($group->fetch()) {
1036                 $this->raw($group->asAtomEntry());
1037             }
1038         }
1039
1040         $this->endDocument('atom');
1041
1042     }
1043
1044     function showJsonTimeline($notice)
1045     {
1046         $this->initDocument('json');
1047
1048         $statuses = array();
1049
1050         if (is_array($notice)) {
1051             $notice = new ArrayWrapper($notice);
1052         }
1053
1054         while ($notice->fetch()) {
1055             try {
1056                 $twitter_status = $this->twitterStatusArray($notice);
1057                 array_push($statuses, $twitter_status);
1058             } catch (Exception $e) {
1059                 common_log(LOG_ERR, $e->getMessage());
1060                 continue;
1061             }
1062         }
1063
1064         $this->showJsonObjects($statuses);
1065
1066         $this->endDocument('json');
1067     }
1068
1069     function showJsonGroups($group)
1070     {
1071         $this->initDocument('json');
1072
1073         $groups = array();
1074
1075         if (is_array($group)) {
1076             foreach ($group as $g) {
1077                 $twitter_group = $this->twitterGroupArray($g);
1078                 array_push($groups, $twitter_group);
1079             }
1080         } else {
1081             while ($group->fetch()) {
1082                 $twitter_group = $this->twitterGroupArray($group);
1083                 array_push($groups, $twitter_group);
1084             }
1085         }
1086
1087         $this->showJsonObjects($groups);
1088
1089         $this->endDocument('json');
1090     }
1091
1092     function showXmlGroups($group)
1093     {
1094
1095         $this->initDocument('xml');
1096         $this->elementStart('groups', array('type' => 'array'));
1097
1098         if (is_array($group)) {
1099             foreach ($group as $g) {
1100                 $twitter_group = $this->twitterGroupArray($g);
1101                 $this->showTwitterXmlGroup($twitter_group);
1102             }
1103         } else {
1104             while ($group->fetch()) {
1105                 $twitter_group = $this->twitterGroupArray($group);
1106                 $this->showTwitterXmlGroup($twitter_group);
1107             }
1108         }
1109
1110         $this->elementEnd('groups');
1111         $this->endDocument('xml');
1112     }
1113
1114     function showTwitterXmlUsers($user)
1115     {
1116         $this->initDocument('xml');
1117         $this->elementStart('users', array('type' => 'array',
1118                                            'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1119
1120         if (is_array($user)) {
1121             foreach ($user as $u) {
1122                 $twitter_user = $this->twitterUserArray($u);
1123                 $this->showTwitterXmlUser($twitter_user);
1124             }
1125         } else {
1126             while ($user->fetch()) {
1127                 $twitter_user = $this->twitterUserArray($user);
1128                 $this->showTwitterXmlUser($twitter_user);
1129             }
1130         }
1131
1132         $this->elementEnd('users');
1133         $this->endDocument('xml');
1134     }
1135
1136     function showJsonUsers($user)
1137     {
1138         $this->initDocument('json');
1139
1140         $users = array();
1141
1142         if (is_array($user)) {
1143             foreach ($user as $u) {
1144                 $twitter_user = $this->twitterUserArray($u);
1145                 array_push($users, $twitter_user);
1146             }
1147         } else {
1148             while ($user->fetch()) {
1149                 $twitter_user = $this->twitterUserArray($user);
1150                 array_push($users, $twitter_user);
1151             }
1152         }
1153
1154         $this->showJsonObjects($users);
1155
1156         $this->endDocument('json');
1157     }
1158
1159     function showSingleJsonGroup($group)
1160     {
1161         $this->initDocument('json');
1162         $twitter_group = $this->twitterGroupArray($group);
1163         $this->showJsonObjects($twitter_group);
1164         $this->endDocument('json');
1165     }
1166
1167     function showSingleXmlGroup($group)
1168     {
1169         $this->initDocument('xml');
1170         $twitter_group = $this->twitterGroupArray($group);
1171         $this->showTwitterXmlGroup($twitter_group);
1172         $this->endDocument('xml');
1173     }
1174
1175     function dateTwitter($dt)
1176     {
1177         $dateStr = date('d F Y H:i:s', strtotime($dt));
1178         $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1179         $d->setTimezone(new DateTimeZone(common_timezone()));
1180         return $d->format('D M d H:i:s O Y');
1181     }
1182
1183     function initDocument($type='xml')
1184     {
1185         switch ($type) {
1186         case 'xml':
1187             header('Content-Type: application/xml; charset=utf-8');
1188             $this->startXML();
1189             break;
1190         case 'json':
1191             header('Content-Type: application/json; charset=utf-8');
1192
1193             // Check for JSONP callback
1194             if (isset($this->callback)) {
1195                 print $this->callback . '(';
1196             }
1197             break;
1198         case 'rss':
1199             header("Content-Type: application/rss+xml; charset=utf-8");
1200             $this->initTwitterRss();
1201             break;
1202         case 'atom':
1203             header('Content-Type: application/atom+xml; charset=utf-8');
1204             $this->initTwitterAtom();
1205             break;
1206         default:
1207             // TRANS: Client error on an API request with an unsupported data format.
1208             $this->clientError(_('Not a supported data format.'));
1209             break;
1210         }
1211
1212         return;
1213     }
1214
1215     function endDocument($type='xml')
1216     {
1217         switch ($type) {
1218         case 'xml':
1219             $this->endXML();
1220             break;
1221         case 'json':
1222             // Check for JSONP callback
1223             if (isset($this->callback)) {
1224                 print ')';
1225             }
1226             break;
1227         case 'rss':
1228             $this->endTwitterRss();
1229             break;
1230         case 'atom':
1231             $this->endTwitterRss();
1232             break;
1233         default:
1234             // TRANS: Client error on an API request with an unsupported data format.
1235             $this->clientError(_('Not a supported data format.'));
1236             break;
1237         }
1238         return;
1239     }
1240
1241     function clientError($msg, $code = 400, $format = null)
1242     {
1243         $action = $this->trimmed('action');
1244         if ($format === null) {
1245             $format = $this->format;
1246         }
1247
1248         common_debug("User error '$code' on '$action': $msg", __FILE__);
1249
1250         if (!array_key_exists($code, ClientErrorAction::$status)) {
1251             $code = 400;
1252         }
1253
1254         $status_string = ClientErrorAction::$status[$code];
1255
1256         // Do not emit error header for JSONP
1257         if (!isset($this->callback)) {
1258             header('HTTP/1.1 ' . $code . ' ' . $status_string);
1259         }
1260
1261         switch($format) {
1262         case 'xml':
1263             $this->initDocument('xml');
1264             $this->elementStart('hash');
1265             $this->element('error', null, $msg);
1266             $this->element('request', null, $_SERVER['REQUEST_URI']);
1267             $this->elementEnd('hash');
1268             $this->endDocument('xml');
1269             break;
1270         case 'json':
1271             $this->initDocument('json');
1272             $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1273             print(json_encode($error_array));
1274             $this->endDocument('json');
1275             break;
1276         case 'text':
1277             header('Content-Type: text/plain; charset=utf-8');
1278             print $msg;
1279             break;
1280         default:
1281             // If user didn't request a useful format, throw a regular client error
1282             throw new ClientException($msg, $code);
1283         }
1284     }
1285
1286     function serverError($msg, $code = 500, $content_type = null)
1287     {
1288         $action = $this->trimmed('action');
1289         if ($content_type === null) {
1290             $content_type = $this->format;
1291         }
1292
1293         common_debug("Server error '$code' on '$action': $msg", __FILE__);
1294
1295         if (!array_key_exists($code, ServerErrorAction::$status)) {
1296             $code = 400;
1297         }
1298
1299         $status_string = ServerErrorAction::$status[$code];
1300
1301         // Do not emit error header for JSONP
1302         if (!isset($this->callback)) {
1303             header('HTTP/1.1 '.$code.' '.$status_string);
1304         }
1305
1306         if ($content_type == 'xml') {
1307             $this->initDocument('xml');
1308             $this->elementStart('hash');
1309             $this->element('error', null, $msg);
1310             $this->element('request', null, $_SERVER['REQUEST_URI']);
1311             $this->elementEnd('hash');
1312             $this->endDocument('xml');
1313         } else {
1314             $this->initDocument('json');
1315             $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1316             print(json_encode($error_array));
1317             $this->endDocument('json');
1318         }
1319     }
1320
1321     function initTwitterRss()
1322     {
1323         $this->startXML();
1324         $this->elementStart(
1325             'rss',
1326             array(
1327                 'version'      => '2.0',
1328                 'xmlns:atom'   => 'http://www.w3.org/2005/Atom',
1329                 'xmlns:georss' => 'http://www.georss.org/georss'
1330             )
1331         );
1332         $this->elementStart('channel');
1333         Event::handle('StartApiRss', array($this));
1334     }
1335
1336     function endTwitterRss()
1337     {
1338         $this->elementEnd('channel');
1339         $this->elementEnd('rss');
1340         $this->endXML();
1341     }
1342
1343     function initTwitterAtom()
1344     {
1345         $this->startXML();
1346         // FIXME: don't hardcode the language here!
1347         $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1348                                           'xml:lang' => 'en-US',
1349                                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1350     }
1351
1352     function endTwitterAtom()
1353     {
1354         $this->elementEnd('feed');
1355         $this->endXML();
1356     }
1357
1358     function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1359     {
1360         $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1361         switch ($content_type) {
1362         case 'xml':
1363             $this->showTwitterXmlUser($profile_array);
1364             break;
1365         case 'json':
1366             $this->showJsonObjects($profile_array);
1367             break;
1368         default:
1369             // TRANS: Client error on an API request with an unsupported data format.
1370             $this->clientError(_('Not a supported data format.'));
1371             return;
1372         }
1373         return;
1374     }
1375
1376     private static function is_decimal($str)
1377     {
1378         return preg_match('/^[0-9]+$/', $str);
1379     }
1380
1381     function getTargetUser($id)
1382     {
1383         if (empty($id)) {
1384             // Twitter supports these other ways of passing the user ID
1385             if (self::is_decimal($this->arg('id'))) {
1386                 return User::staticGet($this->arg('id'));
1387             } else if ($this->arg('id')) {
1388                 $nickname = common_canonical_nickname($this->arg('id'));
1389                 return User::staticGet('nickname', $nickname);
1390             } else if ($this->arg('user_id')) {
1391                 // This is to ensure that a non-numeric user_id still
1392                 // overrides screen_name even if it doesn't get used
1393                 if (self::is_decimal($this->arg('user_id'))) {
1394                     return User::staticGet('id', $this->arg('user_id'));
1395                 }
1396             } else if ($this->arg('screen_name')) {
1397                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1398                 return User::staticGet('nickname', $nickname);
1399             } else {
1400                 // Fall back to trying the currently authenticated user
1401                 return $this->auth_user;
1402             }
1403
1404         } else if (self::is_decimal($id)) {
1405             return User::staticGet($id);
1406         } else {
1407             $nickname = common_canonical_nickname($id);
1408             return User::staticGet('nickname', $nickname);
1409         }
1410     }
1411
1412     function getTargetProfile($id)
1413     {
1414         if (empty($id)) {
1415
1416             // Twitter supports these other ways of passing the user ID
1417             if (self::is_decimal($this->arg('id'))) {
1418                 return Profile::staticGet($this->arg('id'));
1419             } else if ($this->arg('id')) {
1420                 // Screen names currently can only uniquely identify a local user.
1421                 $nickname = common_canonical_nickname($this->arg('id'));
1422                 $user = User::staticGet('nickname', $nickname);
1423                 return $user ? $user->getProfile() : null;
1424             } else if ($this->arg('user_id')) {
1425                 // This is to ensure that a non-numeric user_id still
1426                 // overrides screen_name even if it doesn't get used
1427                 if (self::is_decimal($this->arg('user_id'))) {
1428                     return Profile::staticGet('id', $this->arg('user_id'));
1429                 }
1430             } else if ($this->arg('screen_name')) {
1431                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1432                 $user = User::staticGet('nickname', $nickname);
1433                 return $user ? $user->getProfile() : null;
1434             }
1435         } else if (self::is_decimal($id)) {
1436             return Profile::staticGet($id);
1437         } else {
1438             $nickname = common_canonical_nickname($id);
1439             $user = User::staticGet('nickname', $nickname);
1440             return $user ? $user->getProfile() : null;
1441         }
1442     }
1443
1444     function getTargetGroup($id)
1445     {
1446         if (empty($id)) {
1447             if (self::is_decimal($this->arg('id'))) {
1448                 return User_group::staticGet('id', $this->arg('id'));
1449             } else if ($this->arg('id')) {
1450                 return User_group::getForNickname($this->arg('id'));
1451             } else if ($this->arg('group_id')) {
1452                 // This is to ensure that a non-numeric group_id still
1453                 // overrides group_name even if it doesn't get used
1454                 if (self::is_decimal($this->arg('group_id'))) {
1455                     return User_group::staticGet('id', $this->arg('group_id'));
1456                 }
1457             } else if ($this->arg('group_name')) {
1458                 return User_group::getForNickname($this->arg('group_name'));
1459             }
1460
1461         } else if (self::is_decimal($id)) {
1462             return User_group::staticGet('id', $id);
1463         } else {
1464             return User_group::getForNickname($id);
1465         }
1466     }
1467
1468     /**
1469      * Returns query argument or default value if not found. Certain
1470      * parameters used throughout the API are lightly scrubbed and
1471      * bounds checked.  This overrides Action::arg().
1472      *
1473      * @param string $key requested argument
1474      * @param string $def default value to return if $key is not provided
1475      *
1476      * @return var $var
1477      */
1478     function arg($key, $def=null)
1479     {
1480         // XXX: Do even more input validation/scrubbing?
1481
1482         if (array_key_exists($key, $this->args)) {
1483             switch($key) {
1484             case 'page':
1485                 $page = (int)$this->args['page'];
1486                 return ($page < 1) ? 1 : $page;
1487             case 'count':
1488                 $count = (int)$this->args['count'];
1489                 if ($count < 1) {
1490                     return 20;
1491                 } elseif ($count > 200) {
1492                     return 200;
1493                 } else {
1494                     return $count;
1495                 }
1496             case 'since_id':
1497                 $since_id = (int)$this->args['since_id'];
1498                 return ($since_id < 1) ? 0 : $since_id;
1499             case 'max_id':
1500                 $max_id = (int)$this->args['max_id'];
1501                 return ($max_id < 1) ? 0 : $max_id;
1502             default:
1503                 return parent::arg($key, $def);
1504             }
1505         } else {
1506             return $def;
1507         }
1508     }
1509
1510     /**
1511      * Calculate the complete URI that called up this action.  Used for
1512      * Atom rel="self" links.  Warning: this is funky.
1513      *
1514      * @return string URL    a URL suitable for rel="self" Atom links
1515      */
1516     function getSelfUri()
1517     {
1518         $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1519
1520         $id = $this->arg('id');
1521         $aargs = array('format' => $this->format);
1522         if (!empty($id)) {
1523             $aargs['id'] = $id;
1524         }
1525
1526         $tag = $this->arg('tag');
1527         if (!empty($tag)) {
1528             $aargs['tag'] = $tag;
1529         }
1530
1531         parse_str($_SERVER['QUERY_STRING'], $params);
1532         $pstring = '';
1533         if (!empty($params)) {
1534             unset($params['p']);
1535             $pstring = http_build_query($params);
1536         }
1537
1538         $uri = common_local_url($action, $aargs);
1539
1540         if (!empty($pstring)) {
1541             $uri .= '?' . $pstring;
1542         }
1543
1544         return $uri;
1545     }
1546 }