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