6a8a717f01f8bf0b7fd207c77946c6c65252a521
[WebKit-https.git] / Websites / bugs.webkit.org / Bugzilla / Template.pm
1 # -*- Mode: perl; indent-tabs-mode: nil -*-
2 #
3 # The contents of this file are subject to the Mozilla Public
4 # License Version 1.1 (the "License"); you may not use this file
5 # except in compliance with the License. You may obtain a copy of
6 # the License at http://www.mozilla.org/MPL/
7 #
8 # Software distributed under the License is distributed on an "AS
9 # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
10 # implied. See the License for the specific language governing
11 # rights and limitations under the License.
12 #
13 # The Original Code is the Bugzilla Bug Tracking System.
14 #
15 # The Initial Developer of the Original Code is Netscape Communications
16 # Corporation. Portions created by Netscape are
17 # Copyright (C) 1998 Netscape Communications Corporation. All
18 # Rights Reserved.
19 #
20 # Contributor(s): Terry Weissman <terry@mozilla.org>
21 #                 Dan Mosedale <dmose@mozilla.org>
22 #                 Jacob Steenhagen <jake@bugzilla.org>
23 #                 Bradley Baetz <bbaetz@student.usyd.edu.au>
24 #                 Christopher Aillon <christopher@aillon.com>
25 #                 Tobias Burnus <burnus@net-b.de>
26 #                 Myk Melez <myk@mozilla.org>
27 #                 Max Kanat-Alexander <mkanat@bugzilla.org>
28 #                 Frédéric Buclin <LpSolit@gmail.com>
29 #                 Greg Hendricks <ghendricks@novell.com>
30 #                 David D. Kilzer <ddkilzer@kilzer.net>
31
32
33 package Bugzilla::Template;
34
35 use strict;
36
37 use Bugzilla::Constants;
38 use Bugzilla::Install::Requirements;
39 use Bugzilla::Install::Util qw(install_string template_include_path include_languages);
40 use Bugzilla::Util;
41 use Bugzilla::User;
42 use Bugzilla::Error;
43 use Bugzilla::Status;
44 use Bugzilla::Token;
45 use Bugzilla::Template::Parser;
46
47 use Cwd qw(abs_path);
48 use MIME::Base64;
49 # for time2str - replace by TT Date plugin??
50 use Date::Format ();
51 use File::Basename qw(dirname);
52 use File::Find;
53 use File::Path qw(rmtree mkpath);
54 use File::Spec;
55 use IO::Dir;
56
57 use base qw(Template);
58
59 # As per the Template::Base documentation, the _init() method is being called 
60 # by the new() constructor. We take advantage of this in order to plug our
61 # UTF-8-aware Parser object in neatly after the original _init() method has
62 # happened, in particular, after having set up the constants namespace.
63 # See bug 413121 for details.
64 sub _init {
65     my $self = shift;
66     my $config = $_[0];
67
68     $self->SUPER::_init(@_) || return undef;
69
70     $self->{PARSER} = $config->{PARSER}
71         = new Bugzilla::Template::Parser($config);
72
73     # Now we need to re-create the default Service object, making it aware
74     # of our Parser object.
75     $self->{SERVICE} = $config->{SERVICE}
76         = Template::Config->service($config);
77
78     return $self;
79 }
80
81 # Convert the constants in the Bugzilla::Constants module into a hash we can
82 # pass to the template object for reflection into its "constants" namespace
83 # (which is like its "variables" namespace, but for constants).  To do so, we
84 # traverse the arrays of exported and exportable symbols and ignoring the rest
85 # (which, if Constants.pm exports only constants, as it should, will be nothing else).
86 sub _load_constants {
87     my %constants;
88     foreach my $constant (@Bugzilla::Constants::EXPORT,
89                           @Bugzilla::Constants::EXPORT_OK)
90     {
91         if (ref Bugzilla::Constants->$constant) {
92             $constants{$constant} = Bugzilla::Constants->$constant;
93         }
94         else {
95             my @list = (Bugzilla::Constants->$constant);
96             $constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list;
97         }
98     }
99     return \%constants;
100 }
101
102 # Returns the path to the templates based on the Accept-Language
103 # settings of the user and of the available languages
104 # If no Accept-Language is present it uses the defined default
105 # Templates may also be found in the extensions/ tree
106 sub getTemplateIncludePath {
107     my $cache = Bugzilla->request_cache;
108     my $lang  = $cache->{'language'} || '';
109     $cache->{"template_include_path_$lang"} ||= template_include_path({
110         use_languages => Bugzilla->languages,
111         only_language => $lang });
112     return $cache->{"template_include_path_$lang"};
113 }
114
115 sub get_format {
116     my $self = shift;
117     my ($template, $format, $ctype) = @_;
118
119     $ctype ||= 'html';
120     $format ||= '';
121
122     # Security - allow letters and a hyphen only
123     $ctype =~ s/[^a-zA-Z\-]//g;
124     $format =~ s/[^a-zA-Z\-]//g;
125     trick_taint($ctype);
126     trick_taint($format);
127
128     $template .= ($format ? "-$format" : "");
129     $template .= ".$ctype.tmpl";
130
131     # Now check that the template actually exists. We only want to check
132     # if the template exists; any other errors (eg parse errors) will
133     # end up being detected later.
134     eval {
135         $self->context->template($template);
136     };
137     # This parsing may seem fragile, but it's OK:
138     # http://lists.template-toolkit.org/pipermail/templates/2003-March/004370.html
139     # Even if it is wrong, any sort of error is going to cause a failure
140     # eventually, so the only issue would be an incorrect error message
141     if ($@ && $@->info =~ /: not found$/) {
142         ThrowUserError('format_not_found', {'format' => $format,
143                                             'ctype'  => $ctype});
144     }
145
146     # Else, just return the info
147     return
148     {
149         'template'    => $template,
150         'format'      => $format,
151         'extension'   => $ctype,
152         'ctype'       => Bugzilla::Constants::contenttypes->{$ctype}
153     };
154 }
155
156 # This routine quoteUrls contains inspirations from the HTML::FromText CPAN
157 # module by Gareth Rees <garethr@cre.canon.co.uk>.  It has been heavily hacked,
158 # all that is really recognizable from the original is bits of the regular
159 # expressions.
160 # This has been rewritten to be faster, mainly by substituting 'as we go'.
161 # If you want to modify this routine, read the comments carefully
162
163 sub quoteUrls {
164     my ($text, $curr_bugid) = (@_);
165     return $text unless $text;
166
167     # We use /g for speed, but uris can have other things inside them
168     # (http://foo/bug#3 for example). Filtering that out filters valid
169     # bug refs out, so we have to do replacements.
170     # mailto can't contain space or #, so we don't have to bother for that
171     # Do this by escaping \0 to \1\0, and replacing matches with \0\0$count\0\0
172     # \0 is used because it's unlikely to occur in the text, so the cost of
173     # doing this should be very small
174
175     # escape the 2nd escape char we're using
176     my $chr1 = chr(1);
177     $text =~ s/\0/$chr1\0/g;
178
179     # However, note that adding the title (for buglinks) can affect things
180     # In particular, attachment matches go before bug titles, so that titles
181     # with 'attachment 1' don't double match.
182     # Dupe checks go afterwards, because that uses ^ and \Z, which won't occur
183     # if it was substituted as a bug title (since that always involve leading
184     # and trailing text)
185
186     # Because of entities, it's easier (and quicker) to do this before escaping
187
188     my @things;
189     my $count = 0;
190     my $tmp;
191
192     # Provide tooltips for full bug links (Bug 74355)
193     my $urlbase_re = '(' . join('|',
194         map { qr/$_/ } grep($_, Bugzilla->params->{'urlbase'}, 
195                             Bugzilla->params->{'sslbase'})) . ')';
196     $text =~ s~\b(${urlbase_re}\Qshow_bug.cgi?id=\E([0-9]+)(\#c([0-9]+))?)\b
197               ~($things[$count++] = get_bug_link($3, $1, $5)) &&
198                ("\0\0" . ($count-1) . "\0\0")
199               ~egox;
200
201     # non-mailto protocols
202     my $safe_protocols = join('|', SAFE_PROTOCOLS);
203     my $protocol_re = qr/($safe_protocols)/i;
204
205     $text =~ s~\b(${protocol_re}:  # The protocol:
206                   [^\s<>\"]+       # Any non-whitespace
207                   [\w\/])          # so that we end in \w or /
208               ~($tmp = html_quote($1)) &&
209                ($things[$count++] = "<a href=\"$tmp\">$tmp</a>") &&
210                ("\0\0" . ($count-1) . "\0\0")
211               ~egox;
212
213     # We have to quote now, otherwise the html itself is escaped
214     # THIS MEANS THAT A LITERAL ", <, >, ' MUST BE ESCAPED FOR A MATCH
215
216     $text = html_quote($text);
217
218     # Color quoted text
219     $text =~ s~^(&gt;.+)$~<span class="quote">$1</span >~mg;
220     $text =~ s~</span >\n<span class="quote">~\n~g;
221
222     # mailto:
223     # Use |<nothing> so that $1 is defined regardless
224     $text =~ s~\b(mailto:|)?([\w\.\-\+\=]+\@[\w\-]+(?:\.[\w\-]+)+)\b
225               ~<a href=\"mailto:$2\">$1$2</a>~igx;
226
227     # attachment links - handle both cases separately for simplicity
228     $text =~ s~((?:^Created\ an\ |\b)attachment\s*\(id=(\d+)\)(\s\[edit\])?)
229               ~($things[$count++] = get_attachment_link($2, $1)) &&
230                ("\0\0" . ($count-1) . "\0\0")
231               ~egmx;
232
233     $text =~ s~\b(attachment\s*\#?\s*(\d+))
234               ~($things[$count++] = get_attachment_link($2, $1)) &&
235                ("\0\0" . ($count-1) . "\0\0")
236               ~egmxi;
237
238     # Current bug ID this comment belongs to
239     my $current_bugurl = $curr_bugid ? "show_bug.cgi?id=$curr_bugid" : "";
240
241     # This handles bug a, comment b type stuff. Because we're using /g
242     # we have to do this in one pattern, and so this is semi-messy.
243     # Also, we can't use $bug_re?$comment_re? because that will match the
244     # empty string
245     my $bug_word = get_text('term', { term => 'bug' });
246     my $bug_re = qr/\Q$bug_word\E\s*\#?\s*(\d+)/i;
247     my $comment_re = qr/comment\s*\#?\s*(\d+)/i;
248     $text =~ s~\b($bug_re(?:\s*,?\s*$comment_re)?|$comment_re)
249               ~ # We have several choices. $1 here is the link, and $2-4 are set
250                 # depending on which part matched
251                (defined($2) ? get_bug_link($2,$1,$3) :
252                               "<a href=\"$current_bugurl#c$4\">$1</a>")
253               ~egox;
254
255     # Old duplicate markers. These don't use $bug_word because they are old
256     # and were never customizable.
257     $text =~ s~(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )
258                (\d+)
259                (?=\ \*\*\*\Z)
260               ~get_bug_link($1, $1)
261               ~egmx;
262
263     # Now remove the encoding hacks
264     $text =~ s/\0\0(\d+)\0\0/$things[$1]/eg;
265     $text =~ s/$chr1\0/\0/g;
266
267     return $text;
268 }
269
270 # Creates a link to an attachment, including its title.
271 sub get_attachment_link {
272     my ($attachid, $link_text) = @_;
273     my $dbh = Bugzilla->dbh;
274
275     detaint_natural($attachid)
276       || die "get_attachment_link() called with non-integer attachment number";
277
278     my ($bugid, $isobsolete, $desc) =
279         $dbh->selectrow_array('SELECT bug_id, isobsolete, description
280                                FROM attachments WHERE attach_id = ?',
281                                undef, $attachid);
282
283     if ($bugid) {
284         my $title = "";
285         my $className = "";
286         if (Bugzilla->user->can_see_bug($bugid)) {
287             $title = $desc;
288         }
289         if ($isobsolete) {
290             $className = "bz_obsolete";
291         }
292         # Prevent code injection in the title.
293         $title = html_quote(clean_text($title));
294
295         $link_text =~ s/ \[details\]$//;
296         my $linkval = "attachment.cgi?id=$attachid";
297         # Whitespace matters here because these links are in <pre> tags.
298         return qq|<span class="$className">|
299                . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>|
300                . qq| <a href="${linkval}&amp;action=edit" title="$title">[details]</a>|
301                . qq|</span>|;
302     }
303     else {
304         return qq{$link_text};
305     }
306 }
307
308 # Creates a link to a bug, including its title.
309 # It takes either two or three parameters:
310 #  - The bug number
311 #  - The link text, to place between the <a>..</a>
312 #  - An optional comment number, for linking to a particular
313 #    comment in the bug
314
315 sub get_bug_link {
316     my ($bug_num, $link_text, $comment_num) = @_;
317     my $dbh = Bugzilla->dbh;
318
319     if (!defined($bug_num) || ($bug_num eq "")) {
320         return "&lt;missing bug number&gt;";
321     }
322     my $quote_bug_num = html_quote($bug_num);
323     detaint_natural($bug_num) || return "&lt;invalid bug number: $quote_bug_num&gt;";
324
325     my ($bug_state, $bug_res, $bug_desc) =
326         $dbh->selectrow_array('SELECT bugs.bug_status, resolution, short_desc
327                                FROM bugs WHERE bugs.bug_id = ?',
328                                undef, $bug_num);
329
330     if ($bug_state) {
331         # Initialize these variables to be "" so that we don't get warnings
332         # if we don't change them below (which is highly likely).
333         my ($pre, $title, $post) = ("", "", "");
334
335         $title = get_text('get_status', {status => $bug_state});
336         if ($bug_state eq 'UNCONFIRMED') {
337             $pre = "<i>";
338             $post = "</i>";
339         }
340         elsif (!is_open_state($bug_state)) {
341             $pre = '<span class="bz_closed">';
342             $title .= ' ' . get_text('get_resolution', {resolution => $bug_res});
343             $post = '</span>';
344         }
345         if (Bugzilla->user->can_see_bug($bug_num)) {
346             $title .= " - $bug_desc";
347         }
348         # Prevent code injection in the title.
349         $title = html_quote(clean_text($title));
350
351         my $linkval = "show_bug.cgi?id=$bug_num";
352         if (defined $comment_num) {
353             $linkval .= "#c$comment_num";
354         }
355         return qq{$pre<a href="$linkval" title="$title">$link_text</a>$post};
356     }
357     else {
358         return qq{$link_text};
359     }
360 }
361
362 ###############################################################################
363 # Templatization Code
364
365 # The Template Toolkit throws an error if a loop iterates >1000 times.
366 # We want to raise that limit.
367 # NOTE: If you change this number, you MUST RE-RUN checksetup.pl!!!
368 # If you do not re-run checksetup.pl, the change you make will not apply
369 $Template::Directive::WHILE_MAX = 1000000;
370
371 # Use the Toolkit Template's Stash module to add utility pseudo-methods
372 # to template variables.
373 use Template::Stash;
374
375 # Add "contains***" methods to list variables that search for one or more 
376 # items in a list and return boolean values representing whether or not 
377 # one/all/any item(s) were found.
378 $Template::Stash::LIST_OPS->{ contains } =
379   sub {
380       my ($list, $item) = @_;
381       return grep($_ eq $item, @$list);
382   };
383
384 $Template::Stash::LIST_OPS->{ containsany } =
385   sub {
386       my ($list, $items) = @_;
387       foreach my $item (@$items) { 
388           return 1 if grep($_ eq $item, @$list);
389       }
390       return 0;
391   };
392
393 # Clone the array reference to leave the original one unaltered.
394 $Template::Stash::LIST_OPS->{ clone } =
395   sub {
396       my $list = shift;
397       return [@$list];
398   };
399
400 # Allow us to still get the scalar if we use the list operation ".0" on it,
401 # as we often do for defaults in query.cgi and other places.
402 $Template::Stash::SCALAR_OPS->{ 0 } = 
403   sub {
404       return $_[0];
405   };
406
407 # Add a "substr" method to the Template Toolkit's "scalar" object
408 # that returns a substring of a string.
409 $Template::Stash::SCALAR_OPS->{ substr } = 
410   sub {
411       my ($scalar, $offset, $length) = @_;
412       return substr($scalar, $offset, $length);
413   };
414
415 # Add a "truncate" method to the Template Toolkit's "scalar" object
416 # that truncates a string to a certain length.
417 $Template::Stash::SCALAR_OPS->{ truncate } = 
418   sub {
419       my ($string, $length, $ellipsis) = @_;
420       $ellipsis ||= "";
421       
422       return $string if !$length || length($string) <= $length;
423       
424       my $strlen = $length - length($ellipsis);
425       my $newstr = substr($string, 0, $strlen) . $ellipsis;
426       return $newstr;
427   };
428
429 # Create the template object that processes templates and specify
430 # configuration parameters that apply to all templates.
431
432 ###############################################################################
433
434 # Construct the Template object
435
436 # Note that all of the failure cases here can't use templateable errors,
437 # since we won't have a template to use...
438
439 sub create {
440     my $class = shift;
441     my %opts = @_;
442
443     # checksetup.pl will call us once for any template/lang directory.
444     # We need a possibility to reset the cache, so that no files from
445     # the previous language pollute the action.
446     if ($opts{'clean_cache'}) {
447         delete Bugzilla->request_cache->{template_include_path_};
448     }
449
450     # IMPORTANT - If you make any configuration changes here, make sure to
451     # make them in t/004.template.t and checksetup.pl.
452
453     return $class->new({
454         # Colon-separated list of directories containing templates.
455         INCLUDE_PATH => [\&getTemplateIncludePath],
456
457         # Remove white-space before template directives (PRE_CHOMP) and at the
458         # beginning and end of templates and template blocks (TRIM) for better
459         # looking, more compact content.  Use the plus sign at the beginning
460         # of directives to maintain white space (i.e. [%+ DIRECTIVE %]).
461         PRE_CHOMP => 1,
462         TRIM => 1,
463
464         COMPILE_DIR => bz_locations()->{'datadir'} . "/template",
465
466         # Initialize templates (f.e. by loading plugins like Hook).
467         PRE_PROCESS => "global/initialize.none.tmpl",
468
469         # Functions for processing text within templates in various ways.
470         # IMPORTANT!  When adding a filter here that does not override a
471         # built-in filter, please also add a stub filter to t/004template.t.
472         FILTERS => {
473
474             # Render text in required style.
475
476             inactive => [
477                 sub {
478                     my($context, $isinactive) = @_;
479                     return sub {
480                         return $isinactive ? '<span class="bz_inactive">'.$_[0].'</span>' : $_[0];
481                     }
482                 }, 1
483             ],
484
485             closed => [
486                 sub {
487                     my($context, $isclosed) = @_;
488                     return sub {
489                         return $isclosed ? '<span class="bz_closed">'.$_[0].'</span>' : $_[0];
490                     }
491                 }, 1
492             ],
493
494             obsolete => [
495                 sub {
496                     my($context, $isobsolete) = @_;
497                     return sub {
498                         return $isobsolete ? '<span class="bz_obsolete">'.$_[0].'</span>' : $_[0];
499                     }
500                 }, 1
501             ],
502
503             # Returns the text with backslashes, single/double quotes,
504             # and newlines/carriage returns escaped for use in JS strings.
505             js => sub {
506                 my ($var) = @_;
507                 $var =~ s/([\\\'\"\/])/\\$1/g;
508                 $var =~ s/\n/\\n/g;
509                 $var =~ s/\r/\\r/g;
510                 $var =~ s/\@/\\x40/g; # anti-spam for email addresses
511                 return $var;
512             },
513             
514             # Converts data to base64
515             base64 => sub {
516                 my ($data) = @_;
517                 return encode_base64($data);
518             },
519             
520             # HTML collapses newlines in element attributes to a single space,
521             # so form elements which may have whitespace (ie comments) need
522             # to be encoded using &#013;
523             # See bugs 4928, 22983 and 32000 for more details
524             html_linebreak => sub {
525                 my ($var) = @_;
526                 $var =~ s/\r\n/\&#013;/g;
527                 $var =~ s/\n\r/\&#013;/g;
528                 $var =~ s/\r/\&#013;/g;
529                 $var =~ s/\n/\&#013;/g;
530                 return $var;
531             },
532
533             # Prevents line break on hyphens and whitespaces.
534             no_break => sub {
535                 my ($var) = @_;
536                 $var =~ s/ /\&nbsp;/g;
537                 $var =~ s/-/\&#8209;/g;
538                 return $var;
539             },
540
541             xml => \&Bugzilla::Util::xml_quote ,
542
543             # This filter escapes characters in a variable or value string for
544             # use in a query string.  It escapes all characters NOT in the
545             # regex set: [a-zA-Z0-9_\-.].  The 'uri' filter should be used for
546             # a full URL that may have characters that need encoding.
547             url_quote => \&Bugzilla::Util::url_quote ,
548
549             # This filter is similar to url_quote but used a \ instead of a %
550             # as prefix. In addition it replaces a ' ' by a '_'.
551             css_class_quote => \&Bugzilla::Util::css_class_quote ,
552
553             quoteUrls => [ sub {
554                                my ($context, $bug) = @_;
555                                return sub {
556                                    my $text = shift;
557                                    return quoteUrls($text, $bug);
558                                };
559                            },
560                            1
561                          ],
562
563             bug_link => [ sub {
564                               my ($context, $bug) = @_;
565                               return sub {
566                                   my $text = shift;
567                                   return get_bug_link($bug, $text);
568                               };
569                           },
570                           1
571                         ],
572
573             bug_list_link => sub
574             {
575                 my $buglist = shift;
576                 return join(", ", map(get_bug_link($_, $_), split(/ *, */, $buglist)));
577             },
578
579             # In CSV, quotes are doubled, and any value containing a quote or a
580             # comma is enclosed in quotes.
581             csv => sub
582             {
583                 my ($var) = @_;
584                 $var =~ s/\"/\"\"/g;
585                 if ($var !~ /^-?(\d+\.)?\d*$/) {
586                     $var = "\"$var\"";
587                 }
588                 return $var;
589             } ,
590
591             # Format a filesize in bytes to a human readable value
592             unitconvert => sub
593             {
594                 my ($data) = @_;
595                 my $retval = "";
596                 my %units = (
597                     'KB' => 1024,
598                     'MB' => 1024 * 1024,
599                     'GB' => 1024 * 1024 * 1024,
600                 );
601
602                 if ($data < 1024) {
603                     return "$data bytes";
604                 } 
605                 else {
606                     my $u;
607                     foreach $u ('GB', 'MB', 'KB') {
608                         if ($data >= $units{$u}) {
609                             return sprintf("%.2f %s", $data/$units{$u}, $u);
610                         }
611                     }
612                 }
613             },
614
615             # Format a time for display (more info in Bugzilla::Util)
616             time => \&Bugzilla::Util::format_time,
617
618             # Bug 120030: Override html filter to obscure the '@' in user
619             #             visible strings.
620             # Bug 319331: Handle BiDi disruptions.
621             html => sub {
622                 my ($var) = Template::Filters::html_filter(@_);
623                 # Obscure '@'.
624                 $var =~ s/\@/\&#64;/g;
625                 if (Bugzilla->params->{'utf8'}) {
626                     # Remove the following characters because they're
627                     # influencing BiDi:
628                     # --------------------------------------------------------
629                     # |Code  |Name                      |UTF-8 representation|
630                     # |------|--------------------------|--------------------|
631                     # |U+202a|Left-To-Right Embedding   |0xe2 0x80 0xaa      |
632                     # |U+202b|Right-To-Left Embedding   |0xe2 0x80 0xab      |
633                     # |U+202c|Pop Directional Formatting|0xe2 0x80 0xac      |
634                     # |U+202d|Left-To-Right Override    |0xe2 0x80 0xad      |
635                     # |U+202e|Right-To-Left Override    |0xe2 0x80 0xae      |
636                     # --------------------------------------------------------
637                     #
638                     # The following are characters influencing BiDi, too, but
639                     # they can be spared from filtering because they don't
640                     # influence more than one character right or left:
641                     # --------------------------------------------------------
642                     # |Code  |Name                      |UTF-8 representation|
643                     # |------|--------------------------|--------------------|
644                     # |U+200e|Left-To-Right Mark        |0xe2 0x80 0x8e      |
645                     # |U+200f|Right-To-Left Mark        |0xe2 0x80 0x8f      |
646                     # --------------------------------------------------------
647                     $var =~ s/[\x{202a}-\x{202e}]//g;
648                 }
649                 return $var;
650             },
651
652             html_light => \&Bugzilla::Util::html_light_quote,
653
654             # iCalendar contentline filter
655             ics => [ sub {
656                          my ($context, @args) = @_;
657                          return sub {
658                              my ($var) = shift;
659                              my ($par) = shift @args;
660                              my ($output) = "";
661
662                              $var =~ s/[\r\n]/ /g;
663                              $var =~ s/([;\\\",])/\\$1/g;
664
665                              if ($par) {
666                                  $output = sprintf("%s:%s", $par, $var);
667                              } else {
668                                  $output = $var;
669                              }
670                              
671                              $output =~ s/(.{75,75})/$1\n /g;
672
673                              return $output;
674                          };
675                      },
676                      1
677                      ],
678
679             # Note that using this filter is even more dangerous than
680             # using "none," and you should only use it when you're SURE
681             # the output won't be displayed directly to a web browser.
682             txt => sub {
683                 my ($var) = @_;
684                 # Trivial HTML tag remover
685                 $var =~ s/<[^>]*>//g;
686                 # And this basically reverses the html filter.
687                 $var =~ s/\&#64;/@/g;
688                 $var =~ s/\&lt;/</g;
689                 $var =~ s/\&gt;/>/g;
690                 $var =~ s/\&quot;/\"/g;
691                 $var =~ s/\&amp;/\&/g;
692                 return $var;
693             },
694
695             # Wrap a displayed comment to the appropriate length
696             wrap_comment => [
697                 sub {
698                     my ($context, $cols) = @_;
699                     return sub { wrap_comment($_[0], $cols) }
700                 }, 1],
701
702             # We force filtering of every variable in key security-critical
703             # places; we have a none filter for people to use when they 
704             # really, really don't want a variable to be changed.
705             none => sub { return $_[0]; } ,
706         },
707
708         PLUGIN_BASE => 'Bugzilla::Template::Plugin',
709
710         CONSTANTS => _load_constants(),
711
712         # Default variables for all templates
713         VARIABLES => {
714             # Function for retrieving global parameters.
715             'Param' => sub { return Bugzilla->params->{$_[0]}; },
716
717             # Function to create date strings
718             'time2str' => \&Date::Format::time2str,
719
720             # Generic linear search function
721             'lsearch' => \&Bugzilla::Util::lsearch,
722
723             # Currently logged in user, if any
724             # If an sudo session is in progress, this is the user we're faking
725             'user' => sub { return Bugzilla->user; },
726
727             # If an sudo session is in progress, this is the user who
728             # started the session.
729             'sudoer' => sub { return Bugzilla->sudoer; },
730
731             # SendBugMail - sends mail about a bug, using Bugzilla::BugMail.pm
732             'SendBugMail' => sub {
733                 my ($id, $mailrecipients) = (@_);
734                 require Bugzilla::BugMail;
735                 Bugzilla::BugMail::Send($id, $mailrecipients);
736             },
737
738             # Allow templates to access the "corect" URLBase value
739             'urlbase' => sub { return Bugzilla::Util::correct_urlbase(); },
740
741             # Allow templates to access docs url with users' preferred language
742             'docs_urlbase' => sub { 
743                 my ($language) = include_languages();
744                 my $docs_urlbase = Bugzilla->params->{'docs_urlbase'};
745                 $docs_urlbase =~ s/\%lang\%/$language/;
746                 return $docs_urlbase;
747             },
748
749             # Allow templates to generate a token themselves.
750             'issue_hash_token' => \&Bugzilla::Token::issue_hash_token,
751
752             # These don't work as normal constants.
753             DB_MODULE        => \&Bugzilla::Constants::DB_MODULE,
754             REQUIRED_MODULES => 
755                 \&Bugzilla::Install::Requirements::REQUIRED_MODULES,
756             OPTIONAL_MODULES => sub {
757                 my @optional = @{OPTIONAL_MODULES()};
758                 @optional    = sort {$a->{feature} cmp $b->{feature}} 
759                                     @optional;
760                 return \@optional;
761             },
762         },
763
764    }) || die("Template creation failed: " . $class->error());
765 }
766
767 # Used as part of the two subroutines below.
768 our (%_templates_to_precompile, $_current_path);
769
770 sub precompile_templates {
771     my ($output) = @_;
772
773     # Remove the compiled templates.
774     my $datadir = bz_locations()->{'datadir'};
775     if (-e "$datadir/template") {
776         print install_string('template_removing_dir') . "\n" if $output;
777
778         # XXX This frequently fails if the webserver made the files, because
779         # then the webserver owns the directories. We could fix that by
780         # doing a chmod/chown on all the directories here.
781         rmtree("$datadir/template");
782
783         # Check that the directory was really removed
784         if(-e "$datadir/template") {
785             print "\n\n";
786             print "The directory '$datadir/template' could not be removed.\n";
787             print "Please remove it manually and rerun checksetup.pl.\n\n";
788             exit;
789         }
790     }
791
792     print install_string('template_precompile') if $output;
793
794     my $templatedir = bz_locations()->{'templatedir'};
795     # Don't hang on templates which use the CGI library
796     eval("use CGI qw(-no_debug)");
797     
798     my $dir_reader    = new IO::Dir($templatedir) || die "$templatedir: $!";
799     my @language_dirs = grep { /^[a-z-]+$/i } $dir_reader->read;
800     $dir_reader->close;
801
802     foreach my $dir (@language_dirs) {
803         next if ($dir eq 'CVS');
804         -d "$templatedir/$dir/default" || -d "$templatedir/$dir/custom" 
805             || next;
806         local $ENV{'HTTP_ACCEPT_LANGUAGE'} = $dir;
807         my $template = Bugzilla::Template->create(clean_cache => 1);
808
809         # Precompile all the templates found in all the directories.
810         %_templates_to_precompile = ();
811         foreach my $subdir (qw(custom extension default), bz_locations()->{'project'}) {
812             next unless $subdir; # If 'project' is empty.
813             $_current_path = File::Spec->catdir($templatedir, $dir, $subdir);
814             next unless -d $_current_path;
815             # Traverse the template hierarchy.
816             find({ wanted => \&_precompile_push, no_chdir => 1 }, $_current_path);
817         }
818         # The sort isn't totally necessary, but it makes debugging easier
819         # by making the templates always be compiled in the same order.
820         foreach my $file (sort keys %_templates_to_precompile) {
821             # Compile the template but throw away the result. This has the side-
822             # effect of writing the compiled version to disk.
823             $template->context->template($file);
824         }
825     }
826
827     # Under mod_perl, we look for templates using the absolute path of the
828     # template directory, which causes Template Toolkit to look for their 
829     # *compiled* versions using the full absolute path under the data/template
830     # directory. (Like data/template/var/www/html/mod_perl/.) To avoid
831     # re-compiling templates under mod_perl, we symlink to the
832     # already-compiled templates. This doesn't work on Windows.
833     if (!ON_WINDOWS) {
834         my $abs_root = dirname(abs_path($templatedir));
835         my $todir    = "$datadir/template$abs_root";
836         mkpath($todir);
837         # We use abs2rel so that the symlink will look like 
838         # "../../../../template" which works, while just 
839         # "data/template/template/" doesn't work.
840         my $fromdir = File::Spec->abs2rel("$datadir/template/template", $todir);
841         # We eval for systems that can't symlink at all, where "symlink" 
842         # throws a fatal error.
843         eval { symlink($fromdir, "$todir/template") 
844                    or warn "Failed to symlink from $fromdir to $todir: $!" };
845     }
846
847     # If anything created a Template object before now, clear it out.
848     delete Bugzilla->request_cache->{template};
849     # This is the single variable used to precompile templates,
850     # which needs to be cleared as well.
851     delete Bugzilla->request_cache->{template_include_path_};
852
853     print install_string('done') . "\n" if $output;
854 }
855
856 # Helper for precompile_templates
857 sub _precompile_push {
858     my $name = $File::Find::name;
859     return if (-d $name);
860     return if ($name =~ /\/CVS\//);
861     return if ($name !~ /\.tmpl$/);
862    
863     $name =~ s/\Q$_current_path\E\///;
864     $_templates_to_precompile{$name} = 1;
865 }
866
867 1;
868
869 __END__
870
871 =head1 NAME
872
873 Bugzilla::Template - Wrapper around the Template Toolkit C<Template> object
874
875 =head1 SYNOPSIS
876
877   my $template = Bugzilla::Template->create;
878   my $format = $template->get_format("foo/bar",
879                                      scalar($cgi->param('format')),
880                                      scalar($cgi->param('ctype')));
881
882 =head1 DESCRIPTION
883
884 This is basically a wrapper so that the correct arguments get passed into
885 the C<Template> constructor.
886
887 It should not be used directly by scripts or modules - instead, use
888 C<Bugzilla-E<gt>instance-E<gt>template> to get an already created module.
889
890 =head1 SUBROUTINES
891
892 =over
893
894 =item C<precompile_templates($output)>
895
896 Description: Compiles all of Bugzilla's templates in every language.
897              Used mostly by F<checksetup.pl>.
898
899 Params:      C<$output> - C<true> if you want the function to print
900                out information about what it's doing.
901
902 Returns:     nothing
903
904 =back
905
906 =head1 METHODS
907
908 =over
909
910 =item C<get_format($file, $format, $ctype)>
911
912  Description: Construct a format object from URL parameters.
913
914  Params:      $file   - Name of the template to display.
915               $format - When the template exists under several formats
916                         (e.g. table or graph), specify the one to choose.
917               $ctype  - Content type, see Bugzilla::Constants::contenttypes.
918
919  Returns:     A format object.
920
921 =back
922
923 =head1 SEE ALSO
924
925 L<Bugzilla>, L<Template>