Commit 10f4954b authored by Max Kanat-Alexander's avatar Max Kanat-Alexander

Bug 428313: Properly expire the browser's CSS and JS cache when there

are new versions of those files. This also eliminates single-file skins and should also allow Extensions to have skins. r=glob, a=mkanat
parent 0d280b90
...@@ -2,3 +2,14 @@ ...@@ -2,3 +2,14 @@
<FilesMatch ^(.*\.pm|.*\.pl|.*localconfig.*)$> <FilesMatch ^(.*\.pm|.*\.pl|.*localconfig.*)$>
deny from all deny from all
</FilesMatch> </FilesMatch>
<FilesMatch (\.js|\.css)$>
ExpiresActive On
# According to RFC 2616, "1 year in the future" means "never expire".
# We change the name of the file's URL whenever its modification date
# changes, so browsers can cache any individual JS or CSS URL forever.
# However, since all JS and CSS URLs involve a ? in them (for the changing
# name) we have to explicitly set an Expires header or browsers won't
# *ever* cache them.
ExpiresDefault "now plus 1 years"
Header append Cache-Control "public"
</FilesMatch>
...@@ -284,20 +284,6 @@ sub FILESYSTEM { ...@@ -284,20 +284,6 @@ sub FILESYSTEM {
contents => '' }, contents => '' },
); );
# Each standard stylesheet has an associated custom stylesheet that
# we create. Also, we create placeholders for standard stylesheets
# for contrib skins which don't provide them themselves.
foreach my $skin_dir ("$skinsdir/custom", <$skinsdir/contrib/*>) {
next if basename($skin_dir) =~ /^cvs$/i;
foreach my $base_css (<$skinsdir/standard/*.css>) {
_add_custom_css($skin_dir, basename($base_css), \%create_files);
}
foreach my $dir_css (<$skinsdir/standard/*/*.css>) {
$dir_css =~ s{.+?([^/]+/[^/]+)$}{$1};
_add_custom_css($skin_dir, $dir_css, \%create_files);
}
}
# Because checksetup controls the creation of index.html separately # Because checksetup controls the creation of index.html separately
# from all other files, it gets its very own hash. # from all other files, it gets its very own hash.
my %index_html = ( my %index_html = (
...@@ -455,20 +441,53 @@ EOT ...@@ -455,20 +441,53 @@ EOT
print "Removing duplicates directory...\n"; print "Removing duplicates directory...\n";
rmtree("$datadir/duplicates"); rmtree("$datadir/duplicates");
} }
_remove_empty_css_files();
_convert_single_file_skins();
} }
# A simple helper for creating "empty" CSS files. sub _remove_empty_css_files {
sub _add_custom_css { my $skinsdir = bz_locations()->{'skinsdir'};
my ($skin_dir, $path, $create_files) = @_; foreach my $css_file (glob("$skinsdir/custom/*.css"),
$create_files->{"$skin_dir/$path"} = { perms => WS_SERVE, contents => <<EOT glob("$skinsdir/contrib/*/*.css"))
{
_remove_empty_css($css_file);
}
}
# A simple helper for the update code that removes "empty" CSS files.
sub _remove_empty_css {
my ($file) = @_;
my $basename = basename($file);
my $empty_contents = <<EOT;
/* /*
* Custom rules for $path. * Custom rules for $basename.
* The rules you put here override rules in that stylesheet. * The rules you put here override rules in that stylesheet.
*/ */
EOT EOT
if (length($empty_contents) == -s $file) {
open(my $fh, '<', $file) or warn "$file: $!";
my $file_contents;
{ local $/; $file_contents = <$fh>; }
if ($file_contents eq $empty_contents) {
print install_string('file_remove', { name => $file }), "\n";
unlink $file or warn "$file: $!";
}
}; };
} }
# We used to allow a single css file in the skins/contrib/ directory
# to be a whole skin.
sub _convert_single_file_skins {
my $skinsdir = bz_locations()->{'skinsdir'};
foreach my $skin_file (glob "$skinsdir/contrib/*.css") {
my $dir_name = $skin_file;
$dir_name =~ s/\.css$//;
mkdir $dir_name or warn "$dir_name: $!";
_rename_file($skin_file, "$dir_name/global.css");
}
}
sub create_htaccess { sub create_htaccess {
_create_files(%{FILESYSTEM()->{htaccess}}); _create_files(%{FILESYSTEM()->{htaccess}});
...@@ -492,7 +511,7 @@ sub create_htaccess { ...@@ -492,7 +511,7 @@ sub create_htaccess {
sub _rename_file { sub _rename_file {
my ($from, $to) = @_; my ($from, $to) = @_;
print "Renaming $from to $to...\n"; print install_string('file_rename', { from => $from, to => $to }), "\n";
if (-e $to) { if (-e $to) {
warn "$to already exists, not moving\n"; warn "$to already exists, not moving\n";
} }
......
...@@ -358,6 +358,121 @@ sub get_bug_link { ...@@ -358,6 +358,121 @@ sub get_bug_link {
return qq{$pre<a href="$linkval" title="$title">$link_text</a>$post}; return qq{$pre<a href="$linkval" title="$title">$link_text</a>$post};
} }
#####################
# Header Generation #
#####################
# Returns the last modification time of a file, as an integer number of
# seconds since the epoch.
sub _mtime { return (stat($_[0]))[9] }
sub mtime_filter {
my ($file_url) = @_;
my $cgi_path = bz_locations()->{'cgi_path'};
my $file_path = "$cgi_path/$file_url";
return "$file_url?" . _mtime($file_path);
}
# 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.
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');
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 => [], alternate => {}, 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});
}
}
}
return \%by_type;
}
sub _css_link_set {
my ($file_name) = @_;
my $standard_mtime = _mtime($file_name);
my %set = (standard => $file_name . "?$standard_mtime");
# We use (^|/) to allow Extensions to use the skins system if they
# want.
if ($file_name !~ m{(^|/)skins/standard/}) {
return \%set;
}
my $user = Bugzilla->user;
my $cgi_path = bz_locations()->{'cgi_path'};
my $all_skins = $user->settings->{'skin'}->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} = $skin_file_name . "?$mtime";
}
}
$set{alternate} = \%skin_urls;
my $skin = $user->settings->{'skin'}->{'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/};
if (my $custom_mtime = _mtime("$cgi_path/$custom_file_name")) {
$set{custom} = $custom_file_name . "?$custom_mtime";
}
return \%set;
}
# YUI dependency resolution
sub yui_resolve_deps {
my ($yui, $yui_deps) = @_;
my @yui_resolved;
foreach my $yui_name (@$yui) {
my $deps = $yui_deps->{$yui_name} || [];
foreach my $dep (reverse @$deps) {
push(@yui_resolved, $dep) if !grep { $_ eq $dep } @yui_resolved;
}
push(@yui_resolved, $yui_name) if !grep { $_ eq $yui_name } @yui_resolved;
}
return \@yui_resolved;
}
############################################################################### ###############################################################################
# Templatization Code # Templatization Code
...@@ -647,6 +762,8 @@ sub create { ...@@ -647,6 +762,8 @@ sub create {
html_light => \&Bugzilla::Util::html_light_quote, html_light => \&Bugzilla::Util::html_light_quote,
email => \&Bugzilla::Util::email_filter, email => \&Bugzilla::Util::email_filter,
mtime_url => \&mtime_filter,
# iCalendar contentline filter # iCalendar contentline filter
ics => [ sub { ics => [ sub {
...@@ -769,6 +886,9 @@ sub create { ...@@ -769,6 +886,9 @@ sub create {
Bugzilla->fields({ by_name => 1 }); Bugzilla->fields({ by_name => 1 });
return $cache->{template_bug_fields}; return $cache->{template_bug_fields};
}, },
'css_files' => \&css_files,
yui_resolve_deps => \&yui_resolve_deps,
# Whether or not keywords are enabled, in this Bugzilla. # Whether or not keywords are enabled, in this Bugzilla.
'use_keywords' => sub { return Bugzilla::Keyword->any_exist; }, 'use_keywords' => sub { return Bugzilla::Keyword->any_exist; },
......
...@@ -1044,7 +1044,7 @@ max_allowed_packet=4M ...@@ -1044,7 +1044,7 @@ max_allowed_packet=4M
AddHandler cgi-script .cgi AddHandler cgi-script .cgi
Options +Indexes +ExecCGI Options +Indexes +ExecCGI
DirectoryIndex index.cgi DirectoryIndex index.cgi
AllowOverride Limit AllowOverride Limit FileInfo Indexes
&lt;/Directory&gt; &lt;/Directory&gt;
</programlisting> </programlisting>
......
...@@ -74,7 +74,7 @@ PerlChildInitHandler "sub { srand(); }" ...@@ -74,7 +74,7 @@ PerlChildInitHandler "sub { srand(); }"
$sizelimit $sizelimit
PerlOptions +ParseHeaders PerlOptions +ParseHeaders
Options +ExecCGI Options +ExecCGI
AllowOverride Limit AllowOverride Limit FileInfo Indexes
DirectoryIndex index.cgi index.html DirectoryIndex index.cgi index.html
</Directory> </Directory>
EOT EOT
......
There are three directories here, standard/, custom/, and contrib/.
standard/ holds the standard stylesheets. These are used no matter
what skin the user selects. If the user selects the "Classic" skin,
then *only* the standard/ stylesheets are used.
contrib/ holds "skins" that the user can select in their preferences.
skins are in directories, and they contain files with the same names
as the files in skins/standard/. Simply putting a new directory
into the contrib/ directory adds a new skin as an option in users'
preferences.
custom/ allows you to locally override the standard/ and contrib/ CSS.
If you put files into the custom/ directory with the same names as the CSS
files in skins/standard/, you can override the standard/ and contrib/
CSS. For example, if you want to override some CSS in
skins/standard/global.css, then you should create a file called "global.css"
in custom/ and put some CSS in it. The CSS you put into files in custom/ will
be used *in addition* to the CSS in skins/standard/ or the CSS in
skins/contrib/. It will apply to every skin.
...@@ -64,6 +64,14 @@ ...@@ -64,6 +64,14 @@
datatable => ['json', 'connection', 'datasource', 'element'], datatable => ['json', 'connection', 'datasource', 'element'],
} %] } %]
[%# These are JS URLs that are *always* on the page and come before
# every other JS URL.
#%]
[% SET starting_js_urls = [
"js/yui/yahoo-dom-event/yahoo-dom-event.js",
"js/yui/cookie/cookie-min.js",
] %]
[%# We should be able to set the default value of the header variable [%# We should be able to set the default value of the header variable
# to the value of the title variable using the DEFAULT directive, # to the value of the title variable using the DEFAULT directive,
...@@ -87,118 +95,33 @@ ...@@ -87,118 +95,33 @@
[% PROCESS 'global/setting-descs.none.tmpl' %] [% PROCESS 'global/setting-descs.none.tmpl' %]
[%# Set up the skin CSS cascade: [% SET yui = yui_resolve_deps(yui, yui_deps) %]
# 0. YUI CSS [% SET css_sets = css_files(style_urls, yui, yui_css) %]
# 1. Standard Bugzilla stylesheet set (persistent)
# 2. Standard Bugzilla stylesheet set (selectable)
# 3. All third-party "skin" stylesheet sets (selectable)
# 4. Page-specific styles
# 5. 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.
#%]
[% IF user.settings.skin.value != 'standard' %]
[% user_skin = user.settings.skin.value %]
[% END %]
[% style_urls.unshift('skins/standard/global.css') %]
[%# YUI dependency resolution %]
[%# We have to do this in a separate array, because modifying the
# existing array by unshift'ing dependencies confuses FOREACH.
#%]
[% SET yui_resolved = [] %]
[% FOREACH yui_name = yui %]
[% FOREACH yui_dep = yui_deps.${yui_name}.reverse %]
[% yui_resolved.push(yui_dep) IF NOT yui_resolved.contains(yui_dep) %]
[% END %]
[% yui_resolved.push(yui_name) IF NOT yui_resolved.contains(yui_name) %]
[% END %]
[% SET yui = yui_resolved %]
[%# YUI CSS %]
[% FOREACH yui_name = yui %]
[% IF yui_css.$yui_name %]
<link rel="stylesheet" type="text/css"
href="js/yui/assets/skins/sam/[%- yui_name FILTER html %].css">
[% END %]
[% END %]
[%# CSS cascade, part 1: Standard Bugzilla stylesheet set (persistent). [%# CSS cascade, part 1: Standard Bugzilla stylesheet set (persistent).
# Always present. # Always present.
#%] #%]
[% FOREACH style_url = style_urls %] [%# This allows people to switch back to the "Classic" skin if they
<link href="[% style_url FILTER html %]" # are in another skin.
rel="stylesheet" #%]
type="text/css"> <link href="[% 'skins/standard/global.css' FILTER mtime_url FILTER html %]"
rel="alternate stylesheet"
title="[% setting_descs.standard FILTER html %]">
[% FOREACH style_url = css_sets.standard %]
[% PROCESS format_css_link css_set_name = 'standard' %]
[% END %] [% END %]
<!--[if lte IE 7]>
[%# Internet Explorer treats [if IE] HTML comments as uncommented.
# Use it to import CSS fixes so that Bugzilla looks decent on IE 7
# and below.
#%]
<link href="skins/standard/IE-fixes.css"
rel="stylesheet"
type="text/css">
<![endif]-->
[%# CSS cascade, part 2: Standard Bugzilla stylesheet set (selectable) [%# CSS cascade, part 2 & 3: Third-party stylesheet set (selected and
# Present if skin selection is enabled. # selectable). All third-party skins are present as alternate
# stylesheets, even if they are not currently in use.
#%] #%]
[% IF user.settings.skin.is_enabled %] [% FOREACH style_url = css_sets.skin %]
[% FOREACH style_url = style_urls %] [% PROCESS format_css_link css_set_name = user.settings.skin.value %]
<link href="[% style_url FILTER html %]"
rel="[% 'alternate ' IF user_skin %]stylesheet"
title="[% setting_descs.standard FILTER html %]"
type="text/css">
[% END %]
<!--[if lte IE 7]>
[%# Internet Explorer treats [if IE] HTML comments as uncommented.
# Use it to import CSS fixes so that Bugzilla looks decent on IE 7
# and below.
#%]
<link href="skins/standard/IE-fixes.css"
rel="[% 'alternate ' IF user_skin %]stylesheet"
title="[% setting_descs.standard FILTER html %]"
type="text/css">
<![endif]-->
[% END %] [% END %]
[%# CSS cascade, part 3: Third-party stylesheet set (selectable). [% FOREACH alternate_skin = css_sets.alternate.keys %]
# All third-party skins are present if skin selection is enabled. [% FOREACH style_url = css_sets.alternate.$alternate_skin %]
# The admin-selected skin is always present. [% PROCESS format_css_link css_set_name = alternate_skin %]
#%]
[% FOREACH contrib_skin = user.settings.skin.legal_values %]
[% NEXT IF contrib_skin == 'standard' %]
[% NEXT UNLESS contrib_skin == user_skin
OR user.settings.skin.is_enabled %]
[% contrib_skin = contrib_skin FILTER url_quote %]
[% IF contrib_skin.match('\.css$') %]
[%# 1st skin variant: single-file stylesheet %]
<link href="[% "skins/contrib/$contrib_skin" %]"
rel="[% 'alternate ' UNLESS contrib_skin == user_skin %]stylesheet"
title="[% contrib_skin FILTER html %]"
type="text/css">
[% ELSE %]
[%# 2nd skin variant: stylesheet set %]
[% FOREACH style_url = style_urls %]
[% IF style_url.match('^skins/standard/') %]
<link href="[% style_url.replace('^skins/standard/',
"skins/contrib/$contrib_skin/") %]"
rel="[% 'alternate ' UNLESS contrib_skin == user_skin %]stylesheet"
title="[% contrib_skin FILTER html %]"
type="text/css">
[% END %]
[% END %]
<!--[if lte IE 7]>
[%# Internet Explorer treats [if IE] HTML comments as uncommented.
# Use it to import CSS fixes so that Bugzilla looks decent on IE 7
# and below.
#%]
<link href="skins/contrib/[% contrib_skin FILTER html %]/IE-fixes.css"
rel="[% 'alternate ' UNLESS contrib_skin == user_skin %]stylesheet"
title="[% contrib_skin FILTER html %]"
type="text/css">
<![endif]-->
[% END %] [% END %]
[% END %] [% END %]
...@@ -214,33 +137,19 @@ ...@@ -214,33 +137,19 @@
# Always present. Site administrators may override all other style # Always present. Site administrators may override all other style
# definitions, including skins, using custom stylesheets. # definitions, including skins, using custom stylesheets.
#%] #%]
[% FOREACH style_url = style_urls %] [% FOREACH style_url = css_sets.custom %]
[% IF style_url.match('^skins/standard/') %] [% PROCESS format_css_link css_set_name = 'standard' %]
<link href="[% style_url.replace('^skins/standard/', "skins/custom/")
FILTER html %]" rel="stylesheet" type="text/css">
[% END %]
[% END %] [% END %]
<!--[if lte IE 7]>
[%# Internet Explorer treats [if IE] HTML comments as uncommented.
# Use it to import CSS fixes so that Bugzilla looks decent on IE 7
# and below.
#%]
<link href="skins/custom/IE-fixes.css"
rel="stylesheet"
type="text/css">
<![endif]-->
[%# YUI Scripts %] [%# YUI Scripts %]
<script src="js/yui/yahoo-dom-event/yahoo-dom-event.js"
type="text/javascript"></script>
<script src="js/yui/cookie/cookie-min.js" type="text/javascript"></script>
[% FOREACH yui_name = yui %] [% FOREACH yui_name = yui %]
<script type="text/javascript" [% starting_js_urls.push("js/yui/$yui_name/${yui_name}.js") %]
src="js/yui/[% yui_name FILTER html %]/
[%- yui_name FILTER html %]-min.js"></script>
[% END %] [% END %]
[% starting_js_urls.push('js/global.js') %]
<script src="js/global.js" type="text/javascript"></script> [% FOREACH javascript_url = starting_js_urls %]
[% PROCESS format_js_link %]
[% END %]
<script type="text/javascript"> <script type="text/javascript">
<!-- <!--
...@@ -291,10 +200,8 @@ ...@@ -291,10 +200,8 @@
// --> // -->
</script> </script>
[% IF javascript_urls %] [% FOREACH javascript_url = javascript_urls %]
[% FOREACH javascript_url = javascript_urls %] [% PROCESS format_js_link %]
<script src="[% javascript_url FILTER html %]" type="text/javascript"></script>
[% END %]
[% END %] [% END %]
[%# this puts the live bookmark up on firefox for the Atom feed %] [%# this puts the live bookmark up on firefox for the Atom feed %]
...@@ -380,3 +287,41 @@ ...@@ -380,3 +287,41 @@
[% IF message %] [% IF message %]
<div id="message">[% message %]</div> <div id="message">[% message %]</div>
[% END %] [% END %]
[% BLOCK format_css_link %]
[% IF style_url.match('/IE-fixes\.css') %]
<!--[if lte IE 7]>
[%# Internet Explorer treats [if IE] HTML comments as uncommented.
# We use it to import CSS fixes so that Bugzilla looks decent on IE 7
# and below.
#%]
[% END %]
[% IF css_set_name == 'standard'
OR css_set_name == user.settings.skin.value
%]
[% SET css_rel = 'stylesheet' %]
[% SET css_set_display_name = setting_descs.${user.settings.skin.value}
|| user.settings.skin.value %]
[% ELSE %]
[% SET css_rel = 'alternate stylesheet' %]
[% SET css_set_display_name = setting_descs.$css_set_name || css_set_name %]
[% END %]
[% IF css_set_name == 'standard' %]
[% SET css_title_link = '' %]
[% ELSE %]
[% css_title_link = BLOCK ~%]
title="[% css_set_display_name FILTER html %]"
[% END %]
[% END %]
<link href="[% style_url FILTER html %]" rel="[% css_rel FILTER none %]"
type="text/css" [% css_title_link FILTER none %]>
[% '<![endif]-->' IF style_url.match('/IE-fixes\.css') %]
[% END %]
[% BLOCK format_js_link %]
<script type="text/javascript" src="[% javascript_url FILTER mtime_url FILTER html %]"></script>
[% END %]
...@@ -71,6 +71,8 @@ END ...@@ -71,6 +71,8 @@ END
feature_updates => 'Automatic Update Notifications', feature_updates => 'Automatic Update Notifications',
feature_xmlrpc => 'XML-RPC Interface', feature_xmlrpc => 'XML-RPC Interface',
file_remove => 'Removing ##name##...',
file_rename => 'Renaming ##from## to ##to##...',
header => "* This is Bugzilla ##bz_ver## on perl ##perl_ver##\n" header => "* This is Bugzilla ##bz_ver## on perl ##perl_ver##\n"
. "* Running on ##os_name## ##os_ver##", . "* Running on ##os_name## ##os_ver##",
install_all => <<EOT, install_all => <<EOT,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment