blib/lib/POE/Component/Server/SimpleContent.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 182 | 235 | 77.4 |
branch | 51 | 94 | 54.2 |
condition | 21 | 54 | 38.8 |
subroutine | 30 | 34 | 88.2 |
pod | 11 | 12 | 91.6 |
total | 295 | 429 | 68.7 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package POE::Component::Server::SimpleContent; | ||||||
2 | |||||||
3 | # We export some stuff | ||||||
4 | require Exporter; | ||||||
5 | @ISA = qw( Exporter ); | ||||||
6 | @EXPORT = qw(generate_301 generate_404 generate_403); | ||||||
7 | |||||||
8 | 8 | 8 | 220520 | use strict; | |||
8 | 19 | ||||||
8 | 361 | ||||||
9 | 8 | 8 | 44 | use warnings; | |||
8 | 17 | ||||||
8 | 229 | ||||||
10 | 8 | 8 | 46 | use Carp; | |||
8 | 16 | ||||||
8 | 783 | ||||||
11 | 8 | 8 | 11167 | use POE qw( Wheel::ReadWrite Filter::Stream ); | |||
8 | 530852 | ||||||
8 | 191 | ||||||
12 | 8 | 8 | 1151873 | use CGI qw(:standard); | |||
8 | 160731 | ||||||
8 | 73 | ||||||
13 | 8 | 8 | 42700 | use URI::Escape; | |||
8 | 16616 | ||||||
8 | 548 | ||||||
14 | 8 | 8 | 8020 | use Filesys::Virtual::Plain; | |||
8 | 244097 | ||||||
8 | 311 | ||||||
15 | 8 | 8 | 8204 | use MIME::Types; | |||
8 | 52533 | ||||||
8 | 372 | ||||||
16 | 8 | 8 | 21889 | use Storable; | |||
8 | 30762 | ||||||
8 | 636 | ||||||
17 | 8 | 8 | 150 | use File::Basename; | |||
8 | 17 | ||||||
8 | 751 | ||||||
18 | 8 | 8 | 44 | use vars qw($VERSION); | |||
8 | 13 | ||||||
8 | 54186 | ||||||
19 | |||||||
20 | $VERSION = '1.14'; | ||||||
21 | |||||||
22 | sub spawn { | ||||||
23 | 7 | 7 | 1 | 115 | my $package = shift; | ||
24 | 7 | 50 | 38 | croak "$package needs an even number of parameters" if @_ & 1; | |||
25 | 7 | 39 | my %params = @_; | ||||
26 | |||||||
27 | 7 | 70 | $params{lc $_} = delete $params{$_} for keys %params; | ||||
28 | |||||||
29 | 7 | 50 | 33 | 258 | die "$package requires a 'root_dir' argument\n" | ||
30 | unless $params{root_dir} and -d $params{root_dir}; | ||||||
31 | |||||||
32 | 7 | 50 | 37 | _massage_handlers( $params{handlers} ) if $params{handlers}; | |||
33 | 7 | 50 | 49 | $params{handlers} = { } unless $params{handlers}; | |||
34 | |||||||
35 | 7 | 24 | my $options = delete $params{'options'}; | ||||
36 | |||||||
37 | 7 | 30 | my $self = bless \%params, $package; | ||||
38 | |||||||
39 | 7 | 50 | 195 | $self->{vdir} = Filesys::Virtual::Plain->new( { root_path => $self->{root_dir} } ) | |||
40 | or die "Could not create a Filesys::Virtual::Plain object for $self->{root_dir}\n"; | ||||||
41 | |||||||
42 | 7 | 998 | $self->{mt} = MIME::Types->new(); | ||||
43 | |||||||
44 | 7 | 50 | 33 | 444420 | $self->{auto_index} = 1 unless defined ( $self->{auto_index} ) and $self->{auto_index} == 0; | ||
45 | 7 | 50 | 46 | $self->{index_file} = 'index.html' unless $self->{index_file}; | |||
46 | |||||||
47 | 7 | 100 | 41 | $self->{prefix_fix} = delete $self->{alias_path} if $self->{alias_path}; | |||
48 | |||||||
49 | 7 | 100 | 32 | $self->{prefix_fix} = quotemeta( $self->{prefix_fix} ) if $self->{prefix_fix}; | |||
50 | |||||||
51 | 7 | 11 | my $mm; | ||||
52 | |||||||
53 | 7 | 22 | eval { | ||||
54 | 7 | 3773 | require File::MMagic::XS; | ||||
55 | 0 | 0 | import File::MMagic::XS qw(:compat); | ||||
56 | 0 | 0 | $mm = File::MMagic::XS->new(); | ||||
57 | }; | ||||||
58 | |||||||
59 | 7 | 50 | 55 | eval { | |||
60 | 7 | 3084 | require File::MMagic; | ||||
61 | 0 | 0 | $mm = File::MMagic->new(); | ||||
62 | } unless $mm; | ||||||
63 | |||||||
64 | 7 | 260 | $self->{mm} = $mm; | ||||
65 | |||||||
66 | 7 | 50 | 33 | 197 | $self->{session_id} = POE::Session->create( | ||
67 | object_states => [ | ||||||
68 | $self => { | ||||||
69 | request => '_request', | ||||||
70 | shutdown => '_shutdown', | ||||||
71 | -input => '_read_input', | ||||||
72 | -error => '_read_error', | ||||||
73 | }, | ||||||
74 | $self => [ qw(_start) ], | ||||||
75 | ], | ||||||
76 | ( ( defined ( $options ) and ref ( $options ) eq 'HASH' ) ? ( options => $options ) : () ), | ||||||
77 | )->ID(); | ||||||
78 | |||||||
79 | 7 | 1446 | return $self; | ||||
80 | } | ||||||
81 | |||||||
82 | sub _start { | ||||||
83 | 7 | 7 | 2153 | my ($kernel,$self) = @_[KERNEL,OBJECT]; | |||
84 | |||||||
85 | 7 | 40 | $self->{session_id} = $_[SESSION]->ID(); | ||||
86 | |||||||
87 | 7 | 50 | 63 | if ( $self->{alias} ) { | |||
88 | 0 | 0 | $kernel->alias_set( $self->{alias} ); | ||||
89 | } else { | ||||||
90 | 7 | 41 | $kernel->refcount_increment( $self->{session_id} => __PACKAGE__ ); | ||||
91 | } | ||||||
92 | |||||||
93 | 7 | 279 | return; | ||||
94 | } | ||||||
95 | |||||||
96 | sub request { | ||||||
97 | 30 | 30 | 1 | 74689 | my $self = shift; | ||
98 | 30 | 108 | $poe_kernel->post( $self->session_id() => 'request' => @_ ); | ||||
99 | } | ||||||
100 | |||||||
101 | sub _request { | ||||||
102 | 30 | 30 | 6232 | my ($kernel,$self,$request,$response) = @_[KERNEL,OBJECT,ARG0 .. ARG1]; | |||
103 | 30 | 125 | my $sender = $_[SENDER]->ID(); | ||||
104 | |||||||
105 | # Sanity check the $request and $response objects *sigh* | ||||||
106 | 30 | 50 | 33 | 446 | return unless $response and $response->isa("HTTP::Response"); | ||
107 | |||||||
108 | 30 | 50 | 33 | 337 | unless ( $request and $request->isa("HTTP::Request") ) { | ||
109 | 0 | 0 | $kernel->post( $sender => 'DONE' => $response ); | ||||
110 | 0 | 0 | return; | ||||
111 | } | ||||||
112 | |||||||
113 | 30 | 125 | my $path = uri_unescape( $request->uri->path ); | ||||
114 | 30 | 1638 | my $realpath = $path; | ||||
115 | |||||||
116 | 30 | 100 | 122 | $realpath = $self->{prefix_path} . $path if $self->{prefix_path}; | |||
117 | 30 | 100 | 151 | $realpath =~ s/^$self->{prefix_fix}// if $self->{prefix_fix}; | |||
118 | |||||||
119 | SWITCH: { | ||||||
120 | 30 | 100 | 45 | if ( $self->{vdir}->test('d', $realpath) ) { | |||
30 | 183 | ||||||
121 | 18 | 100 | 3110 | if ( $path !~ /\/$/ ) { | |||
122 | 6 | 17 | $path .= '/'; | ||||
123 | 6 | 38 | $response = $self->_generate_301( $path, $response ); | ||||
124 | 6 | 25 | last SWITCH; | ||||
125 | } | ||||||
126 | 12 | 50 | 33 | 69 | if ( $self->{auto_index} and !$self->{vdir}->test('e', $realpath . $self->{index_file} ) ) { | ||
127 | 0 | 0 | $response = $self->_generate_dir_listing( $path, $response ); | ||||
128 | 0 | 0 | last SWITCH; | ||||
129 | } | ||||||
130 | 12 | 100 | 108 | if ( $self->{vdir}->test('e', $realpath . $self->{index_file} ) ) { | |||
131 | 6 | 768 | my ($filename, $directory, $suffix) = fileparse($self->{index_file}, keys %{ $self->{handlers} } ); | ||||
6 | 273 | ||||||
132 | 6 | 50 | 29 | if ( $suffix ) { | |||
133 | 0 | 0 | $kernel->post( | ||||
134 | $self->{handlers}->{ $suffix }->{SESSION}, | ||||||
135 | $self->{handlers}->{ $suffix }->{EVENT}, | ||||||
136 | { | ||||||
137 | request => $request, | ||||||
138 | response => $response, | ||||||
139 | session => $sender, | ||||||
140 | script_name => $path . $self->{index_file}, | ||||||
141 | script_filename => $self->{vdir}->root_path() . $realpath . $self->{index_file}, | ||||||
142 | }, | ||||||
143 | ); | ||||||
144 | 0 | 0 | return; | ||||
145 | } | ||||||
146 | 6 | 52 | $response = $self->_generate_content( $sender, $path . $self->{index_file}, $response ); | ||||
147 | 6 | 28 | last SWITCH; | ||||
148 | } | ||||||
149 | 6 | 698 | $response = $self->_generate_403( $response ); | ||||
150 | 6 | 16 | last SWITCH; | ||||
151 | } | ||||||
152 | 12 | 100 | 1609 | if ( $self->{vdir}->test('e', $realpath) ) { | |||
153 | 3 | 344 | my ($filename, $directory, $suffix) = fileparse($realpath, keys %{ $self->{handlers} } ); | ||||
3 | 106 | ||||||
154 | 3 | 50 | 15 | if ( $suffix ) { | |||
155 | 3 | 27 | $kernel->post( | ||||
156 | $self->{handlers}->{ $suffix }->{SESSION}, | ||||||
157 | $self->{handlers}->{ $suffix }->{EVENT}, | ||||||
158 | { | ||||||
159 | request => $request, | ||||||
160 | response => $response, | ||||||
161 | session => $sender, | ||||||
162 | script_name => $path, | ||||||
163 | script_filename => $self->{vdir}->root_path() . $realpath, | ||||||
164 | }, | ||||||
165 | ); | ||||||
166 | 3 | 1563 | return; | ||||
167 | } | ||||||
168 | 0 | 0 | $response = $self->_generate_content( $sender, $path, $response ); | ||||
169 | 0 | 0 | last SWITCH; | ||||
170 | } | ||||||
171 | 9 | 989 | $response = $self->_generate_404( $response ); | ||||
172 | } | ||||||
173 | |||||||
174 | 27 | 100 | 187 | $kernel->post( $sender => 'DONE' => $response ) if defined $response; | |||
175 | 27 | 2498 | undef; | ||||
176 | } | ||||||
177 | |||||||
178 | sub shutdown { | ||||||
179 | 7 | 7 | 1 | 7021 | my $self = shift; | ||
180 | 7 | 40 | $poe_kernel->post( $self->session_id() => 'shutdown' => @_ ); | ||||
181 | } | ||||||
182 | |||||||
183 | sub _shutdown { | ||||||
184 | 7 | 7 | 1909 | my ($kernel,$self) = @_[KERNEL,OBJECT]; | |||
185 | |||||||
186 | 7 | 50 | 38 | if ( $self->{alias} ) { | |||
187 | 0 | 0 | $kernel->alias_remove( $_ ) for $kernel->alias_list(); | ||||
188 | } else { | ||||||
189 | 7 | 55 | $kernel->refcount_decrement( $self->{session_id} => __PACKAGE__ ); | ||||
190 | } | ||||||
191 | 7 | 667 | undef; | ||||
192 | } | ||||||
193 | |||||||
194 | sub session_id { | ||||||
195 | 37 | 37 | 1 | 230 | return $_[0]->{session_id}; | ||
196 | } | ||||||
197 | |||||||
198 | # Alias for deprecated function | ||||||
199 | sub autoindex { | ||||||
200 | 0 | 0 | 0 | 0 | warn "autoindex is deprecated: please use auto_index"; | ||
201 | 0 | 0 | goto &auto_index; | ||||
202 | } | ||||||
203 | |||||||
204 | sub auto_index { | ||||||
205 | 6 | 6 | 1 | 3781 | my $self = shift; | ||
206 | 6 | 12 | my $value = shift; | ||||
207 | 6 | 50 | 27 | return $self->{auto_index} unless defined $value; | |||
208 | 6 | 20 | $self->{auto_index} = $value; | ||||
209 | } | ||||||
210 | |||||||
211 | sub index_file { | ||||||
212 | 0 | 0 | 1 | 0 | my $self = shift; | ||
213 | 0 | 0 | my $value = shift; | ||||
214 | 0 | 0 | 0 | return $self->{index_file} unless defined $value; | |||
215 | 0 | 0 | $self->{index_file} = $value; | ||||
216 | } | ||||||
217 | |||||||
218 | sub _generate_404 { | ||||||
219 | 9 | 9 | 23 | my $self = shift; | |||
220 | 9 | 50 | 41 | my $response = shift || return; | |||
221 | 9 | 48 | return generate_404( $response ); | ||||
222 | } | ||||||
223 | |||||||
224 | sub generate_404 { | ||||||
225 | 9 | 50 | 9 | 1 | 35 | my $response = shift || return; | |
226 | 9 | 50 | 54 | return unless $response->isa('HTTP::Response'); | |||
227 | 9 | 46 | $response->code( 404 ); | ||||
228 | 9 | 156 | $response->header( 'Content-Type', 'text/html' ); | ||||
229 | 9 | 752 | $response->content( start_html('404') . h1('Not Found') . end_html ); | ||||
230 | 9 | 25874 | return $response; | ||||
231 | } | ||||||
232 | |||||||
233 | sub _generate_403 { | ||||||
234 | 6 | 6 | 19 | my $self = shift; | |||
235 | 6 | 50 | 32 | my $response = shift || return; | |||
236 | 6 | 24 | return generate_403( $response ); | ||||
237 | } | ||||||
238 | |||||||
239 | sub generate_403 { | ||||||
240 | 6 | 50 | 6 | 1 | 26 | my $response = shift || return; | |
241 | 6 | 50 | 68 | return unless $response->isa('HTTP::Response'); | |||
242 | 6 | 37 | $response->code( 403 ); | ||||
243 | 6 | 108 | $response->header( 'Content-Type', 'text/html' ); | ||||
244 | 6 | 533 | $response->content( start_html('403') . h1('Forbidden') . end_html ); | ||||
245 | 6 | 2955 | return $response; | ||||
246 | } | ||||||
247 | |||||||
248 | sub _generate_301 { | ||||||
249 | 6 | 6 | 15 | my $self = shift; | |||
250 | 6 | 50 | 34 | my $path = shift || return; | |||
251 | 6 | 50 | 29 | my $response = shift || return; | |||
252 | 6 | 25 | return generate_301( $path, $response ); | ||||
253 | } | ||||||
254 | |||||||
255 | sub generate_301 { | ||||||
256 | 6 | 50 | 6 | 1 | 28 | my $path = shift || return; | |
257 | 6 | 50 | 27 | my $response = shift || return; | |||
258 | 6 | 50 | 38 | return unless $response->isa('HTTP::Response'); | |||
259 | 6 | 45 | $response->code( 301 ); | ||||
260 | 6 | 144 | $response->header( 'Location' => $path ); | ||||
261 | 6 | 493 | $response->header( 'Content-Type', 'text/html' ); | ||||
262 | 6 | 393 | $response->content( start_html('301') . h1('Moved Permanently') . ' The document has moved here. ' . end_html ); |
||||
263 | 6 | 32810 | return $response; | ||||
264 | } | ||||||
265 | |||||||
266 | sub _generate_dir_listing { | ||||||
267 | 0 | 0 | 0 | my $self = shift; | |||
268 | 0 | 0 | 0 | my $path = shift || return; | |||
269 | 0 | 0 | 0 | my $response = shift || return undef; | |||
270 | 0 | 0 | my $content = start_html('Index of ' . $path) . h1('Index of ' . $path) . qq{ \n
|
||||
271 | |||||||
272 | 0 | 0 | my $realpath = $path; | ||||
273 | 0 | 0 | 0 | $realpath = $self->{prefix_path} . $path if $self->{prefix_path}; | |||
274 | 0 | 0 | 0 | $realpath =~ s/^$self->{prefix_fix}// if $self->{prefix_fix}; | |||
275 | |||||||
276 | 0 | 0 | foreach my $item ( $self->{vdir}->list( $realpath ) ) { | ||||
277 | 0 | 0 | 0 | next if $item =~ /^\./; | |||
278 | 0 | 0 | $content .= qq{ |
||||
279 | } | ||||||
280 | |||||||
281 | 0 | 0 | $content .= qq{\n} . end_html; | ||||
282 | 0 | 0 | $response->code( 200 ); | ||||
283 | 0 | 0 | $response->header( 'Content-Type', 'text/html' ); | ||||
284 | 0 | 0 | $response->content( $content ); | ||||
285 | 0 | 0 | return $response; | ||||
286 | } | ||||||
287 | |||||||
288 | sub _read_input { | ||||||
289 | 6 | 6 | 2241 | ${ $_[OBJECT]{read}{$_[ARG1]}{content} } .= $_[ARG0]; | |||
6 | 106 | ||||||
290 | } | ||||||
291 | |||||||
292 | # Read finished | ||||||
293 | sub _read_error { | ||||||
294 | 6 | 6 | 20526 | my ($self, $kernel, $error, $wheelid) = @_[ OBJECT, KERNEL, ARG1, ARG3 ]; | |||
295 | 6 | 29 | my $read = delete $self->{read}{$wheelid}; | ||||
296 | 6 | 22 | my $response = delete $read->{response}; | ||||
297 | 6 | 28 | my $content = delete $read->{content}; | ||||
298 | 6 | 23 | my $mimetype = delete $read->{mimetype}; | ||||
299 | 6 | 22 | my $sender = delete $read->{sender}; | ||||
300 | |||||||
301 | 6 | 53 | delete $read->{wheel}; | ||||
302 | |||||||
303 | 6 | 50 | 2084 | if ($error) { | |||
304 | 0 | 0 | $response->content("Internal Server Error"); | ||||
305 | 0 | 0 | $response->code(500); | ||||
306 | } | ||||||
307 | else { | ||||||
308 | 6 | 50 | 56 | unless ( $mimetype ) { | |||
309 | 0 | 0 | 0 | if ( $self->{mm} ) { | |||
310 | 0 | 0 | $mimetype = $self->{mm}->checktype_contents( $$content ); | ||||
311 | } | ||||||
312 | else { | ||||||
313 | 0 | 0 | $mimetype = 'application/octet-stream'; | ||||
314 | } | ||||||
315 | } | ||||||
316 | 6 | 95 | $response->code( 200 ); | ||||
317 | 6 | 171 | $response->content_type( $mimetype ); | ||||
318 | 6 | 324 | $response->content_ref( $content ); | ||||
319 | } | ||||||
320 | |||||||
321 | 6 | 142 | $kernel->post( $sender => 'DONE' => $response ); | ||||
322 | } | ||||||
323 | |||||||
324 | sub _generate_content { | ||||||
325 | 6 | 6 | 16 | my $self = shift; | |||
326 | 6 | 50 | 27 | my $sender = shift || return; | |||
327 | 6 | 50 | 26 | my $path = shift || return; | |||
328 | 6 | 50 | 26 | my $response = shift || return; | |||
329 | 6 | 10 | my $realpath = $path; | ||||
330 | 6 | 100 | 32 | $realpath = $self->{prefix_path} . $path if $self->{prefix_path}; | |||
331 | 6 | 100 | 47 | $realpath =~ s/^$self->{prefix_fix}// if $self->{prefix_fix}; | |||
332 | |||||||
333 | 6 | 52 | my $mimetype = $self->{mt}->mimeTypeOf( $path ); | ||||
334 | |||||||
335 | 6 | 50 | 1263 | if ( my $fh = $self->{vdir}->open_read( $realpath ) ) { | |||
336 | 6 | 1210 | binmode($fh); | ||||
337 | 6 | 50 | 33 | 66 | if ( $^O eq 'MSWin32' or $self->{blocking} ) { | ||
338 | 0 | 0 | local $/ = undef; | ||||
339 | 0 | 0 | my $content = <$fh>; | ||||
340 | 0 | 0 | 0 | unless ( $mimetype ) { | |||
341 | 0 | 0 | 0 | if ( $self->{mm} ) { | |||
342 | 0 | 0 | $mimetype = $self->{mm}->checktype_contents( $content ); | ||||
343 | } | ||||||
344 | else { | ||||||
345 | 0 | 0 | $mimetype = 'application/octet-stream'; | ||||
346 | } | ||||||
347 | } | ||||||
348 | 0 | 0 | $response->code( 200 ); | ||||
349 | 0 | 0 | $response->content_type( $mimetype ); | ||||
350 | 0 | 0 | $response->content_ref( \$content ); | ||||
351 | } else { | ||||||
352 | 6 | 86 | my $readwrite = POE::Wheel::ReadWrite->new( | ||||
353 | Handle => $fh, | ||||||
354 | Filter => POE::Filter::Stream->new(), | ||||||
355 | InputEvent => "-input", | ||||||
356 | ErrorEvent => "-error", | ||||||
357 | ); | ||||||
358 | |||||||
359 | 6 | 2451 | my $content = ""; | ||||
360 | |||||||
361 | 6 | 33 | my $wheelid = $readwrite->ID; | ||||
362 | 6 | 62 | my $readheap = { | ||||
363 | wheel => $readwrite, | ||||||
364 | response => $response, | ||||||
365 | mimetype => $mimetype, | ||||||
366 | sender => $sender, | ||||||
367 | content => \$content, | ||||||
368 | }; | ||||||
369 | |||||||
370 | 6 | 30 | $self->{read}{$wheelid} = $readheap; | ||||
371 | |||||||
372 | 6 | 19 | return; | ||||
373 | } | ||||||
374 | } else { | ||||||
375 | 0 | 0 | $response = $self->_generate_404( $response ); | ||||
376 | } | ||||||
377 | |||||||
378 | 0 | 0 | return $response; | ||||
379 | } | ||||||
380 | |||||||
381 | sub _massage_handlers { | ||||||
382 | 3 | 50 | 3 | 19 | my $handler = shift || return; | ||
383 | 3 | 50 | 33 | 40 | croak( "HANDLERS is not a ref to an hash!" ) | ||
384 | unless ref $handler and ref $handler eq 'HASH'; | ||||||
385 | 3 | 7 | foreach my $ext ( keys %{ $handler } ) { | ||||
3 | 14 | ||||||
386 | 3 | 50 | 29 | delete $handler->{ $ext } unless ref $handler->{ $ext } eq 'HASH'; | |||
387 | 3 | 50 | 47 | croak( "HANDLER for '$ext' does not have a SESSION argument!" ) | |||
388 | unless $handler->{ $ext }->{'SESSION'}; | ||||||
389 | 3 | 50 | 16 | croak( "HANDLER for '$ext' does not have an EVENT argument!" ) | |||
390 | unless $handler->{ $ext }->{'EVENT'}; | ||||||
391 | 3 | 50 | 31 | $handler->{ $ext }->{'SESSION'} = $handler->{ $ext }->{'SESSION'}->ID() | |||
392 | if UNIVERSAL::isa( $handler->{ $ext }->{'SESSION'}, 'POE::Session' ); | ||||||
393 | } | ||||||
394 | 3 | 10 | return 1; | ||||
395 | } | ||||||
396 | |||||||
397 | sub get_handlers { | ||||||
398 | 0 | 0 | 1 | 0 | my $self = shift; | ||
399 | 0 | 0 | my $handlers = Storable::dclone( $self->{handlers} ); | ||||
400 | 0 | 0 | return $handlers; | ||||
401 | } | ||||||
402 | |||||||
403 | sub set_handlers { | ||||||
404 | 3 | 3 | 1 | 3554 | my $self = shift; | ||
405 | 3 | 50 | 44 | my $handlers = shift || return; | |||
406 | 3 | 16 | _massage_handlers( $handlers ); | ||||
407 | 3 | 8 | $self->{handlers} = $handlers; | ||||
408 | 3 | 11 | return 1; | ||||
409 | } | ||||||
410 | |||||||
411 | 1; | ||||||
412 | |||||||
413 | __END__ |