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