Template.pm 33.3 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::Template::Parser;
45

46
use Cwd qw(abs_path);
47
use MIME::Base64;
48 49
# for time2str - replace by TT Date plugin??
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 162 163 164 165 166 167 168 169
# 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
170
    # \0 is used because it's unlikely to occur in the text, so the cost of
171 172 173 174 175 176 177 178 179 180
    # doing this should be very small

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

184
    # Because of entities, it's easier (and quicker) to do this before escaping
185 186 187 188 189

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

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

199
    # non-mailto protocols
200 201
    my $safe_protocols = join('|', SAFE_PROTOCOLS);
    my $protocol_re = qr/($safe_protocols)/i;
202 203 204 205 206 207 208 209 210

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

211
    # We have to quote now, otherwise the html itself is escaped
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
    # 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
243 244
    my $bug_word = get_text('term', { term => 'bug' });
    my $bug_re = qr/\Q$bug_word\E\s*\#?\s*(\d+)/i;
245 246 247 248 249 250 251 252
    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;

253 254
    # Old duplicate markers. These don't use $bug_word because they are old
    # and were never customizable.
255 256 257 258 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
    $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.
291
        $title = html_quote(clean_text($title));
292

293
        $link_text =~ s/ \[details\]$//;
294
        my $linkval = "attachment.cgi?id=$attachid";
295 296
        # Whitespace matters here because these links are in <pre> tags.
        return qq|<span class="$className">|
297
               . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>|
298
               . qq| <a href="${linkval}&amp;action=edit" title="$title">[details]</a>|
299 300 301 302 303 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
               . 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) = ("", "", "");

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

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

360 361 362
###############################################################################
# Templatization Code

363 364 365 366 367 368
# 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;

369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390
# 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;
  };

391 392 393 394 395 396 397
# 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];
  };

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 430 431
# 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;
432 433 434 435 436 437
    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'}) {
438
        delete Bugzilla->request_cache->{template_include_path_};
439
    }
440 441 442 443 444 445

    # 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.
446
        INCLUDE_PATH => [\&getTemplateIncludePath],
447 448 449 450 451 452 453 454

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

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

457 458 459
        # Initialize templates (f.e. by loading plugins like Hook).
        PRE_PROCESS => "global/initialize.none.tmpl",

460 461
        # Functions for processing text within templates in various ways.
        # IMPORTANT!  When adding a filter here that does not override a
462
        # built-in filter, please also add a stub filter to t/004template.t.
463
        FILTERS => {
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492

            # 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
            ],
493 494 495 496 497

            # Returns the text with backslashes, single/double quotes,
            # and newlines/carriage returns escaped for use in JS strings.
            js => sub {
                my ($var) = @_;
498
                $var =~ s/([\\\'\"\/])/\\$1/g;
499 500
                $var =~ s/\n/\\n/g;
                $var =~ s/\r/\\r/g;
501
                $var =~ s/\@/\\x40/g; # anti-spam for email addresses
502 503
                return $var;
            },
504 505 506 507 508 509 510
            
            # Converts data to base64
            base64 => sub {
                my ($data) = @_;
                return encode_base64($data);
            },
            
511 512 513 514 515 516 517 518 519 520 521 522 523
            # 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;
            },

524 525 526 527 528 529 530 531
            # Prevents line break on hyphens and whitespaces.
            no_break => sub {
                my ($var) = @_;
                $var =~ s/ /\&nbsp;/g;
                $var =~ s/-/\&#8209;/g;
                return $var;
            },

532 533 534 535 536 537 538 539
            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 ,

540 541 542 543
            # 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 ,

544 545 546 547
            quoteUrls => [ sub {
                               my ($context, $bug) = @_;
                               return sub {
                                   my $text = shift;
548
                                   return quoteUrls($text, $bug);
549 550 551 552
                               };
                           },
                           1
                         ],
553 554 555 556 557

            bug_link => [ sub {
                              my ($context, $bug) = @_;
                              return sub {
                                  my $text = shift;
558
                                  return get_bug_link($bug, $text);
559 560 561 562 563
                              };
                          },
                          1
                        ],

564 565 566 567 568 569
            bug_list_link => sub
            {
                my $buglist = shift;
                return join(", ", map(get_bug_link($_, $_), split(/ *, */, $buglist)));
            },

570 571 572 573 574 575 576 577 578 579 580 581
            # 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;
            } ,

