Template.pm 34.8 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::Bug;
38
use Bugzilla::Constants;
39
use Bugzilla::Hook;
40
use Bugzilla::Install::Requirements;
41 42 43
use Bugzilla::Install::Util qw(install_string template_include_path 
                               include_languages);
use Bugzilla::Keyword;
44
use Bugzilla::Util;
45
use Bugzilla::User;
46
use Bugzilla::Error;
47
use Bugzilla::Status;
48
use Bugzilla::Token;
49

50
use Cwd qw(abs_path);
51
use MIME::Base64;
52
use Date::Format ();
53
use File::Basename qw(basename dirname);
54
use File::Find;
55
use File::Path qw(rmtree mkpath);
56 57
use File::Spec;
use IO::Dir;
58
use Scalar::Util qw(blessed);
59 60 61

use base qw(Template);

62
# Convert the constants in the Bugzilla::Constants module into a hash we can
63 64
# pass to the template object for reflection into its "constants" namespace
# (which is like its "variables" namespace, but for constants).  To do so, we
65 66
# 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).
67 68 69 70 71
sub _load_constants {
    my %constants;
    foreach my $constant (@Bugzilla::Constants::EXPORT,
                          @Bugzilla::Constants::EXPORT_OK)
    {
72 73 74 75 76 77
        if (ref Bugzilla::Constants->$constant) {
            $constants{$constant} = Bugzilla::Constants->$constant;
        }
        else {
            my @list = (Bugzilla::Constants->$constant);
            $constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list;
78 79
        }
    }
80
    return \%constants;
81 82
}

83 84 85
# 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
86
# Templates may also be found in the extensions/ tree
87 88
sub _include_path {
    my $lang = shift || '';
89
    my $cache = Bugzilla->request_cache;
90 91
    $cache->{"template_include_path_$lang"} ||= 
        template_include_path({ language => $lang });
92
    return $cache->{"template_include_path_$lang"};
93 94
}

95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
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);
    };
117
    # This parsing may seem fragile, but it's OK:
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
    # 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}
    };
}
134

135 136 137 138 139 140 141 142
# 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 {
143
    my ($text, $bug, $comment) = (@_);
144 145 146 147 148 149 150
    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
151
    # \0 is used because it's unlikely to occur in the text, so the cost of
152 153 154 155 156 157 158 159 160 161
    # 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
162
    # if it was substituted as a bug title (since that always involve leading
163 164
    # and trailing text)

165
    # Because of entities, it's easier (and quicker) to do this before escaping
166 167 168 169 170

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

171
    my @hook_regexes;
172
    Bugzilla::Hook::process('bug_format_comment',
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
        { text => \$text, bug => $bug, regexes => \@hook_regexes,
          comment => $comment });

    foreach my $re (@hook_regexes) {
        my ($match, $replace) = @$re{qw(match replace)};
        if (ref($replace) eq 'CODE') {
            $text =~ s/$match/($things[$count++] = $replace->({matches => [
                                                               $1, $2, $3, $4,
                                                               $5, $6, $7, $8, 
                                                               $9, $10]}))
                               && ("\0\0" . ($count-1) . "\0\0")/egx;
        }
        else {
            $text =~ s/$match/($things[$count++] = $replace) 
                              && ("\0\0" . ($count-1) . "\0\0")/egx;
        }
    }

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

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

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

212
    # We have to quote now, otherwise the html itself is escaped
213 214 215 216 217 218 219 220 221 222 223 224 225
    # 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;

226
    # attachment links
227
    $text =~ s~\b(attachment\s*\#?\s*(\d+))
228 229 230 231 232
              ~($things[$count++] = get_attachment_link($2, $1)) &&
               ("\0\0" . ($count-1) . "\0\0")
              ~egmxi;

    # Current bug ID this comment belongs to
233
    my $current_bugurl = $bug ? ("show_bug.cgi?id=" . $bug->id) : "";
234 235 236 237 238

    # 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
239
    my $bug_word = template_var('terms')->{bug};
240 241 242
    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)
243 244
              ~ # We have several choices. $1 here is the link, and $2-4 are set
                # depending on which part matched
245
               (defined($2) ? get_bug_link($2, $1, { comment_num => $3 }) :
246 247 248
                              "<a href=\"$current_bugurl#c$4\">$1</a>")
              ~egox;

249 250
    # Old duplicate markers. These don't use $bug_word because they are old
    # and were never customizable.
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
    $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";

