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