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