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