Make Evergreen Perl modules installable via Module::Build to match OpenSRF
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / WWW / EGWeb.pm
1 package OpenILS::WWW::EGWeb;
2 use strict; use warnings;
3 use Template;
4 use XML::Simple;
5 use XML::LibXML;
6 use File::stat;
7 use Apache2::Const -compile => qw(OK DECLINED HTTP_INTERNAL_SERVER_ERROR);
8 use Apache2::Log;
9 use OpenSRF::EX qw(:try);
10
11 use constant OILS_HTTP_COOKIE_SKIN => 'oils:skin';
12 use constant OILS_HTTP_COOKIE_THEME => 'oils:theme';
13 use constant OILS_HTTP_COOKIE_LOCALE => 'oils:locale';
14
15 my $web_config;
16 my $web_config_file;
17 my $web_config_edit_time;
18
19 sub import {
20     my $self = shift;
21     $web_config_file = shift;
22     unless(-r $web_config_file) {
23         warn "Invalid web config $web_config_file";
24         return;
25     }
26     check_web_config();
27 }
28
29
30 sub handler {
31     my $r = shift;
32     check_web_config($r); # option to disable this
33     my $ctx = load_context($r);
34     my $base = $ctx->{base_path};
35     my($template, $page_args, $as_xml) = find_template($r, $base, $ctx);
36     return Apache2::Const::DECLINED unless $template;
37
38     $template = $ctx->{skin} . "/$template";
39     $ctx->{page_args} = $page_args;
40     $r->content_type('text/html; encoding=utf8');
41
42     my $tt = Template->new({
43         OUTPUT => ($as_xml) ?  sub { parse_as_xml($r, $ctx, @_); } : $r,
44         INCLUDE_PATH => $ctx->{template_paths},
45     });
46
47     unless($tt->process($template, {ctx => $ctx})) {
48         $r->log->warn('Template error: ' . $tt->error);
49         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
50     }
51
52     return Apache2::Const::OK;
53 }
54
55 sub parse_as_xml {
56     my $r = shift;
57     my $ctx = shift;
58     my $data = shift;
59
60     my $success = 0;
61
62     try { 
63         my $doc = XML::LibXML->new->parse_string($data); 
64         $data = $doc->documentElement->toStringC14N;
65         $data = $ctx->{final_dtd} . "\n" . $data;
66         $success = 1;
67     } otherwise {
68         my $e = shift;
69         my $err = "Invalid XML: $e";
70         $r->log->error($err);
71         $r->content_type('text/plain; encoding=utf8');
72         $r->print("\n$err\n\n$data");
73     };
74
75     $r->print($data) if ($success);
76 }
77
78
79 sub load_context {
80     my $r = shift;
81     my $cgi = CGI->new;
82     my $ctx = $web_config->{ctx};
83     $ctx->{hostname} = $r->hostname;
84     $ctx->{base_url} = $cgi->url(-base => 1);
85     $ctx->{skin} = $cgi->cookie(OILS_HTTP_COOKIE_SKIN) || 'default';
86     $ctx->{theme} = $cgi->cookie(OILS_HTTP_COOKIE_THEME) || 'default';
87     $ctx->{locale} = 
88         $cgi->cookie(OILS_HTTP_COOKIE_LOCALE) || 
89         parse_accept_lang($r->headers_in->get('Accept-Language')) || 'en-US';
90     $r->log->debug('skin = ' . $ctx->{skin} . ' : theme = ' . 
91         $ctx->{theme} . ' : locale = ' . $ctx->{locale});
92     return $ctx;
93 }
94
95 # turn Accept-Language into sometihng EG can understand
96 sub parse_accept_lang {
97     my $al = shift;
98     return undef unless $al;
99     my ($locale) = split(/,/, $al);
100     ($locale) = split(/;/, $locale);
101     return undef unless $locale;
102     $locale =~ s/-(.*)/eval '-'.uc("$1")/e;
103     return $locale;
104 }
105
106 # Given a URI, finds the configured template and any extra page 
107 # arguments (trailing path info).  Any extra data is returned
108 # as page arguments, in the form of an array, one item per 
109 # /-separated URI component
110 sub find_template {
111     my $r = shift;
112     my $base = shift;
113     my $ctx = shift;
114     my $skin = $ctx->{skin};
115     my $path = $r->uri;
116     $path =~ s/$base//og;
117     my @parts = split('/', $path);
118     my $template = '';
119     my $page_args = [];
120     my $as_xml = $ctx->{force_valid_xml};
121     my $handler = $web_config->{handlers};
122
123     while(@parts) {
124         my $part = shift @parts;
125         next unless $part;
126         my $t = $handler->{$part};
127         if(ref($t) eq 'PathConfig') {
128             $template = $t->{template};
129             $as_xml = ($t->{as_xml} and $t->{as_xml} =~ /true/io) || $as_xml;
130             $page_args = [@parts];
131             last;
132         } else {
133             $handler = $t;
134         }
135     }
136
137     unless($template) { # no template configured
138
139         # see if we can magically find the template based on the path and default extension
140         my $ext = $ctx->{default_template_extension};
141
142         my @parts = split('/', $path);
143         my $localpath = $path;
144         my @args;
145         while(@parts) {
146             last unless $localpath;
147             for my $tpath (@{$ctx->{template_paths}}) {
148                 my $fpath = "$tpath/$skin/$localpath.$ext";
149                 $r->log->debug("looking at possible template $fpath");
150                 if(-r $fpath) {
151                     $template = "$localpath.$ext";
152                     last;
153                 }
154             }
155             last if $template;
156             push(@args, pop @parts);
157             $localpath = '/'.join('/', @parts);
158         } 
159
160         $page_args = [@args];
161
162         # no template configured or found
163         unless($template) {
164             $r->log->warn("No template configured for path $path");
165             return ();
166         }
167     }
168
169     $r->log->debug("template = $template : page args = @$page_args");
170     return ($template, $page_args, $as_xml);
171 }
172
173 # if the web configuration file has never been loaded or has
174 # changed since the last load, reload it
175 sub check_web_config {
176     my $r = shift;
177     my $epoch = stat($web_config_file)->mtime;
178     unless($web_config_edit_time and $web_config_edit_time == $epoch) {
179         $r->log->debug("Reloading web config after edit...") if $r;
180         $web_config_edit_time = $epoch;
181         $web_config = parse_config($web_config_file);
182     }
183 }
184
185 sub parse_config {
186     my $cfg_file = shift;
187     my $data = XML::Simple->new->XMLin($cfg_file);
188     my $ctx = {};
189     my $handlers = {};
190
191     $ctx->{media_prefix} = (ref $data->{media_prefix}) ? '' : $data->{media_prefix};
192     $ctx->{base_path} = (ref $data->{base_path}) ? '' : $data->{base_path};
193     $ctx->{template_paths} = [];
194     $ctx->{force_valid_xml} = ($data->{force_valid_xml} =~ /true/io) ? 1 : 0;
195     $ctx->{default_template_extension} = $data->{default_template_extension} || 'tt2';
196     $ctx->{web_dir} = $data->{web_dir};
197
198     my $tpaths = $data->{template_paths}->{path};
199     $tpaths = [$tpaths] unless ref $tpaths;
200     push(@{$ctx->{template_paths}}, $_) for @$tpaths;
201
202     for my $handler (@{$data->{handlers}->{handler}}) {
203         my @parts = split('/', $handler->{path});
204         my $h = $handlers;
205         my $pcount = scalar(@parts);
206         for(my $i = 0; $i < $pcount; $i++) {
207             my $p = $parts[$i];
208             unless(defined $h->{$p}) {
209                 if($i == $pcount - 1) {
210                     $h->{$p} = PathConfig->new(%$handler);
211                     last;
212                 } else {
213                     $h->{$p} = {};
214                 }
215             }
216             $h = $h->{$p};
217         }
218     }
219
220     return {ctx => $ctx, handlers => $handlers};
221 }
222
223 package PathConfig;
224 sub new {
225     my($class, %args) = @_;
226     return bless(\%args, $class);
227 }
228
229
230 1;