RSS

(root)/bmo/4.2 : /chart.cgi (revision 8095)

To get this branch, use:
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 = '&amp;' . 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