]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/WWW/EGWeb.pm
8f19a07aa5d5e69b6902d4301540a190f26731f1
[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 Apache2::Const -compile => qw(OK DECLINED HTTP_INTERNAL_SERVER_ERROR);
8 use Apache2::Log;
9 use OpenSRF::EX qw(:try);
10 use OpenILS::Utils::CStoreEditor;
11
12 use constant OILS_HTTP_COOKIE_SKIN => 'oils:skin';
13 use constant OILS_HTTP_COOKIE_THEME => 'oils:theme';
14 use constant OILS_HTTP_COOKIE_LOCALE => 'oils:locale';
15
16 my $web_config;
17 my $web_config_file;
18 my $web_config_edit_time;
19 my %lh_cache; # locale handlers
20
21 sub import {
22     my $self = shift;
23     $web_config_file = shift;
24     unless(-r $web_config_file) {
25         warn "Invalid web config $web_config_file\n";
26         return;
27     }
28     check_web_config();
29 }
30
31
32 sub handler {
33     my $r = shift;
34     check_web_config($r); # option to disable this
35     my $ctx = load_context($r);
36     my $base = $ctx->{base_path};
37
38     $r->content_type('text/html; encoding=utf8');
39
40     my($template, $page_args, $as_xml) = find_template($r, $base, $ctx);
41     $ctx->{page_args} = $page_args;
42
43     my $stat = run_context_loader($r, $ctx);
44
45     return $stat unless $stat == Apache2::Const::OK;
46     return Apache2::Const::DECLINED unless $template;
47
48     $template = $ctx->{skin} . "/$template";
49
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}
54     });
55
56     unless($tt->process($template, {ctx => $ctx, l => set_text_handler($ctx, $r)})) {
57         $r->log->warn('Template error: ' . $tt->error);
58         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
59     }
60
61     return Apache2::Const::OK;
62 }
63
64 sub set_text_handler {
65     my $ctx = shift;
66     my $r = shift;
67
68     my $locale = $ctx->{locale};
69     $locale =~ s/-/_/g;
70
71     $r->log->info("messages locale = $locale");
72
73     unless($lh_cache{$locale}) {
74         $r->log->info("Unsupported locale: $locale");
75         $lh_cache{$locale} = $lh_cache{'en_US'};
76     }
77
78     return sub { return $lh_cache{$locale}->maketext(@_); };
79 }
80
81
82
83 sub run_context_loader {
84     my $r = shift;
85     my $ctx = shift;
86
87     my $stat = Apache2::Const::OK;
88
89     my $loader = $r->dir_config('OILSWebContextLoader');
90     return $stat unless $loader;
91
92     eval {
93         $loader->use;
94         $stat = $loader->new($r, $ctx)->load;
95     };
96
97     if($@) {
98         $r->log->error("Context Loader error: $@");
99         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
100     }
101
102     $r->log->info("context loader resulted in status $stat");
103     return $stat;
104 }
105
106 sub parse_as_xml {
107     my $r = shift;
108     my $ctx = shift;
109     my $data = shift;
110
111     my $success = 0;
112
113     try { 
114         my $doc = XML::LibXML->new->parse_string($data); 
115         $data = $doc->documentElement->toStringC14N;
116         $data = $ctx->{final_dtd} . "\n" . $data;
117         $success = 1;
118     } otherwise {
119             my $e = shift;
120         my $err = "Invalid XML: $e";
121         $r->log->error($err);
122         $r->content_type('text/plain; encoding=utf8');
123         $r->print("\n$err\n\n$data");
124     };
125
126     $r->print($data) if ($success);
127 }
128
129
130 sub load_context {
131     my $r = shift;
132     my $cgi = CGI->new;
133     my $ctx = {}; # new context for each page load
134     $ctx->{$_} = $web_config->{base_ctx}->{$_} for keys %{$web_config->{base_ctx}};
135     $ctx->{hostname} = $r->hostname;
136     $ctx->{base_url} = $cgi->url(-base => 1);
137     $ctx->{skin} = $cgi->cookie(OILS_HTTP_COOKIE_SKIN) || 'default';
138     $ctx->{theme} = $cgi->cookie(OILS_HTTP_COOKIE_THEME) || 'default';
139
140     $ctx->{locale} = 
141         $cgi->cookie(OILS_HTTP_COOKIE_LOCALE) || 
142         parse_accept_lang($r->headers_in->get('Accept-Language')) || 'en-US';
143
144     my $mprefix = $ctx->{media_prefix};
145     if($mprefix and $mprefix !~ /^http/ and $mprefix !~ /^\//) {
146         # if a hostname is provided /w no protocol, match the protocol to the current page
147         $ctx->{media_prefix} = ($cgi->https) ? "https://$mprefix" : "http://$mprefix";
148     }
149
150     return $ctx;
151 }
152
153 # turn Accept-Language into sometihng EG can understand
154 sub parse_accept_lang {
155     my $al = shift;
156     return undef unless $al;
157     my ($locale) = split(/,/, $al);
158     ($locale) = split(/;/, $locale);
159     return undef unless $locale;
160     $locale =~ s/-(.*)/eval '-'.uc("$1")/e;
161     return $locale;
162 }
163
164 # Given a URI, finds the configured template and any extra page 
165 # arguments (trailing path info).  Any extra data is returned
166 # as page arguments, in the form of an array, one item per 
167 # /-separated URI component
168 sub find_template {
169     my $r = shift;
170     my $base = shift;
171     my $ctx = shift;
172     my $skin = $ctx->{skin};
173     my $path = $r->uri;
174     $path =~ s/$base//og;
175     my @parts = split('/', $path);
176     my $template = '';
177     my $page_args = [];
178     my $as_xml = $ctx->{force_valid_xml};
179     my $handler = $web_config->{handlers};
180
181     while(@parts) {
182         my $part = shift @parts;
183         next unless $part;
184         my $t = $handler->{$part};
185         if(ref($t) eq 'PathConfig') {
186             $template = $t->{template};
187             $as_xml = ($t->{as_xml} and $t->{as_xml} =~ /true/io) || $as_xml;
188             $page_args = [@parts];
189             last;
190         } else {
191             $handler = $t;
192         }
193     }
194
195     unless($template) { # no template configured
196
197         # see if we can magically find the template based on the path and default extension
198         my $ext = $ctx->{default_template_extension};
199
200         my @parts = split('/', $path);
201         my $localpath = $path;
202         my @args;
203         while(@parts) {
204             last unless $localpath;
205             for my $tpath (@{$ctx->{template_paths}}) {
206                 my $fpath = "$tpath/$skin/$localpath.$ext";
207                 $r->log->debug("looking at possible template $fpath");
208                 if(-r $fpath) {
209                     $template = "$localpath.$ext";
210                     last;
211                 }
212             }
213             last if $template;
214             push(@args, pop @parts);
215             $localpath = '/'.join('/', @parts);
216         } 
217
218         $page_args = [@args];
219
220         # no template configured or found
221         unless($template) {
222             $r->log->warn("No template configured for path $path");
223             return ();
224         }
225     }
226
227     $r->log->debug("template = $template : page args = @$page_args");
228     return ($template, $page_args, $as_xml);
229 }
230
231 # if the web configuration file has never been loaded or has
232 # changed since the last load, reload it
233 sub check_web_config {
234     my $r = shift;
235     my $epoch = stat($web_config_file)->mtime;
236     unless($web_config_edit_time and $web_config_edit_time == $epoch) {
237         $r->log->debug("Reloading web config after edit...") if $r;
238         $web_config_edit_time = $epoch;
239         $web_config = parse_config($web_config_file);
240     }
241 }
242
243 # Create an I18N sub-module for each supported locale
244 # Each module creates its own MakeText lexicon by parsing .po/.mo files
245 sub load_locale_handlers {
246     my $ctx = shift;
247     my $locales = $ctx->{locales};
248
249     for my $lang (keys %$locales) {
250         my $messages = $locales->{$lang};
251         $messages = '' if ref $messages; # empty {}
252
253         # TODO Can we do this without eval?
254         my $eval = <<EVAL;
255             package OpenILS::WWW::EGWeb::I18N::$lang;
256             use base 'OpenILS::WWW::EGWeb::I18N';
257             if(\$messages) {
258                 use Locale::Maketext::Lexicon::Gettext;
259                 if(open F, '$messages') {
260                     our %Lexicon = (%Lexicon, %{ Locale::Maketext::Lexicon::Gettext->parse(<F>) });
261                     close F;
262                 } else {
263                     warn "unable to open messages file: $messages"; 
264                 }
265             }
266 EVAL
267         eval $eval;
268         warn "$@\n" if $@; # TODO better logging
269         $lh_cache{$lang} = "OpenILS::WWW::EGWeb::I18N::$lang"->new;
270     }
271 }
272
273
274
275 sub parse_config {
276     my $cfg_file = shift;
277     my $data = XML::Simple->new->XMLin($cfg_file);
278     my $ctx = {};
279     my $handlers = {};
280
281     $ctx->{media_prefix} = (ref $data->{media_prefix}) ? '' : $data->{media_prefix};
282     $ctx->{base_path} = (ref $data->{base_path}) ? '' : $data->{base_path};
283     $ctx->{template_paths} = [];
284     $ctx->{force_valid_xml} = ( ($data->{force_valid_xml}||'') =~ /true/io) ? 1 : 0;
285     $ctx->{debug_template} = ( ($data->{debug_template}||'')  =~ /true/io) ? 1 : 0;
286     $ctx->{default_template_extension} = $data->{default_template_extension} || 'tt2';
287     $ctx->{web_dir} = $data->{web_dir};
288     $ctx->{locales} = $data->{locales};
289     load_locale_handlers($ctx);
290
291     my $tpaths = $data->{template_paths}->{path};
292     $tpaths = [$tpaths] unless ref $tpaths;
293     push(@{$ctx->{template_paths}}, $_) for @$tpaths;
294
295     for my $handler (@{$data->{handlers}->{handler}}) {
296         my @parts = split('/', $handler->{path});
297         my $h = $handlers;
298         my $pcount = scalar(@parts);
299         for(my $i = 0; $i < $pcount; $i++) {
300             my $p = $parts[$i];
301             unless(defined $h->{$p}) {
302                 if($i == $pcount - 1) {
303                     $h->{$p} = PathConfig->new(%$handler);
304                     last;
305                 } else {
306                     $h->{$p} = {};
307                 }
308             }
309             $h = $h->{$p};
310         }
311     }
312
313     return {base_ctx => $ctx, handlers => $handlers};
314 }
315
316 package PathConfig;
317 sub new {
318     my($class, %args) = @_;
319     return bless(\%args, $class);
320 }
321
322 # base class for all supported locales
323 package OpenILS::WWW::EGWeb::I18N;
324 use base 'Locale::Maketext';
325 our %Lexicon = (_AUTO => 1);
326
327 1;