File Coverage

lib/URL/Transform.pm
Criterion Covered Total %
statement 114 135 84.4
branch 25 50 50.0
condition 6 10 60.0
subroutine 25 27 92.5
pod 11 11 100.0
total 181 233 77.6


line stmt bran cond sub pod time code
1             package URL::Transform;
2              
3             =head1 NAME
4              
5             URL::Transform - perform URL transformations in various document types
6              
7             =head1 SYNOPSIS
8              
9             my $output;
10             my $urlt = URL::Transform->new(
11             'document_type' => 'text/html;charset=utf-8',
12             'content_encoding' => 'gzip',
13             'output_function' => sub { $output .= "@_" },
14             'transform_function' => sub { return (join '|', @_) },
15             );
16             $urlt->parse_file($Bin.'/data/URL-Transform-01.html');
17              
18             print "and this is the output: ", $output;
19              
20             =head1 DESCRIPTION
21              
22             URL::Transform is a generic module to perform an url transformation in
23             a documents. Accepts callback function using which the url link can be
24             changed.
25              
26             There are different modules to handle different document types, elements
27             or attributes:
28              
29             =over 4
30              
31             =item C, C, C, C
32              
33             L,
34             L (incomplete was used only to benchmark)
35              
36             =item C
37              
38             L
39              
40             =item C
41              
42             L
43              
44             =item C
45              
46             L
47              
48             =back
49              
50             By passing C option to the C<< URL::Transform->new() >> constructor you
51             can set what library will be used to parse and execute the output and transform
52             functions. Note that the elements inside for example C that are
53             of a different type will be transformed via L modules.
54              
55             C is called with following arguments:
56              
57             transform_function->(
58             'tag_name' => 'img',
59             'attribute_name' => 'src',
60             'url' => 'http://search.cpan.org/s/img/cpan_banner.png',
61             );
62              
63             and must return (un)modified url as the return value.
64              
65             C is called with (already modified) document chunk for outputting.
66              
67             =cut
68              
69 2     2   263238 use warnings;
  2         4  
  2         60  
70 2     2   9 use strict;
  2         4  
  2         65  
71              
72             our $VERSION = '0.01';
73              
74 2     2   1392 use Carp::Clan;
  2         9323  
  2         13  
75 2     2   1948 use English '$EVAL_ERROR';
  2         4678  
  2         139  
76 2     2   2310 use HTML::Tagset;
  2         2653  
  2         74  
77 2     2   1001 use Compress::Zlib;
  2         90987  
  2         687  
78 2     2   17 use File::Slurp 'read_file';
  2         3  
  2         98  
79              
80 2     2   11 use base 'Class::Accessor::Fast';
  2         4  
  2         1733  
