diff options
Diffstat (limited to 'extensions/ComponentWatching/Extension.pm')
-rw-r--r-- | extensions/ComponentWatching/Extension.pm | 473 |
1 files changed, 473 insertions, 0 deletions
diff --git a/extensions/ComponentWatching/Extension.pm b/extensions/ComponentWatching/Extension.pm new file mode 100644 index 000000000..b31f416ab --- /dev/null +++ b/extensions/ComponentWatching/Extension.pm @@ -0,0 +1,473 @@ +# 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. + +package Bugzilla::Extension::ComponentWatching; +use strict; +use warnings; +use 5.10.1; +use base qw(Bugzilla::Extension); + +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Group; +use Bugzilla::User; +use Bugzilla::User::Setting; +use Bugzilla::Util qw(detaint_natural trim trick_taint); + +use List::MoreUtils qw(any); + +our $VERSION = '2'; + +use constant REL_COMPONENT_WATCHER => 15; + +# +# installation +# + +sub db_schema_abstract_schema { + my ($self, $args) = @_; + + # Bugzilla 5.0+, the components.id type + # is INT3, while earlier versions used INT2 + my $component_id_type = 'INT2'; + my $len = scalar @{$args->{schema}->{components}->{FIELDS}}; + for (my $i = 0; $i < $len - 1; $i += 2) { + next if $args->{schema}->{components}->{FIELDS}->[$i] ne 'id'; + $component_id_type = 'INT3' + if $args->{schema}->{components}->{FIELDS}->[$i + 1]->{TYPE} eq + 'MEDIUMSERIAL'; + last; + } + $args->{'schema'}->{'component_watch'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + component_id => { + TYPE => $component_id_type, + NOTNULL => 0, + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE',} + }, + product_id => { + TYPE => 'INT2', + NOTNULL => 0, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE',} + }, + component_prefix => {TYPE => 'VARCHAR(64)', NOTNULL => 0,}, + ], + }; + + return; +} + +sub install_update_db { + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column('component_watch', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + ); + $dbh->bz_add_column('component_watch', 'component_prefix', + {TYPE => 'VARCHAR(64)', NOTNULL => 0,}); + + return; +} + +# +# templates +# + +sub template_before_create { + my ($self, $args) = @_; + my $config = $args->{config}; + my $constants = $config->{VARIABLES}->{constants}; + $constants->{REL_COMPONENT_WATCHER} = REL_COMPONENT_WATCHER; + + return; +} + +# +# preferences +# + +sub user_preferences { + my ($self, $args) = @_; + my $tab = $args->{'current_tab'}; + return unless $tab eq 'component_watch'; + + my $save = $args->{'save_changes'}; + my $handled = $args->{'handled'}; + my $vars = $args->{'vars'}; + my $user = Bugzilla->user; + my $input = Bugzilla->input_params; + + if ($save) { + if ($input->{'add'} && $input->{'add_product'}) { + + # add watch + + # load product and verify access + my $productName = $input->{'add_product'}; + my $product = Bugzilla::Product->new({name => $productName, cache => 1}); + unless ($product && $user->can_access_product($product)) { + ThrowUserError('product_access_denied', {product => $productName}); + } + + # starting-with + if (my $prefix = $input->{add_starting}) { + _addPrefixWatch($user, $product, $prefix); + + } + else { + my $ra_componentNames = $input->{'add_component'}; + $ra_componentNames = [$ra_componentNames || ''] unless ref($ra_componentNames); + + if (any { $_ eq '' } @$ra_componentNames) { + + # watching a product + _addProductWatch($user, $product); + + } + else { + # watching specific components + foreach my $componentName (@$ra_componentNames) { + my $component + = Bugzilla::Component->new({ + name => $componentName, product => $product, cache => 1 + }); + unless ($component) { + ThrowUserError('product_access_denied', {product => $productName}); + } + _addComponentWatch($user, $component); + } + } + } + + _addDefaultSettings($user); + + } + else { + # remove watch(s) + + my $delete + = ref $input->{del_watch} ? $input->{del_watch} : [$input->{del_watch}]; + foreach my $id (@$delete) { + next unless $id; ## REDHAT EXTENSION 1399101 + _deleteWatch($user, $id); + } + } + + } + + $vars->{'add_product'} = $input->{'product'}; + $vars->{'add_component'} = $input->{'component'}; + $vars->{'watches'} = _getWatches($user); + + $$handled = 1; + + return; +} + +# +# bugmail +# + +sub bugmail_recipients { + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + my $recipients = $args->{'recipients'}; + my $diffs = $args->{'diffs'}; + + my ($oldProductId, $newProductId) = ($bug->product_id, $bug->product_id); + my ($oldComponentId, $newComponentId) + = ($bug->component_id, $bug->component_id); + + # notify when the product/component is switch from one being watched + if (@$diffs) { + + # we need the product to process the component, so scan for that first + my $product; + foreach my $ra (@$diffs) { + next if !(exists $ra->{'old'} && exists $ra->{'field_name'}); + if ($ra->{'field_name'} eq 'product') { + $product = Bugzilla::Product->new({name => $ra->{'old'}, cache => 1}); + $oldProductId = $product->id; + } + } + if (!$product) { + $product = Bugzilla::Product->new({id => $oldProductId, cache => 1}); + } + foreach my $ra (@$diffs) { + next if !(exists $ra->{'old'} && exists $ra->{'field_name'}); + if ($ra->{'field_name'} eq 'component') { + my $component + = Bugzilla::Component->new({ + name => $ra->{'old'}, product => $product, cache => 1 + }); + next unless($component); + $oldComponentId = $component->id; + } + } + } + + # add component watchers + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare(" + SELECT user_id + FROM component_watch + WHERE ((product_id = ? OR product_id = ?) AND component_id IS NULL) + OR (component_id = ? OR component_id = ?) + UNION + SELECT user_id + FROM component_watch + INNER JOIN components ON components.product_id = component_watch.product_id + WHERE component_prefix IS NOT NULL + AND (component_watch.product_id = ? OR component_watch.product_id = ?) + AND components.name LIKE " + . $dbh->sql_string_concat('component_prefix', "'%'")); + + $sth->execute( + $oldProductId, $newProductId, $oldComponentId, + $newComponentId, $oldProductId, $newProductId + ); + while (my ($uid) = $sth->fetchrow_array) { + if (!exists $recipients->{$uid}) { + $recipients->{$uid}->{+REL_COMPONENT_WATCHER} + = Bugzilla::BugMail::BIT_WATCHING(); + } + } + + return; +} + +sub bugmail_relationships { + my ($self, $args) = @_; + my $relationships = $args->{relationships}; + $relationships->{+REL_COMPONENT_WATCHER} = 'Component-Watcher'; + + return; +} + +# +# db +# + +sub _getWatches { + my ($user) = @_; + my $dbh = Bugzilla->dbh; + + my $sth = $dbh->prepare(" + SELECT id, product_id, component_id, component_prefix + FROM component_watch + WHERE user_id = ? + "); + $sth->execute($user->id); + my @watches; + while (my ($id, $productId, $componentId, $prefix) = $sth->fetchrow_array) { + my $product = Bugzilla::Product->new({id => $productId, cache => 1}); + next unless $product && $user->can_access_product($product); + + my %watch = ( + id => $id, + product => $product, + product_name => $product->name, + component_name => '', + component_prefix => $prefix, + ); + if ($componentId) { + my $component = Bugzilla::Component->new({id => $componentId, cache => 1}); + next unless $component; + $watch{'component'} = $component; + $watch{'component_name'} = $component->name; + } + + push @watches, \%watch; + } + + @watches = sort { + $a->{'product_name'} cmp $b->{'product_name'} + || $a->{'component_name'} cmp $b->{'component_name'} + || $a->{'component_prefix'} cmp $b->{'component_prefix'} + } @watches; + + return \@watches; +} + +sub _addProductWatch { + my ($user, $product) = @_; + my $dbh = Bugzilla->dbh; + + my $sth = $dbh->prepare(" + SELECT 1 + FROM component_watch + WHERE user_id = ? AND product_id = ? AND component_id IS NULL + "); + $sth->execute($user->id, $product->id); + return if $sth->fetchrow_array; + + $sth = $dbh->prepare(" + DELETE FROM component_watch + WHERE user_id = ? AND product_id = ? + "); + $sth->execute($user->id, $product->id); + + $sth = $dbh->prepare(" + INSERT INTO component_watch(user_id, product_id) + VALUES (?, ?) + "); + $sth->execute($user->id, $product->id); + + return; +} + +sub _addComponentWatch { + my ($user, $component) = @_; + my $dbh = Bugzilla->dbh; + + my $sth = $dbh->prepare(" + SELECT 1 + FROM component_watch + WHERE user_id = ? + AND (component_id = ? OR (product_id = ? AND component_id IS NULL)) + "); + $sth->execute($user->id, $component->id, $component->product_id); + return if $sth->fetchrow_array; + + $sth = $dbh->prepare(" + INSERT INTO component_watch(user_id, product_id, component_id) + VALUES (?, ?, ?) + "); + $sth->execute($user->id, $component->product_id, $component->id); + + return; +} + +sub _addPrefixWatch { + my ($user, $product, $prefix) = @_; + my $dbh = Bugzilla->dbh; + + trick_taint($prefix); + my $sth = $dbh->prepare(" + SELECT 1 + FROM component_watch + WHERE user_id = ? + AND ( + (product_id = ? AND component_prefix = ?) + OR (product_id = ? AND component_id IS NULL) + ) + "); + $sth->execute($user->id, $product->id, $prefix, $product->id); + return if $sth->fetchrow_array; + + $sth = $dbh->prepare(" + INSERT INTO component_watch(user_id, product_id, component_prefix) + VALUES (?, ?, ?) + "); + $sth->execute($user->id, $product->id, $prefix); + + return; +} + +sub _deleteWatch { + my ($user, $id) = @_; + my $dbh = Bugzilla->dbh; + + detaint_natural($id) || ThrowCodeError("component_watch_invalid_id"); + $dbh->do("DELETE FROM component_watch WHERE id=? AND user_id=?", + undef, $id, $user->id); + + return; +} + +sub _addDefaultSettings { + my ($user) = @_; + my $dbh = Bugzilla->dbh; + + my $sth = $dbh->prepare(" + SELECT 1 + FROM email_setting + WHERE user_id = ? AND relationship = ? + "); + $sth->execute($user->id, REL_COMPONENT_WATCHER); + return if $sth->fetchrow_array; + + my @defaultEvents = (EVT_OTHER, EVT_COMMENT, + EVT_ATTACHMENT, EVT_ATTACHMENT_DATA, + EVT_PROJ_MANAGEMENT, EVT_OPENED_CLOSED, + EVT_KEYWORD, EVT_DEPEND_BLOCK, + EVT_BUG_CREATED, + ); + foreach my $event (@defaultEvents) { + $dbh->do("INSERT INTO email_setting(user_id,relationship,event) VALUES (?,?,?)", + undef, $user->id, REL_COMPONENT_WATCHER, $event); + } + + return; +} + +sub reorg_move_component { + my ($self, $args) = @_; + my $new_product = $args->{new_product}; + my $component = $args->{component}; + + Bugzilla->dbh->do( + "UPDATE component_watch SET product_id=? WHERE component_id=?", + undef, $new_product->id, $component->id,); + + return; +} + +sub sanitycheck_check { + my ($self, $args) = @_; + my $status = $args->{status}; + + $status->('component_watching_check'); + + my ($count) = Bugzilla->dbh->selectrow_array(" + SELECT COUNT(*) + FROM component_watch + INNER JOIN components ON components.id = component_watch.component_id + WHERE component_watch.product_id <> components.product_id + "); + if ($count) { + $status->('component_watching_alert', undef, 'alert'); + $status->('component_watching_repair'); + } + + return; +} + +sub sanitycheck_repair { + my ($self, $args) = @_; + return unless Bugzilla->cgi->param('component_watching_repair'); + + my $status = $args->{'status'}; + my $dbh = Bugzilla->dbh; + $status->('component_watching_repairing'); + + my $rows = $dbh->selectall_arrayref(" + SELECT DISTINCT component_watch.product_id AS bad_product_id, + components.product_id AS good_product_id, + component_watch.component_id + FROM component_watch + INNER JOIN components ON components.id = component_watch.component_id + WHERE component_watch.product_id <> components.product_id + ", {Slice => {}}); + foreach my $row (@$rows) { + $dbh->do(" + UPDATE component_watch + SET product_id=? + WHERE product_id=? AND component_id=? + ", undef, $row->{good_product_id}, $row->{bad_product_id}, + $row->{component_id},); + } + + return; +} + +__PACKAGE__->NAME; |