File Coverage

blib/lib/WWW/Page.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package WWW::Page;
2              
3 1     1   29787 use vars qw ($VERSION);
  1         2  
  1         76  
4             $VERSION = '2.2';
5              
6 1     1   519 use XML::LibXML;
  0            
  0            
7             use XML::LibXSLT;
8             use File::Cache::Persistent;
9             use XSLT::Cache;
10              
11             sub new {
12             my $class = shift;
13             my $args = shift;
14              
15             my $this = {
16             charset => $args->{'charset'} || 'UTF-8',
17             content_type => $args->{'content-type'} || 'text/html',
18             content => '',
19             source => $args->{'source'} || $ENV{'PATH_TRANSLATED'},
20             script_filename => $args->{'script-filename'} || $ENV{'SCRIPT_FILENAME'},
21             lib_root => $args->{'lib-root'},
22             document_root => $args->{'document-root'} || $ENV{'DOCUMENT_ROOT'},
23             xslt_root => $args->{'xslt-root'} || "$ENV{'DOCUMENT_ROOT'}/xsl",
24             request_uri => $args->{'request-uri'} || $ENV{'REQUEST_URI'},
25              
26             xml => undef,
27             code => undef,
28             xml_cache => new File::Cache::Persistent(
29             reader => \&xml_reader,
30             timeout => $args->{'timeout'} || undef
31             ),
32             xsl_cache => new XSLT::Cache(
33             timeout => $args->{'timeout'} || undef
34             ),
35             xslt_path => undef,
36             };
37              
38             bless $this, $class;
39              
40             return $this;
41             }
42              
43             sub run {
44             my ($this, %args) = @_;
45              
46             $this->{'param'} = _read_params();
47            
48             $this->{'header'} = {
49             'Content-Type' => "$this->{'content_type'}; charset=$this->{'charset'}"
50             };
51              
52             for my $key (keys %args) {
53             $this->{$key} = $args{$key};
54             }
55              
56             $this->readSource();
57             $this->appendInfo();
58              
59             $this->importCode();
60             $this->executeCode();
61              
62             $this->readXSL();
63             $this->transformXML();
64             }
65              
66             sub as_string {
67             my $this = shift;
68              
69             $this->run();
70              
71             return $this->response();
72             }
73              
74             sub response {
75             my $this = shift;
76              
77             return $this->header() . $this->content();
78             }
79              
80             sub readSource {
81             my $this = shift;
82              
83             my $cache = $this->{'xml_cache'}->get($this->{'source'});
84              
85             my $cache_dom = $cache->documentElement()->cloneNode(1);
86              
87             my $dom = new XML::LibXML::Document();
88             $dom->setDocumentElement($cache_dom);
89             $this->{'xml'} = $dom;
90            
91             my @contentType = $this->{'xml'}->findnodes('/page/@content-type');
92             if (@contentType) {
93             $this->{'header'}->{'Content-Type'} = $contentType[0]->firstChild->data;
94             }
95             }
96              
97             sub xml_reader {
98             my $path = shift;
99              
100             my $xmlParser = new XML::LibXML();
101              
102             return $xmlParser->parse_file($path);
103             }
104              
105             sub xsl_reader {
106             my $path = shift;
107              
108             my $xslParser = new XML::LibXSLT();
109              
110             return $xslParser->parse_file($path);
111             }
112              
113             sub appendInfo {
114             my $this = shift;
115              
116             my @manifest = $this->{'xml'}->findnodes('/page/manifest');
117             if (@manifest) {
118             my $manifest = $manifest[0];
119              
120             my $request = new XML::LibXML::Element('request');
121             $manifest->appendChild($request);
122            
123             $request->appendTextChild('server', $ENV{SERVER_NAME});
124             my ($uri, $query_string) = split /\?/, $this->{'request_uri'}, 2;
125             $request->appendTextChild('uri', $uri);
126             $request->appendTextChild('query-string', $query_string);
127              
128             my $source = $this->{'source'};
129             $source =~ s{^$ENV{DOCUMENT_ROOT}}{};
130             $request->appendTextChild('source', $source);
131              
132             my ($sec, $min, $hour, $mday, $mon, $year, $wday) = localtime (time);
133             my $dateNode = new XML::LibXML::Element('date');
134             $manifest->appendChild($dateNode);
135             $dateNode->setAttribute('year', 1900 + $year);
136             $dateNode->setAttribute('day', $mday);
137             $dateNode->setAttribute('month', $mon + 1);
138             $dateNode->setAttribute('hour', $hour);
139             $dateNode->setAttribute('min', $min);
140             $dateNode->setAttribute('sec', $sec);
141             $dateNode->setAttribute('wday', $wday);
142             }
143             }
144              
145             sub importCode {
146             my $this = shift;
147              
148             my $base = $this->{'lib_root'};
149             unless ($base) {
150             $base = $this->{'script_filename'};
151             ($base) = $base =~ m{^(.*)/[^/]+$};
152             }
153             unshift @INC, $base;
154              
155             my @imports = $this->{'xml'}->findnodes('/page/@import');
156             if (@imports) {
157             my $module = $imports[0]->firstChild->data;
158             my $pm = $module;
159             $pm =~ s{::}{/}g;
160             $pm .= '.pm';
161             require "$base/$pm";
162             $this->{'code'} = $module->import();
163             }
164             }
165              
166             sub readXSL {
167             my $this = shift;
168              
169             if (defined $this->param('viewxml')) {
170             $this->{'header'}->{'Content-Type'} = "text/xml; charset=$this->{'charset'}";
171             return;
172             }
173              
174             my $base = $this->{'xslt_root'};
175             my @transforms = $this->{xml}->findnodes('/page/@transform');
176              
177             if (@transforms) {
178             $this->{'xslt_path'} = "$base/" . $transforms[0]->firstChild->data;
179             }
180             else {
181             $this->{'xslt_path'} = undef;
182             $this->{'header'}->{'Content-Type'} = 'text/xml';
183             }
184             }
185              
186             sub executeCode {
187             my $this = shift;
188              
189             my $context = new XML::LibXML::XPathContext;
190             $context->registerNs('page', 'urn:www-page');
191              
192             my @codeNodes = $context->findnodes('/page//page:*', $this->{'xml'});
193             foreach my $codeNode (@codeNodes) {
194             my $nodeName = $codeNode->nodeName();
195             $nodeName =~ s/^.*://;
196             my $function = $nodeName;
197             $function =~ s/-(\w)?/defined $1 ? uc $1 : '_'/ge;
198              
199             my @attributes = $codeNode->getAttributes();
200             my %arguments = ();
201             foreach my $attribute (@attributes){
202             $arguments{$attribute->nodeName()} = $attribute->value();
203             }
204              
205             my $newNode = new XML::LibXML::Element($nodeName);
206             $newNode = $this->{'code'}->$function($this, $newNode, \%arguments);
207             $codeNode->replaceNode ($newNode);
208             }
209             }
210              
211             sub transformXML {
212             my $this = shift;
213              
214             if ($this->{'xslt_path'} && !defined $this->param('viewxml')) {
215             eval {
216             $this->{'content'} = $this->{'xsl_cache'}->transform($this->{'xml'}, $this->{'xslt_path'});
217             };
218             if ($@) {
219             $this->{'reader_error'} = $this->{'xsl_cache'}->reader_error();
220             }
221             }
222             else {
223             $this->{'content'} = $this->{'xml'}->toString();
224             }
225             }
226              
227             sub clearXSLcache {
228             my $this = shift;
229            
230             $this->{'xsl_cache'}->remove($this->{'xslt_path'}) if defined $this->{'xslt_path'};
231             }
232              
233             sub header {
234             my $this = shift;
235              
236             my $ret = '';
237             foreach my $key (keys %{$this->{'header'}}){
238             my $value = $this->{'header'}->{$key};
239             $ret .= "$key: $value\n";
240             }
241              
242             return "$ret\n";
243             }
244              
245             sub content {
246             my $this = shift;
247              
248             return $this->{'content'};
249             }
250              
251             sub error {
252             my $this = shift;
253            
254             return $this->{'reader_error'};
255             }
256              
257             sub param {
258             my $this = shift;
259             my $name = shift;
260              
261             return $this->{'param'}->{$name};
262             }
263              
264             sub _read_params {
265             my $params = '';
266              
267             my %param = ();
268             if ($ENV{CONTENT_TYPE} =~ m/multipart\/form-data/){
269             # parse_multipart();
270             # to get uploaded files you should use either some kind of CGI module or future version of WWW::Page :-)
271             }
272             else {
273             my $buf;
274             my $BUFLEN = 4096;
275             while (my $bytes = sysread STDIN, $buf, $BUFLEN) {
276             if ($bytes == $BUFLEN) {
277             $params .= $buf;
278             }
279             else {
280             $params .= substr $buf, 0, $bytes;
281             }
282             }
283             }
284              
285             $params .= '&' . $ENV{QUERY_STRING};
286             foreach (split /&/, $params) {
287             my ($name, $value) = (m/(.*)=(.*)/);
288             if ($name =~ /\S/) {
289             $param{$name} = _urldecode($value);
290             }
291             }
292              
293             return \%param;
294             }
295              
296             sub _urldecode {
297             my $val = shift;
298              
299             # Known limitation: currently does not support Unicode query strings. Use future versions.
300              
301             $val =~ s/\+/ /g;
302             $val =~ s/%([0-9A-H]{2})/pack('C',hex($1))/ge;
303              
304             return $val;
305             }
306              
307             1;
308              
309             =head1 NAME
310              
311             WWW::Page - XSLT-based and XML-configured website engine
312              
313             =head1 SYNOPSIS
314              
315             mod_perl custom handler
316              
317             use WWW::Page;
318              
319             my $page = new WWW::Page({
320             'xslt-root' => "$ENV{DOCUMENT_ROOT}/../data/xsl",
321             'lib-root' => "$ENV{DOCUMENT_ROOT}/../lib",
322             'timeout' => 30,
323             });
324              
325             sub handler {
326             my $r = shift;
327              
328             $page->run(
329             source => "$ENV{DOCUMENT_ROOT}/index.xml",
330             request_uri => $ENV{REQUEST_URI}
331             );
332             print $page->response();
333              
334             return Apache2::Const::OK;
335             }
336              
337             XML-based page description
338              
339            
340            
341             import="Import::Client"
342             transform="view.xsl"
343             xmlns:page="urn:www-page">
344              
345            
346             My website
347             en-gb
348            
349            
350              
351            
352            
353            
354            
355              
356             Parts of imported controller script
357              
358             package Import::Client;
359             use utf8;
360             use XML::LibXML;
361              
362             sub keywordList
363             {
364             my ($this, $page, $node, $args) = @_;
365              
366             my $sth = $dbh->prepare("select keyword, uri from keywords order by keyword");
367             $sth->execute();
368             while (my ($keyword, $uri) = $sth->fetchrow_array())
369             {
370             my $item = $page->{'xml'}->createElement ('item');
371             $item->appendText($keyword);
372             $item->setAttribute('uri', $uri);
373             $node->appendChild($item);
374             }
375              
376             return $node;
377             }
378              
379             =head1 ABSTRACT
380              
381             WWW::Page makes website built on XSLT technology easy to start. It provides simple mechanism to describe
382             behaviour of pages in XML files, adds external logic and applies XSL transformations. Both XML and XSLT files
383             are being transparently caching.
384              
385             =head1 DESCRIPTION
386              
387             This module provides a framework for organizing XSLT-based websites. It allows to put the process of
388             calling user subroutines and applying XSL transformations behind the scene. Wherever possible, XML and XSL
389             documents are cached which eliminates the need of useles reloading and re-parsing them.
390              
391             =head1 EXAMPLE
392              
393             Directory C in the repository contains an example of sample website running under mod_perl and WWW::Page.
394              
395             =head2 Known limitations
396              
397             GET and POST parser cannot accept uploaded files and Unicode-encoded strings.
398              
399             Example does allow only one editor user; only latin symbols may be in keyword list.
400              
401             =head1 AUTHOR
402              
403             Andrew Shitov,
404              
405             =head1 COPYRIGHT AND LICENCE
406              
407             WWW::Page module is a free software.
408             You may resistribute and (or) modify it under the same terms as Perl.
409              
410             =cut