~ubuntu-branches/ubuntu/oneiric/bugzilla/oneiric

« back to all changes in this revision

Viewing changes to Bugzilla/Template.pm

  • Committer: Bazaar Package Importer
  • Author(s): Raphael Bossek
  • Date: 2008-06-27 22:34:34 UTC
  • mfrom: (1.1.7 upstream)
  • Revision ID: james.westby@ubuntu.com-20080627223434-0ib57vstn43bb4a3
Tags: 3.0.4.1-1
* Update of French, Russian and German translations. (closes: #488251)
* Added Bulgarian and Belarusian translations.

Show diffs side-by-side

added added

removed removed

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