1 package OpenILS::WWW::EGWeb;
2 use strict; use warnings;
8 use Apache2::Const -compile => qw(OK DECLINED HTTP_INTERNAL_SERVER_ERROR);
10 use OpenSRF::EX qw(:try);
11 use OpenILS::Utils::CStoreEditor;
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';
19 my $web_config_edit_time;
23 $web_config_file = shift || '';
24 unless(-r $web_config_file) {
25 warn "Invalid web config $web_config_file\n";
34 check_web_config($r); # option to disable this
35 my $ctx = load_context($r);
36 my $base = $ctx->{base_path};
38 $r->content_type('text/html; encoding=utf8');
40 my($template, $page_args, $as_xml) = find_template($r, $base, $ctx);
41 $ctx->{page_args} = $page_args;
43 my $stat = run_context_loader($r, $ctx);
45 return $stat unless $stat == Apache2::Const::OK;
46 return Apache2::Const::DECLINED unless $template;
48 my $text_handler = set_text_handler($ctx, $r);
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},
55 EGI18N => 'OpenILS::WWW::EGWeb::I18NFilter',
56 CGI_utf8 => 'OpenILS::WWW::EGWeb::CGI_utf8'
59 # Register a dynamic filter factory for our locale::maketext generator
63 return sub { $text_handler->(shift(), @args); }
69 $ctx->{encode_utf8} = sub {return encode_utf8(shift())};
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;
76 return Apache2::Const::OK;
79 sub set_text_handler {
83 my $locale = $ctx->{locale};
85 $r->log->debug("egweb: messages locale = $locale");
88 my $lh = OpenILS::WWW::EGWeb::I18N->get_handle($locale);
89 return $lh->maketext(@_);
95 sub run_context_loader {
99 my $stat = Apache2::Const::OK;
101 my $loader = $r->dir_config('OILSWebContextLoader');
102 return $stat unless $loader;
106 $stat = $loader->new($r, $ctx)->load;
110 $r->log->error("egweb: Context Loader error: $@");
111 return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
114 $r->log->debug("egweb: context loader resulted in status $stat");
126 my $doc = XML::LibXML->new->parse_string($data);
127 $data = $doc->documentElement->toStringC14N;
128 $data = $ctx->{final_dtd} . "\n" . $data;
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");
138 $r->print($data) if ($success);
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';
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}}");
160 $cgi->cookie(OILS_HTTP_COOKIE_LOCALE) ||
161 parse_accept_lang($r->headers_in->get('Accept-Language')) || 'en_us';
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";
172 # turn Accept-Language into sometihng EG can understand
173 # TODO: try all langs, not just the first
174 sub parse_accept_lang {
176 return undef unless $al;
177 my ($locale) = split(/,/, $al);
178 ($locale) = split(/;/, $locale);
179 return undef unless $locale;
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
193 $path =~ s/$base\/?//og;
194 my @parts = split('/', $path);
197 my $as_xml = $ctx->{force_valid_xml};
198 my $handler = $web_config->{handlers};
201 my $part = shift @parts;
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];
214 unless($template) { # no template configured
216 # see if we can magically find the template based on the path and default extension
217 my $ext = $ctx->{default_template_extension};
219 my @parts = split('/', $path);
220 my $localpath = $path;
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");
228 $template = "$localpath.$ext";
233 push(@args, pop @parts);
234 $localpath = join('/', @parts);
237 $page_args = [@args];
239 # no template configured or found
241 $r->log->debug("egweb: No template configured for path $path");
246 $r->log->debug("egweb: template = $template : page args = @$page_args");
247 return ($template, $page_args, $as_xml);
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 {
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);
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 {
266 my $locales = $ctx->{locales};
268 my @locale_tags = sort { length($a) <=> length($b) } keys %$locales;
270 for my $idx (0..$#locale_tags) {
272 my $tag = $locale_tags[$idx];
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";
286 my $messages = $locales->{$tag};
287 $messages = '' if ref $messages; # empty {}
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';
294 use Locale::Maketext::Lexicon::Gettext;
295 if(open F, '$messages') {
296 our %Lexicon = (%Lexicon, %{ Locale::Maketext::Lexicon::Gettext->parse(<F>) });
299 warn "EGWeb: unable to open messages file: $messages";
304 warn "$@\n" if $@; # TODO better logging
311 my $cfg_file = shift;
312 my $data = XML::Simple->new->XMLin($cfg_file);
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);
326 my $tpaths = $data->{template_paths}->{path};
327 $tpaths = [$tpaths] unless ref $tpaths;
328 push(@{$ctx->{template_paths}}, $_) for @$tpaths;
330 for my $handler (@{$data->{handlers}->{handler}}) {
331 my @parts = split('/', $handler->{path});
333 my $pcount = scalar(@parts);
334 for(my $i = 0; $i < $pcount; $i++) {
336 unless(defined $h->{$p}) {
337 if($i == $pcount - 1) {
338 $h->{$p} = PathConfig->new(%$handler);
348 return {base_ctx => $ctx, handlers => $handlers};
353 my($class, %args) = @_;
354 return bless(\%args, $class);
357 # base class for all supported locales
358 package OpenILS::WWW::EGWeb::I18N;
359 use base 'Locale::Maketext';
360 our %Lexicon = (_AUTO => 1);