summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'Bugzilla/Template.pm')
-rw-r--r--Bugzilla/Template.pm2122
1 files changed, 1084 insertions, 1038 deletions
diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm
index 463d38620..b23d711be 100644
--- a/Bugzilla/Template.pm
+++ b/Bugzilla/Template.pm
@@ -16,8 +16,8 @@ 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::Install::Util qw(install_string template_include_path
+ include_languages);
use Bugzilla::Classification;
use Bugzilla::Keyword;
use Bugzilla::Util;
@@ -40,47 +40,49 @@ 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_3_SIZE => [19, 28, 28];
use constant FORMAT_DOUBLE => '%19s %-55s';
-use constant FORMAT_2_SIZE => [19,55];
+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;
+ 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"
+# 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;
- }
+ my %constants;
+ foreach
+ my $constant (@Bugzilla::Constants::EXPORT, @Bugzilla::Constants::EXPORT_OK)
+ {
+ if (ref Bugzilla::Constants->$constant) {
+ $constants{$constant} = Bugzilla::Constants->$constant;
}
-
- 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;
- }
+ 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;
}
- return \%constants;
+ 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
@@ -88,55 +90,51 @@ sub _load_constants {
# 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"};
+ 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}
- };
+ 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
@@ -147,203 +145,216 @@ sub get_format {
# 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 => [
+ 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)
+ }
+ 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
+ }
+
+ # 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;
- # non-mailto protocols
- my $safe_protocols = SAFE_URL_REGEXP();
- $text =~ s~\b($safe_protocols)
+ # 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
+ # 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);
+ $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;
+ # 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
- # &#64; is the encoded '@' character.
- $text =~ s~\b(mailto:|)?([\w\.\-\+\=]+&\#64;[\w\-]+(?:\.[\w\-]+)+)\b
+ # mailto:
+ # Use |<nothing> so that $1 is defined regardless
+ # &#64; 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\])?)
+ # 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)
+ # 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};
+ # 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*
+ my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s*
[0-9]+(?:$s*,$s*\#?$s*[0-9]+)+/ix;
- $text =~ s{($bugs_re)}{
+ $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_word = template_var('terms')->{comments};
- my $comments_re = qr/(?:comments|\Q$comments_word\E)$s*\#?$s*
+ my $comments_re = qr/(?:comments|\Q$comments_word\E)$s*\#?$s*
[0-9]+(?:$s*,$s*\#?$s*[0-9]+)+/ix;
- $text =~ s{($comments_re)}{
+ $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\ )
+ # 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;
- }
+ # 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;
+ 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));
+ my ($attachid, $link_text, $user) = @_;
+ $user ||= Bugzilla->user;
- $link_text =~ s/ \[details(?:, diff)?\]$//;
- my $linkval = "attachment.cgi?id=$attachid";
+ my $attachment = new Bugzilla::Attachment({id => $attachid, cache => 1});
- # 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 = '&amp;action=diff';
- }
+ 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";
+ }
- if ($patchlink) {
- # Whitespace matters here because these links are in <pre> tags.
- return qq|<span class="$className">|
- . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>|
- . qq| [<a href="${linkval}&amp;action=edit" title="$title">details</a>, <a href="${linkval}${patchlink}" title="$title">diff</a>]|
- . qq|</span>|;
- }
- else {
- # Whitespace matters here because these links are in <pre> tags.
- return qq|<span class="$className">|
- . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>|
- . qq| [<a href="${linkval}&amp;action=edit" title="$title">details</a>]|
- . qq|</span>|;
- }
+ # Prevent code injection in the title.
+ $title = html_quote(clean_text($title));
+
+ $link_text =~ s/ \[details(?:, diff)?\]$//;
+ 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 = '&amp;action=diff';
+ }
+
+ if ($patchlink) {
+
+ # Whitespace matters here because these links are in <pre> tags.
+ return
+ qq|<span class="$className">|
+ . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>|
+ . qq| [<a href="${linkval}&amp;action=edit" title="$title">details</a>, <a href="${linkval}${patchlink}" title="$title">diff</a>]|
+ . qq|</span>|;
}
else {
- return qq{$link_text};
+ # Whitespace matters here because these links are in <pre> tags.
+ return
+ qq|<span class="$className">|
+ . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>|
+ . qq| [<a href="${linkval}&amp;action=edit" title="$title">details</a>]|
+ . qq|</span>|;
}
+ }
+ else {
+ return qq{$link_text};
+ }
}
# Creates a link to a bug, including its title.
@@ -354,53 +365,59 @@ sub get_attachment_link {
# 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 ($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});
}
-
- my $template = Bugzilla->template_inner;
- my $linkified;
- $template->process('bug/link.html.tmpl',
- { bug => $bug, link_text => $link_text, %$options }, \$linkified);
- return $linkified;
+ 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;
+ 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;
}
#####################
@@ -412,17 +429,18 @@ sub multiline_sprintf {
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";
+ 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:
@@ -435,183 +453,186 @@ sub mtime_filter {
# 6. Custom Bugzilla stylesheet set
sub css_files {
- my ($style_urls, $yui, $yui_css) = @_;
+ my ($style_urls, $yui, $yui_css) = @_;
- # global.css goes on every page.
- my @requested_css = ('skins/standard/global.css', @$style_urls);
+ # 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});
- }
+ 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});
+ # 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;
+ 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 ($file_name) = @_;
- 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);
- }
+ 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';
+ 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 = '';
- foreach my $source (@minified) {
- $content .= read_text($source);
- }
- write_text($file, $content);
+ 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);
+ $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 . ')';
+ 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';
+ 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 = '';
- foreach my $source (@minified) {
- $content .= read_text($source);
- }
- write_text($file, $content);
+ 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 ];
+ $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;
+ 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;
}
- return \@yui_resolved;
+ push(@yui_resolved, $yui_name) if !grep { $_ eq $yui_name } @yui_resolved;
+ }
+ return \@yui_resolved;
}
###############################################################################
@@ -630,73 +651,75 @@ 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
+# 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;
- };
+$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];
- };
+$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 ];
- };
+$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];
- };
+$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;
- };
+$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.
@@ -704,14 +727,15 @@ $Template::Stash::SCALAR_OPS->{ truncate } =
###############################################################################
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;
+ 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
@@ -720,603 +744,625 @@ sub process {
# 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 &#013;
- # See bugs 4928, 22983 and 32000 for more details
- html_linebreak => sub {
- my ($var) = @_;
- $var = html_quote($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;
- },
-
- 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/\&#64;/@/g;
- $var =~ s/\&lt;/</g;
- $var =~ s/\&gt;/>/g;
- $var =~ s/\&quot;/\"/g;
- $var =~ s/\&amp;/\&/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/\&nbsp;/ /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]; } ,
+ 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 &#013;
+ # See bugs 4928, 22983 and 32000 for more details
+ html_linebreak => sub {
+ my ($var) = @_;
+ $var = html_quote($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;
+ },
+
+ 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;
- 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(); },
- 'httpbase' => sub { return Bugzilla->params->{'urlbase'}; },
- 'sslbase' => sub { return Bugzilla->params->{'sslbase'}; },
- 'ssl_redirect' => sub { return Bugzilla->params->{'ssl_redirect'}; },
-
- # 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;
- },
+ return $output;
+ };
},
- };
- # 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} ];
+ 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/\&#64;/@/g;
+ $var =~ s/\&lt;/</g;
+ $var =~ s/\&gt;/>/g;
+ $var =~ s/\&quot;/\"/g;
+ $var =~ s/\&amp;/\&/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/\&nbsp;/ /g;
+
+ return $var;
+ },
- local $Template::Config::CONTEXT = 'Bugzilla::Template::Context';
+ # 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(); },
+ 'httpbase' => sub { return Bugzilla->params->{'urlbase'}; },
+ 'sslbase' => sub { return Bugzilla->params->{'sslbase'}; },
+ 'ssl_redirect' => sub { return Bugzilla->params->{'ssl_redirect'}; },
+
+ # 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";
+ }
- Bugzilla::Hook::process('template_before_create', { config => $config });
- my $template = $class->new($config)
- || die("Template creation failed: " . $class->error());
+ my $version = BUGZILLA_VERSION;
+ $version =~ /^(\d+)\.(\d+)/;
+ if ($2 % 2 == 1) {
- # Pass on our current language to any template hooks or inner templates
- # called by this Template object.
- $template->context->{bz_language} = $opts{language} || '';
+ # second number is odd; development version
+ $version = 'latest';
+ }
+ else {
+ $version = "$1.$2";
+ }
- return $template;
+ $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());
+ Bugzilla::Hook::process('template_after_create', {template => $template});
+
+ # 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) = @_;
+ my ($output) = @_;
- # Remove the compiled templates.
- my $cache_dir = bz_locations()->{'template_cache'};
- my $datadir = bz_locations()->{'datadir'};
+ # 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) {
- 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: $!";
- }
+ 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;
+ print install_string('template_precompile') if $output;
- # Pre-compile all available languages.
- my $paths = template_include_path({ language => Bugzilla->languages });
+ # 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]);
+ 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);
- }
+ %_templates_to_precompile = ();
- # Clear out the cached Provider object
- Bugzilla->process_cache->{shared_providers} = undef;
- }
+ # Traverse the template hierarchy.
+ find({wanted => \&_precompile_push, no_chdir => 1}, $dir);
- # 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'});
+ # 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);
}
- # If anything created a Template object before now, clear it out.
- delete Bugzilla->request_cache->{template};
+ # 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'});
+ }
- print install_string('done') . "\n" if $output;
+ # 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;
+ 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: $!";
- }
+ 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;