#!/usr/bin/perl # $Id: OO.pm,v 1.9 2006/03/18 23:02:38 hudson Exp $ # # The contents of this file are subject to the Mozilla Public # License Version 1.1 (the "License"); you may not use this file # except in compliance with the License. You may obtain a copy of # the License at http://www.mozilla.org/MPL/ # # Software distributed under the License is distributed on an "AS # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or # implied. See the License for the specific language governing # rights and limitations under the License. # # # OO interface to Bugzilla bugs. # This should be provided by the official bugzilla source, but for some # reason they don't have a rational object model to interface with the # database. Rather than issuing explict SQL commands, this wraps the # bugs into objects with methods that act on the database records in # a safe manner. # # There are so many things wrong with the way Bugzilla has implemented # its libraries, starting with the bogus 'globals.pl' that every routine # must include and that all programs must run from the base directory. # # (c) 20060318 by Trammell Hudson # use warnings; use strict; # Force the lib directory # and other bone-headed Bugzilla stuff # You will need to update the path to point to your Bugzilla # directory. BEGIN { my $bugzilla_dir = '/var/www/html/b'; chdir $bugzilla_dir; push @INC, $bugzilla_dir; } require 'globals.pl'; use Bugzilla::Config qw( :DEFAULT $datadir ); package Bugzilla::OO; =head1 NAME Bugzilla::OO - Object Oriented interface to Bugzilla's bug database =head1 SYNOPSIS use Bugzilla::OO; # Log in as user@example.com my $session = Bugzilla::OO->new( 'user@example.com' ); # Close out bug 471 my $bug = $session->get_bug( 471 ); $bug->add_comment( "This bug is fixed" ); $bug->set_assigned_to( 'user2@example.com' ); $bug->set_status( 'RESOLVED' ); $bug->set_resolution( 'FIXED' ); $bug->send_mail(); =head1 DESCRIPTION The Bugzilla::Bug object is very difficult to work with outside of the web server tree. It doesn't give easy access to the database fields, requiring most users to write their own raw SQL commands. This is a bad thing, since it exposes far more of the database than the users should know about. Additionally, there are many operations that requiring multiple tables to be updated. If the user doesn't know that the activity log must be updated when the status field is changed, well, then you lose that data. These object methods keep everything in sync so that you do not need to know about the multiple phases of the operations. This package encapsulates many common bug operations (setting fields, marking duplicates, closing bugs, etc). It doesn't do all of them yet, but it is still a work in progress. =head1 Session Methods =over =cut # # I -WISH- the Bugzilla::Bug interface actually worked. It doesn't # properly update all the tables, so we have to do this by hand. # Who taught these bozos how to program databases? # my %queries = ( get_uid_by_email => <<"", SELECT userid FROM profiles WHERE login_name = ? get_bug_by_id => <<"", SELECT bug_id FROM bugs WHERE bug_id = ? get_bug_by_alias => <<"", SELECT bug_id FROM bugs WHERE alias = ? get_status_by_name => <<"", SELECT value, id FROM bug_status WHERE value = ? AND isactive get_resolution_by_name => <<"", SELECT value, id FROM resolution WHERE value = ? AND isactive # # Flag a bug as a duplicate # mark_duplicate => <<"", INSERT INTO duplicates( dupe_of, dupe ) VALUES ( ?, ? ) ON DUPLICATE KEY UPDATE dupe_of = ? # # Retrieve the duplicate (if any) that this bug is marked as # get_duplicate => <<"", SELECT dupe_of FROM duplicates WHERE dupe = ? # # Retrieve the bugs (if any) that are marked as duplicate of this # one. Must call in array context! # get_duplicates_of => <<"", SELECT dupe FROM duplicates WHERE dupe_of = ? # # Attach a comment to the bug # add_comment => <<"", INSERT INTO longdescs( bug_id, who, bug_when, thetext, isprivate ) VALUES( ?, ?, ?, ?, ? ); # # Attach a file attachment to the bug # add_attachment => <<"", INSERT INTO attachments( bug_id, submitter_id, creation_ts, description, filename, mimetype, ispatch, thedata ) VALUES( ?, ?, ?, ?, ?, ?, ?, ? ); # # Get field ID # get_field_id => <<"", SELECT fieldid FROM fielddefs WHERE name = ? ); # # Two phase update of the fields and activity logs for each of # the fields in the bug object. We automatically generate the # SQL commands for them here to ensure that things all work right. # # Bug 11 notes that setting assigned_to doesn't properly update # the activity log. # for my $field_name (qw/ assigned_to attachments.description attachments.filename attachments.isobsolete attachments.ispatch attachments.isprivate attachments.mimetype attachments.thedata blocked bug_file_loc bug_group bug_id bug_severity bug_status cc cclist_accessible classification commenter component content days_elapsed deadline delta_ts dependson estimated_time everconfirmed flagtypes.name keywords longdesc op_sys owner_idle_time percentage_complete priority product qa_contact remaining_time reporter reporter_accessible rep_platform requestees.login_name resolution setters.login_name short_desc status_whiteboard target_milestone version votes work_time /) { my $func_name = $field_name; $func_name =~ s/\./_/g; $queries{"set_$func_name"} = <<""; UPDATE bugs SET $field_name = ? WHERE bug_id = ? $queries{"activity_$func_name"} = <<""; INSERT INTO bugs_activity( bug_id, who, bug_when, added, removed, fieldid ) VALUES( ?, ?, ?, ?, (SELECT $field_name FROM bugs WHERE bug_id = ? ), (SELECT fieldid FROM fielddefs WHERE name = "$field_name" ) ); # Write the default accessor functions, too? (Currently disabled) $Bugzilla::OO::Bug::{"set_$func_name"} = sub { my $bug = shift; my $new_value = shift; warn "Default $func_name called\n"; $bug->set_activity( "set_$func_name" => $new_value, "activity_$func_name" => $new_value, ); } if 0; } =item C Construct a session object as the specified user. Returns undef if the user id does not exist =cut sub new { my $class = shift; my $email = shift; my $session = bless { email => $email, dbh => Bugzilla->dbh, }, $class; $session->{uid} = $session->query( get_uid_by_email => $email ) or return; return $session; } =item C Constructs a Bugzilla::OO::Bug object from the database based on the bug id. If the bug is not numeric, then it will look up by alias. Returns undef if no such bug exists. =cut sub get_bug { my $session = shift; my $id = shift; $id = 0 || $session->query( get_bug_by_id => $id ) || $session->query( get_bug_by_alias => $id ) or return; my $bug = bless { session => $session, id => $id, }, 'Bugzilla::OO::Bug'; $bug->set_time_stamp(); return $bug; } =item C Internal method to execute a simple query on the database returning an array of the row. =cut sub query { my $session = shift; my $name = shift; my $query = $session->execute( $name => @_ ) or return; my @rc = $query->fetchrow_array or return; $query->finish; # If they are in a scalar context, return just the first column return $rc[0] unless wantarray; return @rc; } =item C Internal method to run a stored statement against the database. Statements are stored in the C<%queries> hash table as strings until they are used for the first time, at which point they will be compiled with DBI::prepare to speed up future executions. =cut sub execute { my $session = shift; my $name = shift; unless( exists $queries{$name} ) { my $function = caller(1); warn "$function: No such query '$name'\n"; return; } my $query = $queries{$name}; unless( ref $query ) { # Pre-compile it! $query = $queries{$name} = $session->{dbh}->prepare( $query ); } $query->execute( @_ ) or return; # Return the query object, allowing the caller to call fetchrow on it return $query; } package Bugzilla::OO::Bug; =back =head1 Bug Object Methods The Bug object is a subclass in the C tree that has the methods for dealing with bugs. =over =cut =item C Sets the time stamp for operations on the Bug. This is set to the time that the bug is retrieved by C or C, so you should not have to call it again unless you want to set the time to something else. =cut sub set_time_stamp { my $bug = shift; my $time = shift || time; my ($sec,$min,$hour,$mday,$mon,$year) = localtime( $time ); $bug->{now} = sprintf "%04d-%02d-%02d %02d:%02d:%02d", $year + 1900, $mon + 1, $mday, $hour, $min, $sec, ; print STDERR "Bug $bug->{id}: operations are as of $bug->{now}\n"; } =item C Add a comment to the bug. =cut sub add_comment { my $self = shift; my $body = shift; my $private = shift || 0; $self->{session}->execute( add_comment => $self->{id}, $self->{session}->{uid}, $self->{now}, $body, $private, ); } =item C Add an attachment to the bug. We should be able to determine if it is a patch automatically, but we depend on the user to tell us. =cut sub add_attachment { my $bug = shift; my $name = shift; my $filename = shift; my $mime_type = shift; my $is_patch = shift; my $body = shift; $bug->{session}->execute( add_attachment => $bug->{id}, $bug->{session}->{uid}, $bug->{now}, $name, $filename, $mime_type, $is_patch, $body ); } =item C Internal method to do the two-phase update and add to the activity log. This is not one that most users will need to call. =cut sub set_activity { my $bug = shift; my $func_name = shift; my $new_field = shift; my $new_activity = shift; $new_activity = $new_field unless defined $new_activity; # Add to the activity log first $bug->{session}->execute( "activity_$func_name" => $bug->{id}, $bug->{session}->{uid}, $bug->{now}, $new_activity, $bug->{id}, ) or return; # Then update the field value $bug->{session}->execute( "set_$func_name" => $new_field, $bug->{id}, ) or return; # Commit our transaction? return 1; } =item C Assign this bug to another user. =cut sub set_assigned_to { my $bug = shift; my $new_email = shift; my $new_uid = $bug->{session}->query( get_uid_by_email => $new_email ) or return; $bug->set_activity( assigned_to => $new_uid, $new_email, ); } =item C Set the status of the bug, as well as update the activity log. This sort of two-phase operation really should be in a transaction. Allowable status values are: ASSIGNED CLOSED NEW REOPENED RESOLVED UNCONFIRMED VERIFIED =cut sub set_bug_status { my $bug = shift; my $new_status = shift; # Validate the status name is correct $new_status = $bug->{session}->query( get_status_by_name => $new_status ) or return; $bug->set_activity( bug_status => $new_status ); } =item C Changes the resolution of the bug (not the status). Allowable resolutions are the empty string and one of the following: DUPLICATE FIXED INVALID LATER MOVED REMIND WONTFIX WORKSFORME =cut sub set_resolution { my $bug = shift; my $resolution = shift; $resolution = $bug->{session}->query( get_resolution_by_name => $resolution ) or return; $bug->set_activity( resolution => $resolution ); } =item C Return the bug id that this one is marked as a duplicate of, if any. =cut sub get_duplicate { my $bug = shift; return $bug->{session}->query( get_duplicate => $bug->{id} ); } =item C Flag this bug as a duplicate of another bug and add comments to both bugs noting the duplication. =cut sub mark_duplicate { my $bug = shift; my $session = $bug->{session}; my $new_bug_id = shift; my $new_bug = $session->get_bug( $new_bug_id ) or return; # Are they creating a circular loop? return if $new_bug_id == $bug->{id}; # Set our status and resolution for this bug $bug->set_activity( bug_status => 'RESOLVED' ) or return; $bug->set_activity( resolution => 'DUPLICATE' ) or return; # If we already have a duplicate marked, remove it my $old_dupe_id = $bug->get_duplicate(); if( $old_dupe_id and $old_dupe_id != $new_bug_id ) { my $old_dupe_bug = $session->get_bug( $old_dupe_id ); $old_dupe_bug->add_comment( <<"" ); *** Bug $bug->{id} is no longer marked as a duplicate of this bug *** *** It is now a duplicate of bug $new_bug_id *** } # If the bug is not already a dupe, then add comments for each $new_bug->add_comment( <<"" ) or return; *** Bug $bug->{id} has been marked as a duplicate of this bug *** $bug->add_comment( <<"" ) or return; *** This bug has been marked as duplicate of bug $new_bug_id *** @{[ $old_dupe_id ? "*** Was a duplicate of bug $old_dupe_id ***" : ""]} # Update the duplicate table $session->execute( mark_duplicate => $new_bug->{id}, $bug->{id}, $new_bug->{id}, ); # Should commit a transaction! return 1; } =back =head1 BUGS Many. This is still a work in progress =head1 SEE ALSO L =cut "0, but true"; __END__