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