bzr branch
/bmo/4.2
| Line | Revision | Contents |
| 1 | 2193 | #!/usr/bin/perl -wT |
| 2 | 2188 | # -*- 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 | 3761 | # Lance Larsh <lance.larsh@oracle.com> |
| 23 | 6764 | # Frédéric Buclin <LpSolit@gmail.com> |
| 24 | 2188 | |
| 25 | # Glossary: |
|
| 26 | 2441 | # series: An individual, defined set of data plotted over time. |
| 27 | # data set: What a series is called in the UI. |
|
| 28 | # line: A set of one or more series, to be summed and drawn as a single |
|
| 29 | # line when the series is plotted. |
|
| 30 | # chart: A set of lines |
|
| 31 | # |
|
| 32 | 2188 | # So when you select rows in the UI, you are selecting one or more lines, not |
| 33 | # series. |
|
| 34 | ||
| 35 | # Generic Charting TODO: |
|
| 36 | # |
|
| 37 | # JS-less chart creation - hard. |
|
| 38 | # Broken image on error or no data - need to do much better. |
|
| 39 | 4523 | # Centralise permission checking, so Bugzilla->user->in_group('editbugs') |
| 40 | # not scattered everywhere. |
|
| 41 | 2188 | # User documentation :-) |
| 42 | # |
|
| 43 | # Bonus: |
|
| 44 | # Offer subscription when you get a "series already exists" error? |
|
| 45 | ||
| 46 | use strict; |
|
| 47 | 5304 | use lib qw(. lib); |
| 48 | 2188 | |
| 49 | 3582 | use Bugzilla; |
| 50 | 2537 | use Bugzilla::Constants; |
| 51 | 6696 | use Bugzilla::CGI; |
| 52 | 4296 | use Bugzilla::Error; |
| 53 | use Bugzilla::Util; |
|
| 54 | 2188 | use Bugzilla::Chart; |
| 55 | use Bugzilla::Series; |
|
| 56 | 3251 | use Bugzilla::User; |
| 57 | 6764 | use Bugzilla::Token; |
| 58 | 2188 | |
| 59 | 4329 | # For most scripts we don't make $cgi and $template global variables. But |
| 60 | # when preparing Bugzilla for mod_perl, this script used these |
|
| 61 | # variables in so many subroutines that it was easier to just |
|
| 62 | # make them globals. |
|
| 63 | local our $cgi = Bugzilla->cgi; |
|
| 64 | local our $template = Bugzilla->template; |
|
| 65 | local our $vars = {}; |
|
| 66 | 6764 | my $dbh = Bugzilla->dbh; |
| 67 | 2188 | |
| 68 | 6778 | my $user = Bugzilla->login(LOGIN_REQUIRED); |
| 69 | ||
| 70 | if (!Bugzilla->feature('new_charts')) { |
|
| 71 | ThrowCodeError('feature_disabled', { feature => 'new_charts' }); |
|
| 72 | } |
|
| 73 | ||
| 74 | 2188 | # Go back to query.cgi if we are adding a boolean chart parameter. |
| 75 | if (grep(/^cmd-/, $cgi->param())) { |
|
| 76 | my $params = $cgi->canonicalise_query("format", "ctype", "action"); |
|
| 77 | 7673 | print $cgi->redirect("query.cgi?format=" . $cgi->param('query_format') . |
| 78 | ($params ? "&$params" : "")); |
|
| 79 | 2188 | exit; |
| 80 | } |
|
| 81 | ||
| 82 | my $action = $cgi->param('action'); |
|
| 83 | my $series_id = $cgi->param('series_id'); |
|
| 84 | 5323 | $vars->{'doc_section'} = 'reporting.html#charts'; |
| 85 | 2188 | |
| 86 | # Because some actions are chosen by buttons, we can't encode them as the value |
|
| 87 | 5138 | # of the action param, because that value is localization-dependent. So, we |
| 88 | 2188 | # encode it in the name, as "action-<action>". Some params even contain the |
| 89 | 4846 | # series_id they apply to (e.g. subscribe, unsubscribe). |
| 90 | 2188 | my @actions = grep(/^action-/, $cgi->param()); |
| 91 | if ($actions[0] && $actions[0] =~ /^action-([^\d]+)(\d*)$/) { |
|
| 92 | $action = $1; |
|
| 93 | $series_id = $2 if $2; |
|
| 94 | } |
|
| 95 | ||
| 96 | $action ||= "assemble"; |
|
| 97 | ||
| 98 | # Go to buglist.cgi if we are doing a search. |
|
| 99 | if ($action eq "search") { |
|
| 100 | my $params = $cgi->canonicalise_query("format", "ctype", "action"); |
|
| 101 | 7673 | print $cgi->redirect("buglist.cgi" . ($params ? "?$params" : "")); |
| 102 | 2188 | exit; |
| 103 | } |
|
| 104 | ||
| 105 | 6764 | $user->in_group(Bugzilla->params->{"chartgroup"}) |
| 106 | 4317 | || ThrowUserError("auth_failure", {group => Bugzilla->params->{"chartgroup"}, |
| 107 | 3001 | action => "use", |
| 108 | object => "charts"}); |
|
| 109 | 2648 | |
| 110 | 2441 | # Only admins may create public queries |
| 111 | 6764 | $user->in_group('admin') || $cgi->delete('public'); |
| 112 | 2441 | |
| 113 | 2188 | # All these actions relate to chart construction. |
| 114 | if ($action =~ /^(assemble|add|remove|sum|subscribe|unsubscribe)$/) { |
|
| 115 | # These two need to be done before the creation of the Chart object, so |
|
| 116 | # that the changes they make will be reflected in it. |
|
| 117 | if ($action =~ /^subscribe|unsubscribe$/) { |
|
| 118 | 2360 | detaint_natural($series_id) || ThrowCodeError("invalid_series_id"); |
| 119 | 2188 | my $series = new Bugzilla::Series($series_id); |
| 120 | 3780 | $series->$action($user->id); |
| 121 | 2188 | } |
| 122 | ||
| 123 | my $chart = new Bugzilla::Chart($cgi); |
|
| 124 | ||
| 125 | if ($action =~ /^remove|sum$/) { |
|
| 126 | $chart->$action(getSelectedLines()); |
|
| 127 | } |
|
| 128 | elsif ($action eq "add") { |
|
| 129 | my @series_ids = getAndValidateSeriesIDs(); |
|
| 130 | $chart->add(@series_ids); |
|
| 131 | } |
|
| 132 | ||
| 133 | view($chart); |
|
| 134 | } |
|
| 135 | elsif ($action eq "plot") { |
|
| 136 | plot(); |
|
| 137 | } |
|
| 138 | elsif ($action eq "wrap") { |
|
| 139 | # For CSV "wrap", we go straight to "plot". |
|
| 140 | if ($cgi->param('ctype') && $cgi->param('ctype') eq "csv") { |
|
| 141 | plot(); |
|
| 142 | } |
|
| 143 | else { |
|
| 144 | wrap(); |
|
| 145 | } |
|
| 146 | } |
|
| 147 | elsif ($action eq "create") { |
|
| 148 | assertCanCreate($cgi); |
|
| 149 | 7669 | my $token = $cgi->param('token'); |
| 150 | check_hash_token($token, ['create-series']); |
|
| 151 | 2441 | |
| 152 | 2188 | my $series = new Bugzilla::Series($cgi); |
| 153 | ||
| 154 | 6763 | ThrowUserError("series_already_exists", {'series' => $series}) |
| 155 | if $series->existsInDatabase; |
|
| 156 | 2188 | |
| 157 | 6763 | $series->writeToDatabase(); |
| 158 | $vars->{'message'} = "series_created"; |
|
| 159 | 2188 | $vars->{'series'} = $series; |
| 160 | ||
| 161 | 6763 | my $chart = new Bugzilla::Chart($cgi); |
| 162 | view($chart); |
|
| 163 | 2188 | } |
| 164 | elsif ($action eq "edit") { |
|
| 165 | 6764 | my $series = assertCanEdit($series_id); |
| 166 | 2188 | edit($series); |
| 167 | } |
|
| 168 | elsif ($action eq "alter") { |
|
| 169 | 7669 | my $series = assertCanEdit($series_id); |
| 170 | my $token = $cgi->param('token'); |
|
| 171 | check_hash_token($token, [$series->id, $series->name]); |
|
| 172 | 6764 | # XXX - This should be replaced by $series->set_foo() methods. |
| 173 | 7669 | $series = new Bugzilla::Series($cgi); |
| 174 | 2441 | |
| 175 | # We need to check if there is _another_ series in the database with |
|
| 176 | # our (potentially new) name. So we call existsInDatabase() to see if |
|
| 177 | # the return value is us or some other series we need to avoid stomping |
|
| 178 | # on. |
|
| 179 | my $id_of_series_in_db = $series->existsInDatabase(); |
|
| 180 | if (defined($id_of_series_in_db) && |
|
| 181 | $id_of_series_in_db != $series->{'series_id'}) |
|
| 182 | { |
|
| 183 | ThrowUserError("series_already_exists", {'series' => $series}); |
|
| 184 | } |
|
| 185 | ||
| 186 | 2360 | $series->writeToDatabase(); |
| 187 | 2441 | $vars->{'changes_saved'} = 1; |
| 188 | 2360 | |
| 189 | 2188 | edit($series); |
| 190 | } |
|
| 191 | 6764 | elsif ($action eq "confirm-delete") { |
| 192 | $vars->{'series'} = assertCanEdit($series_id); |
|
| 193 | ||
| 194 | print $cgi->header(); |
|
| 195 | $template->process("reports/delete-series.html.tmpl", $vars) |
|
| 196 | || ThrowTemplateError($template->error()); |
|
| 197 | } |
|
| 198 | elsif ($action eq "delete") { |
|
| 199 | my $series = assertCanEdit($series_id); |
|
| 200 | my $token = $cgi->param('token'); |
|
| 201 | check_hash_token($token, [$series->id, $series->name]); |
|
| 202 | ||
| 203 | $dbh->bz_start_transaction(); |
|
| 204 | ||
| 205 | $series->remove_from_db(); |
|
| 206 | # Remove (sub)categories which no longer have any series. |
|
| 207 | 7581 | foreach my $cat (qw(category subcategory)) { |
| 208 | 6764 | my $is_used = $dbh->selectrow_array("SELECT COUNT(*) FROM series WHERE $cat = ?", |
| 209 | undef, $series->{"${cat}_id"}); |
|
| 210 | if (!$is_used) { |
|
| 211 | $dbh->do('DELETE FROM series_categories WHERE id = ?', |
|
| 212 | undef, $series->{"${cat}_id"}); |
|
| 213 | } |
|
| 214 | } |
|
| 215 | $dbh->bz_commit_transaction(); |
|
| 216 | ||
| 217 | $vars->{'message'} = "series_deleted"; |
|
| 218 | $vars->{'series'} = $series; |
|
| 219 | view(); |
|
| 220 | } |
|
| 221 | 6696 | elsif ($action eq "convert_search") { |
| 222 | my $saved_search = $cgi->param('series_from_search') || ''; |
|
| 223 | my ($query) = grep { $_->name eq $saved_search } @{ $user->queries }; |
|
| 224 | my $url = ''; |
|
| 225 | if ($query) { |
|
| 226 | my $params = new Bugzilla::CGI($query->edit_link); |
|
| 227 | # These two parameters conflict with the one below. |
|
| 228 | $url = $params->canonicalise_query('format', 'query_format'); |
|
| 229 | $url = '&' . html_quote($url); |
|
| 230 | } |
|
| 231 | print $cgi->redirect(-location => correct_urlbase() . "query.cgi?format=create-series$url"); |
|
| 232 | } |
|
| 233 | 2188 | else { |
| 234 | 7188 | ThrowUserError('unknown_action', {action => $action}); |
| 235 | 2188 | } |
| 236 | ||
| 237 | exit; |
|
| 238 | ||
| 239 | # Find any selected series and return either the first or all of them. |
|
| 240 | sub getAndValidateSeriesIDs { |
|
| 241 | my @series_ids = grep(/^\d+$/, $cgi->param("name")); |
|
| 242 | ||
| 243 | return wantarray ? @series_ids : $series_ids[0]; |
|
| 244 | } |
|
| 245 | ||
| 246 | # Return a list of IDs of all the lines selected in the UI. |
|
| 247 | sub getSelectedLines { |
|
| 248 | my @ids = map { /^select(\d+)$/ ? $1 : () } $cgi->param(); |
|
| 249 | ||
| 250 | return @ids; |
|
| 251 | } |
|
| 252 | ||
| 253 | # Check if the user is the owner of series_id or is an admin. |
|
| 254 | sub assertCanEdit { |
|
| 255 | 6764 | my $series_id = shift; |
| 256 | 3780 | my $user = Bugzilla->user; |
| 257 | ||
| 258 | 6764 | my $series = new Bugzilla::Series($series_id) |
| 259 | || ThrowCodeError('invalid_series_id'); |
|
| 260 | ||
| 261 | if (!$user->in_group('admin') && $series->{creator_id} != $user->id) { |
|
| 262 | ThrowUserError('illegal_series_edit'); |
|
| 263 | } |
|
| 264 | ||
| 265 | return $series; |
|
| 266 | 2188 | } |
| 267 | ||
| 268 | # Check if the user is permitted to create this series with these parameters. |
|
| 269 | sub assertCanCreate { |
|
| 270 | my ($cgi) = shift; |
|
| 271 | 6764 | my $user = Bugzilla->user; |
| 272 | ||
| 273 | $user->in_group("editbugs") || ThrowUserError("illegal_series_creation"); |
|
| 274 | 2188 | |
| 275 | # Check permission for frequency |
|
| 276 | my $min_freq = 7; |
|
| 277 | 7905 | # Upstreaming: denied, as this min_freq feature is going away. |
| 278 | if ($cgi->param('frequency') < $min_freq && !$user->in_group("bz_canusewhines")) { |
|
| 279 | 2188 | ThrowUserError("illegal_frequency", { 'minimum' => $min_freq }); |
| 280 | 6764 | } |
| 281 | 2188 | } |
| 282 | ||
| 283 | sub validateWidthAndHeight { |
|
| 284 | $vars->{'width'} = $cgi->param('width'); |
|
| 285 | $vars->{'height'} = $cgi->param('height'); |
|
| 286 | ||
| 287 | if (defined($vars->{'width'})) { |
|
| 288 | (detaint_natural($vars->{'width'}) && $vars->{'width'} > 0) |
|
| 289 | || ThrowCodeError("invalid_dimensions"); |
|
| 290 | } |
|
| 291 | ||
| 292 | if (defined($vars->{'height'})) { |
|
| 293 | (detaint_natural($vars->{'height'}) && $vars->{'height'} > 0) |
|
| 294 | || ThrowCodeError("invalid_dimensions"); |
|
| 295 | } |
|
| 296 | ||
| 297 | # The equivalent of 2000 square seems like a very reasonable maximum size. |
|
| 298 | # This is merely meant to prevent accidental or deliberate DOS, and should |
|
| 299 | # have no effect in practice. |
|
| 300 | if ($vars->{'width'} && $vars->{'height'}) { |
|
| 301 | (($vars->{'width'} * $vars->{'height'}) <= 4000000) |
|
| 302 | || ThrowUserError("chart_too_large"); |
|
| 303 | } |
|
| 304 | } |
|
| 305 | ||
| 306 | sub edit { |
|
| 307 | my $series = shift; |
|
| 308 | ||
| 309 | $vars->{'category'} = Bugzilla::Chart::getVisibleSeries(); |
|
| 310 | 2441 | $vars->{'default'} = $series; |
| 311 | 2188 | |
| 312 | 2793 | print $cgi->header(); |
| 313 | 2188 | $template->process("reports/edit-series.html.tmpl", $vars) |
| 314 | || ThrowTemplateError($template->error()); |
|
| 315 | } |
|
| 316 | ||
| 317 | sub plot { |
|
| 318 | validateWidthAndHeight(); |
|
| 319 | $vars->{'chart'} = new Bugzilla::Chart($cgi); |
|
| 320 | ||
| 321 | 3630 | my $format = $template->get_format("reports/chart", "", scalar($cgi->param('ctype'))); |
| 322 | 2188 | |
| 323 | # Debugging PNGs is a pain; we need to be able to see the error messages |
|
| 324 | if ($cgi->param('debug')) { |
|
| 325 | 2793 | print $cgi->header(); |
| 326 | 2188 | $vars->{'chart'}->dump(); |
| 327 | } |
|
| 328 | ||
| 329 | 2793 | print $cgi->header($format->{'ctype'}); |
| 330 | 6407 | disable_utf8() if ($format->{'ctype'} =~ /^image\//); |
| 331 | ||
| 332 | 2188 | $template->process($format->{'template'}, $vars) |
| 333 | || ThrowTemplateError($template->error()); |
|
| 334 | } |
|
| 335 | ||
| 336 | sub wrap { |
|
| 337 | validateWidthAndHeight(); |
|
| 338 | ||
| 339 | # We create a Chart object so we can validate the parameters |
|
| 340 | my $chart = new Bugzilla::Chart($cgi); |
|
| 341 | ||
| 342 | 6197 | $vars->{'time'} = localtime(time()); |
| 343 | 2188 | |
| 344 | $vars->{'imagebase'} = $cgi->canonicalise_query( |
|
| 345 | 3393 | "action", "action-wrap", "ctype", "format", "width", "height"); |
| 346 | 2188 | |
| 347 | 2793 | print $cgi->header(); |
| 348 | 2188 | $template->process("reports/chart.html.tmpl", $vars) |
| 349 | || ThrowTemplateError($template->error()); |
|
| 350 | } |
|
| 351 | ||
| 352 | sub view { |
|
| 353 | my $chart = shift; |
|
| 354 | ||
| 355 | # Set defaults |
|
| 356 | foreach my $field ('category', 'subcategory', 'name', 'ctype') { |
|
| 357 | $vars->{'default'}{$field} = $cgi->param($field) || 0; |
|
| 358 | } |
|
| 359 | ||
| 360 | # Pass the state object to the display UI. |
|
| 361 | $vars->{'chart'} = $chart; |
|
| 362 | $vars->{'category'} = Bugzilla::Chart::getVisibleSeries(); |
|
| 363 | ||
| 364 | 2793 | print $cgi->header(); |
| 365 | 2188 | |
| 366 | # If we have having problems with bad data, we can set debug=1 to dump |
|
| 367 | # the data structure. |
|
| 368 | $chart->dump() if $cgi->param('debug'); |
|
| 369 | ||
| 370 | $template->process("reports/create-chart.html.tmpl", $vars) |
|
| 371 | || ThrowTemplateError($template->error()); |
|
| 372 | } |
Loggerhead 1.18.1 is a web-based interface for Bazaar branches