/[pub]/suikawiki/script/lib/suikawiki.pl
Suika

Diff of /suikawiki/script/lib/suikawiki.pl

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 1.21 by wakaba, Fri Dec 26 06:51:47 2003 UTC revision 1.22 by wakaba, Fri Dec 26 09:40:28 2003 UTC
# Line 1  Line 1 
1    =head1 NAME
2    
3    suikawiki.pl - SuikaWiki Driver for HTTP CGI Script
4    
5    =cut
6    
7  use strict;  use strict;
8  package main;  package main;
 binmode STDOUT; binmode STDIN;  
9  our $WIKI;  our $WIKI;
10    package wiki::driver::http;
11    ## -- Version of WikiDriver --
12    our $VERSION = do{my @r=(q$Revision$=~/\d+/g);sprintf "%d."."%02d" x $#r,@r};
13    $WIKI->{implementation_version} = 'hcs'.$VERSION;
14        
15    ## -- Dying Message as HTTP Response --
16      require SuikaWiki::Output::CGICarp;
17      $SuikaWiki::Output::CGICarp::CUSTOM_REASON_TEXT
18          = 'Internal WikiEngine Error';
19      CGI::Carp::set_message (sub {
20          my $msg = shift;
21          #$msg =~ s/&/&/g;
22          #$msg =~ s/</&lt;/g;
23          my $wiki_name_version = $WIKI->{implementation_name} .'/'. $WIKI->version;
24          my $trace = Carp::longmess ();
25          for ($trace, $wiki_name_version)
26            { s/&/&amp;/g; s/</&lt;/g;
27              s/([^\x20-\x7E])/sprintf '&#x%02X;', ord $1/ge; };
28          print STDOUT <<EOH;
29    <!DOCTYPE html SYSTEM>
30    <title>500 Internal WikiEngine Error</title>
31    <h1>Internal WikiEngine Error</h1>
32    <p>$msg</p>
33    <p>$trace</p>
34    <address>$wiki_name_version</address>
35    EOH
36      });
37    
38    ## -- Required Modules --
39    use SuikaWiki::DB::Util::Error;
40  require SuikaWiki::Plugin;  require SuikaWiki::Plugin;
 our %embed_command = ( # Map, main  
         form    => qr/\[\[\#form(?:\(([A-Za-z0-9-]+)\))?:'((?:[^'\\]|\\.)*)':'((?:[^'\\]|\\.)*)'(?::'((?:[^'\\]|\\.)*)')?\]\]/,  
 );  
 our $database = bless {}, 'wiki::dummy';  
41  require SuikaWiki::Name::Space;  require SuikaWiki::Name::Space;
42  require SuikaWiki::SrcFormat;  require SuikaWiki::SrcFormat;
43    
44  # [to be obsolete]  ## -- Transitional Functions --
 sub do_comment {  
     my ($content) = $main::database{$main::form{mypage}};  
     my $default_name;   ## this code is not strict.  
     $default_name = $1 if $content =~ /default-name="([^"]+)"/;  
     my @time = gmtime (time);  
     my $datestr = sprintf '[WEAK[%04d-%02d-%02d %02d:%02d:%02d +00:00]]', $time[5]+1900,$time[4]+1,@time[3,2,1,0];  
     my $namestr = $main::form{myname} || $default_name || &Resource('WikiForm:WikiComment:DefaultName');  
     ($namestr = '', $datestr = '') if $main::form{myname} eq 'nodate';  
     if ($namestr =~ /^(?:>>)?[0-9]/) {  
       $namestr = qq( ''$namestr'': );  
     } elsif (length $namestr) {  
       $namestr = qq( ''[[$namestr]]'': );  
     }  
     my $anchor = &get_new_anchor_index ($content);  
     my $i = 1;  my $o = 0;  
     $content =~ s{(\[\[\#r?comment\]\])}{  
       my $embed = $1;  
       if ($i == $main::form{comment_index}) {  
         if ($embed ne '[[#rcomment]]') {  
           $embed = "- [$anchor] $datestr$namestr$main::form{mymsg}\n$embed";  $o = 1;  
         } else {  
           $embed .= "\n- [$anchor] $datestr$namestr$main::form{mymsg}";  $o = 1;  
         }  
       }  
       $i++; $embed;  
     }ge;  
     unless ($o) {  
       $content = "#?SuikaWiki/0.9\n\n" unless $content;  
       $content .= "\n" unless $content =~ /\n$/s;  
       $content .= "- [$anchor] $datestr$namestr$main::form{mymsg}\n";  
     }  
     $main::form{__comment_anchor_index} = $anchor;  
     if ($main::form{mymsg} || $main::form{myname}) {  
         $main::form{mymsg} = $content;  
         $main::form{mytouch} = 'on';  
         &do_write;  
     } else {    ## Don't write  
         #$main::form{mycmd} = 'default';  
         #&do_view;  
       die "No comment specified";  
     }  
 }  
   
 # [move to SuikaWiki::Plugin::WikiForm]  
 sub get_new_anchor_index ($) {  
     my $content = shift;  
     my $anchor = 0;  
     $content =~ s/^(?:[-=]+\s*)?\[([0-9]+)\]/$anchor = $1 if $1 > $anchor; $&/mge;  
     $anchor + 1;  
 }  
   
 # [move to SuikaWiki::Plugin::WikiForm]  
 sub do_wikiform {  
     my $content = $main::database{$main::form{mypage}};  
     my $anchor = &get_new_anchor_index ($content);  
     my $write = 0;  
     my $i = 1;  
     $content =~ s{$embed_command{form}}{  
         my ($embed, $wfname, $template, $option) = ($&, $1, $3, $4);  
         if (($wfname && $wfname eq $main::form{wikiform_targetform})  
             || $i == $main::form{wikiform_index}) {  
             $template =~ s/\\([\\'])/$1/g;  
             $option =~ s/\\([\\'])/$1/g;  
             my $param = bless {depth=>10}, 'SuikaWiki::Plugin';  
             $param->{page} = $main::form{mypage};  
             $param->{form_index} = $i;  
             $param->{form_name} = $wfname;  
             $param->{anchor_index} = $anchor;  
             $param->{argv} = \%main::form;  
             $param->{default_name} = $1 if $content =~ /default-name="([^"]+)"/;  
             $param->{default_name} ||= &Resource('WikiForm:WikiComment:DefaultName');  
             SuikaWiki::Plugin->formatter ('form_option')->replace ($option, $param);  
             my $t = 1;  
             for (keys %{$param->{require}||{}}) {  
                 (undef $t, last) unless length $param->{argv}->{'wikiform__'.$_};  
             }  
             $t = SuikaWiki::Plugin->formatter ('form_template')->replace ($template, $param) if $t;  
             if (length $t) {  
                 if ($param->{output}->{reverse}) {  
                     $embed .= "\n" . $t;  
                 } else {  
                     $embed = $t . "\n" . $embed;  
                 }  
                 $write = 1;  
                 $main::form{__comment_anchor_index} = $anchor  
                   if $param->{anchor_index_};  ## $anchor is used!  
             }  
             $main::form{__wikiform_anchor_index} = $i;  
             undef $main::form{wikiform_targetform};  ## Make sure never to match  
             undef $main::form{wikiform_index};       ## with WikiForm in rest of page!  
         }  
         $i++; $embed;  
     }ge;  
     unless ($write) {  
       #$content = "#?SuikaWiki/0.9\n\n" unless $content;  
       #$content .= "\n" unless $content =~ /\n$/s;  
       #  
     }  
     if ($write) {  
         $main::form{mymsg} = $content;  
         $main::form{mytouch} = 'on';  
         &do_write;  
     } else {    ## Don't write!  
         #$main::form{mycmd} = 'default';  
         #&do_view;  
       die "No content specified";  
     }  
 }  
45    
46  # [to be obsolete] ->Message::MIME::Charset  # [to be obsolete] ->Message::MIME::Charset
47  sub code_convert {  sub main::code_convert {
48    require Jcode;    require Jcode;
49    my ($contentref, $code, $srccode) = @_;    my ($contentref, $code, $srccode) = @_;
50    $code ||= $WIKI->{config}->{charset}->{internal};    $code ||= $WIKI->{config}->{charset}->{internal};
# Line 144  sub code_convert { Line 67  sub code_convert {
67  }  }
68    
69  # [to be obsolete] ->Message::Field::Date : Map  # [to be obsolete] ->Message::Field::Date : Map
70  sub _rfc3339_date ($) {  sub main::_rfc3339_date ($) {
71    my @time = gmtime (shift);    my @time = gmtime (shift);
72    sprintf '%04d-%02d-%02dT%02d:%02d:%02d+00:00', $time[5]+1900,$time[4]+1,@time[3,2,1,0];    sprintf '%04d-%02d-%02dT%02d:%02d:%02d+00:00', $time[5]+1900,$time[4]+1,@time[3,2,1,0];
73  }  }
74    
 # [obsolete] ->SuikaWiki::SrcFormat : SuikaWiki09 plugin  
 sub convert_format ($$$;%) {  
   my ($content, $d => $t, %option) = @_;  
   my $f = SuikaWiki::Plugin->format_converter ($d => $t);  
   if (ref $f) {  
     $option{content} = $content;  
     $option{from} = $d;  
     $option{to} = $t;  
     &$f ({}, bless (\%option, 'SuikaWiki::Plugin'));  
   } elsif ($option{-error_no_return}) {  
     return undef;  
   } elsif ($t =~ /HTML|xml/) {  
     if (length $content) {  
 my $NS_XHTML1 = 'http://www.w3.org/1999/xhtml';  
       my $r = Message::Markup::XML::Node->new  
         (type => '#element', namespace_uri => $NS_XHTML1, local_name => 'pre');  
       $r->append_text ($content);  
       return $r;  
     } else {  
       return '';  
     }  
   } else {  
     $content;  
   }  
 }  
   
 package SuikaWiki::Plugin;  
 ## Obsolete  
 sub magic_and_content ($$) {  
   my ($magic, $page) = ('', $_[1]);  
   $magic = $1 if $page =~ s!^((?:\#\?|/\*|<\?)[^\x02\x0A\x0D]+)[\x02\x0A\x0D]+!!s;  
   ($magic, $page);  
 }  
   
75  ## Obsolete  ## Obsolete
76  sub get_data ($$$$;%) {  sub SuikaWiki::Plugin::get_data ($$$$;%) {
77      my ($self, $prop, $key, %opt) = @_;      my ($self, $prop, $key, %opt) = @_;
78      ## TODO: common interface to WikiDB      ## TODO: common interface to WikiDB
79      ## TODO: error recovering      ## TODO: error recovering
80      $main::WIKI->{db}->get ($prop => $key);      $main::WIKI->{db}->get ($prop => $key);
81  }  }
82    
83  package wiki::transitional::uri_param;  ## -- Initializing WikiPlugin --    
 require Tie::Hash;  
 our @ISA = 'Tie::Hash';  
84    
85  sub TIEHASH ($@) {    $WIKI->init_plugin;     ## WikiPlugin manager
   bless {http => $_[1]}, $_[0];  
 }  
86    
87  sub FETCH ($$) {  ## -- Initializing WikiView --
   my ($self, $key) = @_;  
   exists $self->{val}->{$key} ?  
     $self->{val}->{$key}:  
     $self->{http}->parameter ($key);  
 }  
88    
89  sub STORE ($$$) {    $WIKI->init_view;       ## WikiView manager
90    my ($self, $key, $val) = @_;    $WIKI->{view}->register_common_modes;
91    $self->{val}->{$key} = $val;    
92  }    ## WikiView manager error handler
93      push @{$WIKI->{event}->{view_error}}, sub {
94        my ($wiki, $event) = @_;
95        SuikaWiki::Plugin->module_package ('Error')
96                         ->report_error_simple
97                             ($wiki, WikiView => $event->{error}->text,
98                              -trace => 1)
99          if $event->{error}->{-def}->{level} eq 'fatal'
100          or $wiki->{config}->{debug}->{view};
101        unless ($event->{error}->{-def}->{level} eq 'fatal'
102             or $event->{error}->{-def}->{level} eq 'stop') {
103          $event->{cancel} = 1;
104        }
105      };
106      
107      ## "view_in_mode" method definition
108      push @{$WIKI->{event}->{view_in_mode}}, sub {
109        my ($wiki, $opt) = @_;
110        my $arg = {condition => {mode => $opt->{mode} || '-error',
111                                 output => 'http-cgi',
112                                 http_method => $opt->{method} || 'GET'}};
113        my $viewobj = $wiki->{view}->instantiate ($opt->{mode} || '-error', $arg);
114        if (ref $viewobj) {
115          $viewobj->main ($arg);
116        } elsif ($opt->{mode} ne '-error') {
117          report SuikaWiki::View::Implementation::error
118            -type => 'WARN_VIEW_NOT_DEFINED', condition => $arg->{condition},
119            -object => $wiki->{view}, method => 'view_in_mode';
120          $wiki->view_in_mode (mode => '-error', method => 'GET');
121            ## TODO: cache control for non-GET
122        } else {
123          die "Some error occured.  Additionally, error reporting mode not defined";
124        }
125      };
126      
127      ## WikiView formatting template error handler
128      $WIKI->{config}->{catch}->{formatter_view}
129        = catch Message::Util::Formatter::error with {
130        my $err = shift;
131        my $wiki = $err->{-option}->{param}->{wiki};
132        SuikaWiki::Plugin->module_package ('Error')
133                         ->reporting_formatting_template_error ($err, $wiki);
134        $wiki->view_in_mode (mode => '-error', method => 'GET');
135        throw SuikaWiki::View::Implementation::error
136          -type => 'ERROR_REPORTED';
137      };
138      
139      ## WikiView formatting template error handler (occured in "-error" mode)
140      $WIKI->{config}->{catch}->{formatter_view_error}
141        = catch Message::Util::Formatter::error with {
142        my $err = shift;
143        my $wiki = $err->{-option}->{param}->{wiki};
144        SuikaWiki::Plugin->module_package ('Error')
145                         ->reporting_formatting_template_error ($err, $wiki);
146        $wiki->view_in_mode (mode => '-error-error', method => 'GET');
147        throw SuikaWiki::View::Implementation::error
148          -type => 'ERROR_REPORTED';
149      };
150    
151  sub DELETE ($$) {  ## -- WikiDatabase Error Reporting --
152    my ($self, $key) = @_;  {
153    $self->{val}->{$key} = undef;    my $error_report = sub {
154        my ($wiki, $err) = @_;
155          my $report = ($err->{-def}->{level} eq 'fatal' or
156                        $err->{-def}->{level} eq 'stop' or
157                        $wiki->{config}->{debug}->{db}) ? 1 : 0;
158          if ($report and $wiki->{config}->{path_to}->{db__content__error_log}) {
159            my $err_msg = caller (1).($err->{-method}? '->'.$err->{-method}: '').': '
160                        .(defined $err->{-file}? $err->{-file} . ': ' : '')  
161                        .(defined $err->{-prop}? $err->{-prop} . ': ' : '')
162                        .(defined $err->{-key}? join ('//', @{$err->{-key}}).': ':'')
163                        . $err->text;
164            open LOG, '>>', $wiki->{config}->{path_to}->{db__content__error_log};
165              print LOG scalar (gmtime), " @{[$$]} {$err->{-def}->{level}}: ",
166                        $err_msg, "\n";
167            close LOG;
168          }
169          SuikaWiki::Plugin->module_package ('WikiDB')
170                           ->reporting_error ($err, $wiki) if $report;
171          if ($err->{-def}->{level} eq 'fatal' or $err->{-def}->{level} eq 'stop') {
172            $wiki->view_in_mode (mode => '-wdb--fatal-error');
173            throw SuikaWiki::DB::Util::Error -type => 'ERROR_REPORTED';
174          }
175      };
176      unshift @{$WIKI->{event}->{database_loaded}}, sub {
177        my $wiki = shift;
178        unshift @{$wiki->{db}->{event}->{error}}, sub {
179          my ($db, $event) = @_;
180          $error_report->($wiki, $event->{error});
181          if ($event->{error}->{-type} eq 'INFO_DB_PROP_OPENED') {
182            unshift @{$db->{prop}->{$event->{error}->{prop}}->{-db}
183                         ->{event}->{error}}, sub {
184              my ($db, $event) = @_;
185              $error_report->($wiki, $event->{error});
186            };
187          }
188        }; # database error
189      }; # database_loaded
190  }  }
191      
192    ## -- Misc. Error Reporting --
193    
194  sub EXISTS ($$) {    if ($WIKI->{config}->{debug}->{general}) {
195    my ($self, $key) = @_;      $main::SIG{__WARN__} = sub {
196    exists $self->{val}->{$key} ?        push @{$WIKI->{var}->{error}||=[]}, {
197      1:          description => Message::Markup::XML::Node->new
198      defined $self->{http}->parameter ($key);                                           (type => '#text',
199  }                                            value => $_[0]),
200          };
201        };
202      }
203    
204  package main;  ## -- Initializing $wiki->{var} (Declaration) --
 our $VERSION = do{my @r=(q$Revision$=~/\d+/g);sprintf "%d."."%02d" x $#r,@r};  
205    
     $WIKI->{implementation_version} = 'hcs'.$VERSION;  
       
     ## Error output  
     require SuikaWiki::Output::CGICarp;  
     $SuikaWiki::Output::CGICarp::CUSTOM_REASON_TEXT  
       = 'Internal WikiEngine Error';  
     CGI::Carp::set_message (sub {  
       my $msg = shift;  
       #$msg =~ s/&/&amp;/g;  
       #$msg =~ s/</&lt;/g;  
       my $wiki_name_version = $WIKI->{implementation_name} .'/'. $WIKI->version;  
       for ($wiki_name_version) { s/&/&amp;/g; s/</&lt;/g;  
                                  s/([^\x20-\x7E])/sprintf '&#x%02X;',  
                                                           ord $1/g; };  
       print STDOUT <<EOH;  
 <!DOCTYPE html SYSTEM>  
 <title>500 Internal WikiEngine Error</title>  
 <h1>Internal WikiEngine Error</h1>  
 <p>$msg</p>  
 <address>$wiki_name_version</address>  
 EOH  
     });  
       
206    push @{$WIKI->{event}->{setting_initial_variables}}, sub {    push @{$WIKI->{event}->{setting_initial_variables}}, sub {
207      my $wiki = shift;      my $wiki = shift;
208      $wiki->{var}->{db}->{read_only}->{'#default'} = 1;      $wiki->{var}->{db}->{read_only}->{'#default'} = 1;
# Line 307  EOH Line 261  EOH
261      $wiki->{var}->{mode} = $mode;      $wiki->{var}->{mode} = $mode;
262    };    };
263    
264  {  ## -- Initializing $wiki->{var} (Actual) --
   my $error_report = sub {  
     my ($wiki, $err) = @_;  
       my $report = ($err->{-def}->{level} eq 'fatal' or  
                     $err->{-def}->{level} eq 'stop' or  
                     $wiki->{config}->{debug}->{db}) ? 1 : 0;  
       if ($report and $wiki->{config}->{path_to}->{db__content__error_log}) {  
         my $err_msg = caller (1).($err->{-method}? '->'.$err->{-method}: '').': '  
                     .(defined $err->{-file}? $err->{-file} . ': ' : '')    
                     .(defined $err->{-prop}? $err->{-prop} . ': ' : '')  
                     .(defined $err->{-key}? join ('//', @{$err->{-key}}).': ':'')  
                     . $err->text;  
         open LOG, '>>', $wiki->{config}->{path_to}->{db__content__error_log};  
           print LOG scalar (gmtime), " @{[$$]} {$err->{-def}->{level}}: ",  
                     $err_msg, "\n";  
         close LOG;  
       }  
       SuikaWiki::Plugin->module_package ('WikiDB')  
                        ->reporting_error ($err, $wiki) if $report;  
       if ($err->{-def}->{level} eq 'fatal' or $err->{-def}->{level} eq 'stop') {  
         $wiki->view_in_mode (mode => '-wdb--fatal-error');  
         throw SuikaWiki::DB::Util::Error -type => 'ERROR_REPORTED';  
       }  
   };  
   unshift @{$WIKI->{event}->{database_loaded}}, sub {  
     my $wiki = shift;  
     unshift @{$wiki->{db}->{event}->{error}}, sub {  
       my ($db, $event) = @_;  
       $error_report->($wiki, $event->{error});  
       if ($event->{error}->{-type} eq 'INFO_DB_PROP_OPENED') {  
         unshift @{$db->{prop}->{$event->{error}->{prop}}->{-db}  
                      ->{event}->{error}}, sub {  
           my ($db, $event) = @_;  
           $error_report->($wiki, $event->{error});  
         };  
       }  
     }; # database error  
   }; # database_loaded  
 }  
265        
266    if ($WIKI->{config}->{debug}->{general}) {    $WIKI->init_variables;  ## Per-session variables
267      $main::SIG{__WARN__} = sub {    
268      push @{$WIKI->{var}->{error}||=[]}, {  ## -- Instantiating WikiView --
         description => Message::Markup::XML::Node->new  
                                          (type => '#text',  
                                           value => $_[0]),  
       };  
     };  
   }  
       
   push @{$WIKI->{event}->{view_in_mode}}, sub {  
     my ($wiki, $opt) = @_;  
     my $arg = {condition => {mode => $opt->{mode} || '-error',  
                              output => 'http-cgi',  
                              http_method => $opt->{method} || 'GET'}};  
     my $viewobj = $wiki->{view}->instantiate ($opt->{mode} || '-error', $arg);  
     if (ref $viewobj) {  
       $viewobj->main ($arg);  
     } elsif ($opt->{mode} ne '-error') {  
       $wiki->view_in_mode (mode => '-error', method => 'GET');  
         ## TODO: cache control for non-GET  
     } else {  
       die "Some error happens.  Additionally, error reporting mode not defined";  
     }  
   };  
   
   $WIKI->init_plugin;     ## WikiPlugin manager  
   $WIKI->init_view;       ## WikiView manager  
   $WIKI->{view}->register_common_modes;  
269    
 ## Error handlers  
 use SuikaWiki::DB::Util::Error;  
 my $catcher = catch SuikaWiki::DB::Util::Error with {  
   my $err = shift;  
   exit if $err->{-type} eq 'ERROR_REPORTED';  
   $err->throw;  
 } catch SuikaWiki::View::Implementation::error with {  
   my $err = shift;  
   exit if $err->{-type} eq 'ERROR_REPORTED';  
   $err->throw;  
 };  
   
 $WIKI->{config}->{catch}->{formatter_view}  
   = catch Message::Util::Formatter::error with {  
   my $err = shift;  
   my $wiki = $err->{-option}->{param}->{wiki};  
   SuikaWiki::Plugin->module_package ('Error')  
                    ->reporting_formatting_template_error ($err, $wiki);  
   $wiki->view_in_mode (mode => '-error', method => 'GET');  
   throw SuikaWiki::View::Implementation::error  
     -type => 'ERROR_REPORTED';  
 };  
   
 $WIKI->{config}->{catch}->{formatter_view_error}  
   = catch Message::Util::Formatter::error with {  
   my $err = shift;  
   my $wiki = $err->{-option}->{param}->{wiki};  
   SuikaWiki::Plugin->module_package ('Error')  
                    ->reporting_formatting_template_error ($err, $wiki);  
   $wiki->view_in_mode (mode => '-error-error', method => 'GET');  
   throw SuikaWiki::View::Implementation::error  
     -type => 'ERROR_REPORTED';  
 };  
   
 ## Main  
 $WIKI->init_variables;  ## Per-session variables  
 try {  
   $WIKI->view_in_mode  
     (mode => $WIKI->{var}->{mode},  
      method => $WIKI->{input}->meta_variable ('REQUEST_METHOD'));  
 } $catcher;  
 exit;  
 END {  
270    try {    try {
271      $WIKI->exit;      $WIKI->view_in_mode
272    } $catcher;        (mode => $WIKI->{var}->{mode},
273  };         method => $WIKI->{input}->meta_variable ('REQUEST_METHOD'));
274      } catch SuikaWiki::DB::Util::Error with {
275  =head1 NAME      my $err = shift;
276        $err->throw unless $err->{-type} eq 'ERROR_REPORTED';
277      } catch SuikaWiki::View::Implementation::error with {
278        my $err = shift;
279        $err->throw unless $err->{-type} eq 'ERROR_REPORTED';
280      } finally {
281        $WIKI->close_input;
282        $WIKI->close_db;
283      };
284      exit;
285    
286  lib/suikawiki.pl --- SuikaWiki : WikiEngine driver for HTTP CGI  ## -- Terminating WikiEngine --
287    END {
288      $WIKI->exit;
289    }
290    
291  =head1 LICENSE  =head1 LICENSE
292    

Legend:
Removed from v.1.21  
changed lines
  Added in v.1.22

admin@suikawiki.org
ViewVC Help
Powered by ViewVC 1.1.24