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