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