blob: 7b2d2f55d41f833a0a4612a0766b3bd3c6d5337d [file] [log] [blame]
#!/usr/bin/perl -T
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
use 5.10.1;
use strict;
use warnings;
use lib qw(. lib);
use File::Temp;
use Bugzilla;
use Bugzilla::Constants;
use Bugzilla::Install::Filesystem;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Bug;
use Bugzilla::Status;
my $user = Bugzilla->login();
my $cgi = Bugzilla->cgi;
my $template = Bugzilla->template;
my $vars = {};
# Connect to the shadow database if this installation is using one to improve
# performance.
my $dbh = Bugzilla->switch_to_shadow_db();
our (%seen, %edgesdone, %bugtitles);
our $bug_count = 0;
# CreateImagemap: This sub grabs a local filename as a parameter, reads the
# dot-generated image map datafile residing in that file and turns it into
# an HTML map element. THIS SUB IS ONLY USED FOR LOCAL DOT INSTALLATIONS.
# The map datafile won't necessarily contain the bug summaries, so we'll
# pull possible HTML titles from the %bugtitles hash (filled elsewhere
# in the code)
# The dot mapdata lines have the following format (\nsummary is optional):
# rectangle (LEFTX,TOPY) (RIGHTX,BOTTOMY) URLBASE/show_bug.cgi?id=BUGNUM BUGNUM[\nSUMMARY]
sub CreateImagemap {
my $mapfilename = shift;
my $map = "<map name=\"imagemap\">\n";
my $default = "";
open MAP, "<", $mapfilename;
while(my $line = <MAP>) {
if($line =~ /^default ([^ ]*)(.*)$/) {
$default = qq{<area alt="" shape="default" href="$1">\n};
}
if ($line =~ /^rectangle \((\d+),(\d+)\) \((\d+),(\d+)\) (http[^ ]*) (\d+)(?:\\n.*)?$/) {
my ($leftx, $rightx, $topy, $bottomy, $url, $bugid) = ($1, $3, $2, $4, $5, $6);
# Pick up bugid from the mapdata label field. Getting the title from
# bugtitle hash instead of mapdata allows us to get the summary even
# when showsummary is off, and also gives us status and resolution.
# This text is safe; it has already been escaped.
my $bugtitle = $bugtitles{$bugid};
# The URL is supposed to be safe, because it's built manually.
# But in case someone manages to inject code, it's safer to escape it.
$url = html_quote($url);
$map .= qq{<area alt="bug $bugid" name="bug$bugid" shape="rect" } .
qq{title="$bugtitle" href="$url" } .
qq{coords="$leftx,$topy,$rightx,$bottomy">\n};
}
}
close MAP;
$map .= "$default</map>";
return $map;
}
sub AddLink {
my ($blocked, $dependson, $fh) = (@_);
my $key = "$blocked,$dependson";
if (!exists $edgesdone{$key}) {
$edgesdone{$key} = 1;
print $fh "$dependson -> $blocked\n";
$bug_count++;
$seen{$blocked} = 1;
$seen{$dependson} = 1;
}
}
ThrowUserError("missing_bug_id") unless $cgi->param('id');
# The list of valid directions. Some are not proposed in the dropdrown
# menu despite the fact that they are valid.
my @valid_rankdirs = ('LR', 'RL', 'TB', 'BT');
my $rankdir = $cgi->param('rankdir') || 'TB';
# Make sure the submitted 'rankdir' value is valid.
if (!grep { $_ eq $rankdir } @valid_rankdirs) {
$rankdir = 'TB';
}
my $display = $cgi->param('display') || 'tree';
my $webdotdir = bz_locations()->{'webdotdir'};
my ($fh, $filename) = File::Temp::tempfile("XXXXXXXXXX",
SUFFIX => '.dot',
DIR => $webdotdir,
UNLINK => 1);
chmod Bugzilla::Install::Filesystem::CGI_WRITE, $filename
or warn install_string('chmod_failed', { path => $filename,
error => $! });
my $urlbase = correct_urlbase();
print $fh "digraph G {";
print $fh qq(
graph [URL="${urlbase}query.cgi", rankdir=$rankdir]
node [URL="${urlbase}show_bug.cgi?id=\\N", style=filled, color=lightgrey]
);
my %baselist;
foreach my $i (split('[\s,]+', $cgi->param('id'))) {
my $bug = Bugzilla::Bug->check($i);
$baselist{$bug->id} = 1;
}
my @stack = keys(%baselist);
if ($display eq 'web') {
my $sth = $dbh->prepare(q{SELECT blocked, dependson
FROM dependencies
WHERE blocked = ? OR dependson = ?});
foreach my $id (@stack) {
my $dependencies = $dbh->selectall_arrayref($sth, undef, ($id, $id));
foreach my $dependency (@$dependencies) {
my ($blocked, $dependson) = @$dependency;
if ($blocked != $id && !exists $seen{$blocked}) {
push @stack, $blocked;
}
if ($dependson != $id && !exists $seen{$dependson}) {
push @stack, $dependson;
}
AddLink($blocked, $dependson, $fh);
}
}
}
# This is the default: a tree instead of a spider web.
else {
my @blocker_stack = @stack;
foreach my $id (@blocker_stack) {
my $blocker_ids = Bugzilla::Bug::EmitDependList('blocked', 'dependson', $id);
foreach my $blocker_id (@$blocker_ids) {
push(@blocker_stack, $blocker_id) unless $seen{$blocker_id};
AddLink($id, $blocker_id, $fh);
}
}
my @dependent_stack = @stack;
foreach my $id (@dependent_stack) {
my $dep_bug_ids = Bugzilla::Bug::EmitDependList('dependson', 'blocked', $id);
foreach my $dep_bug_id (@$dep_bug_ids) {
push(@dependent_stack, $dep_bug_id) unless $seen{$dep_bug_id};
AddLink($dep_bug_id, $id, $fh);
}
}
}
foreach my $k (keys(%baselist)) {
$seen{$k} = 1;
}
my $sth = $dbh->prepare(
q{SELECT bug_status, resolution, short_desc
FROM bugs
WHERE bugs.bug_id = ?});
my @bug_ids = keys %seen;
$user->visible_bugs(\@bug_ids);
foreach my $k (@bug_ids) {
# Retrieve bug information from the database
my ($stat, $resolution, $summary) = $dbh->selectrow_array($sth, undef, $k);
$vars->{'short_desc'} = $summary if ($k eq $cgi->param('id'));
# The bug summary is shown only if the user can see the bug.
if ($user->can_see_bug($k)) {
$summary = html_quote(clean_text($summary));
}
else {
$summary = '';
}
my @params;
if ($summary ne "" && $cgi->param('showsummary')) {
# Wide characters cause GraphViz to die.
if (Bugzilla->params->{'utf8'}) {
utf8::encode($summary) if utf8::is_utf8($summary);
}
$summary =~ s/([\\\"])/\\$1/g;
# Newlines must be escaped too, to not break the .map file
# and to prevent code injection.
$summary =~ s/\n/\\n/g;
push(@params, qq{label="$k\\n$summary"});
}
if (exists $baselist{$k}) {
push(@params, "shape=box");
}
if (is_open_state($stat)) {
push(@params, "color=green");
}
if (@params) {
print $fh "$k [" . join(',', @params) . "]\n";
} else {
print $fh "$k\n";
}
# Push the bug tooltip texts into a global hash so that
# CreateImagemap sub (used with local dot installations) can
# use them later on.
my $stat_display = display_value('bug_status', $stat);
my $resolution_display = display_value('resolution', $resolution);
$bugtitles{$k} = trim("$stat_display $resolution_display");
# Show the bug summary in tooltips only if not shown on
# the graph and it is non-empty (the user can see the bug)
if (!$cgi->param('showsummary') && $summary ne "") {
$bugtitles{$k} .= " - $summary";
}
}
print $fh "}\n";
close $fh;
if ($bug_count > MAX_WEBDOT_BUGS) {
unlink($filename);
ThrowUserError("webdot_too_large");
}
my $webdotbase = Bugzilla->params->{'webdotbase'};
if ($webdotbase =~ /^https?:/) {
# Remote dot server. We don't hardcode 'urlbase' here in case
# 'sslbase' is in use.
$webdotbase =~ s/%([a-z]*)%/Bugzilla->params->{$1}/eg;
my $url = $webdotbase . $filename;
$vars->{'image_url'} = $url . ".gif";
$vars->{'map_url'} = $url . ".map";
} else {
# Local dot installation
# First, generate the png image file from the .dot source
my ($pngfh, $pngfilename) = File::Temp::tempfile("XXXXXXXXXX",
SUFFIX => '.png',
DIR => $webdotdir);
chmod Bugzilla::Install::Filesystem::WS_SERVE, $pngfilename
or warn install_string('chmod_failed', { path => $pngfilename,
error => $! });
binmode $pngfh;
open(DOT, '-|', "\"$webdotbase\" -Tpng $filename");
binmode DOT;
print $pngfh $_ while <DOT>;
close DOT;
close $pngfh;
# On Windows $pngfilename will contain \ instead of /
$pngfilename =~ s|\\|/|g if ON_WINDOWS;
# Under mod_perl, pngfilename will have an absolute path, and we
# need to make that into a relative path.
my $cgi_root = bz_locations()->{cgi_path};
$pngfilename =~ s#^\Q$cgi_root\E/?##;
$vars->{'image_url'} = $pngfilename;
# Then, generate a imagemap datafile that contains the corner data
# for drawn bug objects. Pass it on to CreateImagemap that
# turns this monster into html.
my ($mapfh, $mapfilename) = File::Temp::tempfile("XXXXXXXXXX",
SUFFIX => '.map',
DIR => $webdotdir);
chmod Bugzilla::Install::Filesystem::WS_SERVE, $mapfilename
or warn install_string('chmod_failed', { path => $mapfilename,
error => $! });
binmode $mapfh;
open(DOT, '-|', "\"$webdotbase\" -Tismap $filename");
binmode DOT;
print $mapfh $_ while <DOT>;
close DOT;
close $mapfh;
$vars->{'image_map'} = CreateImagemap($mapfilename);
}
# Cleanup any old .dot files created from previous runs.
my $since = time() - 24 * 60 * 60;
# Can't use glob, since even calling that fails taint checks for perl < 5.6
opendir(DIR, $webdotdir);
my @files = grep { /\.dot$|\.png$|\.map$/ && -f "$webdotdir/$_" } readdir(DIR);
closedir DIR;
foreach my $f (@files)
{
$f = "$webdotdir/$f";
# Here we are deleting all old files. All entries are from the
# $webdot directory. Since we're deleting the file (not following
# symlinks), this can't escape to delete anything it shouldn't
# (unless someone moves the location of $webdotdir, of course)
trick_taint($f);
my $mtime = (stat($f))[9];
if ($mtime && $mtime < $since) {
unlink $f;
}
}
# Make sure we only include valid integers (protects us from XSS attacks).
my @bugs = grep(detaint_natural($_), split(/[\s,]+/, $cgi->param('id')));
$vars->{'bug_id'} = join(', ', @bugs);
$vars->{'multiple_bugs'} = ($cgi->param('id') =~ /[ ,]/);
$vars->{'display'} = $display;
$vars->{'rankdir'} = $rankdir;
$vars->{'showsummary'} = $cgi->param('showsummary');
# Generate and return the UI (HTML page) from the appropriate template.
print $cgi->header();
$template->process("bug/dependency-graph.html.tmpl", $vars)
|| ThrowTemplateError($template->error());