Code

gitweb: Introduce esc_html_match_hl and esc_html_hl_regions
[git.git] / gitweb / gitweb.perl
index 9ee58114d946a4fb751590b7bd74d15d1894aef2..f8c5b6a8b065b2f8391cccb61996501487e67cde 100755 (executable)
@@ -52,7 +52,7 @@ sub evaluate_uri {
        # as base URL.
        # Therefore, if we needed to strip PATH_INFO, then we know that we have
        # to build the base URL ourselves:
-       our $path_info = $ENV{"PATH_INFO"};
+       our $path_info = decode_utf8($ENV{"PATH_INFO"});
        if ($path_info) {
                if ($my_url =~ s,\Q$path_info\E$,, &&
                    $my_uri =~ s,\Q$path_info\E$,, &&
@@ -817,9 +817,9 @@ sub evaluate_query_params {
 
        while (my ($name, $symbol) = each %cgi_param_mapping) {
                if ($symbol eq 'opt') {
-                       $input_params{$name} = [ $cgi->param($symbol) ];
+                       $input_params{$name} = [ map { decode_utf8($_) } $cgi->param($symbol) ];
                } else {
-                       $input_params{$name} = $cgi->param($symbol);
+                       $input_params{$name} = decode_utf8($cgi->param($symbol));
                }
        }
 }
@@ -1715,6 +1715,47 @@ sub chop_and_escape_str {
        }
 }
 
+# Highlight selected fragments of string, using given CSS class,
+# and escape HTML.  It is assumed that fragments do not overlap.
+# Regions are passed as list of pairs (array references).
+#
+# Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
+# '<span class="mark">foo</span>bar'
+sub esc_html_hl_regions {
+       my ($str, $css_class, @sel) = @_;
+       return esc_html($str) unless @sel;
+
+       my $out = '';
+       my $pos = 0;
+
+       for my $s (@sel) {
+               $out .= esc_html(substr($str, $pos, $s->[0] - $pos))
+                       if ($s->[0] - $pos > 0);
+               $out .= $cgi->span({-class => $css_class},
+                                  esc_html(substr($str, $s->[0], $s->[1] - $s->[0])));
+
+               $pos = $s->[1];
+       }
+       $out .= esc_html(substr($str, $pos))
+               if ($pos < length($str));
+
+       return $out;
+}
+
+# highlight match (if any), and escape HTML
+sub esc_html_match_hl {
+       my ($str, $regexp) = @_;
+       return esc_html($str) unless defined $regexp;
+
+       my @matches;
+       while ($str =~ /$regexp/g) {
+               push @matches, [$-[0], $+[0]];
+       }
+       return esc_html($str) unless @matches;
+
+       return esc_html_hl_regions($str, 'match', @matches);
+}
+
 ## ----------------------------------------------------------------------
 ## functions returning short strings
 
@@ -2775,7 +2816,7 @@ sub git_populate_project_tagcloud {
        }
 
        my $cloud;
-       my $matched = $cgi->param('by_tag');
+       my $matched = $input_params{'ctag'};
        if (eval { require HTML::TagCloud; 1; }) {
                $cloud = HTML::TagCloud->new;
                foreach my $ctag (sort keys %ctags_lc) {
@@ -2987,6 +3028,10 @@ sub search_projects_list {
        return @$projlist
                unless ($tagfilter || $searchtext);
 
+       # searching projects require filling to be run before it;
+       fill_project_list_info($projlist,
+                              $tagfilter  ? 'ctags' : (),
+                              $searchtext ? ('path', 'descr') : ());
        my @projects;
  PROJECT:
        foreach my $pr (@$projlist) {
@@ -3744,7 +3789,7 @@ sub get_page_title {
 
        unless (defined $project) {
                if (defined $project_filter) {
-                       $title .= " - " . to_utf8($project_filter);
+                       $title .= " - projects in '" . esc_path($project_filter) . "'";
                }
                return $title;
        }
@@ -3906,7 +3951,7 @@ sub print_search_form {
                               -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
              $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
              " search:\n",
-             $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
+             $cgi->textfield(-name => "s", -value => $searchtext, -override => 1) . "\n" .
              "<span title=\"Extended regular expression\">" .
              $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
                             -checked => $search_use_regexp) .
@@ -5188,35 +5233,70 @@ sub git_project_search_form {
        print "</div>\n";
 }
 
-# fills project list info (age, description, owner, category, forks)
+# entry for given @keys needs filling if at least one of keys in list
+# is not present in %$project_info
+sub project_info_needs_filling {
+       my ($project_info, @keys) = @_;
+
+       # return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
+       foreach my $key (@keys) {
+               if (!exists $project_info->{$key}) {
+                       return 1;
+               }
+       }
+       return;
+}
+
+# fills project list info (age, description, owner, category, forks, etc.)
 # for each project in the list, removing invalid projects from
-# returned list
+# returned list, or fill only specified info.
+#
+# Invalid projects are removed from the returned list if and only if you
+# ask 'age' or 'age_string' to be filled, because they are the only fields
+# that run unconditionally git command that requires repository, and
+# therefore do always check if project repository is invalid.
+#
+# USAGE:
+# * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
+#   ensures that 'descr_long' and 'ctags' fields are filled
+# * @project_list = fill_project_list_info(\@project_list)
+#   ensures that all fields are filled (and invalid projects removed)
+#
 # NOTE: modifies $projlist, but does not remove entries from it
 sub fill_project_list_info {
-       my $projlist = shift;
+       my ($projlist, @wanted_keys) = @_;
        my @projects;
+       my $filter_set = sub { return @_; };
+       if (@wanted_keys) {
+               my %wanted_keys = map { $_ => 1 } @wanted_keys;
+               $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
+       }
 
        my $show_ctags = gitweb_check_feature('ctags');
  PROJECT:
        foreach my $pr (@$projlist) {
-               my (@activity) = git_get_last_activity($pr->{'path'});
-               unless (@activity) {
-                       next PROJECT;
+               if (project_info_needs_filling($pr, $filter_set->('age', 'age_string'))) {
+                       my (@activity) = git_get_last_activity($pr->{'path'});
+                       unless (@activity) {
+                               next PROJECT;
+                       }
+                       ($pr->{'age'}, $pr->{'age_string'}) = @activity;
                }
-               ($pr->{'age'}, $pr->{'age_string'}) = @activity;
-               if (!defined $pr->{'descr'}) {
+               if (project_info_needs_filling($pr, $filter_set->('descr', 'descr_long'))) {
                        my $descr = git_get_project_description($pr->{'path'}) || "";
                        $descr = to_utf8($descr);
                        $pr->{'descr_long'} = $descr;
                        $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
                }
-               if (!defined $pr->{'owner'}) {
+               if (project_info_needs_filling($pr, $filter_set->('owner'))) {
                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
                }
-               if ($show_ctags) {
+               if ($show_ctags &&
+                   project_info_needs_filling($pr, $filter_set->('ctags'))) {
                        $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
                }
-               if ($projects_list_group_categories && !defined $pr->{'category'}) {
+               if ($projects_list_group_categories &&
+                   project_info_needs_filling($pr, $filter_set->('category'))) {
                        my $cat = git_get_project_category($pr->{'path'}) ||
                                                           $project_list_default_category;
                        $pr->{'category'} = to_utf8($cat);
@@ -5345,19 +5425,20 @@ sub git_project_list_body {
 
        my $check_forks = gitweb_check_feature('forks');
        my $show_ctags  = gitweb_check_feature('ctags');
-       my $tagfilter = $show_ctags ? $cgi->param('by_tag') : undef;
+       my $tagfilter = $show_ctags ? $input_params{'ctag'} : undef;
        $check_forks = undef
                if ($tagfilter || $searchtext);
 
        # filtering out forks before filling info allows to do less work
        @projects = filter_forks_from_projects_list(\@projects)
                if ($check_forks);
-       @projects = fill_project_list_info(\@projects);
-       # searching projects require filling to be run before it
+       # search_projects_list pre-fills required info
        @projects = search_projects_list(\@projects,
                                         'searchtext' => $searchtext,
                                         'tagfilter'  => $tagfilter)
                if ($tagfilter || $searchtext);
+       # fill the rest
+       @projects = fill_project_list_info(\@projects);
 
        $order ||= $default_projects_order;
        $from = 0 unless defined $from;
@@ -6261,7 +6342,7 @@ sub git_tag {
 
 sub git_blame_common {
        my $format = shift || 'porcelain';
-       if ($format eq 'porcelain' && $cgi->param('js')) {
+       if ($format eq 'porcelain' && $input_params{'javascript'}) {
                $format = 'incremental';
                $action = 'blame_incremental'; # for page title etc
        }