272 273
    my ($bugid, $isobsolete, $desc, $is_patch) =
        $dbh->selectrow_array('SELECT bug_id, isobsolete, description, ispatch
274 275 276 277 278 279 280 281 282 283 284 285 286
                               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.
287
        $title = html_quote(clean_text($title));
288

289
        $link_text =~ s/ \[details\]$//;
290
        my $linkval = "attachment.cgi?id=$attachid";
291 292 293 294

        # If the attachment is a patch, try to link to the diff rather
        # than the text, by default.
        my $patchlink = "";
295 296
        if ($is_patch and Bugzilla->feature('patch_viewer')) {
            $patchlink = '&amp;action=diff';
297 298
        }

299 300
        # Whitespace matters here because these links are in <pre> tags.
        return qq|<span class="$className">|
301
               . qq|<a href="${linkval}${patchlink}" 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, $link_text, $options) = @_;
319 320
    my $dbh = Bugzilla->dbh;

321 322
    if (!$bug) {
        return html_quote('<missing bug number>');
323 324
    }

325 326 327 328 329 330
    $bug = blessed($bug) ? $bug : new Bugzilla::Bug($bug);
    return $link_text if $bug->{error};
    
    # 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) = ("", "", "");
331

332 333 334 335
    $title = get_text('get_status', { status => $bug->bug_status });
    if ($bug->bug_status eq 'UNCONFIRMED') {
        $pre = "<i>";
        $post = "</i>";
336
    }
337 338 339 340 341 342 343 344
    if ($bug->resolution) {
        $pre .= '<span class="bz_closed">';
        $title .= ' ' . get_text('get_resolution',
                                 { resolution => $bug->resolution });
        $post .= '</span>';
    }
    if (Bugzilla->user->can_see_bug($bug)) {
        $title .= " - " . $bug->short_desc;
345 346 347
        if ($options->{use_alias} && $link_text =~ /^\d+$/ && $bug->alias) {
            $link_text = $bug->alias;
        }
348 349 350 351 352 353 354
    }
    # Prevent code injection in the title.
    $title = html_quote(clean_text($title));

    my $linkval = "show_bug.cgi?id=" . $bug->id;
    if ($options->{comment_num}) {
        $linkval .= "#c" . $options->{comment_num};
355
    }
356
    return qq{$pre<a href="$linkval" title="$title">$link_text</a>$post};
357 358
}

359 360 361
###############################################################################
# Templatization Code

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

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

390 391 392 393 394 395 396
# Clone the array reference to leave the original one unaltered.
$Template::Stash::LIST_OPS->{ clone } =
  sub {
      my $list = shift;
      return [@$list];
  };

397 398 399 400 401 402 403
# 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];
  };

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

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

431 432 433 434 435 436 437 438 439 440 441
sub process {
    my $self = shift;
    # All of this current_langs stuff allows template_inner to correctly
    # determine what-language Template object it should instantiate.
    my $current_langs = Bugzilla->request_cache->{template_current_lang} ||= [];
    unshift(@$current_langs, $self->context->{bz_language});
    my $retval = $self->SUPER::process(@_);
    shift @$current_langs;
    return $retval;
}

442 443 444 445 446 447 448
# 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;
449 450
    my %opts = @_;

451 452
    # IMPORTANT - If you make any FILTER changes here, make sure to
    # make them in t/004.template.t also, if required.
453

454
    my $config = {
455
        # Colon-separated list of directories containing templates.
456 457
        INCLUDE_PATH => $opts{'include_path'} 
                        || _include_path($opts{'language'}),
458 459 460 461 462 463 464 465

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

466 467 468 469 470 471 472 473
        # Bugzilla::Template::Plugin::Hook uses the absolute (in mod_perl)
        # or relative (in mod_cgi) paths of hook files to explicitly compile
        # a specific file. Also, these paths may be absolute at any time
        # if a packager has modified bz_locations() to contain absolute
        # paths.
        ABSOLUTE => 1,
        RELATIVE => $ENV{MOD_PERL} ? 0 : 1,

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

476
        # Initialize templates (f.e. by loading plugins like Hook).
477
        PRE_PROCESS => ["global/initialize.none.tmpl"],
478

479 480
        ENCODING => Bugzilla->params->{'utf8'} ? 'UTF-8' : undef,

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

            # 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
            ],
514 515 516 517 518

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

546 547 548 549 550 551 552 553
            # Prevents line break on hyphens and whitespaces.
            no_break => sub {
                my ($var) = @_;
                $var =~ s/ /\&nbsp;/g;
                $var =~ s/-/\&#8209;/g;
                return $var;
            },

