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