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