<FilesMatch ^(.*\.pm|.*\.pl|.*localconfig.*)$>
deny from all
</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>
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
# from all other files, it gets its very own hash.
my %index_html = (
print "Removing duplicates directory...\n";
rmtree("$datadir/duplicates");
}
+
+ _remove_empty_css_files();
+ _convert_single_file_skins();
}
-# A simple helper for creating "empty" CSS files.
-sub _add_custom_css {
- my ($skin_dir, $path, $create_files) = @_;
- $create_files->{"$skin_dir/$path"} = { perms => WS_SERVE, contents => <<EOT
+sub _remove_empty_css_files {
+ my $skinsdir = bz_locations()->{'skinsdir'};
+ foreach my $css_file (glob("$skinsdir/custom/*.css"),
+ 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.
*/
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 {
_create_files(%{FILESYSTEM()->{htaccess}});
sub _rename_file {
my ($from, $to) = @_;
- print "Renaming $from to $to...\n";
+ print install_string('file_rename', { from => $from, to => $to }), "\n";
if (-e $to) {
warn "$to already exists, not moving\n";
}
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
html_light => \&Bugzilla::Util::html_light_quote,
email => \&Bugzilla::Util::email_filter,
+
+ mtime_url => \&mtime_filter,
# iCalendar contentline filter
ics => [ sub {
{ map { $_->name => $_ } Bugzilla->get_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.
'use_keywords' => sub { return Bugzilla::Keyword->any_exist; },
AddHandler cgi-script .cgi
Options +Indexes +ExecCGI
DirectoryIndex index.cgi
- AllowOverride Limit
+ AllowOverride Limit FileInfo Indexes
</Directory>
</programlisting>
$sizelimit
PerlOptions +ParseHeaders
Options +ExecCGI
- AllowOverride Limit
+ AllowOverride Limit FileInfo Indexes
DirectoryIndex index.cgi index.html
</Directory>
EOT
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
# to the value of the title variable using the DEFAULT directive,
[% PROCESS 'global/setting-descs.none.tmpl' %]
- [%# Set up the skin CSS cascade:
- # 0. 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 %]
+ [% SET yui = yui_resolve_deps(yui, yui_deps) %]
+ [% SET css_sets = css_files(style_urls, yui, yui_css) %]
[%# CSS cascade, part 1: Standard Bugzilla stylesheet set (persistent).
# Always present.
#%]
- [% FOREACH style_url = style_urls %]
- <link href="[% style_url FILTER html %]"
- rel="stylesheet"
- type="text/css">
+ [%# This allows people to switch back to the "Classic" skin if they
+ # are in another skin.
+ #%]
+ <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 %]
- <!--[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)
- # Present if skin selection is enabled.
+ [%# CSS cascade, part 2 & 3: Third-party stylesheet set (selected and
+ # 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 = style_urls %]
- <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]-->
+ [% FOREACH style_url = css_sets.skin %]
+ [% PROCESS format_css_link css_set_name = user.settings.skin.value %]
[% END %]
- [%# CSS cascade, part 3: Third-party stylesheet set (selectable).
- # All third-party skins are present if skin selection is enabled.
- # The admin-selected skin is always present.
- #%]
- [% 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]-->
+ [% FOREACH alternate_skin = css_sets.alternate.keys %]
+ [% FOREACH style_url = css_sets.alternate.$alternate_skin %]
+ [% PROCESS format_css_link css_set_name = alternate_skin %]
[% END %]
[% END %]
# Always present. Site administrators may override all other style
# definitions, including skins, using custom stylesheets.
#%]
- [% FOREACH style_url = style_urls %]
- [% IF style_url.match('^skins/standard/') %]
- <link href="[% style_url.replace('^skins/standard/', "skins/custom/")
- FILTER html %]" rel="stylesheet" type="text/css">
- [% END %]
+ [% FOREACH style_url = css_sets.custom %]
+ [% PROCESS format_css_link css_set_name = 'standard' %]
[% 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 %]
- <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 %]
- <script type="text/javascript"
- src="js/yui/[% yui_name FILTER html %]/
- [%- yui_name FILTER html %]-min.js"></script>
+ [% starting_js_urls.push("js/yui/$yui_name/${yui_name}.js") %]
[% 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>
- [% IF javascript_urls %]
- [% FOREACH javascript_url = javascript_urls %]
- <script src="[% javascript_url FILTER html %]" type="text/javascript"></script>
- [% END %]
+ [% FOREACH javascript_url = javascript_urls %]
+ [% PROCESS format_js_link %]
[% END %]
[%# this puts the live bookmark up on firefox for the Atom feed %]
[% IF message %]
<div id="message">[% message %]</div>
[% 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 %]
feature_updates => 'Automatic Update Notifications',
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"
. "* Running on ##os_name## ##os_ver##",
install_all => <<EOT,