X-Git-Url: https://git.ladys.computer/Gitweb/blobdiff_plain/f587ef9f50ee30c1e1944ef9d0dfac0708bcd4b3a6722b0c5ebb1458443a077c..74043e29c6da3430abdef8d5ab279ad96a2000614c0489c39220e3eea82bd699:/gitweb.perl diff --git a/gitweb.perl b/gitweb.perl index 4be6c18..22df906 100755 --- a/gitweb.perl +++ b/gitweb.perl @@ -27,13 +27,29 @@ our $version = "++GIT_VERSION++"; our $my_url = $cgi->url(); our $my_uri = $cgi->url(-absolute => 1); -# if we're called with PATH_INFO, we have to strip that -# from the URL to find our real URL -# we make $path_info global because it's also used later on +# Base URL for relative URLs in gitweb ($logo, $favicon, ...), +# needed and used only for URLs with nonempty PATH_INFO +our $base_url = $my_url; + +# When the script is used as DirectoryIndex, the URL does not contain the name +# of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we +# have to do it ourselves. We make $path_info global because it's also used +# later on. +# +# Another issue with the script being the DirectoryIndex is that the resulting +# $my_url data is not the full script URL: this is good, because we want +# generated links to keep implying the script name if it wasn't explicitly +# indicated in the URL we're handling, but it means that $my_url cannot be used +# 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"}; if ($path_info) { - $my_url =~ s,\Q$path_info\E$,,; - $my_uri =~ s,\Q$path_info\E$,,; + if ($my_url =~ s,\Q$path_info\E$,, && + $my_uri =~ s,\Q$path_info\E$,, && + defined $ENV{'SCRIPT_NAME'}) { + $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'}; + } } # core git executable to use @@ -133,6 +149,10 @@ our $fallback_encoding = 'latin1'; # - one might want to include '-B' option, e.g. '-B', '-M' our @diff_opts = ('-M'); # taken from git_commit +# Disables features that would allow repository owners to inject script into +# the gitweb domain. +our $prevent_xss = 0; + # information about snapshot formats that gitweb is capable of serving our %known_snapshot_formats = ( # name => { @@ -204,7 +224,7 @@ our %feature = ( # $feature{'blame'}{'override'} = 1; # and in project config gitweb.blame = 0|1; 'blame' => { - 'sub' => \&feature_blame, + 'sub' => sub { feature_bool('blame', @_) }, 'override' => 0, 'default' => [0]}, @@ -242,7 +262,7 @@ our %feature = ( # $feature{'grep'}{'override'} = 1; # and in project config gitweb.grep = 0|1; 'grep' => { - 'sub' => \&feature_grep, + 'sub' => sub { feature_bool('grep', @_) }, 'override' => 0, 'default' => [1]}, @@ -256,7 +276,7 @@ our %feature = ( # $feature{'pickaxe'}{'override'} = 1; # and in project config gitweb.pickaxe = 0|1; 'pickaxe' => { - 'sub' => \&feature_pickaxe, + 'sub' => sub { feature_bool('pickaxe', @_) }, 'override' => 0, 'default' => [1]}, @@ -331,6 +351,21 @@ our %feature = ( 'ctags' => { 'override' => 0, 'default' => [0]}, + + # The maximum number of patches in a patchset generated in patch + # view. Set this to 0 or undef to disable patch view, or to a + # negative number to remove any limit. + + # To disable system wide have in $GITWEB_CONFIG + # $feature{'patches'}{'default'} = [0]; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'patches'}{'override'} = 1; + # and in project config gitweb.patches = 0|n; + # where n is the maximum number of patches allowed in a patchset. + 'patches' => { + 'sub' => \&feature_patches, + 'override' => 0, + 'default' => [16]}, ); sub gitweb_get_feature { @@ -364,16 +399,17 @@ sub gitweb_check_feature { } -sub feature_blame { - my ($val) = git_get_project_config('blame', '--bool'); +sub feature_bool { + my $key = shift; + my ($val) = git_get_project_config($key, '--bool'); - if ($val eq 'true') { - return 1; + if (!defined $val) { + return ($_[0]); + } elsif ($val eq 'true') { + return (1); } elsif ($val eq 'false') { - return 0; + return (0); } - - return $_[0]; } sub feature_snapshot { @@ -388,25 +424,11 @@ sub feature_snapshot { return @fmts; } -sub feature_grep { - my ($val) = git_get_project_config('grep', '--bool'); +sub feature_patches { + my @val = (git_get_project_config('patches', '--int')); - if ($val eq 'true') { - return (1); - } elsif ($val eq 'false') { - return (0); - } - - return ($_[0]); -} - -sub feature_pickaxe { - my ($val) = git_get_project_config('pickaxe', '--bool'); - - if ($val eq 'true') { - return (1); - } elsif ($val eq 'false') { - return (0); + if (@val) { + return @val; } return ($_[0]); @@ -505,6 +527,8 @@ our %actions = ( "heads" => \&git_heads, "history" => \&git_history, "log" => \&git_log, + "patch" => \&git_patch, + "patches" => \&git_patches, "rss" => \&git_rss, "atom" => \&git_atom, "search" => \&git_search, @@ -665,10 +689,10 @@ sub evaluate_path_info { # extensions. Allowed extensions are both the defined suffix # (which includes the initial dot already) and the snapshot # format key itself, with a prepended dot - while (my ($fmt, %opt) = each %known_snapshot_formats) { + while (my ($fmt, $opt) = each %known_snapshot_formats) { my $hash = $refname; my $sfx; - $hash =~ s/(\Q$opt{'suffix'}\E|\Q.$fmt\E)$//; + $hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//; next unless $sfx = $1; # a valid suffix was found, so set the snapshot format # and reset the hash parameter @@ -815,7 +839,7 @@ exit; ## ====================================================================== ## action links -sub href (%) { +sub href { my %params = @_; # default is to use -absolute url() i.e. $my_uri my $href = $params{-full} ? $my_url : $my_uri; @@ -831,7 +855,7 @@ sub href (%) { } my $use_pathinfo = gitweb_check_feature('pathinfo'); - if ($use_pathinfo) { + if ($use_pathinfo and defined $params{'project'}) { # try to put as many parameters as possible in PATH_INFO: # - project name # - action @@ -846,7 +870,7 @@ sub href (%) { $href =~ s,/$,,; # Then add the project name, if present - $href .= "/".esc_url($params{'project'}) if defined $params{'project'}; + $href .= "/".esc_url($params{'project'}); delete $params{'project'}; # since we destructively absorb parameters, we keep this @@ -1013,7 +1037,7 @@ sub esc_url { } # replace invalid utf8 character with SUBSTITUTION sequence -sub esc_html ($;%) { +sub esc_html { my $str = shift; my %opts = @_; @@ -1273,7 +1297,7 @@ use constant { }; # submodule/subproject, a commit object reference -sub S_ISGITLINK($) { +sub S_ISGITLINK { my $mode = shift; return (($mode & S_IFMT) == S_IFGITLINK) @@ -1361,13 +1385,11 @@ sub format_log_line_html { my $line = shift; $line = esc_html($line, -nbsp=>1); - if ($line =~ m/([0-9a-fA-F]{8,40})/) { - my $hash_text = $1; - my $link = - $cgi->a({-href => href(action=>"object", hash=>$hash_text), - -class => "text"}, $hash_text); - $line =~ s/$hash_text/$link/; - } + $line =~ s{\b([0-9a-fA-F]{8,40})\b}{ + $cgi->a({-href => href(action=>"object", hash=>$1), + -class => "text"}, $1); + }eg; + return $line; } @@ -1891,18 +1913,19 @@ sub git_parse_project_config { return %config; } -# convert config value to boolean, 'true' or 'false' +# convert config value to boolean: 'true' or 'false' # no value, number > 0, 'true' and 'yes' values are true # rest of values are treated as false (never as error) sub config_to_bool { my $val = shift; + return 1 if !defined $val; # section.key + # strip leading and trailing whitespace $val =~ s/^\s+//; $val =~ s/\s+$//; - return (!defined $val || # section.key - ($val =~ /^\d+$/ && $val) || # section.key = 1 + return (($val =~ /^\d+$/ && $val) || # section.key = 1 ($val =~ /^(?:true|yes)$/i)); # section.key = true } @@ -1955,6 +1978,9 @@ sub git_get_project_config { $config_file = "$git_dir/config"; } + # check if config variable (key) exists + return unless exists $config{"gitweb.$key"}; + # ensure given type if (!defined $type) { return $config{"gitweb.$key"}; @@ -2040,18 +2066,17 @@ sub git_get_project_ctags { my $ctags = {}; $git_dir = "$projectroot/$path"; - unless (opendir D, "$git_dir/ctags") { - return $ctags; - } - foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir(D)) { - open CT, $_ or next; - my $val = ; + opendir my $dh, "$git_dir/ctags" + or return $ctags; + foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh)) { + open my $ct, $_ or next; + my $val = <$ct>; chomp $val; - close CT; + close $ct; my $ctag = $_; $ctag =~ s#.*/##; $ctags->{$ctag} = $val; } - closedir D; + closedir $dh; $ctags; } @@ -2148,8 +2173,9 @@ sub git_get_projects_list { my $subdir = substr($File::Find::name, $pfxlen + 1); # we check related file in $projectroot - if (check_export_ok("$projectroot/$filter/$subdir")) { - push @list, { path => ($filter ? "$filter/" : '') . $subdir }; + my $path = ($filter ? "$filter/" : '') . $subdir; + if (check_export_ok("$projectroot/$path")) { + push @list, { path => $path }; $File::Find::prune = 1; } }, @@ -2589,7 +2615,7 @@ sub parsed_difftree_line { } # parse line of git-ls-tree output -sub parse_ls_tree_line ($;%) { +sub parse_ls_tree_line { my $line = shift; my %opts = @_; my %res; @@ -2778,18 +2804,18 @@ sub mimetype_guess_file { -r $mimemap or return undef; my %mimemap; - open(MIME, $mimemap) or return undef; - while () { + open(my $mh, $mimemap) or return undef; + while (<$mh>) { next if m/^#/; # skip comments - my ($mime, $exts) = split(/\t+/); + my ($mimetype, $exts) = split(/\t+/); if (defined $exts) { my @exts = split(/\s+/, $exts); foreach my $ext (@exts) { - $mimemap{$ext} = $mime; + $mimemap{$ext} = $mimetype; } } } - close(MIME); + close($mh); $filename =~ /\.([^.]*)$/; return $mimemap{$1}; @@ -2897,9 +2923,14 @@ sub git_header_html { $title EOF -# print out each stylesheet that exist + # the stylesheet, favicon etc urls won't work correctly with path_info + # unless we set the appropriate base URL + if ($ENV{'PATH_INFO'}) { + print "\n"; + } + # print out each stylesheet that exist, providing backwards capability + # for those people who defined $stylesheet in a config file if (defined $stylesheet) { -#provides backwards capability for those people who define style sheet in a config file print ''."\n"; } else { foreach my $stylesheet (@stylesheets) { @@ -3182,7 +3213,6 @@ sub git_print_header_div { "\n\n"; } -#sub git_print_authorship (\%) { sub git_print_authorship { my $co = shift; @@ -3238,8 +3268,7 @@ sub git_print_page_path { print "
\n"; } -# sub git_print_log (\@;%) { -sub git_print_log ($;%) { +sub git_print_log { my $log = shift; my %opts = @_; @@ -4494,7 +4523,9 @@ sub git_summary { print "\n"; - if (-s "$projectroot/$project/README.html") { + # If XSS prevention is on, we don't include README.html. + # TODO: Allow a readme in some safe format. + if (!$prevent_xss && -s "$projectroot/$project/README.html") { print "
readme
\n" . "
\n"; insert_file("$projectroot/$project/README.html"); @@ -4667,11 +4698,17 @@ HTML esc_html($short_rev)); print "\n"; } - open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^") - or die_error(500, "Open git-rev-parse failed"); - my $parent_commit = <$dd>; - close $dd; - chomp($parent_commit); + my $parent_commit; + if (!exists $meta->{'parent'}) { + open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^") + or die_error(500, "Open git-rev-parse failed"); + $parent_commit = <$dd>; + close $dd; + chomp($parent_commit); + $meta->{'parent'} = $parent_commit; + } else { + $parent_commit = $meta->{'parent'}; + } my $blamed = href(action => 'blame', file_name => $meta->{'filename'}, hash_base => $parent_commit); @@ -4749,10 +4786,21 @@ sub git_blob_plain { $save_as .= '.txt'; } + # With XSS prevention on, blobs of all types except a few known safe + # ones are served with "Content-Disposition: attachment" to make sure + # they don't run in our security domain. For certain image types, + # blob view writes an tag referring to blob_plain view, and we + # want to be sure not to break that by serving the image as an + # attachment (though Firefox 3 doesn't seem to care). + my $sandbox = $prevent_xss && + $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!; + print $cgi->header( -type => $type, -expires => $expires, - -content_disposition => 'inline; filename="' . $save_as . '"'); + -content_disposition => + ($sandbox ? 'attachment' : 'inline') + . '; filename="' . $save_as . '"'); undef $/; binmode STDOUT, ':raw'; print <$fd>; @@ -5008,6 +5056,15 @@ sub git_log { my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100); + my ($patch_max) = gitweb_get_feature('patches'); + if ($patch_max) { + if ($patch_max < 0 || @commitlist <= $patch_max) { + $paging_nav .= " ⋅ " . + $cgi->a({-href => href(action=>"patches", -replay=>1)}, + "patches"); + } + } + git_header_html(); git_print_page_nav('log','', $hash,undef,undef, $paging_nav); @@ -5087,6 +5144,11 @@ sub git_commit { } @$parents ) . ')'; } + if (gitweb_check_feature('patches')) { + $formats_nav .= " | " . + $cgi->a({-href => href(action=>"patch", -replay=>1)}, + "patch"); + } if (!defined $parent) { $parent = "--root"; @@ -5296,43 +5358,9 @@ sub git_blobdiff { or die_error(500, "Open git-diff-tree failed"); } - # old/legacy style URI - if (!%diffinfo && # if new style URI failed - defined $hash && defined $hash_parent) { - # fake git-diff-tree raw output - $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob"; - $diffinfo{'from_id'} = $hash_parent; - $diffinfo{'to_id'} = $hash; - if (defined $file_name) { - if (defined $file_parent) { - $diffinfo{'status'} = '2'; - $diffinfo{'from_file'} = $file_parent; - $diffinfo{'to_file'} = $file_name; - } else { # assume not renamed - $diffinfo{'status'} = '1'; - $diffinfo{'from_file'} = $file_name; - $diffinfo{'to_file'} = $file_name; - } - } else { # no filename given - $diffinfo{'status'} = '2'; - $diffinfo{'from_file'} = $hash_parent; - $diffinfo{'to_file'} = $hash; - } - - # non-textual hash id's can be cached - if ($hash =~ m/^[0-9a-fA-F]{40}$/ && - $hash_parent =~ m/^[0-9a-fA-F]{40}$/) { - $expires = '+1d'; - } - - # open patch output - open $fd, "-|", git_cmd(), "diff", @diff_opts, - '-p', ($format eq 'html' ? "--full-index" : ()), - $hash_parent, $hash, "--" - or die_error(500, "Open git-diff failed"); - } else { - die_error(400, "Missing one of the blob diff parameters") - unless %diffinfo; + # old/legacy style URI -- not generated anymore since 1.4.3. + if (!%diffinfo) { + die_error('404 Not Found', "Missing one of the blob diff parameters") } # header @@ -5397,7 +5425,14 @@ sub git_blobdiff_plain { } sub git_commitdiff { - my $format = shift || 'html'; + my %params = @_; + my $format = $params{-format} || 'html'; + + my ($patch_max) = gitweb_get_feature('patches'); + if ($format eq 'patch') { + die_error(403, "Patch view not allowed") unless $patch_max; + } + $hash ||= $hash_base || "HEAD"; my %co = parse_commit($hash) or die_error(404, "Unknown commit object"); @@ -5412,6 +5447,11 @@ sub git_commitdiff { $formats_nav = $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)}, "raw"); + if ($patch_max) { + $formats_nav .= " | " . + $cgi->a({-href => href(action=>"patch", -replay=>1)}, + "patch"); + } if (defined $hash_parent && $hash_parent ne '-c' && $hash_parent ne '--cc') { @@ -5495,7 +5535,31 @@ sub git_commitdiff { open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, '-p', $hash_parent_param, $hash, "--" or die_error(500, "Open git-diff-tree failed"); - + } elsif ($format eq 'patch') { + # For commit ranges, we limit the output to the number of + # patches specified in the 'patches' feature. + # For single commits, we limit the output to a single patch, + # diverging from the git-format-patch default. + my @commit_spec = (); + if ($hash_parent) { + if ($patch_max > 0) { + push @commit_spec, "-$patch_max"; + } + push @commit_spec, '-n', "$hash_parent..$hash"; + } else { + if ($params{-single}) { + push @commit_spec, '-1'; + } else { + if ($patch_max > 0) { + push @commit_spec, "-$patch_max"; + } + push @commit_spec, "-n"; + } + push @commit_spec, '--root', $hash; + } + open $fd, "-|", git_cmd(), "format-patch", '--encoding=utf8', + '--stdout', @commit_spec + or die_error(500, "Open git-format-patch failed"); } else { die_error(400, "Unknown commitdiff format"); } @@ -5544,6 +5608,14 @@ sub git_commitdiff { print to_utf8($line) . "\n"; } print "---\n\n"; + } elsif ($format eq 'patch') { + my $filename = basename($project) . "-$hash.patch"; + + print $cgi->header( + -type => 'text/plain', + -charset => 'utf-8', + -expires => $expires, + -content_disposition => 'inline; filename="' . "$filename" . '"'); } # write patch @@ -5565,11 +5637,25 @@ sub git_commitdiff { print <$fd>; close $fd or print "Reading git-diff-tree failed\n"; + } elsif ($format eq 'patch') { + local $/ = undef; + print <$fd>; + close $fd + or print "Reading git-format-patch failed\n"; } } sub git_commitdiff_plain { - git_commitdiff('plain'); + git_commitdiff(-format => 'plain'); +} + +# format-patch-style patches +sub git_patch { + git_commitdiff(-format => 'patch', -single=> 1); +} + +sub git_patches { + git_commitdiff(-format => 'patch'); } sub git_history { @@ -5922,6 +6008,14 @@ sub git_shortlog { $cgi->a({-href => href(-replay=>1, page=>$page+1), -accesskey => "n", -title => "Alt-n"}, "next"); } + my $patch_max = gitweb_check_feature('patches'); + if ($patch_max) { + if ($patch_max < 0 || @commitlist <= $patch_max) { + $paging_nav .= " ⋅ " . + $cgi->a({-href => href(action=>"patches", -replay=>1)}, + "patches"); + } + } git_header_html(); git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav); @@ -5959,7 +6053,25 @@ sub git_feed { } if (defined($commitlist[0])) { %latest_commit = %{$commitlist[0]}; - %latest_date = parse_date($latest_commit{'author_epoch'}); + my $latest_epoch = $latest_commit{'committer_epoch'}; + %latest_date = parse_date($latest_epoch); + my $if_modified = $cgi->http('IF_MODIFIED_SINCE'); + if (defined $if_modified) { + my $since; + if (eval { require HTTP::Date; 1; }) { + $since = HTTP::Date::str2time($if_modified); + } elsif (eval { require Time::ParseDate; 1; }) { + $since = Time::ParseDate::parsedate($if_modified, GMT => 1); + } + if (defined $since && $latest_epoch <= $since) { + print $cgi->header( + -type => $content_type, + -charset => 'utf-8', + -last_modified => $latest_date{'rfc2822'}, + -status => '304 Not Modified'); + return; + } + } print $cgi->header( -type => $content_type, -charset => 'utf-8', @@ -6018,7 +6130,24 @@ XML print "$title\n" . "$alt_url\n" . "$descr\n" . - "en\n"; + "en\n" . + # project owner is responsible for 'editorial' content + "$owner\n"; + if (defined $logo || defined $favicon) { + # prefer the logo to the favicon, since RSS + # doesn't allow both + my $img = esc_url($logo || $favicon); + print "\n" . + "$img\n" . + "$title\n" . + "$alt_url\n" . + "\n"; + } + if (%latest_date) { + print "$latest_date{'rfc2822'}\n"; + print "$latest_date{'rfc2822'}\n"; + } + print "gitweb v.$version/$git_version\n"; } elsif ($format eq 'atom') { print < @@ -6045,6 +6174,7 @@ XML } else { print "$latest_date{'iso-8601'}\n"; } + print "gitweb\n"; } # contents @@ -6166,7 +6296,11 @@ sub git_atom { sub git_opml { my @list = git_get_projects_list(); - print $cgi->header(-type => 'text/xml', -charset => 'utf-8'); + print $cgi->header( + -type => 'text/xml', + -charset => 'utf-8', + -content_disposition => 'inline; filename="opml.xml"'); + print < @@ -6190,8 +6324,8 @@ XML } my $path = esc_html(chop_str($proj{'path'}, 25, 5)); - my $rss = "$my_url?p=$proj{'path'};a=rss"; - my $html = "$my_url?p=$proj{'path'};a=summary"; + my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1); + my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1); print "\n"; } print <