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