/[suikacvs]/messaging/manakai/lib/Message/Markup/XML/EntityManager.pm
Suika

Contents of /messaging/manakai/lib/Message/Markup/XML/EntityManager.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.5 - (hide annotations) (download)
Mon Jun 30 11:06:28 2003 UTC (23 years, 1 month ago) by wakaba
Branch: MAIN
Changes since 1.4: +18 -12 lines
_parse_start_tag: New

1 wakaba 1.1
2     =head1 NAME
3    
4     SuikaWiki::Markup::XML::EntityManager --- SuikaWiki XML: Entity manager
5    
6     =head1 DESCRIPTION
7    
8 wakaba 1.3 The entity manager is part of XML system to retrive internal/external entity in
9     this implementation. In addition to it, this module provides some procedures
10     to validate public identifier or system identifier and to get one of/list of
11     markup declarations for element, attribute list or notation from the DTD.
12    
13     This module have customizable interface to get external resource.
14     Defining the additional or replacing function for the "external identifier(s)
15     to entity value convertion", more flexible or secure entity resolving can be
16     implemented. (For detail, see examples below.)
17    
18 wakaba 1.1 This module is part of SuikaWiki XML support.
19    
20     =cut
21    
22     package SuikaWiki::Markup::XML::EntityManager;
23     use strict;
24 wakaba 1.4 our $VERSION = do{my @r=(q$Revision: 1.3 $=~/\d+/g);sprintf "%d."."%02d" x $#r,@r};
25 wakaba 1.5 our %NS;
26     *NS = \%SuikaWiki::Markup::XML::NS;
27 wakaba 1.1
28 wakaba 1.5 # $class->new ($yourself)
29 wakaba 1.1 sub new ($$) {
30 wakaba 1.5 my $self = bless {node => $_[1]}, $_[0];
31     for (@{$self->{node}->{node}}) {
32 wakaba 1.1 if ($_->{type} eq '#declaration' && $_->{namespace_uri} eq $NS{SGML}.'doctype') {
33     $self->{doctype} = $_;
34     last;
35     }
36     }
37     $self;
38     }
39    
40 wakaba 1.5 sub set_doctype_node ($$) { $_[0]->{doctype} = $_[1] }
41 wakaba 1.4
42     sub is_declared_entity ($$;%) {
43     my ($self, $name, %o) = @_;
44     if (ref $name) {
45     $o{namespace_uri} ||= $name->{namespace_uri};
46     $name = $name->{local_name};
47     } else {
48     $o{namespace_uri} ||= $NS{SGML}.'entity';
49     }
50     $self->{cache}->{is_declared_entity}->{$o{namespace_uri}} = {} if $o{clear_cache};
51     $self->{cache}->{is_declared_entity}->{$o{namespace_uri}}->{$name} = $o{set_value}
52     if defined $o{set_value};
53     unless (defined $self->{cache}->{is_declared_entity}->{$o{namespace_uri}}->{$name}) {
54     if ($o{seek}) {
55     $self->{cache}->{is_declared_entity}->{$o{namespace_uri}}->{$name}
56     = $self->get_entity ($name, %o) ? 1 : 0;
57     }
58     }
59     $self->{cache}->{is_declared_entity}->{$o{namespace_uri}}->{$name};
60     }
61    
62     sub get_entity ($$;%) {
63 wakaba 1.1 my ($self, $name, %o) = @_;
64     if (ref $name) {
65     $o{namespace_uri} ||= $name->{namespace_uri};
66     $name = $name->{local_name};
67     } else {
68     $o{namespace_uri} ||= $NS{SGML}.'entity';
69     }
70 wakaba 1.2 if (!$o{dont_use_predefined_entities}
71 wakaba 1.3 && ($o{namespace_uri} eq $NS{SGML}.'entity')) { ## General entity
72 wakaba 1.2 my $predec = {
73     amp => '&',
74     apos => ''',
75     gt => '>',
76     lt => '<',
77     quot => '"',
78     }->{$name};
79     if ($predec) {
80     for (SuikaWiki::Markup::XML->new (type => '#declaration',
81     namespace_uri => $NS{SGML}.'entity')) {
82     $_->set_attribute ('value')->append_new_node (type => '#xml', value => $predec);
83     return $_;
84     }
85 wakaba 1.1 }
86     }
87 wakaba 1.4 $self->{cache}->{entity_declaration}->{$o{namespace_uri}} = {} if $o{clear_cache};
88     my $e = $self->{cache}->{entity_declaration}->{$o{namespace_uri}}->{$name};
89 wakaba 1.5 return $e if ref $e;
90     return undef unless ref $self->{doctype};
91 wakaba 1.4 $e = $self->_get_entity ($name, $self->{doctype}->{node}, \%o);
92     if (ref $e) {
93     $self->{cache}->{entity_declaration}->{$o{namespace_uri}}->{$name} = $e;
94     return $e;
95     }
96     my $xsub = $self->{doctype}->get_attribute ('external-subset');
97     if (ref $xsub) {
98     $e = $self->_get_entity ($name, $xsub->{node}, \%o);
99     if (ref $e) {
100     $self->{cache}->{entity_declaration}->{$o{namespace_uri}}->{$name} = $e;
101     return $e;
102     }
103     }
104     return undef;
105 wakaba 1.1 }
106     sub _get_entity ($$$$) {
107     my ($self, $name, $nodes, $o) = @_;
108     return undef unless ref $nodes;
109     for (@$nodes) {
110 wakaba 1.4 next if $_->{flag}->{smxp__non_processed_declaration};
111 wakaba 1.1 if ($_->{type} eq '#declaration' && $_->{namespace_uri} eq $o->{namespace_uri}
112     && $_->{local_name} eq $name) {
113     return $_;
114     } elsif ($_->{type} eq '#reference') {
115     my $e = $self->_get_entity ($name, $_->{node}, $o);
116     return $e if ref $e;
117 wakaba 1.4 } elsif ($_->{type} eq '#section'
118     && ($_->get_attribute ('status', make_new_node => 1)->inner_text||'INCLUDE')
119     eq 'INCLUDE') {
120     my $e = $self->_get_entity ($name, $_->{node}, $o);
121     return $e if ref $e;
122 wakaba 1.1 }
123     }
124     return undef;
125     }
126    
127 wakaba 1.2 # DOM's get*By*
128     sub get_entities ($$%) {
129     my ($self, $l, %o) = @_;
130 wakaba 1.4 $o{namespace_uri} = $NS{SGML}.'entity' unless defined $o{namespace_uri};
131     $o{type} ||= '#declaration';
132     $o{parent_node} ||= $self->{doctype};
133     $self->_get_entities ($l, $o{parent_node}->{node}, \%o);
134 wakaba 1.2 }
135     sub _get_entities ($$$$) {
136     my ($self, $l, $nodes, $o) = @_;
137     return undef unless ref $nodes;
138     for (@$nodes) {
139 wakaba 1.4 next if $_->{flag}->{smxp__non_processed_declaration};
140     if (($_->{type} eq $o->{type}) && ($_->{namespace_uri} eq $o->{namespace_uri})) {
141 wakaba 1.2 push @$l, $_;
142     } elsif ($_->{type} eq '#reference') {
143     $self->_get_entities ($l, $_->{node}, $o);
144 wakaba 1.4 } elsif ($_->{type} eq '#section'
145     && ($_->get_attribute ('status', make_new_node => 1)->inner_text||'INCLUDE')
146     eq 'INCLUDE') {
147     $self->_get_entities ($l, $_->{node}, $o);
148     } elsif ($_->{type} eq '#attribute' && $_->{local_name} eq 'external-subset') {
149     $self->_get_entities ($l, $_->{node}, $o);
150 wakaba 1.2 }
151     }
152     }
153    
154 wakaba 1.4 ## TODO: uri based recursion
155 wakaba 1.3 sub get_external_entity ($$$$) {
156     my ($self, $parser, $decl, $o) = @_;
157 wakaba 1.4 my $declns = $decl->namespace_uri;
158     my $name = $declns eq $NS{SGML}.'doctype' ?
159     $decl->get_attribute ('qname', make_new_node => 1)->inner_text :
160     $decl->local_name;
161     my $p = $self->{external_entity_cache}->{$declns}->{$name};
162 wakaba 1.3 if ($name && !$p) {
163 wakaba 1.4 $p = {name => $name}; $self->{external_entity_cache}->{$declns}->{$name} = $p;
164 wakaba 1.3 for (qw/PUBLIC SYSTEM NDATA/) {
165     $p->{$_} = $decl->get_attribute ($_);
166     $p->{$_} = ref $p->{$_} ? $p->{$_}->inner_text : undef;
167     }
168     $p->{uri} = $decl->resolve_relative_uri ($p->{SYSTEM}, use_references_base_uri => 1);
169     }
170     for ($o) {
171     $_->{entity_type} ||= 'external_parsed_entity';
172     $_->{uri} = $p->{uri}; $_->{line} = 0; $_->{pos} = 0;
173     }
174     if ($name && !$p->{__flag}) {
175     my $resolver = $self->{node}->flag ('smxe__uri_resolver');
176     if (ref $resolver) {
177     $resolver = &$resolver ($self, $parser, $decl, $p, $o); ## If returned false,
178     $self->default_uri_resolver ($parser, $decl, $p, $o) if $resolver; ## don't call this.
179     } else {
180     $self->default_uri_resolver ($parser, $decl, $p, $o);
181     }
182     $p->{__flag} = 1;
183     }
184     $p;
185     }
186    
187     =pod example
188    
189     my $parser = SuikaWiki::Markup::XML::Parser->new (flag => {smxe__uri_resolver => sub {
190     my ($self, $decl, $p) = @_;
191     @@ $p->{SYSTEM} =~ s///g @@
192     return 1;
193     }});
194    
195     =cut
196    
197     sub default_uri_resolver ($$$) {
198     my ($self, $parser, $decl, $p, $o) = @_;
199     require LWP::UserAgent;
200     my $ua = LWP::UserAgent->new;
201     $ua->agent ('"SuikaWiki::Markup::XML::EntityManager"/'.$VERSION);
202     ## TODO: use Message::Field::UA
203     my $req = HTTP::Request->new (GET => $p->{uri});
204     my $res = $ua->request ($req);
205     if ($res->is_success) {
206     ## TODO: use Message::Entity for more intelligent/strict parsing:-)
207     $p->{base_uri} = $res->base; ## See Content-Base: and Content-Location: (and HTML:BASE)
208     $p->{uri} = $res->request->uri; $o->{uri} = $p->{uri}; ## Redirect support
209     ## Check media type
210     my $CT = $res->header ('Content-Type');
211     my $ct = lc $res->content_type;
212     $self->_check_media_type ($o, $ct);
213     $p->{text} = $res->content;
214     #$p->{text} .= "<!--$p->{uri}-->"; ## DEBUG: base URI
215     ## Charset/encoding convertion
216     my $encoding;
217     if ($CT =~ /charset\s*=\s*"?([^"]+)"?/i) { ## BUG: This check is not strict
218     $encoding = lc $1;
219     } else { ## No charset parameter
220     if ($p->{uri}->scheme eq 'file') {
221     ## Protocol does not provide charset information
222     } elsif (lc (substr ($ct, 0, 5)) eq 'text/') { ## BUG: This check is not strict
223     $encoding = 'us-ascii'; ## See RFC 3023
224     $self->_raise_error ($o, type => 'WARN_NO_CHARSET_PARAM_FOR_TEXT', t => $ct);
225     } else {
226     ## BUG: Warn even if the media type does not have the charset parameter
227     $self->_raise_error ($o, type => 'WARN_NO_CHARSET_PARAM', t => $ct);
228     }
229     }
230     unless ($encoding) {
231     $encoding = $self->_guess_entity_encoding ($p->{text}, $o) || 'utf-8';
232     }
233     #print "<$p->{uri}>: encode is {$encoding}\n"; ## DEBUG: Detected encoding
234     if ($encoding) {
235     require Encode;
236     eval q{$p->{text} = Encode::decode ($encoding, $p->{text}); 1}
237     or $self->_raise_error ($o, type => 'FATAL_ERR_DECODE_IMPL_ERR', t => $@);
238     } else {
239     #$self->_raise_error ($o, type => 'WARN_NO_EXPLICIT_ENCODING_INFO');
240     }
241     ## parse and remove xml declaration
242     $p->{text_declaration} = ref ($decl)->new (type => '#pi', local_name => 'xml');
243     $parser->_parse_xml_or_text_declaration ($p->{text_declaration}, \$p->{text}, $o);
244     } else {
245     $p->{error}->{no_data} = 1;
246     $p->{error}->{reason_text} = $res->status_line;
247     }
248     }
249    
250 wakaba 1.4 sub is_standalone_document ($) {
251     my $self = shift;
252     return $self->{node}->{flag}->{smxe__standalone}
253     if defined $self->{node}->{flag}->{smxe__standalone};
254     for (@{$self->{node}->{node}}) {
255     if ($_->{type} eq '#pi' && $_->{local_name} eq 'xml') {
256     my $a = $_->get_attribute ('standalone');
257     if (ref $a) {
258     $self->{node}->{flag}->{smxe__standalone} = $a->inner_text eq 'yes' ? 1 : 0;
259     return $self->{node}->{flag}->{smxe__standalone};
260     }
261     }
262     last;
263     }
264     $self->{node}->{flag}->{smxe__standalone} = 0;
265     return $self->{node}->{flag}->{smxe__standalone};
266     }
267 wakaba 1.1 sub is_standalone_document_1 ($) {
268     my $self = shift;
269     return $self->{node}->{flag}->{smxe__standalone_1}
270     if defined $self->{node}->{flag}->{smxe__standalone_1};
271     for (@{$self->{node}->{node}}) {
272     if ($_->{type} eq '#pi' && $_->{local_name} eq 'xml') {
273     my $a = $_->get_attribute ('standalone');
274     if (ref $a) {
275     $self->{node}->{flag}->{smxe__standalone_1} = $a->inner_text eq 'yes' ? 1 : 0;
276     return $self->{node}->{flag}->{smxe__standalone_1};
277     }
278     }
279 wakaba 1.4 last;
280 wakaba 1.1 }
281     if ($self->{doctype}) {
282     if ($self->{doctype}->external_id) {
283     $self->{node}->{flag}->{smxe__standalone_1} = 0;
284     return $self->{node}->{flag}->{smxe__standalone_1};
285     }
286     for (@{$self->{doctype}->{node}}) {
287     if ($_->{type} eq '#declaration' && $_->{namespace_uri} eq $NS{SGML}.'entity:parameter') {
288     $self->{node}->{flag}->{smxe__standalone_1} = 0;
289     return $self->{node}->{flag}->{smxe__standalone_1};
290     }
291     }
292     }
293     $self->{node}->{flag}->{smxe__standalone_1} = 1;
294     return $self->{node}->{flag}->{smxe__standalone_1};
295     }
296    
297 wakaba 1.3 sub check_public_id ($$$) {
298     my ($self, $o, $pubid) = @_;
299 wakaba 1.4 if (length ($pubid) == 0) {
300     $self->_raise_error ($o, type => 'WARN_PID_EMPTY');
301     }
302     if ($pubid =~ m"([^\x0A\x0D\x20A-Za-z0-9'()+,./:=?;!*#\@\$_%-])"s) {
303 wakaba 1.3 $self->_raise_error ($o, type => 'SYNTAX_INVALID_PUBID', t => $1);
304     }
305     $pubid =~ s/[\x0A\x0D\x20]+/\x20/gs;
306     if (length ($pubid) > 240) { ## this check is not strict
307     $self->_raise_error ($o, type => 'WARN_PID_IS_TOO_LONG', t => $pubid);
308     }
309     $pubid =~ s/^\x20//; $pubid =~ s/\x20$//;
310     if ($pubid =~ /^[Uu][Rr][Nn]:/) {
311     if ($pubid !~ m"^[Uu][Rr][Nn]:[0-9A-Za-z][0-9A-Za-z-]{0,31}:(?:[0-9A-Za-z()+,.:=\@;\$_!*'/?-]|%[0-9A-Fa-f]{2})+$") {
312     $self->_raise_error ($o, type => 'WARN_PID_IS_INVALID_URN', t => $pubid);
313     } elsif ($pubid =~ m![/?]!) {
314     $self->_raise_error ($o, type => 'WARN_PID_IS_URN_WITH_RESERVED_CHAR', t => $pubid);
315     }
316     } elsif ($pubid !~ m<^(?:[+-]//|ISO)(?:(?!//).)+//[A-Z]+ (?:(?!//).)+//(?:(?!//).)+(?://(?:(?!//).)+)?$>) {
317     $self->_raise_error ($o, type => 'WARN_PID_IS_NOT_FPI_NOR_URN', t => $pubid);
318     }
319     $pubid;
320     }
321    
322     sub check_system_id ($$$) {
323     my ($self, $o, $sysid) = @_;
324     if ($sysid =~ m"([0-9A-Za-z_.!~*'();/?:\@&=+\$,%[]#-])"s) {
325     $self->_raise_error ($o, type => 'WARN_INVALID_URI_CHAR_IN_SYSID', t => $1);
326 wakaba 1.4 }
327     if ($sysid =~ s/(#[^#]*)$//g) {
328     $self->_raise_error ($o, type => 'ERR_XML_SYSID_HAS_FRAGMENT', t => $1);
329     }
330     if (length ($sysid) == 0) {
331     $self->_raise_error ($o, type => 'WARN_SYSID_EMPTY');
332 wakaba 1.3 }
333     $sysid;
334 wakaba 1.5 }
335    
336     sub check_ns_uri ($$$$) { ## TODO: check predefined NS
337     my ($self, $o, $ns_pfx => $ns_name) = @_;
338     if ($ns_name =~ m"([0-9A-Za-z_.!~*'();/?:\@&=+\$,%[]#-])"s) {
339     $self->_raise_error ($o, type => 'WARN_INVALID_URI_CHAR_IN_NS_NAME', t => $1);
340     }
341     if ($ns_name !~ /^[0-9A-Za-z.+-]+:/) {
342     $self->_raise_error ($o, type => 'WARN_XML_NS_URI_IS_RELATIVE', t => $ns_name);
343     }
344 wakaba 1.3 }
345    
346     ## Guess encoding of the entity by BOM and '<?' and Encode::Guess --- Used by default resolver
347     sub _guess_entity_encoding ($$) {
348     my ($self, $entity, $o) = @_;
349     my $encoding;
350     my $f2 = substr ($entity, 0, 2);
351     my $s2 = substr ($entity, 2, 2);
352     if ($f2 eq "<?") {
353     $encoding = '*ascii';
354     } elsif ($f2 eq "\xEF\xBB" && substr ($s2, 0, 1) eq "\xBF") {
355     $encoding = '*utf-8';
356     } elsif ($f2 eq "\xFE\xFF" || $f2 eq "\x00\x3C") {
357     if ($s2 eq "\x00\x00") {
358     $encoding = '*ucs-4-3412';
359     } else {
360     $encoding = '*ucs-2be';
361     }
362     } elsif ($f2 eq "\xFF\xFE" || $f2 eq "\x3C\x00") {
363     if ($s2 eq "\x00\x00") {
364     $encoding = '*ucs-4le';
365     } else {
366     $encoding = '*ucs-2le';
367     }
368     } elsif ($f2 eq "\x00\x00") {
369     if ($s2 eq "\xFE\xFF" || $s2 eq "\x00\x3C") {
370     $encoding = '*ucs-4be';
371     } elsif ($s2 eq "\xFF\xFE" || $s2 eq "\x3C\x00") {
372     $encoding = '*ucs-4-2143';
373     }
374     } elsif ($f2 eq "\x4C\x6F") {
375     $encoding = '*ebcdic';
376     }
377    
378     ## TODO: Charset list need more consideration
379     my @guess_list;
380     if ($encoding eq '*ascii' || $encoding eq '*utf-8') {
381     if ($entity =~ /<\?xml.+?encoding=["']([0-9A-Za-z._-]+)["']/) {
382     $encoding = lc $1;
383     } else {
384     if ($encoding eq '*utf-8') {
385     $encoding = undef;
386     @guess_list = qw/utf-8 cesu-8 unicode-1-1-utf-8/;
387     } else {
388     $encoding = undef;
389     @guess_list = qw/us-ascii iso-8859-1 iso-8859-2 iso-8859-8 iso-8859-15
390     iso-2022-jp euc-jp shift_jis euc-kr
391     gbk gb18030 big5-eten big5-hkscs windows-1252 koi8-r koi8-u/;
392     }
393     }
394     } elsif ($encoding =~ /ucs/) {
395     my $ent = $entity; $ent =~ tr/\x00//d;
396     if ($ent =~ /<\?xml.+?encoding=["']([0-9A-Za-z._-]+)["']/) {
397     $encoding = lc $1;
398     } else {
399     if ($encoding eq '*ucs-2be') {
400     $encoding = 'utf-16be';
401     } elsif ($encoding eq '*ucs-2le') {
402     $encoding = 'utf-16le';
403     } elsif ($encoding =~ /(ucs-4.+)/) {
404     $encoding = $1;
405     }
406     }
407     } elsif ($encoding eq '*ebcdic') {
408     @guess_list = qw/cp037 cp500 cp875 cp1026 cp1047 posix-bc/; ## All Encode::EBCDIC supporteds
409     }
410    
411     unless ($encoding) {
412     $self->_raise_error ($o, type => 'WARN_NO_EXPLICIT_ENCODING_INFO');
413     require Encode::Guess;
414     unless (@guess_list) {
415     @guess_list = qw/utf-8 utf-16 utf-16be utf-16le
416     7bit-jis euc-jp shift_jis gbk gb18030 euc-kr big5-eten big5-hkscs
417     iso-8859-1 iso-8859-8 koi8-r koi8-u tis-620
418     windows-1252/;
419     }
420     my @gl;
421     for (@guess_list) {
422     push @gl, $_ if Encode::find_encoding ($_);
423     }
424     my $enc;
425     eval q{$enc = Encode::Guess::guess_encoding ($entity, @gl); 1}
426     or $self->_raise_error ($o, type => 'WARN_GUESS_ENCODING_IMPL_ERR', t => $@);
427     $encoding = $enc->name if ref $enc;
428     }
429     $encoding;
430     }
431    
432     ## Check whether the media type specified is better one for that type of entity
433     ## and raise error if not --- See RFC 3023
434     ##
435     ## Known error: Warn even when source is local file doesn't have meta type information
436     ## (but LWP give inappropreate value such as 'text/plain'). This is an error but spec.
437     sub _check_media_type ($$$) {
438     my ($self, $o, $ct) = @_;
439     if ($ct eq 'application/xml' || $ct eq 'text/xml') {
440     if ($o->{entity_type} eq 'external_parsed_entity'
441     || $o->{entity_type} eq 'external_general_parsed_entity') {
442     $self->_raise_error ($o, type => 'WARN_MT_XML_FOR_EXT_GENERAL_ENTITY', t => $ct);
443     } elsif ($o->{entity_type} ne 'document_entity') {
444     $self->_raise_error ($o, type => 'ERR_MT_XML_FOR_EXT_ENTITY', t => $ct);
445     }
446     if ($ct eq 'text/xml') {
447     $self->_raise_error ($o, type => 'WARN_MT_TEXT_XML');
448     }
449     } elsif ($o->{entity_type} eq 'external_general_parsed_entity') {
450     if ($ct eq 'text/xml-external-parsed-entity') {
451     $self->_raise_error ($o, type => 'WARN_MT_TEXT_XML_EXTERNAL_PARSED_ENTITY');
452     } elsif ($ct ne 'application/xml-external-parsed-entity') {
453     $self->_raise_error ($o, type => 'WARN_MT_EXTERNAL_GENERAL_PARSED_ENTITY', t => $ct);
454     }
455     } elsif ($o->{entity_type} eq 'dtd_external_subset'
456     || $o->{entity_type} eq 'external_parameter_entity') {
457     if ($ct ne 'application/xml-dtd') {
458     $self->_raise_error ($o, type => 'WARN_MT_DTD_EXTERNAL_SUBSET', t => $ct);
459     }
460     }
461     }
462    
463     ## $self->_raise_error: Raising error or warn
464     require SuikaWiki::Markup::XML::Error;
465     *_raise_error = \&SuikaWiki::Markup::XML::Error::raise;
466    
467 wakaba 1.4 =head1 DEVELOPER'S NOTE
468    
469     This module "knows" how SuikaWiki::Markup::XML works, i.e. this module accesses
470     internal structure of that module directly.
471    
472 wakaba 1.1 =head1 LICENSE
473    
474     Copyright 2003 Wakaba <[email protected]>
475    
476     This program is free software; you can redistribute it and/or
477     modify it under the same terms as Perl itself.
478    
479     =cut
480    
481 wakaba 1.4 1; # $Date: 2003/06/27 13:05:57 $

[email protected]
ViewVC Help
Powered by ViewVC 1.1.24