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