enabling immutable finishing porting Log and stats
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Engine.pm
CommitLineData
fc7ec1d9 1package Catalyst::Engine;
2
7fa2c9c1 3use Moose;
4with 'MooseX::Emulate::Class::Accessor::Fast';
5
fa32ac82 6use CGI::Simple::Cookie;
f63c03e4 7use Data::Dump qw/dump/;
d04b2ffd 8use Errno 'EWOULDBLOCK';
fc7ec1d9 9use HTML::Entities;
fbcc39ad 10use HTTP::Body;
fc7ec1d9 11use HTTP::Headers;
e0616220 12use URI::QueryParam;
2832cb5d 13use Scalar::Util ();
fbcc39ad 14
15# input position and length
7fa2c9c1 16has read_length => (is => 'rw');
17has read_position => (is => 'rw');
fbcc39ad 18
19# Stringify to class
20use overload '""' => sub { return ref shift }, fallback => 1;
fc7ec1d9 21
4bd82c41 22# Amount of data to read from input on each pass
4bb8bd62 23our $CHUNKSIZE = 64 * 1024;
4bd82c41 24
fc7ec1d9 25=head1 NAME
26
27Catalyst::Engine - The Catalyst Engine
28
29=head1 SYNOPSIS
30
31See L<Catalyst>.
32
33=head1 DESCRIPTION
34
23f9d934 35=head1 METHODS
fc7ec1d9 36
cd3bb248 37
b5ecfcf0 38=head2 $self->finalize_body($c)
06e1b616 39
fbcc39ad 40Finalize body. Prints the response output.
06e1b616 41
42=cut
43
fbcc39ad 44sub finalize_body {
45 my ( $self, $c ) = @_;
7257e9db 46 my $body = $c->response->body;
f9b6d612 47 no warnings 'uninitialized';
48 if ( Scalar::Util::blessed($body) && $body->can('read') or ref($body) eq 'GLOB' ) {
7257e9db 49 while ( !eof $body ) {
4c423abf 50 read $body, my ($buffer), $CHUNKSIZE;
6484fba0 51 last unless $self->write( $c, $buffer );
f4a57de4 52 }
7257e9db 53 close $body;
f4a57de4 54 }
55 else {
7257e9db 56 $self->write( $c, $body );
f4a57de4 57 }
fbcc39ad 58}
6dc87a0f 59
b5ecfcf0 60=head2 $self->finalize_cookies($c)
6dc87a0f 61
fa32ac82 62Create CGI::Simple::Cookie objects from $c->res->cookies, and set them as
63response headers.
4ab87e27 64
6dc87a0f 65=cut
66
67sub finalize_cookies {
fbcc39ad 68 my ( $self, $c ) = @_;
6dc87a0f 69
fbcc39ad 70 my @cookies;
7fa2c9c1 71 my $response = $c->response;
c82ed742 72
7fa2c9c1 73 while( my($name, $val) = each %{ $response->cookies } ) {
fbcc39ad 74
2832cb5d 75 my $cookie = (
76 Scalar::Util::blessed($val)
77 ? $val
78 : CGI::Simple::Cookie->new(
79 -name => $name,
80 -value => $val->{value},
81 -expires => $val->{expires},
82 -domain => $val->{domain},
83 -path => $val->{path},
84 -secure => $val->{secure} || 0
85 )
6dc87a0f 86 );
87
fbcc39ad 88 push @cookies, $cookie->as_string;
6dc87a0f 89 }
6dc87a0f 90
b39840da 91 for my $cookie (@cookies) {
7fa2c9c1 92 $response->headers->push_header( 'Set-Cookie' => $cookie );
fbcc39ad 93 }
94}
969647fd 95
b5ecfcf0 96=head2 $self->finalize_error($c)
969647fd 97
4ab87e27 98Output an apropriate error message, called if there's an error in $c
99after the dispatch has finished. Will output debug messages if Catalyst
100is in debug mode, or a `please come back later` message otherwise.
101
969647fd 102=cut
103
104sub finalize_error {
fbcc39ad 105 my ( $self, $c ) = @_;
969647fd 106
7299a7b4 107 $c->res->content_type('text/html; charset=utf-8');
34d28dfd 108 my $name = $c->config->{name} || join(' ', split('::', ref $c));
969647fd 109
110 my ( $title, $error, $infos );
111 if ( $c->debug ) {
62d9b030 112
113 # For pretty dumps
b5ecfcf0 114 $error = join '', map {
115 '<p><code class="error">'
116 . encode_entities($_)
117 . '</code></p>'
118 } @{ $c->error };
969647fd 119 $error ||= 'No output';
2666dd3b 120 $error = qq{<pre wrap="">$error</pre>};
969647fd 121 $title = $name = "$name on Catalyst $Catalyst::VERSION";
d82cc9ae 122 $name = "<h1>$name</h1>";
fbcc39ad 123
124 # Don't show context in the dump
125 delete $c->req->{_context};
126 delete $c->res->{_context};
127
128 # Don't show body parser in the dump
129 delete $c->req->{_body};
130
131 # Don't show response header state in dump
132 delete $c->res->{_finalized_headers};
133
c6ef5e69 134 my @infos;
135 my $i = 0;
c6ef5e69 136 for my $dump ( $c->dump_these ) {
c6ef5e69 137 my $name = $dump->[0];
f63c03e4 138 my $value = encode_entities( dump( $dump->[1] ));
c6ef5e69 139 push @infos, sprintf <<"EOF", $name, $value;
9619f23c 140<h2><a href="#" onclick="toggleDump('dump_$i'); return false">%s</a></h2>
c6ef5e69 141<div id="dump_$i">
2666dd3b 142 <pre wrap="">%s</pre>
c6ef5e69 143</div>
144EOF
145 $i++;
146 }
147 $infos = join "\n", @infos;
969647fd 148 }
149 else {
150 $title = $name;
151 $error = '';
152 $infos = <<"";
153<pre>
154(en) Please come back later
0c2b4ac0 155(fr) SVP veuillez revenir plus tard
969647fd 156(de) Bitte versuchen sie es spaeter nocheinmal
d82cc9ae 157(at) Konnten's bitt'schoen spaeter nochmal reinschauen
969647fd 158(no) Vennligst prov igjen senere
d82cc9ae 159(dk) Venligst prov igen senere
160(pl) Prosze sprobowac pozniej
969647fd 161</pre>
162
163 $name = '';
164 }
e060fe05 165 $c->res->body( <<"" );
7299a7b4 166<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
167 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
168<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
969647fd 169<head>
7299a7b4 170 <meta http-equiv="Content-Language" content="en" />
171 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
969647fd 172 <title>$title</title>
7299a7b4 173 <script type="text/javascript">
c6ef5e69 174 <!--
175 function toggleDump (dumpElement) {
7299a7b4 176 var e = document.getElementById( dumpElement );
177 if (e.style.display == "none") {
178 e.style.display = "";
c6ef5e69 179 }
180 else {
7299a7b4 181 e.style.display = "none";
c6ef5e69 182 }
183 }
184 -->
185 </script>
969647fd 186 <style type="text/css">
187 body {
188 font-family: "Bitstream Vera Sans", "Trebuchet MS", Verdana,
189 Tahoma, Arial, helvetica, sans-serif;
34d28dfd 190 color: #333;
969647fd 191 background-color: #eee;
192 margin: 0px;
193 padding: 0px;
194 }
c6ef5e69 195 :link, :link:hover, :visited, :visited:hover {
34d28dfd 196 color: #000;
c6ef5e69 197 }
969647fd 198 div.box {
9619f23c 199 position: relative;
969647fd 200 background-color: #ccc;
201 border: 1px solid #aaa;
202 padding: 4px;
203 margin: 10px;
969647fd 204 }
205 div.error {
34d28dfd 206 background-color: #cce;
969647fd 207 border: 1px solid #755;
208 padding: 8px;
209 margin: 4px;
210 margin-bottom: 10px;
969647fd 211 }
212 div.infos {
34d28dfd 213 background-color: #eee;
969647fd 214 border: 1px solid #575;
215 padding: 8px;
216 margin: 4px;
217 margin-bottom: 10px;
969647fd 218 }
219 div.name {
34d28dfd 220 background-color: #cce;
969647fd 221 border: 1px solid #557;
222 padding: 8px;
223 margin: 4px;
969647fd 224 }
7f8e0078 225 code.error {
226 display: block;
227 margin: 1em 0;
228 overflow: auto;
7f8e0078 229 }
9619f23c 230 div.name h1, div.error p {
231 margin: 0;
232 }
233 h2 {
234 margin-top: 0;
235 margin-bottom: 10px;
236 font-size: medium;
237 font-weight: bold;
238 text-decoration: underline;
239 }
240 h1 {
241 font-size: medium;
242 font-weight: normal;
243 }
2666dd3b 244 /* from http://users.tkk.fi/~tkarvine/linux/doc/pre-wrap/pre-wrap-css3-mozilla-opera-ie.html */
245 /* Browser specific (not valid) styles to make preformatted text wrap */
7fa2c9c1 246 pre {
2666dd3b 247 white-space: pre-wrap; /* css-3 */
248 white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
249 white-space: -pre-wrap; /* Opera 4-6 */
250 white-space: -o-pre-wrap; /* Opera 7 */
251 word-wrap: break-word; /* Internet Explorer 5.5+ */
252 }
969647fd 253 </style>
254</head>
255<body>
256 <div class="box">
257 <div class="error">$error</div>
258 <div class="infos">$infos</div>
259 <div class="name">$name</div>
260 </div>
261</body>
262</html>
263
d82cc9ae 264
265 # Trick IE
266 $c->res->{body} .= ( ' ' x 512 );
267
268 # Return 500
33117422 269 $c->res->status(500);
969647fd 270}
271
b5ecfcf0 272=head2 $self->finalize_headers($c)
fc7ec1d9 273
4ab87e27 274Abstract method, allows engines to write headers to response
275
fc7ec1d9 276=cut
277
278sub finalize_headers { }
279
b5ecfcf0 280=head2 $self->finalize_read($c)
fc7ec1d9 281
282=cut
283
878b821c 284sub finalize_read { }
fc7ec1d9 285
b5ecfcf0 286=head2 $self->finalize_uploads($c)
fc7ec1d9 287
4ab87e27 288Clean up after uploads, deleting temp files.
289
fc7ec1d9 290=cut
291
fbcc39ad 292sub finalize_uploads {
293 my ( $self, $c ) = @_;
99fe1710 294
7fa2c9c1 295 my $request = $c->request;
296 while( my($key,$upload) = each %{ $request->uploads } ) {
297 unlink grep { -e $_ } map { $_->tempname }
298 (ref $upload eq 'ARRAY' ? @{$upload} : ($upload));
c85ff642 299 }
7fa2c9c1 300
fc7ec1d9 301}
302
b5ecfcf0 303=head2 $self->prepare_body($c)
fc7ec1d9 304
4ab87e27 305sets up the L<Catalyst::Request> object body using L<HTTP::Body>
306
fc7ec1d9 307=cut
308
fbcc39ad 309sub prepare_body {
310 my ( $self, $c ) = @_;
99fe1710 311
878b821c 312 if ( my $length = $self->read_length ) {
7fa2c9c1 313 my $request = $c->request;
314 unless ( $request->{_body} ) {
315 my $type = $request->header('Content-Type');
316 $request->{_body} = HTTP::Body->new( $type, $length );
317 $request->{_body}->{tmpdir} = $c->config->{uploadtmp}
847e3257 318 if exists $c->config->{uploadtmp};
319 }
7fa2c9c1 320
4f5ebacd 321 while ( my $buffer = $self->read($c) ) {
322 $c->prepare_body_chunk($buffer);
fbcc39ad 323 }
fdb3773e 324
325 # paranoia against wrong Content-Length header
847e3257 326 my $remaining = $length - $self->read_position;
34d28dfd 327 if ( $remaining > 0 ) {
fdb3773e 328 $self->finalize_read($c);
34d28dfd 329 Catalyst::Exception->throw(
847e3257 330 "Wrong Content-Length value: $length" );
fdb3773e 331 }
fc7ec1d9 332 }
847e3257 333 else {
334 # Defined but will cause all body code to be skipped
335 $c->request->{_body} = 0;
336 }
fc7ec1d9 337}
338
b5ecfcf0 339=head2 $self->prepare_body_chunk($c)
4bd82c41 340
4ab87e27 341Add a chunk to the request body.
342
4bd82c41 343=cut
344
345sub prepare_body_chunk {
346 my ( $self, $c, $chunk ) = @_;
4f5ebacd 347
348 $c->request->{_body}->add($chunk);
4bd82c41 349}
350
b5ecfcf0 351=head2 $self->prepare_body_parameters($c)
06e1b616 352
7fa2c9c1 353Sets up parameters from body.
4ab87e27 354
06e1b616 355=cut
356
fbcc39ad 357sub prepare_body_parameters {
358 my ( $self, $c ) = @_;
7fa2c9c1 359
847e3257 360 return unless $c->request->{_body};
7fa2c9c1 361
fbcc39ad 362 $c->request->body_parameters( $c->request->{_body}->param );
363}
0556eb49 364
b5ecfcf0 365=head2 $self->prepare_connection($c)
0556eb49 366
4ab87e27 367Abstract method implemented in engines.
368
0556eb49 369=cut
370
371sub prepare_connection { }
372
b5ecfcf0 373=head2 $self->prepare_cookies($c)
fc7ec1d9 374
fa32ac82 375Parse cookies from header. Sets a L<CGI::Simple::Cookie> object.
4ab87e27 376
fc7ec1d9 377=cut
378
6dc87a0f 379sub prepare_cookies {
fbcc39ad 380 my ( $self, $c ) = @_;
6dc87a0f 381
382 if ( my $header = $c->request->header('Cookie') ) {
fa32ac82 383 $c->req->cookies( { CGI::Simple::Cookie->parse($header) } );
6dc87a0f 384 }
385}
fc7ec1d9 386
b5ecfcf0 387=head2 $self->prepare_headers($c)
fc7ec1d9 388
389=cut
390
391sub prepare_headers { }
392
b5ecfcf0 393=head2 $self->prepare_parameters($c)
fc7ec1d9 394
4ab87e27 395sets up parameters from query and post parameters.
396
fc7ec1d9 397=cut
398
fbcc39ad 399sub prepare_parameters {
400 my ( $self, $c ) = @_;
fc7ec1d9 401
7fa2c9c1 402 my $request = $c->request;
403 my $parameters = $request->parameters;
404 my $body_parameters = $request->body_parameters;
405 my $query_parameters = $request->query_parameters;
fbcc39ad 406 # We copy, no references
7fa2c9c1 407 while( my($name, $param) = each(%$query_parameters) ) {
408 $parameters->{$name} = ref $param eq 'ARRAY' ? [ @$param ] : $param;
fbcc39ad 409 }
fc7ec1d9 410
fbcc39ad 411 # Merge query and body parameters
7fa2c9c1 412 while( my($name, $param) = each(%$body_parameters) ) {
413 my @values = ref $param eq 'ARRAY' ? @$param : ($param);
414 if ( my $existing = $parameters->{$name} ) {
415 unshift(@values, (ref $existing eq 'ARRAY' ? @$existing : $existing));
fbcc39ad 416 }
7fa2c9c1 417 $parameters->{$name} = @values > 1 ? \@values : $values[0];
fbcc39ad 418 }
419}
420
b5ecfcf0 421=head2 $self->prepare_path($c)
fc7ec1d9 422
4ab87e27 423abstract method, implemented by engines.
424
fc7ec1d9 425=cut
426
427sub prepare_path { }
428
b5ecfcf0 429=head2 $self->prepare_request($c)
fc7ec1d9 430
b5ecfcf0 431=head2 $self->prepare_query_parameters($c)
fc7ec1d9 432
4ab87e27 433process the query string and extract query parameters.
434
fc7ec1d9 435=cut
436
e0616220 437sub prepare_query_parameters {
438 my ( $self, $c, $query_string ) = @_;
7fa2c9c1 439
3b4d1251 440 # Check for keywords (no = signs)
441 # (yes, index() is faster than a regex :))
933ba403 442 if ( index( $query_string, '=' ) < 0 ) {
3b4d1251 443 $c->request->query_keywords( $self->unescape_uri($query_string) );
933ba403 444 return;
445 }
446
447 my %query;
e0616220 448
449 # replace semi-colons
450 $query_string =~ s/;/&/g;
7fa2c9c1 451
933ba403 452 my @params = split /&/, $query_string;
e0616220 453
933ba403 454 for my $item ( @params ) {
7fa2c9c1 455
456 my ($param, $value)
933ba403 457 = map { $self->unescape_uri($_) }
e5542b70 458 split( /=/, $item, 2 );
7fa2c9c1 459
933ba403 460 $param = $self->unescape_uri($item) unless defined $param;
7fa2c9c1 461
933ba403 462 if ( exists $query{$param} ) {
463 if ( ref $query{$param} ) {
464 push @{ $query{$param} }, $value;
465 }
466 else {
467 $query{$param} = [ $query{$param}, $value ];
468 }
469 }
470 else {
471 $query{$param} = $value;
472 }
e0616220 473 }
933ba403 474
475 $c->request->query_parameters( \%query );
e0616220 476}
fbcc39ad 477
b5ecfcf0 478=head2 $self->prepare_read($c)
fbcc39ad 479
4ab87e27 480prepare to read from the engine.
481
fbcc39ad 482=cut
fc7ec1d9 483
fbcc39ad 484sub prepare_read {
485 my ( $self, $c ) = @_;
4f5ebacd 486
878b821c 487 # Initialize the read position
4f5ebacd 488 $self->read_position(0);
7fa2c9c1 489
878b821c 490 # Initialize the amount of data we think we need to read
491 $self->read_length( $c->request->header('Content-Length') || 0 );
fbcc39ad 492}
fc7ec1d9 493
b5ecfcf0 494=head2 $self->prepare_request(@arguments)
fc7ec1d9 495
4ab87e27 496Populate the context object from the request object.
497
fc7ec1d9 498=cut
499
fbcc39ad 500sub prepare_request { }
fc7ec1d9 501
b5ecfcf0 502=head2 $self->prepare_uploads($c)
c9afa5fc 503
fbcc39ad 504=cut
505
506sub prepare_uploads {
507 my ( $self, $c ) = @_;
7fa2c9c1 508
509 my $request = $c->request;
510 return unless $request->{_body};
511
512 my $uploads = $request->{_body}->upload;
513 my $parameters = $request->parameters;
514 while(my($name,$files) = each(%$uploads) ) {
fbcc39ad 515 my @uploads;
7fa2c9c1 516 for my $upload (ref $files eq 'ARRAY' ? @$files : ($files)) {
517 my $headers = HTTP::Headers->new( %{ $upload->{headers} } );
518 my $u = Catalyst::Request::Upload->new
519 (
520 size => $upload->{size},
521 type => $headers->content_type,
522 headers => $headers,
523 tempname => $upload->{tempname},
524 filename => $upload->{filename},
525 );
fbcc39ad 526 push @uploads, $u;
527 }
7fa2c9c1 528 $request->uploads->{$name} = @uploads > 1 ? \@uploads : $uploads[0];
f4a57de4 529
c4bed79a 530 # support access to the filename as a normal param
531 my @filenames = map { $_->{filename} } @uploads;
a7e05d9d 532 # append, if there's already params with this name
7fa2c9c1 533 if (exists $parameters->{$name}) {
534 if (ref $parameters->{$name} eq 'ARRAY') {
535 push @{ $parameters->{$name} }, @filenames;
a7e05d9d 536 }
537 else {
7fa2c9c1 538 $parameters->{$name} = [ $parameters->{$name}, @filenames ];
a7e05d9d 539 }
540 }
541 else {
7fa2c9c1 542 $parameters->{$name} = @filenames > 1 ? \@filenames : $filenames[0];
a7e05d9d 543 }
fbcc39ad 544 }
545}
546
b5ecfcf0 547=head2 $self->prepare_write($c)
c9afa5fc 548
4ab87e27 549Abstract method. Implemented by the engines.
550
c9afa5fc 551=cut
552
fbcc39ad 553sub prepare_write { }
554
b5ecfcf0 555=head2 $self->read($c, [$maxlength])
fbcc39ad 556
557=cut
558
559sub read {
560 my ( $self, $c, $maxlength ) = @_;
4f5ebacd 561
fbcc39ad 562 my $remaining = $self->read_length - $self->read_position;
4bd82c41 563 $maxlength ||= $CHUNKSIZE;
4f5ebacd 564
fbcc39ad 565 # Are we done reading?
566 if ( $remaining <= 0 ) {
4f5ebacd 567 $self->finalize_read($c);
fbcc39ad 568 return;
569 }
c9afa5fc 570
fbcc39ad 571 my $readlen = ( $remaining > $maxlength ) ? $maxlength : $remaining;
572 my $rc = $self->read_chunk( $c, my $buffer, $readlen );
573 if ( defined $rc ) {
574 $self->read_position( $self->read_position + $rc );
575 return $buffer;
576 }
577 else {
4f5ebacd 578 Catalyst::Exception->throw(
579 message => "Unknown error reading input: $!" );
fbcc39ad 580 }
581}
fc7ec1d9 582
b5ecfcf0 583=head2 $self->read_chunk($c, $buffer, $length)
23f9d934 584
fbcc39ad 585Each engine inplements read_chunk as its preferred way of reading a chunk
586of data.
fc7ec1d9 587
fbcc39ad 588=cut
61b1e958 589
fbcc39ad 590sub read_chunk { }
61b1e958 591
b5ecfcf0 592=head2 $self->read_length
ca39d576 593
fbcc39ad 594The length of input data to be read. This is obtained from the Content-Length
595header.
fc7ec1d9 596
b5ecfcf0 597=head2 $self->read_position
fc7ec1d9 598
fbcc39ad 599The amount of input data that has already been read.
63b763c5 600
b5ecfcf0 601=head2 $self->run($c)
63b763c5 602
4ab87e27 603Start the engine. Implemented by the various engine classes.
604
fbcc39ad 605=cut
fc7ec1d9 606
fbcc39ad 607sub run { }
fc7ec1d9 608
b5ecfcf0 609=head2 $self->write($c, $buffer)
fc7ec1d9 610
e512dd24 611Writes the buffer to the client.
4ab87e27 612
fc7ec1d9 613=cut
614
fbcc39ad 615sub write {
616 my ( $self, $c, $buffer ) = @_;
4f5ebacd 617
fbcc39ad 618 unless ( $self->{_prepared_write} ) {
4f5ebacd 619 $self->prepare_write($c);
fbcc39ad 620 $self->{_prepared_write} = 1;
fc7ec1d9 621 }
7fa2c9c1 622
d04b2ffd 623 my $len = length($buffer);
624 my $wrote = syswrite STDOUT, $buffer;
7fa2c9c1 625
d04b2ffd 626 if ( !defined $wrote && $! == EWOULDBLOCK ) {
627 # Unable to write on the first try, will retry in the loop below
628 $wrote = 0;
629 }
7fa2c9c1 630
d04b2ffd 631 if ( defined $wrote && $wrote < $len ) {
632 # We didn't write the whole buffer
633 while (1) {
634 my $ret = syswrite STDOUT, $buffer, $CHUNKSIZE, $wrote;
635 if ( defined $ret ) {
636 $wrote += $ret;
637 }
638 else {
639 next if $! == EWOULDBLOCK;
640 return;
641 }
7fa2c9c1 642
d04b2ffd 643 last if $wrote >= $len;
e2b0ddd3 644 }
e512dd24 645 }
7fa2c9c1 646
e512dd24 647 return $wrote;
fc7ec1d9 648}
649
933ba403 650=head2 $self->unescape_uri($uri)
651
6a44fe01 652Unescapes a given URI using the most efficient method available. Engines such
653as Apache may implement this using Apache's C-based modules, for example.
933ba403 654
655=cut
656
657sub unescape_uri {
8c7d83e1 658 my ( $self, $str ) = @_;
7d22a537 659
660 $str =~ s/(?:%([0-9A-Fa-f]{2})|\+)/defined $1 ? chr(hex($1)) : ' '/eg;
661
8c7d83e1 662 return $str;
933ba403 663}
34d28dfd 664
4ab87e27 665=head2 $self->finalize_output
666
667<obsolete>, see finalize_body
668
fbcc39ad 669=head1 AUTHORS
670
671Sebastian Riedel, <sri@cpan.org>
fc7ec1d9 672
fbcc39ad 673Andy Grundman, <andy@hybridized.org>
fc7ec1d9 674
675=head1 COPYRIGHT
676
677This program is free software, you can redistribute it and/or modify it under
678the same terms as Perl itself.
679
680=cut
681
6821;