showdependencygraph.cgi 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. #!/usr/bin/env perl -wT
  2. # -*- Mode: perl; indent-tabs-mode: nil -*-
  3. #
  4. # The contents of this file are subject to the Mozilla Public
  5. # License Version 1.1 (the "License"); you may not use this file
  6. # except in compliance with the License. You may obtain a copy of
  7. # the License at http://www.mozilla.org/MPL/
  8. #
  9. # Software distributed under the License is distributed on an "AS
  10. # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
  11. # implied. See the License for the specific language governing
  12. # rights and limitations under the License.
  13. #
  14. # The Original Code is the Bugzilla Bug Tracking System.
  15. #
  16. # The Initial Developer of the Original Code is Netscape Communications
  17. # Corporation. Portions created by Netscape are
  18. # Copyright (C) 1998 Netscape Communications Corporation. All
  19. # Rights Reserved.
  20. #
  21. # Contributor(s): Terry Weissman <terry@mozilla.org>
  22. # Gervase Markham <gerv@gerv.net>
  23. use strict;
  24. use lib qw(. lib);
  25. use File::Temp;
  26. use Bugzilla;
  27. use Bugzilla::Constants;
  28. use Bugzilla::Util;
  29. use Bugzilla::Error;
  30. use Bugzilla::Bug;
  31. use Bugzilla::Status;
  32. Bugzilla->login();
  33. my $cgi = Bugzilla->cgi;
  34. my $template = Bugzilla->template;
  35. my $vars = {};
  36. # Connect to the shadow database if this installation is using one to improve
  37. # performance.
  38. my $dbh = Bugzilla->switch_to_shadow_db();
  39. local our (%seen, %edgesdone, %bugtitles);
  40. # CreateImagemap: This sub grabs a local filename as a parameter, reads the
  41. # dot-generated image map datafile residing in that file and turns it into
  42. # an HTML map element. THIS SUB IS ONLY USED FOR LOCAL DOT INSTALLATIONS.
  43. # The map datafile won't necessarily contain the bug summaries, so we'll
  44. # pull possible HTML titles from the %bugtitles hash (filled elsewhere
  45. # in the code)
  46. # The dot mapdata lines have the following format (\nsummary is optional):
  47. # rectangle (LEFTX,TOPY) (RIGHTX,BOTTOMY) URLBASE/show_bug.cgi?id=BUGNUM BUGNUM[\nSUMMARY]
  48. sub CreateImagemap {
  49. my $mapfilename = shift;
  50. my $map = "<map name=\"imagemap\">\n";
  51. my $default;
  52. open MAP, "<$mapfilename";
  53. while(my $line = <MAP>) {
  54. if($line =~ /^default ([^ ]*)(.*)$/) {
  55. $default = qq{<area alt="" shape="default" href="$1">\n};
  56. }
  57. if ($line =~ /^rectangle \((.*),(.*)\) \((.*),(.*)\) (http[^ ]*) (\d+)(\\n.*)?$/) {
  58. my ($leftx, $rightx, $topy, $bottomy, $url, $bugid) = ($1, $3, $2, $4, $5, $6);
  59. # Pick up bugid from the mapdata label field. Getting the title from
  60. # bugtitle hash instead of mapdata allows us to get the summary even
  61. # when showsummary is off, and also gives us status and resolution.
  62. my $bugtitle = html_quote(clean_text($bugtitles{$bugid}));
  63. $map .= qq{<area alt="bug $bugid" name="bug$bugid" shape="rect" } .
  64. qq{title="$bugtitle" href="$url" } .
  65. qq{coords="$leftx,$topy,$rightx,$bottomy">\n};
  66. }
  67. }
  68. close MAP;
  69. $map .= "$default</map>";
  70. return $map;
  71. }
  72. sub AddLink {
  73. my ($blocked, $dependson, $fh) = (@_);
  74. my $key = "$blocked,$dependson";
  75. if (!exists $edgesdone{$key}) {
  76. $edgesdone{$key} = 1;
  77. print $fh "$blocked -> $dependson\n";
  78. $seen{$blocked} = 1;
  79. $seen{$dependson} = 1;
  80. }
  81. }
  82. # The list of valid directions. Some are not proposed in the dropdrown
  83. # menu despite the fact that they are valid.
  84. my @valid_rankdirs = ('LR', 'RL', 'TB', 'BT');
  85. my $rankdir = $cgi->param('rankdir') || 'TB';
  86. # Make sure the submitted 'rankdir' value is valid.
  87. if (lsearch(\@valid_rankdirs, $rankdir) < 0) {
  88. $rankdir = 'TB';
  89. }
  90. my $display = $cgi->param('display') || 'tree';
  91. my $webdotdir = bz_locations()->{'webdotdir'};
  92. if (!defined $cgi->param('id') && $display ne 'doall') {
  93. ThrowCodeError("missing_bug_id");
  94. }
  95. my ($fh, $filename) = File::Temp::tempfile("XXXXXXXXXX",
  96. SUFFIX => '.dot',
  97. DIR => $webdotdir);
  98. my $urlbase = Bugzilla->params->{'urlbase'};
  99. print $fh "digraph G {";
  100. print $fh qq{
  101. graph [URL="${urlbase}query.cgi", rankdir=$rankdir]
  102. node [URL="${urlbase}show_bug.cgi?id=\\N", style=filled, color=lightgrey]
  103. };
  104. my %baselist;
  105. if ($display eq 'doall') {
  106. my $dependencies = $dbh->selectall_arrayref(
  107. "SELECT blocked, dependson FROM dependencies");
  108. foreach my $dependency (@$dependencies) {
  109. my ($blocked, $dependson) = @$dependency;
  110. AddLink($blocked, $dependson, $fh);
  111. }
  112. } else {
  113. foreach my $i (split('[\s,]+', $cgi->param('id'))) {
  114. ValidateBugID($i);
  115. $baselist{$i} = 1;
  116. }
  117. my @stack = keys(%baselist);
  118. if ($display eq 'web') {
  119. my $sth = $dbh->prepare(q{SELECT blocked, dependson
  120. FROM dependencies
  121. WHERE blocked = ? OR dependson = ?});
  122. foreach my $id (@stack) {
  123. my $dependencies = $dbh->selectall_arrayref($sth, undef, ($id, $id));
  124. foreach my $dependency (@$dependencies) {
  125. my ($blocked, $dependson) = @$dependency;
  126. if ($blocked != $id && !exists $seen{$blocked}) {
  127. push @stack, $blocked;
  128. }
  129. if ($dependson != $id && !exists $seen{$dependson}) {
  130. push @stack, $dependson;
  131. }
  132. AddLink($blocked, $dependson, $fh);
  133. }
  134. }
  135. }
  136. # This is the default: a tree instead of a spider web.
  137. else {
  138. my @blocker_stack = @stack;
  139. foreach my $id (@blocker_stack) {
  140. my $blocker_ids = Bugzilla::Bug::EmitDependList('blocked', 'dependson', $id);
  141. foreach my $blocker_id (@$blocker_ids) {
  142. push(@blocker_stack, $blocker_id) unless $seen{$blocker_id};
  143. AddLink($id, $blocker_id, $fh);
  144. }
  145. }
  146. my @dependent_stack = @stack;
  147. foreach my $id (@dependent_stack) {
  148. my $dep_bug_ids = Bugzilla::Bug::EmitDependList('dependson', 'blocked', $id);
  149. foreach my $dep_bug_id (@$dep_bug_ids) {
  150. push(@dependent_stack, $dep_bug_id) unless $seen{$dep_bug_id};
  151. AddLink($dep_bug_id, $id, $fh);
  152. }
  153. }
  154. }
  155. foreach my $k (keys(%baselist)) {
  156. $seen{$k} = 1;
  157. }
  158. }
  159. my $sth = $dbh->prepare(
  160. q{SELECT bug_status, resolution, short_desc
  161. FROM bugs
  162. WHERE bugs.bug_id = ?});
  163. foreach my $k (keys(%seen)) {
  164. # Retrieve bug information from the database
  165. my ($stat, $resolution, $summary) = $dbh->selectrow_array($sth, undef, $k);
  166. $stat ||= 'NEW';
  167. $resolution ||= '';
  168. $summary ||= '';
  169. # Resolution and summary are shown only if user can see the bug
  170. if (!Bugzilla->user->can_see_bug($k)) {
  171. $resolution = $summary = '';
  172. }
  173. $vars->{'short_desc'} = $summary if ($k eq $cgi->param('id'));
  174. my @params;
  175. if ($summary ne "" && $cgi->param('showsummary')) {
  176. $summary =~ s/([\\\"])/\\$1/g;
  177. push(@params, qq{label="$k\\n$summary"});
  178. }
  179. if (exists $baselist{$k}) {
  180. push(@params, "shape=box");
  181. }
  182. if (is_open_state($stat)) {
  183. push(@params, "color=green");
  184. }
  185. if (@params) {
  186. print $fh "$k [" . join(',', @params) . "]\n";
  187. } else {
  188. print $fh "$k\n";
  189. }
  190. # Push the bug tooltip texts into a global hash so that
  191. # CreateImagemap sub (used with local dot installations) can
  192. # use them later on.
  193. $bugtitles{$k} = trim("$stat $resolution");
  194. # Show the bug summary in tooltips only if not shown on
  195. # the graph and it is non-empty (the user can see the bug)
  196. if (!$cgi->param('showsummary') && $summary ne "") {
  197. $bugtitles{$k} .= " - $summary";
  198. }
  199. }
  200. print $fh "}\n";
  201. close $fh;
  202. chmod 0777, $filename;
  203. my $webdotbase = Bugzilla->params->{'webdotbase'};
  204. if ($webdotbase =~ /^https?:/) {
  205. # Remote dot server. We don't hardcode 'urlbase' here in case
  206. # 'sslbase' is in use.
  207. $webdotbase =~ s/%([a-z]*)%/Bugzilla->params->{$1}/eg;
  208. my $url = $webdotbase . $filename;
  209. $vars->{'image_url'} = $url . ".gif";
  210. $vars->{'map_url'} = $url . ".map";
  211. } else {
  212. # Local dot installation
  213. # First, generate the png image file from the .dot source
  214. my ($pngfh, $pngfilename) = File::Temp::tempfile("XXXXXXXXXX",
  215. SUFFIX => '.png',
  216. DIR => $webdotdir);
  217. binmode $pngfh;
  218. open(DOT, "\"$webdotbase\" -Tpng $filename|");
  219. binmode DOT;
  220. print $pngfh $_ while <DOT>;
  221. close DOT;
  222. close $pngfh;
  223. # On Windows $pngfilename will contain \ instead of /
  224. $pngfilename =~ s|\\|/|g if $^O eq 'MSWin32';
  225. # Under mod_perl, pngfilename will have an absolute path, and we
  226. # need to make that into a relative path.
  227. my $cgi_root = bz_locations()->{cgi_path};
  228. $pngfilename =~ s#^\Q$cgi_root\E/?##;
  229. $vars->{'image_url'} = $pngfilename;
  230. # Then, generate a imagemap datafile that contains the corner data
  231. # for drawn bug objects. Pass it on to CreateImagemap that
  232. # turns this monster into html.
  233. my ($mapfh, $mapfilename) = File::Temp::tempfile("XXXXXXXXXX",
  234. SUFFIX => '.map',
  235. DIR => $webdotdir);
  236. binmode $mapfh;
  237. open(DOT, "\"$webdotbase\" -Tismap $filename|");
  238. binmode DOT;
  239. print $mapfh $_ while <DOT>;
  240. close DOT;
  241. close $mapfh;
  242. $vars->{'image_map'} = CreateImagemap($mapfilename);
  243. }
  244. # Cleanup any old .dot files created from previous runs.
  245. my $since = time() - 24 * 60 * 60;
  246. # Can't use glob, since even calling that fails taint checks for perl < 5.6
  247. opendir(DIR, $webdotdir);
  248. my @files = grep { /\.dot$|\.png$|\.map$/ && -f "$webdotdir/$_" } readdir(DIR);
  249. closedir DIR;
  250. foreach my $f (@files)
  251. {
  252. $f = "$webdotdir/$f";
  253. # Here we are deleting all old files. All entries are from the
  254. # $webdot directory. Since we're deleting the file (not following
  255. # symlinks), this can't escape to delete anything it shouldn't
  256. # (unless someone moves the location of $webdotdir, of course)
  257. trick_taint($f);
  258. if (file_mod_time($f) < $since) {
  259. unlink $f;
  260. }
  261. }
  262. # Make sure we only include valid integers (protects us from XSS attacks).
  263. my @bugs = grep(detaint_natural($_), split(/[\s,]+/, $cgi->param('id')));
  264. $vars->{'bug_id'} = join(', ', @bugs);
  265. $vars->{'multiple_bugs'} = ($cgi->param('id') =~ /[ ,]/);
  266. $vars->{'display'} = $display;
  267. $vars->{'rankdir'} = $rankdir;
  268. $vars->{'showsummary'} = $cgi->param('showsummary');
  269. # Generate and return the UI (HTML page) from the appropriate template.
  270. print $cgi->header();
  271. $template->process("bug/dependency-graph.html.tmpl", $vars)
  272. || ThrowTemplateError($template->error());