554 555 556 557 558 559 560 561
            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 ,

562 563 564 565
            # 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 ,

566
            quoteUrls => [ sub {
567
                               my ($context, $bug, $comment) = @_;
568 569
                               return sub {
                                   my $text = shift;
570
                                   return quoteUrls($text, $bug, $comment);
571 572 573 574
                               };
                           },
                           1
                         ],
575 576

            bug_link => [ sub {
577
                              my ($context, $bug, $options) = @_;
578 579
                              return sub {
                                  my $text = shift;
580
                                  return get_bug_link($bug, $text, $options);
581 582 583 584 585
                              };
                          },
                          1
                        ],

586 587 588 589 590 591
            bug_list_link => sub
            {
                my $buglist = shift;
                return join(", ", map(get_bug_link($_, $_), split(/ *, */, $buglist)));
            },

592 593 594 595 596 597 598 599 600 601 602 603
            # 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;
            } ,

604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627
            # 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);
                        }
                    }
                }
            },

628
            # Format a time for display (more info in Bugzilla::Util)
629
            time => [ sub {
630
                          my ($context, $format, $timezone) = @_;
631 632
                          return sub {
                              my $time = shift;
633
                              return format_time($time, $format, $timezone);
634 635 636 637
                          };
                      },
                      1
                    ],
638

639
            html => \&Bugzilla::Util::html_quote,
640 641 642

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

643 644
            email => \&Bugzilla::Util::email_filter,

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
            # 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;
683 684 685 686
                # 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);
687 688 689
                return $var;
            },

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

697 698 699 700
            # 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]; } ,
701 702 703 704
        },

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

705
        CONSTANTS => _load_constants(),
706

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

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

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

718
            # Currently logged in user, if any
719
            # If an sudo session is in progress, this is the user we're faking
720
            'user' => sub { return Bugzilla->user; },
721 722 723 724 725 726 727 728
           
            # Currenly active language
            # XXX Eventually this should probably be replaced with something
            # like Bugzilla->language.
            'current_language' => sub {
                my ($language) = include_languages();
                return $language;
            },
729

730 731 732 733
            # If an sudo session is in progress, this is the user who
            # started the session.
            'sudoer' => sub { return Bugzilla->sudoer; },

734 735 736 737 738 739
            # SendBugMail - sends mail about a bug, using Bugzilla::BugMail.pm
            'SendBugMail' => sub {
                my ($id, $mailrecipients) = (@_);
                require Bugzilla::BugMail;
                Bugzilla::BugMail::Send($id, $mailrecipients);
            },
740

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

744 745 746 747 748 749 750 751
            # 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;
            },

752 753 754
            # Allow templates to generate a token themselves.
            'issue_hash_token' => \&Bugzilla::Token::issue_hash_token,

755 756 757 758 759 760 761 762
            # A way for all templates to get at Field data, cached.
            'bug_fields' => sub {
                my $cache = Bugzilla->request_cache;
                $cache->{template_bug_fields} ||= 
                    { map { $_->name => $_ } Bugzilla->get_fields() };
                return $cache->{template_bug_fields};
            },

763 764
            # Whether or not keywords are enabled, in this Bugzilla.
            'use_keywords' => sub { return Bugzilla::Keyword->any_exist; },
765 766 767 768 769 770 771 772 773

            'last_bug_list' => sub {
                my @bug_list;
                my $cgi = Bugzilla->cgi;
                if ($cgi->cookie("BUGLIST")) {
                    @bug_list = split(/:/, $cgi->cookie("BUGLIST"));
                }
                return \@bug_list;
            },
774

775 776
            'feature_enabled' => sub { return Bugzilla->feature(@_); },

777 778 779 780 781
            # field_descs can be somewhat slow to generate, so we generate
            # it only once per-language no matter how many times
            # $template->process() is called.
            'field_descs' => sub { return template_var('field_descs') },

782 783
            'install_string' => \&Bugzilla::Install::Util::install_string,

784 785 786 787 788 789
            # 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()};
790 791 792 793 794 795 796
                foreach my $item (@optional) {
                    my @features;
                    foreach my $feat_id (@{ $item->{feature} }) {
                        push(@features, install_string("feature_$feat_id"));
                    }
                    $item->{feature} = \@features;
                }
797 798
                return \@optional;
            },
799
        },
800
    };
801

802 803
    local $Template::Config::CONTEXT = 'Bugzilla::Template::Context';

804
    Bugzilla::Hook::process('template_before_create', { config => $config });
