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