]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/WWW/EGWeb.pm
LP#1306675 TPAC maketext default handler
[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 OpenSRF::AppSession;
12 use OpenILS::Utils::CStoreEditor q/:funcs/;
13 use List::MoreUtils qw/uniq/;
14
15 use constant OILS_HTTP_COOKIE_SKIN => 'eg_skin';
16 use constant OILS_HTTP_COOKIE_THEME => 'eg_theme';
17 use constant OILS_HTTP_COOKIE_LOCALE => 'eg_locale';
18
19 # cache string bundles
20 my %registered_locales;
21
22 sub handler {
23     my $r = shift;
24     my $stat = handler_guts($r);
25
26     # other opensrf clients share this apache process,
27     # so it's critical to reset the locale after each
28     # response is handled, lest the other clients 
29     # adopt our temporary, global locale value.
30     OpenSRF::AppSession->reset_locale;
31     return $stat;
32 }
33     
34 sub handler_guts {
35     my $r = shift;
36     my $ctx = load_context($r);
37     my $base = $ctx->{base_path};
38
39     my($template, $page_args, $as_xml) = find_template($r, $base, $ctx);
40     $ctx->{page_args} = $page_args;
41
42     my $stat = run_context_loader($r, $ctx);
43
44     return $stat unless $stat == Apache2::Const::OK;
45     return Apache2::Const::DECLINED unless $template;
46
47     my $text_handler = set_text_handler($ctx, $r);
48
49     my $tt = Template->new({
50         ENCODING => 'utf-8',
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     if (!$tt) {
70         $r->log->error("Error creating template processor: $@");
71         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
72     }   
73
74     $ctx->{encode_utf8} = sub {return encode_utf8(shift())};
75
76     unless($tt->process($template, {ctx => $ctx, ENV => \%ENV, l => $text_handler})) {
77         $r->log->warn('egweb: template error: ' . $tt->error);
78         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
79     }
80
81     return Apache2::Const::OK;
82 }
83
84 sub set_text_handler {
85     my $ctx = shift;
86     my $r = shift;
87
88     my $locale = $ctx->{locale};
89
90     $r->log->debug("egweb: messages locale = $locale");
91
92     return sub {
93         my $lh = OpenILS::WWW::EGWeb::I18N->get_handle($locale) 
94             || OpenILS::WWW::EGWeb::I18N->new;
95         return $lh->maketext(@_);
96     };
97 }
98
99
100
101 sub run_context_loader {
102     my $r = shift;
103     my $ctx = shift;
104
105     my $stat = Apache2::Const::OK;
106
107     my $loader = $r->dir_config('OILSWebContextLoader');
108     return $stat unless $loader;
109
110     eval {
111         $loader->use;
112         $stat = $loader->new($r, $ctx)->load;
113     };
114
115     if($@) {
116         $r->log->error("egweb: Context Loader error: $@");
117         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
118     }
119
120     $r->log->debug("egweb: context loader resulted in status $stat");
121     return $stat;
122 }
123
124 sub parse_as_xml {
125     my $r = shift;
126     my $ctx = shift;
127     my $data = shift;
128
129     my $success = 0;
130
131     try { 
132         my $doc = XML::LibXML->new->parse_string($data); 
133         $data = $doc->documentElement->toStringC14N;
134         $data = $ctx->{final_dtd} . "\n" . $data;
135         $success = 1;
136     } otherwise {
137         my $e = shift;
138         my $err = "Invalid XML: $e";
139         $r->log->error("egweb: $err");
140         $r->content_type('text/plain; encoding=utf8');
141         $r->print("\n$err\n\n$data");
142     };
143
144     $r->print($data) if ($success);
145 }
146
147 sub load_context {
148     my $r = shift;
149     my $cgi = CGI->new;
150     my $ctx = {}; # new context for each page load
151
152     $ctx->{base_path} = $r->dir_config('OILSWebBasePath');
153     $ctx->{web_dir} = $r->dir_config('OILSWebWebDir');
154     $ctx->{debug_template} = ($r->dir_config('OILSWebDebugTemplate') =~ /true/io);
155     $ctx->{media_prefix} = $r->dir_config('OILSWebMediaPrefix');
156     $ctx->{hostname} = $r->hostname;
157     $ctx->{base_url} = $cgi->url(-base => 1);
158     $ctx->{skin} = $cgi->cookie(OILS_HTTP_COOKIE_SKIN) || 'default';
159     $ctx->{theme} = $cgi->cookie(OILS_HTTP_COOKIE_THEME) || 'default';
160     $ctx->{proto} = $cgi->https ? 'https' : 'http';
161     $ctx->{ext_proto} = $ctx->{proto};
162     my $default_locale = $r->dir_config('OILSWebDefaultLocale') || 'en_us';
163
164     my @template_paths = uniq $r->dir_config->get('OILSWebTemplatePath');
165     $ctx->{template_paths} = [ reverse @template_paths ];
166
167     my %locales = $r->dir_config->get('OILSWebLocale');
168     load_locale_handlers($ctx, %locales);
169
170     $ctx->{locales} = \%registered_locales;
171
172     # Set a locale cookie if the requested locale is valid
173     my $set_locale = $cgi->param('set_eg_locale') || '';
174     if (!(grep {$_ eq $set_locale} keys %registered_locales)) {
175         $set_locale = '';
176     } else {
177         my $slc = $cgi->cookie({
178             '-name' => OILS_HTTP_COOKIE_LOCALE,
179             '-value' => $set_locale,
180             '-expires' => '+10y'
181         });
182         $r->headers_out->add('Set-Cookie' => $slc);
183     }
184
185     $ctx->{locale} = $set_locale ||
186         $cgi->cookie(OILS_HTTP_COOKIE_LOCALE) || $default_locale ||
187         parse_accept_lang($r->headers_in->get('Accept-Language'));
188
189     # set the editor default locale for each page load
190     my $ses_locale = parse_eg_locale($ctx->{locale});
191     OpenSRF::AppSession->default_locale($ses_locale);
192     # give templates access to the en-US style locale
193     $ctx->{eg_locale} = $ses_locale;
194
195     my $mprefix = $ctx->{media_prefix};
196     if($mprefix and $mprefix !~ /^http/ and $mprefix !~ /^\//) {
197         # if a hostname is provided /w no protocol, match the protocol to the current page
198         $ctx->{media_prefix} = ($cgi->https) ? "https://$mprefix" : "http://$mprefix";
199     }
200
201     return $ctx;
202 }
203
204 # turn Accept-Language into something EG can understand
205 # TODO: try all langs, not just the first
206 sub parse_accept_lang {
207     my $al = shift;
208     return undef unless $al;
209     my ($locale) = split(/,/, $al);
210     ($locale) = split(/;/, $locale);
211     return undef unless $locale;
212     $locale =~ s/-/_/og;
213     return $locale;
214 }
215
216 # Accept-Language uses locales like 'en', 'fr', 'fr_fr', while Evergreen
217 # internally uses 'en-US', 'fr-CA', 'fr-FR' (always with the 2 lowercase,
218 # hyphen, 2 uppercase convention)
219 sub parse_eg_locale {
220     my $ua_locale = shift || 'en_us';
221
222     $ua_locale =~ m/^(..).?(..)?$/;
223     my $lang_code = lc($1);
224     my $region_code = $2 ? uc($2) : uc($1);
225     return "$lang_code-$region_code";
226 }
227
228 # Given a URI, finds the configured template and any extra page 
229 # arguments (trailing path info).  Any extra data is returned
230 # as page arguments, in the form of an array, one item per 
231 # /-separated URI component
232 sub find_template {
233     my $r = shift;
234     my $base = shift;
235     my $ctx = shift;
236     my $path = $r->uri;
237     $path =~ s/$base\/?//og;
238     my $template = '';
239     my $page_args = [];
240     my $as_xml = $r->dir_config('OILSWebForceValidXML');
241     my $ext = $r->dir_config('OILSWebDefaultTemplateExtension');
242
243     my @parts = split('/', $path);
244     my $localpath = $path;
245
246     if ($localpath =~ m|opac/css|) {
247         $r->content_type('text/css; encoding=utf8');
248     } else {
249         $r->content_type('text/html; encoding=utf8');
250     }
251     my @args;
252     while(@parts) {
253         last unless $localpath;
254         for my $tpath (@{$ctx->{template_paths}}) {
255             my $fpath = "$tpath/$localpath.$ext";
256             $r->log->debug("egweb: looking at possible template $fpath");
257             if(-r $fpath) {
258                 $template = "$localpath.$ext";
259                 last;
260             }
261         }
262         last if $template;
263         push(@args, pop @parts);
264         $localpath = join('/', @parts);
265     } 
266
267     $page_args = [@args];
268
269     # no template configured or found
270     unless($template) {
271         $r->log->debug("egweb: No template configured for path $path");
272         return ();
273     }
274
275     $r->log->debug("egweb: template = $template : page args = @$page_args");
276     return ($template, $page_args, $as_xml);
277 }
278
279 # Create an I18N sub-module for each supported locale
280 # Each module creates its own MakeText lexicon by parsing .po/.mo files
281 sub load_locale_handlers {
282     my $ctx = shift;
283     my %locales = @_;
284
285     my $editor = new_editor();
286     my @locale_tags = sort { length($a) <=> length($b) } keys %locales;
287
288     # always fall back to en_us, the assumed template language
289     push(@locale_tags, 'en_us');
290
291     for my $idx (0..$#locale_tags) {
292
293         my $tag = $locale_tags[$idx];
294         next if grep { $_ eq $tag } keys %registered_locales;
295
296         my $res = $editor->json_query({
297             "from" => [
298                 "evergreen.get_locale_name",
299                 $tag
300             ]
301         });
302
303         my $locale_name = $res->[0]->{"name"} if exists $res->[0]->{"name"};
304         next unless $locale_name;
305
306         my $parent_tag = '';
307         my $sub_idx = $idx;
308
309         # find the parent locale if possible.  It will be 
310         # longest left-anchored substring of the current tag
311         while( --$sub_idx >= 0 ) {
312             my $ptag = $locale_tags[$sub_idx];
313             if( substr($tag, 0, length($ptag)) eq $ptag ) {
314                 $parent_tag = "::$ptag";
315                 last;
316             }
317         }
318
319         my $messages = $locales{$tag} || '';
320
321         # TODO Can we do this without eval?
322         my $eval = <<"        EVAL";
323             package OpenILS::WWW::EGWeb::I18N::$tag;
324             use base 'OpenILS::WWW::EGWeb::I18N$parent_tag';
325             if(\$messages) {
326                 use Locale::Maketext::Lexicon {
327                     _decode => 1
328                 };
329                 use Locale::Maketext::Lexicon::Gettext;
330                 if(open F, '$messages') {
331                     our %Lexicon = (%Lexicon, %{ Locale::Maketext::Lexicon::Gettext->parse(<F>) });
332                     close F;
333                 } else {
334                     warn "EGWeb: unable to open messages file: $messages"; 
335                 }
336             }
337         EVAL
338         eval $eval;
339
340         if ($@) {
341             warn "$@\n" if $@;
342         } else {
343             $registered_locales{"$tag"} = $locale_name;
344         }
345     }
346 }
347
348
349 # base class for all supported locales
350 package OpenILS::WWW::EGWeb::I18N;
351 use base 'Locale::Maketext';
352 our %Lexicon = (_AUTO => 1);
353
354 1;