805 806
    my $template = $class->new($config) 
        || die("Template creation failed: " . $class->error());
807 808 809 810 811

    # Pass on our current language to any template hooks or inner templates
    # called by this Template object.
    $template->context->{bz_language} = $opts{language} || '';

812
    return $template;
813 814
}

815
# Used as part of the two subroutines below.
816
our %_templates_to_precompile;
817 818 819 820 821 822
sub precompile_templates {
    my ($output) = @_;

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

825 826
        # This frequently fails if the webserver made the files, because
        # then the webserver owns the directories.
827 828
        rmtree("$datadir/template");

829 830 831 832 833 834 835 836 837 838
        # Check that the directory was really removed, and if not, move it
        # into data/deleteme/.
        if (-e "$datadir/template") {
            print STDERR "\n\n",
                install_string('template_removal_failed', 
                               { datadir => $datadir }), "\n\n";
            mkpath("$datadir/deleteme");
            my $random = generate_random_password();
            rename("$datadir/template", "$datadir/deleteme/$random")
              or die "move failed: $!";
839 840 841
        }
    }

842
    print install_string('template_precompile') if $output;
843

844
    my $paths = template_include_path();
845 846 847 848

    foreach my $dir (@$paths) {
        my $template = Bugzilla::Template->create(include_path => [$dir]);

849
        %_templates_to_precompile = ();
850 851
        # Traverse the template hierarchy.
        find({ wanted => \&_precompile_push, no_chdir => 1 }, $dir);
852 853 854
        # 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) {
855
            $file =~ s{^\Q$dir\E/}{};
856 857 858 859 860
            # Compile the template but throw away the result. This has the side-
            # effect of writing the compiled version to disk.
            $template->context->template($file);
        }
    }
861

862 863 864
    # 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
865
    # directory. (Like data/template/var/www/html/bugzilla/.) To avoid
866
    # re-compiling templates under mod_perl, we symlink to the
867 868
    # already-compiled templates. This doesn't work on Windows.
    if (!ON_WINDOWS) {
869 870 871
        # We do these separately in case they're in different locations.
        _do_template_symlink(bz_locations()->{'templatedir'});
        _do_template_symlink(bz_locations()->{'extensionsdir'});
872
    }
873

874 875
    # If anything created a Template object before now, clear it out.
    delete Bugzilla->request_cache->{template};
876 877

    print install_string('done') . "\n" if $output;
878 879 880 881 882 883 884 885 886 887 888
}

# Helper for precompile_templates
sub _precompile_push {
    my $name = $File::Find::name;
    return if (-d $name);
    return if ($name =~ /\/CVS\//);
    return if ($name !~ /\.tmpl$/);
    $_templates_to_precompile{$name} = 1;
}

889 890 891 892 893 894 895 896 897 898 899
# Helper for precompile_templates
sub _do_template_symlink {
    my $dir_to_symlink = shift;

    my $abs_path = abs_path($dir_to_symlink);

    # If $dir_to_symlink is already an absolute path (as might happen
    # with packagers who set $libpath to an absolute path), then we don't
    # need to do this symlink.
    return if ($abs_path eq $dir_to_symlink);

900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917
    my $abs_root  = dirname($abs_path);
    my $dir_name  = basename($abs_path);
    my $datadir   = bz_locations()->{'datadir'};
    my $container = "$datadir/template$abs_root";
    mkpath($container);
    my $target = "$datadir/template/$dir_name";
    # Check if the directory exists, because if there are no extensions,
    # there won't be an "data/template/extensions" directory to link to.
    if (-d $target) {
        # We use abs2rel so that the symlink will look like 
        # "../../../../template" which works, while just 
        # "data/template/template/" doesn't work.
        my $relative_target = File::Spec->abs2rel($target, $container);

        my $link_name = "$container/$dir_name";
        symlink($relative_target, $link_name)
          or warn "Could not make $link_name a symlink to $relative_target: $!";
    }
918 919
}

920 921 922 923 924 925
1;

__END__

=head1 NAME

926
Bugzilla::Template - Wrapper around the Template Toolkit C<Template> object
927

928
=head1 SYNOPSIS
929 930

  my $template = Bugzilla::Template->create;
931 932 933 934
  my $format = $template->get_format("foo/bar",
                                     scalar($cgi->param('format')),
                                     scalar($cgi->param('ctype')));

935 936 937 938 939 940 941 942
=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.

943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958
=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

959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975
=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

976 977 978
=head1 SEE ALSO

L<Bugzilla>, L<Template>