Template.pm 34.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Terry Weissman <terry@mozilla.org>
#                 Dan Mosedale <dmose@mozilla.org>
#                 Jacob Steenhagen <jake@bugzilla.org>
#                 Bradley Baetz <bbaetz@student.usyd.edu.au>
#                 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 34 35 36

package Bugzilla::Template;

use strict;

37
use Bugzilla::Constants;
38
use Bugzilla::Util;
39
use Bugzilla::User;
40
use Bugzilla::Error;
41
use MIME::Base64;
42
use Bugzilla::Bug;
43 44 45

# for time2str - replace by TT Date plugin??
use Date::Format ();
46 47 48 49
use File::Find;
use File::Path;
use File::Spec;
use IO::Dir;
50 51 52

use base qw(Template);

53
# Convert the constants in the Bugzilla::Constants module into a hash we can
54 55 56 57 58
# pass to the template object for reflection into its "constants" namespace
# (which is like its "variables" namespace, but for constants).  To do so, we
# traverse the arrays of exported and exportable symbols, pulling out functions
# (which is how Perl implements constants) and ignoring the rest (which, if
# Constants.pm exports only constants, as it should, will be nothing else).
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
sub _load_constants {
    use Bugzilla::Constants ();
    my %constants;
    foreach my $constant (@Bugzilla::Constants::EXPORT,
                          @Bugzilla::Constants::EXPORT_OK)
    {
        if (defined &{$Bugzilla::Constants::{$constant}}) {
            # Constants can be lists, and we can't know whether we're
            # getting a scalar or a list in advance, since they come to us
            # as the return value of a function call, so we have to
            # retrieve them all in list context into anonymous arrays,
            # then extract the scalar ones (i.e. the ones whose arrays
            # contain a single element) from their arrays.
            $constants{$constant} = [&{$Bugzilla::Constants::{$constant}}];
            if (scalar(@{$constants{$constant}}) == 1) {
                $constants{$constant} = @{$constants{$constant}}[0];
            }
76 77
        }
    }
78
    return \%constants;
79 80
}

81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
# Make an ordered list out of a HTTP Accept-Language header see RFC 2616, 14.4
# We ignore '*' and <language-range>;q=0
# For languages with the same priority q the order remains unchanged.
sub sortAcceptLanguage {
    sub sortQvalue { $b->{'qvalue'} <=> $a->{'qvalue'} }
    my $accept_language = $_[0];

    # clean up string.
    $accept_language =~ s/[^A-Za-z;q=0-9\.\-,]//g;
    my @qlanguages;
    my @languages;
    foreach(split /,/, $accept_language) {
        if (m/([A-Za-z\-]+)(?:;q=(\d(?:\.\d+)))?/) {
            my $lang   = $1;
            my $qvalue = $2;
            $qvalue = 1 if not defined $qvalue;
            next if $qvalue == 0;
            $qvalue = 1 if $qvalue > 1;
            push(@qlanguages, {'qvalue' => $qvalue, 'language' => $lang});
        }
    }

    return map($_->{'language'}, (sort sortQvalue @qlanguages));
}

