File Coverage

blib/lib/OurNet/Site.pm
Criterion Covered Total %
statement 37 199 18.5
branch 0 98 0.0
condition 0 8 0.0
subroutine 12 18 66.6
pod 0 5 0.0
total 49 328 14.9


line stmt bran cond sub pod time code
1             package OurNet::Site;
2             require 5.005;
3              
4             $OurNet::Site::VERSION = '1.52';
5              
6 1     1   12 use strict;
  1         3  
  1         97  
7              
8             =head1 NAME
9              
10             OurNet::Site - Extract web pages via templates
11              
12             =head1 SYNOPSIS
13              
14             use LWP::Simple;
15             use OurNet::Site;
16              
17             my ($query, $hits) = ('autrijus', 10);
18             my $found;
19              
20             # Create a bot
21             $bot = OurNet::Site->new('google');
22              
23             # Parse the result got from LWP::Simple
24             $bot->callme($self, 0, get($bot->geturl($query, $hits)), \&callmeback);
25              
26             print '*** ' . ($found ? $found : 'No') . ' match(es) found.';
27              
28             # Callback routine
29             sub callmeback {
30             my ($self, $himself) = @_;
31              
32             foreach my $entry (@{$himself->{response}}) {
33             if ($entry->{url}) {
34             print "*** [$entry->{title}]" .
35             " ($entry->{score})" .
36             " - [$entry->{id}]\n" .
37             " URL: [$entry->{url}]\n" .
38             " $entry->{preview}\n";
39             $found++;
40             delete($entry->{url});
41             }
42             }
43             }
44              
45             =head1 DESCRIPTION
46              
47             This module emulates a typical search engine by reading a XML script
48             defining its aspects, and parses results on-the-fly accordingly.
49              
50             Note that it also takes Inforia Quest .fmt scripts, available at
51             http://www.inforian.com/. The author of course cannot support this
52             usage.
53              
54             As per v1.52, Site.pm also accepts Template Toolkit format templates
55             with extention '.tt2' as site descriptors, provided that it contains
56             at least one C<[% FOREACH entry %]> block, and C<[% SET url.start %]>
57             accordingly.
58              
59             Note that tt2 support is *highly* experimental and should not be
60             relied upon until a more stable release comes.
61              
62             =head1 BUGS
63              
64             Probably lots. Most notably the 'More' facilities is lacking. Also
65             there is no template-generating abilities. This is a must, but I
66             couldn't find enough motivation to do it. Maybe you could.
67              
68             Currently, tt2 does not (quite) support incremental parsing in
69             conjunction with L.
70              
71             =cut
72              
73             # ---------------
74             # Variable Fields
75             # ---------------
76 1     1   12 use vars qw/$Myself/;
  1         2  
  1         77  
77              
78 1         7 use fields qw/id charset proc expression template tempdata
79             name info url var response category score
80 1     1   1869 allow_partial allow_tags tmplobj/;
  1         2399  
81              
82             # -----------------
83             # Package Constants
84             # -----------------
85 1     1   258 use constant PATH_SITE => join('/', ('', split('::', __PACKAGE__), ''));
  1         2  
  1         77  
86 1     1   5 use constant ERROR_SITE_NEEDED => __PACKAGE__ . ' needs a file';
  1         1  
  1         89  
87 1     1   5 use constant ERROR_FILE_NEEDED => __PACKAGE__ . ' cannot find definition for ';
  1         11  
  1         45  
88 1     1   5 use constant ERROR_FORMAT => __PACKAGE__ . ' cannot determine file format for';
  1         2  
  1         55  
89 1         54 use constant CHARSET_MAP => {'JIS' => 'ja-jp.jis', 'EUC' => 'ja-jp.euc',
90 1     1   4 'BIG5' => 'zh-tw', 'GB' => 'zh-cn'};
  1         2  
91 1     1   5 use constant ENTITY_STRIP => '<.*?>|^[\015\012\s]+|[\015\012\s]+$|\t';
  1         1  
  1         73  
92 1         78 use constant ENTITY_MAP => {'nbsp' => ' ', 'quot' => '"', 'amp' => '&',
93 1     1   5 'gt' => '>', 'lt' => '<', 'copy' => '(c)'};
  1         2  
