summaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/addcustomfield.pl63
-rwxr-xr-xscripts/bugzilla-queue.rhel115
-rwxr-xr-xscripts/clear-memcached.pl27
-rwxr-xr-xscripts/clear-templates.pl40
-rwxr-xr-xscripts/fix_all_open_status_queries.pl144
-rwxr-xr-xscripts/fixgroupqueries.pl123
-rwxr-xr-xscripts/fixperms.pl28
-rwxr-xr-xscripts/fixqueries.pl136
-rwxr-xr-xscripts/issue-api-key.pl33
-rwxr-xr-xscripts/merge-users.pl271
-rwxr-xr-xscripts/moco-ldap-check.pl542
-rwxr-xr-xscripts/move_flag_types.pl172
-rwxr-xr-xscripts/move_os.pl81
-rwxr-xr-xscripts/movebugs.pl190
-rwxr-xr-xscripts/movecomponent.pl154
-rwxr-xr-xscripts/nagios_blocker_checker.pl201
-rwxr-xr-xscripts/nuke-bugs.pl69
-rwxr-xr-xscripts/reassign_open_bugs.pl87
-rwxr-xr-xscripts/reset_default_user.pl145
-rwxr-xr-xscripts/sanitizeme.pl230
-rwxr-xr-xscripts/sendunsentbugmail.pl60
-rwxr-xr-xscripts/syncflags.pl88
-rwxr-xr-xscripts/syncmsandversions.pl122
23 files changed, 3121 insertions, 0 deletions
diff --git a/scripts/addcustomfield.pl b/scripts/addcustomfield.pl
new file mode 100755
index 000000000..886d1ac5c
--- /dev/null
+++ b/scripts/addcustomfield.pl
@@ -0,0 +1,63 @@
+#!/usr/bin/perl -wT
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# Contributor(s): Frédéric Buclin <LpSolit@gmail.com>
+# David Miller <justdave@mozilla.com>
+
+use strict;
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Field;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my %types = (
+ 'freetext' => FIELD_TYPE_FREETEXT,
+ 'single_select' => FIELD_TYPE_SINGLE_SELECT,
+ 'multi_select' => FIELD_TYPE_MULTI_SELECT,
+ 'textarea' => FIELD_TYPE_TEXTAREA,
+ 'datetime' => FIELD_TYPE_DATETIME,
+ 'date' => FIELD_TYPE_DATE,
+ 'bug_id' => FIELD_TYPE_BUG_ID,
+ 'bug_urls' => FIELD_TYPE_BUG_URLS,
+ 'keywords' => FIELD_TYPE_KEYWORDS,
+);
+
+my $syntax =
+ "syntax: addcustomfield.pl <field name> [field type]\n\n" .
+ "valid field types:\n " . join("\n ", sort keys %types) . "\n\n" .
+ "the default field type is single_select\n";
+
+my $name = shift || die $syntax;
+my $type = lc(shift || 'single_select');
+exists $types{$type} || die "Invalid field type '$type'.\n\n$syntax";
+$type = $types{$type};
+
+Bugzilla::Field->create({
+ name => $name,
+ description => 'Please give me a description!',
+ type => $type,
+ mailhead => 0,
+ enter_bug => 0,
+ obsolete => 1,
+ custom => 1,
+ buglist => 1,
+});
+print "Done!\n";
+
+my $urlbase = Bugzilla->params->{urlbase};
+print "Please visit ${urlbase}editfields.cgi?action=edit&name=$name to finish setting up this field.\n";
diff --git a/scripts/bugzilla-queue.rhel b/scripts/bugzilla-queue.rhel
new file mode 100755
index 000000000..dd7a2c664
--- /dev/null
+++ b/scripts/bugzilla-queue.rhel
@@ -0,0 +1,115 @@
+#!/bin/bash
+# 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.
+#
+# bugzilla-queue This starts, stops, and restarts the Bugzilla jobqueue.pl
+# daemon, which manages sending queued mail and possibly
+# other queued tasks in the future.
+#
+# chkconfig: 345 85 15
+# description: Bugzilla queue runner
+#
+### BEGIN INIT INFO
+# Provides: bugzilla-queue
+# Required-Start: $local_fs $syslog MTA mysqld
+# Required-Stop: $local_fs $syslog MTA mysqld
+# Default-Start: 3 5
+# Default-Stop: 0 1 2 6
+# Short-Description: Start and stop the Bugzilla queue runner.
+# Description: The Bugzilla queue runner (jobqueue.pl) sends any mail
+# that Bugzilla has queued to be sent in the background. If you
+# have enabled the use_mailer_queue parameter in Bugzilla, you
+# must run this daemon.
+### END INIT INFO
+
+NAME=`basename $0`
+
+#################
+# Configuration #
+#################
+
+# This should be the path to your Bugzilla
+BUGZILLA=/var/www/html/bugzilla
+# Who owns the Bugzilla directory and files?
+USER=root
+
+# If you want to pass any options to the daemon (like -d for debugging)
+# specify it here.
+OPTIONS=""
+
+# You can also override the configuration by creating a
+# /etc/sysconfig/bugzilla-queue file so that you don't
+# have to edit this script.
+if [ -r /etc/sysconfig/$NAME ]; then
+ . /etc/sysconfig/$NAME
+fi
+
+##########
+# Script #
+##########
+
+RETVAL=0
+BIN=$BUGZILLA/jobqueue.pl
+PIDFILE=/var/run/$NAME.pid
+
+# Source function library.
+. /etc/rc.d/init.d/functions
+
+usage ()
+{
+ echo "Usage: service $NAME {start|stop|status|restart|condrestart}"
+ RETVAL=1
+}
+
+
+start ()
+{
+ if [ -f "$PIDFILE" ]; then
+ checkpid `cat $PIDFILE` && return 0
+ fi
+ echo -n "Starting $NAME: "
+ touch $PIDFILE
+ chown $USER $PIDFILE
+ daemon --user=$USER \
+ "$BIN ${OPTIONS} -p '$PIDFILE' -n $NAME start > /dev/null"
+ ret=$?
+ [ $ret -eq "0" ] && touch /var/lock/subsys/$NAME
+ echo
+ return $ret
+}
+
+stop ()
+{
+ [ -f /var/lock/subsys/$NAME ] || return 0
+ echo -n "Killing $NAME: "
+ killproc $NAME
+ echo
+ rm -f /var/lock/subsys/$NAME
+}
+
+restart ()
+{
+ stop
+ start
+}
+
+condrestart ()
+{
+ [ -e /var/lock/subsys/$NAME ] && restart || return 0
+}
+
+
+case "$1" in
+ start) start; RETVAL=$? ;;
+ stop) stop; RETVAL=$? ;;
+ status) $BIN -p $PIDFILE -n $NAME check; RETVAL=$?;;
+ restart) restart; RETVAL=$? ;;
+ condrestart) condrestart; RETVAL=$? ;;
+ *) usage ; RETVAL=2 ;;
+esac
+
+exit $RETVAL
diff --git a/scripts/clear-memcached.pl b/scripts/clear-memcached.pl
new file mode 100755
index 000000000..01202ce7c
--- /dev/null
+++ b/scripts/clear-memcached.pl
@@ -0,0 +1,27 @@
+#!/usr/bin/perl
+
+# 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;
+use warnings;
+
+use FindBin qw($Bin);
+use lib "$Bin/..";
+use lib "$Bin/../lib";
+
+use Bugzilla;
+use Bugzilla::Constants;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (Bugzilla->memcached->{memcached}) {
+ Bugzilla->memcached->clear_all();
+ print "memcached cleared\n";
+} else {
+ print "memcached is not enabled\n";
+}
diff --git a/scripts/clear-templates.pl b/scripts/clear-templates.pl
new file mode 100755
index 000000000..8b0864d46
--- /dev/null
+++ b/scripts/clear-templates.pl
@@ -0,0 +1,40 @@
+#!/usr/bin/perl -w
+
+# 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;
+use warnings;
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Install::Filesystem qw(fix_dir_permissions);
+use File::Path qw(mkpath rmtree);
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+$| = 1;
+
+# rename the current directory and create a new empty one
+# the templates will lazy-compile on demand
+
+my $path = bz_locations()->{'template_cache'};
+my $delete_path = "$path.deleteme";
+
+print "clearing $path\n";
+
+rmtree("$delete_path") if -e "$delete_path";
+rename($path, $delete_path)
+ or die "renaming '$path' to '$delete_path' failed: $!\n";
+
+mkpath($path)
+ or die "creating '$path' failed: $!\n";
+fix_dir_permissions($path);
+
+# delete the temp directory (it's ok if this fails)
+
+rmtree("$delete_path");
diff --git a/scripts/fix_all_open_status_queries.pl b/scripts/fix_all_open_status_queries.pl
new file mode 100755
index 000000000..e10fdd44d
--- /dev/null
+++ b/scripts/fix_all_open_status_queries.pl
@@ -0,0 +1,144 @@
+#!/usr/bin/perl -w
+# 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;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Status;
+use Bugzilla::Util;
+
+sub usage() {
+ print <<USAGE;
+Usage: fix_all_open_status_queries.pl <new_open_status>
+
+E.g.: fix_all_open_status_queries.pl READY
+This will add a new open state to user queries which currently look for
+all open bugs by listing every open status in their query criteria.
+For users who only look for bug_status=__open__, they will get the new
+open status automatically.
+USAGE
+}
+
+sub do_namedqueries {
+ my ($new_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $replace_count = 0;
+
+ my $query = $dbh->selectall_arrayref("SELECT id, query FROM namedqueries");
+
+ if ($query) {
+ $dbh->bz_start_transaction();
+
+ my $sth = $dbh->prepare("UPDATE namedqueries SET query = ? WHERE id = ?");
+
+ foreach my $row (@$query) {
+ my ($id, $old_query) = @$row;
+ my $new_query = all_open_states($new_status, $old_query);
+ if ($new_query) {
+ trick_taint($new_query);
+ $sth->execute($new_query, $id);
+ $replace_count++;
+ }
+ }
+
+ $dbh->bz_commit_transaction();
+ }
+
+ print "namedqueries: $replace_count replacements made.\n";
+}
+
+# series
+sub do_series {
+ my ($new_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $replace_count = 0;
+
+ my $query = $dbh->selectall_arrayref("SELECT series_id, query FROM series");
+
+ if ($query) {
+ $dbh->bz_start_transaction();
+
+ my $sth = $dbh->prepare("UPDATE series SET query = ? WHERE series_id = ?");
+
+ foreach my $row (@$query) {
+ my ($series_id, $old_query) = @$row;
+ my $new_query = all_open_states($new_status, $old_query);
+ if ($new_query) {
+ trick_taint($new_query);
+ $sth->execute($new_query, $series_id);
+ $replace_count++;
+ }
+ }
+
+ $dbh->bz_commit_transaction();
+ }
+
+ print "series: $replace_count replacements made.\n";
+}
+
+sub all_open_states {
+ my ($new_status, $query) = @_;
+
+ my @open_states = Bugzilla::Status::BUG_STATE_OPEN();
+ my $cgi = Bugzilla::CGI->new($query);
+ my @query_states = $cgi->param('bug_status');
+
+ my ($removed, $added) = diff_arrays(\@query_states, \@open_states);
+
+ if (scalar @$added == 1 && $added->[0] eq $new_status) {
+ push(@query_states, $new_status);
+ $cgi->param('bug_status', @query_states);
+ return $cgi->canonicalise_query();
+ }
+
+ return '';
+}
+
+sub validate_status {
+ my ($status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $exists = $dbh->selectrow_array("SELECT 1 FROM bug_status
+ WHERE value = ?",
+ undef, $status);
+ return $exists ? 1 : 0;
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+# This is a pure command line script.
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 1) {
+ usage();
+ exit(1);
+}
+
+my ($new_status) = @ARGV;
+
+$new_status = uc($new_status);
+
+if (!validate_status($new_status)) {
+ print "Invalid status: $new_status\n\n";
+ usage();
+ exit(1);
+}
+
+print "Adding new status '$new_status'.\n\n";
+
+do_namedqueries($new_status);
+do_series($new_status);
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
+
+exit(0);
diff --git a/scripts/fixgroupqueries.pl b/scripts/fixgroupqueries.pl
new file mode 100755
index 000000000..13dd0cb3e
--- /dev/null
+++ b/scripts/fixgroupqueries.pl
@@ -0,0 +1,123 @@
+#!/usr/bin/perl -w
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Gervase Markham <gerv@gerv.net>
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+
+sub usage() {
+ print <<USAGE;
+Usage: fixgroupqueries.pl <oldvalue> <newvalue>
+
+E.g.: fixgroupqueries.pl w-security webtools-security
+will change all occurrences of "w-security" to "webtools-security" in the
+appropriate places in the namedqueries.
+
+Note that all parameters are case-sensitive.
+USAGE
+}
+
+sub do_namedqueries($$) {
+ my ($old, $new) = @_;
+ $old = url_quote($old);
+ $new = url_quote($new);
+
+ my $dbh = Bugzilla->dbh;
+
+ my $replace_count = 0;
+ my $query = $dbh->selectall_arrayref("SELECT id, query FROM namedqueries");
+ if ($query) {
+ my $sth = $dbh->prepare("UPDATE namedqueries SET query = ?
+ WHERE id = ?");
+
+ foreach my $row (@$query) {
+ my ($id, $query) = @$row;
+ if (($query =~ /field\d+-\d+-\d+=bug_group/) &&
+ ($query =~ /(?:^|&|;)value\d+-\d+-\d+=$old(?:;|&|$)/)) {
+ $query =~ s/((?:^|&|;)value\d+-\d+-\d+=)$old(;|&|$)/$1$new$2/;
+ $sth->execute($query, $id);
+ $replace_count++;
+ }
+ }
+ }
+
+ print "namedqueries: $replace_count replacements made.\n";
+}
+
+# series
+sub do_series($$) {
+ my ($old, $new) = @_;
+ $old = url_quote($old);
+ $new = url_quote($new);
+
+ my $dbh = Bugzilla->dbh;
+ #$dbh->bz_start_transaction();
+
+ my $replace_count = 0;
+ my $query = $dbh->selectall_arrayref("SELECT series_id, query
+ FROM series");
+ if ($query) {
+ my $sth = $dbh->prepare("UPDATE series SET query = ?
+ WHERE series_id = ?");
+ foreach my $row (@$query) {
+ my ($series_id, $query) = @$row;
+
+ if (($query =~ /field\d+-\d+-\d+=bug_group/) &&
+ ($query =~ /(?:^|&|;)value\d+-\d+-\d+=$old(?:;|&|$)/)) {
+ $query =~ s/((?:^|&|;)value\d+-\d+-\d+=)$old(;|&|$)/$1$new$2/;
+ $sth->execute($query, $series_id);
+ $replace_count++;
+ }
+ }
+ }
+
+ #$dbh->bz_commit_transaction();
+ print "series: $replace_count replacements made.\n";
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+# This is a pure command line script.
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 2) {
+ usage();
+ exit();
+}
+
+my ($old, $new) = @ARGV;
+
+print "Changing all instances of '$old' to '$new'.\n\n";
+
+#do_namedqueries($old, $new);
+do_series($old, $new);
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
+
+exit(0);
diff --git a/scripts/fixperms.pl b/scripts/fixperms.pl
new file mode 100755
index 000000000..406c149cb
--- /dev/null
+++ b/scripts/fixperms.pl
@@ -0,0 +1,28 @@
+#!/usr/bin/perl -w
+#
+# 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.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Everything Solved, Inc.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Max Kanat-Alexander <mkanat@bugzilla.org>
+
+use strict;
+use warnings;
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Install::Filesystem qw(fix_all_file_permissions);
+fix_all_file_permissions(1);
diff --git a/scripts/fixqueries.pl b/scripts/fixqueries.pl
new file mode 100755
index 000000000..1fe25f261
--- /dev/null
+++ b/scripts/fixqueries.pl
@@ -0,0 +1,136 @@
+#!/usr/bin/perl -w
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Gervase Markham <gerv@gerv.net>
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+
+sub usage() {
+ print <<USAGE;
+Usage: fixqueries.pl <parameter> <oldvalue> <newvalue>
+
+E.g.: fixqueries.pl product FoodReplicator SeaMonkey
+will change all occurrences of "FoodReplicator" to "Seamonkey" in the
+appropriate places in the namedqueries, series and series_categories tables.
+
+Note that all parameters are case-sensitive.
+USAGE
+}
+
+sub do_namedqueries($$$) {
+ my ($field, $old, $new) = @_;
+ $old = url_quote($old);
+ $new = url_quote($new);
+
+ my $dbh = Bugzilla->dbh;
+ #$dbh->bz_start_transaction();
+
+ my $replace_count = 0;
+ my $query = $dbh->selectall_arrayref("SELECT id, query FROM namedqueries");
+ if ($query) {
+ my $sth = $dbh->prepare("UPDATE namedqueries SET query = ?
+ WHERE id = ?");
+
+ foreach my $row (@$query) {
+ my ($id, $query) = @$row;
+ if ($query =~ /(?:^|&|;)$field=$old(?:&|$|;)/) {
+ $query =~ s/((?:^|&|;)$field=)$old(;|&|$)/$1$new$2/;
+ $sth->execute($query, $id);
+ $replace_count++;
+ }
+ }
+ }
+
+ #$dbh->bz_commit_transaction();
+ print "namedqueries: $replace_count replacements made.\n";
+}
+
+# series
+sub do_series($$$) {
+ my ($field, $old, $new) = @_;
+ $old = url_quote($old);
+ $new = url_quote($new);
+
+ my $dbh = Bugzilla->dbh;
+ #$dbh->bz_start_transaction();
+
+ my $replace_count = 0;
+ my $query = $dbh->selectall_arrayref("SELECT series_id, query
+ FROM series");
+ if ($query) {
+ my $sth = $dbh->prepare("UPDATE series SET query = ?
+ WHERE series_id = ?");
+ foreach my $row (@$query) {
+ my ($series_id, $query) = @$row;
+
+ if ($query =~ /(?:^|&|;)$field=$old(?:&|$|;)/) {
+ $query =~ s/((?:^|&|;)$field=)$old(;|&|$)/$1$new$2/;
+ $replace_count++;
+ }
+
+ $sth->execute($query, $series_id);
+ }
+ }
+
+ #$dbh->bz_commit_transaction();
+ print "series: $replace_count replacements made.\n";
+}
+
+# series_categories
+sub do_series_categories($$) {
+ my ($old, $new) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->do("UPDATE series_categories SET name = ? WHERE name = ?",
+ undef,
+ ($new, $old));
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+# This is a pure command line script.
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 3) {
+ usage();
+ exit();
+}
+
+my ($field, $old, $new) = @ARGV;
+
+print "Changing all instances of '$old' to '$new'.\n\n";
+
+do_namedqueries($field, $old, $new);
+do_series($field, $old, $new);
+do_series_categories($old, $new);
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
+
+exit(0);
+
diff --git a/scripts/issue-api-key.pl b/scripts/issue-api-key.pl
new file mode 100755
index 000000000..c5bdd8ca4
--- /dev/null
+++ b/scripts/issue-api-key.pl
@@ -0,0 +1,33 @@
+#!/usr/bin/perl -w
+
+# 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;
+use feature 'say';
+
+use FindBin qw( $RealBin );
+use lib "$RealBin/..";
+use lib "$RealBin/../lib";
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::User;
+use Bugzilla::User::APIKey;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my $login = shift
+ or die "syntax: $0 bugzilla-login [description]\n";
+my $description = shift;
+
+my $user = Bugzilla::User->check({ name => $login });
+my $api_key = Bugzilla::User::APIKey->create({
+ user_id => $user->id,
+ description => $description,
+});
+say $api_key->api_key;
diff --git a/scripts/merge-users.pl b/scripts/merge-users.pl
new file mode 100755
index 000000000..ebe68a6a8
--- /dev/null
+++ b/scripts/merge-users.pl
@@ -0,0 +1,271 @@
+#!/usr/bin/perl -wT
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Myk Melez <myk@mozilla.org>
+# Frédéric Buclin <LpSolit@gmail.com>
+
+use strict;
+
+=head1 NAME
+
+merge-users.pl - Merge two user accounts.
+
+=head1 SYNOPSIS
+
+ This script moves activity from one user account to another.
+ Specify the two accounts on the command line, e.g.:
+
+ ./merge-users.pl old_account@foo.com new_account@bar.com
+ or:
+ ./merge-users.pl id:old_userid id:new_userid
+ or:
+ ./merge-users.pl id:old_userid new_account@bar.com
+
+ Notes: - the new account must already exist.
+ - the id:old_userid syntax permits you to migrate
+ activity from a deleted account to an existing one.
+
+=cut
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+use Bugzilla::User;
+use Bugzilla::Hook;
+
+use Getopt::Long;
+use Pod::Usage;
+
+my $dbh = Bugzilla->dbh;
+
+# Display the help if called with --help or -?.
+my $help = 0;
+my $result = GetOptions("help|?" => \$help);
+pod2usage(0) if $help;
+
+
+# Make sure accounts were specified on the command line and exist.
+my $old = $ARGV[0] || die "You must specify an old user account.\n";
+my $old_id;
+if ($old =~ /^id:(\d+)$/) {
+ # As the old user account may be a deleted one, we don't
+ # check whether this user ID is valid or not.
+ # If it never existed, no damage will be done.
+ $old_id = $1;
+}
+else {
+ trick_taint($old);
+ $old_id = $dbh->selectrow_array('SELECT userid FROM profiles
+ WHERE login_name = ?',
+ undef, $old);
+}
+if ($old_id) {
+ print "OK, old user account $old found; user ID: $old_id.\n";
+}
+else {
+ die "The old user account $old does not exist.\n";
+}
+
+my $new = $ARGV[1] || die "You must specify a new user account.\n";
+my $new_id;
+if ($new =~ /^id:(\d+)$/) {
+ $new_id = $1;
+ # Make sure this user ID exists.
+ $new_id = $dbh->selectrow_array('SELECT userid FROM profiles
+ WHERE userid = ?',
+ undef, $new_id);
+}
+else {
+ trick_taint($new);
+ $new_id = $dbh->selectrow_array('SELECT userid FROM profiles
+ WHERE login_name = ?',
+ undef, $new);
+}
+if ($new_id) {
+ print "OK, new user account $new found; user ID: $new_id.\n";
+}
+else {
+ die "The new user account $new does not exist.\n";
+}
+
+# Make sure the old and new accounts are different.
+if ($old_id == $new_id) {
+ die "\nBoth accounts are identical. There is nothing to migrate.\n";
+}
+
+
+# A list of tables and columns to be changed:
+# - keys of the hash are table names to be locked/altered;
+# - values of the hash contain column names to be updated
+# as well as the columns they depend on:
+# = each array is of the form:
+# ['foo1 bar11 bar12 bar13', 'foo2 bar21 bar22', 'foo3 bar31 bar32']
+# where fooN is the column to update, and barN1, barN2, ... are
+# the columns to take into account to avoid duplicated entries.
+# Note that the barNM columns are optional.
+#
+# We set the tables that require custom stuff (multiple columns to check)
+# here, but the simple stuff is all handled below by bz_get_related_fks.
+my %changes = (
+ cc => ['who bug_id'],
+ # Tables affecting global behavior / other users.
+ component_cc => ['user_id component_id'],
+ watch => ['watcher watched', 'watched watcher'],
+ # Tables affecting the user directly.
+ namedqueries => ['userid name'],
+ namedqueries_link_in_footer => ['user_id namedquery_id'],
+ user_group_map => ['user_id group_id isbless grant_type'],
+ email_setting => ['user_id relationship event'],
+ profile_setting => ['user_id setting_name'],
+
+ # Only do it if mailto_type = 0, i.e is pointing to a user account!
+ # This requires to be done separately due to this condition.
+ whine_schedules => [], # ['mailto'],
+);
+
+my $userid_fks = $dbh->bz_get_related_fks('profiles', 'userid');
+foreach my $item (@$userid_fks) {
+ my ($table, $column) = @$item;
+ $changes{$table} ||= [];
+ push(@{ $changes{$table} }, $column);
+}
+
+# Delete all old records for these tables; no migration.
+foreach my $table (qw(logincookies tokens profiles)) {
+ $changes{$table} = [];
+}
+
+# Start the transaction
+$dbh->bz_start_transaction();
+
+# BMO - pre-work hook
+Bugzilla::Hook::process('merge_users_before', { old_id => $old_id, new_id => $new_id });
+
+# Delete old records from logincookies and tokens tables.
+$dbh->do('DELETE FROM logincookies WHERE userid = ?', undef, $old_id);
+$dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $old_id);
+
+# Special care needs to be done with bug_user_last_visit table as the
+# source user and destination user may have visited the same bug id at one time.
+# In this case we remove the one with the oldest timestamp.
+my $dupe_ids = $dbh->selectcol_arrayref("
+ SELECT earlier.id
+ FROM bug_user_last_visit as earlier
+ INNER JOIN bug_user_last_visit as later
+ ON (earlier.user_id != later.user_id
+ AND earlier.last_visit_ts < later.last_visit_ts
+ AND earlier.bug_id = later.bug_id)
+ WHERE (earlier.user_id = ? OR earlier.user_id = ?)
+ AND (later.user_id = ? OR later.user_id = ?)",
+ undef, $old_id, $new_id, $old_id, $new_id);
+
+if (@$dupe_ids) {
+ $dbh->do("DELETE FROM bug_user_last_visit WHERE " .
+ $dbh->sql_in('id', $dupe_ids));
+}
+
+# Migrate records from old user to new user.
+foreach my $table (keys %changes) {
+ foreach my $column_list (@{ $changes{$table} }) {
+ # Get all columns to consider. There is always at least
+ # one column given: the one to update.
+ my @columns = split(/[\s]+/, $column_list);
+ my $cols_to_check = join(' AND ', map {"$_ = ?"} @columns);
+ # The first column of the list is the one to update.
+ my $col_to_update = shift @columns;
+
+ # Will be used to migrate the old user account to the new one.
+ my $sth_update = $dbh->prepare("UPDATE $table
+ SET $col_to_update = ?
+ WHERE $cols_to_check");
+
+ # Do we have additional columns to take care of?
+ if (scalar(@columns)) {
+ my $cols_to_query = join(', ', @columns);
+
+ # Get existing entries for the old user account.
+ my $old_entries =
+ $dbh->selectall_arrayref("SELECT $cols_to_query
+ FROM $table
+ WHERE $col_to_update = ?",
+ undef, $old_id);
+
+ # Will be used to check whether the same entry exists
+ # for the new user account.
+ my $sth_select = $dbh->prepare("SELECT COUNT(*)
+ FROM $table
+ WHERE $cols_to_check");
+
+ # Will be used to delete duplicated entries.
+ my $sth_delete = $dbh->prepare("DELETE FROM $table
+ WHERE $cols_to_check");
+
+ foreach my $entry (@$old_entries) {
+ my $exists = $dbh->selectrow_array($sth_select, undef,
+ ($new_id, @$entry));
+
+ if ($exists) {
+ $sth_delete->execute($old_id, @$entry);
+ }
+ else {
+ $sth_update->execute($new_id, $old_id, @$entry);
+ }
+ }
+ }
+ # No check required. Update the column directly.
+ else {
+ $sth_update->execute($new_id, $old_id);
+ }
+ print "OK, records in the '$col_to_update' column of the '$table' table\n" .
+ "have been migrated to the new user account.\n";
+ }
+}
+
+# Only update 'whine_schedules' if mailto_type = 0.
+# (i.e. is pointing to a user ID).
+$dbh->do('UPDATE whine_schedules SET mailto = ?
+ WHERE mailto = ? AND mailto_type = ?',
+ undef, ($new_id, $old_id, 0));
+print "OK, records in the 'mailto' column of the 'whine_schedules' table\n" .
+ "have been migrated to the new user account.\n";
+
+# Delete the old record from the profiles table.
+$dbh->do('DELETE FROM profiles WHERE userid = ?', undef, $old_id);
+
+# rederive regexp-based group memberships, because we merged all memberships
+# from all of the accounts, and since the email address isn't the same on
+# them, some of them may no longer match the regexps.
+my $user = new Bugzilla::User($new_id);
+$user->derive_regexp_groups();
+
+# BMO - post-work hook
+Bugzilla::Hook::process('merge_users_after', { old_id => $old_id, new_id => $new_id });
+
+# Commit the transaction
+$dbh->bz_commit_transaction();
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As user merge is expected to be a rare event, we just flush the entire cache
+# when users are merged.
+Bugzilla->memcached->clear_all();
+
+print "Done.\n";
diff --git a/scripts/moco-ldap-check.pl b/scripts/moco-ldap-check.pl
new file mode 100755
index 000000000..7a3a6ca8c
--- /dev/null
+++ b/scripts/moco-ldap-check.pl
@@ -0,0 +1,542 @@
+#!/usr/bin/perl
+
+# 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;
+use warnings;
+
+use FindBin qw($Bin);
+use lib "$Bin/..";
+use lib "$Bin/../lib";
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Group;
+use Bugzilla::Mailer;
+use Data::Dumper;
+use File::Slurp;
+use Getopt::Long;
+use Net::LDAP;
+use Safe;
+
+#
+
+use constant BUGZILLA_IGNORE => <<'EOF';
+ infra+bot@mozilla.com # Mozilla Infrastructure Bot
+ qa-auto@mozilla.com # QA Desktop Automation
+ qualys@mozilla.com # Qualys Security Scanner
+ recruiting@mozilla.com # Recruiting
+ release@mozilla.com # Mozilla RelEng Bot
+ sumo-dev@mozilla.com # SUMOdev [:sumodev]
+ airmozilla@mozilla.com # Air Mozilla
+ ux-review@mozilla.com
+ release-mgmt@mozilla.com
+ reps@mozilla.com
+ moz_bug_r_a4@mozilla.com # Security contractor
+ nightwatch@mozilla.com # Security distribution list for whines
+EOF
+
+use constant LDAP_IGNORE => <<'EOF';
+ airmozilla@mozilla.com # Air Mozilla
+EOF
+
+# REPORT_SENDER has to be a valid @mozilla.com LDAP account
+use constant REPORT_SENDER => 'bjones@mozilla.com';
+
+use constant BMO_RECIPIENTS => qw(
+ glob@mozilla.com
+ dkl@mozilla.com
+);
+
+use constant SUPPORT_RECIPIENTS => qw(
+ desktop@mozilla.com
+);
+
+#
+
+my ($ldap_host, $ldap_user, $ldap_pass, $debug, $no_update);
+GetOptions('h=s' => \$ldap_host,
+ 'u=s' => \$ldap_user,
+ 'p=s' => \$ldap_pass,
+ 'd' => \$debug,
+ 'n' => \$no_update);
+die "syntax: -h ldap_host -u ldap_user -p ldap_pass\n"
+ unless $ldap_host && $ldap_user && $ldap_pass;
+
+my $data_dir = bz_locations()->{'datadir'} . '/moco-ldap-check';
+mkdir($data_dir) unless -d $data_dir;
+
+if ($ldap_user !~ /,/) {
+ $ldap_user = "mail=$ldap_user,o=com,dc=mozilla";
+}
+
+#
+# group members
+#
+
+my @bugzilla_ignore;
+foreach my $line (split(/\n/, BUGZILLA_IGNORE)) {
+ $line =~ s/^([^#]+)#.*$/$1/;
+ $line =~ s/(^\s+|\s+$)//g;
+ push @bugzilla_ignore, clean_email($line);
+}
+
+my @bugzilla_moco;
+if ($no_update && -s "$data_dir/bugzilla_moco.last") {
+ $debug && print "Using cached user list from Bugzilla...\n";
+ my $ra = deserialise("$data_dir/bugzilla_moco.last");
+ @bugzilla_moco = @$ra;
+} else {
+ $debug && print "Getting user list from Bugzilla...\n";
+
+ my $group = Bugzilla::Group->new({ name => 'mozilla-corporation' })
+ or die "Failed to find group mozilla-corporation\n";
+
+ foreach my $user (@{ $group->members_non_inherited }) {
+ next unless $user->is_enabled;
+ my $mail = clean_email($user->login);
+ my $name = trim($user->name);
+ $name =~ s/\s+/ /g;
+ next if grep { $mail eq $_ } @bugzilla_ignore;
+ push @bugzilla_moco, {
+ mail => $user->login,
+ canon => $mail,
+ name => $name,
+ };
+ }
+
+ @bugzilla_moco = sort { $a->{mail} cmp $b->{mail} } @bugzilla_moco;
+ serialise("$data_dir/bugzilla_moco.last", \@bugzilla_moco);
+}
+
+#
+# build list of current mo-co bugmail accounts
+#
+
+my @ldap_ignore;
+foreach my $line (split(/\n/, LDAP_IGNORE)) {
+ $line =~ s/^([^#]+)#.*$/$1/;
+ $line =~ s/(^\s+|\s+$)//g;
+ push @ldap_ignore, canon_email($line);
+}
+
+my %ldap;
+if ($no_update && -s "$data_dir/ldap.last") {
+ $debug && print "Using cached user list from LDAP...\n";
+ my $rh = deserialise("$data_dir/ldap.last");
+ %ldap = %$rh;
+} else {
+ $debug && print "Logging into LDAP as $ldap_user...\n";
+ my $ldap = Net::LDAP->new($ldap_host,
+ scheme => 'ldaps', onerror => 'die') or die "$@";
+ $ldap->bind($ldap_user, password => $ldap_pass);
+ foreach my $ldap_base ('o=com,dc=mozilla', 'o=org,dc=mozilla') {
+ $debug && print "Getting user list from LDAP $ldap_base...\n";
+ my $result = $ldap->search(
+ base => $ldap_base,
+ scope => 'sub',
+ filter => '(mail=*)',
+ attrs => ['mail', 'bugzillaEmail', 'emailAlias', 'cn', 'employeeType'],
+ );
+ foreach my $entry ($result->entries) {
+ my ($name, $bugMail, $mail, $type) =
+ map { $entry->get_value($_) || '' }
+ qw(cn bugzillaEmail mail employeeType);
+ next if $type eq 'DISABLED';
+ $mail = lc $mail;
+ next if grep { $_ eq canon_email($mail) } @ldap_ignore;
+ $bugMail = '' if $bugMail !~ /\@/;
+ $bugMail =~ s/(^\s+|\s+$)//g;
+ if ($bugMail =~ / /) {
+ $bugMail = (grep { /\@/ } split / /, $bugMail)[0];
+ }
+ $name =~ s/\s+/ /g;
+ $ldap{$mail}{name} = trim($name);
+ $ldap{$mail}{bugmail} = $bugMail;
+ $ldap{$mail}{bugmail_canon} = canon_email($bugMail);
+ $ldap{$mail}{aliases} = [];
+ foreach my $alias (
+ @{$entry->get_value('emailAlias', asref => 1) || []}
+ ) {
+ push @{$ldap{$mail}{aliases}}, canon_email($alias);
+ }
+ }
+ $debug && printf "Found %s entries\n", scalar($result->entries);
+ }
+ serialise("$data_dir/ldap.last", \%ldap);
+}
+
+#
+# validate all bugmail entries from the phonebook
+#
+
+my %bugzilla_login;
+if ($no_update && -s "$data_dir/bugzilla_login.last") {
+ $debug && print "Using cached bugzilla checks...\n";
+ my $rh = deserialise("$data_dir/bugzilla_login.last");
+ %bugzilla_login = %$rh;
+} else {
+ my %logins;
+ foreach my $mail (keys %ldap) {
+ $logins{$mail} = 1;
+ $logins{$ldap{$mail}{bugmail}} = 1 if $ldap{$mail}{bugmail};
+ }
+ my @logins = sort keys %logins;
+ $debug && print "Checking " . scalar(@logins) . " bugmail accounts...\n";
+
+ foreach my $login (@logins) {
+ if (Bugzilla::User->new({ name => $login })) {
+ $bugzilla_login{$login} = 1;
+ }
+ }
+ serialise("$data_dir/bugzilla_login.last", \%bugzilla_login);
+}
+
+#
+# load previous ldap list
+#
+
+my %ldap_old;
+{
+ my $rh = deserialise("$data_dir/ldap.data");
+ %ldap_old = %$rh if $rh;
+}
+
+#
+# save current ldap list
+#
+
+{
+ serialise("$data_dir/ldap.data", \%ldap);
+}
+
+#
+# new ldap accounts
+#
+
+my @new_ldap;
+{
+ foreach my $mail (sort keys %ldap) {
+ next if exists $ldap_old{$mail};
+ push @new_ldap, {
+ mail => $mail,
+ name => $ldap{$mail}{name},
+ bugmail => $ldap{$mail}{bugmail},
+ };
+ }
+}
+
+#
+# deleted ldap accounts
+#
+
+my @gone_ldap_bmo;
+my @gone_ldap_no_bmo;
+{
+ foreach my $mail (sort keys %ldap_old) {
+ next if exists $ldap{$mail};
+ if ($ldap_old{$mail}{bugmail}) {
+ push @gone_ldap_bmo, {
+ mail => $mail,
+ name => $ldap_old{$mail}{name},
+ bugmail => $ldap_old{$mail}{bugmail},
+ }
+ } else {
+ push @gone_ldap_no_bmo, {
+ mail => $mail,
+ name => $ldap_old{$mail}{name},
+ }
+ }
+ }
+}
+
+#
+# check bugmail entry for all users in bmo/moco group
+#
+
+my @suspect_bugzilla;
+my @invalid_bugzilla;
+foreach my $rh (@bugzilla_moco) {
+ my @check = ($rh->{mail}, $rh->{canon});
+ if ($rh->{mail} =~ /^([^\@]+)\@mozilla\.org$/) {
+ push @check, "$1\@mozilla.com";
+ }
+
+ my $exists;
+ foreach my $check (@check) {
+ $exists = 0;
+
+ # don't complain about deleted accounts
+ if (grep { $_->{mail} eq $check } (@gone_ldap_bmo, @gone_ldap_no_bmo)) {
+ $exists = 1;
+ last;
+ }
+
+ # check for matching bugmail entry
+ foreach my $mail (sort keys %ldap) {
+ next unless $ldap{$mail}{bugmail_canon} eq $check;
+ $exists = 1;
+ last;
+ }
+ last if $exists;
+
+ # check for matching mail
+ $exists = 0;
+ foreach my $mail (sort keys %ldap) {
+ next unless $mail eq $check;
+ $exists = 1;
+ last;
+ }
+ last if $exists;
+
+ # check for matching email alias
+ $exists = 0;
+ foreach my $mail (sort keys %ldap) {
+ next unless grep { $check eq $_ } @{$ldap{$mail}{aliases}};
+ $exists = 1;
+ last;
+ }
+ last if $exists;
+ }
+
+ if (!$exists) {
+ # flag the account
+ if ($rh->{mail} =~ /\@mozilla\.(com|org)$/i) {
+ push @invalid_bugzilla, {
+ mail => $rh->{mail},
+ name => $rh->{name},
+ };
+ } else {
+ push @suspect_bugzilla, {
+ mail => $rh->{mail},
+ name => $rh->{name},
+ };
+ }
+ }
+}
+
+#
+# check bugmail entry for ldap users
+#
+
+my @ldap_unblessed;
+my @invalid_ldap;
+my @invalid_bugmail;
+foreach my $mail (sort keys %ldap) {
+ # try to find the bmo account
+ my $found;
+ foreach my $address ($ldap{$mail}{bugmail}, $ldap{$mail}{bugmail_canon}, $mail, @{$ldap{$mail}{aliases}}) {
+ if (exists $bugzilla_login{$address}) {
+ $found = $address;
+ last;
+ }
+ }
+
+ # not on bmo
+ if (!$found) {
+ # if they have specified a bugmail account, warn, otherwise ignore
+ if ($ldap{$mail}{bugmail}) {
+ if (grep { $_->{canon} eq $ldap{$mail}{bugmail_canon} } @bugzilla_moco) {
+ push @invalid_bugmail, {
+ mail => $mail,
+ name => $ldap{$mail}{name},
+ bugmail => $ldap{$mail}{bugmail},
+ };
+ } else {
+ push @invalid_ldap, {
+ mail => $mail,
+ name => $ldap{$mail}{name},
+ bugmail => $ldap{$mail}{bugmail},
+ };
+ }
+ }
+ next;
+ }
+
+ # warn about mismatches
+ if ($ldap{$mail}{bugmail} && $found ne $ldap{$mail}{bugmail}) {
+ push @invalid_bugmail, {
+ mail => $mail,
+ name => $ldap{$mail}{name},
+ bugmail => $ldap{$mail}{bugmail},
+ };
+ }
+
+ # warn about unblessed accounts
+ if ($mail =~ /\@mozilla\.com$/) {
+ unless (grep { $_->{mail} eq $found || $_->{canon} eq canon_email($found) } @bugzilla_moco) {
+ push @ldap_unblessed, {
+ mail => $mail,
+ name => $ldap{$mail}{name},
+ bugmail => $ldap{$mail}{bugmail} || $mail,
+ };
+ }
+ }
+}
+
+#
+# reports
+#
+
+my @bmo_report;
+push @bmo_report, generate_report(
+ 'new ldap accounts',
+ 'no action required',
+ @new_ldap);
+
+push @bmo_report, generate_report(
+ 'deleted ldap accounts',
+ 'disable bmo account',
+ @gone_ldap_bmo);
+
+push @bmo_report, generate_report(
+ 'deleted ldap accounts',
+ 'no action required (no bmo account)',
+ @gone_ldap_no_bmo);
+
+push @bmo_report, generate_report(
+ 'suspect bugzilla accounts',
+ 'remove from mo-co if required',
+ @suspect_bugzilla);
+
+push @bmo_report, generate_report(
+ 'miss-configured bugzilla accounts',
+ 'ask owner to update phonebook, disable if not on phonebook',
+ @invalid_bugzilla);
+
+push @bmo_report, generate_report(
+ 'ldap accounts without mo-co group',
+ 'verify, and add mo-co group to bmo account',
+ @ldap_unblessed);
+
+push @bmo_report, generate_report(
+ 'missmatched bugmail entries on ldap accounts',
+ 'ask owner to update phonebook',
+ @invalid_bugmail);
+
+push @bmo_report, generate_report(
+ 'invalid bugmail entries on ldap accounts',
+ 'ask owner to update phonebook',
+ @invalid_ldap);
+
+if (!scalar @bmo_report) {
+ push @bmo_report, '**';
+ push @bmo_report, '** nothing to report \o/';
+ push @bmo_report, '**';
+}
+
+email_report(\@bmo_report, 'moco-ldap-check', BMO_RECIPIENTS);
+
+my @support_report;
+
+push @support_report, generate_report(
+ 'Missmatched "Bugzilla Email" entries on LDAP accounts',
+ 'Ask owner to update phonebook, or update directly',
+ @invalid_bugmail);
+
+push @support_report, generate_report(
+ 'Invalid "Bugzilla Email" entries on LDAP accounts',
+ 'Ask owner to update phonebook',
+ @invalid_ldap);
+
+if (scalar @support_report) {
+ email_report(\@support_report, 'Invalid "Bugzilla Email" entries in LDAP', SUPPORT_RECIPIENTS);
+}
+
+#
+#
+#
+
+sub generate_report {
+ my ($title, $action, @lines) = @_;
+
+ my $count = scalar @lines;
+ return unless $count;
+
+ my @report;
+ push @report, '';
+ push @report, '**';
+ push @report, "** $title ($count)";
+ push @report, "** [ $action ]";
+ push @report, '**';
+ push @report, '';
+
+ my $max_length = 0;
+ foreach my $rh (@lines) {
+ $max_length = length($rh->{mail}) if length($rh->{mail}) > $max_length;
+ }
+
+ foreach my $rh (@lines) {
+ my $template = "%-${max_length}s %s";
+ my @fields = ($rh->{mail}, $rh->{name});
+
+ if ($rh->{bugmail}) {
+ $template .= ' (%s)';
+ push @fields, $rh->{bugmail};
+ };
+
+ push @report, sprintf($template, @fields);
+ }
+
+ return @report;
+}
+
+sub email_report {
+ my ($report, $subject, @recipients) = @_;
+ unshift @$report, (
+ "Subject: $subject",
+ 'X-Bugzilla-Type: moco-ldap-check',
+ 'From: ' . REPORT_SENDER,
+ 'To: ' . join(',', @recipients),
+ );
+ if ($debug) {
+ print "\n", join("\n", @$report), "\n";
+ } else {
+ MessageToMTA(join("\n", @$report));
+ }
+}
+
+sub clean_email {
+ my $email = shift;
+ $email = trim($email);
+ $email = $1 if $email =~ /^(\S+)/;
+ $email =~ s/&#64;/@/;
+ $email = lc $email;
+ return $email;
+}
+
+sub canon_email {
+ my $email = shift;
+ $email = clean_email($email);
+ $email =~ s/^([^\+]+)\+[^\@]+(\@.+)$/$1$2/;
+ return $email;
+}
+
+sub trim {
+ my $value = shift;
+ $value =~ s/(^\s+|\s+$)//g;
+ return $value;
+}
+
+sub serialise {
+ my ($filename, $ref) = @_;
+ local $Data::Dumper::Purity = 1;
+ local $Data::Dumper::Deepcopy = 1;
+ local $Data::Dumper::Sortkeys = 1;
+ write_file($filename, Dumper($ref));
+}
+
+sub deserialise {
+ my ($filename) = @_;
+ return unless -s $filename;
+ my $cpt = Safe->new();
+ $cpt->reval('our ' . read_file($filename))
+ || die "$!";
+ return ${$cpt->varglob('VAR1')};
+}
+
diff --git a/scripts/move_flag_types.pl b/scripts/move_flag_types.pl
new file mode 100755
index 000000000..1f4398be1
--- /dev/null
+++ b/scripts/move_flag_types.pl
@@ -0,0 +1,172 @@
+#!/usr/bin/perl
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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.
+#
+# The Initial Developer of the Original Code is Mozilla Foundation
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+#===============================================================================
+#
+# FILE: move_flag_types.pl
+#
+# USAGE: ./move_flag_types.pl
+#
+# DESCRIPTION: Move current set flag from one type_id to another
+# based on product and optionally component.
+#
+# OPTIONS: ---
+# REQUIREMENTS: ---
+# BUGS: ---
+# NOTES: ---
+# AUTHOR: David Lawrence (:dkl), dkl@mozilla.com
+# COMPANY: Mozilla Foundation
+# VERSION: 1.0
+# CREATED: 08/22/2011 05:18:06 PM
+# REVISION: ---
+#===============================================================================
+
+=head1 NAME
+
+move_flag_types.pl - Move currently set flags from one type id to another based
+on product and optionally component.
+
+=head1 SYNOPSIS
+
+This script will move bugs matching a specific product (and optionally a component)
+from one flag type id to another if the bug has the flag set to either +, -, or ?.
+
+./move_flag_types.pl --old-id 4 --new-id 720 --product Firefox --component Installer
+
+=head1 OPTIONS
+
+=over
+
+=item B<--help|-h|?>
+
+Print a brief help message and exits.
+
+=item B<--oldid|-o>
+
+Old flag type id. Use editflagtypes.cgi to determine the type id from the URL.
+
+=item B<--newid|-n>
+
+New flag type id. Use editflagtypes.cgi to determine the type id from the URL.
+
+=item B<--product|-p>
+
+The product that the bugs most be assigned to.
+
+=item B<--component|-c>
+
+Optional: The component of the given product that the bugs must be assigned to.
+
+=item B<--doit|-d>
+
+Without this argument, changes are not actually committed to the database.
+
+=back
+
+=cut
+
+use strict;
+use warnings;
+
+use lib '.';
+
+use Bugzilla;
+use Getopt::Long;
+use Pod::Usage;
+
+my %params;
+GetOptions(\%params, 'help|h|?', 'oldid|o=s', 'newid|n=s',
+ 'product|p=s', 'component|c:s', 'doit|d') or pod2usage(1);
+
+if ($params{'help'} || !$params{'oldid'}
+ || !$params{'newid'} || !$params{'product'}) {
+ pod2usage({ -message => "Missing required argument",
+ -exitval => 1 });
+}
+
+# Set defaults
+$params{'doit'} ||= 0;
+$params{'component'} ||= '';
+
+my $dbh = Bugzilla->dbh;
+
+# Get the flag names
+my $old_flag_name = $dbh->selectrow_array(
+ "SELECT name FROM flagtypes WHERE id = ?",
+ undef, $params{'oldid'});
+my $new_flag_name = $dbh->selectrow_array(
+ "SELECT name FROM flagtypes WHERE id = ?",
+ undef, $params{'newid'});
+
+# Find the product id
+my $product_id = $dbh->selectrow_array(
+ "SELECT id FROM products WHERE name = ?",
+ undef, $params{'product'});
+
+# Find the component id if not __ANY__
+my $component_id;
+if ($params{'component'}) {
+ $component_id = $dbh->selectrow_array(
+ "SELECT id FROM components WHERE name = ? AND product_id = ?",
+ undef, $params{'component'}, $product_id);
+}
+
+my @query_args = ($params{'oldid'});
+
+my $flag_query = "SELECT flags.id AS flag_id, flags.bug_id AS bug_id
+ FROM flags JOIN bugs ON flags.bug_id = bugs.bug_id
+ WHERE flags.type_id = ? ";
+
+if ($component_id) {
+ # No need to compare against product_id as component_id is already
+ # tied to a specific product
+ $flag_query .= "AND bugs.component_id = ?";
+ push(@query_args, $component_id);
+}
+else {
+ # All bugs for a product regardless of component
+ $flag_query .= "AND bugs.product_id = ?";
+ push(@query_args, $product_id);
+}
+
+my $flags = $dbh->selectall_arrayref($flag_query, undef, @query_args);
+
+if (@$flags) {
+ print "Moving '" . scalar @$flags . "' flags " .
+ "from $old_flag_name (" . $params{'oldid'} . ") " .
+ "to $new_flag_name (" . $params{'newid'} . ")...\n";
+
+ if (!$params{'doit'}) {
+ print "Pass the argument --doit or -d to permanently make changes to the database.\n";
+ }
+ else {
+ my $flag_update_sth = $dbh->prepare("UPDATE flags SET type_id = ? WHERE id = ?");
+
+ foreach my $flag (@$flags) {
+ my ($flag_id, $bug_id) = @$flag;
+ print "Bug: $bug_id Flag: $flag_id\n";
+ $flag_update_sth->execute($params{'newid'}, $flag_id);
+ }
+ }
+
+ # It's complex to determine which items now need to be flushed from memcached.
+ # As this is expected to be a rare event, we just flush the entire cache.
+ Bugzilla->memcached->clear_all();
+}
+else {
+ print "No flags to move\n";
+}
diff --git a/scripts/move_os.pl b/scripts/move_os.pl
new file mode 100755
index 000000000..96b58d616
--- /dev/null
+++ b/scripts/move_os.pl
@@ -0,0 +1,81 @@
+#!/usr/bin/perl
+# 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 5.10.1;
+use strict;
+use warnings;
+
+use FindBin '$RealBin';
+use lib "$RealBin/../..", "$RealBin/../../lib";
+
+use Bugzilla;
+use Bugzilla::Field;
+use Bugzilla::Constants;
+
+use Getopt::Long qw( :config gnu_getopt );
+use Pod::Usage;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my ($from_os, $to_os);
+GetOptions('from=s' => \$from_os, 'to=s' => \$to_os);
+
+pod2usage(1) unless defined $from_os && defined $to_os;
+
+
+my $check_from_os = Bugzilla::Field::Choice->type('op_sys')->match({ value => $from_os });
+my $check_to_os = Bugzilla::Field::Choice->type('op_sys')->match({ value => $to_os });
+die "Cannot move $from_os because it does not exist\n"
+ unless @$check_from_os == 1;
+die "Cannot move $from_os because $to_os doesn't exist.\n"
+ unless @$check_to_os == 1;
+
+my $dbh = Bugzilla->dbh;
+my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+my $bug_ids = $dbh->selectcol_arrayref(q{SELECT bug_id FROM bugs WHERE bugs.op_sys = ?}, undef, $from_os);
+my $field = Bugzilla::Field->check({ name => 'op_sys', cache => 1 });
+my $nobody = Bugzilla::User->check({ name => 'nobody@mozilla.org', cache => 1 });
+
+my $bug_count = @$bug_ids;
+if ($bug_count == 0) {
+ warn "There are no bugs to move.\n";
+ exit 1;
+}
+
+print STDERR <<EOF;
+About to move $bug_count bugs from $from_os to $to_os.
+
+Press <Ctrl-C> to stop or <Enter> to continue...
+EOF
+getc();
+
+$dbh->bz_start_transaction;
+foreach my $bug_id (@$bug_ids) {
+ warn "Moving $bug_id...\n";
+
+ $dbh->do(q{INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added)
+ VALUES (?, ?, ?, ?, ?, ?)},
+ undef, $bug_id, $nobody->id, $timestamp, $field->id, $from_os, $to_os);
+ $dbh->do(q{UPDATE bugs SET op_sys = ?, delta_ts = ?, lastdiffed = ? WHERE bug_id = ?},
+ undef, $to_os, $timestamp, $timestamp, $bug_id);
+}
+$dbh->bz_commit_transaction;
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
+
+__END__
+
+=head1 NAME
+
+move_os.pl - move the os on all bugs with a particular os to a new os
+
+=head1 SYNOPSIS
+
+ move_os.pl --from 'Windows 8 Metro' --to 'Windows 8.1'
diff --git a/scripts/movebugs.pl b/scripts/movebugs.pl
new file mode 100755
index 000000000..ebe41ceb4
--- /dev/null
+++ b/scripts/movebugs.pl
@@ -0,0 +1,190 @@
+#!/usr/bin/perl -w
+
+# 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;
+
+use Cwd 'abs_path';
+use File::Basename;
+use FindBin;
+use lib "$FindBin::Bin/../..";
+use lib "$FindBin::Bin/../../lib";
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::FlagType;
+use Bugzilla::Hook;
+use Bugzilla::Util;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 4) {
+ die <<USAGE;
+Usage: movebugs.pl <old-product> <old-component> <new-product> <new-component>
+
+Eg. movebugs.pl mozilla.org bmo bugzilla.mozilla.org admin
+Will move all bugs in the mozilla.org:bmo component to the
+bugzilla.mozilla.org:admin component.
+
+The new product must have matching versions, milestones, and flags from the old
+product (will be validated by this script).
+USAGE
+}
+
+my ($old_product, $old_component, $new_product, $new_component) = @ARGV;
+
+my $dbh = Bugzilla->dbh;
+
+my $old_product_id = $dbh->selectrow_array(
+ "SELECT id FROM products WHERE name=?",
+ undef, $old_product);
+$old_product_id
+ or die "Can't find product ID for '$old_product'.\n";
+
+my $old_component_id = $dbh->selectrow_array(
+ "SELECT id FROM components WHERE name=? AND product_id=?",
+ undef, $old_component, $old_product_id);
+$old_component_id
+ or die "Can't find component ID for '$old_component'.\n";
+
+my $new_product_id = $dbh->selectrow_array(
+ "SELECT id FROM products WHERE name=?",
+ undef, $new_product);
+$new_product_id
+ or die "Can't find product ID for '$new_product'.\n";
+
+my $new_component_id = $dbh->selectrow_array(
+ "SELECT id FROM components WHERE name=? AND product_id=?",
+ undef, $new_component, $new_product_id);
+$new_component_id
+ or die "Can't find component ID for '$new_component'.\n";
+
+my $product_field_id = $dbh->selectrow_array(
+ "SELECT id FROM fielddefs WHERE name = 'product'");
+$product_field_id
+ or die "Can't find field ID for 'product' field\n";
+my $component_field_id = $dbh->selectrow_array(
+ "SELECT id FROM fielddefs WHERE name = 'component'");
+$component_field_id
+ or die "Can't find field ID for 'component' field\n";
+
+my $user_id = $dbh->selectrow_array(
+ "SELECT userid FROM profiles WHERE login_name='nobody\@mozilla.org'");
+$user_id
+ or die "Can't find user ID for 'nobody\@mozilla.org'\n";
+
+$dbh->bz_start_transaction();
+
+# build list of bugs
+my $ra_ids = $dbh->selectcol_arrayref(
+ "SELECT bug_id FROM bugs WHERE product_id=? AND component_id=?",
+ undef, $old_product_id, $old_component_id);
+my $bug_count = scalar @$ra_ids;
+$bug_count
+ or die "No bugs were found in '$old_component'\n";
+my $where_sql = 'bug_id IN (' . join(',', @$ra_ids) . ')';
+
+# check versions
+my @missing_versions;
+my $ra_versions = $dbh->selectcol_arrayref(
+ "SELECT DISTINCT version FROM bugs WHERE $where_sql");
+foreach my $version (@$ra_versions) {
+ my $has_version = $dbh->selectrow_array(
+ "SELECT 1 FROM versions WHERE product_id=? AND value=?",
+ undef, $new_product_id, $version);
+ push @missing_versions, $version unless $has_version;
+}
+
+# check milestones
+my @missing_milestones;
+my $ra_milestones = $dbh->selectcol_arrayref(
+ "SELECT DISTINCT target_milestone FROM bugs WHERE $where_sql");
+foreach my $milestone (@$ra_milestones) {
+ my $has_milestone = $dbh->selectrow_array(
+ "SELECT 1 FROM milestones WHERE product_id=? AND value=?",
+ undef, $new_product_id, $milestone);
+ push @missing_milestones, $milestone unless $has_milestone;
+}
+
+# check flags
+my @missing_flags;
+my $ra_old_types = $dbh->selectcol_arrayref(
+ "SELECT DISTINCT type_id
+ FROM flags
+ INNER JOIN flagtypes ON flagtypes.id = flags.type_id
+ WHERE $where_sql");
+my $ra_new_types =
+ Bugzilla::FlagType::match({ product_id => $new_product_id,
+ component_id => $new_component_id });
+foreach my $old_type (@$ra_old_types) {
+ unless (grep { $_->id == $old_type } @$ra_new_types) {
+ my $flagtype = Bugzilla::FlagType->new($old_type);
+ push @missing_flags, $flagtype->name . ' (' . $flagtype->target_type . ')';
+ }
+}
+
+# show missing
+my $missing_error = '';
+if (@missing_versions) {
+ $missing_error .= "'$new_product' is missing the following version(s):\n " .
+ join("\n ", @missing_versions) . "\n";
+}
+if (@missing_milestones) {
+ $missing_error .= "'$new_product' is missing the following milestone(s):\n " .
+ join("\n ", @missing_milestones) . "\n";
+}
+if (@missing_flags) {
+ $missing_error .= "'$new_product'::'$new_component' is missing the following flag(s):\n " .
+ join("\n ", @missing_flags) . "\n";
+}
+die $missing_error if $missing_error;
+
+# confirmation
+print <<EOF;
+About to move $bug_count bugs
+From '$old_product' : '$old_component'
+To '$new_product' : '$new_component'
+
+Press <Ctrl-C> to stop or <Enter> to continue...
+EOF
+getc();
+
+print "Moving $bug_count bugs from $old_product:$old_component to $new_product:$new_component\n";
+
+# update bugs
+$dbh->do(
+ "UPDATE bugs SET product_id=?, component_id=? WHERE $where_sql",
+ undef, $new_product_id, $new_component_id);
+
+# touch bugs
+$dbh->do("UPDATE bugs SET delta_ts=NOW() WHERE $where_sql");
+$dbh->do("UPDATE bugs SET lastdiffed=NOW() WHERE $where_sql");
+
+# update bugs_activity
+$dbh->do(
+ "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added)
+ SELECT bug_id, ?, delta_ts, ?, ?, ? FROM bugs WHERE $where_sql",
+ undef,
+ $user_id, $product_field_id, $old_product, $new_product);
+$dbh->do(
+ "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added)
+ SELECT bug_id, ?, delta_ts, ?, ?, ? FROM bugs WHERE $where_sql",
+ undef,
+ $user_id, $component_field_id, $old_component, $new_component);
+
+Bugzilla::Hook::process('reorg_move_bugs', { bug_ids => $ra_ids } );
+
+$dbh->bz_commit_transaction();
+
+foreach my $bug_id (@$ra_ids) {
+ Bugzilla->memcached->clear({ table => 'bugs', id => $bug_id });
+}
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
diff --git a/scripts/movecomponent.pl b/scripts/movecomponent.pl
new file mode 100755
index 000000000..cb07b84fc
--- /dev/null
+++ b/scripts/movecomponent.pl
@@ -0,0 +1,154 @@
+#!/usr/bin/perl -w
+# 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;
+
+use FindBin '$RealBin';
+use lib "$RealBin/../..", "$RealBin/../../lib";
+
+use Bugzilla;
+use Bugzilla::Component;
+use Bugzilla::Constants;
+use Bugzilla::Field;
+use Bugzilla::Hook;
+use Bugzilla::Product;
+use Bugzilla::Util;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 3) {
+ die <<USAGE;
+Usage: movecomponent.pl <oldproduct> <newproduct> <component>
+
+E.g.: movecomponent.pl ReplicationEngine FoodReplicator SeaMonkey
+will move the component "SeaMonkey" from the product "ReplicationEngine"
+to the product "FoodReplicator".
+
+Important: You must make sure the milestones and versions of the bugs in the
+component are available in the new product. See syncmsandversions.pl.
+
+USAGE
+}
+
+my ($old_product_name, $new_product_name, $component_name) = @ARGV;
+my $old_product = Bugzilla::Product->check({ name => $old_product_name });
+my $new_product = Bugzilla::Product->check({ name => $new_product_name });
+my $component = Bugzilla::Component->check({ product => $old_product, name => $component_name });
+my $field_id = get_field_id('product');
+
+my $dbh = Bugzilla->dbh;
+
+# check versions
+my @missing_versions;
+my $ra_versions = $dbh->selectcol_arrayref(
+ "SELECT DISTINCT version FROM bugs WHERE component_id = ?",
+ undef, $component->id);
+foreach my $version (@$ra_versions) {
+ my $has_version = $dbh->selectrow_array(
+ "SELECT 1 FROM versions WHERE product_id = ? AND value = ?",
+ undef, $new_product->id, $version);
+ push @missing_versions, $version unless $has_version;
+}
+
+# check milestones
+my @missing_milestones;
+my $ra_milestones = $dbh->selectcol_arrayref(
+ "SELECT DISTINCT target_milestone FROM bugs WHERE component_id = ?",
+ undef, $component->id);
+foreach my $milestone (@$ra_milestones) {
+ my $has_milestone = $dbh->selectrow_array(
+ "SELECT 1 FROM milestones WHERE product_id=? AND value=?",
+ undef, $new_product->id, $milestone);
+ push @missing_milestones, $milestone unless $has_milestone;
+}
+
+my $missing_error = '';
+if (@missing_versions) {
+ $missing_error .= "'$new_product_name' is missing the following version(s):\n " .
+ join("\n ", @missing_versions) . "\n";
+}
+if (@missing_milestones) {
+ $missing_error .= "'$new_product_name' is missing the following milestone(s):\n " .
+ join("\n ", @missing_milestones) . "\n";
+}
+die $missing_error if $missing_error;
+
+# confirmation
+print <<EOF;
+About to move the component '$component_name'
+From '$old_product_name'
+To '$new_product_name'
+
+Press <Ctrl-C> to stop or <Enter> to continue...
+EOF
+getc();
+
+print "Moving '$component_name' from '$old_product_name' to '$new_product_name'...\n\n";
+$dbh->bz_start_transaction();
+
+my $ra_ids = $dbh->selectcol_arrayref(
+ "SELECT bug_id FROM bugs WHERE product_id=? AND component_id=?",
+ undef, $old_product->id, $component->id);
+
+# Bugs table
+$dbh->do("UPDATE bugs SET product_id = ? WHERE component_id = ?",
+ undef,
+ ($new_product->id, $component->id));
+
+# Flags tables
+fix_flags('flaginclusions', $new_product, $component);
+fix_flags('flagexclusions', $new_product, $component);
+
+# Components
+$dbh->do("UPDATE components SET product_id = ? WHERE id = ?",
+ undef,
+ ($new_product->id, $component->id));
+
+Bugzilla::Hook::process('reorg_move_component', {
+ old_product => $old_product,
+ new_product => $new_product,
+ component => $component,
+} );
+
+# Mark bugs as touched
+$dbh->do("UPDATE bugs SET delta_ts = NOW()
+ WHERE component_id = ?", undef, $component->id);
+$dbh->do("UPDATE bugs SET lastdiffed = NOW()
+ WHERE component_id = ?", undef, $component->id);
+
+# Update bugs_activity
+my $userid = 1; # nobody@mozilla.org
+
+$dbh->do("INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed,
+ added)
+ SELECT bug_id, ?, delta_ts, ?, ?, ?
+ FROM bugs WHERE component_id = ?",
+ undef,
+ ($userid, $field_id, $old_product_name, $new_product_name, $component->id));
+
+Bugzilla::Hook::process('reorg_move_bugs', { bug_ids => $ra_ids } );
+
+$dbh->bz_commit_transaction();
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
+
+sub fix_flags {
+ my ($table, $new_product, $component) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $type_ids = $dbh->selectcol_arrayref("SELECT DISTINCT type_id FROM $table WHERE component_id = ?",
+ undef,
+ $component->id);
+ $dbh->do("DELETE FROM $table WHERE component_id = ?", undef, $component->id);
+ foreach my $type_id (@$type_ids) {
+ $dbh->do("INSERT INTO $table (type_id, product_id, component_id) VALUES (?, ?, ?)",
+ undef, ($type_id, $new_product->id, $component->id));
+ }
+}
diff --git a/scripts/nagios_blocker_checker.pl b/scripts/nagios_blocker_checker.pl
new file mode 100755
index 000000000..768053126
--- /dev/null
+++ b/scripts/nagios_blocker_checker.pl
@@ -0,0 +1,201 @@
+#!/usr/bin/perl
+
+# 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;
+use warnings;
+
+use FindBin qw($Bin);
+use lib "$Bin/..";
+use lib "$Bin/../lib";
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Product;
+use Bugzilla::User;
+use Getopt::Long;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my $config = {
+ # filter by assignee, product or component
+ assignee => '',
+ product => '',
+ component => '',
+ unassigned => 'nobody@mozilla.org',
+ # severities
+ severity => 'major,critical,blocker',
+ # time in hours to wait before paging/warning
+ major_alarm => 24,
+ major_warn => 20,
+ critical_alarm => 8,
+ critical_warn => 5,
+ blocker_alarm => 0,
+ blocker_warn => 0,
+ any_alarm => 24,
+ any_warn => 20,
+};
+
+my $usage = <<EOF;
+FILTERS
+
+ the filter determines which bugs to check, either by assignee, product or the
+ product's component. For backward compatibility, if just an email address is
+ provided, it will be used as the assignee.
+
+ --assignee <email> filter bugs by assignee
+ --product <name> filter bugs by product name
+ --component <name> filter bugs by product's component name
+ --unassigned <email> set the unassigned user (default: $config->{unassigned})
+
+SEVERITIES
+
+ by default alerts and warnings will be generated for 'major', 'critical', and
+ 'blocker' bugs. you can alter this list with the 'severity' switch.
+
+ setting severity to 'any' will result in alerting on unassigned bugs
+ regardless of severity.
+
+ --severity <major|critical|blocker>[,..]
+ --severity any
+
+TIMING
+
+ time in hours to wait before paging or warning.
+
+ --major_alarm <hours> (default: $config->{major_alarm})
+ --major_warn <hours> (default: $config->{major_warn})
+ --critical_alarm <hours> (default: $config->{critical_alarm})
+ --critical_warn <hours> (default: $config->{critical_warn})
+ --blocker_alarm <hours> (default: $config->{blocker_alarm})
+ --blocker_warn <hours> (default: $config->{blocker_warn})
+
+ when severity checking is set to "any", use the any_* switches instead:
+
+ --any_alarm <hours> (default: $config->{any_alarm})
+ --any_warn <hours> (default: $config->{any_warn})
+
+EXAMPLES
+
+ nagios_blocker_checker.pl --assignee server-ops\@mozilla-org.bugs
+ nagios_blocker_checker.pl server-ops\@mozilla-org.bugs
+ nagios_blocker_checker.pl --product 'Release Engineering' \
+ --component 'Loan Requests' \
+ --severity any --any_warn 24 --any_alarm 24
+EOF
+
+die($usage) unless GetOptions(
+ 'assignee=s' => \$config->{assignee},
+ 'product=s' => \$config->{product},
+ 'component=s' => \$config->{component},
+ 'severity=s' => \$config->{severity},
+ 'major_alarm=i' => \$config->{major_alarm},
+ 'major_warn=i' => \$config->{major_warn},
+ 'critical_alarm=i' => \$config->{critical_alarm},
+ 'critical_warn=i' => \$config->{critical_warn},
+ 'blocker_alarm=i' => \$config->{blocker_alarm},
+ 'blocker_warn=i' => \$config->{blocker_warn},
+ 'any_alarm=i' => \$config->{any_alarm},
+ 'any_warn=i' => \$config->{any_warn},
+ 'help|?' => \$config->{help},
+);
+$config->{assignee} = $ARGV[0] if !$config->{assignee} && @ARGV;
+die $usage if
+ $config->{help}
+ || !($config->{assignee} || $config->{product})
+ || ($config->{assignee} && $config->{product})
+ || ($config->{component} && !$config->{product})
+ || !$config->{severity};
+
+#
+
+use constant NAGIOS_OK => 0;
+use constant NAGIOS_WARNING => 1;
+use constant NAGIOS_CRITICAL => 2;
+use constant NAGIOS_NAMES => [qw( OK WARNING CRITICAL )];
+
+my $dbh = Bugzilla->switch_to_shadow_db;
+my $any_severity = $config->{severity} eq 'any';
+my ($where, @values);
+
+if ($config->{assignee}) {
+ $where = 'bugs.assigned_to = ?';
+ push @values, Bugzilla::User->check({ name => $config->{assignee} })->id;
+
+} elsif ($config->{component}) {
+ $where = 'bugs.product_id = ? AND bugs.component_id = ? AND bugs.assigned_to = ?';
+ my $product = Bugzilla::Product->check({ name => $config->{product} });
+ push @values, $product->id;
+ push @values, Bugzilla::Component->check({ product => $product, name => $config->{component} })->id;
+ push @values, Bugzilla::User->check({ name => $config->{unassigned} })->id;
+
+} else {
+ $where = 'bugs.product_id = ? AND bugs.assigned_to = ?';
+ push @values, Bugzilla::Product->check({ name => $config->{product} })->id;
+ push @values, Bugzilla::User->check({ name => $config->{unassigned} })->id;
+}
+
+if (!$any_severity) {
+ $where .= ' AND bug_severity IN (' .
+ join(',', map { $dbh->quote($_) } split(/,/, $config->{severity})) . ')';
+}
+
+my $sql = <<EOF;
+ SELECT bug_id, bug_severity, UNIX_TIMESTAMP(bugs.creation_ts) AS ts
+ FROM bugs
+ WHERE $where
+ AND COALESCE(resolution, '') = ''
+EOF
+
+my $bugs = {
+ 'major' => [],
+ 'critical' => [],
+ 'blocker' => [],
+ 'any' => [],
+};
+my $current_state = NAGIOS_OK;
+my $current_time = time;
+
+foreach my $bug (@{ $dbh->selectall_arrayref($sql, { Slice => {} }, @values) }) {
+ my $severity = $any_severity ? 'any' : $bug->{bug_severity};
+ my $age = ($current_time - $bug->{ts}) / 3600;
+
+ if ($age > $config->{"${severity}_alarm"}) {
+ $current_state = NAGIOS_CRITICAL;
+ push @{$bugs->{$severity}}, $bug->{bug_id};
+
+ } elsif ($age > $config->{"${severity}_warn"}) {
+ if ($current_state < NAGIOS_WARNING) {
+ $current_state = NAGIOS_WARNING;
+ }
+ push @{$bugs->{$severity}}, $bug->{bug_id};
+
+ }
+}
+
+print "bugs " . NAGIOS_NAMES->[$current_state] . ": ";
+if ($current_state == NAGIOS_OK) {
+ if ($config->{severity} eq 'any') {
+ print "No unassigned bugs found.";
+ } else {
+ print "No $config->{severity} bugs found."
+ }
+}
+foreach my $severity (qw( blocker critical major any )) {
+ my $list = $bugs->{$severity};
+ if (@$list) {
+ printf
+ "%s %s %s found https://bugzil.la/" . join(',', @$list) . " ",
+ scalar(@$list),
+ ($any_severity ? 'unassigned' : $severity),
+ (scalar(@$list) == 1 ? 'bug' : 'bugs');
+ }
+}
+print "\n";
+
+exit $current_state;
diff --git a/scripts/nuke-bugs.pl b/scripts/nuke-bugs.pl
new file mode 100755
index 000000000..0226c1726
--- /dev/null
+++ b/scripts/nuke-bugs.pl
@@ -0,0 +1,69 @@
+#!/usr/bin/perl -w
+# 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;
+
+use lib qw(.);
+
+use Bugzilla;
+use Bugzilla::Constants;
+
+use Getopt::Long;
+
+# This SQL is designed to delete the bugs and other activity in a Bugzilla database
+# so that one can use for development purposes or to start using it as a fresh installation.
+# Other data will be retained such as products, versions, flags, profiles, etc.
+
+$| = 1;
+my $trace = 0;
+
+GetOptions("trace" => \$trace) || exit;
+
+my $dbh = Bugzilla->dbh;
+
+$dbh->{TraceLevel} = 1 if $trace;
+
+print <<EOF;
+WARNING - This will delete all bugs, hit <enter> to continue or <ctrl-c> to cancel - WARNING
+EOF
+getc();
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+$dbh->bz_start_transaction();
+
+print "Deleting all bug data...\n";
+
+delete_from_table('bug_group_map');
+delete_from_table('bugs_activity');
+delete_from_table('cc');
+delete_from_table('dependencies');
+delete_from_table('duplicates');
+delete_from_table('flags');
+delete_from_table('keywords');
+delete_from_table('attach_data');
+delete_from_table('attachments');
+delete_from_table('bug_group_map');
+delete_from_table('bugs');
+delete_from_table('longdescs');
+
+$dbh->do($dbh->_bz_real_schema->get_set_serial_sql('bugs', 'bug_id', 1));
+
+$dbh->bz_commit_transaction();
+
+# This has to happen outside of the transaction
+$dbh->do("DELETE FROM bugs_fulltext");
+
+print "All done!\n";
+
+sub delete_from_table {
+ my $table = shift;
+ print "Deleting from $table...";
+ $dbh->do("DELETE FROM $table");
+ print "done.\n";
+}
diff --git a/scripts/reassign_open_bugs.pl b/scripts/reassign_open_bugs.pl
new file mode 100755
index 000000000..6496f9a95
--- /dev/null
+++ b/scripts/reassign_open_bugs.pl
@@ -0,0 +1,87 @@
+#!/usr/bin/perl
+# 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 5.10.1;
+use strict;
+use warnings;
+
+use FindBin '$RealBin';
+use lib "$RealBin/../..", "$RealBin/../../lib";
+
+use Bugzilla;
+use Bugzilla::User;
+use Bugzilla::Constants;
+
+use Getopt::Long qw( :config gnu_getopt );
+use Pod::Usage;
+
+# Load extensions for monkeypatched $user->clear_last_statistics_ts()
+BEGIN { Bugzilla->extensions(); }
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my ($from, $to);
+GetOptions(
+ "from|f=s" => \$from,
+ "to|t=s" => \$to,
+);
+
+pod2usage(1) unless defined $from && defined $to;
+
+my $dbh = Bugzilla->dbh;
+
+my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+my $field = Bugzilla::Field->check({ name => 'assigned_to', cache => 1 });
+my $from_user = Bugzilla::User->check({ name => $from, cache => 1 });
+my $to_user = Bugzilla::User->check({ name => $to, cache => 1 });
+
+my $bugs = $dbh->selectcol_arrayref(q{SELECT bug_id
+ FROM bugs
+ LEFT JOIN bug_status
+ ON bug_status.value = bugs.bug_status
+ WHERE bug_status.is_open = 1
+ AND bugs.assigned_to = ?}, undef, $from_user->id);
+my $bug_count = @$bugs;
+if ($bug_count == 0) {
+ warn "There are no bugs to move.\n";
+ exit 1;
+}
+
+print STDERR <<EOF;
+About to move $bug_count bugs from $from to $to.
+
+Press <Ctrl-C> to stop or <Enter> to continue...
+EOF
+getc();
+
+$dbh->bz_start_transaction;
+foreach my $bug_id (@$bugs) {
+ warn "Updating bug $bug_id\n";
+ $dbh->do(q{INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added)
+ VALUES (?, ?, ?, ?, ?, ?)},
+ undef, $bug_id, $to_user->id, $timestamp, $field->id, $from_user->login, $to_user->login);
+ $dbh->do(q{UPDATE bugs SET assigned_to = ?, delta_ts = ?, lastdiffed = ? WHERE bug_id = ?},
+ undef, $to_user->id, $timestamp, $timestamp, $bug_id);
+}
+$from_user->clear_last_statistics_ts();
+$to_user->clear_last_statistics_ts();
+$dbh->bz_commit_transaction;
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
+
+__END__
+
+=head1 NAME
+
+reassign-open-bugs.pl - reassign all open bugs from one user to another.
+
+=head1 SYNOPSIS
+
+ reassign-open-bugs.pl --from general@js.bugs --to nobody@mozilla.org
diff --git a/scripts/reset_default_user.pl b/scripts/reset_default_user.pl
new file mode 100755
index 000000000..173d03849
--- /dev/null
+++ b/scripts/reset_default_user.pl
@@ -0,0 +1,145 @@
+#!/usr/bin/perl -wT
+# 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;
+
+use lib '.';
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::User;
+use Bugzilla::Field;
+use Bugzilla::Util qw(trick_taint);
+
+use Getopt::Long;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my $dbh = Bugzilla->dbh;
+
+my $field_name = "";
+my $product = "";
+my $component = "";
+my $help = "";
+my %user_cache = ();
+
+my $result = GetOptions('field=s' => \$field_name,
+ 'product=s' => \$product,
+ 'component=s' => \$component,
+ 'help|h' => \$help);
+
+sub usage {
+ print <<USAGE;
+Usage: reset_default_user.pl --field <fieldname> --product <product> [--component <component>] [--help]
+
+This script will load all bugs matching the product, and optionally component,
+and reset the default user value back to the default value for the component.
+Valid field names are assigned_to and qa_contact.
+USAGE
+}
+
+if (!$product || $help
+ || ($field_name ne 'assigned_to' && $field_name ne 'qa_contact'))
+{
+ usage();
+ exit(1);
+}
+
+# We will need these for entering into bugs_activity
+my $who = Bugzilla::User->new({ name => 'nobody@mozilla.org' });
+my $field = Bugzilla::Field->new({ name => $field_name });
+
+trick_taint($product);
+my $product_id = $dbh->selectrow_array(
+ "SELECT id FROM products WHERE name = ?",
+ undef, $product);
+$product_id or die "Can't find product ID for '$product'.\n";
+
+my $component_id;
+my $default_user_id;
+if ($component) {
+ trick_taint($component);
+ my $colname = $field->name eq 'qa_contact'
+ ? 'initialqacontact'
+ : 'initialowner';
+ ($component_id, $default_user_id) = $dbh->selectrow_array(
+ "SELECT id, $colname FROM components " .
+ "WHERE name = ? AND product_id = ?",
+ undef, $component, $product_id);
+ $component_id or die "Can't find component ID for '$component'.\n";
+ $user_cache{$default_user_id} ||= Bugzilla::User->new($default_user_id);
+}
+
+# build list of bugs
+my $bugs_query = "SELECT bug_id, qa_contact, component_id " .
+ "FROM bugs WHERE product_id = ?";
+my @args = ($product_id);
+
+if ($component_id) {
+ $bugs_query .= " AND component_id = ? AND qa_contact != ?";
+ push(@args, $component_id, $default_user_id);
+}
+
+my $bugs = $dbh->selectall_arrayref($bugs_query, {Slice => {}}, @args);
+my $bug_count = scalar @$bugs;
+$bug_count
+ or die "No bugs were found.\n";
+
+# confirmation
+print <<EOF;
+About to reset $field_name for $bug_count bugs.
+
+Press <Ctrl-C> to stop or <Enter> to continue...
+EOF
+getc();
+
+$dbh->bz_start_transaction();
+
+foreach my $bug (@$bugs) {
+ my $bug_id = $bug->{bug_id};
+ my $old_user_id = $bug->{$field->name};
+ my $old_comp_id = $bug->{component_id};
+
+ # If only changing one component, we already have the default user id
+ my $new_user_id;
+ if ($default_user_id) {
+ $new_user_id = $default_user_id;
+ }
+ else {
+ my $colname = $field->name eq 'qa_contact'
+ ? 'initialqacontact'
+ : 'initialowner';
+ $new_user_id = $dbh->selectrow_array(
+ "SELECT $colname FROM components WHERE id = ?",
+ undef, $old_comp_id);
+ }
+
+ if ($old_user_id != $new_user_id) {
+ print "Resetting " . $field->name . " for bug $bug_id ...";
+
+ # Use the cached version if already exists
+ my $old_user = $user_cache{$old_user_id} ||= Bugzilla::User->new($old_user_id);
+ my $new_user = $user_cache{$new_user_id} ||= Bugzilla::User->new($new_user_id);
+
+ my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+
+ $dbh->do("UPDATE bugs SET " . $field->name . " = ? WHERE bug_id = ?",
+ undef, $new_user_id, $bug_id);
+ $dbh->do("INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) " .
+ "VALUES (?, ?, ?, ?, ?, ?)",
+ undef, $bug_id, $who->id, $timestamp, $field->id, $old_user->login, $new_user->login);
+ $dbh->do("UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?",
+ undef, $timestamp, $timestamp, $bug_id);
+
+ Bugzilla->memcached->clear({ table => 'bugs', id => $bug_id });
+
+ print "done.\n";
+ }
+}
+
+$dbh->bz_commit_transaction();
diff --git a/scripts/sanitizeme.pl b/scripts/sanitizeme.pl
new file mode 100755
index 000000000..af0c167bf
--- /dev/null
+++ b/scripts/sanitizeme.pl
@@ -0,0 +1,230 @@
+#!/usr/bin/perl -w
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is the Mozilla
+# Corporation. Portions created by Mozilla are
+# Copyright (C) 2006 Mozilla Foundation. All Rights Reserved.
+#
+# Contributor(s): Myk Melez <myk@mozilla.org>
+# Alex Brugh <alex@cs.umn.edu>
+# Dave Miller <justdave@mozilla.com>
+# Byron Jones <glob@mozilla.com>
+
+use strict;
+
+use FindBin '$RealBin';
+use lib "$RealBin/..", "$RealBin/lib";
+
+use Bugzilla;
+use Bugzilla::Bug;
+use Bugzilla::Constants;
+use Bugzilla::Hook;
+use Bugzilla::Util;
+use Getopt::Long;
+use List::MoreUtils qw(uniq);
+$| = 1;
+
+my $dbh = Bugzilla->dbh;
+
+# This SQL is designed to sanitize a copy of a Bugzilla database so that it
+# doesn't contain any information that can't be viewed from a web browser by
+# a user who is not logged in.
+
+my ($dry_run, $from_cron, $keep_attachments, $keep_group_bugs, $keep_groups, $execute,
+ $keep_passwords, $keep_insider, $trace, $enable_email) = (0, 0, 0, '', 0, 0, 0, 0, 0, 0);
+my $keep_group_bugs_sql = '';
+
+my $syntax = <<EOF;
+options:
+--execute perform database sanitization
+--keep-attachments disable removal of attachment content
+--keep-passwords disable resetting of passwords
+--keep-insider disable removal of insider comments and attachments
+--keep-group-bugs disable removal of the specified groups and associated bugs
+--keep-groups disable removal of group definitions
+--enable-email do not disable email for all users
+--dry-run do not update the database, just output what will be deleted
+--from-cron quite mode - suppress non-warning/error output
+--trace output sql statements
+EOF
+GetOptions(
+ "execute" => \$execute,
+ "dry-run" => \$dry_run,
+ "from-cron" => \$from_cron,
+ "keep-attachments" => \$keep_attachments,
+ "keep-passwords" => \$keep_passwords,
+ "keep-insider" => \$keep_insider,
+ "keep-group-bugs:s" => \$keep_group_bugs,
+ "keep-groups" => \$keep_groups,
+ "trace" => \$trace,
+ "enable-email" => \$enable_email,
+) or die $syntax;
+die "--execute switch required to perform database sanitization.\n\n$syntax"
+ unless $execute or $dry_run;
+
+if ($keep_group_bugs ne '') {
+ my @groups;
+ foreach my $group_id (split(/\s*,\s*/, $keep_group_bugs)) {
+ my $group;
+ if ($group_id =~ /\D/) {
+ $group = Bugzilla::Group->new({ name => $group_id });
+ } else {
+ $group = Bugzilla::Group->new($group_id);
+ }
+ die "Invalid group '$group_id'\n" unless $group;
+ push @groups, $group->id;
+ }
+ $keep_group_bugs_sql = "NOT IN (" . join(",", @groups) . ")";
+}
+
+$dbh->{TraceLevel} = 1 if $trace;
+
+if ($dry_run) {
+ print "** dry run : no changes to the database will be made **\n";
+ $dbh->bz_start_transaction();
+}
+eval {
+ delete_non_public_products();
+ delete_secure_bugs();
+ delete_deleted_comments();
+ delete_insider_comments() unless $keep_insider;
+ delete_security_groups() unless $keep_groups;
+ delete_sensitive_user_data();
+ delete_attachment_data() unless $keep_attachments;
+ delete_bug_user_last_visit();
+ Bugzilla::Hook::process('db_sanitize');
+ disable_email_delivery() unless $enable_email;
+ print "All done!\n";
+ $dbh->bz_rollback_transaction() if $dry_run;
+};
+if ($@) {
+ $dbh->bz_rollback_transaction() if $dry_run;
+ die "$@" if $@;
+}
+
+sub delete_non_public_products {
+ # Delete all non-public products, and all data associated with them
+ my @products = Bugzilla::Product->get_all();
+ my $mandatory = CONTROLMAPMANDATORY;
+ foreach my $product (@products) {
+ # if there are any mandatory groups on the product, nuke it and
+ # everything associated with it (including the bugs)
+ Bugzilla->params->{'allowbugdeletion'} = 1; # override this in memory for now
+ my $mandatorygroups = $dbh->selectcol_arrayref("SELECT group_id FROM group_control_map WHERE product_id = ? AND (membercontrol = $mandatory)", undef, $product->id);
+ if (0 < scalar(@$mandatorygroups)) {
+ print "Deleting product '" . $product->name . "'...\n";
+ $product->remove_from_db();
+ }
+ }
+}
+
+sub delete_secure_bugs {
+ # Delete all data for bugs in security groups.
+ my $buglist = $dbh->selectall_arrayref(
+ $keep_group_bugs
+ ? "SELECT DISTINCT bug_id FROM bug_group_map WHERE group_id $keep_group_bugs_sql"
+ : "SELECT DISTINCT bug_id FROM bug_group_map"
+ );
+ my $numbugs = scalar(@$buglist);
+ my $bugnum = 0;
+ print "Deleting $numbugs bugs in " . ($keep_group_bugs ? 'non-' : '') . "security groups...\n";
+ foreach my $row (@$buglist) {
+ my $bug_id = $row->[0];
+ $bugnum++;
+ print "\r$bugnum/$numbugs" unless $from_cron;
+ my $bug = new Bugzilla::Bug($bug_id);
+ $bug->remove_from_db();
+ }
+ print "\rDone \n" unless $from_cron;
+}
+
+sub delete_deleted_comments {
+ # Delete all comments tagged as 'deleted'
+ my $comment_ids = $dbh->selectcol_arrayref("SELECT comment_id FROM longdescs_tags WHERE tag='deleted'");
+ return unless @$comment_ids;
+ print "Deleting 'deleted' comments...\n";
+ my @bug_ids = uniq @{
+ $dbh->selectcol_arrayref("SELECT bug_id FROM longdescs WHERE comment_id IN (" . join(',', @$comment_ids) . ")")
+ };
+ $dbh->do("DELETE FROM longdescs WHERE comment_id IN (" . join(',', @$comment_ids) . ")");
+ foreach my $bug_id (@bug_ids) {
+ Bugzilla::Bug->new($bug_id)->_sync_fulltext(update_comments => 1);
+ }
+}
+
+sub delete_insider_comments {
+ # Delete all 'insidergroup' comments and attachments
+ print "Deleting 'insidergroup' comments and attachments...\n";
+ $dbh->do("DELETE FROM longdescs WHERE isprivate = 1");
+ $dbh->do("DELETE attach_data FROM attachments JOIN attach_data ON attachments.attach_id = attach_data.id WHERE attachments.isprivate = 1");
+ $dbh->do("DELETE FROM attachments WHERE isprivate = 1");
+ $dbh->do("UPDATE bugs_fulltext SET comments = comments_noprivate");
+}
+
+sub delete_security_groups {
+ # Delete all security groups.
+ print "Deleting " . ($keep_group_bugs ? 'non-' : '') . "security groups...\n";
+ $dbh->do("DELETE user_group_map FROM groups JOIN user_group_map ON groups.id = user_group_map.group_id WHERE groups.isbuggroup = 1");
+ $dbh->do("DELETE group_group_map FROM groups JOIN group_group_map ON (groups.id = group_group_map.member_id OR groups.id = group_group_map.grantor_id) WHERE groups.isbuggroup = 1");
+ $dbh->do("DELETE group_control_map FROM groups JOIN group_control_map ON groups.id = group_control_map.group_id WHERE groups.isbuggroup = 1");
+ $dbh->do("UPDATE flagtypes LEFT JOIN groups ON flagtypes.grant_group_id = groups.id SET grant_group_id = NULL WHERE groups.isbuggroup = 1");
+ $dbh->do("UPDATE flagtypes LEFT JOIN groups ON flagtypes.request_group_id = groups.id SET request_group_id = NULL WHERE groups.isbuggroup = 1");
+ if ($keep_group_bugs) {
+ $dbh->do("DELETE FROM groups WHERE isbuggroup = 1 AND id $keep_group_bugs_sql");
+ } else {
+ $dbh->do("DELETE FROM groups WHERE isbuggroup = 1");
+ }
+}
+
+sub delete_sensitive_user_data {
+ # Remove sensitive user account data.
+ print "Deleting sensitive user account data...\n";
+ $dbh->do("UPDATE profiles SET cryptpassword = 'deleted'") unless $keep_passwords;
+ $dbh->do("DELETE FROM user_api_keys");
+ $dbh->do("DELETE FROM profiles_activity");
+ $dbh->do("DELETE FROM profile_search");
+ $dbh->do("DELETE FROM namedqueries");
+ $dbh->do("DELETE FROM tokens");
+ $dbh->do("DELETE FROM logincookies");
+ $dbh->do("DELETE FROM login_failure");
+ $dbh->do("DELETE FROM audit_log");
+ # queued bugmail
+ $dbh->do("DELETE FROM ts_error");
+ $dbh->do("DELETE FROM ts_exitstatus");
+ $dbh->do("DELETE FROM ts_funcmap");
+ $dbh->do("DELETE FROM ts_job");
+ $dbh->do("DELETE FROM ts_note");
+}
+
+sub delete_attachment_data {
+ # Delete unnecessary attachment data.
+ print "Removing attachment data to preserve disk space...\n";
+ $dbh->do("UPDATE attach_data SET thedata = ''");
+}
+
+sub delete_bug_user_last_visit {
+ print "Removing all entries from bug_user_last_visit...\n";
+ $dbh->do('TRUNCATE TABLE bug_user_last_visit');
+}
+
+sub disable_email_delivery {
+ # turn off email delivery for all users.
+ print "Turning off email delivery...\n";
+ $dbh->do("UPDATE profiles SET disable_mail = 1");
+
+ # Also clear out the default flag cc as well since they do not
+ # have to be in the profiles table
+ $dbh->do("UPDATE flagtypes SET cc_list = NULL");
+}
diff --git a/scripts/sendunsentbugmail.pl b/scripts/sendunsentbugmail.pl
new file mode 100755
index 000000000..eeee41ced
--- /dev/null
+++ b/scripts/sendunsentbugmail.pl
@@ -0,0 +1,60 @@
+#!/usr/bin/perl -wT
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Dave Miller <justdave@bugzilla.org>
+# Myk Melez <myk@mozilla.org>
+
+use 5.10.1;
+use strict;
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::BugMail;
+
+my $dbh = Bugzilla->dbh;
+
+my $list = $dbh->selectcol_arrayref(
+ 'SELECT bug_id FROM bugs
+ WHERE lastdiffed IS NULL
+ OR lastdiffed < delta_ts
+ AND delta_ts < '
+ . $dbh->sql_date_math('NOW()', '-', 30, 'MINUTE') .
+ ' ORDER BY bug_id');
+
+if (scalar(@$list) > 0) {
+ say "OK, now attempting to send unsent mail";
+ say scalar(@$list) . " bugs found with possibly unsent mail.\n";
+ foreach my $bugid (@$list) {
+ my $start_time = time;
+ say "Sending mail for bug $bugid...";
+ my $outputref = Bugzilla::BugMail::Send($bugid);
+ if ($ARGV[0] && $ARGV[0] eq "--report") {
+ say "Mail sent to:";
+ say $_ foreach (sort @{$outputref->{sent}});
+ }
+ else {
+ my $sent = scalar @{$outputref->{sent}};
+ say "$sent mails sent.";
+ say "Took " . (time - $start_time) . " seconds.\n";
+ }
+ }
+ say "Unsent mail has been sent.";
+}
diff --git a/scripts/syncflags.pl b/scripts/syncflags.pl
new file mode 100755
index 000000000..520a8305c
--- /dev/null
+++ b/scripts/syncflags.pl
@@ -0,0 +1,88 @@
+#!/usr/bin/perl -w
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Gervase Markham <gerv@gerv.net>
+
+# See also https://bugzilla.mozilla.org/show_bug.cgi?id=119569
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+
+sub usage() {
+ print <<USAGE;
+Usage: syncflags.pl <srcproduct> <tgtproduct>
+
+E.g.: syncflags.pl FoodReplicator SeaMonkey
+will copy any flag inclusions (only) for the product "FoodReplicator"
+so matching inclusions exist for the product "SeaMonkey". This script is
+normally used prior to moving components from srcproduct to tgtproduct.
+USAGE
+
+ exit(1);
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+
+# This is a pure command line script.
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 2) {
+ usage();
+ exit();
+}
+
+my ($srcproduct, $tgtproduct) = @ARGV;
+
+my $dbh = Bugzilla->dbh;
+
+# Find product IDs
+my $srcprodid = $dbh->selectrow_array("SELECT id FROM products WHERE name = ?",
+ undef, $srcproduct);
+if (!$srcprodid) {
+ print "Can't find product ID for '$srcproduct'.\n";
+ exit(1);
+}
+
+my $tgtprodid = $dbh->selectrow_array("SELECT id FROM products WHERE name = ?",
+ undef, $tgtproduct);
+if (!$tgtprodid) {
+ print "Can't find product ID for '$tgtproduct'.\n";
+ exit(1);
+}
+
+$dbh->do("INSERT INTO flaginclusions(component_id, type_id, product_id)
+ SELECT fi1.component_id, fi1.type_id, ? FROM flaginclusions fi1
+ LEFT JOIN flaginclusions fi2
+ ON fi1.type_id = fi2.type_id
+ AND fi2.product_id = ?
+ WHERE fi1.product_id = ?
+ AND fi2.type_id IS NULL",
+ undef,
+ $tgtprodid, $tgtprodid, $srcprodid);
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
diff --git a/scripts/syncmsandversions.pl b/scripts/syncmsandversions.pl
new file mode 100755
index 000000000..20e88252e
--- /dev/null
+++ b/scripts/syncmsandversions.pl
@@ -0,0 +1,122 @@
+#!/usr/bin/perl -w
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Gervase Markham <gerv@gerv.net>
+
+# See also https://bugzilla.mozilla.org/show_bug.cgi?id=119569
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+
+sub usage() {
+ print <<USAGE;
+Usage: syncmsandversions.pl <srcproduct> <tgtproduct>
+
+E.g.: syncmsandversions.pl FoodReplicator SeaMonkey
+will copy any versions and milstones in the product "FoodReplicator"
+which do not exist in product "SeaMonkey" into it. This script is normally
+used prior to moving components from srcproduct to tgtproduct.
+USAGE
+
+ exit(1);
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+
+# This is a pure command line script.
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 2) {
+ usage();
+ exit();
+}
+
+my ($srcproduct, $tgtproduct) = @ARGV;
+
+my $dbh = Bugzilla->dbh;
+
+# Find product IDs
+my $srcprodid = $dbh->selectrow_array("SELECT id FROM products WHERE name = ?",
+ undef, $srcproduct);
+if (!$srcprodid) {
+ print "Can't find product ID for '$srcproduct'.\n";
+ exit(1);
+}
+
+my $tgtprodid = $dbh->selectrow_array("SELECT id FROM products WHERE name = ?",
+ undef, $tgtproduct);
+if (!$tgtprodid) {
+ print "Can't find product ID for '$tgtproduct'.\n";
+ exit(1);
+}
+
+$dbh->bz_start_transaction();
+
+$dbh->do("
+ INSERT INTO milestones(value, sortkey, isactive, product_id)
+ SELECT m1.value, m1.sortkey, m1.isactive, ?
+ FROM milestones m1
+ LEFT JOIN milestones m2 ON m1.value = m2.value
+ AND m2.product_id = ?
+ WHERE m1.product_id = ?
+ AND m2.value IS NULL
+ ",
+ undef,
+ $tgtprodid, $tgtprodid, $srcprodid);
+
+$dbh->do("
+ INSERT INTO versions(value, isactive, product_id)
+ SELECT v1.value, v1.isactive, ?
+ FROM versions v1
+ LEFT JOIN versions v2 ON v1.value = v2.value
+ AND v2.product_id = ?
+ WHERE v1.product_id = ?
+ AND v2.value IS NULL
+ ",
+ undef,
+ $tgtprodid, $tgtprodid, $srcprodid);
+
+$dbh->do("
+ INSERT INTO group_control_map (group_id, product_id, entry, membercontrol,
+ othercontrol, canedit, editcomponents,
+ editbugs, canconfirm)
+ SELECT g1.group_id, ?, g1.entry, g1.membercontrol, g1.othercontrol,
+ g1.canedit, g1.editcomponents, g1.editbugs, g1.canconfirm
+ FROM group_control_map g1
+ LEFT JOIN group_control_map g2 ON g1.product_id = ?
+ AND g2.product_id = ?
+ AND g1.group_id = g2.group_id
+ WHERE g1.product_id = ?
+ AND g2.group_id IS NULL
+ ",
+ undef,
+ $tgtprodid, $srcprodid, $tgtprodid, $srcprodid);
+
+$dbh->bz_commit_transaction();
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();