diff options
Diffstat (limited to 'Bugzilla/Template.pm')
-rw-r--r-- | Bugzilla/Template.pm | 290 |
1 files changed, 208 insertions, 82 deletions
diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm index 7fd3f0e8d..9bd0c51bd 100644 --- a/Bugzilla/Template.pm +++ b/Bugzilla/Template.pm @@ -51,9 +51,11 @@ use Bugzilla::Token; use Cwd qw(abs_path); use MIME::Base64; use Date::Format (); +use Digest::MD5 qw(md5_hex); use File::Basename qw(basename dirname); use File::Find; use File::Path qw(rmtree mkpath); +use File::Slurp; use File::Spec; use IO::Dir; use List::MoreUtils qw(firstidx); @@ -154,9 +156,10 @@ sub get_format { # If you want to modify this routine, read the comments carefully sub quoteUrls { - my ($text, $bug, $comment, $user) = @_; + my ($text, $bug, $comment, $user, $bug_link_func) = @_; return $text unless $text; $user ||= Bugzilla->user; + $bug_link_func ||= \&get_bug_link; # We use /g for speed, but uris can have other things inside them # (http://foo/bug#3 for example). Filtering that out filters valid @@ -168,6 +171,10 @@ sub quoteUrls { # 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. @@ -206,7 +213,7 @@ sub quoteUrls { 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 })) && + ~($things[$count++] = $bug_link_func->($3, $1, { comment_num => $5, user => $user })) && ("\x{FDD2}" . ($count-1) . "\x{FDD3}") ~egox; @@ -234,7 +241,8 @@ sub quoteUrls { ~<a href=\"mailto:$2\">$1$2</a>~igx; # attachment links - $text =~ s~\b(attachment\s*\#?\s*(\d+)(?:\s+\[details\])?) + # BMO: don't make diff view the default for patches (Bug 652332) + $text =~ s~\b(attachment$s*\#?$s*(\d+)(?:$s+\[diff\])?(?:\s+\[details\])?) ~($things[$count++] = get_attachment_link($2, $1, $user)) && ("\x{FDD2}" . ($count-1) . "\x{FDD3}") ~egmxi; @@ -247,21 +255,21 @@ sub quoteUrls { # 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*(\d+)/i; - my $comment_re = qr/comment\s*\#?\s*(\d+)/i; - $text =~ s~\b($bug_re(?:\s*,?\s*$comment_re)?|$comment_re) + my $bug_re = qr/\Q$bug_word\E$s*\#?$s*(\d+)/i; + my $comment_re = qr/comment$s*\#?$s*(\d+)/i; + $text =~ s~\b($bug_re(?:$s*,?$s*$comment_re)?|$comment_re) ~ # 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 }) : + (defined($2) ? $bug_link_func->($2, $1, { comment_num => $3, user => $user }) : "<a href=\"$current_bugurl#c$4\">$1</a>") - ~egox; + ~egx; # 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\ ) (\d+) (?=\ \*\*\*\Z) - ~get_bug_link($1, $1, { user => $user }) + ~$bug_link_func->($1, $1, { user => $user }) ~egmx; # Now remove the encoding hacks in reverse order @@ -278,7 +286,7 @@ sub get_attachment_link { my $dbh = Bugzilla->dbh; $user ||= Bugzilla->user; - my $attachment = new Bugzilla::Attachment($attachid); + my $attachment = new Bugzilla::Attachment({ id => $attachid, cache => 1 }); if ($attachment) { my $title = ""; @@ -295,19 +303,21 @@ sub get_attachment_link { $title = html_quote(clean_text($title)); $link_text =~ s/ \[details\]$//; + $link_text =~ s/ \[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. + # If the attachment is a patch and patch_viewer feature is + # enabled, add link to the diff. my $patchlink = ""; if ($attachment->ispatch and Bugzilla->feature('patch_viewer')) { - $patchlink = '&action=diff'; + $patchlink = qq| <a href="${linkval}&action=diff" title="$title">[diff]</a>|; } # Whitespace matters here because these links are in <pre> tags. return qq|<span class="$className">| - . qq|<a href="${linkval}${patchlink}" name="attach_${attachid}" title="$title">$link_text</a>| + . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>| . qq| <a href="${linkval}&action=edit" title="$title">[details]</a>| + . qq|${patchlink}| . qq|</span>|; } else { @@ -328,8 +338,8 @@ sub get_bug_link { $options->{user} ||= Bugzilla->user; my $dbh = Bugzilla->dbh; - if (defined $bug) { - $bug = blessed($bug) ? $bug : new Bugzilla::Bug($bug); + if (defined $bug && $bug ne '') { + $bug = blessed($bug) ? $bug : new Bugzilla::Bug({ id => $bug, cache => 1 }); return $link_text if $bug->{error}; } @@ -394,21 +404,18 @@ sub mtime_filter { # Set up the skin CSS cascade: # -# 1. YUI CSS -# 2. Standard Bugzilla stylesheet set (persistent) -# 3. Standard Bugzilla stylesheet set (selectable) -# 4. All third-party "skin" stylesheet sets (selectable) -# 5. Page-specific styles -# 6. Custom Bugzilla stylesheet set (persistent) -# -# "Selectable" skin file sets may be either preferred or alternate. -# Exactly one is preferred, determined by the "skin" user preference. +# 1. standard/global.css +# 2. YUI CSS +# 3. Standard Bugzilla stylesheet set +# 4. Third-party "skin" stylesheet set, per user prefs +# 5. Inline css passed to global/header.html.tmpl +# 6. Custom Bugzilla stylesheet set + sub css_files { my ($style_urls, $yui, $yui_css) = @_; - - # global.css goes on every page, and so does IE-fixes.css. - my @requested_css = ('skins/standard/global.css', @$style_urls, - 'skins/standard/IE-fixes.css'); + + # global.css belongs on every page + my @requested_css = ( 'skins/standard/global.css', @$style_urls ); my @yui_required_css; foreach my $yui_name (@$yui) { @@ -419,21 +426,18 @@ sub css_files { my @css_sets = map { _css_link_set($_) } @requested_css; - my %by_type = (standard => [], alternate => {}, skin => [], custom => []); + my %by_type = (standard => [], skin => [], custom => []); foreach my $set (@css_sets) { foreach my $key (keys %$set) { - if ($key eq 'alternate') { - foreach my $alternate_skin (keys %{ $set->{alternate} }) { - my $files = $by_type{alternate}->{$alternate_skin} ||= []; - push(@$files, $set->{alternate}->{$alternate_skin}); - } - } - else { - push(@{ $by_type{$key} }, $set->{$key}); - } + push(@{ $by_type{$key} }, $set->{$key}); } } - + + # build unified + $by_type{unified_standard_skin} = _concatenate_css($by_type{standard}, + $by_type{skin}); + $by_type{unified_custom} = _concatenate_css($by_type{custom}); + return \%by_type; } @@ -441,42 +445,135 @@ 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/}) { + + # We use (?:^|/) to allow Extensions to use the skins system if they want. + if ($file_name !~ m{(?:^|/)skins/standard/}) { return \%set; } - - my $skin_user_prefs = Bugzilla->user->settings->{skin}; + + my $skin = Bugzilla->user->settings->{skin}->{value}; my $cgi_path = bz_locations()->{'cgi_path'}; - # If the DB is not accessible, user settings are not available. - my $all_skins = $skin_user_prefs ? $skin_user_prefs->legal_values : []; - my %skin_urls; - foreach my $option (@$all_skins) { - next if $option eq 'standard'; - my $skin_file_name = $file_name; - $skin_file_name =~ s{(^|/)skins/standard/}{skins/contrib/$option/}; - if (my $mtime = _mtime("$cgi_path/$skin_file_name")) { - $skin_urls{$option} = mtime_filter($skin_file_name, $mtime); - } + 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); } - $set{alternate} = \%skin_urls; - - my $skin = $skin_user_prefs->{'value'}; - if ($skin ne 'standard' and defined $set{alternate}->{$skin}) { - $set{skin} = delete $set{alternate}->{$skin}; - } - + my $custom_file_name = $file_name; - $custom_file_name =~ s{(^|/)skins/standard/}{skins/custom/}; + $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_file("$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_file($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_file($source); + } + write_file($file, $content); + } + + $file =~ s/^\Q$cgi_path\E\///o; + return mtime_filter($file); +} + +sub _css_url_rewrite { + my ($source, $url) = @_; + # rewrite relative urls as the unified stylesheet lives in a different + # directory from the source + $url =~ s/(^['"]|['"]$)//g; + return $url if substr($url, 0, 1) eq '/'; + return 'url(../../' . 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_file("$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_file($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_file($source); + } + write_file($file, $content); + } + + $file =~ s/^\Q$cgi_path\E\///o; + return [ $file ]; +} + # YUI dependency resolution sub yui_resolve_deps { my ($yui, $yui_deps) = @_; @@ -553,10 +650,9 @@ $Template::Stash::SCALAR_OPS->{ 0 } = $Template::Stash::SCALAR_OPS->{ truncate } = sub { my ($string, $length, $ellipsis) = @_; - $ellipsis ||= ""; - return $string if !$length || length($string) <= $length; - + + $ellipsis ||= ''; my $strlen = $length - length($ellipsis); my $newstr = substr($string, 0, $strlen) . $ellipsis; return $newstr; @@ -612,6 +708,10 @@ sub create { 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/initialize.none.tmpl"], @@ -663,6 +763,18 @@ sub create { $var =~ s/>/\\x3e/g; return $var; }, + + # Sadly, different to the above. See http://www.json.org/ + # for details. + json => sub { + my ($var) = @_; + $var =~ s/([\\\"\/])/\\$1/g; + $var =~ s/\n/\\n/g; + $var =~ s/\r/\\r/g; + $var =~ s/\f/\\f/g; + $var =~ s/\t/\\t/g; + return $var; + }, # Converts data to base64 base64 => sub { @@ -839,9 +951,7 @@ sub create { # (Wrapping the message in the WebService is unnecessary # and causes awkward things like \n's appearing in error # messages in JSON-RPC.) - unless (Bugzilla->usage_mode == USAGE_MODE_JSON - or Bugzilla->usage_mode == USAGE_MODE_XMLRPC) - { + unless (i_am_webservice()) { $var = wrap_comment($var, 72); } $var =~ s/\ / /g; @@ -891,14 +1001,9 @@ sub create { # 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 - # XXX Eventually this should probably be replaced with something - # like Bugzilla->language. - 'current_language' => sub { - my ($language) = include_languages(); - return $language; - }, + 'current_language' => sub { return Bugzilla->current_language; }, # If an sudo session is in progress, this is the user who # started the session. @@ -909,7 +1014,7 @@ sub create { # Allow templates to access docs url with users' preferred language 'docs_urlbase' => sub { - my ($language) = include_languages(); + my $language = Bugzilla->current_language; my $docs_urlbase = Bugzilla->params->{'docs_urlbase'}; $docs_urlbase =~ s/\%lang\%/$language/; return $docs_urlbase; @@ -938,9 +1043,18 @@ sub create { 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, # Whether or not keywords are enabled, in this Bugzilla. 'use_keywords' => sub { return Bugzilla::Keyword->any_exist; }, @@ -985,8 +1099,17 @@ sub create { 'default_authorizer' => sub { return Bugzilla::Auth->new() }, }, }; - - local $Template::Config::CONTEXT = 'Bugzilla::Template::Context'; + # 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} ]; + + # BMO - use metrics subclass + local $Template::Config::CONTEXT = Bugzilla->metrics_enabled() + ? 'Bugzilla::Metrics::Template::Context' + : 'Bugzilla::Template::Context'; Bugzilla::Hook::process('template_before_create', { config => $config }); my $template = $class->new($config) @@ -1066,6 +1189,9 @@ sub precompile_templates { # 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; + print install_string('done') . "\n" if $output; } |