showdependencygraph.cgi 10.3 KB
Newer Older
1
#!/usr/bin/perl -T
2 3 4
# 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/.
5
#
6 7
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
8

9
use 5.10.1;
10
use strict;
11 12
use warnings;

13
use lib qw(. lib);
14

15
use File::Temp;
16

17
use Bugzilla;
18
use Bugzilla::Constants;
19
use Bugzilla::Install::Filesystem;
20
use Bugzilla::Util;
21
use Bugzilla::Error;
22
use Bugzilla::Bug;
23
use Bugzilla::Status;
24

25
my $user = Bugzilla->login();
26

27
my $cgi      = Bugzilla->cgi;
28
my $template = Bugzilla->template;
29 30
my $vars     = {};

31 32
# Connect to the shadow database if this installation is using one to improve
# performance.
33
my $dbh = Bugzilla->switch_to_shadow_db();
34

35 36
our (%seen, %edgesdone, %bugtitles);
our $bug_count = 0;
37

38
# CreateImagemap: This sub grabs a local filename as a parameter, reads the
39 40 41 42 43 44 45 46
# 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]
47

48
sub CreateImagemap {
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
  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};
78
    }
79 80
  }
  close MAP;
81

82 83
  $map .= "$default</map>";
  return $map;
84 85
}

86
sub AddLink {
87 88 89 90 91 92 93 94 95
  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;
  }
96 97
}

98
ThrowUserError("missing_bug_id") unless $cgi->param('id');
99

100
# The list of valid directions. Some are not proposed in the dropdrown
101
# menu despite the fact that they are valid.
102 103
my @valid_rankdirs = ('LR', 'RL', 'TB', 'BT');

104
my $rankdir = $cgi->param('rankdir') || 'TB';
105

106
# Make sure the submitted 'rankdir' value is valid.
107
if (!grep { $_ eq $rankdir } @valid_rankdirs) {
108
  $rankdir = 'TB';
109 110
}

111
my $display   = $cgi->param('display') || 'tree';
112
my $webdotdir = bz_locations()->{'webdotdir'};
113

114 115 116 117 118 119
my ($fh, $filename) = File::Temp::tempfile(
  "XXXXXXXXXX",
  SUFFIX => '.dot',
  DIR    => $webdotdir,
  UNLINK => 1
);
120 121

chmod Bugzilla::Install::Filesystem::CGI_WRITE, $filename
122
  or warn install_string('chmod_failed', {path => $filename, error => $!});
123

124
my $urlbase = correct_urlbase();
125

126
print $fh "digraph G {";
127
print $fh qq(
128
graph [URL="${urlbase}query.cgi", rankdir=$rankdir]
129
node [URL="${urlbase}show_bug.cgi?id=\\N", style=filled, color=lightgrey]
130
);
131

132
my %baselist;
133

134
foreach my $i (split('[\s,]+', $cgi->param('id'))) {
135 136
  my $bug = Bugzilla::Bug->check($i);
  $baselist{$bug->id} = 1;
137
}
138

139 140 141
my @stack = keys(%baselist);

if ($display eq 'web') {
142
  my $sth = $dbh->prepare(q{SELECT blocked, dependson
143
                                FROM dependencies
144 145 146 147 148 149 150 151 152 153 154 155 156 157
                               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);
158
    }
159
  }
160
}
161

162 163
# This is the default: a tree instead of a spider web.
else {
164 165 166 167 168 169
  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);
170
    }
171 172 173 174 175 176 177
  }
  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);
178
    }
179
  }
180
}
181

182
foreach my $k (keys(%baselist)) {
183
  $seen{$k} = 1;
184 185
}

186
my $sth = $dbh->prepare(q{SELECT bug_status, resolution, short_desc
187
                  FROM bugs
188 189
                 WHERE bugs.bug_id = ?}
);
190 191 192 193

my @bug_ids = keys %seen;
$user->visible_bugs(\@bug_ids);
foreach my $k (@bug_ids) {
194

195 196
  # Retrieve bug information from the database
  my ($stat, $resolution, $summary) = $dbh->selectrow_array($sth, undef, $k);
197

198
  $vars->{'short_desc'} = $summary if ($k eq $cgi->param('id'));
199

200 201 202 203 204 205 206
  # 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 = '';
  }
207

208
  my @params;
209

210
  if ($summary ne "" && $cgi->param('showsummary')) {
211

212 213 214
    # Wide characters cause GraphViz to die.
    if (Bugzilla->params->{'utf8'}) {
      utf8::encode($summary) if utf8::is_utf8($summary);
215
    }
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
    $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";
  }
251 252 253
}


254 255
print $fh "}\n";
close $fh;
256

257
if ($bug_count > MAX_WEBDOT_BUGS) {
258 259
  unlink($filename);
  ThrowUserError("webdot_too_large");
260 261
}

262
my $webdotbase = Bugzilla->params->{'webdotbase'};
263 264

if ($webdotbase =~ /^https?:/) {
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318

  # 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);
319 320 321 322
}

# Cleanup any old .dot files created from previous runs.
my $since = time() - 24 * 60 * 60;
323

324
# Can't use glob, since even calling that fails taint checks for perl < 5.6
325 326
opendir(DIR, $webdotdir);
my @files = grep { /\.dot$|\.png$|\.map$/ && -f "$webdotdir/$_" } readdir(DIR);
327
closedir DIR;
328 329 330 331 332 333 334 335 336 337 338 339
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;
  }
340 341
}

342 343
# Make sure we only include valid integers (protects us from XSS attacks).
my @bugs = grep(detaint_natural($_), split(/[\s,]+/, $cgi->param('id')));
344
$vars->{'bug_id'}        = join(', ', @bugs);
345
$vars->{'multiple_bugs'} = ($cgi->param('id') =~ /[ ,]/);
346 347 348
$vars->{'display'}       = $display;
$vars->{'rankdir'}       = $rankdir;
$vars->{'showsummary'}   = $cgi->param('showsummary');
349

350
# Generate and return the UI (HTML page) from the appropriate template.
351
print $cgi->header();
352 353
$template->process("bug/dependency-graph.html.tmpl", $vars)
  || ThrowTemplateError($template->error());