showdependencygraph.cgi 10.5 KB
Newer Older
1
#!/usr/bin/perl -wT
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
use lib qw(. lib);
12

13
use File::Temp;
14

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

23
my $user = Bugzilla->login();
24

25
my $cgi = Bugzilla->cgi;
26 27
my $template = Bugzilla->template;
my $vars = {};
28 29
# Connect to the shadow database if this installation is using one to improve
# performance.
30
my $dbh = Bugzilla->switch_to_shadow_db();
31

32
local our (%seen, %edgesdone, %bugtitles);
33 34 35 36 37 38 39 40 41 42

# 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]
43

44 45 46
sub CreateImagemap {
    my $mapfilename = shift;
    my $map = "<map name=\"imagemap\">\n";
47
    my $default = "";
48 49 50 51

    open MAP, "<$mapfilename";
    while(my $line = <MAP>) {
        if($line =~ /^default ([^ ]*)(.*)$/) {
52
            $default = qq{<area alt="" shape="default" href="$1">\n};
53
        }
54

55
        if ($line =~ /^rectangle \((.*),(.*)\) \((.*),(.*)\) (http[^ ]*) (\d+)(\\n.*)?$/) {
56
            my ($leftx, $rightx, $topy, $bottomy, $url, $bugid) = ($1, $3, $2, $4, $5, $6);
57 58 59 60

            # 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.
61
            my $bugtitle = html_quote(clean_text($bugtitles{$bugid}));
62 63 64
            $map .= qq{<area alt="bug $bugid" name="bug$bugid" shape="rect" } .
                    qq{title="$bugtitle" href="$url" } .
                    qq{coords="$leftx,$topy,$rightx,$bottomy">\n};
65 66 67 68 69 70 71 72
        }
    }
    close MAP;

    $map .= "$default</map>";
    return $map;
}

73
sub AddLink {
74
    my ($blocked, $dependson, $fh) = (@_);
75 76 77
    my $key = "$blocked,$dependson";
    if (!exists $edgesdone{$key}) {
        $edgesdone{$key} = 1;
78
        print $fh "$dependson -> $blocked\n";
79 80 81 82 83
        $seen{$blocked} = 1;
        $seen{$dependson} = 1;
    }
}

84
ThrowUserError("missing_bug_id") unless $cgi->param('id');
85

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

90
my $rankdir = $cgi->param('rankdir') || 'TB';
91
# Make sure the submitted 'rankdir' value is valid.
92
if (!grep { $_ eq $rankdir } @valid_rankdirs) {
93
    $rankdir = 'TB';
94 95
}

96
my $display = $cgi->param('display') || 'tree';
97
my $webdotdir = bz_locations()->{'webdotdir'};
98

99 100
my ($fh, $filename) = File::Temp::tempfile("XXXXXXXXXX",
                                           SUFFIX => '.dot',
101 102 103 104 105 106 107
                                           DIR => $webdotdir,
                                           UNLINK => 1);

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

108
my $urlbase = Bugzilla->params->{'urlbase'};
109

110 111
print $fh "digraph G {";
print $fh qq{
112
graph [URL="${urlbase}query.cgi", rankdir=$rankdir]
113 114 115
node [URL="${urlbase}show_bug.cgi?id=\\N", style=filled, color=lightgrey]
};

116
my %baselist;
117

118 119 120 121
foreach my $i (split('[\s,]+', $cgi->param('id'))) {
    my $bug = Bugzilla::Bug->check($i);
    $baselist{$bug->id} = 1;
}
122

123 124 125 126 127 128
my @stack = keys(%baselist);

if ($display eq 'web') {
    my $sth = $dbh->prepare(q{SELECT blocked, dependson
                                FROM dependencies
                               WHERE blocked = ? OR dependson = ?});
129

130 131 132 133 134 135
    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;
136
            }
137 138 139 140
            if ($dependson != $id && !exists $seen{$dependson}) {
                push @stack, $dependson;
            }
            AddLink($blocked, $dependson, $fh);
141 142
        }
    }
143 144 145 146 147 148 149 150 151
}
# 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);
152
        }
153 154 155 156 157 158 159
    }
    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);
160
        }
161
    }
162
}
163

164 165
foreach my $k (keys(%baselist)) {
    $seen{$k} = 1;
166 167
}

168 169 170 171
my $sth = $dbh->prepare(
              q{SELECT bug_status, resolution, short_desc
                  FROM bugs
                 WHERE bugs.bug_id = ?});
172 173 174 175

