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