File Coverage

blib/lib/Apache/iNcom.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             #
2             # iNcom.pm - Main module of the iNcom package.
3             #
4             # This file is part of Apache::iNcom
5             #
6             # Author: Francis J. Lacoste
7             #
8             # Copyright (C) 1999 Francis J. Lacoste, iNsu Innovations
9             #
10             # This program is free software; you can redistribute it and/or modify
11             # it under the terms of the GNU General Public License as published by
12             # the Free Software Foundation; either version 2 of the License, or
13             # (at your option) any later version.
14             #
15             # This program is distributed in the hope that it will be useful,
16             # but WITHOUT ANY WARRANTY; without even the implied warranty of
17             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18             # GNU General Public License for more details.
19             #
20             # You should have received a copy of the GNU General Public License
21             # along with this program; if not, write to the Free Software
22             # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23             #
24             package Apache::iNcom;
25              
26 1     1   988 use strict;
  1         2  
  1         49  
27              
28             require 5.005;
29              
30 1     1   2459 use DBI;
  1         23640  
  1         86  
31              
32 1     1   1751 use Apache;
  0            
  0            
33             use Apache::Log;
34             use Apache::Cookie;
35             use Apache::Request;
36             use Apache::File;
37             use Apache::Constants qw( :common :response HTTP_PRECONDITION_FAILED );
38              
39             use HTML::Embperl;
40              
41             use Apache::iNcom::Request;
42             use Apache::iNcom::Localizer;
43              
44             use vars qw($VERSION);
45             BEGIN {
46             ($VERSION) = '0.09';
47             }
48              
49             my %VALID_PNOTES = map { $_ => 1 } qw (
50             INCOM_SESSION INCOM_DBH INCOM_LOCALIZER INCOM_COOKIES
51             );
52              
53             # Grabbed from CGI.pm by Lincoln Stein
54             sub offset_calc {
55             my($time) = @_;
56             my(%mult) = ('s'=>1,
57             'm'=>60,
58             'h'=>60*60,
59             'd'=>60*60*24,
60             'M'=>60*60*24*30,
61             'y'=>60*60*24*365);
62             # format for time can be in any of the forms...
63             # "now" -- expire immediately
64             # "+180s" -- in 180 seconds
65             # "+2m" -- in 2 minutes
66             # "+12h" -- in 12 hours
67             # "+1d" -- in 1 day
68             # "+3M" -- in 3 months
69             # "+2y" -- in 2 years
70             # "-3m" -- 3 minutes ago(!)
71             my($offset);
72             if (!$time || (lc($time) eq 'now')) {
73             $offset = 0;
74             } elsif ($time=~/^([+-]?(?:\d+|\d*\.\d*))([mhdMy]?)/) {
75             $offset = ($mult{$2} || 1)*$1;
76             } else {
77             die "invalid expiration offset: $time\n";
78             }
79             return ($offset);
80             }
81              
82             sub db_init {
83             my $r = shift;
84             my $dsn = $r->dir_config( "INCOM_DBI_DSN" );
85             my $user = $r->dir_config( "INCOM_DBI_USER" );
86             my $passwd = $r->dir_config( "INCOM_DBI_PASSWD" );
87              
88             unless ( $dsn ) {
89             $r->log_error( "iNcom configuration error: INCOM_DBI_DSN is not defined" );
90             return SERVER_ERROR;
91             }
92              
93             my $dbh;
94             eval {
95             $dbh = DBI->connect( $dsn, $user, $passwd, { RaiseError => 1,
96             AutoCommit => 0,
97             } );
98             my $trace_lvl = $r->dir_config( "INCOM_DBI_TRACE" );
99             my $trace_file = $r->dir_config( "INCOM_DBI_LOG" );
100              
101             # Turn tracing on if requested
102             if ( $trace_lvl ) {
103             $trace_file = $r->server_root_relative( $trace_file )
104             if defined $trace_file;
105             $dbh->trace( $trace_lvl, $trace_file );
106             }
107              
108             # Saves it in the request record for access from
109             # other handlers
110             $r->pnotes( INCOM_DBH => $dbh );
111              
112             };
113             if ($@ ) {
114             $r->log_error( "error opening connection to database: $@" );
115             return return_error( $r, SERVER_ERROR );
116             }
117             return OK;
118             }
119              
120             sub i18n_init {
121             my $r = shift;
122              
123             my $langs = $r->header_in( "Accept-Language" );
124             my @languages;
125             if ( $langs ) {
126             my $q = 100;
127             @languages = map {
128             $_->[0];
129             } sort { $b->[1] <=> $a->[1] } map {
130             my $l;
131             if ( /([-\w]+)\s*;\s*q=([\d.]+)/ ) {
132             $l = [$1, $2 ];
133             } else {
134             $l = [$_, $q--];
135             }
136             } split /\s*,\s*/, $langs;
137             }
138              
139             # Add the language set in cookies
140             my $cookies = $r->pnotes( "INCOM_COOKIES" );
141             unshift @languages, $cookies->{INCOM_LANGUAGE}->value
142             if $cookies->{INCOM_LANGUAGE};
143              
144             # Check each languages tags for validity
145             my $localizer =
146             new Apache::iNcom::Localizer( $r->dir_config( "INCOM_DEFAULT_LANGUAGE" ) || "en",
147             @languages
148             );
149              
150             # Set environment variables so that other parts of the system
151             # does hopefully the Right Things(tm)
152             $ENV{LANG} = $localizer->preferred_lang;
153              
154             # Long live GNU !
155             $ENV{LANGUAGE} = join ":", $localizer->preferred_langs,
156             $localizer->default_lang;
157              
158             # Cache it for further use.
159             $r->pnotes( "INCOM_LOCALIZER", $localizer );
160              
161             return OK;
162             }
163              
164             *Apache::iNcom::handler = \&request_init;
165              
166             sub request_init {
167             my $r = shift;
168              
169             # If we are in a subrequest, just copy
170             # what was initialized to the new request
171             if ( $r->is_main ) {
172             my $prefix = $r->dir_config( "INCOM_URL_PREFIX" ) || "/";
173             unless ( $prefix =~ m|/$| ) {
174             $r->log_error( "iNcom configuration error: INCOM_URL_PREFIX must ends with /" );
175             return SERVER_ERROR;
176             }
177              
178             # Parse cookies
179             my $c = $r->header_in( "Cookie" );
180             my $cookies = Apache::Cookie->new( $r )->parse( $c );
181             $r->pnotes( "INCOM_COOKIES", $cookies );
182              
183             # Parse languages
184             my $rv = i18n_init( $r );
185             return $rv if $rv != OK;
186              
187             } else {
188             my $prev = $r->prev;
189             foreach my $name ( keys %VALID_PNOTES ) {
190             $r->pnotes( $name, $prev->pnotes( $name ) );
191             }
192             return OK;
193             }
194              
195             # Next handler is dispatch_handler
196             $r->push_handlers( PerlTransHandler => \&dispatch_handler );
197              
198             return OK;
199             }
200              
201             sub bake_session_cookie {
202             my ($r, $session_id) = @_;
203              
204             my $prefix = $r->dir_config( "INCOM_URL_PREFIX" ) || "/";
205             my $session_secure = $r->dir_config( "INCOM_SESSION_SECURE" );
206             my $session_domain = $r->dir_config( "INCOM_SESSION_DOMAIN" );
207             my $session_expires = $r->dir_config( "INCOM_SESSION_EXPIRES" );
208             my $session_path = $r->dir_config( "INCOM_SESSION_PATH" )
209             || $prefix;
210              
211             my $cookie = new Apache::Cookie( $r,
212             -name => "INCOM_SESSION",
213             -value => $session_id,
214             -path => $session_path
215             );
216             $cookie->domain( $session_domain ) if $session_domain;
217             $cookie->expires( $session_expires ) if $session_expires;
218             $cookie->secure( 1 ) if $session_secure;
219              
220             # Add cookie to outgoing headers
221             $cookie->bake;
222             }
223              
224             sub session_init {
225             my $r = shift;
226              
227             my %session;
228              
229             # Check if there is a session id in the cookies
230             my $cookies = $r->pnotes( "INCOM_COOKIES" );
231             if ( $cookies->{INCOM_SESSION} ) {
232             my $session_id = $cookies->{INCOM_SESSION}->value;
233              
234             # Load the user's session
235             eval {
236             # Make sure it looks like a session id
237             die "Invalid session id: $session_id\n"
238             unless length $session_id == 32 &&
239             $session_id =~ tr/a-fA-F0-9/a-fA-F0-9/ == 32;
240              
241             tie %session, 'Apache::iNcom::Session', $session_id,
242             { dbh => $r->pnotes( "INCOM_DBH"),
243             Serialize => $r->dir_config( "INCOM_SESSION_SERIALIZE_ACCESS" ),
244             };
245              
246             # Save the session for future handlers
247             $r->pnotes( INCOM_SESSION => \%session );
248              
249             if ( $r->dir_config( "INCOM_SESSION_EXPIRES" ) ) {
250             # If session doesn't expire with the browser session
251             # we must renew the cookie.
252             bake_session_cookie( $r, $session_id );
253             }
254              
255             };
256             if ( $@ ) {
257             # The session ID is probably invalid
258             chomp $@;
259             $r->warn( "error loading session: $@" );
260             } else {
261             # Return ref to session to indicate success
262             return \%session;
263             }
264             }
265              
266             # No valid session could be loaded
267             return undef;
268             }
269              
270              
271             # Return the requested error code but sets a custom response
272             # if the error condition is present in the error map.
273             sub return_error {
274             my ( $r, $status ) = @_;
275              
276             my $prefix = $r->dir_config( "INCOM_URL_PREFIX" ) || "/";
277             my $map = $r->dir_config( "INCOM_ERROR_PROFILE" );
278             return $status unless $map;
279              
280             $map = $r->server_root_relative( $map );
281             unless ( -e $map && -f _ && -r _ ) {
282             $r->warn( "INCOM_ERROR_PROFILE is not valid" );
283             return $status;
284             }
285              
286             my $response = eval {
287             my $profile = do $map;
288             unless ( ref $profile eq "HASH" ) {
289             $r->warn( "INCOM_ERROR_PROFILE didn't return an hash ref" );
290             return $status;
291             }
292              
293             my $error_cond = $r->pnotes( "INCOM_ERROR" );
294              
295             $profile->{$error_cond} || $profile->{$status};
296             };
297             if ( $@) {
298             $r->warn( "error while evaluating error profile: $@" );
299             return $status;
300             }
301              
302             $r->custom_response( $status, $prefix . "/incom_error/" . $response ) if $response;
303              
304             return $status;
305             }
306              
307             # This is a handler used to transform the request
308             # to an action. It is invoked during the URI
309             # translation phase of the request
310             #
311             # It is responsible for loading the user session. If
312             # there is no session it sets the content handler to
313             # the new_session_handler
314             sub dispatch_handler {
315             my $r = shift;
316              
317             # Get configuration
318             my $prefix = $r->dir_config( "INCOM_URL_PREFIX" ) || "/";
319             my $index_file = $r->dir_config( "INCOM_INDEX" ) || "index.html";
320             my $incom_root = $r->dir_config( "INCOM_ROOT" )
321             || $r->document_root;
322             $incom_root = $r->server_root_relative( $incom_root );
323              
324             my $uri = $r->uri;
325              
326             # Decline to handle this unless the request URI match our prefix
327             return DECLINED unless $uri =~ s!^$prefix/*!!;
328              
329             # Only support GET or POST
330             return NOT_IMPLEMENTED unless $r->method =~ /^(GET|POST)$/;
331              
332             if ( $r->is_main ) {
333             # On the first request, we open the connection to the database
334             # and loads the user session
335             my $rc = db_init( $r );
336             session_init( $r );
337              
338             # To clean DB connection and Session
339             $r->push_handlers( PerlCleanupHandler => \&request_cleanup );
340             }
341              
342             # Determine the handler
343             if ( $uri =~ s!^incom_cookie_check/!! ) {
344             # Check if the session was loaded properly
345             if ( ref $r->pnotes( "INCOM_SESSION") ) {
346             # Cookie test suceeded. Tell browser to refetch
347             # original file
348             $r->pnotes( "INCOM_REDIRECT_TO", $prefix . $uri );
349             $r->push_handlers( PerlHandler => \&redirect_handler );
350             $r->handler( "perl-script" );
351             } else {
352             # Cookie test failed
353             $r->pnotes( "INCOM_ERROR", "no_cookies" );
354             return return_error( $r, HTTP_PRECONDITION_FAILED );
355             }
356             } elsif ( $uri =~ s!^incom_set_lang/([-\w]+)/!! ) {
357             $r->pnotes( "INCOM_NEW_LANG", "$1" );
358             $r->pnotes( "INCOM_REDIRECT_TO", $prefix . $uri );
359              
360             $r->push_handlers( PerlHandler => \&set_lang_handler );
361             $r->handler( "perl-script" );
362              
363             # incom_error magic URL should only be called as a subrequest.
364             } elsif ( (!$r->main) && $uri =~ s!^incom_error/!! ) {
365              
366             $incom_root = $r->dir_config( "INCOM_ERROR_ROOT" ) || $incom_root;
367             $incom_root = $r->server_root_relative( $incom_root );
368              
369             $r->push_handlers( PerlHandler => \&error_handler );
370             $r->handler( "perl-script" );
371              
372             } elsif ( not ref $r->pnotes( "INCOM_SESSION" ) ) {
373              
374             # The user doesn't belong to an existing session
375             $r->push_handlers( PerlHandler => \&new_session_handler );
376             $r->handler( "perl-script" );
377             } else {
378              
379             # Default handler
380             $r->push_handlers( PerlHandler => \&default_handler );
381             $r->handler( "perl-script" );
382             }
383              
384             # Set the filename
385             $uri ||= $index_file;
386              
387             # Handle directory index
388             $uri =~ s!/$!/$index_file!;
389              
390             # Find the properly localized file
391             my $localizer = $r->pnotes( "INCOM_LOCALIZER" );
392             my $file = $localizer->find_localized_file( $incom_root . "/" . $uri );
393              
394             # Set filename of the request
395             $r->filename( $file );
396              
397             # Request should never be cached
398             $r->header_out( 'Pragma', 'no-cache' );
399             $r->header_out( 'Cache-control', 'no-cache' );
400             $r->no_cache(1);
401              
402             # Default content-type
403             $r->content_type( "text/html" );
404              
405             return OK;
406             }
407              
408             # Content handler invoked when the request is not
409             # part of a session.
410             #
411             # It creates a new session. Sets a cookie to it
412             # and redirect the user to resubmit the request
413             # to a rewritten URL.
414             sub new_session_handler {
415             my $r = shift;
416              
417             my %session;
418             eval {
419             tie %session, 'Apache::iNcom::Session', undef,
420             { dbh => $r->pnotes( "INCOM_DBH"),
421             Serialize => $r->dir_config( "INCOM_SESSION_SERIALIZE_ACCESS" ),
422             };
423              
424             bake_session_cookie( $r, $session{_session_id} );
425             };
426             if ($@) {
427             $r->log_error( "error creating session: $@" );
428             return return_error( $r, SERVER_ERROR );
429             }
430              
431             # Tell the browser to repost its request. We will then be
432             # able to check if he has cookie turn on
433             my $prefix = $r->dir_config( "INCOM_URL_PREFIX" ) || "/";
434             my $uri = $r->uri;
435             $uri =~ s!^$prefix/*!${prefix}incom_cookie_check/!;
436              
437             $r->content_type( "text/html" );
438             $r->header_out( Location => $uri );
439              
440             return REDIRECT;
441             }
442              
443             sub redirect_handler {
444             my $r = shift;
445              
446             $r->content_type( "text/html" );
447             $r->header_out( Location => $r->pnotes( "INCOM_REDIRECT_TO" ) );
448              
449             return REDIRECT;
450             }
451              
452             sub set_lang_handler {
453             my $r = shift;
454              
455             my $prefix = $r->dir_config( "INCOM_URL_PREFIX" ) || "/";
456             my $session_domain = $r->dir_config( "INCOM_SESSION_DOMAIN" );
457             my $session_path = $r->dir_config( "INCOM_SESSION_PATH" )
458             || $prefix;
459             my $session_expires = $r->dir_config( "INCOM_SESSION_EXPIRES" );
460              
461             # Create a cookie which has the same lifespan than
462             # the session cookie.
463             my $cookie = new Apache::Cookie( $r,
464             -name => "INCOM_LANGUAGE",
465             -value => $r->pnotes( "INCOM_NEW_LANG" ),
466             -path => $session_path,
467             );
468             $cookie->domain( $session_domain ) if $session_domain;
469             $cookie->expires( $session_expires ) if $session_expires;
470              
471             # Add cookie to outgoing headers
472             $cookie->bake;
473              
474             # Tell the browser to repost its request. The next
475             # request will favorise the new language.
476             $r->content_type( "text/html" );
477             $r->header_out( Location => $r->pnotes( "INCOM_REDIRECT_TO" ) );
478              
479             return REDIRECT;
480             }
481              
482             sub package_name {
483             my $r = shift;
484             my $file = shift;
485              
486             my $host = $r->server->server_hostname;
487             my $root = $r->dir_config( "INCOM_ROOT" ) || $r->document_root;
488             $root = $r->server_root_relative( $root );
489              
490             # Remove document root
491             $file =~ s!^$root/!!;
492              
493             # Remove trailing suffixes of the last component
494             # of the path name
495             $file =~ s!\.[^/]*$!!;
496              
497             # Munge invalid character
498             $file =~ tr/a-zA-Z0-9/_/cs;
499              
500             # Munge invalid character in hostname
501             $host =~ tr/a-zA-Z0-9/_/cs;
502              
503             return "Apache::iNcom::" . $host . "::" . $file;
504             }
505              
506             sub error_handler {
507             my $r = shift;
508              
509             my $filename = $r->filename;
510              
511             unless ( -e $r->finfo ) {
512             $r->log_reason( "nonexistent file", $filename );
513             return NOT_FOUND;
514             }
515              
516             unless ( -f _ ) {
517             $r->log_reason( "not a regular file", $filename );
518             return FORBIDDEN;
519             }
520              
521             unless ( -r _ ) {
522             $r->log_reason( "No permissions to read", $filename );
523             return FORBIDDEN;
524             }
525              
526             # Determine the package name of this error page
527             # Package is Apache::iNcom::basename of the page
528             my $package = package_name( $r, $filename );
529             my $req = new Apache::iNcom::Request( $r, $package );
530             # Play magic
531             $req->setup_aliases;
532              
533             # Send the response
534             my $output;
535             $r->content_type( "text/html" );
536              
537             my $debug = $r->dir_config( "EMBPERL_DEBUG" ) || $ENV{EMBPERL_DEBUG} || 0;
538             my $options = $r->dir_config( "EMBPERL_OPTIONS" ) ||
539             $ENV{EMBPERL_OPTIONS} || 16; # Default = optRawInput
540             # optDisableFormData,optReturnError
541             $options |= 256 | 262144;
542              
543             my $params = {
544             package => $package,
545             output => \$output,
546             inputfile => $filename,
547             req_rec => $r,
548             debug => $debug,
549             options => $options,
550             param => $r->prev->pnotes( "INCOM_HTML_EMBPERL_ERRORS" ),
551             };
552             my $rc = HTML::Embperl::Execute( $params );
553              
554             $req->cleanup_aliases;
555              
556             my $dbh = $r->pnotes( "INCOM_DBH" );
557             if ($rc != OK && $rc != MOVED && $rc != REDIRECT ) {
558             # If there was an error, rollback all changes
559             # to the database
560             eval { $dbh->rollback; };
561             $r->log_error( "error reverting changes to database: $@" ) if $@;
562              
563             $r->log_reason( "error in embperl code", $filename );
564              
565             return $rc;
566             } else {
567             # Commit all changes to the database
568             eval { $dbh->commit; };
569             $r->log_error( "error commiting changes to database: $@" ) if $@;
570             $r->header_out( "Content-Length", length $output );
571             $r->send_http_header;
572              
573             $r->print( $output );
574             return OK;
575             }
576             }
577              
578             sub default_handler {
579             # Create an Apache::Request object for
580             # parsing POST and GET request
581             my $r = new Apache::Request( shift );
582              
583             my $filename = $r->filename;
584              
585             unless ( -e $r->finfo ) {
586             $r->log_reason( "nonexistent file", $filename );
587             return return_error( $r, NOT_FOUND );
588             }
589              
590             unless ( -f _ ) {
591             $r->log_reason( "not a regular file", $filename );
592             return return_error( $r, FORBIDDEN );
593             }
594              
595             unless ( -r _ ) {
596             $r->log_reason( "No permissions to read", $filename );
597             return return_error( $r, FORBIDDEN );
598             }
599              
600             # Read the POST data or the Query stringn
601             my $status = $r->parse;
602             unless ( $status == OK ) {
603             $r->log_reason( "error reading request body: " .
604             $r->notes( "error-notes"), $filename);
605             return return_error( $r, $status );
606             }
607              
608             # Copy the elements into the fdat hash
609             my %fdat = ();
610             my @ffld = ();
611             for my $key ( $r->param ) {
612             # Discard empty fields
613             my @values = grep { $_ ne "" } $r->param( $key );
614             next unless @values;
615             push @ffld, $key;
616              
617             # This is what is expected from HTML::Embperl
618             $fdat{$key} = join "\t", @values;
619             }
620              
621             # Determine the package name of this page
622             # Package is Apache::iNcom::basename of the page
623             my $package = package_name( $r, $filename );
624             my $req = new Apache::iNcom::Request( $r, $package );
625             # Play magic
626             $req->setup_aliases;
627              
628             # Send the response
629             my $output;
630             $r->content_type( "text/html" );
631              
632             my $debug = $r->dir_config( "EMBPERL_DEBUG" ) || $ENV{EMBPERL_DEBUG} || 0;
633             my $options = $r->dir_config( "EMBPERL_OPTIONS" ) ||
634             $ENV{EMBPERL_OPTIONS} || 16; # Default = optRawInput
635             # optDisableFormData,optReturnError
636             $options |= 256 | 262144;
637              
638             my $params = {
639             package => $package,
640             output => \$output,
641             inputfile => $filename,
642             req_rec => $r,
643             errors => [],
644             debug => $debug,
645             options => $options,
646             fdat => \%fdat,
647             ffld => \@ffld,
648             };
649             my $rc = HTML::Embperl::Execute( $params );
650              
651             $req->cleanup_aliases;
652              
653             my $dbh = $r->pnotes( "INCOM_DBH" );
654             if ($rc != OK && $rc != MOVED && $rc != REDIRECT ) {
655             # If there was an error, rollback all changes
656             # to the database
657             eval { $dbh->rollback; };
658             $r->log_error( "error reverting changes to database: $@" ) if $@;
659              
660             $r->log_reason( "error in embperl code", $filename );
661              
662             # Save HTML error messages for the error page
663             $r->pnotes( "INCOM_HTML_EMBPERL_ERRORS", $params->{errors} );
664              
665             return return_error( $r, $rc );
666             } else {
667             # Commit all changes to the database
668             eval { $dbh->commit; };
669             $r->log_error( "error commiting changes to database: $@" ) if $@;
670             $r->header_out( "Content-Length", length $output );
671             $r->send_http_header;
672              
673             $r->print( $output );
674             return OK;
675             }
676             }
677              
678             sub request_cleanup {
679             my $r = shift;
680              
681             my $session = $r->pnotes( "INCOM_SESSION" );
682             if ( $session ) {
683             eval { untie %$session; };
684             $r->log_error( "error untying session: $@" ) if $@;
685             }
686              
687             my $dbh = $r->pnotes( "INCOM_DBH" );
688             if ( $dbh ) {
689             # Delete expired sessions on 5% of the requests
690             if ( rand 100 < 5 ) {
691             eval {
692             my $session_expires =
693             $r->dir_config( "INCOM_SESSION_EXPIRES" );
694             my $offset;
695             if ( $session_expires ) {
696             $offset = offset_calc( $session_expires );
697             } else {
698             $offset = 3600 * 24; # One day
699             }
700             # XXX Is this really portable ????
701             my $time = localtime ( time - $offset);
702             $dbh->do( "DELETE FROM sessions WHERE last_update < '$time'" );
703             $dbh->commit;
704             };
705             $r->log_error( "error removing old sessions: $@" ) if $@;
706             }
707              
708             eval {
709             $dbh->disconnect unless $dbh;
710             };
711             $r->log_error( "error closing connection to database: $@" ) if $@;
712             }
713              
714             return OK;
715             }
716              
717             1;
718             __END__