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;
24 my $stat = handler_guts($r);
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;
36 my $ctx = load_context($r);
37 my $base = $ctx->{base_path};
39 my($template, $page_args, $as_xml) = find_template($r, $base, $ctx);
40 $ctx->{page_args} = $page_args;
42 my $stat = run_context_loader($r, $ctx);
44 # Handle deleted or never existing records a little more gracefully.
45 # For these two special cases, we set the status so that the request
46 # header will contain the appropriate HTTP status code, but reset the
47 # status so that Apache will continue to process the request and provide
48 # more than just the raw HTTP error page.
49 if ($stat == Apache2::Const::HTTP_GONE || $stat == Apache2::Const::HTTP_NOT_FOUND) {
51 $stat = Apache2::Const::OK;
53 return $stat unless $stat == Apache2::Const::OK;
54 return Apache2::Const::DECLINED unless $template;
56 my $text_handler = set_text_handler($ctx, $r);
58 my $tt = Template->new({
60 OUTPUT => ($as_xml) ? sub { parse_as_xml($r, $ctx, @_); } : $r,
61 INCLUDE_PATH => $ctx->{template_paths},
62 DEBUG => $ctx->{debug_template},
64 EGI18N => 'OpenILS::WWW::EGWeb::I18NFilter',
65 CGI_utf8 => 'OpenILS::WWW::EGWeb::CGI_utf8'
68 # Register a dynamic filter factory for our locale::maketext generator
72 return sub { $text_handler->(shift(), @args); }
79 $r->log->error("Error creating template processor: $@");
80 return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
83 $ctx->{encode_utf8} = sub {return encode_utf8(shift())};
85 unless($tt->process($template, {ctx => $ctx, ENV => \%ENV, l => $text_handler})) {
86 $r->log->warn('egweb: template error: ' . $tt->error);
87 return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
90 return Apache2::Const::OK;
93 sub set_text_handler {
97 my $locale = $ctx->{locale};
99 $r->log->debug("egweb: messages locale = $locale");
102 my $lh = OpenILS::WWW::EGWeb::I18N->get_handle($locale)
103 || OpenILS::WWW::EGWeb::I18N->new;
104 return $lh->maketext(@_);
110 sub run_context_loader {
114 my $stat = Apache2::Const::OK;
116 my $loader = $r->dir_config('OILSWebContextLoader');
117 return $stat unless $loader;
121 $stat = $loader->new($r, $ctx)->load;
125 $r->log->error("egweb: Context Loader error: $@");
126 return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
129 $r->log->debug("egweb: context loader resulted in status $stat");
141 my $doc = XML::LibXML->new->parse_string($data);
142 $data = $doc->documentElement->toStringC14N;
143 $data = $ctx->{final_dtd} . "\n" . $data;
147 my $err = "Invalid XML: $e";
148 $r->log->error("egweb: $err");
149 $r->content_type('text/plain; encoding=utf8');
150 $r->print("\n$err\n\n$data");
153 $r->print($data) if ($success);
159 my $ctx = {}; # new context for each page load
161 $ctx->{base_path} = $r->dir_config('OILSWebBasePath');
162 $ctx->{web_dir} = $r->dir_config('OILSWebWebDir');
163 $ctx->{debug_template} = ($r->dir_config('OILSWebDebugTemplate') =~ /true/io);
164 $ctx->{media_prefix} = $r->dir_config('OILSWebMediaPrefix');
165 $ctx->{hostname} = $r->hostname;
166 $ctx->{base_url} = $cgi->url(-base => 1);
167 $ctx->{skin} = $cgi->cookie(OILS_HTTP_COOKIE_SKIN) || 'default';
168 $ctx->{theme} = $cgi->cookie(OILS_HTTP_COOKIE_THEME) || 'default';
169 $ctx->{proto} = $cgi->https ? 'https' : 'http';
170 $ctx->{ext_proto} = $ctx->{proto};
171 my $default_locale = $r->dir_config('OILSWebDefaultLocale') || 'en_us';
173 my @template_paths = uniq $r->dir_config->get('OILSWebTemplatePath');
174 $ctx->{template_paths} = [ reverse @template_paths ];
176 my %locales = $r->dir_config->get('OILSWebLocale');
177 load_locale_handlers($ctx, %locales);
179 $ctx->{locales} = \%registered_locales;
181 # Set a locale cookie if the requested locale is valid
182 my $set_locale = $cgi->param('set_eg_locale') || '';
183 if (!(grep {$_ eq $set_locale} keys %registered_locales)) {
186 my $slc = $cgi->cookie({
187 '-name' => OILS_HTTP_COOKIE_LOCALE,
188 '-value' => $set_locale,
191 $r->headers_out->add('Set-Cookie' => $slc);
194 $ctx->{locale} = $set_locale ||
195 $cgi->cookie(OILS_HTTP_COOKIE_LOCALE) || $default_locale ||
196 parse_accept_lang($r->headers_in->get('Accept-Language'));
198 # set the editor default locale for each page load
199 my $ses_locale = parse_eg_locale($ctx->{locale});
200 OpenSRF::AppSession->default_locale($ses_locale);
201 # give templates access to the en-US style locale
202 $ctx->{eg_locale} = $ses_locale;
204 my $mprefix = $ctx->{media_prefix};
205 if($mprefix and $mprefix !~ /^http/ and $mprefix !~ /^\//) {
206 # if a hostname is provided /w no protocol, match the protocol to the current page
207 $ctx->{media_prefix} = ($cgi->https) ? "https://$mprefix" : "http://$mprefix";
213 # turn Accept-Language into something EG can understand
214 # TODO: try all langs, not just the first
215 sub parse_accept_lang {
217 return undef unless $al;
218 my ($locale) = split(/,/, $al);
219 ($locale) = split(/;/, $locale);
220 return undef unless $locale;
225 # Accept-Language uses locales like 'en', 'fr', 'fr_fr', while Evergreen
226 # internally uses 'en-US', 'fr-CA', 'fr-FR' (always with the 2 lowercase,
227 # hyphen, 2 uppercase convention)
228 sub parse_eg_locale {
229 my $ua_locale = shift || 'en_us';
231 $ua_locale =~ m/^(..).?(..)?$/;
232 my $lang_code = lc($1);
233 my $region_code = $2 ? uc($2) : uc($1);
234 return "$lang_code-$region_code";
237 # Given a URI, finds the configured template and any extra page
238 # arguments (trailing path info). Any extra data is returned
239 # as page arguments, in the form of an array, one item per
240 # /-separated URI component
246 $path =~ s/$base\/?//og;
249 my $as_xml = $r->dir_config('OILSWebForceValidXML');
250 my $ext = $r->dir_config('OILSWebDefaultTemplateExtension');
251 my $at_index = $r->dir_config('OILSWebStopAtIndex');
253 my @parts = split('/', $path);
254 my $localpath = $path;
256 if ($localpath =~ m|/css/|) {
257 $r->content_type('text/css; encoding=utf8');
259 $r->content_type('text/html; encoding=utf8');
263 last unless $localpath;
264 for my $tpath (@{$ctx->{template_paths}}) {
265 my $fpath = "$tpath/$localpath.$ext";
266 $r->log->debug("egweb: looking at possible template $fpath");
268 $template = "$localpath.$ext";
275 # no matching template was found in the current directory.
276 # stop-at-index requested; see if there is an index.ext
277 # file in the same directory instead.
278 for my $tpath (@{$ctx->{template_paths}}) {
279 # replace the final path component with 'index'
280 if ($localpath =~ m|/$|) {
281 $localpath .= 'index';
283 $localpath =~ s|/[^/]+$|/index|;
285 my $fpath = "$tpath/$localpath.$ext";
286 $r->log->debug("egweb: looking at possible template $fpath");
288 $template = "$localpath.$ext";
295 push(@args, pop @parts);
296 $localpath = join('/', @parts);
299 $page_args = [@args];
301 # no template configured or found
303 $r->log->debug("egweb: No template configured for path $path");
307 $r->log->debug("egweb: template = $template : page args = @$page_args");
308 return ($template, $page_args, $as_xml);
311 # Create an I18N sub-module for each supported locale
312 # Each module creates its own MakeText lexicon by parsing .po/.mo files
313 sub load_locale_handlers {
317 my $editor = new_editor();
318 my @locale_tags = sort { length($a) <=> length($b) } keys %locales;
320 # always fall back to en_us, the assumed template language
321 push(@locale_tags, 'en_us');
323 for my $idx (0..$#locale_tags) {
325 my $tag = $locale_tags[$idx];
326 next if grep { $_ eq $tag } keys %registered_locales;
328 my $res = $editor->json_query({
330 "evergreen.get_locale_name",
335 my $locale_name = $res->[0]->{"name"} if exists $res->[0]->{"name"};
336 next unless $locale_name;
341 # find the parent locale if possible. It will be
342 # longest left-anchored substring of the current tag
343 while( --$sub_idx >= 0 ) {
344 my $ptag = $locale_tags[$sub_idx];
345 if( substr($tag, 0, length($ptag)) eq $ptag ) {
346 $parent_tag = "::$ptag";
351 my $messages = $locales{$tag} || '';
353 # TODO Can we do this without eval?
354 my $eval = <<" EVAL";
355 package OpenILS::WWW::EGWeb::I18N::$tag;
356 use base 'OpenILS::WWW::EGWeb::I18N$parent_tag';
358 use Locale::Maketext::Lexicon {
361 use Locale::Maketext::Lexicon::Gettext;
362 if(open F, '$messages') {
363 our %Lexicon = (%Lexicon, %{ Locale::Maketext::Lexicon::Gettext->parse(<F>) });
366 warn "EGWeb: unable to open messages file: $messages";
375 $registered_locales{"$tag"} = $locale_name;
381 # base class for all supported locales
382 package OpenILS::WWW::EGWeb::I18N;
383 use base 'Locale::Maketext';
384 our %Lexicon = (_AUTO => 1);