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