]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/apiaction.php
Merge branch '1.0.x' into people_tags_rebase
[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     function prepare($args)
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     function handle($args)
176     {
177         header('Access-Control-Allow-Origin: *');
178         parent::handle($args);
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         $twitter_user['id'] = intval($profile->id);
206         $twitter_user['name'] = $profile->getBestName();
207         $twitter_user['screen_name'] = $profile->nickname;
208         $twitter_user['location'] = ($profile->location) ? $profile->location : null;
209         $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
210
211         $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
212         $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
213             Avatar::defaultImage(AVATAR_STREAM_SIZE);
214
215         $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
216         $twitter_user['protected'] = false; # not supported by StatusNet yet
217         $twitter_user['followers_count'] = $profile->subscriberCount();
218
219         $design        = null;
220         $user          = $profile->getUser();
221
222         // Note: some profiles don't have an associated user
223
224         $defaultDesign = Design::siteDesign();
225
226         if (!empty($user)) {
227             $design = $user->getDesign();
228         }
229
230         if (empty($design)) {
231             $design = $defaultDesign;
232         }
233
234         $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
235         $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
236         $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
237         $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
238         $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
239         $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
240         $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
241         $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
242         $twitter_user['profile_sidebar_border_color'] = '';
243
244         $twitter_user['friends_count'] = $profile->subscriptionCount();
245
246         $twitter_user['created_at'] = $this->dateTwitter($profile->created);
247
248         $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
249
250         $timezone = 'UTC';
251
252         if (!empty($user) && $user->timezone) {
253             $timezone = $user->timezone;
254         }
255
256         $t = new DateTime;
257         $t->setTimezone(new DateTimeZone($timezone));
258
259         $twitter_user['utc_offset'] = $t->format('Z');
260         $twitter_user['time_zone'] = $timezone;
261
262         $twitter_user['profile_background_image_url']
263             = empty($design->backgroundimage)
264             ? '' : ($design->disposition & BACKGROUND_ON)
265             ? Design::url($design->backgroundimage) : '';
266
267         $twitter_user['profile_background_tile']
268             = (bool)($design->disposition & BACKGROUND_TILE);
269
270         $twitter_user['statuses_count'] = $profile->noticeCount();
271
272         // Is the requesting user following this user?
273         $twitter_user['following'] = false;
274         $twitter_user['statusnet:blocking'] = false;
275         $twitter_user['notifications'] = false;
276
277         if (isset($this->auth_user)) {
278
279             $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
280             $twitter_user['statusnet:blocking']  = $this->auth_user->hasBlocked($profile);
281
282             // Notifications on?
283             $sub = Subscription::pkeyGet(array('subscriber' =>
284                                                $this->auth_user->id,
285                                                'subscribed' => $profile->id));
286
287             if ($sub) {
288                 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
289             }
290         }
291
292         if ($get_notice) {
293             $notice = $profile->getCurrentNotice();
294             if ($notice) {
295                 # don't get user!
296                 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
297             }
298         }
299
300         // StatusNet-specific
301
302         $twitter_user['statusnet_profile_url'] = $profile->profileurl;
303
304         return $twitter_user;
305     }
306
307     function twitterStatusArray($notice, $include_user=true)
308     {
309         $base = $this->twitterSimpleStatusArray($notice, $include_user);
310
311         if (!empty($notice->repeat_of)) {
312             $original = Notice::staticGet('id', $notice->repeat_of);
313             if (!empty($original)) {
314                 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
315                 $base['retweeted_status'] = $original_array;
316             }
317         }
318
319         return $base;
320     }
321
322     function twitterSimpleStatusArray($notice, $include_user=true)
323     {
324         $profile = $notice->getProfile();
325
326         $twitter_status = array();
327         $twitter_status['text'] = $notice->content;
328         $twitter_status['truncated'] = false; # Not possible on StatusNet
329         $twitter_status['created_at'] = $this->dateTwitter($notice->created);
330         $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
331             intval($notice->reply_to) : null;
332
333         $source = null;
334
335         $ns = $notice->getSource();
336         if ($ns) {
337             if (!empty($ns->name) && !empty($ns->url)) {
338                 $source = '<a href="'
339                     . htmlspecialchars($ns->url)
340                     . '" rel="nofollow">'
341                     . htmlspecialchars($ns->name)
342                     . '</a>';
343             } else {
344                 $source = $ns->code;
345             }
346         }
347
348         $twitter_status['source'] = $source;
349         $twitter_status['id'] = intval($notice->id);
350
351         $replier_profile = null;
352
353         if ($notice->reply_to) {
354             $reply = Notice::staticGet(intval($notice->reply_to));
355             if ($reply) {
356                 $replier_profile = $reply->getProfile();
357             }
358         }
359
360         $twitter_status['in_reply_to_user_id'] =
361             ($replier_profile) ? intval($replier_profile->id) : null;
362         $twitter_status['in_reply_to_screen_name'] =
363             ($replier_profile) ? $replier_profile->nickname : null;
364
365         if (isset($notice->lat) && isset($notice->lon)) {
366             // This is the format that GeoJSON expects stuff to be in
367             $twitter_status['geo'] = array('type' => 'Point',
368                                            'coordinates' => array((float) $notice->lat,
369                                                                   (float) $notice->lon));
370         } else {
371             $twitter_status['geo'] = null;
372         }
373
374         if (isset($this->auth_user)) {
375             $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
376         } else {
377             $twitter_status['favorited'] = false;
378         }
379
380         // Enclosures
381         $attachments = $notice->attachments();
382
383         if (!empty($attachments)) {
384
385             $twitter_status['attachments'] = array();
386
387             foreach ($attachments as $attachment) {
388                 $enclosure_o=$attachment->getEnclosure();
389                 if ($enclosure_o) {
390                     $enclosure = array();
391                     $enclosure['url'] = $enclosure_o->url;
392                     $enclosure['mimetype'] = $enclosure_o->mimetype;
393                     $enclosure['size'] = $enclosure_o->size;
394                     $twitter_status['attachments'][] = $enclosure;
395                 }
396             }
397         }
398
399         if ($include_user && $profile) {
400             # Don't get notice (recursive!)
401             $twitter_user = $this->twitterUserArray($profile, false);
402             $twitter_status['user'] = $twitter_user;
403         }
404
405         // StatusNet-specific
406
407         $twitter_status['statusnet_html'] = $notice->rendered;
408
409         return $twitter_status;
410     }
411
412     function twitterGroupArray($group)
413     {
414         $twitter_group = array();
415
416         $twitter_group['id'] = intval($group->id);
417         $twitter_group['url'] = $group->permalink();
418         $twitter_group['nickname'] = $group->nickname;
419         $twitter_group['fullname'] = $group->fullname;
420
421         if (isset($this->auth_user)) {
422             $twitter_group['member'] = $this->auth_user->isMember($group);
423             $twitter_group['blocked'] = Group_block::isBlocked(
424                 $group,
425                 $this->auth_user->getProfile()
426             );
427         }
428
429         $twitter_group['member_count'] = $group->getMemberCount();
430         $twitter_group['original_logo'] = $group->original_logo;
431         $twitter_group['homepage_logo'] = $group->homepage_logo;
432         $twitter_group['stream_logo'] = $group->stream_logo;
433         $twitter_group['mini_logo'] = $group->mini_logo;
434         $twitter_group['homepage'] = $group->homepage;
435         $twitter_group['description'] = $group->description;
436         $twitter_group['location'] = $group->location;
437         $twitter_group['created'] = $this->dateTwitter($group->created);
438         $twitter_group['modified'] = $this->dateTwitter($group->modified);
439
440         return $twitter_group;
441     }
442
443     function twitterRssGroupArray($group)
444     {
445         $entry = array();
446         $entry['content']=$group->description;
447         $entry['title']=$group->nickname;
448         $entry['link']=$group->permalink();
449         $entry['published']=common_date_iso8601($group->created);
450         $entry['updated']==common_date_iso8601($group->modified);
451         $taguribase = common_config('integration', 'groupuri');
452         $entry['id'] = "group:$groupuribase:$entry[link]";
453
454         $entry['description'] = $entry['content'];
455         $entry['pubDate'] = common_date_rfc2822($group->created);
456         $entry['guid'] = $entry['link'];
457
458         return $entry;
459     }
460
461     function twitterListArray($list)
462     {
463         $profile = Profile::staticGet('id', $list->tagger);
464
465         $twitter_list = array();
466         $twitter_list['id'] = $list->id;
467         $twitter_list['name'] = $list->tag;
468         $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
469         $twitter_list['slug'] = $list->tag;
470         $twitter_list['description'] = $list->description;
471         $twitter_list['subscriber_count'] = $list->subscriberCount();
472         $twitter_list['member_count'] = $list->taggedCount();
473         $twitter_list['uri'] = $list->getUri();
474
475         if (isset($this->auth_user)) {
476             $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
477         } else {
478             $twitter_list['following'] = false;
479         }
480
481         $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
482         $twitter_list['user'] = $this->twitterUserArray($profile, false);
483
484         return $twitter_list;
485     }
486
487     function twitterRssEntryArray($notice)
488     {
489         $entry = array();
490
491         if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
492             $profile = $notice->getProfile();
493
494             // We trim() to avoid extraneous whitespace in the output
495
496             $entry['content'] = common_xml_safe_str(trim($notice->rendered));
497             $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
498             $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
499             $entry['published'] = common_date_iso8601($notice->created);
500
501             $taguribase = TagURI::base();
502             $entry['id'] = "tag:$taguribase:$entry[link]";
503
504             $entry['updated'] = $entry['published'];
505             $entry['author'] = $profile->getBestName();
506
507             // Enclosures
508             $attachments = $notice->attachments();
509             $enclosures = array();
510
511             foreach ($attachments as $attachment) {
512                 $enclosure_o=$attachment->getEnclosure();
513                 if ($enclosure_o) {
514                     $enclosure = array();
515                     $enclosure['url'] = $enclosure_o->url;
516                     $enclosure['mimetype'] = $enclosure_o->mimetype;
517                     $enclosure['size'] = $enclosure_o->size;
518                     $enclosures[] = $enclosure;
519                 }
520             }
521
522             if (!empty($enclosures)) {
523                 $entry['enclosures'] = $enclosures;
524             }
525
526             // Tags/Categories
527             $tag = new Notice_tag();
528             $tag->notice_id = $notice->id;
529             if ($tag->find()) {
530                 $entry['tags']=array();
531                 while ($tag->fetch()) {
532                     $entry['tags'][]=$tag->tag;
533                 }
534             }
535             $tag->free();
536
537             // RSS Item specific
538             $entry['description'] = $entry['content'];
539             $entry['pubDate'] = common_date_rfc2822($notice->created);
540             $entry['guid'] = $entry['link'];
541
542             if (isset($notice->lat) && isset($notice->lon)) {
543                 // This is the format that GeoJSON expects stuff to be in.
544                 // showGeoRSS() below uses it for XML output, so we reuse it
545                 $entry['geo'] = array('type' => 'Point',
546                                       'coordinates' => array((float) $notice->lat,
547                                                              (float) $notice->lon));
548             } else {
549                 $entry['geo'] = null;
550             }
551
552             Event::handle('EndRssEntryArray', array($notice, &$entry));
553         }
554
555         return $entry;
556     }
557
558     function twitterRelationshipArray($source, $target)
559     {
560         $relationship = array();
561
562         $relationship['source'] =
563             $this->relationshipDetailsArray($source, $target);
564         $relationship['target'] =
565             $this->relationshipDetailsArray($target, $source);
566
567         return array('relationship' => $relationship);
568     }
569
570     function relationshipDetailsArray($source, $target)
571     {
572         $details = array();
573
574         $details['screen_name'] = $source->nickname;
575         $details['followed_by'] = $target->isSubscribed($source);
576         $details['following'] = $source->isSubscribed($target);
577
578         $notifications = false;
579
580         if ($source->isSubscribed($target)) {
581             $sub = Subscription::pkeyGet(array('subscriber' =>
582                 $source->id, 'subscribed' => $target->id));
583
584             if (!empty($sub)) {
585                 $notifications = ($sub->jabber || $sub->sms);
586             }
587         }
588
589         $details['notifications_enabled'] = $notifications;
590         $details['blocking'] = $source->hasBlocked($target);
591         $details['id'] = intval($source->id);
592
593         return $details;
594     }
595
596     function showTwitterXmlRelationship($relationship)
597     {
598         $this->elementStart('relationship');
599
600         foreach($relationship as $element => $value) {
601             if ($element == 'source' || $element == 'target') {
602                 $this->elementStart($element);
603                 $this->showXmlRelationshipDetails($value);
604                 $this->elementEnd($element);
605             }
606         }
607
608         $this->elementEnd('relationship');
609     }
610
611     function showXmlRelationshipDetails($details)
612     {
613         foreach($details as $element => $value) {
614             $this->element($element, null, $value);
615         }
616     }
617
618     function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
619     {
620         $attrs = array();
621         if ($namespaces) {
622             $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
623         }
624         $this->elementStart($tag, $attrs);
625         foreach($twitter_status as $element => $value) {
626             switch ($element) {
627             case 'user':
628                 $this->showTwitterXmlUser($twitter_status['user']);
629                 break;
630             case 'text':
631                 $this->element($element, null, common_xml_safe_str($value));
632                 break;
633             case 'attachments':
634                 $this->showXmlAttachments($twitter_status['attachments']);
635                 break;
636             case 'geo':
637                 $this->showGeoXML($value);
638                 break;
639             case 'retweeted_status':
640                 $this->showTwitterXmlStatus($value, 'retweeted_status');
641                 break;
642             default:
643                 if (strncmp($element, 'statusnet_', 10) == 0) {
644                     $this->element('statusnet:'.substr($element, 10), null, $value);
645                 } else {
646                     $this->element($element, null, $value);
647                 }
648             }
649         }
650         $this->elementEnd($tag);
651     }
652
653     function showTwitterXmlGroup($twitter_group)
654     {
655         $this->elementStart('group');
656         foreach($twitter_group as $element => $value) {
657             $this->element($element, null, $value);
658         }
659         $this->elementEnd('group');
660     }
661
662     function showTwitterXmlList($twitter_list)
663     {
664         $this->elementStart('list');
665         foreach($twitter_list as $element => $value) {
666             if($element == 'user') {
667                 $this->showTwitterXmlUser($value, 'user');
668             }
669             else {
670                 $this->element($element, null, $value);
671             }
672         }
673         $this->elementEnd('list');
674     }
675
676     function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
677     {
678         $attrs = array();
679         if ($namespaces) {
680             $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
681         }
682         $this->elementStart($role, $attrs);
683         foreach($twitter_user as $element => $value) {
684             if ($element == 'status') {
685                 $this->showTwitterXmlStatus($twitter_user['status']);
686             } else if (strncmp($element, 'statusnet_', 10) == 0) {
687                 $this->element('statusnet:'.substr($element, 10), null, $value);
688             } else {
689                 $this->element($element, null, $value);
690             }
691         }
692         $this->elementEnd($role);
693     }
694
695     function showXmlAttachments($attachments) {
696         if (!empty($attachments)) {
697             $this->elementStart('attachments', array('type' => 'array'));
698             foreach ($attachments as $attachment) {
699                 $attrs = array();
700                 $attrs['url'] = $attachment['url'];
701                 $attrs['mimetype'] = $attachment['mimetype'];
702                 $attrs['size'] = $attachment['size'];
703                 $this->element('enclosure', $attrs, '');
704             }
705             $this->elementEnd('attachments');
706         }
707     }
708
709     function showGeoXML($geo)
710     {
711         if (empty($geo)) {
712             // empty geo element
713             $this->element('geo');
714         } else {
715             $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
716             $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
717             $this->elementEnd('geo');
718         }
719     }
720
721     function showGeoRSS($geo)
722     {
723         if (!empty($geo)) {
724             $this->element(
725                 'georss:point',
726                 null,
727                 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
728             );
729         }
730     }
731
732     function showTwitterRssItem($entry)
733     {
734         $this->elementStart('item');
735         $this->element('title', null, $entry['title']);
736         $this->element('description', null, $entry['description']);
737         $this->element('pubDate', null, $entry['pubDate']);
738         $this->element('guid', null, $entry['guid']);
739         $this->element('link', null, $entry['link']);
740
741         # RSS only supports 1 enclosure per item
742         if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
743             $enclosure = $entry['enclosures'][0];
744             $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
745         }
746
747         if(array_key_exists('tags', $entry)){
748             foreach($entry['tags'] as $tag){
749                 $this->element('category', null,$tag);
750             }
751         }
752
753         $this->showGeoRSS($entry['geo']);
754         $this->elementEnd('item');
755     }
756
757     function showJsonObjects($objects)
758     {
759         print(json_encode($objects));
760     }
761
762     function showSingleXmlStatus($notice)
763     {
764         $this->initDocument('xml');
765         $twitter_status = $this->twitterStatusArray($notice);
766         $this->showTwitterXmlStatus($twitter_status, 'status', true);
767         $this->endDocument('xml');
768     }
769
770     function showSingleAtomStatus($notice)
771     {
772         header('Content-Type: application/atom+xml; charset=utf-8');
773         print $notice->asAtomEntry(true, true, true, $this->auth_user);
774     }
775
776     function show_single_json_status($notice)
777     {
778         $this->initDocument('json');
779         $status = $this->twitterStatusArray($notice);
780         $this->showJsonObjects($status);
781         $this->endDocument('json');
782     }
783
784     function showXmlTimeline($notice)
785     {
786         $this->initDocument('xml');
787         $this->elementStart('statuses', array('type' => 'array',
788                                               'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
789
790         if (is_array($notice)) {
791             $notice = new ArrayWrapper($notice);
792         }
793
794         while ($notice->fetch()) {
795             try {
796                 $twitter_status = $this->twitterStatusArray($notice);
797                 $this->showTwitterXmlStatus($twitter_status);
798             } catch (Exception $e) {
799                 common_log(LOG_ERR, $e->getMessage());
800                 continue;
801             }
802         }
803
804         $this->elementEnd('statuses');
805         $this->endDocument('xml');
806     }
807
808     function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
809     {
810         $this->initDocument('rss');
811
812         $this->element('title', null, $title);
813         $this->element('link', null, $link);
814
815         if (!is_null($self)) {
816             $this->element(
817                 'atom:link',
818                 array(
819                     'type' => 'application/rss+xml',
820                     'href' => $self,
821                     'rel'  => 'self'
822                 )
823            );
824         }
825
826         if (!is_null($suplink)) {
827             // For FriendFeed's SUP protocol
828             $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
829                                          'rel' => 'http://api.friendfeed.com/2008/03#sup',
830                                          'href' => $suplink,
831                                          'type' => 'application/json'));
832         }
833
834         if (!is_null($logo)) {
835             $this->elementStart('image');
836             $this->element('link', null, $link);
837             $this->element('title', null, $title);
838             $this->element('url', null, $logo);
839             $this->elementEnd('image');
840         }
841
842         $this->element('description', null, $subtitle);
843         $this->element('language', null, 'en-us');
844         $this->element('ttl', null, '40');
845
846         if (is_array($notice)) {
847             $notice = new ArrayWrapper($notice);
848         }
849
850         while ($notice->fetch()) {
851             try {
852                 $entry = $this->twitterRssEntryArray($notice);
853                 $this->showTwitterRssItem($entry);
854             } catch (Exception $e) {
855                 common_log(LOG_ERR, $e->getMessage());
856                 // continue on exceptions
857             }
858         }
859
860         $this->endTwitterRss();
861     }
862
863     function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
864     {
865         $this->initDocument('atom');
866
867         $this->element('title', null, $title);
868         $this->element('id', null, $id);
869         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
870
871         if (!is_null($logo)) {
872             $this->element('logo',null,$logo);
873         }
874
875         if (!is_null($suplink)) {
876             # For FriendFeed's SUP protocol
877             $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
878                                          'href' => $suplink,
879                                          'type' => 'application/json'));
880         }
881
882         if (!is_null($selfuri)) {
883             $this->element('link', array('href' => $selfuri,
884                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
885         }
886
887         $this->element('updated', null, common_date_iso8601('now'));
888         $this->element('subtitle', null, $subtitle);
889
890         if (is_array($notice)) {
891             $notice = new ArrayWrapper($notice);
892         }
893
894         while ($notice->fetch()) {
895             try {
896                 $this->raw($notice->asAtomEntry());
897             } catch (Exception $e) {
898                 common_log(LOG_ERR, $e->getMessage());
899                 continue;
900             }
901         }
902
903         $this->endDocument('atom');
904     }
905
906     function showRssGroups($group, $title, $link, $subtitle)
907     {
908         $this->initDocument('rss');
909
910         $this->element('title', null, $title);
911         $this->element('link', null, $link);
912         $this->element('description', null, $subtitle);
913         $this->element('language', null, 'en-us');
914         $this->element('ttl', null, '40');
915
916         if (is_array($group)) {
917             foreach ($group as $g) {
918                 $twitter_group = $this->twitterRssGroupArray($g);
919                 $this->showTwitterRssItem($twitter_group);
920             }
921         } else {
922             while ($group->fetch()) {
923                 $twitter_group = $this->twitterRssGroupArray($group);
924                 $this->showTwitterRssItem($twitter_group);
925             }
926         }
927
928         $this->endTwitterRss();
929     }
930
931     function showTwitterAtomEntry($entry)
932     {
933         $this->elementStart('entry');
934         $this->element('title', null, common_xml_safe_str($entry['title']));
935         $this->element(
936             'content',
937             array('type' => 'html'),
938             common_xml_safe_str($entry['content'])
939         );
940         $this->element('id', null, $entry['id']);
941         $this->element('published', null, $entry['published']);
942         $this->element('updated', null, $entry['updated']);
943         $this->element('link', array('type' => 'text/html',
944                                      'href' => $entry['link'],
945                                      'rel' => 'alternate'));
946         $this->element('link', array('type' => $entry['avatar-type'],
947                                      'href' => $entry['avatar'],
948                                      'rel' => 'image'));
949         $this->elementStart('author');
950
951         $this->element('name', null, $entry['author-name']);
952         $this->element('uri', null, $entry['author-uri']);
953
954         $this->elementEnd('author');
955         $this->elementEnd('entry');
956     }
957
958     function showXmlDirectMessage($dm, $namespaces=false)
959     {
960         $attrs = array();
961         if ($namespaces) {
962             $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
963         }
964         $this->elementStart('direct_message', $attrs);
965         foreach($dm as $element => $value) {
966             switch ($element) {
967             case 'sender':
968             case 'recipient':
969                 $this->showTwitterXmlUser($value, $element);
970                 break;
971             case 'text':
972                 $this->element($element, null, common_xml_safe_str($value));
973                 break;
974             default:
975                 $this->element($element, null, $value);
976                 break;
977             }
978         }
979         $this->elementEnd('direct_message');
980     }
981
982     function directMessageArray($message)
983     {
984         $dmsg = array();
985
986         $from_profile = $message->getFrom();
987         $to_profile = $message->getTo();
988
989         $dmsg['id'] = intval($message->id);
990         $dmsg['sender_id'] = intval($from_profile);
991         $dmsg['text'] = trim($message->content);
992         $dmsg['recipient_id'] = intval($to_profile);
993         $dmsg['created_at'] = $this->dateTwitter($message->created);
994         $dmsg['sender_screen_name'] = $from_profile->nickname;
995         $dmsg['recipient_screen_name'] = $to_profile->nickname;
996         $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
997         $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
998
999         return $dmsg;
1000     }
1001
1002     function rssDirectMessageArray($message)
1003     {
1004         $entry = array();
1005
1006         $from = $message->getFrom();
1007
1008         $entry['title'] = sprintf('Message from %1$s to %2$s',
1009             $from->nickname, $message->getTo()->nickname);
1010
1011         $entry['content'] = common_xml_safe_str($message->rendered);
1012         $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
1013         $entry['published'] = common_date_iso8601($message->created);
1014
1015         $taguribase = TagURI::base();
1016
1017         $entry['id'] = "tag:$taguribase:$entry[link]";
1018         $entry['updated'] = $entry['published'];
1019
1020         $entry['author-name'] = $from->getBestName();
1021         $entry['author-uri'] = $from->homepage;
1022
1023         $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
1024
1025         $entry['avatar']      = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
1026         $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
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::staticGet($this->arg('id'));
1501             } else if ($this->arg('id')) {
1502                 $nickname = common_canonical_nickname($this->arg('id'));
1503                 return User::staticGet('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::staticGet('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::staticGet('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::staticGet($id);
1520         } else {
1521             $nickname = common_canonical_nickname($id);
1522             return User::staticGet('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::staticGet($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::staticGet('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::staticGet('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::staticGet('nickname', $nickname);
1547                 return $user ? $user->getProfile() : null;
1548             }
1549         } else if (self::is_decimal($id)) {
1550             return Profile::staticGet($id);
1551         } else {
1552             $nickname = common_canonical_nickname($id);
1553             $user = User::staticGet('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::staticGet('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::staticGet('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::staticGet('id', $id);
1577         } else {
1578             return User_group::getForNickname($id);
1579         }
1580     }
1581
1582     function getTargetList($user=null, $id=null)
1583     {
1584         $tagger = $this->getTargetUser($user);
1585         $list = null;
1586
1587         if (empty($id)) {
1588             $id = $this->arg('id');
1589         }
1590
1591         if($id) {
1592             if (is_numeric($id)) {
1593                 $list = Profile_list::staticGet('id', $id);
1594
1595                 // only if the list with the id belongs to the tagger
1596                 if(empty($list) || $list->tagger != $tagger->id) {
1597                     $list = null;
1598                 }
1599             }
1600             if (empty($list)) {
1601                 $tag = common_canonical_tag($id);
1602                 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1603             }
1604
1605             if (!empty($list) && $list->private) {
1606                 if ($this->auth_user->id == $list->tagger) {
1607                     return $list;
1608                 }
1609             } else {
1610                 return $list;
1611             }
1612         }
1613         return null;
1614     }
1615
1616     /**
1617      * Returns query argument or default value if not found. Certain
1618      * parameters used throughout the API are lightly scrubbed and
1619      * bounds checked.  This overrides Action::arg().
1620      *
1621      * @param string $key requested argument
1622      * @param string $def default value to return if $key is not provided
1623      *
1624      * @return var $var
1625      */
1626     function arg($key, $def=null)
1627     {
1628         // XXX: Do even more input validation/scrubbing?
1629
1630         if (array_key_exists($key, $this->args)) {
1631             switch($key) {
1632             case 'page':
1633                 $page = (int)$this->args['page'];
1634                 return ($page < 1) ? 1 : $page;
1635             case 'count':
1636                 $count = (int)$this->args['count'];
1637                 if ($count < 1) {
1638                     return 20;
1639                 } elseif ($count > 200) {
1640                     return 200;
1641                 } else {
1642                     return $count;
1643                 }
1644             case 'since_id':
1645                 $since_id = (int)$this->args['since_id'];
1646                 return ($since_id < 1) ? 0 : $since_id;
1647             case 'max_id':
1648                 $max_id = (int)$this->args['max_id'];
1649                 return ($max_id < 1) ? 0 : $max_id;
1650             default:
1651                 return parent::arg($key, $def);
1652             }
1653         } else {
1654             return $def;
1655         }
1656     }
1657
1658     /**
1659      * Calculate the complete URI that called up this action.  Used for
1660      * Atom rel="self" links.  Warning: this is funky.
1661      *
1662      * @return string URL    a URL suitable for rel="self" Atom links
1663      */
1664     function getSelfUri()
1665     {
1666         $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1667
1668         $id = $this->arg('id');
1669         $aargs = array('format' => $this->format);
1670         if (!empty($id)) {
1671             $aargs['id'] = $id;
1672         }
1673
1674         $tag = $this->arg('tag');
1675         if (!empty($tag)) {
1676             $aargs['tag'] = $tag;
1677         }
1678
1679         parse_str($_SERVER['QUERY_STRING'], $params);
1680         $pstring = '';
1681         if (!empty($params)) {
1682             unset($params['p']);
1683             $pstring = http_build_query($params);
1684         }
1685
1686         $uri = common_local_url($action, $aargs);
1687
1688         if (!empty($pstring)) {
1689             $uri .= '?' . $pstring;
1690         }
1691
1692         return $uri;
1693     }
1694 }