94 1     1   5 use constant ENTITY_LIST => '&('.join('|', keys(%{ENTITY_MAP()})).');';
  1         3  
  1         2  
  1         98  
95              
96             # ---------------------
97             # Subroutine new($site)
98             # ---------------------
99             sub new {
100 0     0 0   my $class = shift;
101             my $self = ($] > 5.00562) ? fields::new($class)
102 1 0   1   13 : do { no strict 'refs';
  1         9  
  1         3863  
  0            
103 0           bless [\%{"$class\::FIELDS"}], $class };
  0            
104 0 0         my $file = $_[0] or (warn(ERROR_SITE_NEEDED), return);
105              
106 0 0         (%{$self} = %{$file}, return $self) if UNIVERSAL::isa($file, 'HASH');
  0            
  0            
107              
108 0 0         unless (-e $file) {
109 0 0         if (-e "$_[0].xml") {
    0          
    0          
110 0           $file = "$_[0].xml";
111             }
112             elsif (-e "$_[0].fmt") {
113 0           $file = "$_[0].fmt";
114             }
115             elsif (-e "$_[0].tt2") {
116 0           $file = "$_[0].tt2";
117             }
118             else {
119 0           foreach my $inc (@INC) {
120 0 0         last if -e ($file = $inc . PATH_SITE . $_[0]);
121 0 0         last if -e ($file = $inc . PATH_SITE . "$_[0].xml");
122 0 0         last if -e ($file = $inc . PATH_SITE . "$_[0].fmt");
123 0 0         last if -e ($file = $inc . PATH_SITE . "$_[0].tt2");
124             }
125             };
126             }
127              
128 0 0         die(ERROR_FILE_NEEDED . $file) if !(-e $file);
129              
130 0           $self->parse($file);
131 0           $self->{tempdata} = '';
132              
133 0           return $self;
134             }
135              
136             # ---------------------------------------
137             # Subroutine geturl($self, $query, $hits)
138             # ---------------------------------------
139             sub geturl {
140 0     0 0   my $self = shift;
141 0           my $url = $self->{url}{start};
142              
143 0           $url =~ s|_QUERY_|$_[0]|g;
144 0           $url =~ s|_HITS_|$_[1]|g;
145 0           $url =~ s|\${ query }|$_[0]|g;
146 0           $url =~ s|\${ hits }|$_[1]|g;
147              
148 0           return $url;
149             }
150              
151             # ------------------------------
152             # Subroutine parse($self, $file)
153             # ------------------------------
154             sub parse {
155 0     0 0   my $self = shift;
156 0           open(local *SITEFILE, $_[0]);
157              
158 0 0         if ($_[0] =~ m|\.tt2$|i) {
    0          
    0          
159 0           local $/;
160 0           my $content = ;
161              
162 0           require OurNet::Template;
163 0           $self->{tmplobj} = OurNet::Template->new();
164 0           $self->{tmplobj}->extract($content, undef, $self);
165             }
166             elsif ($_[0] =~ m|\.xml$|i) {
167 0           local $/;
168 0           my $content = ;
169              
170 0           my $xml_cdata_re = '()?';
171              
172 0 0         $self->{id} = $1 if $content =~ m||i;
173              
174 0           foreach my $tag ('charset', 'score', 'expression', 'template', 'proc') {
175 0 0         $self->{$tag} = $2 if $content =~ m|<$tag>$xml_cdata_re|is;
176             }
177              
178 0           foreach my $tag ('url', 'var', 'name', 'info') {
179 0           $self->{$tag}{lc($1)} = $3 while
180             $content =~ s|<$tag \w+="(.*?)">$xml_cdata_re||is;
181             }
182              
183 0 0         if ($content =~ m|(.*?)|i) {
184 0           $self->{category} = [ split(',', $1) ];
185             }
186             }
187             elsif ($_[0] =~ m|(?:.*[/\\])?(.*?)(?:\.fmt)?$|i) {
188 0           $self->{id} = $1;
189              
190 0           chomp($self->{name}{'en-us'} = );
191 0 0         if ($self->{name}{'en-us'} =~ s|\((.+)\)||) {
192 0           $self->{info}{'en-us'} = $1;
193             }
194              
195 0           chomp($self->{url}{start} = );
196 0 0         if ($self->{url}{start} =~ m|_START_\d+_\d+_|) {
197 0           $self->{url}{more} = $self->{url}{start};
198 0           $self->{url}{start} =~ s|_START_\d+_(\d+)_|$1|;
199             }
200              
201 0           while (chomp($_ = )) {
202             (m|^---|) ? do {
203 0           last;
204             } :
205             (m|^\w+://|) ? do {
206 0           $self->{url}{backup} = $_;
207             } :
208             (m|^MORE\t(.+)|) ? do {
209 0           $self->{url}{more} = $1;
210             } :
211             (m|^PROC\t(.+)|) ? do {
212 0           $self->{proc} = $1;
213             } :
214             (m|^VAR\t(.+)|) ? do {
215 0           $self->{var}{$1} = . $1 . ;
216 0           $self->{var}{$1} =~ s|[\t\015\012]||g;
217             } :
218             (m|^SCORE\t(.+)|) ? do {
219 0           $self->{score} = $1;
220 0           $self->{score} =~ s|\bx\b|_SCORE_|ig;
221 0           $self->{score} =~ s|\by\b|_RANK_|ig;
222             } :
223             (m|^CHARSET\t(.+)|) ? do {
224 0           $self->{charset} = CHARSET_MAP->{uc($1)};
225             } :
226             (m|^CHT\t(.+)|) ? do {
227 0           $self->{name}{'zh-tw'} = $1;
228 0           $self->{info}{'zh-tw'} = $self->{info}{'en-us'};
229             } :
230             (m|^CHS\t(.+)|) ? do {
231 0           $self->{name}{'zh-cn'} = $1;
232 0           $self->{info}{'zh-cn'} = $self->{info}{'en-us'};
233             } :
234             (m|^EXPR\t(.+)|) ? do {
235 0           $self->{expression} = $1;
236             } :
237 0 0         (m|^TYPE\t(.+)|) ? do {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
238 0           $self->{category} = $1;
239             } : undef;
240             }
241              
242 0           chomp($self->{url}{home} = );
243 0           chomp($self->{template} = );
244              
245 0           while (chomp($_ = )) {
246 0 0         next unless m|^[A-Z_]*$|;
247              
248 0 0         $self->{template} .= $_ ? "_${_}_" : '___';
249 0           chomp($self->{template} .= );
250             }
251             }
252             else {
253 0           die(ERROR_FORMAT . $_[0]);
254             };
255              
256 0           close(SITEFILE);
257             }
258              
259             # ---------------------------------------
260             # Subroutine contemplate($self, $content)
261             # ---------------------------------------
262             sub contemplate {
263 0     0 0   my ($self, $content) = @_;
264              
265 0 0         if ($self->{tmplobj}) {
266             # tt2 support goes here
267             # XXX macros, etc incomplete
268 0           my $result = $self->{tmplobj}->extract(undef, $content);
269 0           push @{$self->{response}}, @{$result->{entry}};
  0            
  0            
270 0           return $self;
271             }
272              
273 0           my $template = _quote($self->{template});
274 0           my @vars = map {lc($_)} ($template =~ m|_(\w+?)_|g); # slurp!
  0            
275 0           my $length = length($content);
276 0           $template =~ s|\015?\012?_\w+?_\015?\012?|(.*?)|g;
277              
278 0           while (my @vals = ($content =~ m|$template|is)) {
279 0           $content =~ s|$template||is;
280 0 0         last if $length == length($content); # infinite loop
281 0           $length = length($content);
282              
283 0           my $rank = ($#{$self->{response}} + 2); # begins with 1
  0            
284              
285 0           push(@{$self->{response}}, {'rank' => $rank});
  0            
286 0           my $entry = $self->{response}[$rank - 1];
287 0           $entry->{id} = $self->{id};
288              
289 0           foreach my $idx (0 .. $#vars) {
290 0           my ($var, $val) = ($vars[$idx], $vals[$idx]);
291              
292             # Null variable ___
293 0 0         next if $var eq '_';
294              
295             # Expand HTML entities
296 0 0         if (!$self->{allow_tags}) {
297 0           $val =~ s|@{[ENTITY_STRIP]}||gs;
  0            
298 0           $val =~ s|@{[ENTITY_LIST]}|ENTITY_MAP->{$1}|ge;
  0            
  0            
299             }
300              
301 0 0         if ($var eq 'sizek') {
    0          
    0          
302 0           $entry->{size} = $val * 1024;
303             }
304             elsif ($var eq 'score') {
305 0           my $proc = $self->{score};
306              
307 0           $proc =~ s|_RANK_|$rank|ig;
308 0           $proc =~ s|_SCORE_|$val|ig;
309              
310 0 0         if ($proc =~ m|^\d*|) {
311 0           $entry->{$var} = $proc;
312             }
313             else {
314 0           require Safe;
315              
316 0           my $compartment = Safe->new();
317 0           $compartment->permit_only(qw/:base_core :base_mem/);
318 0           $compartment->share(qw/$rank $val $self/);
319 0           $entry->{$var} = $compartment->reval($proc);
320             }
321             }
322             elsif ($var eq 'url') {
323 0           $entry->{$var} = $val;
324              
325 0 0         if ($entry->{$var} !~ m|^\w+://|) {
326 0 0 0       if ($self->{url}{home}) {
    0          
327 0           $entry->{$var} = $self->{url}{home} . $entry->{$var};
328             }
329             elsif (!$self->{allow_partial} and
330             $self->{url}{start} =~ m|^(\w+://.*?)/|) {
331 0           $entry->{$var} = $1 . $entry->{$var};
332             }
333             }
334             }
335             else {
336 0           $entry->{$var} = $val;
337             };
338             }
339              
340 0 0         if (!$entry->{score}) {
341 0           my $proc = $self->{score};
342 0           $proc =~ s|_RANK_|\$rank|ig;
343 0           $entry->{score} = eval($proc);
344             }
345              
346 0 0         if (my $proc = $self->{proc}) {
347 0           require Safe;
348 0   0       $Myself ||= $self;
349              
350 0           my $compartment = Safe->new();
351 0           $compartment->share(qw/$Myself/);
352 0           $compartment->permit_only(qw/:base_core :base_mem pushre regcmaybe regcreset regcomp/);
353              
354 0           $proc =~ s|_(\w+)_|\$Myself->{response}[$rank - 1]{lc('$1')}|ig;
355 0           $compartment->reval($proc);
356             }
357             }
358              
359 0           undef $Myself;
360 0           return $self;
361             }
362              
363             # ----------------------------------------------------------
364             # Subroutine callme($self, $herself, $id, $data, \&callback)
365             # ----------------------------------------------------------
366             sub callme {
367 0     0 0   my ($self, $herself, $id, $data, $callback) = @_;
368 0           my $template = _quote($self->{template});
369 0           my $count = $#{$self->{response}};
  0            
370              
371             # Append old ones
372 0           $self->{tempdata} = $data = $self->{tempdata} . $data;
373              
374 0 0         unless ($self->{tmplobj}) {
375             # Deep magic here
376 0           $template =~ s|\015?\012?_\w+?_\015?\012?|(.*?)|g; # Find variables
377            
378 0           $template = '^.*' . $template;
379            
380 0           $self->{tempdata} =~ s|$template||is;
381             }
382              
383 0 0         if (defined $callback) {
384 0           return &$callback($herself, $self->contemplate($data));
385             }
386             else {
387 0           return $self->contemplate($data);
388             }
389             }
390              
391             sub _quote {
392 0     0     my $quoted;
393              
394 0   0       foreach my $chunk (split(/({{.*?}})/, $_[0] || '')) {
395 0 0         if ($chunk =~ m|{{(.*?)}}|) {
396 0           $quoted .= $1;
397             }
398             else {
399 0           $quoted .= quotemeta($chunk);
400             }
401             }
402              
403 0           return $quoted;
404             }
405              
406             1;
407              
408             =head1 SEE ALSO
409              
410             L
411              
412             =head1 AUTHORS
413              
414             Autrijus Tang Eautrijus@autrijus.org>
415              
416             =head1 COPYRIGHT
417              
418             Copyright 2001 by Autrijus Tang Eautrijus@autrijus.org>.
419              
420             All rights reserved. You can redistribute and/or modify
421             this module under the same terms as Perl itself.
422              
423             =cut