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