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