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