You need to sign in or sign up before continuing.
showdependencygraph.cgi 10.7 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
use lib qw(. lib);
27

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
use Bugzilla::Status;
36

37
Bugzilla->login();
38

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

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

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

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

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

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

            # 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.
75
            my $bugtitle = html_quote(clean_text($bugtitles{$bugid}));
76 77 78
            $map .= qq{<area alt="bug $bugid" name="bug$bugid" shape="rect" } .
                    qq{title="$bugtitle" href="$url" } .
                    qq{coords="$leftx,$topy,$rightx,$bottomy">\n};
79 80 81 82 83 84 85 86
        }
    }
    close MAP;

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

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

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

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

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

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

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

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

126
my %baselist;
127

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

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

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

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 178
    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);
179
            }
180
        }
181 182
    }

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

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

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

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

206
    my @params;
207

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

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

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

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

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


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

chmod 0777, $filename;

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

if ($webdotbase =~ /^https?:/) {
248 249 250 251
     # 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;
252 253
     $vars->{'image_url'} = $url . ".gif";
     $vars->{'map_url'} = $url . ".map";
254
} else {
255
    # Local dot installation
256 257 258

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

259 260
    my ($pngfh, $pngfilename) = File::Temp::tempfile("XXXXXXXXXX",
                                                     SUFFIX => '.png',
261
                                                     DIR => $webdotdir);
262
    binmode $pngfh;
263
    open(DOT, "\"$webdotbase\" -Tpng $filename|");
264
    binmode DOT;
265 266 267
    print $pngfh $_ while <DOT>;
    close DOT;
    close $pngfh;
268 269 270
    
    # On Windows $pngfilename will contain \ instead of /
    $pngfilename =~ s|\\|/|g if $^O eq 'MSWin32';
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};
275
    $pngfilename =~ s#^\Q$cgi_root\E/?##;
276
    
277
    $vars->{'image_url'} = $pngfilename;
278

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

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

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

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

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