Template.pm 34.6 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::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
use Date::Format ();
50
use File::Basename qw(dirname);
51
use File::Find;
52
use File::Path qw(rmtree mkpath);
53 54
use File::Spec;
use IO::Dir;
55 56 57

use base qw(Template);

58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
# As per the Template::Base documentation, the _init() method is being called 
# by the new() constructor. We take advantage of this in order to plug our
# UTF-8-aware Parser object in neatly after the original _init() method has
# happened, in particular, after having set up the constants namespace.
# See bug 413121 for details.
sub _init {
    my $self = shift;
    my $config = $_[0];

    $self->SUPER::_init(@_) || return undef;

    $self->{PARSER} = $config->{PARSER}
        = new Bugzilla::Template::Parser($config);

    # Now we need to re-create the default Service object, making it aware
    # of our Parser object.
    $self->{SERVICE} = $config->{SERVICE}
        = Template::Config->service($config);

    return $self;
}

80
# Convert the constants in the Bugzilla::Constants module into a hash we can
81 82
# pass to the template object for reflection into its "constants" namespace
# (which is like its "variables" namespace, but for constants).  To do so, we
83 84
# traverse the arrays of exported and exportable symbols and ignoring the rest
# (which, if Constants.pm exports only constants, as it should, will be nothing else).
85 86 87 88 89
sub _load_constants {
    my %constants;
    foreach my $constant (@Bugzilla::Constants::EXPORT,
                          @Bugzilla::Constants::EXPORT_OK)
    {
90 91 92 93 94 95
        if (ref Bugzilla::Constants->$constant) {
            $constants{$constant} = Bugzilla::Constants->$constant;
        }
        else {
            my @list = (Bugzilla::Constants->$constant);
            $constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list;
96 97
        }
    }
98
    return \%constants;
99 100
}

101 102 103
# 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
104
# Templates may also be found in the extensions/ tree
105
sub getTemplateIncludePath {
106
    my $cache = Bugzilla->request_cache;
107
    my $lang  = $cache->{'language'} || '';
108
    $cache->{"template_include_path_$lang"} ||= template_include_path({
109
        use_languages => Bugzilla->languages,
110 111
        only_language => $lang });
    return $cache->{"template_include_path_$lang"};
112 113
}

114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
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);
    };
136
    # This parsing may seem fragile, but it's OK:
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
    # 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}
    };
}
153

154 155 156 157 158 159 160 161
# 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 {
162
    my ($text, $curr_bugid, $already_wrapped) = (@_);
163 164 165 166 167 168 169
    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
170
    # \0 is used because it's unlikely to occur in the text, so the cost of
171 172 173 174 175 176
    # doing this should be very small

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

177 178
    # If the comment is already wrapped, we should ignore newlines when
    # looking for matching regexps. Else we should take them into account.
179
    my $s = $already_wrapped ? qr/\s/ : qr/[[:blank:]]/;
180

181 182 183 184
    # 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
185
    # if it was substituted as a bug title (since that always involve leading
186 187
    # and trailing text)

188
    # Because of entities, it's easier (and quicker) to do this before escaping
189 190 191 192 193

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

194
    # Provide tooltips for full bug links (Bug 74355)
195 196 197
    my $urlbase_re = '(' . join('|',
        map { qr/$_/ } grep($_, Bugzilla->params->{'urlbase'}, 
                            Bugzilla->params->{'sslbase'})) . ')';
