diff options
Diffstat (limited to 'Bugzilla/Template.pm')
-rw-r--r-- | Bugzilla/Template.pm | 2122 |
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~^(>.+)$~<span class="quote">$1</span >~mg; - $text =~ s~</span >\n<span class="quote">~\n~g; + # 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 + # 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\])?) + # 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 = '&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}&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}&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 = '&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}&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}&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 
 - # 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]; } , + 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; - 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/\@/@/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; + }, - 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; |