# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # # This Source Code Form is "Incompatible With Secondary Licenses", as # defined by the Mozilla Public License, v. 2.0. use strict; # This module implements a series - a set of data to be plotted on a chart. # # This Series is in the database if and only if self->{'series_id'} is defined. # Note that the series being in the database does not mean that the fields of # this object are the same as the DB entries, as the object may have been # altered. package Bugzilla::Series; use Bugzilla::Error; use Bugzilla::Util; # This is a hack so that we can re-use the rename_field_value # code from Bugzilla::Search::Saved. use constant DB_TABLE => 'series'; use constant ID_FIELD => 'series_id'; sub new { my $invocant = shift; my $class = ref($invocant) || $invocant; # Create a ref to an empty hash and bless it my $self = {}; bless($self, $class); my $arg_count = scalar(@_); # new() can return undef if you pass in a series_id and the user doesn't # have sufficient permissions. If you create a new series in this way, # you need to check for an undef return, and act appropriately. my $retval = $self; # There are three ways of creating Series objects. Two (CGI and Parameters) # are for use when creating a new series. One (Database) is for retrieving # information on existing series. if ($arg_count == 1) { if (ref($_[0])) { # We've been given a CGI object to create a new Series from. # This series may already exist - external code needs to check # before it calls writeToDatabase(). $self->initFromCGI($_[0]); } else { # We've been given a series_id, which should represent an existing # Series. $retval = $self->initFromDatabase($_[0]); } } elsif ($arg_count >= 6 && $arg_count <= 8) { # We've been given a load of parameters to create a new Series from. # Currently, undef is always passed as the first parameter; this allows # you to call writeToDatabase() unconditionally. # XXX - You cannot set category_id and subcategory_id from here. $self->initFromParameters(@_); } else { die("Bad parameters passed in - invalid number of args: $arg_count"); } return $retval; } sub initFromDatabase { my ($self, $series_id) = @_; my $dbh = Bugzilla->dbh; my $user = Bugzilla->user; detaint_natural($series_id) || ThrowCodeError("invalid_series_id", { 'series_id' => $series_id }); my $grouplist = $user->groups_as_string; my @series = $dbh->selectrow_array("SELECT series.series_id, cc1.name, " . "cc2.name, series.name, series.creator, series.frequency, " . "series.query, series.is_public, series.category, series.subcategory " . "FROM series " . "INNER JOIN series_categories AS cc1 " . " ON series.category = cc1.id " . "INNER JOIN series_categories AS cc2 " . " ON series.subcategory = cc2.id " . "LEFT JOIN category_group_map AS cgm " . " ON series.category = cgm.category_id " . " AND cgm.group_id NOT IN($grouplist) " . "WHERE series.series_id = ? " . " AND (creator = ? OR (is_public = 1 AND cgm.category_id IS NULL))", undef, ($series_id, $user->id)); if (@series) { $self->initFromParameters(@series); return $self; } else { return undef; } } sub initFromParameters { # Pass undef as the first parameter if you are creating a new series. my $self = shift; ($self->{'series_id'}, $self->{'category'}, $self->{'subcategory'}, $self->{'name'}, $self->{'creator_id'}, $self->{'frequency'}, $self->{'query'}, $self->{'public'}, $self->{'category_id'}, $self->{'subcategory_id'}) = @_; # If the first parameter is undefined, check if this series already # exists and update it series_id accordingly $self->{'series_id'} ||= $self->existsInDatabase(); } sub initFromCGI { my $self = shift; my $cgi = shift; $self->{'series_id'} = $cgi->param('series_id') || undef; if (defined($self->{'series_id'})) { detaint_natural($self->{'series_id'}) || ThrowCodeError("invalid_series_id", { 'series_id' => $self->{'series_id'} }); } $self->{'category'} = $cgi->param('category') || $cgi->param('newcategory') || ThrowUserError("missing_category"); $self->{'subcategory'} = $cgi->param('subcategory') || $cgi->param('newsubcategory') || ThrowUserError("missing_subcategory"); $self->{'name'} = $cgi->param('name') || ThrowUserError("missing_name"); $self->{'creator_id'} = Bugzilla->user->id; $self->{'frequency'} = $cgi->param('frequency'); detaint_natural($self->{'frequency'}) || ThrowUserError("missing_frequency"); $self->{'query'} = $cgi->canonicalise_query("format", "ctype", "action", "category", "subcategory", "name", "frequency", "public", "query_format"); trick_taint($self->{'query'}); $self->{'public'} = $cgi->param('public') ? 1 : 0; # Change 'admin' here and in series.html.tmpl, or remove the check # completely, if you want to change who can make series public. $self->{'public'} = 0 unless Bugzilla->user->in_group('admin'); } sub writeToDatabase { my $self = shift; my $dbh = Bugzilla->dbh; $dbh->bz_start_transaction(); my $category_id = getCategoryID($self->{'category'}); my $subcategory_id = getCategoryID($self->{'subcategory'}); my $exists; if ($self->{'series_id'}) { $exists = $dbh->selectrow_array("SELECT series_id FROM series WHERE series_id = $self->{'series_id'}"); } # Is this already in the database? if ($exists) { # Update existing series my $dbh = Bugzilla->dbh; $dbh->do("UPDATE series SET " . "category = ?, subcategory = ?," . "name = ?, frequency = ?, is_public = ? " . "WHERE series_id = ?", undef, $category_id, $subcategory_id, $self->{'name'}, $self->{'frequency'}, $self->{'public'}, $self->{'series_id'}); } else { # Insert the new series into the series table $dbh->do("INSERT INTO series (creator, category, subcategory, " . "name, frequency, query, is_public) VALUES " . "(?, ?, ?, ?, ?, ?, ?)", undef, $self->{'creator_id'}, $category_id, $subcategory_id, $self->{'name'}, $self->{'frequency'}, $self->{'query'}, $self->{'public'}); # Retrieve series_id $self->{'series_id'} = $dbh->selectrow_array("SELECT MAX(series_id) " . "FROM series"); $self->{'series_id'} || ThrowCodeError("missing_series_id", { 'series' => $self }); } $dbh->bz_commit_transaction(); } # Check whether a series with this name, category and subcategory exists in # the DB and, if so, returns its series_id. sub existsInDatabase { my $self = shift; my $dbh = Bugzilla->dbh; my $category_id = getCategoryID($self->{'category'}); my $subcategory_id = getCategoryID($self->{'subcategory'}); trick_taint($self->{'name'}); my $series_id = $dbh->selectrow_array("SELECT series_id " . "FROM series WHERE category = $category_id " . "AND subcategory = $subcategory_id AND name = " . $dbh->quote($self->{'name'})); return($series_id); } # Get a category or subcategory IDs, creating the category if it doesn't exist. sub getCategoryID { my ($category) = @_; my $category_id; my $dbh = Bugzilla->dbh; # This seems for the best idiom for "Do A. Then maybe do B and A again." while (1) { # We are quoting this to put it in the DB, so we can remove taint trick_taint($category); $category_id = $dbh->selectrow_array("SELECT id " . "from series_categories " . "WHERE name =" . $dbh->quote($category)); last if defined($category_id); $dbh->do("INSERT INTO series_categories (name) " . "VALUES (" . $dbh->quote($category) . ")"); } return $category_id; } ########## # Methods ########## sub id { return $_[0]->{'series_id'}; } sub name { return $_[0]->{'name'}; } sub creator { my $self = shift; if (!$self->{creator} && $self->{creator_id}) { require Bugzilla::User; $self->{creator} = new Bugzilla::User($self->{creator_id}); } return $self->{creator}; } sub remove_from_db { my $self = shift; my $dbh = Bugzilla->dbh; $dbh->do('DELETE FROM series WHERE series_id = ?', undef, $self->id); } 1;