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