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