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