198
    $text =~ s~\b(${urlbase_re}\Qshow_bug.cgi?id=\E([0-9]+)(\#c([0-9]+))?)\b
199
              ~($things[$count++] = get_bug_link($3, $1, { comment_num => $5 })) &&
200 201 202
               ("\0\0" . ($count-1) . "\0\0")
              ~egox;

203
    # non-mailto protocols
204 205
    my $safe_protocols = join('|', SAFE_PROTOCOLS);
    my $protocol_re = qr/($safe_protocols)/i;
206 207 208 209 210 211 212 213 214

    $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;

215
    # We have to quote now, otherwise the html itself is escaped
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
    # 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;

235
    $text =~ s~\b(attachment$s*\#?$s*(\d+))
236 237 238 239 240 241 242 243 244 245 246
              ~($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
247
    my $bug_word = get_text('term', { term => 'bug' });
248 249 250
    my $bug_re = qr/\Q$bug_word\E$s*\#?$s*(\d+)/i;
    my $comment_re = qr/comment$s*\#?$s*(\d+)/i;
    $text =~ s~\b($bug_re(?:$s*,?$s*$comment_re)?|$comment_re)
251 252
              ~ # We have several choices. $1 here is the link, and $2-4 are set
                # depending on which part matched
253
               (defined($2) ? get_bug_link($2, $1, { comment_num => $3 }) :
254 255 256
                              "<a href=\"$current_bugurl#c$4\">$1</a>")
              ~egox;

257 258
    # Old duplicate markers. These don't use $bug_word because they are old
    # and were never customizable.
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
    $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.
295
        $title = html_quote(clean_text($title));
296

297
        $link_text =~ s/ \[details\]$//;
298
        my $linkval = "attachment.cgi?id=$attachid";
299 300
        # Whitespace matters here because these links are in <pre> tags.
        return qq|<span class="$className">|
301
               . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>|
302
               . qq| <a href="${linkval}&amp;action=edit" title="$title">[details]</a>|
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
               . 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 {
318
    my ($bug_num, $link_text, $options) = @_;
319 320 321 322 323 324 325 326
    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;";

327 328 329 330
    my ($bug_alias, $bug_state, $bug_res, $bug_desc) =
        $dbh->selectrow_array('SELECT bugs.alias, bugs.bug_status, bugs.resolution, bugs.short_desc
                               FROM bugs WHERE bugs.bug_id = ?',
                               undef, $bug_num);
331

332 333
    if ($options->{use_alias} && $link_text =~ /^\d+$/ && $bug_alias) {
        $link_text = $bug_alias;
334
    }
335 336 337 338 339 340

    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) = ("", "", "");

341
        $title = get_text('get_status', {status => $bug_state});
342 343 344 345
        if ($bug_state eq 'UNCONFIRMED') {
            $pre = "<i>";
            $post = "</i>";
        }
346
        elsif (!is_open_state($bug_state)) {
347
            $pre = '<span class="bz_closed">';
348
            $title .= ' ' . get_text('get_resolution', {resolution => $bug_res});
349 350 351 352 353 354
            $post = '</span>';
        }
        if (Bugzilla->user->can_see_bug($bug_num)) {
            $title .= " - $bug_desc";
        }
        # Prevent code injection in the title.
355
        $title = html_quote(clean_text($title));
356 357

        my $linkval = "show_bug.cgi?id=$bug_num";
358 359
        if ($options->{comment_num}) {
            $linkval .= "#c" . $options->{comment_num};
360 361 362 363 364 365 366 367
        }
        return qq{$pre<a href="$linkval" title="$title">$link_text</a>$post};
    }
    else {
        return qq{$link_text};
    }
}

368 369 370
###############################################################################
# Templatization Code

371 372 373 374 375 376
# 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;

377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
# 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;
  };

399 400 401 402 403 404 405
# Clone the array reference to leave the original one unaltered.
$Template::Stash::LIST_OPS->{ clone } =
  sub {
      my $list = shift;
      return [@$list];
  };

406 407 408 409 410 411 412
# 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];
  };

413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
# 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;
447 448 449 450 451 452
    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'}) {
453
        delete Bugzilla->request_cache->{template_include_path_};
454
    }
455 456 457 458 459 460

    # 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.
461
        INCLUDE_PATH => [\&getTemplateIncludePath],
462 463 464 465 466 467 468 469

        # 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,

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

472 473 474
        # Initialize templates (f.e. by loading plugins like Hook).
        PRE_PROCESS => "global/initialize.none.tmpl",

475 476
        # Functions for processing text within templates in various ways.
        # IMPORTANT!  When adding a filter here that does not override a
477
        # built-in filter, please also add a stub filter to t/004template.t.
478
        FILTERS => {
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507

            # 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
            ],
