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