my @bug_ids = keys %seen;
$user->visible_bugs(\@bug_ids);
foreach my $k (@bug_ids) {
176
    # Retrieve bug information from the database
177
    my ($stat, $resolution, $summary) = $dbh->selectrow_array($sth, undef, $k);
178 179

    # Resolution and summary are shown only if user can see the bug
180
    if (!$user->can_see_bug($k)) {
181
        $resolution = $summary = '';
182
    }
183

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

186
    my @params;
187

188
    if ($summary ne "" && $cgi->param('showsummary')) {
189 190 191 192
        # Wide characters cause GraphViz to die.
        if (Bugzilla->params->{'utf8'}) {
            utf8::encode($summary) if utf8::is_utf8($summary);
        }
193 194
        $summary =~ s/([\\\"])/\\$1/g;
        push(@params, qq{label="$k\\n$summary"});
195
    }
196 197 198

    if (exists $baselist{$k}) {
        push(@params, "shape=box");
199 200
    }

201
    if (is_open_state($stat)) {
202 203
        push(@params, "color=green");
    }
204

205
    if (@params) {
206
        print $fh "$k [" . join(',', @params) . "]\n";
207
    } else {
208
        print $fh "$k\n";
209
    }
210 211 212 213 214 215 216 217

    # Push the bug tooltip texts into a global hash so that 
    # CreateImagemap sub (used with local dot installations) can
    # use them later on.
    $bugtitles{$k} = trim("$stat $resolution");

    # Show the bug summary in tooltips only if not shown on 
    # the graph and it is non-empty (the user can see the bug)
218
    if (!$cgi->param('showsummary') && $summary ne "") {
219 220
        $bugtitles{$k} .= " - $summary";
    }
221 222 223
}


224 225
print $fh "}\n";
close $fh;
226

227
my $webdotbase = Bugzilla->params->{'webdotbase'};
228 229

if ($webdotbase =~ /^https?:/) {
230 231
     # Remote dot server. We don't hardcode 'urlbase' here in case
     # 'sslbase' is in use.
232
     $webdotbase =~ s/%([a-z]*)%/Bugzilla->params->{$1}/eg;
233
     my $url = $webdotbase . $filename;
234 235
     $vars->{'image_url'} = $url . ".gif";
     $vars->{'map_url'} = $url . ".map";
236
} else {
237
    # Local dot installation
238 239 240

    # First, generate the png image file from the .dot source

241 242
    my ($pngfh, $pngfilename) = File::Temp::tempfile("XXXXXXXXXX",
                                                     SUFFIX => '.png',
243
                                                     DIR => $webdotdir);
244 245 246 247 248

    chmod Bugzilla::Install::Filesystem::WS_SERVE, $pngfilename
        or warn install_string('chmod_failed', { path => $pngfilename,
                                                 error => $! });

249
    binmode $pngfh;
250
    open(DOT, "\"$webdotbase\" -Tpng $filename|");
251
    binmode DOT;
252 253 254
    print $pngfh $_ while <DOT>;
    close DOT;
    close $pngfh;
255

256
    # On Windows $pngfilename will contain \ instead of /
257
    $pngfilename =~ s|\\|/|g if ON_WINDOWS;
258 259 260 261

    # 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};
262
    $pngfilename =~ s#^\Q$cgi_root\E/?##;
263
    
264
    $vars->{'image_url'} = $pngfilename;
265

266 267 268 269
    # 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.

270 271
    my ($mapfh, $mapfilename) = File::Temp::tempfile("XXXXXXXXXX",
                                                     SUFFIX => '.map',
272
                                                     DIR => $webdotdir);
273 274 275 276 277

    chmod Bugzilla::Install::Filesystem::WS_SERVE, $mapfilename
        or warn install_string('chmod_failed', { path => $mapfilename,
                                                 error => $! });

278
    binmode $mapfh;
279
    open(DOT, "\"$webdotbase\" -Tismap $filename|");
280
    binmode DOT;
281 282 283
    print $mapfh $_ while <DOT>;
    close DOT;
    close $mapfh;
284

285
    $vars->{'image_map'} = CreateImagemap($mapfilename);
286 287 288 289
}

# Cleanup any old .dot files created from previous runs.
my $since = time() - 24 * 60 * 60;
290
# Can't use glob, since even calling that fails taint checks for perl < 5.6
291 292
opendir(DIR, $webdotdir);
my @files = grep { /\.dot$|\.png$|\.map$/ && -f "$webdotdir/$_" } readdir(DIR);
293 294
closedir DIR;
foreach my $f (@files)
295
{
296
    $f = "$webdotdir/$f";
297
    # Here we are deleting all old files. All entries are from the
298
    # $webdot directory. Since we're deleting the file (not following
299
    # symlinks), this can't escape to delete anything it shouldn't
300
    # (unless someone moves the location of $webdotdir, of course)
301
    trick_taint($f);
302
    my $mtime = (stat($f))[9];
303
    if ($mtime && $mtime < $since) {
304 305 306 307
        unlink $f;
    }
}

308 309 310
# 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);
311
$vars->{'multiple_bugs'} = ($cgi->param('id') =~ /[ ,]/);
312
$vars->{'display'} = $display;
313 314
$vars->{'rankdir'} = $rankdir;
$vars->{'showsummary'} = $cgi->param('showsummary');
315

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