508 509 510 511 512

            # Returns the text with backslashes, single/double quotes,
            # and newlines/carriage returns escaped for use in JS strings.
            js => sub {
                my ($var) = @_;
513
                $var =~ s/([\\\'\"\/])/\\$1/g;
514 515
                $var =~ s/\n/\\n/g;
                $var =~ s/\r/\\r/g;
516
                $var =~ s/\@/\\x40/g; # anti-spam for email addresses
517 518
                return $var;
            },
519 520 521 522 523 524 525
            
            # Converts data to base64
            base64 => sub {
                my ($data) = @_;
                return encode_base64($data);
            },
            
526 527 528 529 530 531 532 533 534 535 536 537 538
            # 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;
            },

539 540 541 542 543 544 545 546
            # Prevents line break on hyphens and whitespaces.
            no_break => sub {
                my ($var) = @_;
                $var =~ s/ /\&nbsp;/g;
                $var =~ s/-/\&#8209;/g;
                return $var;
            },

547 548 549 550 551 552 553 554
            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 ,

555 556 557 558
            # 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 ,

559
            quoteUrls => [ sub {
560
                               my ($context, $bug, $already_wrapped) = @_;
561 562
                               return sub {
                                   my $text = shift;
563
                                   return quoteUrls($text, $bug, $already_wrapped);
564 565 566 567
                               };
                           },
                           1
                         ],
568 569

            bug_link => [ sub {
570
                              my ($context, $bug, $options) = @_;
571 572
                              return sub {
                                  my $text = shift;
573
                                  return get_bug_link($bug, $text, $options);
574 575 576 577 578
                              };
                          },
                          1
                        ],

579 580 581 582 583 584
            bug_list_link => sub
            {
                my $buglist = shift;
                return join(", ", map(get_bug_link($_, $_), split(/ *, */, $buglist)));
            },

585 586 587 588 589 590 591 592 593 594 595 596
            # 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;
            } ,

597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620
            # 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);
                        }
                    }
                }
            },

621
            # Format a time for display (more info in Bugzilla::Util)
622
            time => [ sub {
623
                          my ($context, $format, $timezone) = @_;
624 625
                          return sub {
                              my $time = shift;
626
                              return format_time($time, $format, $timezone);
627 628 629 630
                          };
                      },
                      1
                    ],
631

632 633 634
            # Bug 120030: Override html filter to obscure the '@' in user
            #             visible strings.
            # Bug 319331: Handle BiDi disruptions.
635 636
            html => sub {
                my ($var) = Template::Filters::html_filter(@_);
637
                # Obscure '@'.
638
                $var =~ s/\@/\&#64;/g;
639
                if (Bugzilla->params->{'utf8'}) {
640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660
                    # 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      |
                    # --------------------------------------------------------
661
                    $var =~ s/[\x{202a}-\x{202e}]//g;
662
                }
663 664
                return $var;
            },
665 666 667

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

668 669
            email => \&Bugzilla::Util::email_filter,

670 671 672 673 674 675 676 677 678
            # iCalendar contentline filter
            ics => [ sub {
                         my ($context, @args) = @_;
                         return sub {
                             my ($var) = shift;
                             my ($par) = shift @args;
                             my ($output) = "";

                             $var =~ s/[\r\n]/ /g;
679
                             $var =~ s/([;\\\",])/\\$1/g;
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694

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

                             return $output;
                         };
                     },
                     1
                     ],

695 696 697 698 699 700 701 702 703 704 705 706 707
            # 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;
708 709 710 711
                # Now remove extra whitespace, and wrap it to 72 characters.
                my $collapse_filter = $Template::Filters::FILTERS->{collapse};
                $var = $collapse_filter->($var);
                $var = wrap_comment($var, 72);
712 713 714
                return $var;
            },

715
            # Wrap a displayed comment to the appropriate length
716 717 718 719 720
            wrap_comment => [
                sub {
                    my ($context, $cols) = @_;
                    return sub { wrap_comment($_[0], $cols) }
                }, 1],
721

722 723 724 725
            # 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]; } ,
726 727 728 729
        },

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

730
        CONSTANTS => _load_constants(),
731

