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