582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605
            # 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);
                        }
                    }
                }
            },

606 607
            # Format a time for display (more info in Bugzilla::Util)
            time => \&Bugzilla::Util::format_time,
608

609 610 611
            # Bug 120030: Override html filter to obscure the '@' in user
            #             visible strings.
            # Bug 319331: Handle BiDi disruptions.
612 613
            html => sub {
                my ($var) = Template::Filters::html_filter(@_);
614
                # Obscure '@'.
615
                $var =~ s/\@/\&#64;/g;
616
                if (Bugzilla->params->{'utf8'}) {
617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637
                    # 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      |
                    # --------------------------------------------------------
638
                    $var =~ s/[\x{202a}-\x{202e}]//g;
639
                }
640 641
                return $var;
            },
642 643 644

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

645 646 647 648 649 650 651 652 653
            # iCalendar contentline filter
            ics => [ sub {
                         my ($context, @args) = @_;
                         return sub {
                             my ($var) = shift;
                             my ($par) = shift @args;
                             my ($output) = "";

                             $var =~ s/[\r\n]/ /g;
654
                             $var =~ s/([;\\\",])/\\$1/g;
655 656 657 658 659 660 661 662 663 664 665 666 667 668 669

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

                             return $output;
                         };
                     },
                     1
                     ],

670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685
            # 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;
            },

686
            # Wrap a displayed comment to the appropriate length
687 688 689 690 691
            wrap_comment => [
                sub {
                    my ($context, $cols) = @_;
                    return sub { wrap_comment($_[0], $cols) }
                }, 1],
692

693 694 695 696
            # 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]; } ,
697 698 699 700
        },

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

701
        CONSTANTS => _load_constants(),
702

703 704 705
        # Default variables for all templates
        VARIABLES => {
            # Function for retrieving global parameters.
706
            'Param' => sub { return Bugzilla->params->{$_[0]}; },
707 708 709 710 711 712 713

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

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

714
            # Currently logged in user, if any
715
            # If an sudo session is in progress, this is the user we're faking
716 717
            'user' => sub { return Bugzilla->user; },

718 719 720 721
            # If an sudo session is in progress, this is the user who
            # started the session.
            'sudoer' => sub { return Bugzilla->sudoer; },

722 723 724 725 726 727
            # SendBugMail - sends mail about a bug, using Bugzilla::BugMail.pm
            'SendBugMail' => sub {
                my ($id, $mailrecipients) = (@_);
                require Bugzilla::BugMail;
                Bugzilla::BugMail::Send($id, $mailrecipients);
            },
728

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

732 733 734 735 736 737 738 739
            # 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;
            },

740 741 742 743 744 745 746 747 748 749
            # 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;
            },
750 751 752 753 754
        },

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

755 756 757 758 759 760 761 762 763
# 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") {
764
        print install_string('template_removing_dir') . "\n" if $output;
765 766 767 768 769 770 771 772 773 774 775 776 777 778 779

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

780
    print install_string('template_precompile') if $output;
781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799

    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'}) {
800
            next unless $subdir; # If 'project' is empty.
801 802 803 804 805 806 807 808 809 810 811 812 813
            $_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);
        }
    }
814

815 816 817 818 819
    # 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
820 821 822 823 824 825 826 827 828 829 830 831 832 833
    # 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: $!" };
    }
834

835 836
    # If anything created a Template object before now, clear it out.
    delete Bugzilla->request_cache->{template};
837 838 839
    # This is the single variable used to precompile templates,
    # which needs to be cleared as well.
    delete Bugzilla->request_cache->{template_include_path_};
840 841

    print install_string('done') . "\n" if $output;
842 843 844 845 846 847 848 849 850
}

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

855 856 857 858 859 860
1;

__END__

=head1 NAME

861
Bugzilla::Template - Wrapper around the Template Toolkit C<Template> object
862

863
=head1 SYNOPSIS
864 865

  my $template = Bugzilla::Template->create;
866 867 868 869
  my $format = $template->get_format("foo/bar",
                                     scalar($cgi->param('format')),
                                     scalar($cgi->param('ctype')));

870 871 872 873 874 875 876 877
=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.

878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893
=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

894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910
=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

911 912 913
=head1 SEE ALSO

L<Bugzilla>, L<Template>