732 733 734
        # Default variables for all templates
        VARIABLES => {
            # Function for retrieving global parameters.
735
            'Param' => sub { return Bugzilla->params->{$_[0]}; },
736 737 738 739 740 741 742

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

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

743
            # Currently logged in user, if any
744
            # If an sudo session is in progress, this is the user we're faking
745 746
            'user' => sub { return Bugzilla->user; },

747 748 749 750
            # If an sudo session is in progress, this is the user who
            # started the session.
            'sudoer' => sub { return Bugzilla->sudoer; },

751 752 753 754 755 756
            # SendBugMail - sends mail about a bug, using Bugzilla::BugMail.pm
            'SendBugMail' => sub {
                my ($id, $mailrecipients) = (@_);
                require Bugzilla::BugMail;
                Bugzilla::BugMail::Send($id, $mailrecipients);
            },
757

758 759 760
            # Allow templates to access the "corect" URLBase value
            'urlbase' => sub { return Bugzilla::Util::correct_urlbase(); },

761 762 763 764 765 766 767 768
            # Allow templates to access docs url with users' preferred language
            'docs_urlbase' => sub { 
                my ($language) = include_languages();
                my $docs_urlbase = Bugzilla->params->{'docs_urlbase'};
                $docs_urlbase =~ s/\%lang\%/$language/;
                return $docs_urlbase;
            },

769 770 771
            # Allow templates to generate a token themselves.
            'issue_hash_token' => \&Bugzilla::Token::issue_hash_token,

772 773 774 775 776 777 778 779 780 781
            # These don't work as normal constants.
            DB_MODULE        => \&Bugzilla::Constants::DB_MODULE,
            REQUIRED_MODULES => 
                \&Bugzilla::Install::Requirements::REQUIRED_MODULES,
            OPTIONAL_MODULES => sub {
                my @optional = @{OPTIONAL_MODULES()};
                @optional    = sort {$a->{feature} cmp $b->{feature}} 
                                    @optional;
                return \@optional;
            },
782 783 784 785 786
        },

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

787 788 789 790 791 792 793 794 795
# 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") {
796
        print install_string('template_removing_dir') . "\n" if $output;
797 798 799 800 801 802 803 804 805 806 807 808 809 810 811

        # 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;
        }
    }

812
    print install_string('template_precompile') if $output;
813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831

    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;
        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'}) {
832
            next unless $subdir; # If 'project' is empty.
833 834 835 836 837 838 839 840 841 842 843 844 845
            $_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);
        }
    }
846

847 848 849 850 851
    # Under mod_perl, we look for templates using the absolute path of the
    # template directory, which causes Template Toolkit to look for their 
    # *compiled* versions using the full absolute path under the data/template
    # directory. (Like data/template/var/www/html/mod_perl/.) To avoid
    # re-compiling templates under mod_perl, we symlink to the
852 853 854 855 856 857 858 859 860 861 862 863 864 865
    # already-compiled templates. This doesn't work on Windows.
    if (!ON_WINDOWS) {
        my $abs_root = dirname(abs_path($templatedir));
        my $todir    = "$datadir/template$abs_root";
        mkpath($todir);
        # We use abs2rel so that the symlink will look like 
        # "../../../../template" which works, while just 
        # "data/template/template/" doesn't work.
        my $fromdir = File::Spec->abs2rel("$datadir/template/template", $todir);
        # We eval for systems that can't symlink at all, where "symlink" 
        # throws a fatal error.
        eval { symlink($fromdir, "$todir/template") 
                   or warn "Failed to symlink from $fromdir to $todir: $!" };
    }
866

867 868
    # If anything created a Template object before now, clear it out.
    delete Bugzilla->request_cache->{template};
869 870 871
    # This is the single variable used to precompile templates,
    # which needs to be cleared as well.
    delete Bugzilla->request_cache->{template_include_path_};
872 873

    print install_string('done') . "\n" if $output;
874 875 876 877 878 879 880 881 882
}

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

887 888 889 890 891 892
1;

__END__

=head1 NAME

893
Bugzilla::Template - Wrapper around the Template Toolkit C<Template> object
894

895
=head1 SYNOPSIS
896 897

  my $template = Bugzilla::Template->create;
898 899 900 901
  my $format = $template->get_format("foo/bar",
                                     scalar($cgi->param('format')),
                                     scalar($cgi->param('ctype')));

902 903 904 905 906 907 908 909
=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.

910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925
=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

926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942
=head1 METHODS

=over

=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

943 944 945
=head1 SEE ALSO

L<Bugzilla>, L<Template>