# Returns the path to the templates based on the Accept-Language
# settings of the user and of the available languages
# If no Accept-Language is present it uses the defined default
109
# Templates may also be found in the extensions/ tree
110
sub getTemplateIncludePath {
111
    # Return cached value if available
112

113 114 115
    my $include_path = Bugzilla->request_cache->{template_include_path};
    return $include_path if $include_path;

116 117 118
    my $templatedir = bz_locations()->{'templatedir'};
    my $project     = bz_locations()->{'project'};

119
    my $languages = trim(Bugzilla->params->{'languages'});
120
    if (not ($languages =~ /,/)) {
121
       if ($project) {
122
           $include_path = [
123 124 125 126 127
               "$templatedir/$languages/$project",
               "$templatedir/$languages/custom",
               "$templatedir/$languages/default"
           ];
       } else {
128
           $include_path = [
129 130 131 132
               "$templatedir/$languages/custom",
               "$templatedir/$languages/default"
           ];
       }
133 134 135 136 137 138
    }
    my @languages       = sortAcceptLanguage($languages);
    my @accept_language = sortAcceptLanguage($ENV{'HTTP_ACCEPT_LANGUAGE'} || "" );
    my @usedlanguages;
    foreach my $lang (@accept_language) {
        # Per RFC 1766 and RFC 2616 any language tag matches also its 
139
        # primary tag. That is 'en' (accept language)  matches 'en-us',
140
        # 'en-uk' etc. but not the otherway round. (This is unfortunately
141 142
        # not very clearly stated in those RFC; see comment just over 14.5
        # in http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4)
143
        if(my @found = grep /^\Q$lang\E(-.+)?$/i, @languages) {
144 145 146
            push (@usedlanguages, @found);
        }
    }
147
    push(@usedlanguages, Bugzilla->params->{'defaultlanguage'});
148
    if ($project) {
149
        $include_path = [
150 151 152 153 154 155 156 157
           map((
               "$templatedir/$_/$project",
               "$templatedir/$_/custom",
               "$templatedir/$_/default"
               ), @usedlanguages
            )
        ];
    } else {
158
        $include_path = [
159 160 161 162 163 164 165
           map((
               "$templatedir/$_/custom",
               "$templatedir/$_/default"
               ), @usedlanguages
            )
        ];
    }
166 167
    
    # add in extension template directories:
168
    my @extensions = glob(bz_locations()->{'extensionsdir'} . "/*");
169 170 171
    foreach my $extension (@extensions) {
        trick_taint($extension); # since this comes right from the filesystem
                                 # we have bigger issues if it is insecure
172
        push(@$include_path,
173 174 175 176 177 178 179
            map((
                $extension."/template/".$_),
               @usedlanguages));
    }
    
    # remove duplicates since they keep popping up:
    my @dirs;
180
    foreach my $dir (@$include_path) {
181 182
        push(@dirs, $dir) unless grep ($dir eq $_, @dirs);
    }
183
    Bugzilla->request_cache->{template_include_path} = \@dirs;
184
    
185
    return Bugzilla->request_cache->{template_include_path};
186 187
}

188 189
sub put_header {
    my $self = shift;
190
    my $vars = {};
191 192 193 194 195 196 197 198 199
    ($vars->{'title'}, $vars->{'h1'}, $vars->{'h2'}) = (@_);
     
    $self->process("global/header.html.tmpl", $vars)
      || ThrowTemplateError($self->error());
    $vars->{'header_done'} = 1;
}

sub put_footer {
    my $self = shift;
200
    $self->process("global/footer.html.tmpl")
201 202 203
      || ThrowTemplateError($self->error());
}

204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
sub get_format {
    my $self = shift;
    my ($template, $format, $ctype) = @_;

    $ctype ||= 'html';
    $format ||= '';

    # Security - allow letters and a hyphen only
    $ctype =~ s/[^a-zA-Z\-]//g;
    $format =~ s/[^a-zA-Z\-]//g;
    trick_taint($ctype);
    trick_taint($format);

    $template .= ($format ? "-$format" : "");
    $template .= ".$ctype.tmpl";

    # Now check that the template actually exists. We only want to check
    # if the template exists; any other errors (eg parse errors) will
    # end up being detected later.
    eval {
        $self->context->template($template);
    };
226
    # This parsing may seem fragile, but it's OK:
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
    # http://lists.template-toolkit.org/pipermail/templates/2003-March/004370.html
    # Even if it is wrong, any sort of error is going to cause a failure
    # eventually, so the only issue would be an incorrect error message
    if ($@ && $@->info =~ /: not found$/) {
        ThrowUserError('format_not_found', {'format' => $format,
                                            'ctype'  => $ctype});
    }

    # Else, just return the info
    return
    {
        'template'    => $template,
        'extension'   => $ctype,
        'ctype'       => Bugzilla::Constants::contenttypes->{$ctype}
    };
}
243

244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
# This routine quoteUrls contains inspirations from the HTML::FromText CPAN
# module by Gareth Rees <garethr@cre.canon.co.uk>.  It has been heavily hacked,
# all that is really recognizable from the original is bits of the regular
# expressions.
# This has been rewritten to be faster, mainly by substituting 'as we go'.
# If you want to modify this routine, read the comments carefully

