showdependencygraph.cgi 10.6 KB
Newer Older
1
#!/usr/bin/perl -wT
2 3
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
4 5 6 7 8 9 10 11 12 13
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
14
# The Original Code is the Bugzilla Bug Tracking System.
15
#
16
# The Initial Developer of the Original Code is Netscape Communications
17 18 19 20
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
21
# Contributor(s): Terry Weissman <terry@mozilla.org>
22
#                 Gervase Markham <gerv@gerv.net>
23 24 25

use strict;

26 27
use lib qw(.);

28
use File::Temp;
29

30
use Bugzilla;
31
use Bugzilla::Constants;
32
use Bugzilla::Util;
33
use Bugzilla::Error;
34
use Bugzilla::Bug;
35

36
Bugzilla->login();
37

38
my $cgi = Bugzilla->cgi;
39 40
my $template = Bugzilla->template;
my $vars = {};
41 42
# Connect to the shadow database if this installation is using one to improve
# performance.
43
my $dbh = Bugzilla->switch_to_shadow_db();
44

45
local our (%seen, %edgesdone, %bugtitles);
46 47 48 49 50 51 52 53 54 55

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

57 58 59 60 61 62 63 64
sub CreateImagemap {
    my $mapfilename = shift;
    my $map = "<map name=\"imagemap\">\n";
    my $default;

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

68
        if ($line =~ /^rectangle \((.*),(.*)\) \((.*),(.*)\) (http[^ ]*) (\d+)(\\n.*)?$/) {
69
            my ($leftx, $rightx, $topy, $bottomy, $url, $bugid) = ($1, $3, $2, $4, $5, $6);
70 71 72 73 74 75 76 77

            # 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.
            my $bugtitle = value_quote($bugtitles{$bugid});
            $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 81 82 83 84 85
        }
    }
    close MAP;

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

86
sub AddLink {
87
    my ($blocked, $dependson, $fh) = (@_);
88 89 90
    my $key = "$blocked,$dependson";
    if (!exists $edgesdone{$key}) {
        $edgesdone{$key} = 1;
91
        print $fh "$blocked -> $dependson\n";
92 93 94 95 96
        $seen{$blocked} = 1;
        $seen{$dependson} = 1;
    }
}

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

101
my $rankdir = $cgi->param('rankdir') || 'TB';
102 103
# Make sure the submitted 'rankdir' value is valid.
if (lsearch(\@valid_rankdirs, $rankdir) < 0) {
104
    $rankdir = 'TB';
105 106
}

107
my $display = $cgi->param('display') || 'tree';
108
my $webdotdir = bz_locations()->{'webdotdir'};
109

110
if (!defined $cgi->param('id') && $display ne 'doall') {
111
    ThrowCodeError("missing_bug_id");
112
}
113

114 115
my ($fh, $filename) = File::Temp::tempfile("XXXXXXXXXX",
                                           SUFFIX => '.dot',
116
                                           DIR => $webdotdir);
117
my $urlbase = Bugzilla->params->{'urlbase'};
118

119 120
print $fh "digraph G {";
print $fh qq{
121
graph [URL="${urlbase}query.cgi", rankdir=$rankdir]
122 123 124
node [URL="${urlbase}show_bug.cgi?id=\\N", style=filled, color=lightgrey]
};

125
my %baselist;
126

127
if ($display eq 'doall') {
128 129
    my $dependencies = $dbh->selectall_arrayref(
                           "SELECT blocked, dependson FROM dependencies");
130

131 132
    foreach my $dependency (@$dependencies) {
        my ($blocked, $dependson) = @$dependency;
133
        AddLink($blocked, $dependson, $fh);
134 135
    }
} else {
136
    foreach my $i (split('[\s,]+', $cgi->param('id'))) {
137 138 139 140
        ValidateBugID($i);
        $baselist{$i} = 1;
    }

141 142
    my @stack = keys(%baselist);

143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
    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);
178
            }
179
        }
180 181
    }

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

187 188 189 190
my $sth = $dbh->prepare(
              q{SELECT bug_status, resolution, short_desc
                  FROM bugs
                 WHERE bugs.bug_id = ?});
191
foreach my $k (keys(%seen)) {
192
    # Retrieve bug information from the database
193
    my ($stat, $resolution, $summary) = $dbh->selectrow_array($sth, undef, $k);
194 195 196 197 198
    $stat ||= 'NEW';
    $resolution ||= '';
    $summary ||= '';

    # Resolution and summary are shown only if user can see the bug
199
    if (!Bugzilla->user->can_see_bug($k)) {
200
        $resolution = $summary = '';
201
    }
202

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

205
    my @params;
206

207
    if ($summary ne "" && $cgi->param('showsummary')) {
208 209
        $summary =~ s/([\\\"])/\\$1/g;
        push(@params, qq{label="$k\\n$summary"});
210
    }
211 212 213

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

216
    if (is_open_state($stat)) {
217 218
        push(@params, "color=green");
    }
219

220
    if (@params) {
221
        print $fh "$k [" . join(',', @params) . "]\n";
222
    } else {
223
        print $fh "$k\n";
224
    }
225 226 227 228 229 230 231 232

    # 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)
233
    if (!$cgi->param('showsummary') && $summary ne "") {
234 235
        $bugtitles{$k} .= " - $summary";
    }
236 237 238
}


239 240
print $fh "}\n";
close $fh;
241 242 243

chmod 0777, $filename;

244
my $webdotbase = Bugzilla->params->{'webdotbase'};
245 246

if ($webdotbase =~ /^https?:/) {
247 248 249 250
     # Remote dot server. We don't hardcode 'urlbase' here in case
     # 'sslbase' is in use.
     $webdotbase =~ s/%([a-z]*)%/Bugzilla->params->{$1}/g;
     my $url = $webdotbase . $filename;
251 252
     $vars->{'image_url'} = $url . ".gif";
     $vars->{'map_url'} = $url . ".map";
253
} else {
254
    # Local dot installation
255 256 257

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

258 259
    my ($pngfh, $pngfilename) = File::Temp::tempfile("XXXXXXXXXX",
                                                     SUFFIX => '.png',
260
                                                     DIR => $webdotdir);
261
    binmode $pngfh;
262
    open(DOT, "\"$webdotbase\" -Tpng $filename|");
263
    binmode DOT;
264 265 266
    print $pngfh $_ while <DOT>;
    close DOT;
    close $pngfh;
267 268 269
    
    # On Windows $pngfilename will contain \ instead of /
    $pngfilename =~ s|\\|/|g if $^O eq 'MSWin32';
270 271 272 273 274

    # 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//;
275
    
276
    $vars->{'image_url'} = $pngfilename;
277

278 279 280 281
    # 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.

282 283
    my ($mapfh, $mapfilename) = File::Temp::tempfile("XXXXXXXXXX",
                                                     SUFFIX => '.map',
284
                                                     DIR => $webdotdir);
285
    binmode $mapfh;
286
    open(DOT, "\"$webdotbase\" -Tismap $filename|");
287
    binmode DOT;
288 289 290
    print $mapfh $_ while <DOT>;
    close DOT;
    close $mapfh;
291
    $vars->{'image_map'} = CreateImagemap($mapfilename);
292 293 294 295
}

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

313 314 315
# 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);
316
$vars->{'multiple_bugs'} = ($cgi->param('id') =~ /[ ,]/);
317
$vars->{'display'} = $display;
318 319
$vars->{'rankdir'} = $rankdir;
$vars->{'showsummary'} = $cgi->param('showsummary');
320

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