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