chart.cgi 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  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): Gervase Markham <gerv@gerv.net>
  22. # Lance Larsh <lance.larsh@oracle.com>
  23. # Glossary:
  24. # series: An individual, defined set of data plotted over time.
  25. # data set: What a series is called in the UI.
  26. # line: A set of one or more series, to be summed and drawn as a single
  27. # line when the series is plotted.
  28. # chart: A set of lines
  29. #
  30. # So when you select rows in the UI, you are selecting one or more lines, not
  31. # series.
  32. # Generic Charting TODO:
  33. #
  34. # JS-less chart creation - hard.
  35. # Broken image on error or no data - need to do much better.
  36. # Centralise permission checking, so Bugzilla->user->in_group('editbugs')
  37. # not scattered everywhere.
  38. # User documentation :-)
  39. #
  40. # Bonus:
  41. # Offer subscription when you get a "series already exists" error?
  42. use strict;
  43. use lib qw(. lib);
  44. use Bugzilla;
  45. use Bugzilla::Constants;
  46. use Bugzilla::Error;
  47. use Bugzilla::Util;
  48. use Bugzilla::Chart;
  49. use Bugzilla::Series;
  50. use Bugzilla::User;
  51. # For most scripts we don't make $cgi and $template global variables. But
  52. # when preparing Bugzilla for mod_perl, this script used these
  53. # variables in so many subroutines that it was easier to just
  54. # make them globals.
  55. local our $cgi = Bugzilla->cgi;
  56. local our $template = Bugzilla->template;
  57. local our $vars = {};
  58. # Go back to query.cgi if we are adding a boolean chart parameter.
  59. if (grep(/^cmd-/, $cgi->param())) {
  60. my $params = $cgi->canonicalise_query("format", "ctype", "action");
  61. print "Location: query.cgi?format=" . $cgi->param('query_format') .
  62. ($params ? "&$params" : "") . "\n\n";
  63. exit;
  64. }
  65. my $action = $cgi->param('action');
  66. my $series_id = $cgi->param('series_id');
  67. $vars->{'doc_section'} = 'reporting.html#charts';
  68. # Because some actions are chosen by buttons, we can't encode them as the value
  69. # of the action param, because that value is localization-dependent. So, we
  70. # encode it in the name, as "action-<action>". Some params even contain the
  71. # series_id they apply to (e.g. subscribe, unsubscribe).
  72. my @actions = grep(/^action-/, $cgi->param());
  73. if ($actions[0] && $actions[0] =~ /^action-([^\d]+)(\d*)$/) {
  74. $action = $1;
  75. $series_id = $2 if $2;
  76. }
  77. $action ||= "assemble";
  78. # Go to buglist.cgi if we are doing a search.
  79. if ($action eq "search") {
  80. my $params = $cgi->canonicalise_query("format", "ctype", "action");
  81. print "Location: buglist.cgi" . ($params ? "?$params" : "") . "\n\n";
  82. exit;
  83. }
  84. my $user = Bugzilla->login(LOGIN_REQUIRED);
  85. Bugzilla->user->in_group(Bugzilla->params->{"chartgroup"})
  86. || ThrowUserError("auth_failure", {group => Bugzilla->params->{"chartgroup"},
  87. action => "use",
  88. object => "charts"});
  89. # Only admins may create public queries
  90. Bugzilla->user->in_group('admin') || $cgi->delete('public');
  91. # All these actions relate to chart construction.
  92. if ($action =~ /^(assemble|add|remove|sum|subscribe|unsubscribe)$/) {
  93. # These two need to be done before the creation of the Chart object, so
  94. # that the changes they make will be reflected in it.
  95. if ($action =~ /^subscribe|unsubscribe$/) {
  96. detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
  97. my $series = new Bugzilla::Series($series_id);
  98. $series->$action($user->id);
  99. }
  100. my $chart = new Bugzilla::Chart($cgi);
  101. if ($action =~ /^remove|sum$/) {
  102. $chart->$action(getSelectedLines());
  103. }
  104. elsif ($action eq "add") {
  105. my @series_ids = getAndValidateSeriesIDs();
  106. $chart->add(@series_ids);
  107. }
  108. view($chart);
  109. }
  110. elsif ($action eq "plot") {
  111. plot();
  112. }
  113. elsif ($action eq "wrap") {
  114. # For CSV "wrap", we go straight to "plot".
  115. if ($cgi->param('ctype') && $cgi->param('ctype') eq "csv") {
  116. plot();
  117. }
  118. else {
  119. wrap();
  120. }
  121. }
  122. elsif ($action eq "create") {
  123. assertCanCreate($cgi);
  124. my $series = new Bugzilla::Series($cgi);
  125. if (!$series->existsInDatabase()) {
  126. $series->writeToDatabase();
  127. $vars->{'message'} = "series_created";
  128. }
  129. else {
  130. ThrowUserError("series_already_exists", {'series' => $series});
  131. }
  132. $vars->{'series'} = $series;
  133. print $cgi->header();
  134. $template->process("global/message.html.tmpl", $vars)
  135. || ThrowTemplateError($template->error());
  136. }
  137. elsif ($action eq "edit") {
  138. detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
  139. assertCanEdit($series_id);
  140. my $series = new Bugzilla::Series($series_id);
  141. edit($series);
  142. }
  143. elsif ($action eq "alter") {
  144. # This is the "commit" action for editing a series
  145. detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
  146. assertCanEdit($series_id);
  147. my $series = new Bugzilla::Series($cgi);
  148. # We need to check if there is _another_ series in the database with
  149. # our (potentially new) name. So we call existsInDatabase() to see if
  150. # the return value is us or some other series we need to avoid stomping
  151. # on.
  152. my $id_of_series_in_db = $series->existsInDatabase();
  153. if (defined($id_of_series_in_db) &&
  154. $id_of_series_in_db != $series->{'series_id'})
  155. {
  156. ThrowUserError("series_already_exists", {'series' => $series});
  157. }
  158. $series->writeToDatabase();
  159. $vars->{'changes_saved'} = 1;
  160. edit($series);
  161. }
  162. else {
  163. ThrowCodeError("unknown_action");
  164. }
  165. exit;
  166. # Find any selected series and return either the first or all of them.
  167. sub getAndValidateSeriesIDs {
  168. my @series_ids = grep(/^\d+$/, $cgi->param("name"));
  169. return wantarray ? @series_ids : $series_ids[0];
  170. }
  171. # Return a list of IDs of all the lines selected in the UI.
  172. sub getSelectedLines {
  173. my @ids = map { /^select(\d+)$/ ? $1 : () } $cgi->param();
  174. return @ids;
  175. }
  176. # Check if the user is the owner of series_id or is an admin.
  177. sub assertCanEdit {
  178. my ($series_id) = @_;
  179. my $user = Bugzilla->user;
  180. return if $user->in_group('admin');
  181. my $dbh = Bugzilla->dbh;
  182. my $iscreator = $dbh->selectrow_array("SELECT CASE WHEN creator = ? " .
  183. "THEN 1 ELSE 0 END FROM series " .
  184. "WHERE series_id = ?", undef,
  185. $user->id, $series_id);
  186. $iscreator || ThrowUserError("illegal_series_edit");
  187. }
  188. # Check if the user is permitted to create this series with these parameters.
  189. sub assertCanCreate {
  190. my ($cgi) = shift;
  191. Bugzilla->user->in_group("editbugs") || ThrowUserError("illegal_series_creation");
  192. # Check permission for frequency
  193. my $min_freq = 7;
  194. if ($cgi->param('frequency') < $min_freq && !Bugzilla->user->in_group("admin")) {
  195. ThrowUserError("illegal_frequency", { 'minimum' => $min_freq });
  196. }
  197. }
  198. sub validateWidthAndHeight {
  199. $vars->{'width'} = $cgi->param('width');
  200. $vars->{'height'} = $cgi->param('height');
  201. if (defined($vars->{'width'})) {
  202. (detaint_natural($vars->{'width'}) && $vars->{'width'} > 0)
  203. || ThrowCodeError("invalid_dimensions");
  204. }
  205. if (defined($vars->{'height'})) {
  206. (detaint_natural($vars->{'height'}) && $vars->{'height'} > 0)
  207. || ThrowCodeError("invalid_dimensions");
  208. }
  209. # The equivalent of 2000 square seems like a very reasonable maximum size.
  210. # This is merely meant to prevent accidental or deliberate DOS, and should
  211. # have no effect in practice.
  212. if ($vars->{'width'} && $vars->{'height'}) {
  213. (($vars->{'width'} * $vars->{'height'}) <= 4000000)
  214. || ThrowUserError("chart_too_large");
  215. }
  216. }
  217. sub edit {
  218. my $series = shift;
  219. $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
  220. $vars->{'creator'} = new Bugzilla::User($series->{'creator'});
  221. $vars->{'default'} = $series;
  222. print $cgi->header();
  223. $template->process("reports/edit-series.html.tmpl", $vars)
  224. || ThrowTemplateError($template->error());
  225. }
  226. sub plot {
  227. validateWidthAndHeight();
  228. $vars->{'chart'} = new Bugzilla::Chart($cgi);
  229. my $format = $template->get_format("reports/chart", "", scalar($cgi->param('ctype')));
  230. # Debugging PNGs is a pain; we need to be able to see the error messages
  231. if ($cgi->param('debug')) {
  232. print $cgi->header();
  233. $vars->{'chart'}->dump();
  234. }
  235. print $cgi->header($format->{'ctype'});
  236. disable_utf8() if ($format->{'ctype'} =~ /^image\//);
  237. $template->process($format->{'template'}, $vars)
  238. || ThrowTemplateError($template->error());
  239. }
  240. sub wrap {
  241. validateWidthAndHeight();
  242. # We create a Chart object so we can validate the parameters
  243. my $chart = new Bugzilla::Chart($cgi);
  244. $vars->{'time'} = time();
  245. $vars->{'imagebase'} = $cgi->canonicalise_query(
  246. "action", "action-wrap", "ctype", "format", "width", "height");
  247. print $cgi->header();
  248. $template->process("reports/chart.html.tmpl", $vars)
  249. || ThrowTemplateError($template->error());
  250. }
  251. sub view {
  252. my $chart = shift;
  253. # Set defaults
  254. foreach my $field ('category', 'subcategory', 'name', 'ctype') {
  255. $vars->{'default'}{$field} = $cgi->param($field) || 0;
  256. }
  257. # Pass the state object to the display UI.
  258. $vars->{'chart'} = $chart;
  259. $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
  260. print $cgi->header();
  261. # If we have having problems with bad data, we can set debug=1 to dump
  262. # the data structure.
  263. $chart->dump() if $cgi->param('debug');
  264. $template->process("reports/create-chart.html.tmpl", $vars)
  265. || ThrowTemplateError($template->error());
  266. }