summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/ComponentWatching/Extension.pm')
-rw-r--r--extensions/ComponentWatching/Extension.pm473
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;