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