Tpac: Support for skinning via Apache configuration
[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 Encode;
8 use Apache2::Const -compile => qw(OK DECLINED HTTP_INTERNAL_SERVER_ERROR);
9 use Apache2::Log;
10 use OpenSRF::EX qw(:try);
11 use OpenILS::Utils::CStoreEditor;
12
13 use constant OILS_HTTP_COOKIE_SKIN => 'eg_skin';
14 use constant OILS_HTTP_COOKIE_THEME => 'eg_theme';
15 use constant OILS_HTTP_COOKIE_LOCALE => 'eg_locale';
16
17 my $web_config;
18 my $web_config_file;
19 my $web_config_edit_time;
20 my %lh_cache; # locale handlers
21
22 sub import {
23     my $self = shift;
24     $web_config_file = shift || '';
25     unless(-r $web_config_file) {
26         warn "Invalid web config $web_config_file\n";
27         return;
28     }
29     check_web_config();
30 }
31
32
33 sub handler {
34     my $r = shift;
35     check_web_config($r); # option to disable this
36     my $ctx = load_context($r);
37     my $base = $ctx->{base_path};
38
39     $r->content_type('text/html; encoding=utf8');
40
41     my($template, $page_args, $as_xml) = find_template($r, $base, $ctx);
42     $ctx->{page_args} = $page_args;
43
44     my $stat = run_context_loader($r, $ctx);
45
46     return $stat unless $stat == Apache2::Const::OK;
47     return Apache2::Const::DECLINED unless $template;
48
49     my $text_handler = set_text_handler($ctx, $r);
50
51     my $tt = Template->new({
52         OUTPUT => ($as_xml) ?  sub { parse_as_xml($r, $ctx, @_); } : $r,
53         INCLUDE_PATH => $ctx->{template_paths},
54         DEBUG => $ctx->{debug_template},
55         PLUGINS => {
56             EGI18N => 'OpenILS::WWW::EGWeb::I18NFilter',
57             CGI_utf8 => 'OpenILS::WWW::EGWeb::CGI_utf8'
58         },
59         FILTERS => {
60             # Register a dynamic filter factory for our locale::maketext generator
61             l => [
62                 sub {
63                     my($ctx, @args) = @_;
64                     return sub { $text_handler->(shift(), @args); }
65                 }, 1
66             ]
67         }
68     });
69
70     $ctx->{encode_utf8} = sub {return encode_utf8(shift())};
71
72     unless($tt->process($template, {ctx => $ctx, ENV => \%ENV, l => $text_handler})) {
73         $r->log->warn('egweb: template error: ' . $tt->error);
74         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
75     }
76
77     return Apache2::Const::OK;
78 }
79
80 sub set_text_handler {
81     my $ctx = shift;
82     my $r = shift;
83
84     my $locale = $ctx->{locale};
85     $locale =~ s/-/_/g;
86
87     $r->log->debug("egweb: messages locale = $locale");
88
89     unless($lh_cache{$locale}) {
90         $r->log->info("egweb: Unsupported locale: $locale");
91         $lh_cache{$locale} = $lh_cache{'en_US'};
92     }
93
94     return sub { return $lh_cache{$locale}->maketext(@_); };
95 }
96
97
98
99 sub run_context_loader {
100     my $r = shift;
101     my $ctx = shift;
102
103     my $stat = Apache2::Const::OK;
104
105     my $loader = $r->dir_config('OILSWebContextLoader');
106     return $stat unless $loader;
107
108     eval {
109         $loader->use;
110         $stat = $loader->new($r, $ctx)->load;
111     };
112
113     if($@) {
114         $r->log->error("egweb: Context Loader error: $@");
115         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
116     }
117
118     $r->log->debug("egweb: context loader resulted in status $stat");
119     return $stat;
120 }
121
122 sub parse_as_xml {
123     my $r = shift;
124     my $ctx = shift;
125     my $data = shift;
126
127     my $success = 0;
128
129     try { 
130         my $doc = XML::LibXML->new->parse_string($data); 
131         $data = $doc->documentElement->toStringC14N;
132         $data = $ctx->{final_dtd} . "\n" . $data;
133         $success = 1;
134     } otherwise {
135             my $e = shift;
136         my $err = "Invalid XML: $e";
137         $r->log->error("egweb: $err");
138         $r->content_type('text/plain; encoding=utf8');
139         $r->print("\n$err\n\n$data");
140     };
141
142     $r->print($data) if ($success);
143 }
144
145
146 sub load_context {
147     my $r = shift;
148     my $cgi = CGI->new;
149     my $ctx = {}; # new context for each page load
150     $ctx->{$_} = $web_config->{base_ctx}->{$_} for keys %{$web_config->{base_ctx}};
151     $ctx->{hostname} = $r->hostname;
152     $ctx->{base_url} = $cgi->url(-base => 1);
153     $ctx->{skin} = $cgi->cookie(OILS_HTTP_COOKIE_SKIN) || 'default';
154     $ctx->{theme} = $cgi->cookie(OILS_HTTP_COOKIE_THEME) || 'default';
155
156     # Any paths configured in Apache will be placed in front of
157     # any paths configured in the global oils_web.xml config.
158     my @template_paths = $r->dir_config->get('OILSTemplatePath');
159     unshift(@{$ctx->{template_paths}}, $_) for reverse @template_paths;
160     $r->log->debug("template paths => @{$ctx->{template_paths}}");
161
162     $ctx->{locale} = 
163         $cgi->cookie(OILS_HTTP_COOKIE_LOCALE) || 
164         parse_accept_lang($r->headers_in->get('Accept-Language')) || 'en-US';
165
166     my $mprefix = $ctx->{media_prefix};
167     if($mprefix and $mprefix !~ /^http/ and $mprefix !~ /^\//) {
168         # if a hostname is provided /w no protocol, match the protocol to the current page
169         $ctx->{media_prefix} = ($cgi->https) ? "https://$mprefix" : "http://$mprefix";
170     }
171
172     return $ctx;
173 }
174
175 # turn Accept-Language into sometihng EG can understand
176 sub parse_accept_lang {
177     my $al = shift;
178     return undef unless $al;
179     my ($locale) = split(/,/, $al);
180     ($locale) = split(/;/, $locale);
181     return undef unless $locale;
182     $locale =~ s/-(.*)/eval '-'.uc("$1")/e;
183     return $locale;
184 }
185
186 # Given a URI, finds the configured template and any extra page 
187 # arguments (trailing path info).  Any extra data is returned
188 # as page arguments, in the form of an array, one item per 
189 # /-separated URI component
190 sub find_template {
191     my $r = shift;
192     my $base = shift;
193     my $ctx = shift;
194     my $path = $r->uri;
195     $path =~ s/$base\/?//og;
196     my @parts = split('/', $path);
197     my $template = '';
198     my $page_args = [];
199     my $as_xml = $ctx->{force_valid_xml};
200     my $handler = $web_config->{handlers};
201
202     while(@parts) {
203         my $part = shift @parts;
204         next unless $part;
205         my $t = $handler->{$part};
206         if(ref($t) eq 'PathConfig') {
207             $template = $t->{template};
208             $as_xml = ($t->{as_xml} and $t->{as_xml} =~ /true/io) || $as_xml;
209             $page_args = [@parts];
210             last;
211         } else {
212             $handler = $t;
213         }
214     }
215
216     unless($template) { # no template configured
217
218         # see if we can magically find the template based on the path and default extension
219         my $ext = $ctx->{default_template_extension};
220
221         my @parts = split('/', $path);
222         my $localpath = $path;
223         my @args;
224         while(@parts) {
225             last unless $localpath;
226             for my $tpath (@{$ctx->{template_paths}}) {
227                 my $fpath = "$tpath/$localpath.$ext";
228                 $r->log->debug("egweb: looking at possible template $fpath");
229                 if(-r $fpath) {
230                     $template = "$localpath.$ext";
231                     last;
232                 }
233             }
234             last if $template;
235             push(@args, pop @parts);
236             $localpath = join('/', @parts);
237         } 
238
239         $page_args = [@args];
240
241         # no template configured or found
242         unless($template) {
243             $r->log->debug("egweb: No template configured for path $path");
244             return ();
245         }
246     }
247
248     $r->log->debug("egweb: template = $template : page args = @$page_args");
249     return ($template, $page_args, $as_xml);
250 }
251
252 # if the web configuration file has never been loaded or has
253 # changed since the last load, reload it
254 sub check_web_config {
255     my $r = shift;
256     my $epoch = stat($web_config_file)->mtime;
257     unless($web_config_edit_time and $web_config_edit_time == $epoch) {
258         $r->log->debug("egweb: Reloading web config after edit...") if $r;
259         $web_config_edit_time = $epoch;
260         $web_config = parse_config($web_config_file);
261     }
262 }
263
264 # Create an I18N sub-module for each supported locale
265 # Each module creates its own MakeText lexicon by parsing .po/.mo files
266 sub load_locale_handlers {
267     my $ctx = shift;
268     my $locales = $ctx->{locales};
269
270     $locales->{en_US} = {} unless exists $locales->{en_US};
271
272     for my $lang (keys %$locales) {
273         my $messages = $locales->{$lang};
274         $messages = '' if ref $messages; # empty {}
275
276         # TODO Can we do this without eval?
277         my $eval = <<EVAL;
278             package OpenILS::WWW::EGWeb::I18N::$lang;
279             use base 'OpenILS::WWW::EGWeb::I18N';
280             if(\$messages) {
281                 use Locale::Maketext::Lexicon::Gettext;
282                 if(open F, '$messages') {
283                     our %Lexicon = (%Lexicon, %{ Locale::Maketext::Lexicon::Gettext->parse(<F>) });
284                     close F;
285                 } else {
286                     warn "EGWeb: unable to open messages file: $messages"; 
287                 }
288             }
289 EVAL
290         eval $eval;
291         warn "$@\n" if $@; # TODO better logging
292         $lh_cache{$lang} = "OpenILS::WWW::EGWeb::I18N::$lang"->new;
293     }
294 }
295
296
297
298 sub parse_config {
299     my $cfg_file = shift;
300     my $data = XML::Simple->new->XMLin($cfg_file);
301     my $ctx = {};
302     my $handlers = {};
303
304     $ctx->{media_prefix} = (ref $data->{media_prefix}) ? '' : $data->{media_prefix};
305     $ctx->{base_path} = (ref $data->{base_path}) ? '' : $data->{base_path};
306     $ctx->{template_paths} = [];
307     $ctx->{force_valid_xml} = ( ($data->{force_valid_xml}||'') =~ /true/io) ? 1 : 0;
308     $ctx->{debug_template} = ( ($data->{debug_template}||'')  =~ /true/io) ? 1 : 0;
309     $ctx->{default_template_extension} = $data->{default_template_extension} || 'tt2';
310     $ctx->{web_dir} = $data->{web_dir};
311     $ctx->{locales} = $data->{locales};
312     load_locale_handlers($ctx);
313
314     my $tpaths = $data->{template_paths}->{path};
315     $tpaths = [$tpaths] unless ref $tpaths;
316     push(@{$ctx->{template_paths}}, $_) for @$tpaths;
317
318     for my $handler (@{$data->{handlers}->{handler}}) {
319         my @parts = split('/', $handler->{path});
320         my $h = $handlers;
321         my $pcount = scalar(@parts);
322         for(my $i = 0; $i < $pcount; $i++) {
323             my $p = $parts[$i];
324             unless(defined $h->{$p}) {
325                 if($i == $pcount - 1) {
326                     $h->{$p} = PathConfig->new(%$handler);
327                     last;
328                 } else {
329                     $h->{$p} = {};
330                 }
331             }
332             $h = $h->{$p};
333         }
334     }
335
336     return {base_ctx => $ctx, handlers => $handlers};
337 }
338
339 package PathConfig;
340 sub new {
341     my($class, %args) = @_;
342     return bless(\%args, $class);
343 }
344
345 # base class for all supported locales
346 package OpenILS::WWW::EGWeb::I18N;
347 use base 'Locale::Maketext';
348 our %Lexicon = (_AUTO => 1);
349
350 1;