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