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