1 package OpenILS::WWW::EGWeb;
2 use strict; use warnings;
8 use Apache2::Const -compile => qw(OK DECLINED HTTP_INTERNAL_SERVER_ERROR HTTP_NOT_FOUND HTTP_GONE);
10 use OpenSRF::EX qw(:try);
11 use OpenSRF::AppSession;
12 use OpenILS::Utils::CStoreEditor q/:funcs/;
13 use List::MoreUtils qw/uniq/;
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';
19 # cache string bundles
20 my %registered_locales;
22 # cache template path -r tests
25 # cache template processors by vhost
26 my %vhost_processor_cache;
29 my @context_loaders_to_preinit = ();
30 my %locales_to_preinit = ();
33 my ($self, $bootstrap_config, $loaders, $locales) = @_;
34 @context_loaders_to_preinit = split /\s+/, $loaders, -1 if defined($loaders);
35 %locales_to_preinit = map { $_ => parse_eg_locale($_) }
36 split /\s+/, $locales, -1 if defined($locales);
40 OpenSRF::System->bootstrap_client(config_file => $bootstrap_config);
41 my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
42 Fieldmapper->import(IDL => $idl);
43 foreach my $loader (@context_loaders_to_preinit) {
46 $loader->child_init(%locales_to_preinit);
49 return Apache2::Const::OK;
55 my @params_to_redact = uniq $r->dir_config->get('OILSUrlParamToRedact');
56 my $re = '('. join('|', map { quotemeta($_) } @params_to_redact) . ')=(?:[^&;]*)';
58 my $args = $r->args();
59 $args =~ s/$re/$1=[REDACTED]/g;
61 my $req = $r->the_request(); # munging args doesn't update the
62 # original requested URI
63 $req =~ s/$re/$1=[REDACTED]/g;
64 $r->the_request($req);
66 if ($r->headers_in->{Referer}) {
67 $r->headers_in->{Referer} =~ s/$re/$1=[REDACTED]/g;
70 return Apache2::Const::OK;
75 my $stat = handler_guts($r);
77 # other opensrf clients share this apache process,
78 # so it's critical to reset the locale after each
79 # response is handled, lest the other clients
80 # adopt our temporary, global locale value.
81 OpenSRF::AppSession->reset_locale;
87 my $ctx = load_context($r);
88 my $base = $ctx->{base_path};
90 my($template, $page_args, $as_xml) = find_template($r, $base, $ctx);
91 $ctx->{page_args} = $page_args;
93 my $stat = run_context_loader($r, $ctx);
95 # Handle deleted or never existing records a little more gracefully.
96 # For these two special cases, we set the status so that the request
97 # header will contain the appropriate HTTP status code, but reset the
98 # status so that Apache will continue to process the request and provide
99 # more than just the raw HTTP error page.
100 if ($stat == Apache2::Const::HTTP_GONE || $stat == Apache2::Const::HTTP_NOT_FOUND) {
102 $stat = Apache2::Const::OK;
104 return $stat unless $stat == Apache2::Const::OK;
106 # emit context as JSON if handler requests
107 if ($ctx->{json_response}) {
108 $r->content_type("application/json; charset=utf-8");
109 $r->headers_out->add("cache-control" => "no-store, no-cache, must-revalidate");
110 $r->headers_out->add("expires" => "-1");
111 if ($ctx->{json_reponse_cookie}) {
112 $r->headers_out->add('Set-Cookie' => $ctx->{json_reponse_cookie})
114 $r->print(OpenSRF::Utils::JSON->perl2JSON($ctx->{json_response}));
115 return Apache2::Const::OK;
118 return Apache2::Const::DECLINED unless $template;
120 my $text_handler = set_text_handler($ctx, $r);
122 my $processor_key = $as_xml ? 'xml:' : 'text:'; # separate by XML strictness
123 $processor_key .= $r->hostname.':'; # ... and vhost
124 $processor_key .= $r->dir_config('OILSWebContextLoader').':'; # ... and context loader
125 $processor_key .= $ctx->{locale}; # ... and locale
126 # NOTE: context loader and vhost together imply template path and debug template values
128 my $tt = $vhost_processor_cache{$processor_key} || Template->new({
130 OUTPUT => ($as_xml) ? sub { parse_as_xml($r, $ctx, @_); } : $r,
131 INCLUDE_PATH => $ctx->{template_paths},
132 DEBUG => $ctx->{debug_template},
134 $r->dir_config('OILSWebCompiledTemplateCache') ?
135 (COMPILE_DIR => $r->dir_config('OILSWebCompiledTemplateCache')) :
139 ($r->dir_config('OILSWebTemplateStatTTL') =~ /^\d+$/) ?
140 (STAT_TTL => $r->dir_config('OILSWebTemplateStatTTL')) :
144 EGI18N => 'OpenILS::WWW::EGWeb::I18NFilter',
145 CGI_utf8 => 'OpenILS::WWW::EGWeb::CGI_utf8'
148 # Register a dynamic filter factory for our locale::maketext generator
151 my($ctx, @args) = @_;
152 return sub { $text_handler->(shift(), @args); }
159 $r->log->error("Error creating template processor: $@");
160 return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
163 $vhost_processor_cache{$processor_key} = $tt;
164 $ctx->{encode_utf8} = sub {return encode_utf8(shift())};
166 unless($tt->process($template, {ctx => $ctx, ENV => \%ENV, l => $text_handler}, $r)) {
167 $r->log->warn('egweb: template error: ' . $tt->error);
168 return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
171 return Apache2::Const::OK;
174 sub set_text_handler {
178 my $locale = $ctx->{locale};
180 $r->log->debug("egweb: messages locale = $locale");
183 my $lh = OpenILS::WWW::EGWeb::I18N->get_handle($locale)
184 || OpenILS::WWW::EGWeb::I18N->new;
185 return $lh->maketext(@_);
191 sub run_context_loader {
195 my $stat = Apache2::Const::OK;
197 my $loader = $r->dir_config('OILSWebContextLoader');
198 return $stat unless $loader;
202 $stat = $loader->new($r, $ctx)->load;
206 $r->log->error("egweb: Context Loader error: $@");
207 return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
210 $r->log->debug("egweb: context loader resulted in status $stat");
222 my $doc = XML::LibXML->new->parse_string($data);
223 $data = $doc->documentElement->toStringC14N;
224 $data = $ctx->{final_dtd} . "\n" . $data;
228 my $err = "Invalid XML: $e";
229 $r->log->error("egweb: $err");
230 $r->content_type('text/plain; encoding=utf8');
231 $r->print("\n$err\n\n$data");
234 $r->print($data) if ($success);
240 my $ctx = {}; # new context for each page load
242 $ctx->{base_path} = $r->dir_config('OILSWebBasePath');
243 $ctx->{web_dir} = $r->dir_config('OILSWebWebDir');
244 $ctx->{debug_template} = ($r->dir_config('OILSWebDebugTemplate') =~ /true/io) ? 1 : 0;
245 $ctx->{hostname} = $r->hostname;
246 $ctx->{media_prefix} = $r->dir_config('OILSWebMediaPrefix') || $ctx->{hostname};
247 $ctx->{base_url} = $cgi->url(-base => 1);
248 $ctx->{skin} = $cgi->cookie(OILS_HTTP_COOKIE_SKIN) || 'default';
249 $ctx->{theme} = $cgi->cookie(OILS_HTTP_COOKIE_THEME) || 'default';
250 $ctx->{proto} = $cgi->https ? 'https' : 'http';
251 $ctx->{ext_proto} = $ctx->{proto};
252 my $default_locale = $r->dir_config('OILSWebDefaultLocale') || 'en_us';
254 my @template_paths = uniq $r->dir_config->get('OILSWebTemplatePath');
255 $ctx->{template_paths} = [ reverse @template_paths ];
257 my @locales = $r->dir_config->get('OILSWebLocale');
258 load_locale_handlers($ctx, @locales);
260 $ctx->{locales} = \%registered_locales;
262 # Set a locale cookie if the requested locale is valid
263 my $set_locale = $cgi->param('set_eg_locale') || '';
264 if (!(grep {$_ eq $set_locale} keys %registered_locales)) {
267 my $slc = $cgi->cookie({
268 '-name' => OILS_HTTP_COOKIE_LOCALE,
269 '-value' => $set_locale,
272 $r->headers_out->add('Set-Cookie' => $slc);
275 $ctx->{locale} = $set_locale ||
276 $cgi->cookie(OILS_HTTP_COOKIE_LOCALE) || $default_locale ||
277 parse_accept_lang($r->headers_in->get('Accept-Language'));
279 # set the editor default locale for each page load
280 my $ses_locale = parse_eg_locale($ctx->{locale});
281 OpenSRF::AppSession->default_locale($ses_locale);
282 # give templates access to the en-US style locale
283 $ctx->{eg_locale} = $ses_locale;
285 my $mprefix = $ctx->{media_prefix};
286 if($mprefix and $mprefix !~ /^http/ and $mprefix !~ /^\//) {
287 # if a hostname is provided /w no protocol, match the protocol to the current page
288 $ctx->{media_prefix} = ($cgi->https) ? "https://$mprefix" : "http://$mprefix";
294 # turn Accept-Language into something EG can understand
295 # TODO: try all langs, not just the first
296 sub parse_accept_lang {
298 return undef unless $al;
299 my ($locale) = split(/,/, $al);
300 ($locale) = split(/;/, $locale);
301 return undef unless $locale;
306 # Accept-Language uses locales like 'en', 'fr', 'fr_fr', while Evergreen
307 # internally uses 'en-US', 'fr-CA', 'fr-FR' (always with the 2 lowercase,
308 # hyphen, 2 uppercase convention)
309 sub parse_eg_locale {
310 my $ua_locale = shift || 'en_us';
312 $ua_locale =~ m/^(..).?(..)?$/;
313 my $lang_code = lc($1);
314 my $region_code = $2 ? uc($2) : uc($1);
315 return "$lang_code-$region_code";
318 # Given a URI, finds the configured template and any extra page
319 # arguments (trailing path info). Any extra data is returned
320 # as page arguments, in the form of an array, one item per
321 # /-separated URI component
327 $path =~ s/$base\/?//og;
330 my $as_xml = $r->dir_config('OILSWebForceValidXML');
331 my $ext = $r->dir_config('OILSWebDefaultTemplateExtension');
332 my $at_index = $r->dir_config('OILSWebStopAtIndex');
334 $vhost_path_cache{$r->hostname} ||= {};
335 my $path_cache = $vhost_path_cache{$r->hostname};
337 my @parts = split('/', $path);
338 my $localpath = $path;
340 if ($localpath =~ m|/css/|) {
341 $r->content_type('text/css; encoding=utf8');
343 $r->content_type('text/html; encoding=utf8');
348 last unless $localpath;
349 for my $tpath (@{$ctx->{template_paths}}) {
350 my $fpath = "$tpath/$localpath.$ext";
351 $r->log->debug("egweb: looking at possible template $fpath");
352 if ($template = $path_cache->{$fpath}) { # we've checked with -r before...
353 next if ($template eq '0E0'); # ... and found nothing
355 } elsif (-r $fpath) { # or, we haven't checked, and if we find a file...
356 $path_cache->{$fpath} = $template = "$localpath.$ext"; # ... note it
358 } else { # Nothing there...
359 $path_cache->{$fpath} = '0E0'; # ... note that fact
362 last if $template and $template ne '0E0';
365 # no matching template was found in the current directory.
366 # stop-at-index requested; see if there is an index.ext
367 # file in the same directory instead.
368 for my $tpath (@{$ctx->{template_paths}}) {
369 # replace the final path component with 'index'
370 if ($localpath =~ m|/$|) {
371 $localpath .= 'index';
373 $localpath =~ s|/[^/]+$|/index|;
375 my $fpath = "$tpath/$localpath.$ext";
376 $r->log->debug("egweb: looking at possible template $fpath");
377 if ($template = $path_cache->{$fpath}) { # See above block
378 next if ($template eq '0E0');
380 } elsif (-r $fpath) {
381 $path_cache->{$fpath} = $template = "$localpath.$ext";
384 $path_cache->{$fpath} = '0E0';
388 last if $template and $template ne '0E0';
390 push(@args, pop @parts);
391 $localpath = join('/', @parts);
394 $page_args = [@args];
396 # no template configured or found
397 if(!$template or $template eq '0E0') {
398 $r->log->debug("egweb: No template configured for path $path");
402 $r->log->debug("egweb: template = $template : page args = @$page_args");
403 return ($template, $page_args, $as_xml);
406 # Create an I18N sub-module for each supported locale
407 # Each module creates its own MakeText lexicon by parsing .po/.mo files
408 sub load_locale_handlers {
411 my %locales = (en_us => []);
413 my ($l,$file) = (shift(@raw),shift(@raw));
415 push @{$locales{$l}}, $file;
418 my $editor = new_editor();
419 my @locale_tags = sort { length($a) <=> length($b) } keys %locales;
421 for my $idx (0..$#locale_tags) {
423 my $tag = $locale_tags[$idx];
424 my $parent_tag = 'OpenILS::WWW::EGWeb::I18N';
426 my $res = $editor->json_query({
428 "evergreen.get_locale_name",
433 my $locale_name = $res->[0]->{"name"} if exists $res->[0]->{"name"};
434 next unless $locale_name;
438 # find the parent locale if possible. It will be
439 # longest left-anchored substring of the current tag
440 while( --$sub_idx >= 0 ) {
441 my $ptag = $locale_tags[$sub_idx];
442 if( substr($tag, 0, length($ptag)) eq $ptag ) {
443 $parent_tag .= "::$ptag";
448 my $eval = <<" EVAL"; # Dynamic part
449 package OpenILS::WWW::EGWeb::I18N::$tag;
450 use base '$parent_tag';
455 if(@{$locales{$tag}}) {
456 use Locale::Maketext::Lexicon {
459 use Locale::Maketext::Lexicon::Gettext;
460 for my $messages (@{$locales{$tag}}) {
461 if(open F, $messages) {
462 %Lexicon = (%Lexicon, %{ Locale::Maketext::Lexicon::Gettext->parse(<F>) });
465 warn "EGWeb: unable to open messages file: $messages";
476 $registered_locales{"$tag"} = $locale_name;
482 # base class for all supported locales
483 package OpenILS::WWW::EGWeb::I18N;
484 use base 'Locale::Maketext';
485 our %Lexicon = (_AUTO => 1);