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