sub quoteUrls {
    my ($text, $curr_bugid) = (@_);
    return $text unless $text;

    # We use /g for speed, but uris can have other things inside them
    # (http://foo/bug#3 for example). Filtering that out filters valid
    # bug refs out, so we have to do replacements.
    # mailto can't contain space or #, so we don't have to bother for that
    # Do this by escaping \0 to \1\0, and replacing matches with \0\0$count\0\0
260
    # \0 is used because it's unlikely to occur in the text, so the cost of
261 262 263 264 265 266 267 268 269 270 271 272
    # doing this should be very small
    # Also, \0 won't appear in the value_quote'd bug title, so we don't have
    # to worry about bogus substitutions from there

    # escape the 2nd escape char we're using
    my $chr1 = chr(1);
    $text =~ s/\0/$chr1\0/g;

    # However, note that adding the title (for buglinks) can affect things
    # In particular, attachment matches go before bug titles, so that titles
    # with 'attachment 1' don't double match.
    # Dupe checks go afterwards, because that uses ^ and \Z, which won't occur
273
    # if it was substituted as a bug title (since that always involve leading
274 275
    # and trailing text)

276
    # Because of entities, it's easier (and quicker) to do this before escaping
277 278 279 280 281

    my @things;
    my $count = 0;
    my $tmp;

282
    # Provide tooltips for full bug links (Bug 74355)
283 284 285
    my $urlbase_re = '(' . join('|',
        map { qr/$_/ } grep($_, Bugzilla->params->{'urlbase'}, 
                            Bugzilla->params->{'sslbase'})) . ')';
286 287 288 289 290
    $text =~ s~\b(${urlbase_re}\Qshow_bug.cgi?id=\E([0-9]+))\b
              ~($things[$count++] = get_bug_link($3, $1)) &&
               ("\0\0" . ($count-1) . "\0\0")
              ~egox;

291
    # non-mailto protocols
292 293
    my $safe_protocols = join('|', SAFE_PROTOCOLS);
    my $protocol_re = qr/($safe_protocols)/i;
294 295 296 297 298 299 300 301 302

    $text =~ s~\b(${protocol_re}:  # The protocol:
                  [^\s<>\"]+       # Any non-whitespace
                  [\w\/])          # so that we end in \w or /
              ~($tmp = html_quote($1)) &&
               ($things[$count++] = "<a href=\"$tmp\">$tmp</a>") &&
               ("\0\0" . ($count-1) . "\0\0")
              ~egox;

303
    # We have to quote now, otherwise the html itself is escaped
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
    # THIS MEANS THAT A LITERAL ", <, >, ' MUST BE ESCAPED FOR A MATCH

    $text = html_quote($text);

    # Color quoted text
    $text =~ s~^(&gt;.+)$~<span class="quote">$1</span >~mg;
    $text =~ s~</span >\n<span class="quote">~\n~g;

    # mailto:
    # Use |<nothing> so that $1 is defined regardless
    $text =~ s~\b(mailto:|)?([\w\.\-\+\=]+\@[\w\-]+(?:\.[\w\-]+)+)\b
              ~<a href=\"mailto:$2\">$1$2</a>~igx;

    # attachment links - handle both cases separately for simplicity
    $text =~ s~((?:^Created\ an\ |\b)attachment\s*\(id=(\d+)\)(\s\[edit\])?)
              ~($things[$count++] = get_attachment_link($2, $1)) &&
               ("\0\0" . ($count-1) . "\0\0")
              ~egmx;

    $text =~ s~\b(attachment\s*\#?\s*(\d+))
              ~($things[$count++] = get_attachment_link($2, $1)) &&
               ("\0\0" . ($count-1) . "\0\0")
              ~egmxi;

    # Current bug ID this comment belongs to
    my $current_bugurl = $curr_bugid ? "show_bug.cgi?id=$curr_bugid" : "";

    # This handles bug a, comment b type stuff. Because we're using /g
    # we have to do this in one pattern, and so this is semi-messy.
    # Also, we can't use $bug_re?$comment_re? because that will match the
    # empty string
335 336
    my $bug_word = get_text('term', { term => 'bug' });
    my $bug_re = qr/\Q$bug_word\E\s*\#?\s*(\d+)/i;
337 338 339 340 341 342 343 344
    my $comment_re = qr/comment\s*\#?\s*(\d+)/i;
    $text =~ s~\b($bug_re(?:\s*,?\s*$comment_re)?|$comment_re)
              ~ # We have several choices. $1 here is the link, and $2-4 are set
                # depending on which part matched
               (defined($2) ? get_bug_link($2,$1,$3) :
                              "<a href=\"$current_bugurl#c$4\">$1</a>")
              ~egox;

345 346
    # Old duplicate markers. These don't use $bug_word because they are old
    # and were never customizable.
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
    $text =~ s~(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )
               (\d+)
               (?=\ \*\*\*\Z)
              ~get_bug_link($1, $1)
              ~egmx;

    # Now remove the encoding hacks
    $text =~ s/\0\0(\d+)\0\0/$things[$1]/eg;
    $text =~ s/$chr1\0/\0/g;

    return $text;
}

# Creates a link to an attachment, including its title.
sub get_attachment_link {
    my ($attachid, $link_text) = @_;
    my $dbh = Bugzilla->dbh;

    detaint_natural($attachid)
      || die "get_attachment_link() called with non-integer attachment number";

    my ($bugid, $isobsolete, $desc) =
        $dbh->selectrow_array('SELECT bug_id, isobsolete, description
                               FROM attachments WHERE attach_id = ?',
                               undef, $attachid);

    if ($bugid) {
        my $title = "";
        my $className = "";
        if (Bugzilla->user->can_see_bug($bugid)) {
            $title = $desc;
        }
        if ($isobsolete) {
            $className = "bz_obsolete";
        }
        # Prevent code injection in the title.
        $title = value_quote($title);

385
        $link_text =~ s/ \[details\]$//;
386
        my $linkval = "attachment.cgi?id=$attachid";
387 388
        # Whitespace matters here because these links are in <pre> tags.
        return qq|<span class="$className">|
389 390
               . qq|<a href="${linkval}" title="$title">$link_text</a>|
               . qq| <a href="${linkval}&amp;action=edit" title="$title">[details]</a>|
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
               . qq|</span>|;
    }
    else {
        return qq{$link_text};
    }
}

# Creates a link to a bug, including its title.
# It takes either two or three parameters:
#  - The bug number
#  - The link text, to place between the <a>..</a>
#  - An optional comment number, for linking to a particular
#    comment in the bug

sub get_bug_link {
    my ($bug_num, $link_text, $comment_num) = @_;
    my $dbh = Bugzilla->dbh;

    if (!defined($bug_num) || ($bug_num eq "")) {
        return "&lt;missing bug number&gt;";
    }
    my $quote_bug_num = html_quote($bug_num);
    detaint_natural($bug_num) || return "&lt;invalid bug number: $quote_bug_num&gt;";

    my ($bug_state, $bug_res, $bug_desc) =
        $dbh->selectrow_array('SELECT bugs.bug_status, resolution, short_desc
                               FROM bugs WHERE bugs.bug_id = ?',
                               undef, $bug_num);

    if ($bug_state) {
        # Initialize these variables to be "" so that we don't get warnings
        # if we don't change them below (which is highly likely).
        my ($pre, $title, $post) = ("", "", "");

        $title = $bug_state;
        if ($bug_state eq 'UNCONFIRMED') {
            $pre = "<i>";
            $post = "</i>";
        }
430
        elsif (!is_open_state($bug_state)) {
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
            $pre = '<span class="bz_closed">';
            $title .= " $bug_res";
            $post = '</span>';
        }
        if (Bugzilla->user->can_see_bug($bug_num)) {
            $title .= " - $bug_desc";
        }
        # Prevent code injection in the title.
        $title = value_quote($title);

        my $linkval = "show_bug.cgi?id=$bug_num";
        if (defined $comment_num) {
            $linkval .= "#c$comment_num";
        }
        return qq{$pre<a href="$linkval" title="$title">$link_text</a>$post};
    }
    else {
        return qq{$link_text};
    }
}

452 453 454
###############################################################################
# Templatization Code

455 456 457 458 459 460
# The Template Toolkit throws an error if a loop iterates >1000 times.
# We want to raise that limit.
# NOTE: If you change this number, you MUST RE-RUN checksetup.pl!!!
# If you do not re-run checksetup.pl, the change you make will not apply
$Template::Directive::WHILE_MAX = 1000000;

461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482
# Use the Toolkit Template's Stash module to add utility pseudo-methods
# to template variables.
use Template::Stash;

# Add "contains***" methods to list variables that search for one or more 
# items in a list and return boolean values representing whether or not 
# one/all/any item(s) were found.
$Template::Stash::LIST_OPS->{ contains } =
  sub {
      my ($list, $item) = @_;
      return grep($_ eq $item, @$list);
  };

$Template::Stash::LIST_OPS->{ containsany } =
  sub {
      my ($list, $items) = @_;
      foreach my $item (@$items) { 
          return 1 if grep($_ eq $item, @$list);
      }
      return 0;
  };

483 484 485 486 487 488 489
# Allow us to still get the scalar if we use the list operation ".0" on it,
# as we often do for defaults in query.cgi and other places.
$Template::Stash::SCALAR_OPS->{ 0 } = 
  sub {
      return $_[0];
  };

490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523
# Add a "substr" method to the Template Toolkit's "scalar" object
# that returns a substring of a string.
$Template::Stash::SCALAR_OPS->{ substr } = 
  sub {
      my ($scalar, $offset, $length) = @_;
      return substr($scalar, $offset, $length);
  };

# Add a "truncate" method to the Template Toolkit's "scalar" object
# that truncates a string to a certain length.
$Template::Stash::SCALAR_OPS->{ truncate } = 
  sub {
      my ($string, $length, $ellipsis) = @_;
      $ellipsis ||= "";
      
      return $string if !$length || length($string) <= $length;
      
      my $strlen = $length - length($ellipsis);
      my $newstr = substr($string, 0, $strlen) . $ellipsis;
      return $newstr;
  };

# Create the template object that processes templates and specify
# configuration parameters that apply to all templates.

###############################################################################

# Construct the Template object

# Note that all of the failure cases here can't use templateable errors,
# since we won't have a template to use...

sub create {
    my $class = shift;
524 525 526 527 528 529
    my %opts = @_;

    # checksetup.pl will call us once for any template/lang directory.
    # We need a possibility to reset the cache, so that no files from
    # the previous language pollute the action.
    if ($opts{'clean_cache'}) {
530
        delete Bugzilla->request_cache->{template_include_path};
531
    }
532 533 534 535 536 537

    # IMPORTANT - If you make any configuration changes here, make sure to
    # make them in t/004.template.t and checksetup.pl.

    return $class->new({
        # Colon-separated list of directories containing templates.
538
        INCLUDE_PATH => [\&getTemplateIncludePath],
539 540 541 542 543 544 545 546

        # Remove white-space before template directives (PRE_CHOMP) and at the
        # beginning and end of templates and template blocks (TRIM) for better
        # looking, more compact content.  Use the plus sign at the beginning
        # of directives to maintain white space (i.e. [%+ DIRECTIVE %]).
        PRE_CHOMP => 1,
        TRIM => 1,

547
        COMPILE_DIR => bz_locations()->{'datadir'} . "/template",
548

549 550 551
        # Initialize templates (f.e. by loading plugins like Hook).
        PRE_PROCESS => "global/initialize.none.tmpl",

552 553
        # Functions for processing text within templates in various ways.
        # IMPORTANT!  When adding a filter here that does not override a
554
        # built-in filter, please also add a stub filter to t/004template.t.
555
        FILTERS => {
556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584

            # Render text in required style.

            inactive => [
                sub {
                    my($context, $isinactive) = @_;
                    return sub {
                        return $isinactive ? '<span class="bz_inactive">'.$_[0].'</span>' : $_[0];
                    }
                }, 1
            ],

            closed => [
                sub {
                    my($context, $isclosed) = @_;
                    return sub {
                        return $isclosed ? '<span class="bz_closed">'.$_[0].'</span>' : $_[0];
                    }
                }, 1
            ],

            obsolete => [
                sub {
                    my($context, $isobsolete) = @_;
                    return sub {
                        return $isobsolete ? '<span class="bz_obsolete">'.$_[0].'</span>' : $_[0];
                    }
                }, 1
            ],
585 586 587 588 589

            # Returns the text with backslashes, single/double quotes,
            # and newlines/carriage returns escaped for use in JS strings.
            js => sub {
                my ($var) = @_;
590
                $var =~ s/([\\\'\"\/])/\\$1/g;
591 592
                $var =~ s/\n/\\n/g;
                $var =~ s/\r/\\r/g;
593
                $var =~ s/\@/\\x40/g; # anti-spam for email addresses
594 595
                return $var;
            },
596 597 598 599 600 601 602
            
            # Converts data to base64
            base64 => sub {
                my ($data) = @_;
                return encode_base64($data);
            },
            
603 604 605 606 607 608 609 610 611 612 613 614 615
            # HTML collapses newlines in element attributes to a single space,
            # so form elements which may have whitespace (ie comments) need
            # to be encoded using &#013;
            # See bugs 4928, 22983 and 32000 for more details
            html_linebreak => sub {
                my ($var) = @_;
                $var =~ s/\r\n/\&#013;/g;
                $var =~ s/\n\r/\&#013;/g;
                $var =~ s/\r/\&#013;/g;
                $var =~ s/\n/\&#013;/g;
                return $var;
            },

616 617 618 619 620 621 622 623
            # Prevents line break on hyphens and whitespaces.
            no_break => sub {
                my ($var) = @_;
                $var =~ s/ /\&nbsp;/g;
                $var =~ s/-/\&#8209;/g;
                return $var;
            },

624 625 626 627 628 629 630 631
            xml => \&Bugzilla::Util::xml_quote ,

            # This filter escapes characters in a variable or value string for
            # use in a query string.  It escapes all characters NOT in the
            # regex set: [a-zA-Z0-9_\-.].  The 'uri' filter should be used for
            # a full URL that may have characters that need encoding.
            url_quote => \&Bugzilla::Util::url_quote ,

632 633 634 635
            # This filter is similar to url_quote but used a \ instead of a %
            # as prefix. In addition it replaces a ' ' by a '_'.
            css_class_quote => \&Bugzilla::Util::css_class_quote ,

636 637 638 639
            quoteUrls => [ sub {
                               my ($context, $bug) = @_;
                               return sub {
                                   my $text = shift;
640
                                   return quoteUrls($text, $bug);
641 642 643 644
                               };
                           },
                           1
                         ],
645 646 647 648 649

            bug_link => [ sub {
                              my ($context, $bug) = @_;
                              return sub {
                                  my $text = shift;
650
                                  return get_bug_link($bug, $text);
651 652 653 654 655
                              };
                          },
                          1
                        ],

656 657 658 659 660 661
            bug_list_link => sub
            {
                my $buglist = shift;
                return join(", ", map(get_bug_link($_, $_), split(/ *, */, $buglist)));
            },

662 663 664 665 666 667 668 669 670 671 672 673
            # In CSV, quotes are doubled, and any value containing a quote or a
            # comma is enclosed in quotes.
            csv => sub
            {
                my ($var) = @_;
                $var =~ s/\"/\"\"/g;
                if ($var !~ /^-?(\d+\.)?\d*$/) {
                    $var = "\"$var\"";
                }
                return $var;
            } ,

674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697
            # Format a filesize in bytes to a human readable value
            unitconvert => sub
            {
                my ($data) = @_;
                my $retval = "";
                my %units = (
                    'KB' => 1024,
                    'MB' => 1024 * 1024,
                    'GB' => 1024 * 1024 * 1024,
                );

                if ($data < 1024) {
                    return "$data bytes";
                } 
                else {
                    my $u;
                    foreach $u ('GB', 'MB', 'KB') {
                        if ($data >= $units{$u}) {
                            return sprintf("%.2f %s", $data/$units{$u}, $u);
                        }
                    }
                }
            },

698 699
            # Format a time for display (more info in Bugzilla::Util)
            time => \&Bugzilla::Util::format_time,
700

701 702 703
            # Bug 120030: Override html filter to obscure the '@' in user
            #             visible strings.
            # Bug 319331: Handle BiDi disruptions.
704 705
            html => sub {
                my ($var) = Template::Filters::html_filter(@_);
706
                # Obscure '@'.
707
                $var =~ s/\@/\&#64;/g;
708
                if (Bugzilla->params->{'utf8'}) {
709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735
                    # Remove the following characters because they're
                    # influencing BiDi:
                    # --------------------------------------------------------
                    # |Code  |Name                      |UTF-8 representation|
                    # |------|--------------------------|--------------------|
                    # |U+202a|Left-To-Right Embedding   |0xe2 0x80 0xaa      |
                    # |U+202b|Right-To-Left Embedding   |0xe2 0x80 0xab      |
                    # |U+202c|Pop Directional Formatting|0xe2 0x80 0xac      |
                    # |U+202d|Left-To-Right Override    |0xe2 0x80 0xad      |
                    # |U+202e|Right-To-Left Override    |0xe2 0x80 0xae      |
                    # --------------------------------------------------------
                    #
                    # The following are characters influencing BiDi, too, but
                    # they can be spared from filtering because they don't
                    # influence more than one character right or left:
                    # --------------------------------------------------------
                    # |Code  |Name                      |UTF-8 representation|
                    # |------|--------------------------|--------------------|
                    # |U+200e|Left-To-Right Mark        |0xe2 0x80 0x8e      |
                    # |U+200f|Right-To-Left Mark        |0xe2 0x80 0x8f      |
                    # --------------------------------------------------------
                    #
                    # Do the replacing in a loop so that we don't get tricked
                    # by stuff like 0xe2 0xe2 0x80 0xae 0x80 0xae.
                    while ($var =~ s/\xe2\x80(\xaa|\xab|\xac|\xad|\xae)//g) {
                    }
                }
736 737
                return $var;
            },
738 739 740

            html_light => \&Bugzilla::Util::html_light_quote,

741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765
            # iCalendar contentline filter
            ics => [ sub {
                         my ($context, @args) = @_;
                         return sub {
                             my ($var) = shift;
                             my ($par) = shift @args;
                             my ($output) = "";

                             $var =~ s/[\r\n]/ /g;
                             $var =~ s/([;\\\"])/\\$1/g;

                             if ($par) {
                                 $output = sprintf("%s:%s", $par, $var);
                             } else {
                                 $output = $var;
                             }
                             
                             $output =~ s/(.{75,75})/$1\n /g;

                             return $output;
                         };
                     },
                     1
                     ],

766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781
            # Note that using this filter is even more dangerous than
            # using "none," and you should only use it when you're SURE
            # the output won't be displayed directly to a web browser.
            txt => sub {
                my ($var) = @_;
                # Trivial HTML tag remover
                $var =~ s/<[^>]*>//g;
                # And this basically reverses the html filter.
                $var =~ s/\&#64;/@/g;
                $var =~ s/\&lt;/</g;
                $var =~ s/\&gt;/>/g;
                $var =~ s/\&quot;/\"/g;
                $var =~ s/\&amp;/\&/g;
                return $var;
            },

782 783 784
            # Wrap a displayed comment to the appropriate length
            wrap_comment => \&Bugzilla::Util::wrap_comment,

785 786 787 788
            # We force filtering of every variable in key security-critical
            # places; we have a none filter for people to use when they 
            # really, really don't want a variable to be changed.
            none => sub { return $_[0]; } ,
789 790 791 792
        },

        PLUGIN_BASE => 'Bugzilla::Template::Plugin',

793
        CONSTANTS => _load_constants(),
794

795 796 797
        # Default variables for all templates
        VARIABLES => {
            # Function for retrieving global parameters.
798
            'Param' => sub { return Bugzilla->params->{$_[0]}; },
799 800 801 802 803 804 805

            # Function to create date strings
            'time2str' => \&Date::Format::time2str,

            # Generic linear search function
            'lsearch' => \&Bugzilla::Util::lsearch,

806
            # Currently logged in user, if any
807
            # If an sudo session is in progress, this is the user we're faking
808 809
            'user' => sub { return Bugzilla->user; },

810 811 812 813
            # If an sudo session is in progress, this is the user who
            # started the session.
            'sudoer' => sub { return Bugzilla->sudoer; },

814 815 816 817 818 819
            # SendBugMail - sends mail about a bug, using Bugzilla::BugMail.pm
            'SendBugMail' => sub {
                my ($id, $mailrecipients) = (@_);
                require Bugzilla::BugMail;
                Bugzilla::BugMail::Send($id, $mailrecipients);
            },
820 821 822 823 824 825

        },

   }) || die("Template creation failed: " . $class->error());
}

826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873
# Used as part of the two subroutines below.
our (%_templates_to_precompile, $_current_path);

sub precompile_templates {
    my ($output) = @_;

    # Remove the compiled templates.
    my $datadir = bz_locations()->{'datadir'};
    if (-e "$datadir/template") {
        print "Removing existing compiled templates ...\n" if $output;

        # XXX This frequently fails if the webserver made the files, because
        # then the webserver owns the directories. We could fix that by
        # doing a chmod/chown on all the directories here.
        rmtree("$datadir/template");

        # Check that the directory was really removed
        if(-e "$datadir/template") {
            print "\n\n";
            print "The directory '$datadir/template' could not be removed.\n";
            print "Please remove it manually and rerun checksetup.pl.\n\n";
            exit;
        }
    }

    print "Precompiling templates...\n" if $output;

    my $templatedir = bz_locations()->{'templatedir'};
    # Don't hang on templates which use the CGI library
    eval("use CGI qw(-no_debug)");
    
    my $dir_reader    = new IO::Dir($templatedir) || die "$templatedir: $!";
    my @language_dirs = grep { /^[a-z-]+$/i } $dir_reader->read;
    $dir_reader->close;

    foreach my $dir (@language_dirs) {
        next if ($dir eq 'CVS');
        -d "$templatedir/$dir/default" || -d "$templatedir/$dir/custom" 
            || next;
        local $ENV{'HTTP_ACCEPT_LANGUAGE'} = $dir;
        # We locally hack this parameter so that Bugzilla::Template
        # accepts this language no matter what.
        local Bugzilla->params->{'languages'} = "$dir,en";
        my $template = Bugzilla::Template->create(clean_cache => 1);

        # Precompile all the templates found in all the directories.
        %_templates_to_precompile = ();
        foreach my $subdir (qw(custom extension default), bz_locations()->{'project'}) {
874
            next unless $subdir; # If 'project' is empty.
875 876 877 878 879 880 881 882 883 884 885 886 887
            $_current_path = File::Spec->catdir($templatedir, $dir, $subdir);
            next unless -d $_current_path;
            # Traverse the template hierarchy.
            find({ wanted => \&_precompile_push, no_chdir => 1 }, $_current_path);
        }
        # The sort isn't totally necessary, but it makes debugging easier
        # by making the templates always be compiled in the same order.
        foreach my $file (sort keys %_templates_to_precompile) {
            # Compile the template but throw away the result. This has the side-
            # effect of writing the compiled version to disk.
            $template->context->template($file);
        }
    }
888 889 890

    # If anything created a Template object before now, clear it out.
    delete Bugzilla->request_cache->{template};
891 892 893 894 895 896 897 898 899
}

# Helper for precompile_templates
sub _precompile_push {
    my $name = $File::Find::name;
    return if (-d $name);
    return if ($name =~ /\/CVS\//);
    return if ($name !~ /\.tmpl$/);
   
900
    $name =~ s/\Q$_current_path\E\///;
901 902 903
    $_templates_to_precompile{$name} = 1;
}

904 905 906 907 908 909
1;

__END__

=head1 NAME

910
Bugzilla::Template - Wrapper around the Template Toolkit C<Template> object
911

912
=head1 SYNOPSIS
913 914 915

  my $template = Bugzilla::Template->create;

916 917 918 919 920 921 922
  $template->put_header($title, $h1, $h2);
  $template->put_footer();

  my $format = $template->get_format("foo/bar",
                                     scalar($cgi->param('format')),
                                     scalar($cgi->param('ctype')));

923 924 925 926 927 928 929 930
=head1 DESCRIPTION

This is basically a wrapper so that the correct arguments get passed into
the C<Template> constructor.

It should not be used directly by scripts or modules - instead, use
C<Bugzilla-E<gt>instance-E<gt>template> to get an already created module.

931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946
=head1 SUBROUTINES

=over

=item C<precompile_templates($output)>

Description: Compiles all of Bugzilla's templates in every language.
             Used mostly by F<checksetup.pl>.

Params:      C<$output> - C<true> if you want the function to print
               out information about what it's doing.

Returns:     nothing

=back

947 948 949 950 951 952
=head1 METHODS

=over

=item C<put_header($title, $h1, $h2)>

953
 Description: Display the header of the page for non yet templatized .cgi files.
954 955 956 957 958 959 960 961 962

 Params:      $title - Page title.
              $h1    - Main page header.
              $h2    - Page subheader.

 Returns:     nothing

=item C<put_footer()>

963
 Description: Display the footer of the page for non yet templatized .cgi files.
964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981

 Params:      none

 Returns:     nothing

=item C<get_format($file, $format, $ctype)>

 Description: Construct a format object from URL parameters.

 Params:      $file   - Name of the template to display.
              $format - When the template exists under several formats
                        (e.g. table or graph), specify the one to choose.
              $ctype  - Content type, see Bugzilla::Constants::contenttypes.

 Returns:     A format object.

=back

982 983 984
=head1 SEE ALSO

L<Bugzilla>, L<Template>