1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4#
5# This Source Code Form is "Incompatible With Secondary Licenses", as
6# defined by the Mozilla Public License, v. 2.0.
7
8use strict;
9
10# This module implements a series - a set of data to be plotted on a chart.
11#
12# This Series is in the database if and only if self->{'series_id'} is defined.
13# Note that the series being in the database does not mean that the fields of
14# this object are the same as the DB entries, as the object may have been
15# altered.
16
17package Bugzilla::Series;
18
19use Bugzilla::Error;
20use Bugzilla::Util;
21
22# This is a hack so that we can re-use the rename_field_value
23# code from Bugzilla::Search::Saved.
24use constant DB_TABLE => 'series';
25use constant ID_FIELD => 'series_id';
26
27sub new {
28    my $invocant = shift;
29    my $class = ref($invocant) || $invocant;
30
31    # Create a ref to an empty hash and bless it
32    my $self = {};
33    bless($self, $class);
34
35    my $arg_count = scalar(@_);
36
37    # new() can return undef if you pass in a series_id and the user doesn't
38    # have sufficient permissions. If you create a new series in this way,
39    # you need to check for an undef return, and act appropriately.
40    my $retval = $self;
41
42    # There are three ways of creating Series objects. Two (CGI and Parameters)
43    # are for use when creating a new series. One (Database) is for retrieving
44    # information on existing series.
45    if ($arg_count == 1) {
46        if (ref($_[0])) {
47            # We've been given a CGI object to create a new Series from.
48            # This series may already exist - external code needs to check
49            # before it calls writeToDatabase().
50            $self->initFromCGI($_[0]);
51        }
52        else {
53            # We've been given a series_id, which should represent an existing
54            # Series.
55            $retval = $self->initFromDatabase($_[0]);
56        }
57    }
58    elsif ($arg_count >= 6 && $arg_count <= 8) {
59        # We've been given a load of parameters to create a new Series from.
60        # Currently, undef is always passed as the first parameter; this allows
61        # you to call writeToDatabase() unconditionally.
62        # XXX - You cannot set category_id and subcategory_id from here.
63        $self->initFromParameters(@_);
64    }
65    else {
66        die("Bad parameters passed in - invalid number of args: $arg_count");
67    }
68
69    return $retval;
70}
71
72sub initFromDatabase {
73    my ($self, $series_id) = @_;
74    my $dbh = Bugzilla->dbh;
75    my $user = Bugzilla->user;
76
77    detaint_natural($series_id)
78      || ThrowCodeError("invalid_series_id", { 'series_id' => $series_id });
79
80    my $grouplist = $user->groups_as_string;
81
82    my @series = $dbh->selectrow_array("SELECT series.series_id, cc1.name, " .
83        "cc2.name, series.name, series.creator, series.frequency, " .
84        "series.query, series.is_public, series.category, series.subcategory " .
85        "FROM series " .
86        "INNER JOIN series_categories AS cc1 " .
87        "    ON series.category = cc1.id " .
88        "INNER JOIN series_categories AS cc2 " .
89        "    ON series.subcategory = cc2.id " .
90        "LEFT JOIN category_group_map AS cgm " .
91        "    ON series.category = cgm.category_id " .
92        "    AND cgm.group_id NOT IN($grouplist) " .
93        "WHERE series.series_id = ? " .
94        "    AND (creator = ? OR (is_public = 1 AND cgm.category_id IS NULL))",
95        undef, ($series_id, $user->id));
96
97    if (@series) {
98        $self->initFromParameters(@series);
99        return $self;
100    }
101    else {
102        return undef;
103    }
104}
105
106sub initFromParameters {
107    # Pass undef as the first parameter if you are creating a new series.
108    my $self = shift;
109
110    ($self->{'series_id'}, $self->{'category'},  $self->{'subcategory'},
111     $self->{'name'}, $self->{'creator_id'}, $self->{'frequency'},
112     $self->{'query'}, $self->{'public'}, $self->{'category_id'},
113     $self->{'subcategory_id'}) = @_;
114
115    # If the first parameter is undefined, check if this series already
116    # exists and update it series_id accordingly
117    $self->{'series_id'} ||= $self->existsInDatabase();
118}
119
120sub initFromCGI {
121    my $self = shift;
122    my $cgi = shift;
123
124    $self->{'series_id'} = $cgi->param('series_id') || undef;
125    if (defined($self->{'series_id'})) {
126        detaint_natural($self->{'series_id'})
127          || ThrowCodeError("invalid_series_id",
128                               { 'series_id' => $self->{'series_id'} });
129    }
130
131    $self->{'category'} = $cgi->param('category')
132      || $cgi->param('newcategory')
133      || ThrowUserError("missing_category");
134
135    $self->{'subcategory'} = $cgi->param('subcategory')
136      || $cgi->param('newsubcategory')
137      || ThrowUserError("missing_subcategory");
138
139    $self->{'name'} = $cgi->param('name')
140      || ThrowUserError("missing_name");
141
142    $self->{'creator_id'} = Bugzilla->user->id;
143
144    $self->{'frequency'} = $cgi->param('frequency');
145    detaint_natural($self->{'frequency'})
146      || ThrowUserError("missing_frequency");
147
148    $self->{'query'} = $cgi->canonicalise_query("format", "ctype", "action",
149                                        "category", "subcategory", "name",
150                                        "frequency", "public", "query_format");
151    trick_taint($self->{'query'});
152
153    $self->{'public'} = $cgi->param('public') ? 1 : 0;
154
155    # Change 'admin' here and in series.html.tmpl, or remove the check
156    # completely, if you want to change who can make series public.
157    $self->{'public'} = 0 unless Bugzilla->user->in_group('admin');
158}
159
160sub writeToDatabase {
161    my $self = shift;
162
163    my $dbh = Bugzilla->dbh;
164    $dbh->bz_start_transaction();
165
166    my $category_id = getCategoryID($self->{'category'});
167    my $subcategory_id = getCategoryID($self->{'subcategory'});
168
169    my $exists;
170    if ($self->{'series_id'}) {
171        $exists =
172            $dbh->selectrow_array("SELECT series_id FROM series
173                                   WHERE series_id = $self->{'series_id'}");
174    }
175
176    # Is this already in the database?
177    if ($exists) {
178        # Update existing series
179        my $dbh = Bugzilla->dbh;
180        $dbh->do("UPDATE series SET " .
181                 "category = ?, subcategory = ?," .
182                 "name = ?, frequency = ?, is_public = ?  " .
183                 "WHERE series_id = ?", undef,
184                 $category_id, $subcategory_id, $self->{'name'},
185                 $self->{'frequency'}, $self->{'public'},
186                 $self->{'series_id'});
187    }
188    else {
189        # Insert the new series into the series table
190        $dbh->do("INSERT INTO series (creator, category, subcategory, " .
191                 "name, frequency, query, is_public) VALUES " .
192                 "(?, ?, ?, ?, ?, ?, ?)", undef,
193                 $self->{'creator_id'}, $category_id, $subcategory_id, $self->{'name'},
194                 $self->{'frequency'}, $self->{'query'}, $self->{'public'});
195
196        # Retrieve series_id
197        $self->{'series_id'} = $dbh->selectrow_array("SELECT MAX(series_id) " .
198                                                     "FROM series");
199        $self->{'series_id'}
200          || ThrowCodeError("missing_series_id", { 'series' => $self });
201    }
202
203    $dbh->bz_commit_transaction();
204}
205
206# Check whether a series with this name, category and subcategory exists in
207# the DB and, if so, returns its series_id.
208sub existsInDatabase {
209    my $self = shift;
210    my $dbh = Bugzilla->dbh;
211
212    my $category_id = getCategoryID($self->{'category'});
213    my $subcategory_id = getCategoryID($self->{'subcategory'});
214
215    trick_taint($self->{'name'});
216    my $series_id = $dbh->selectrow_array("SELECT series_id " .
217                              "FROM series WHERE category = $category_id " .
218                              "AND subcategory = $subcategory_id AND name = " .
219                              $dbh->quote($self->{'name'}));
220
221    return($series_id);
222}
223
224# Get a category or subcategory IDs, creating the category if it doesn't exist.
225sub getCategoryID {
226    my ($category) = @_;
227    my $category_id;
228    my $dbh = Bugzilla->dbh;
229
230    # This seems for the best idiom for "Do A. Then maybe do B and A again."
231    while (1) {
232        # We are quoting this to put it in the DB, so we can remove taint
233        trick_taint($category);
234
235        $category_id = $dbh->selectrow_array("SELECT id " .
236                                      "from series_categories " .
237                                      "WHERE name =" . $dbh->quote($category));
238
239        last if defined($category_id);
240
241        $dbh->do("INSERT INTO series_categories (name) " .
242                 "VALUES (" . $dbh->quote($category) . ")");
243    }
244
245    return $category_id;
246}
247
248##########
249# Methods
250##########
251sub id   { return $_[0]->{'series_id'}; }
252sub name { return $_[0]->{'name'}; }
253
254sub creator {
255    my $self = shift;
256
257    if (!$self->{creator} && $self->{creator_id}) {
258        require Bugzilla::User;
259        $self->{creator} = new Bugzilla::User($self->{creator_id});
260    }
261    return $self->{creator};
262}
263
264sub remove_from_db {
265    my $self = shift;
266    my $dbh = Bugzilla->dbh;
267
268    $dbh->do('DELETE FROM series WHERE series_id = ?', undef, $self->id);
269}
270
2711;
272