81              
82             =head1 PROPERTIES
83              
84             content_encoding
85             document_type
86             parser
87             transform_function
88             output_function
89              
90             =over 4
91              
92             =item parser
93              
94             For HTML/XML can be HTML::Parser, XML::SAX
95              
96             =item document_type
97              
98             text/html - default
99              
100             =item transform_function
101              
102             Function that will be called to make the transformation. The function will receive
103             one argument - url text.
104              
105             =item output_function
106              
107             Reference to function that will receive resulting output. The default one is to use
108             print.
109              
110             =item content_encoding
111              
112             Can be set to C or C. By default it is C, so there is
113             no content encoding.
114              
115             =back
116              
117             =cut
118              
119             __PACKAGE__->mk_accessors(qw{
120             document_type
121             content_encoding
122             transform_function
123             output_function
124             parser
125             supported_document_types
126             });
127              
128             =head1 METHODS
129              
130             =head2 new
131              
132             Object constructor.
133              
134             Requires C a CODE ref argument.
135              
136             The rest of the arguments are optional. Here is the list with defaults:
137              
138             document_type => 'text/html;charset=utf-8',
139             output_function => sub { print @_ },
140             parser => 'HTML::Parser',
141             content_encoding => undef,
142              
143             =cut
144              
145             my %supported_document_types = (
146             'text/html' => 'HTML::Parser',
147             'text/vnd.wap.wml' => 'HTML::Parser',
148             'application/xhtml+xml' => 'HTML::Parser',
149             'application/vnd.wap.xhtml+xml' => 'HTML::Parser',
150             'text/css' => 'CSS::RegExp',
151             'text/html/meta-content' => 'HTML::Meta',
152             'application/x-javascript' => 'Remove',
153             );
154              
155             my %supported_content_encoding = (
156             'gzip' => 1,
157             'deflate' => 1,
158             );
159              
160             sub new {
161 6     6 1 5209 my $class = shift;
162 6         79 my $self = $class->SUPER::new({
163             'supported_document_types' => {},
164             @_
165             });
166              
167 6 50       100 croak 'pass transform_function'
168             if not ref $self->transform_function eq 'CODE';
169              
170             # default document type
171 6   100     76 my ($document_type, $encoding) = split ';', $self->document_type || '';
172 6   100     69 $document_type ||= 'text/html';
173              
174             # default output function
175 0     0   0 $self->output_function(sub { print @_ })
176 6 50       22 if not defined $self->output_function;
177              
178             # setup thinks for parsing html documents
179 6   66     53 my $parser = $self->parser || $supported_document_types{$document_type};
180 6 50       61 croak 'unsupported document type: ', $document_type
181             if not $parser;
182              
183             # check content_encoding
184 6         19 my $content_encoding = $self->content_encoding;
185 6 100       36 if ($content_encoding){
186 1 50       5 croak 'unsupported content_encoding: ', $content_encoding
187             if (not $supported_content_encoding{$content_encoding});
188             }
189              
190             # construct parser object
191 6         12 eval {
192 2     2   7960 no strict 'refs';
  2         5  
  2         2432  
193 6         15 $parser = 'URL::Transform::using::'.$parser;
194 2     2   874 eval 'use '.$parser;
  2     1   4  
  2     1   23  
  1     1   8  
  1     1   2  
  1         9  
  1         7  
  1         2  
  1         7  
  1         7  
  1         1  
  1         10  
  1         7  
  1         2  
  1         10  
  6         578  
195             $parser = $parser->new(
196             'output_function' => $self->output_function,
197             'transform_function' => $self->transform_function,
198 64     64   330 'parser_for' => sub { $self->default_for(@_) },
199 6         240 );
200             };
201 6 50       23 croak 'error loading parser "'.$parser.'" - '.$EVAL_ERROR.' ' if $EVAL_ERROR;
202              
203 6         23 $self->parser($parser);
204              
205 6         42 return $self;
206             }
207              
208              
209             =head2 default_for($document_type)
210              
211             Returns default parser for a supplied $document_type.
212              
213             Can be used also as a set function with additional argument - parser name.
214              
215             If called as object method set the default parser for the object.
216             If called as module function set the default parser for a whole module.
217              
218             =cut
219              
220             sub default_for {
221 64     64 1 86 my $self = shift;
222            
223             # if called from object get/set object parsers for different content types
224 64 50       114 if (ref $self) {
225 64         68 my $document_type = shift;
226              
227             # if case of set
228 64 50       122 if (@_ > 0) {
229 0         0 $self->supported_document_types->{$document_type} = shift;
230             }
231            
232 64 50       347 return exists($self->{supported_document_types}->{$document_type})
233             ? $self->{supported_document_types}->{$document_type}
234             : $supported_document_types{$document_type};
235             }
236             # if called directly get/set the module defaults
237             else {
238 0         0 my $document_type = $self;
239            
240             # if case of set
241 0 0       0 if (@_ > 0) {
242 0         0 $supported_document_types{$document_type} = shift;
243             }
244            
245 0         0 return $supported_document_types{$document_type};
246             }
247             }
248              
249              
250             =head2 parse_string($string)
251              
252             Submit document as a string for parsing.
253              
254             This some function must be implemented by helper parsing classes.
255              
256             =cut
257              
258             sub parse_string {
259 3     3 1 2920 my $self = shift;
260 3         7 my $data = shift;
261              
262 3         16 return $self->parser->parse_string(
263             $self->decode_string($data)
264             );
265             }
266              
267              
268             =head2 parse_chunk($chunk)
269              
270             Submit chunk of a document for parsing.
271              
272             This some function should be implemented by helper parsing classes.
273              
274             =cut
275              
276             sub parse_chunk {
277 6     6 1 1042 my $self = shift;
278 6         7 my $data = shift;
279              
280 6         16 my $parser = $self->parser;
281              
282 6 50       30 if ($self->can_parse_chunks) {
283 6         17 return $self->parser->parse_chunk($data);
284             }
285             else {
286 0         0 die $self->parser.' is not able to parse in chunks. :(';
287             }
288             }
289              
290              
291             =head2 can_parse_chunks
292              
293             Return true/false if the parser can parse in chunks.
294              
295             =cut
296              
297             sub can_parse_chunks {
298 7     7 1 1040 my $self = shift;
299 7         19 my $parser = $self->parser;
300              
301 7 50       36 if ( defined $self->content_encoding ) {
302 0         0 return 0;
303             }
304              
305 7         87 return $parser->can('parse_chunk');
306             }
307              
308              
309             =head2 parse_file($file_name)
310              
311             Submit file for parsing.
312              
313             This some function should be implemented by helper parsing classes.
314              
315             =cut
316              
317             sub parse_file {
318 7     7 1 2315 my $self = shift;
319              
320             # if the content is not encoded the call parser parse_file method
321 7 100       26 return $self->parser->parse_file(@_)
322             if not $self->content_encoding;
323              
324             # otherwise use parse_string that uses decode_string
325 1         10 return $self->parse_string(scalar read_file(@_))
326             }
327              
328              
329             =head2 link_tags
330              
331             # To simplify things, reformat the %HTML::Tagset::linkElements
332             # hash so that it is always a hash of hashes.
333              
334             # Construct a hash of tag names that may have links.
335              
336             =cut
337              
338             # FIXME should be moved outside or URL::Transform because it HTML specific
339             my $_link_tags;
340             sub link_tags {
341             # if it's already generate return the reference right away
342 2 50   2 1 13 return $_link_tags if defined $_link_tags;
343              
344 2         4 my %link_tags;
345              
346             # meta can have a refresh url in the content, wml has some special tags
347 2         66 my %link_elements = (
348             %HTML::Tagset::linkElements,
349             'meta' => 'content',
350             'go' => 'href',
351             'card' => 'ontimer',
352             'option' => 'onpick',
353             );
354            
355             # To simplify things, reformat the %HTML::Tagset::linkElements
356             # hash so that it is always a hash of hashes.
357 2         15 while (my($k,$v) = each %link_elements) {
358 64 100       107 if (ref($v)) {
359 56         68 $v = { map {$_ => 1} @$v };
  86         237  
360             }
361             else {
362 8         16 $v = { $v => 1};
363             }
364 64         199 $link_tags{$k} = $v;
365             }
366              
367             # attributes that match all tags
368 2 50       11 $link_tags{''} = {}
369             if not exists $link_tags{''};
370              
371             # add tags with style
372 2         6 $link_tags{''}->{'style'} = 1;
373 2         6 $link_tags{'style'}->{''} = 1;
374              
375             # add tags with javascript
376 2         3 foreach my $attr (keys %{js_attributes()}) {
  2         7  
377 36         50 $link_tags{''}->{$attr} = 1;
378             }
379 2         7 $link_tags{'script'}->{''} = 1;
380              
381             # Uncomment this to see what HTML::Tagset::linkElements thinks are
382             # the tags with link attributes
383             #use Data::Dump; Data::Dump::dump(\%link_tags); exit;
384            
385 2         4 $_link_tags = \%link_tags;
386 2         13 return $_link_tags;
387             }
388              
389              
390             =head2 js_attributes
391              
392             # Construct a hash of all possible JavaScript attribute names
393              
394             =cut
395              
396             # FIXME should be moved outside or URL::Transform because it HTML specific
397             my $_js_attributes;
398             sub js_attributes {
399             # if it's already generate return the reference right away
400 3 100   3 1 16 return $_js_attributes if defined $_js_attributes;
401              
402             # taken from http://www.w3.org/TR/html401/interact/scripts.html#h-18.2.3
403 2         19 my @js_attributes = qw(
404             onload
405             onunload
406             onclick
407             ondblclick
408             onmousedown
409             onmouseup
410             onmouseover
411             onmousemove
412             onmouseout
413             onfocus
414             onblur
415             onkeypress
416             onkeydown
417             onkeyup
418             onsubmit
419             onreset
420             onselect
421             onchange
422             );
423              
424 36         78 $_js_attributes = {
425 2         5 map { $_ => 1 } @js_attributes
426             };
427 2         15 return $_js_attributes;
428             }
429              
430              
431             =head2 decode_string($string)
432              
433             Will return decoded string suitable for parsing. Decoding
434             is chosen according to the $self->content_encoding.
435              
436             Decoding is run automatically for every chunk/string/file.
437              
438             =cut
439              
440             sub decode_string {
441 3     3 1 24 my $self = shift;
442 3         6 my $data = shift;
443              
444 3         20 my $content_encoding = $self->content_encoding;
445              
446             # just ignore without content encoding
447 3 100       29 return $data
448             if (not $content_encoding);
449              
450 1 50       9 return Compress::Zlib::memGunzip($data)
451             if ($content_encoding eq 'gzip');
452            
453 0 0       0 if ($content_encoding eq 'deflate') {
454 0         0 my $raw = Compress::Zlib::uncompress($data);
455 0 0       0 if ( defined $raw ) {
456 0         0 return $raw;
457             }
458             else {
459             # "Content-Encoding: deflate" is supposed to mean the "zlib"
460             # format of RFC 1950, but Microsoft got that wrong, so some
461             # servers sends the raw compressed "deflate" data. This
462             # tries to inflate this format.
463 0         0 my($i, $status) = Compress::Zlib::inflateInit(
464             WindowBits => -Compress::Zlib::MAX_WBITS(),
465             );
466 0         0 my $OK = Compress::Zlib::Z_OK();
467 0 0 0     0 die "Can't init inflate object" unless $i && $status == $OK;
468 0         0 ($raw, $status) = $i->inflate(\$data);
469 0         0 return $raw;
470             }
471             }
472              
473 0         0 return $data;
474             }
475              
476              
477             =head2 encode_string($string)
478              
479             Will return encoded string. Encoding
480             is chosen according to the $self->content_encoding.
481              
482             NOTE if you want to have your content encoded back to the
483             $self->content_encoding you will have to run this method
484             in your code. Argument to the C are always
485             plain text.
486              
487             =cut
488              
489             sub encode_string {
490 1     1 1 923 my $self = shift;
491 1         4 my $data = shift;
492              
493 1         4 my $content_encoding = $self->content_encoding;
494              
495             # just ignore without content encoding
496 1 50       24 return $data
497             if not $content_encoding;
498              
499 1 50       10 return Compress::Zlib::memGzip($data)
500             if ($content_encoding eq 'gzip');
501              
502 0 0         return Compress::Zlib::compress($data)
503             if ($content_encoding eq 'deflate');
504              
505 0           return $data;
506             }
507              
508              
509             =head2 get_supported_content_encodings()
510              
511             Returns hash reference of supported content encodings.
512              
513             =cut
514              
515             sub get_supported_content_encodings {
516 0     0 1   return \%supported_content_encoding;
517             }
518              
519              
520             1;
521              
522              
523             __END__