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