This is an automated email from the git hooks/post-receive script.

Git pushed a commit to branch master
in repository fateserver.

commit a5920b91589250ec07afe2676ea67b1845d1623c
Author:     Kacper Michajłow <[email protected]>
AuthorDate: Thu Jul 30 01:05:53 2026 +0200
Commit:     Kacper Michajłow <[email protected]>
CommitDate: Wed Aug 12 00:42:16 2026 +0200

    index: allow "/" and "%" in search criteria
    
    CGI already percent-decodes each parameter, so decoding the query again
    turned an escaped "/" inside a value back into the "//" separator and tore
    the criterion in two.
    
    Pass one criterion per parameter instead: CGI splits on "&" before decoding,
    so a value can no longer be mistaken for a separator. Escape values as
    bytes, as uri_escape_utf8 double-encodes the bytes CGI and the report files
    actually hand out.
    
    Signed-off-by: Kacper Michajłow <[email protected]>
---
 index.cgi | 95 ++++++++++++++++++++++++++++++++++-----------------------------
 1 file changed, 52 insertions(+), 43 deletions(-)

diff --git a/index.cgi b/index.cgi
index 377651e..d3dc943 100755
--- a/index.cgi
+++ b/index.cgi
@@ -20,7 +20,7 @@ use warnings;
 
 use lib "/var/www/fateweb";
 
-use CGI qw/param/;
+use CGI qw/param multi_param/;
 use HTML::Entities;
 use FATE;
 use Time::Zone;
@@ -28,12 +28,26 @@ use URI::Escape;
 
 cgi_path_is_trustworthy;
 
-# Format for /?query= : /?query=type:value//type:value// (URI encoded).
-# Trailing // does not matter (i.e. may be added).
-# @queries contains an array of 'type:value' strings.
-# Every member of @queries can be further parsed with another simple
-# split(/:/, $this_query, 2);
-my @queries = split(/\/\//, uri_unescape scalar param 'query') if (param 
'query');
+# Format for /?filter= : /?filter=type:value, repeated once per criterion, as
+# an array of [type, value] pairs. CGI splits the parameters before decoding
+# them, so a value may contain any character. Decoding again here would let a
+# '/' in a value act as a separator.
+#
+# Links generated before this format used /?query=type:value//type:value//,
+# URI-encoded once more. Keep accepting those, a legacy separator cannot be
+# told apart from '//' in a value, which is why the new format has a new name.
+my @raw_queries = multi_param 'filter';
+if (!@raw_queries) {
+    my $legacy = scalar param 'query';
+    @raw_queries = split /\/\//, uri_unescape $legacy if defined $legacy;
+}
+my @queries;
+for my $this_query (@raw_queries) {
+    my ($type, $text) = split(/:/, $this_query, 2);
+    defined $text or next;
+    ($type) = $type =~ /^([a-z]{1,16})\z/ or next;
+    push @queries, [$type, $text];
+}
 
 my $sort = safeparam_sort;
 $sort = "subarch" if defined($sort) && $sort eq "arch";
@@ -55,8 +69,8 @@ for my $slot (@slots) {
     my $not_matched = 0;
     $$rep{subarch} = $$rep{arch} if not $$rep{subarch};
     for my $this_query (@queries) {
-        my ($type, $text) = split(/:/, $this_query, 2);
-        $not_matched = 1 if ($$rep{$type} ne $text);
+        my ($type, $text) = @$this_query;
+        $not_matched = 1 if not defined $$rep{$type} or $$rep{$type} ne $text;
     }
     next if $not_matched;
 
@@ -102,16 +116,29 @@ sub repcmp {
     return 0;
 };
 
-sub lsort {
-    my $params = '';
+# Every parameter except the criteria and, optionally, $skip, in HTTP format.
+# The criteria may occur several times and are regenerated by query_params().
+sub other_params {
+    my ($skip) = @_;
+    my @params;
     for my $thisparam (param) {
         next if $thisparam =~ /[^a-z0-9_]/;
-        next if $thisparam =~ 'sort';
-        $params .= '&' if $params ne '';
-        $params .= "$thisparam=" . uri_escape(param($thisparam));
+        next if $thisparam eq 'filter' or $thisparam eq 'query';
+        next if defined $skip and $thisparam eq $skip;
+        push @params, "$thisparam=" . uri_escape(scalar param($thisparam));
     }
-    $params .= '&' if $params;
+    return @params;
+}
+
+# Serialise [type, value] pairs as repeated 'filter' parameters.  Values are
+# bytes, as CGI and the report files give them, so escape them as bytes.
+sub query_params {
+    return map "filter=" . uri_escape("$$_[0]:" . ($$_[1] // '')), @_;
+}
+
+sub lsort {
     my ($text, $key) = @_;
+    my @params = (other_params('sort'), query_params(@queries));
 
     my $newkey = '';
     if ($sort eq $key) {                           # $key     = $sort
@@ -130,39 +157,21 @@ sub lsort {
     }
 
     $key = $newkey if $newkey ne '';
-    anchor $text, href => "?${params}sort=$key";
+    push @params, "sort=$key";
+    anchor $text, href => '?' . join '&amp;', @params;
 }
 
 sub category {
     my ($category, $rep) = @_;
-    my $head_printed = 0;
 
-    # $params will contain parameters else than query, if any, in HTTP format.
-    my $params = '';
-    for my $thisparam (param) {
-        next if $thisparam =~ /[^a-z0-9_]/;
-        next if $thisparam eq 'query';
-        $params .= '&' if $params ne '';
-        $params .= "$thisparam=" . uri_escape(param($thisparam));
-    }
-    my $head = ($params ? '&' : '') . 'query=';
-
-    if (@queries) {
-        for my $this_query (@queries) {
-            my ($type, $text) = split(/:/, $this_query, 2);
-            if ($type ne $category) {
-                $params .= $head if (!$head_printed);
-                $params .= $this_query . '//';
-                $head_printed = 1;
-            }
-        }
-    }
-    $params .= $head if (!$head_printed);
-    $params .= "$category:" . uri_escape_utf8 "$$rep{$category}" . '//';
-    $head_printed = 1;                 # for the sake of completeness
+    # Keep every search criterion except the one for $category, which the
+    # value in this cell replaces.
+    my @params = (other_params(),
+                  query_params(grep($$_[0] ne $category, @queries),
+                               [$category, $$rep{$category}]));
 
     start 'td';
-    anchor $$rep{$category}, href => "?$params";
+    anchor $$rep{$category}, href => '?' . join '&amp;', @params;
     end 'td';
 }
 
@@ -197,8 +206,8 @@ if (@queries) {
     start 'p';
     print 'Search patterns: ';
     for my $this_query (@queries) {
-        my ($type, $text) = split(/:/, $this_query, 2);
-        print "$type: $text; ";
+        my ($type, $text) = @$this_query;
+        print encode_entities("$type: $text", '<>&"'), '; ';
     }
     anchor 'clear all.', href => "";
     end 'p';

_______________________________________________
ffmpeg-cvslog mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to