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