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