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