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