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