1402 lines
49 KiB
Perl
1402 lines
49 KiB
Perl
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
#
|
|
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
|
# defined by the Mozilla Public License, v. 2.0.
|
|
|
|
|
|
package Bugzilla::Template;
|
|
|
|
use 5.10.1;
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Bugzilla::Constants;
|
|
use Bugzilla::WebService::Constants;
|
|
use Bugzilla::Hook;
|
|
use Bugzilla::Install::Requirements;
|
|
use Bugzilla::Install::Util qw(install_string template_include_path
|
|
include_languages);
|
|
use Bugzilla::Classification;
|
|
use Bugzilla::Keyword;
|
|
use Bugzilla::Util;
|
|
use Bugzilla::Error;
|
|
use Bugzilla::Search;
|
|
use Bugzilla::Token;
|
|
|
|
use Cwd qw(abs_path);
|
|
use MIME::Base64;
|
|
use Date::Format ();
|
|
use Digest::MD5 qw(md5_hex);
|
|
use File::Basename qw(basename dirname);
|
|
use File::Find;
|
|
use File::Path qw(rmtree mkpath);
|
|
use File::Spec;
|
|
use IO::Dir;
|
|
use List::MoreUtils qw(firstidx);
|
|
use Scalar::Util qw(blessed);
|
|
|
|
use parent qw(Template);
|
|
|
|
use constant FORMAT_TRIPLE => '%19s|%-28s|%-28s';
|
|
use constant FORMAT_3_SIZE => [19,28,28];
|
|
use constant FORMAT_DOUBLE => '%19s %-55s';
|
|
use constant FORMAT_2_SIZE => [19,55];
|
|
|
|
# Pseudo-constant.
|
|
sub SAFE_URL_REGEXP {
|
|
my $safe_protocols = join('|', SAFE_PROTOCOLS);
|
|
return qr/($safe_protocols):[^:\s<>\"][^\s<>\"]+[\w\/]/i;
|
|
}
|
|
|
|
# Convert the constants in the Bugzilla::Constants and Bugzilla::WebService::Constants
|
|
# modules into a hash we can pass to the template object for reflection into its "constants"
|
|
# namespace (which is like its "variables" namespace, but for constants). To do so, we
|
|
# traverse the arrays of exported and exportable symbols and ignoring the rest
|
|
# (which, if Constants.pm exports only constants, as it should, will be nothing else).
|
|
sub _load_constants {
|
|
my %constants;
|
|
foreach my $constant (@Bugzilla::Constants::EXPORT,
|
|
@Bugzilla::Constants::EXPORT_OK)
|
|
{
|
|
if (ref Bugzilla::Constants->$constant) {
|
|
$constants{$constant} = Bugzilla::Constants->$constant;
|
|
}
|
|
else {
|
|
my @list = (Bugzilla::Constants->$constant);
|
|
$constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list;
|
|
}
|
|
}
|
|
|
|
foreach my $constant (@Bugzilla::WebService::Constants::EXPORT,
|
|
@Bugzilla::WebService::Constants::EXPORT_OK)
|
|
{
|
|
if (ref Bugzilla::WebService::Constants->$constant) {
|
|
$constants{$constant} = Bugzilla::WebService::Constants->$constant;
|
|
}
|
|
else {
|
|
my @list = (Bugzilla::WebService::Constants->$constant);
|
|
$constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list;
|
|
}
|
|
}
|
|
return \%constants;
|
|
}
|
|
|
|
# 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
|
|
# Templates may also be found in the extensions/ tree
|
|
sub _include_path {
|
|
my $lang = shift || '';
|
|
my $cache = Bugzilla->request_cache;
|
|
$cache->{"template_include_path_$lang"} ||=
|
|
template_include_path({ language => $lang });
|
|
return $cache->{"template_include_path_$lang"};
|
|
}
|
|
|
|
sub get_format {
|
|
my $self = shift;
|
|
my ($template, $format, $ctype) = @_;
|
|
|
|
$ctype //= 'html';
|
|
$format //= '';
|
|
|
|
# ctype and format can have letters and a hyphen only.
|
|
if ($ctype =~ /[^a-zA-Z\-]/ || $format =~ /[^a-zA-Z\-]/) {
|
|
ThrowUserError('format_not_found', {'format' => $format,
|
|
'ctype' => $ctype,
|
|
'invalid' => 1});
|
|
}
|
|
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);
|
|
};
|
|
# This parsing may seem fragile, but it's OK:
|
|
# 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,
|
|
'format' => $format,
|
|
'extension' => $ctype,
|
|
'ctype' => Bugzilla::Constants::contenttypes->{$ctype}
|
|
};
|
|
}
|
|
|
|
# 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, $bug, $comment, $user) = @_;
|
|
return $text unless $text;
|
|
$user ||= Bugzilla->user;
|
|
|
|
# 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 replacing matches with \x{FDD2}$count\x{FDD3}
|
|
# \x{FDDx} is used because it's unlikely to occur in the text
|
|
# and are reserved unicode characters. We disable warnings for now
|
|
# until we require Perl 5.13.9 or newer.
|
|
no warnings 'utf8';
|
|
|
|
# If the comment is already wrapped, we should ignore newlines when
|
|
# looking for matching regexps. Else we should take them into account.
|
|
my $s = ($comment && $comment->already_wrapped) ? qr/\s/ : qr/\h/;
|
|
|
|
# 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
|
|
# if it was substituted as a bug title (since that always involve leading
|
|
# and trailing text)
|
|
|
|
# Because of entities, it's easier (and quicker) to do this before escaping
|
|
|
|
my @things;
|
|
my $count = 0;
|
|
my $tmp;
|
|
|
|
my @hook_regexes;
|
|
Bugzilla::Hook::process('bug_format_comment',
|
|
{ text => \$text, bug => $bug, regexes => \@hook_regexes,
|
|
comment => $comment, user => $user });
|
|
|
|
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]}))
|
|
&& ("\x{FDD2}" . ($count-1) . "\x{FDD3}")/egx;
|
|
}
|
|
else {
|
|
$text =~ s/$match/($things[$count++] = $replace)
|
|
&& ("\x{FDD2}" . ($count-1) . "\x{FDD3}")/egx;
|
|
}
|
|
}
|
|
|
|
# Provide tooltips for full bug links (Bug 74355)
|
|
my $urlbase_re = '(' . join('|',
|
|
map { qr/$_/ } grep($_, Bugzilla->params->{'urlbase'},
|
|
Bugzilla->params->{'sslbase'})) . ')';
|
|
$text =~ s~\b(${urlbase_re}\Qshow_bug.cgi?id=\E([0-9]+)(\#c([0-9]+))?)\b
|
|
~($things[$count++] = get_bug_link($3, $1, { comment_num => $5, user => $user })) &&
|
|
("\x{FDD2}" . ($count-1) . "\x{FDD3}")
|
|
~egox;
|
|
|
|
# WEBKIT_CHANGES
|
|
# Provides tooltips for short bug links (Bug 169707)
|
|
$text =~ s~\b((https?://)?\Qwebkit.org/b/\E([0-9]+)(\#c([0-9]+))?)\b
|
|
~($things[$count++] = get_bug_link($3, $1, { comment_num => $5, user => $user })) &&
|
|
("\x{FDD2}" . ($count-1) . "\x{FDD3}")
|
|
~egox;
|
|
|
|
# non-mailto protocols
|
|
my $safe_protocols = SAFE_URL_REGEXP();
|
|
$text =~ s~\b($safe_protocols)
|
|
~($tmp = html_quote($1)) &&
|
|
($things[$count++] = "<a href=\"$tmp\">$tmp</a>") &&
|
|
("\x{FDD2}" . ($count-1) . "\x{FDD3}")
|
|
~egox;
|
|
|
|
# We have to quote now, otherwise the html itself is escaped
|
|
# THIS MEANS THAT A LITERAL ", <, >, ' MUST BE ESCAPED FOR A MATCH
|
|
|
|
$text = html_quote($text);
|
|
|
|
# Color quoted text
|
|
$text =~ s~^(>.+)$~<span class="quote">$1</span >~mg;
|
|
$text =~ s~</span >\n<span class="quote">~\n~g;
|
|
|
|
# mailto:
|
|
# Use |<nothing> so that $1 is defined regardless
|
|
# @ is the encoded '@' character.
|
|
$text =~ s~\b(mailto:|)?([\w\.\-\+\=]+&\#64;[\w\-]+(?:\.[\w\-]+)+)\b
|
|
~<a href=\"mailto:$2\">$1$2</a>~igx;
|
|
|
|
# attachment links
|
|
$text =~ s~\b(attachment$s*\#?$s*([0-9]+)(?:$s+\[details\])?)
|
|
~($things[$count++] = get_attachment_link($2, $1, $user)) &&
|
|
("\x{FDD2}" . ($count-1) . "\x{FDD3}")
|
|
~egmxi;
|
|
|
|
# Current bug ID this comment belongs to
|
|
my $current_bugurl = $bug ? ("show_bug.cgi?id=" . $bug->id) : "";
|
|
|
|
# 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
|
|
my $bug_word = template_var('terms')->{bug};
|
|
my $bug_re = qr/\Q$bug_word\E$s*\#?$s*([0-9]+)/i;
|
|
my $comment_word = template_var('terms')->{comment};
|
|
my $comment_re = qr/(?:\Q$comment_word\E|comment)$s*\#?$s*([0-9]+)/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, { comment_num => $3, user => $user }) :
|
|
"<a href=\"$current_bugurl#c$4\">$1</a>")
|
|
~egx;
|
|
|
|
# Handle a list of bug ids: bugs 1, #2, 3, 4
|
|
# Currently, the only delimiter supported is comma.
|
|
# Concluding "and" and "or" are not supported.
|
|
my $bugs_word = template_var('terms')->{bugs};
|
|
|
|
my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s*
|
|
[0-9]+(?:$s*,$s*\#?$s*[0-9]+)+/ix;
|
|
|
|
$text =~ s{($bugs_re)}{
|
|
my $match = $1;
|
|
$match =~ s/((?:#$s*)?([0-9]+))/get_bug_link($2, $1);/eg;
|
|
$match;
|
|
}eg;
|
|
|
|
my $comments_word = template_var('terms')->{comments};
|
|
|
|
my $comments_re = qr/(?:comments|\Q$comments_word\E)$s*\#?$s*
|
|
[0-9]+(?:$s*,$s*\#?$s*[0-9]+)+/ix;
|
|
|
|
$text =~ s{($comments_re)}{
|
|
my $match = $1;
|
|
$match =~ s|((?:#$s*)?([0-9]+))|<a href="$current_bugurl#c$2">$1</a>|g;
|
|
$match;
|
|
}eg;
|
|
|
|
# Old duplicate markers. These don't use $bug_word because they are old
|
|
# and were never customizable.
|
|
$text =~ s~(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )
|
|
([0-9]+)
|
|
(?=\ \*\*\*\Z)
|
|
~get_bug_link($1, $1, { user => $user })
|
|
~egmx;
|
|
|
|
# Now remove the encoding hacks in reverse order
|
|
for (my $i = $#things; $i >= 0; $i--) {
|
|
$text =~ s/\x{FDD2}($i)\x{FDD3}/$things[$i]/eg;
|
|
}
|
|
|
|
return $text;
|
|
}
|
|
|
|
# Creates a link to an attachment, including its title.
|
|
sub get_attachment_link {
|
|
my ($attachid, $link_text, $user) = @_;
|
|
$user ||= Bugzilla->user;
|
|
|
|
my $attachment = new Bugzilla::Attachment({ id => $attachid, cache => 1 });
|
|
|
|
if ($attachment) {
|
|
my $title = "";
|
|
my $className = "";
|
|
if ($user->can_see_bug($attachment->bug_id)
|
|
&& (!$attachment->isprivate || $user->is_insider))
|
|
{
|
|
$title = $attachment->description;
|
|
}
|
|
if ($attachment->isobsolete) {
|
|
$className = "bz_obsolete";
|
|
}
|
|
# Prevent code injection in the title.
|
|
$title = html_quote(clean_text($title));
|
|
|
|
$link_text =~ s/ \[details\]$//;
|
|
my $linkval = "attachment.cgi?id=$attachid";
|
|
|
|
# If the attachment is a patch, try to link to the diff rather
|
|
# than the text, by default.
|
|
my $patchlink = "";
|
|
if ($attachment->ispatch and Bugzilla->feature('patch_viewer')) {
|
|
$patchlink = '&action=diff';
|
|
}
|
|
|
|
# Whitespace matters here because these links are in <pre> tags.
|
|
return qq|<span class="$className">|
|
|
. qq|<a href="${linkval}${patchlink}" name="attach_${attachid}" title="$title">$link_text</a>|
|
|
. qq| <a href="${linkval}&action=edit" title="$title">[details]</a>|
|
|
. 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, $link_text, $options) = @_;
|
|
$options ||= {};
|
|
$options->{user} ||= Bugzilla->user;
|
|
|
|
if (defined $bug && $bug ne '') {
|
|
if (!blessed($bug)) {
|
|
require Bugzilla::Bug;
|
|
$bug = new Bugzilla::Bug({ id => $bug, cache => 1 });
|
|
}
|
|
return $link_text if $bug->{error};
|
|
}
|
|
|
|
my $template = Bugzilla->template_inner;
|
|
my $linkified;
|
|
$template->process('bug/link.html.tmpl',
|
|
{ bug => $bug, link_text => $link_text, %$options }, \$linkified);
|
|
return $linkified;
|
|
}
|
|
|
|
# We use this instead of format because format doesn't deal well with
|
|
# multi-byte languages.
|
|
sub multiline_sprintf {
|
|
my ($format, $args, $sizes) = @_;
|
|
my @parts;
|
|
my @my_sizes = @$sizes; # Copy this so we don't modify the input array.
|
|
foreach my $string (@$args) {
|
|
my $size = shift @my_sizes;
|
|
my @pieces = split("\n", wrap_hard($string, $size));
|
|
push(@parts, \@pieces);
|
|
}
|
|
|
|
my $formatted;
|
|
while (1) {
|
|
# Get the first item of each part.
|
|
my @line = map { shift @$_ } @parts;
|
|
# If they're all undef, we're done.
|
|
last if !grep { defined $_ } @line;
|
|
# Make any single undef item into ''
|
|
@line = map { defined $_ ? $_ : '' } @line;
|
|
# And append a formatted line
|
|
$formatted .= sprintf($format, @line);
|
|
# Remove trailing spaces, or they become lots of =20's in
|
|
# quoted-printable emails.
|
|
$formatted =~ s/\s+$//;
|
|
$formatted .= "\n";
|
|
}
|
|
return $formatted;
|
|
}
|
|
|
|
#####################
|
|
# Header Generation #
|
|
#####################
|
|
|
|
# Returns the last modification time of a file, as an integer number of
|
|
# seconds since the epoch.
|
|
sub _mtime { return (stat($_[0]))[9] }
|
|
|
|
sub mtime_filter {
|
|
my ($file_url, $mtime) = @_;
|
|
# This environment var is set in the .htaccess if we have mod_headers
|
|
# and mod_expires installed, to make sure that JS and CSS with "?"
|
|
# after them will still be cached by clients.
|
|
return $file_url if !$ENV{BZ_CACHE_CONTROL};
|
|
if (!$mtime) {
|
|
my $cgi_path = bz_locations()->{'cgi_path'};
|
|
my $file_path = "$cgi_path/$file_url";
|
|
$mtime = _mtime($file_path);
|
|
}
|
|
return "$file_url?$mtime";
|
|
}
|
|
|
|
# Set up the skin CSS cascade:
|
|
#
|
|
# 1. standard/global.css
|
|
# 2. YUI CSS
|
|
# 3. Standard Bugzilla stylesheet set
|
|
# 4. Third-party "skin" stylesheet set, per user prefs
|
|
# 5. Inline css passed to global/header.html.tmpl
|
|
# 6. Custom Bugzilla stylesheet set
|
|
|
|
sub css_files {
|
|
my ($style_urls, $yui, $yui_css) = @_;
|
|
|
|
# global.css goes on every page.
|
|
my @requested_css = ('skins/standard/global.css', @$style_urls);
|
|
|
|
my @yui_required_css;
|
|
foreach my $yui_name (@$yui) {
|
|
next if !$yui_css->{$yui_name};
|
|
push(@yui_required_css, "js/yui/assets/skins/sam/$yui_name.css");
|
|
}
|
|
unshift(@requested_css, @yui_required_css);
|
|
|
|
my @css_sets = map { _css_link_set($_) } @requested_css;
|
|
|
|
my %by_type = (standard => [], skin => [], custom => []);
|
|
foreach my $set (@css_sets) {
|
|
foreach my $key (keys %$set) {
|
|
push(@{ $by_type{$key} }, $set->{$key});
|
|
}
|
|
}
|
|
|
|
# build unified
|
|
$by_type{unified_standard_skin} = _concatenate_css($by_type{standard},
|
|
$by_type{skin});
|
|
$by_type{unified_custom} = _concatenate_css($by_type{custom});
|
|
|
|
return \%by_type;
|
|
}
|
|
|
|
sub _css_link_set {
|
|
my ($file_name) = @_;
|
|
|
|
my %set = (standard => mtime_filter($file_name));
|
|
|
|
# We use (?:^|/) to allow Extensions to use the skins system if they want.
|
|
if ($file_name !~ m{(?:^|/)skins/standard/}) {
|
|
return \%set;
|
|
}
|
|
|
|
my $skin = Bugzilla->user->settings->{skin}->{value};
|
|
my $cgi_path = bz_locations()->{'cgi_path'};
|
|
my $skin_file_name = $file_name;
|
|
$skin_file_name =~ s{(?:^|/)skins/standard/}{skins/contrib/$skin/};
|
|
if (my $mtime = _mtime("$cgi_path/$skin_file_name")) {
|
|
$set{skin} = mtime_filter($skin_file_name, $mtime);
|
|
}
|
|
|
|
my $custom_file_name = $file_name;
|
|
$custom_file_name =~ s{(?:^|/)skins/standard/}{skins/custom/};
|
|
if (my $custom_mtime = _mtime("$cgi_path/$custom_file_name")) {
|
|
$set{custom} = mtime_filter($custom_file_name, $custom_mtime);
|
|
}
|
|
|
|
return \%set;
|
|
}
|
|
|
|
sub _concatenate_css {
|
|
my @sources = map { @$_ } @_;
|
|
return unless @sources;
|
|
|
|
my %files =
|
|
map {
|
|
(my $file = $_) =~ s/(^[^\?]+)\?.+/$1/;
|
|
$_ => $file;
|
|
} @sources;
|
|
|
|
my $cgi_path = bz_locations()->{cgi_path};
|
|
my $skins_path = bz_locations()->{assetsdir};
|
|
|
|
# build minified files
|
|
my @minified;
|
|
foreach my $source (@sources) {
|
|
next unless -e "$cgi_path/$files{$source}";
|
|
my $file = $skins_path . '/' . md5_hex($source) . '.css';
|
|
if (!-e $file) {
|
|
my $content = read_text("$cgi_path/$files{$source}");
|
|
|
|
# minify
|
|
$content =~ s{/\*.*?\*/}{}sg; # comments
|
|
$content =~ s{(^\s+|\s+$)}{}mg; # leading/trailing whitespace
|
|
$content =~ s{\n}{}g; # single line
|
|
|
|
# rewrite urls
|
|
$content =~ s{url\(([^\)]+)\)}{_css_url_rewrite($source, $1)}eig;
|
|
|
|
write_text($file, "/* $files{$source} */\n" . $content . "\n");
|
|
}
|
|
push @minified, $file;
|
|
}
|
|
|
|
# concat files
|
|
my $file = $skins_path . '/' . md5_hex(join(' ', @sources)) . '.css';
|
|
if (!-e $file) {
|
|
my $content = '';
|
|
foreach my $source (@minified) {
|
|
$content .= read_text($source);
|
|
}
|
|
write_text($file, $content);
|
|
}
|
|
|
|
$file =~ s/^\Q$cgi_path\E\///o;
|
|
return mtime_filter($file);
|
|
}
|
|
|
|
sub _css_url_rewrite {
|
|
my ($source, $url) = @_;
|
|
# rewrite relative urls as the unified stylesheet lives in a different
|
|
# directory from the source
|
|
$url =~ s/(^['"]|['"]$)//g;
|
|
if (substr($url, 0, 1) eq '/' || substr($url, 0, 5) eq 'data:') {
|
|
return 'url(' . $url . ')';
|
|
}
|
|
return 'url(../../' . ($ENV{'PROJECT'} ? '../' : '') . dirname($source) . '/' . $url . ')';
|
|
}
|
|
|
|
sub _concatenate_js {
|
|
return @_ unless CONCATENATE_ASSETS;
|
|
my ($sources) = @_;
|
|
return [] unless $sources;
|
|
$sources = ref($sources) ? $sources : [ $sources ];
|
|
|
|
my %files =
|
|
map {
|
|
(my $file = $_) =~ s/(^[^\?]+)\?.+/$1/;
|
|
$_ => $file;
|
|
} @$sources;
|
|
|
|
my $cgi_path = bz_locations()->{cgi_path};
|
|
my $skins_path = bz_locations()->{assetsdir};
|
|
|
|
# build minified files
|
|
my @minified;
|
|
foreach my $source (@$sources) {
|
|
next unless -e "$cgi_path/$files{$source}";
|
|
my $file = $skins_path . '/' . md5_hex($source) . '.js';
|
|
if (!-e $file) {
|
|
my $content = read_text("$cgi_path/$files{$source}");
|
|
|
|
# minimal minification
|
|
$content =~ s#/\*.*?\*/##sg; # block comments
|
|
$content =~ s#(^ +| +$)##gm; # leading/trailing spaces
|
|
$content =~ s#^//.+$##gm; # single line comments
|
|
$content =~ s#\n{2,}#\n#g; # blank lines
|
|
$content =~ s#(^\s+|\s+$)##g; # whitespace at the start/end of file
|
|
|
|
write_text($file, ";/* $files{$source} */\n" . $content . "\n");
|
|
}
|
|
push @minified, $file;
|
|
}
|
|
|
|
# concat files
|
|
my $file = $skins_path . '/' . md5_hex(join(' ', @$sources)) . '.js';
|
|
if (!-e $file) {
|
|
my $content = '';
|
|
foreach my $source (@minified) {
|
|
$content .= read_text($source);
|
|
}
|
|
write_text($file, $content);
|
|
}
|
|
|
|
$file =~ s/^\Q$cgi_path\E\///o;
|
|
return [ $file ];
|
|
}
|
|
|
|
# YUI dependency resolution
|
|
sub yui_resolve_deps {
|
|
my ($yui, $yui_deps) = @_;
|
|
|
|
my @yui_resolved;
|
|
foreach my $yui_name (@$yui) {
|
|
my $deps = $yui_deps->{$yui_name} || [];
|
|
foreach my $dep (reverse @$deps) {
|
|
push(@yui_resolved, $dep) if !grep { $_ eq $dep } @yui_resolved;
|
|
}
|
|
push(@yui_resolved, $yui_name) if !grep { $_ eq $yui_name } @yui_resolved;
|
|
}
|
|
return \@yui_resolved;
|
|
}
|
|
|
|
###############################################################################
|
|
# Templatization Code
|
|
|
|
# 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;
|
|
|
|
# Use the Toolkit Template's Stash module to add utility pseudo-methods
|
|
# to template variables.
|
|
use Template::Stash;
|
|
|
|
# Allow keys to start with an underscore or a dot.
|
|
$Template::Stash::PRIVATE = undef;
|
|
|
|
# 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) = @_;
|
|
if (ref $item && $item->isa('Bugzilla::Object')) {
|
|
return grep($_->id == $item->id, @$list);
|
|
} else {
|
|
return grep($_ eq $item, @$list);
|
|
}
|
|
};
|
|
|
|
$Template::Stash::LIST_OPS->{ containsany } =
|
|
sub {
|
|
my ($list, $items) = @_;
|
|
foreach my $item (@$items) {
|
|
if (ref $item && $item->isa('Bugzilla::Object')) {
|
|
return 1 if grep($_->id == $item->id, @$list);
|
|
} else {
|
|
return 1 if grep($_ eq $item, @$list);
|
|
}
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
# Clone the array reference to leave the original one unaltered.
|
|
$Template::Stash::LIST_OPS->{ clone } =
|
|
sub {
|
|
my $list = shift;
|
|
return [@$list];
|
|
};
|
|
|
|
# Allow us to sort the list of fields correctly
|
|
$Template::Stash::LIST_OPS->{ sort_by_field_name } =
|
|
sub {
|
|
sub field_name {
|
|
if ($_[0] eq 'noop') {
|
|
# Sort --- first
|
|
return '';
|
|
}
|
|
# Otherwise sort by field_desc or description
|
|
return $_[1]{$_[0]} || $_[0];
|
|
}
|
|
my ($list, $field_desc) = @_;
|
|
return [ sort { lc field_name($a, $field_desc) cmp lc field_name($b, $field_desc) } @$list ];
|
|
};
|
|
|
|
# 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];
|
|
};
|
|
|
|
# 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) = @_;
|
|
return $string if !$length || length($string) <= $length;
|
|
|
|
$ellipsis ||= '';
|
|
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.
|
|
|
|
###############################################################################
|
|
|
|
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;
|
|
}
|
|
|
|
# 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;
|
|
my %opts = @_;
|
|
|
|
# IMPORTANT - If you make any FILTER changes here, make sure to
|
|
# make them in t/004.template.t also, if required.
|
|
|
|
my $config = {
|
|
# Colon-separated list of directories containing templates.
|
|
INCLUDE_PATH => $opts{'include_path'}
|
|
|| _include_path($opts{'language'}),
|
|
|
|
# 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,
|
|
|
|
# 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,
|
|
|
|
COMPILE_DIR => bz_locations()->{'template_cache'},
|
|
|
|
# Don't check for a template update until 1 hour has passed since the
|
|
# last check.
|
|
STAT_TTL => 60 * 60,
|
|
|
|
# Initialize templates (f.e. by loading plugins like Hook).
|
|
PRE_PROCESS => ["global/variables.none.tmpl"],
|
|
|
|
ENCODING => Bugzilla->params->{'utf8'} ? 'UTF-8' : undef,
|
|
|
|
# Functions for processing text within templates in various ways.
|
|
# IMPORTANT! When adding a filter here that does not override a
|
|
# built-in filter, please also add a stub filter to t/004template.t.
|
|
FILTERS => {
|
|
|
|
# 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
|
|
],
|
|
|
|
# Returns the text with backslashes, single/double quotes,
|
|
# and newlines/carriage returns escaped for use in JS strings.
|
|
js => sub {
|
|
my ($var) = @_;
|
|
$var =~ s/([\\\'\"\/])/\\$1/g;
|
|
$var =~ s/\n/\\n/g;
|
|
$var =~ s/\r/\\r/g;
|
|
$var =~ s/\x{2028}/\\u2028/g; # unicode line separator
|
|
$var =~ s/\x{2029}/\\u2029/g; # unicode paragraph separator
|
|
$var =~ s/\@/\\x40/g; # anti-spam for email addresses
|
|
$var =~ s/</\\x3c/g;
|
|
$var =~ s/>/\\x3e/g;
|
|
return $var;
|
|
},
|
|
|
|
# Converts data to base64
|
|
base64 => sub {
|
|
my ($data) = @_;
|
|
return encode_base64($data);
|
|
},
|
|
|
|
# Strips out control characters excepting whitespace
|
|
strip_control_chars => sub {
|
|
my ($data) = @_;
|
|
state $use_utf8 = Bugzilla->params->{'utf8'};
|
|
# Only run for utf8 to avoid issues with other multibyte encodings
|
|
# that may be reassigning meaning to ascii characters.
|
|
if ($use_utf8) {
|
|
$data =~ s/(?![\t\r\n])[[:cntrl:]]//g;
|
|
}
|
|
return $data;
|
|
},
|
|
|
|
# HTML collapses newlines in element attributes to a single space,
|
|
# so form elements which may have whitespace (ie comments) need
|
|
# to be encoded using 
|
|
# See bugs 4928, 22983 and 32000 for more details
|
|
html_linebreak => sub {
|
|
my ($var) = @_;
|
|
$var = html_quote($var);
|
|
$var =~ s/\r\n/\
/g;
|
|
$var =~ s/\n\r/\
/g;
|
|
$var =~ s/\r/\
/g;
|
|
$var =~ s/\n/\
/g;
|
|
return $var;
|
|
},
|
|
|
|
xml => \&Bugzilla::Util::xml_quote ,
|
|
|
|
# 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 ,
|
|
|
|
# Removes control characters and trims extra whitespace.
|
|
clean_text => \&Bugzilla::Util::clean_text ,
|
|
|
|
quoteUrls => [ sub {
|
|
my ($context, $bug, $comment, $user) = @_;
|
|
return sub {
|
|
my $text = shift;
|
|
return quoteUrls($text, $bug, $comment, $user);
|
|
};
|
|
},
|
|
1
|
|
],
|
|
|
|
bug_link => [ sub {
|
|
my ($context, $bug, $options) = @_;
|
|
return sub {
|
|
my $text = shift;
|
|
return get_bug_link($bug, $text, $options);
|
|
};
|
|
},
|
|
1
|
|
],
|
|
|
|
bug_list_link => sub {
|
|
my ($buglist, $options) = @_;
|
|
return join(", ", map(get_bug_link($_, $_, $options), split(/ *, */, $buglist)));
|
|
},
|
|
|
|
# In CSV, quotes are doubled, and any value containing a quote or a
|
|
# comma is enclosed in quotes.
|
|
# If a field starts with either "=", "+", "-" or "@", it is preceded
|
|
# by a space to prevent stupid formula execution from Excel & co.
|
|
csv => sub
|
|
{
|
|
my ($var) = @_;
|
|
$var = ' ' . $var if $var =~ /^[+=@-]/;
|
|
# backslash is not special to CSV, but it can be used to confuse some browsers...
|
|
# so we do not allow it to happen. We only do this for logged-in users.
|
|
$var =~ s/\\/\x{FF3C}/g if Bugzilla->user->id;
|
|
$var =~ s/\"/\"\"/g;
|
|
if ($var !~ /^-?(\d+\.)?\d*$/) {
|
|
$var = "\"$var\"";
|
|
}
|
|
return $var;
|
|
} ,
|
|
|
|
# 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);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
# Format a time for display (more info in Bugzilla::Util)
|
|
time => [ sub {
|
|
my ($context, $format, $timezone) = @_;
|
|
return sub {
|
|
my $time = shift;
|
|
return format_time($time, $format, $timezone);
|
|
};
|
|
},
|
|
1
|
|
],
|
|
|
|
html => \&Bugzilla::Util::html_quote,
|
|
|
|
html_light => \&Bugzilla::Util::html_light_quote,
|
|
|
|
email => \&Bugzilla::Util::email_filter,
|
|
|
|
mtime => \&mtime_filter,
|
|
|
|
# iCalendar contentline filter
|
|
ics => [ sub {
|
|
my ($context, @args) = @_;
|
|
return sub {
|
|
my ($var) = shift;
|
|
my ($par) = shift @args;
|
|
my ($output) = "";
|
|
|
|
$var =~ s/[\r\n]/ /g;
|
|
$var =~ s/([;\\\",])/\\$1/g;
|
|
|
|
if ($par) {
|
|
$output = sprintf("%s:%s", $par, $var);
|
|
} else {
|
|
$output = $var;
|
|
}
|
|
|
|
$output =~ s/(.{75,75})/$1\n /g;
|
|
|
|
return $output;
|
|
};
|
|
},
|
|
1
|
|
],
|
|
|
|
# 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/\@/@/g;
|
|
$var =~ s/\</</g;
|
|
$var =~ s/\>/>/g;
|
|
$var =~ s/\"/\"/g;
|
|
$var =~ s/\&/\&/g;
|
|
# Now remove extra whitespace...
|
|
my $collapse_filter = $Template::Filters::FILTERS->{collapse};
|
|
$var = $collapse_filter->($var);
|
|
# And if we're not in the WebService, wrap the message.
|
|
# (Wrapping the message in the WebService is unnecessary
|
|
# and causes awkward things like \n's appearing in error
|
|
# messages in JSON-RPC.)
|
|
unless (i_am_webservice()) {
|
|
$var = wrap_comment($var, 72);
|
|
}
|
|
$var =~ s/\ / /g;
|
|
|
|
return $var;
|
|
},
|
|
|
|
# Wrap a displayed comment to the appropriate length
|
|
wrap_comment => [
|
|
sub {
|
|
my ($context, $cols) = @_;
|
|
return sub { wrap_comment($_[0], $cols) }
|
|
}, 1],
|
|
|
|
# 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]; } ,
|
|
},
|
|
|
|
PLUGIN_BASE => 'Bugzilla::Template::Plugin',
|
|
|
|
CONSTANTS => _load_constants(),
|
|
|
|
# Default variables for all templates
|
|
VARIABLES => {
|
|
# Function for retrieving global parameters.
|
|
'Param' => sub { return Bugzilla->params->{$_[0]}; },
|
|
|
|
# Function to create date strings
|
|
'time2str' => \&Date::Format::time2str,
|
|
|
|
# Fixed size column formatting for bugmail.
|
|
'format_columns' => sub {
|
|
my $cols = shift;
|
|
my $format = ($cols == 3) ? FORMAT_TRIPLE : FORMAT_DOUBLE;
|
|
my $col_size = ($cols == 3) ? FORMAT_3_SIZE : FORMAT_2_SIZE;
|
|
return multiline_sprintf($format, \@_, $col_size);
|
|
},
|
|
|
|
# Generic linear search function
|
|
'lsearch' => sub {
|
|
my ($array, $item) = @_;
|
|
return firstidx { $_ eq $item } @$array;
|
|
},
|
|
|
|
# Currently logged in user, if any
|
|
# If an sudo session is in progress, this is the user we're faking
|
|
'user' => sub { return Bugzilla->user; },
|
|
|
|
# Currenly active language
|
|
'current_language' => sub { return Bugzilla->current_language; },
|
|
|
|
# If an sudo session is in progress, this is the user who
|
|
# started the session.
|
|
'sudoer' => sub { return Bugzilla->sudoer; },
|
|
|
|
# Allow templates to access the "correct" URLBase value
|
|
'urlbase' => sub { return Bugzilla::Util::correct_urlbase(); },
|
|
|
|
# Allow templates to access docs url with users' preferred language
|
|
# We fall back to English if documentation in the preferred
|
|
# language is not available
|
|
'docs_urlbase' => sub {
|
|
my $docs_urlbase;
|
|
my $lang = Bugzilla->current_language;
|
|
# Translations currently available on readthedocs.org
|
|
my @rtd_translations = ('en', 'fr');
|
|
|
|
if ($lang ne 'en' && -f "docs/$lang/html/index.html") {
|
|
$docs_urlbase = "docs/$lang/html/";
|
|
}
|
|
elsif (-f "docs/en/html/index.html") {
|
|
$docs_urlbase = "docs/en/html/";
|
|
}
|
|
else {
|
|
if (!grep { $_ eq $lang } @rtd_translations) {
|
|
$lang = "en";
|
|
}
|
|
|
|
my $version = BUGZILLA_VERSION;
|
|
$version =~ /^(\d+)\.(\d+)/;
|
|
if ($2 % 2 == 1) {
|
|
# second number is odd; development version
|
|
$version = 'latest';
|
|
}
|
|
else {
|
|
$version = "$1.$2";
|
|
}
|
|
|
|
$docs_urlbase = "https://bugzilla.readthedocs.org/$lang/$version/";
|
|
}
|
|
|
|
return $docs_urlbase;
|
|
},
|
|
|
|
# Check whether the URL is safe.
|
|
'is_safe_url' => sub {
|
|
my $url = shift;
|
|
return 0 unless $url;
|
|
|
|
my $safe_url_regexp = SAFE_URL_REGEXP();
|
|
return 1 if $url =~ /^$safe_url_regexp$/;
|
|
# Pointing to a local file with no colon in its name is fine.
|
|
return 1 if $url =~ /^[^\s<>\":]+[\w\/]$/i;
|
|
# If we come here, then we cannot guarantee it's safe.
|
|
return 0;
|
|
},
|
|
|
|
# Allow templates to generate a token themselves.
|
|
'issue_hash_token' => \&Bugzilla::Token::issue_hash_token,
|
|
|
|
'get_login_request_token' => sub {
|
|
my $cookie = Bugzilla->cgi->cookie('Bugzilla_login_request_cookie');
|
|
return $cookie ? issue_hash_token(['login_request', $cookie]) : '';
|
|
},
|
|
|
|
'get_api_token' => sub {
|
|
return '' unless Bugzilla->user->id;
|
|
my $cache = Bugzilla->request_cache;
|
|
return $cache->{api_token} //= issue_api_token();
|
|
},
|
|
|
|
# A way for all templates to get at Field data, cached.
|
|
'bug_fields' => sub {
|
|
my $cache = Bugzilla->request_cache;
|
|
$cache->{template_bug_fields} ||=
|
|
Bugzilla->fields({ by_name => 1 });
|
|
return $cache->{template_bug_fields};
|
|
},
|
|
|
|
# A general purpose cache to store rendered templates for reuse.
|
|
# Make sure to not mix language-specific data.
|
|
'template_cache' => sub {
|
|
my $cache = Bugzilla->request_cache->{template_cache} ||= {};
|
|
$cache->{users} ||= {};
|
|
return $cache;
|
|
},
|
|
|
|
'css_files' => \&css_files,
|
|
yui_resolve_deps => \&yui_resolve_deps,
|
|
concatenate_js => \&_concatenate_js,
|
|
|
|
# All classifications (sorted by sortkey, name)
|
|
'all_classifications' => sub {
|
|
return [map { $_->name } Bugzilla::Classification->get_all()];
|
|
},
|
|
|
|
# Whether or not keywords are enabled, in this Bugzilla.
|
|
'use_keywords' => sub { return Bugzilla::Keyword->any_exist; },
|
|
|
|
# All the keywords.
|
|
'all_keywords' => sub {
|
|
return [map { $_->name } Bugzilla::Keyword->get_all()];
|
|
},
|
|
|
|
'feature_enabled' => sub { return Bugzilla->feature(@_); },
|
|
|
|
# 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') },
|
|
|
|
# Calling bug/field-help.none.tmpl once per label is very
|
|
# expensive, so we generate it once per-language.
|
|
'help_html' => sub { return template_var('help_html') },
|
|
|
|
# This way we don't have to load field-descs.none.tmpl in
|
|
# many templates.
|
|
'display_value' => \&Bugzilla::Util::display_value,
|
|
|
|
'install_string' => \&Bugzilla::Install::Util::install_string,
|
|
|
|
'report_columns' => \&Bugzilla::Search::REPORT_COLUMNS,
|
|
|
|
# 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()};
|
|
foreach my $item (@optional) {
|
|
my @features;
|
|
foreach my $feat_id (@{ $item->{feature} }) {
|
|
push(@features, install_string("feature_$feat_id"));
|
|
}
|
|
$item->{feature} = \@features;
|
|
}
|
|
return \@optional;
|
|
},
|
|
'default_authorizer' => sub { return Bugzilla::Auth->new() },
|
|
|
|
'login_not_email' => sub {
|
|
my $params = Bugzilla->params;
|
|
my $cache = Bugzilla->request_cache;
|
|
|
|
return $cache->{login_not_email} //=
|
|
($params->{emailsuffix}
|
|
|| ($params->{user_verify_class} =~ /LDAP/ && $params->{LDAPmailattribute})
|
|
|| ($params->{user_verify_class} =~ /RADIUS/ && $params->{RADIUS_email_suffix}))
|
|
? 1 : 0;
|
|
},
|
|
},
|
|
};
|
|
# Use a per-process provider to cache compiled templates in memory across
|
|
# requests.
|
|
my $provider_key = join(':', @{ $config->{INCLUDE_PATH} });
|
|
my $shared_providers = Bugzilla->process_cache->{shared_providers} ||= {};
|
|
$shared_providers->{$provider_key} ||= Template::Provider->new($config);
|
|
$config->{LOAD_TEMPLATES} = [ $shared_providers->{$provider_key} ];
|
|
|
|
local $Template::Config::CONTEXT = 'Bugzilla::Template::Context';
|
|
|
|
Bugzilla::Hook::process('template_before_create', { config => $config });
|
|
my $template = $class->new($config)
|
|
|| die("Template creation failed: " . $class->error());
|
|
|
|
# Pass on our current language to any template hooks or inner templates
|
|
# called by this Template object.
|
|
$template->context->{bz_language} = $opts{language} || '';
|
|
|
|
return $template;
|
|
}
|
|
|
|
# Used as part of the two subroutines below.
|
|
our %_templates_to_precompile;
|
|
sub precompile_templates {
|
|
my ($output) = @_;
|
|
|
|
# Remove the compiled templates.
|
|
my $cache_dir = bz_locations()->{'template_cache'};
|
|
my $datadir = bz_locations()->{'datadir'};
|
|
if (-e $cache_dir) {
|
|
print install_string('template_removing_dir') . "\n" if $output;
|
|
|
|
# This frequently fails if the webserver made the files, because
|
|
# then the webserver owns the directories.
|
|
rmtree($cache_dir);
|
|
|
|
# Check that the directory was really removed, and if not, move it
|
|
# into data/deleteme/.
|
|
if (-e $cache_dir) {
|
|
my $deleteme = "$datadir/deleteme";
|
|
|
|
print STDERR "\n\n",
|
|
install_string('template_removal_failed',
|
|
{ deleteme => $deleteme,
|
|
template_cache => $cache_dir }), "\n\n";
|
|
mkpath($deleteme);
|
|
my $random = generate_random_password();
|
|
rename($cache_dir, "$deleteme/$random")
|
|
or die "move failed: $!";
|
|
}
|
|
}
|
|
|
|
print install_string('template_precompile') if $output;
|
|
|
|
# Pre-compile all available languages.
|
|
my $paths = template_include_path({ language => Bugzilla->languages });
|
|
|
|
foreach my $dir (@$paths) {
|
|
my $template = Bugzilla::Template->create(include_path => [$dir]);
|
|
|
|
%_templates_to_precompile = ();
|
|
# Traverse the template hierarchy.
|
|
find({ wanted => \&_precompile_push, no_chdir => 1 }, $dir);
|
|
# 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) {
|
|
$file =~ s{^\Q$dir\E/}{};
|
|
# Compile the template but throw away the result. This has the side-
|
|
# effect of writing the compiled version to disk.
|
|
$template->context->template($file);
|
|
}
|
|
|
|
# Clear out the cached Provider object
|
|
Bugzilla->process_cache->{shared_providers} = undef;
|
|
}
|
|
|
|
# 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/bugzilla/.) To avoid
|
|
# re-compiling templates under mod_perl, we symlink to the
|
|
# already-compiled templates. This doesn't work on Windows.
|
|
if (!ON_WINDOWS) {
|
|
# We do these separately in case they're in different locations.
|
|
_do_template_symlink(bz_locations()->{'templatedir'});
|
|
_do_template_symlink(bz_locations()->{'extensionsdir'});
|
|
}
|
|
|
|
# If anything created a Template object before now, clear it out.
|
|
delete Bugzilla->request_cache->{template};
|
|
|
|
print install_string('done') . "\n" if $output;
|
|
}
|
|
|
|
# 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;
|
|
}
|
|
|
|
# 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);
|
|
|
|
my $abs_root = dirname($abs_path);
|
|
my $dir_name = basename($abs_path);
|
|
my $cache_dir = bz_locations()->{'template_cache'};
|
|
my $container = "$cache_dir$abs_root";
|
|
mkpath($container);
|
|
my $target = "$cache_dir/$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: $!";
|
|
}
|
|
}
|
|
|
|
1;
|
|
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
Bugzilla::Template - Wrapper around the Template Toolkit C<Template> object
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
my $template = Bugzilla::Template->create;
|
|
my $format = $template->get_format("foo/bar",
|
|
scalar($cgi->param('format')),
|
|
scalar($cgi->param('ctype')));
|
|
|
|
=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.
|
|
|
|
=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
|
|
|
|
=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
|
|
|
|
=head1 SEE ALSO
|
|
|
|
L<Bugzilla>, L<Template>
|
|
|
|
=head1 B<Methods in need of POD>
|
|
|
|
=over
|
|
|
|
=item multiline_sprintf
|
|
|
|
=item create
|
|
|
|
=item css_files
|
|
|
|
=item mtime_filter
|
|
|
|
=item yui_resolve_deps
|
|
|
|
=item process
|
|
|
|
=item get_bug_link
|
|
|
|
=item quoteUrls
|
|
|
|
=item get_attachment_link
|
|
|
|
=item SAFE_URL_REGEXP
|
|
|
|
=back
|