From ba5a5c404b8ef0b71efd873e604184ab90ebff94 Mon Sep 17 00:00:00 2001 From: "mkanat%bugzilla.org" <> Date: Sat, 17 Mar 2007 21:13:34 +0000 Subject: Bug 374227: Create a system for localizing basic installation strings Patch By Max Kanat-Alexander (module owner) a=mkanat --- Bugzilla/Install/Util.pm | 253 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 250 insertions(+), 3 deletions(-) (limited to 'Bugzilla/Install') diff --git a/Bugzilla/Install/Util.pm b/Bugzilla/Install/Util.pm index 16f8f3e04..b02c435e5 100644 --- a/Bugzilla/Install/Util.pm +++ b/Bugzilla/Install/Util.pm @@ -28,18 +28,20 @@ use strict; use Bugzilla::Constants; +use File::Basename; use POSIX (); +use Safe; use base qw(Exporter); our @EXPORT_OK = qw( display_version_and_os indicate_progress + install_string vers_cmp ); sub display_version_and_os { # Display version information - printf "\n* This is Bugzilla " . BUGZILLA_VERSION . " on perl %vd\n", $^V; my @os_details = POSIX::uname; # 0 is the name of the OS, 2 is the major version, my $os_name = $os_details[0] . ' ' . $os_details[2]; @@ -47,8 +49,12 @@ sub display_version_and_os { require Win32; $os_name = Win32::GetOSName(); } - # 3 is the minor version. - print "* Running on $os_name $os_details[3]\n" + # $os_details[3] is the minor version. + print install_string('version_and_os', { bz_ver => BUGZILLA_VERSION, + perl_ver => sprintf('%vd', $^V), + os_name => $os_name, + os_ver => $os_details[3] }) + . "\n"; } sub indicate_progress { @@ -63,6 +69,126 @@ sub indicate_progress { } } +sub install_string { + my ($string_id, $vars) = @_; + _cache()->{template_include_path} ||= template_include_path(); + my $path = _cache()->{template_include_path}; + + my $string_template; + # Find the first set of templates that defines this string. + foreach my $dir (@$path) { + my $file = "$dir/setup/strings.txt.pl"; + next unless -e $file; + my $safe = new Safe; + $safe->rdo($file); + my %strings = %{$safe->varglob('strings')}; + $string_template = $strings{$string_id}; + last if $string_template; + } + + die "No language defines the string '$string_id'" if !$string_template; + + $vars ||= {}; + my @replace_keys = keys %$vars; + foreach my $key (@replace_keys) { + my $replacement = $vars->{$key}; + die "'$key' in '$string_id' is tainted: '$replacement'" + if is_tainted($replacement); + # We don't want people to start getting clever and inserting + # ##variable## into their values. So we check if any other + # key is listed in the *replacement* string, before doing + # the replacement. This is mostly to protect programmers from + # making mistakes. + if (grep($replacement =~ /##$key##/, @replace_keys)) { + die "Unsafe replacement for '$key' in '$string_id': '$replacement'"; + } + $string_template =~ s/\Q##$key##\E/$replacement/g; + } + + return $string_template; +} + +sub template_include_path { + my ($params) = @_; + $params ||= {}; + + # Basically, the way this works is that we have a list of languages + # that we *want*, and a list of languages that Bugzilla actually + # supports. The caller tells us what languages they want, by setting + # $ENV{HTTP_ACCEPT_LANGUAGE} or $params->{only_language}. The languages + # we support are those specified in $params->{use_languages}. Otherwise + # we support every language installed in the template/ directory. + + my @wanted; + if (defined $params->{only_language}) { + @wanted = ($params->{only_language}); + } + else { + @wanted = _sort_accept_language($ENV{'HTTP_ACCEPT_LANGUAGE'} || ''); + } + + my @supported; + if (defined $params->{use_languages}) { + @supported = $params->{use_languages}; + } + else { + my @dirs = glob(bz_locations()->{'templatedir'} . "/*"); + @dirs = map(basename($_), @dirs); + @supported = grep($_ ne 'CVS', @dirs); + } + + my @usedlanguages; + foreach my $wanted (@wanted) { + # If we support the language we want, or *any version* of + # the language we want, it gets pushed into @usedlanguages. + # + # Per RFC 1766 and RFC 2616, things like 'en' match 'en-us' and + # 'en-uk', but not the other way around. (This is unfortunately + # not very clearly stated in those RFC; see comment just over 14.5 + # in http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4) + if(my @found = grep /^\Q$wanted\E(-.+)?$/i, @supported) { + push (@usedlanguages, @found); + } + } + + # If we didn't want *any* of the languages we support, just use all + # of the languages we said we support, in the order they were specified. + # This is only done when you ask for a certain set of languages, because + # otherwise @supported just came off the disk in alphabetical order, + # and it could give you de (German) when you speak English. + # (If @supported came off the disk, we fall back on English if no language + # is available--that happens below.) + if (!@usedlanguages && $params->{use_languages}) { + @usedlanguages = @supported; + } + + # We always include English at the bottom if it's not there, even if + # somebody removed it from use_languages. + if (!grep($_ eq 'en', @usedlanguages)) { + push(@usedlanguages, 'en'); + } + + # Now, we add template directories in the order they will be searched: + + # First, we add extension template directories, because extension templates + # override standard templates. Extensions may be localized in the same way + # that Bugzilla templates are localized. + my @include_path; + my @extensions = glob(bz_locations()->{'extensionsdir'} . "/*"); + foreach my $extension (@extensions) { + foreach my $lang (@usedlanguages) { + _add_language_set(\@include_path, $lang, "$extension/template"); + } + } + + # Then, we add normal template directories, sorted by language. + foreach my $lang (@usedlanguages) { + _add_language_set(\@include_path, $lang); + } + + return \@include_path; +} + # This is taken straight from Sort::Versions 1.5, which is not included # with perl by default. sub vers_cmp { @@ -106,6 +232,81 @@ sub vers_cmp { @A <=> @B; } +###################### +# Helper Subroutines # +###################### + +# Used by template_include_path. +sub _add_language_set { + my ($array, $lang, $templatedir) = @_; + + $templatedir ||= bz_locations()->{'templatedir'}; + my @add = ("$templatedir/$lang/custom", "$templatedir/$lang/default"); + + my $project = bz_locations->{'project'}; + push(@add, "$templatedir/$lang/$project") if $project; + + foreach my $dir (@add) { + #if (-d $dir) { + trick_taint($dir); + push(@$array, $dir); + #} + } +} + +# Make an ordered list out of a HTTP Accept-Language header (see RFC 2616, 14.4) +# We ignore '*' and ;q=0 +# For languages with the same priority q the order remains unchanged. +sub _sort_accept_language { + sub sortQvalue { $b->{'qvalue'} <=> $a->{'qvalue'} } + my $accept_language = $_[0]; + + # clean up string. + $accept_language =~ s/[^A-Za-z;q=0-9\.\-,]//g; + my @qlanguages; + my @languages; + foreach(split /,/, $accept_language) { + if (m/([A-Za-z\-]+)(?:;q=(\d(?:\.\d+)))?/) { + my $lang = $1; + my $qvalue = $2; + $qvalue = 1 if not defined $qvalue; + next if $qvalue == 0; + $qvalue = 1 if $qvalue > 1; + push(@qlanguages, {'qvalue' => $qvalue, 'language' => $lang}); + } + } + + return map($_->{'language'}, (sort sortQvalue @qlanguages)); +} + + +# This is like request_cache, but it's used only by installation code +# for setup.cgi and things like that. +our $_cache = {}; +sub _cache { + if ($ENV{MOD_PERL}) { + require Apache2::RequestUtil; + return Apache2::RequestUtil->request->pnotes(); + } + return $_cache; +} + +############################### +# Copied from Bugzilla::Util # +############################## + +sub trick_taint { + require Carp; + Carp::confess("Undef to trick_taint") unless defined $_[0]; + my $match = $_[0] =~ /^(.*)$/s; + $_[0] = $match ? $1 : undef; + return (defined($_[0])); +} + +sub is_tainted { + return not eval { my $foo = join('',@_), kill 0; 1; }; +} + __END__ =head1 NAME @@ -173,6 +374,52 @@ ten items. Defaults to 1 if not specified. =back +=item C + +=over + +=item B + +This is a very simple method of templating strings for installation. +It should only be used by code that has to run before the Template Toolkit +can be used. (See the comments at the top of the various L +modules to find out when it's safe to use Template Toolkit.) + +It pulls strings out of the F "template" and replaces +any variable surrounded by double-hashes (##) with a value you specify. + +This allows for localization of strings used during installation. + +=item B + +Let's say your template string looks like this: + + The ##animal## jumped over the ##plant##. + +Let's say that string is called 'animal_jump_plant'. So you call the function +like this: + + install_string('animal_jump_plant', { animal => 'fox', plant => 'tree' }); + +That will output this: + + The fox jumped over the tree. + +=item B + +=over + +=item C<$string_id> - The name of the string from F. + +=item C<$vars> - A hashref containing the replacement values for variables +inside of the string. + +=back + +=item B: The appropriate string, with variables replaced. + +=back + =item C =over -- cgit v1.2.3-24-g4f1b