summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDylan William Hardison <dylan@hardison.net>2018-07-08 06:59:55 +0200
committerDylan William Hardison <dylan@hardison.net>2018-07-08 06:59:55 +0200
commitbe1f92450788dc89280c9e04a4bf983b5d7fac54 (patch)
treeb5513fd846597fe5f152177dbbda88dca08fdf5f
parent9bbd8d368598046c47964ee043620621b6c3634b (diff)
parent446a08b30b0dbaac9f2b88e0a5cad410f0446140 (diff)
downloadbugzilla-be1f92450788dc89280c9e04a4bf983b5d7fac54.tar.gz
bugzilla-be1f92450788dc89280c9e04a4bf983b5d7fac54.tar.xz
Merge remote-tracking branch 'bmo/master'
-rw-r--r--.circleci/config.yml7
-rw-r--r--.htaccess5
-rw-r--r--.perlcriticrc1
-rw-r--r--.vscode/tasks.json45
-rw-r--r--Bugzilla.pm227
-rw-r--r--Bugzilla/Attachment.pm6
-rw-r--r--Bugzilla/Bloomfilter.pm34
-rw-r--r--Bugzilla/BugMail.pm2
-rw-r--r--Bugzilla/CGI.pm12
-rw-r--r--Bugzilla/Config/Advanced.pm37
-rw-r--r--Bugzilla/Constants.pm3
-rw-r--r--Bugzilla/DB.pm64
-rw-r--r--Bugzilla/DB/Mysql.pm35
-rw-r--r--Bugzilla/DB/Oracle.pm27
-rw-r--r--Bugzilla/DB/Pg.pm20
-rw-r--r--Bugzilla/DB/Schema.pm10
-rw-r--r--Bugzilla/DB/Sqlite.pm37
-rw-r--r--Bugzilla/DaemonControl.pm6
-rw-r--r--Bugzilla/Error.pm111
-rw-r--r--Bugzilla/Extension.pm6
-rw-r--r--Bugzilla/Group.pm5
-rw-r--r--Bugzilla/Install.pm4
-rw-r--r--Bugzilla/Install/DB.pm12
-rw-r--r--Bugzilla/Install/Filesystem.pm16
-rw-r--r--Bugzilla/Install/Localconfig.pm5
-rw-r--r--Bugzilla/JobQueue.pm2
-rw-r--r--Bugzilla/Logging.pm19
-rw-r--r--Bugzilla/Mailer.pm26
-rw-r--r--Bugzilla/Markdown/GFM.pm92
-rw-r--r--Bugzilla/Markdown/GFM/Node.pm33
-rw-r--r--Bugzilla/Markdown/GFM/Parser.pm109
-rw-r--r--Bugzilla/Markdown/GFM/SyntaxExtension.pm31
-rw-r--r--Bugzilla/Markdown/GFM/SyntaxExtensionList.pm23
-rw-r--r--Bugzilla/Memcached.pm8
-rw-r--r--Bugzilla/Metrics/Collector.pm171
-rw-r--r--Bugzilla/Metrics/Memcached.pm24
-rw-r--r--Bugzilla/Metrics/Mysql.pm148
-rw-r--r--Bugzilla/Metrics/Reporter.pm103
-rw-r--r--Bugzilla/Metrics/Reporter/ElasticSearch.pm105
-rw-r--r--Bugzilla/Metrics/Reporter/STDERR.pm152
-rw-r--r--Bugzilla/Metrics/Template.pm24
-rw-r--r--Bugzilla/Metrics/Template/Context.pm30
-rw-r--r--Bugzilla/ModPerl.pm8
-rw-r--r--Bugzilla/Object.pm23
-rw-r--r--Bugzilla/Search/Quicksearch.pm8
-rw-r--r--Bugzilla/Sentry.pm374
-rw-r--r--Bugzilla/Template.pm5
-rw-r--r--Bugzilla/WebService/Bug.pm43
-rw-r--r--Bugzilla/WebService/Server.pm5
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Bug.pm5
-rw-r--r--Bugzilla/WebService/Server/XMLRPC.pm12
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--Dockerfile1
-rw-r--r--Log/Log4perl/Layout/Mozilla.pm5
-rwxr-xr-xMakefile.PL20
-rw-r--r--README.rst13
-rw-r--r--Vagrantfile3
-rwxr-xr-xadmin.cgi1
-rw-r--r--conf/httpd.conf1
-rw-r--r--conf/log4perl-docker.conf14
-rw-r--r--conf/log4perl-json.conf12
-rw-r--r--conf/log4perl-test.conf11
-rw-r--r--conf/log4perl-vagrant.conf24
-rw-r--r--docker-compose.yml2
-rw-r--r--docs/en/rst/api/core/v1/bug.rst116
-rwxr-xr-xeditusers.cgi34
-rw-r--r--extensions/BMO/Extension.pm2
-rw-r--r--extensions/BMO/lib/Data.pm4
-rw-r--r--extensions/BMO/template/en/default/account/create.html.tmpl4
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-third-party-apps.txt.tmpl22
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-dev-engagement-event.html.tmpl2
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-third-party-apps.html.tmpl191
-rw-r--r--extensions/BMO/template/en/default/bug/create/created-fxos-betaprogram.html.tmpl30
-rw-r--r--extensions/BMO/template/en/default/bug/create/custom_forms.none.tmpl10
-rw-r--r--extensions/BMO/template/en/default/email/bugmail.html.tmpl3
-rw-r--r--extensions/BMO/template/en/default/global/choose-product.html.tmpl18
-rw-r--r--extensions/BMO/template/en/default/hook/attachment/createformcontents-mimetypes.html.tmpl2
-rw-r--r--extensions/BMO/template/en/default/pages/attachment_bounty_form.html.tmpl4
-rw-r--r--extensions/BMO/web/producticons/devtools.pngbin0 -> 6807 bytes
-rw-r--r--extensions/BMO/web/producticons/webextensions.pngbin0 -> 3711 bytes
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/header.html.tmpl8
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/user.html.tmpl2
-rw-r--r--extensions/BugModal/web/bug_modal.css48
-rw-r--r--extensions/BugModal/web/bug_modal.js152
-rw-r--r--extensions/BugModal/web/comments.js2
-rw-r--r--extensions/BugModal/web/common_bug_modal.js2
-rw-r--r--extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl43
-rw-r--r--extensions/MyDashboard/web/js/flags.js142
-rw-r--r--extensions/MyDashboard/web/js/query.js7
-rw-r--r--extensions/MyDashboard/web/styles/mydashboard.css5
-rw-r--r--extensions/OpenGraph/template/en/default/hook/global/header-start.html.tmpl7
-rw-r--r--extensions/OrangeFactor/Extension.pm4
-rw-r--r--extensions/OrangeFactor/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl9
-rw-r--r--extensions/OrangeFactor/template/en/default/hook/bug_modal/edit-details_rhs.html.tmpl9
-rw-r--r--extensions/OrangeFactor/web/js/orange_factor.js59
-rw-r--r--extensions/PhabBugz/Extension.pm7
-rwxr-xr-xextensions/PhabBugz/bin/update_project_members.pl99
-rw-r--r--extensions/PhabBugz/lib/Config.pm5
-rw-r--r--extensions/PhabBugz/lib/Constants.pm8
-rw-r--r--extensions/PhabBugz/lib/Feed.pm461
-rw-r--r--extensions/PhabBugz/lib/Policy.pm26
-rw-r--r--extensions/PhabBugz/lib/Project.pm43
-rw-r--r--extensions/PhabBugz/lib/Revision.pm154
-rw-r--r--extensions/PhabBugz/lib/User.pm5
-rw-r--r--extensions/PhabBugz/lib/Util.pm457
-rw-r--r--extensions/PhabBugz/lib/WebService.pm389
-rw-r--r--extensions/PhabBugz/template/en/default/admin/email/squatter-alert.txt.tmpl34
-rw-r--r--extensions/PhabBugz/template/en/default/admin/params/phabbugz.html.tmpl1
-rw-r--r--extensions/PhabBugz/template/en/default/hook/global/user-error-errors.html.tmpl11
-rw-r--r--extensions/ProdCompSearch/web/js/prod_comp_search.js2
-rw-r--r--extensions/Push/lib/Backoff.pm9
-rw-r--r--extensions/Push/lib/Config.pm5
-rw-r--r--extensions/Push/lib/Connector/Phabricator.pm83
-rw-r--r--extensions/Push/lib/Connectors.pm28
-rw-r--r--extensions/Push/lib/Push.pm3
-rw-r--r--extensions/Review/Extension.pm2
-rw-r--r--extensions/SecureMail/Extension.pm61
-rw-r--r--extensions/SecureMail/lib/TCT.pm112
-rw-r--r--extensions/SecureMail/template/en/default/hook/admin/users/userdata-end.html.tmpl4
-rw-r--r--helper.psgi35
-rw-r--r--js/attachment.js15
-rw-r--r--js/comment-tagging.js2
-rw-r--r--js/field.js4
-rw-r--r--js/global.js2
-rwxr-xr-xmetrics.pl42
-rw-r--r--mod_perl.pl4
-rwxr-xr-xpage.cgi5
-rwxr-xr-xscripts/bloomfilter-populate.pl6
-rwxr-xr-xscripts/generate_conduit_data.pl1
-rwxr-xr-xscripts/nagios_blocker_checker.pl4
-rwxr-xr-xscripts/remove-non-public-data.pl16
-rwxr-xr-xscripts/sanitizeme.pl1
-rw-r--r--scripts/undo.pl203
-rwxr-xr-xsentry.pl94
-rwxr-xr-xses/index.cgi67
-rw-r--r--skins/standard/attachment.css8
-rw-r--r--skins/standard/global.css1
-rw-r--r--static/metricsgraphics/socorro-lens.html2
-rw-r--r--t/008filter.t3
-rw-r--r--t/markdown.t73
-rw-r--r--template/en/default/admin/admin.html.tmpl2
-rw-r--r--template/en/default/admin/params/advanced.html.tmpl21
-rw-r--r--template/en/default/admin/users/edit.html.tmpl73
-rw-r--r--template/en/default/admin/users/list.html.tmpl15
-rw-r--r--template/en/default/admin/users/userdata.html.tmpl49
-rw-r--r--template/en/default/attachment/createformcontents.html.tmpl6
-rw-r--r--template/en/default/attachment/show-multiple.html.tmpl4
-rw-r--r--template/en/default/global/code-error.html.tmpl10
-rw-r--r--template/en/default/global/header.html.tmpl5
-rw-r--r--template/en/default/global/site-navigation.html.tmpl20
-rw-r--r--template/en/default/global/user.html.tmpl2
-rw-r--r--template/en/default/index.html.tmpl1
-rw-r--r--template/en/default/setup/strings.txt.pl3
-rw-r--r--vagrant_support/apache.j22
-rw-r--r--vagrant_support/apache.yml8
-rw-r--r--vagrant_support/checksetup.yml6
-rw-r--r--vagrant_support/playbook.yml23
157 files changed, 2904 insertions, 3313 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml
index de4f2549d..5b18fac1a 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -111,6 +111,13 @@ jobs:
mv /opt/bmo/local /app/local
mkdir artifacts
- run: |
+<<<<<<< HEAD
+=======
+ [[ -f build_info/only_version_changed.txt ]] && exit 0
+ perl -I/app -I/app/local/lib/perl5 -c -E 'use Bugzilla; BEGIN { Bugzilla->extensions }'
+ - run: |
+ [[ -f build_info/only_version_changed.txt ]] && exit 0
+>>>>>>> bmo/master
perl Makefile.PL
- run:
name: run sanity tests
diff --git a/.htaccess b/.htaccess
index 196028005..06690e2c6 100644
--- a/.htaccess
+++ b/.htaccess
@@ -27,8 +27,6 @@ RewriteRule ^__version__$ version.json [L]
# heartbeat.cgi returns 200 if the DB and memcached are both working, and 500 otherwise.
RewriteRule ^__heartbeat__$ heartbeat.cgi [L]
-RewriteRule ^(\d+|quicksearch\.html|bugwritinghelp\.html)$ /helper/$1 [L]
-
RewriteRule ^static/v\d{4}\d{2}\d{2}\.\d+/(.+\.(?:js|css|woff2?|png|jpe?g|gif|ico|svg))$ $1 [NC,E=IMMUTABLE:1,L]
Header set Cache-Control "public, max-age=31536000" env=REDIRECT_IMMUTABLE
@@ -42,6 +40,7 @@ RewriteRule ^template_cache.deleteme/ - [F,L,NC]
RewriteRule ^review$ page.cgi?id=splinter.html$1 [QSA]
RewriteRule ^user_?profile$ page.cgi?id=user_profile.html$1 [QSA]
RewriteRule ^request_defer$ page.cgi?id=request_defer.html$1 [QSA]
+RewriteRule ^([0-9]+)$ show_bug.cgi?id=$1 [QSA]
RewriteRule ^favicon\.ico$ extensions/BMO/web/images/favicon.ico
RewriteRule ^form[\.:]itrequest$ enter_bug.cgi?product=Infrastructure+\%26+Operations&format=itrequest [QSA]
RewriteRule ^form[\.:](mozlist|poweredby|presentation|trademark|recoverykey)$ enter_bug.cgi?product=mozilla.org&format=$1 [QSA]
@@ -70,11 +69,9 @@ RewriteRule ^form[\.:]dev[\.\-:]engagement[\.\-\:]event$ enter_bug.cgi?product=D
RewriteRule ^form[\.:]mobile[\.\-:]compat$ enter_bug.cgi?product=Tech+Evangelism&format=mobile-compat [QSA]
RewriteRule ^form[\.:]web[\.:]bounty$ enter_bug.cgi?product=mozilla.org&format=web-bounty [QSA]
RewriteRule ^form[\.:]automative$ enter_bug.cgi?product=Testing&format=automative [QSA]
-RewriteRule ^form[\.:]fxos[\.\-:]preload[\.\-:]app$ enter_bug.cgi?product=Marketplace&format=fxos-preload-app [QSA]
RewriteRule ^form[\.:]comm[\.:]newsletter$ enter_bug.cgi?product=Marketing&format=comm-newsletter [QSA]
RewriteRule ^form[\.:]screen[\.:]share[\.:]whitelist$ enter_bug.cgi?product=Firefox&format=screen-share-whitelist [QSA]
RewriteRule ^form[\.:]data[\.\-:]compliance$ enter_bug.cgi?product=Data+Compliance&format=data-compliance [QSA]
-RewriteRule ^form[\.:]third[\.\-:]party$ enter_bug.cgi?product=Marketing&format=third-party-apps [QSA]
RewriteRule ^form[\.:]fsa[\.:]budget$ enter_bug.cgi?product=FSA&format=fsa-budget [QSA]
RewriteRule ^form[\.:]triage[\.\-]request$ page.cgi?id=triage_request.html [QSA]
RewriteRule ^form[\.:](crm|CRM)$ enter_bug.cgi?product=Marketing&format=crm [QSA]
diff --git a/.perlcriticrc b/.perlcriticrc
index 44254f64e..0b8e4c862 100644
--- a/.perlcriticrc
+++ b/.perlcriticrc
@@ -46,6 +46,7 @@ severity = 2
[-ValuesAndExpressions::ProhibitNoisyQuotes]
[-ValuesAndExpressions::ProhibitNoisyQuotes]
[-ValuesAndExpressions::ProhibitVersionStrings]
+[-ValuesAndExpressions::ProhibitImplicitNewlines]
[-Variables::ProhibitLocalVars]
[-Variables::ProhibitPackageVars]
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 000000000..757c6b5f7
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,45 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Vagrant: Start Local Server",
+ "type": "shell",
+ "command": "vagrant up",
+ "group": "none",
+ "problemMatcher": []
+ },
+ {
+ "label": "Vagrant: Suspend Local Server",
+ "type": "shell",
+ "command": "vagrant suspend",
+ "group": "none",
+ "problemMatcher": []
+ },
+ {
+ "label": "Vagrant: Update Local Server",
+ "type": "shell",
+ "command": "bash -c 'vagrant rsync && vagrant provision --provision-with update'",
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "problemMatcher": []
+ },
+ {
+ "label": "Vagrant: Force Update Local Server",
+ "type": "shell",
+ "command": "bash -c 'vagrant rsync && vagrant provision'",
+ "group": "build",
+ "problemMatcher": []
+ },
+ {
+ "label": "Vagrant: Update Perl Dependencies",
+ "type": "shell",
+ "command": "vagrant ssh web -c bmo-refresh-bundle",
+ "group": "none",
+ "problemMatcher": []
+ }
+ ]
+}
diff --git a/Bugzilla.pm b/Bugzilla.pm
index f5e64c371..3d7cb50d5 100644
--- a/Bugzilla.pm
+++ b/Bugzilla.pm
@@ -22,7 +22,7 @@ BEGIN {
}
}
-our $VERSION = '20180330.1';
+our $VERSION = '20180703.1';
use Bugzilla::Auth;
use Bugzilla::Auth::Persist::Cookie;
@@ -38,6 +38,8 @@ use Bugzilla::Flag;
use Bugzilla::Hook;
use Bugzilla::Install::Localconfig qw(read_localconfig);
use Bugzilla::Install::Util qw(init_console include_languages);
+use Bugzilla::Markdown::GFM;
+use Bugzilla::Markdown::GFM::Parser;
use Bugzilla::Memcached;
use Bugzilla::Template;
use Bugzilla::Token;
@@ -46,17 +48,12 @@ use Bugzilla::Util;
use Bugzilla::CPAN;
use Bugzilla::Bloomfilter;
-use Bugzilla::Metrics::Collector;
-use Bugzilla::Metrics::Template;
-use Bugzilla::Metrics::Memcached;
-
use Date::Parse;
use DateTime::TimeZone;
use Encode;
use File::Basename;
use File::Spec::Functions;
use Safe;
-use Sys::Syslog qw(:DEFAULT);
use JSON::XS qw(decode_json);
use URI;
@@ -83,6 +80,11 @@ use constant SHUTDOWNHTML_EXIT_SILENTLY => qw(
# should search engines attempt to index the page again?
use constant SHUTDOWNHTML_RETRY_AFTER => 3600;
+# This is identical to Install::Util::_cache so that things loaded
+# into Install::Util::_cache during installation can be read out
+# of request_cache later in installation.
+use constant request_cache => Bugzilla::Install::Util::_cache();
+
#####################################################################
# Global Code
#####################################################################
@@ -101,7 +103,7 @@ sub init_page {
}
if (i_am_cgi()) {
- Log::Log4perl::MDC->put(remote_ip => remote_ip());
+ Bugzilla::Logging->fields->{remote_ip} = remote_ip();
}
if (${^TAINT}) {
@@ -120,30 +122,6 @@ sub init_page {
my $script = basename($0);
- # BMO - init metrics collection if required
- if (i_am_cgi() && $script eq 'show_bug.cgi') {
- # we need to measure loading the params, so default to on
- Bugzilla->metrics_enabled(1);
- Bugzilla->metrics($script);
- # we can now hit params to check if we really should be enabled.
- # note - we can't use anything which uses templates or the database, as
- # that would initialise those modules with metrics enabled.
- if (!Bugzilla->params->{metrics_enabled}) {
- Bugzilla->metrics_enabled(0);
- }
- else {
- # to avoid generating massive amounts of data, we're only interested in
- # a small subset of users
- my $user_id = Bugzilla->cgi->cookie('Bugzilla_login');
- if (!$user_id
- || !grep { $user_id == $_ }
- split(/\s*,\s*/, Bugzilla->params->{metrics_user_ids}))
- {
- Bugzilla->metrics_enabled(0);
- }
- }
- }
-
# Because of attachment_base, attachment.cgi handles this itself.
if ($script ne 'attachment.cgi') {
do_ssl_redirect_if_required();
@@ -212,28 +190,21 @@ sub init_page {
#####################################################################
sub template {
- # BMO - use metrics subclass if required
- if (Bugzilla->metrics_enabled) {
- $_[0]->request_cache->{template} ||= Bugzilla::Metrics::Template->create();
- } else {
- $_[0]->request_cache->{template} ||= Bugzilla::Template->create();
- }
- $_[0]->request_cache->{template}->{_is_main} = 1;
+ request_cache->{template} ||= Bugzilla::Template->create();
+ request_cache->{template}->{_is_main} = 1;
- return $_[0]->request_cache->{template};
+ return request_cache->{template};
}
sub template_inner {
- my ($class, $lang) = @_;
- my $cache = $class->request_cache;
+ my (undef, $lang) = @_;
+ my $cache = request_cache;
my $current_lang = $cache->{template_current_lang}->[0];
$lang ||= $current_lang || '';
return $cache->{"template_inner_$lang"} ||= Bugzilla::Template->create(language => $lang);
}
sub extensions {
- my ($class) = @_;
-
# Guard against extensions querying the extension list during initialization
# (through this method or has_extension).
# The extension list is not fully populated at that point,
@@ -242,7 +213,7 @@ sub extensions {
die "Recursive attempt to load/query extensions" if $recursive;
$recursive = 1;
- my $cache = $class->request_cache;
+ my $cache = request_cache;
if (!$cache->{extensions}) {
my $extension_packages = Bugzilla::Extension->load_all();
my @extensions;
@@ -269,12 +240,12 @@ sub has_extension {
}
sub cgi {
- return $_[0]->request_cache->{cgi} ||= new Bugzilla::CGI();
+ return request_cache->{cgi} ||= new Bugzilla::CGI();
}
sub input_params {
my ($class, $params) = @_;
- my $cache = $class->request_cache;
+ my $cache = request_cache;
# This is how the WebService and other places set input_params.
if (defined $params) {
$cache->{input_params} = $params;
@@ -300,7 +271,7 @@ sub urlbase {
}
sub params {
- return $_[0]->request_cache->{params} ||= Bugzilla::Config::read_param_file();
+ return request_cache->{params} ||= Bugzilla::Config::read_param_file();
}
sub get_param_with_override {
@@ -309,32 +280,32 @@ sub get_param_with_override {
}
sub user {
- return $_[0]->request_cache->{user} ||= new Bugzilla::User;
+ return request_cache->{user} ||= new Bugzilla::User;
}
sub set_user {
- my ($class, $user) = @_;
- $class->request_cache->{user} = $user;
+ my (undef, $user) = @_;
+ request_cache->{user} = $user;
}
sub sudoer {
- return $_[0]->request_cache->{sudoer};
+ return request_cache->{sudoer};
}
sub sudo_request {
- my ($class, $new_user, $new_sudoer) = @_;
- $class->request_cache->{user} = $new_user;
- $class->request_cache->{sudoer} = $new_sudoer;
+ my (undef, $new_user, $new_sudoer) = @_;
+ request_cache->{user} = $new_user;
+ request_cache->{sudoer} = $new_sudoer;
# NOTE: If you want to log the start of an sudo session, do it here.
}
sub page_requires_login {
- return $_[0]->request_cache->{page_requires_login};
+ return request_cache->{page_requires_login};
}
sub github_secret {
my ($class) = @_;
- my $cache = $class->request_cache;
+ my $cache = request_cache;
my $cgi = $class->cgi;
$cache->{github_secret} //= $cgi->cookie('github_secret') // generate_random_password(256);
@@ -346,7 +317,7 @@ sub passwdqc {
my ($class) = @_;
require Data::Password::passwdqc;
- my $cache = $class->request_cache;
+ my $cache = request_cache;
my $params = $class->params;
return $cache->{passwdqc} if $cache->{passwdqc};
@@ -395,13 +366,13 @@ sub login {
# Allow templates to know that we're in a page that always requires
# login.
if ($type == LOGIN_REQUIRED) {
- $class->request_cache->{page_requires_login} = 1;
+ request_cache->{page_requires_login} = 1;
}
my $authenticated_user = $authorizer->login($type);
- if (i_am_cgi()) {
- Log::Log4perl::MDC->put(user_id => $authenticated_user->id);
+ if (i_am_cgi() && $authenticated_user->id) {
+ Bugzilla::Logging->fields->{user_id} = $authenticated_user->id;
}
# At this point, we now know if a real person is logged in.
@@ -484,7 +455,7 @@ sub login {
&& !$sudo_target->in_group('bz_sudo_protect'))
{
$class->set_user($sudo_target);
- $class->request_cache->{sudoer} = $authenticated_user;
+ request_cache->{sudoer} = $authenticated_user;
# And make sure that both users have the same Auth object,
# since we never call Auth::login for the sudo target.
$sudo_target->set_authorizer($authenticated_user->authorizer);
@@ -539,24 +510,25 @@ sub logout_user_by_id {
# hack that invalidates credentials for a single request
sub logout_request {
my $class = shift;
- delete $class->request_cache->{user};
- delete $class->request_cache->{sudoer};
+ delete request_cache->{user};
+ delete request_cache->{sudoer};
# We can't delete from $cgi->cookie, so logincookie data will remain
# there. Don't rely on it: use Bugzilla->user->login instead!
}
sub job_queue {
require Bugzilla::JobQueue;
- return $_[0]->request_cache->{job_queue} ||= Bugzilla::JobQueue->new();
+ return request_cache->{job_queue} ||= Bugzilla::JobQueue->new();
}
sub dbh {
+ my ($class) = @_;
# If we're not connected, then we must want the main db
- return $_[0]->request_cache->{dbh} ||= $_[0]->dbh_main;
+ return request_cache->{dbh} ||= $class->dbh_main;
}
sub dbh_main {
- return $_[0]->request_cache->{dbh_main} ||= Bugzilla::DB::connect_main();
+ return request_cache->{dbh_main} ||= Bugzilla::DB::connect_main();
}
sub languages {
@@ -564,25 +536,25 @@ sub languages {
}
sub current_language {
- return $_[0]->request_cache->{current_language} ||= (include_languages())[0];
+ return request_cache->{current_language} ||= (include_languages())[0];
}
sub error_mode {
- my ($class, $newval) = @_;
+ my (undef, $newval) = @_;
if (defined $newval) {
- $class->request_cache->{error_mode} = $newval;
+ request_cache->{error_mode} = $newval;
}
- return $class->request_cache->{error_mode}
+ return request_cache->{error_mode}
|| (i_am_cgi() ? ERROR_MODE_WEBPAGE : ERROR_MODE_DIE);
}
# This is used only by Bugzilla::Error to throw errors.
sub _json_server {
- my ($class, $newval) = @_;
+ my (undef, $newval) = @_;
if (defined $newval) {
- $class->request_cache->{_json_server} = $newval;
+ request_cache->{_json_server} = $newval;
}
- return $class->request_cache->{_json_server};
+ return request_cache->{_json_server};
}
sub usage_mode {
@@ -613,21 +585,21 @@ sub usage_mode {
ThrowCodeError('usage_mode_invalid',
{'invalid_usage_mode', $newval});
}
- $class->request_cache->{usage_mode} = $newval;
+ request_cache->{usage_mode} = $newval;
}
- return $class->request_cache->{usage_mode}
+ return request_cache->{usage_mode}
|| (i_am_cgi()? USAGE_MODE_BROWSER : USAGE_MODE_CMDLINE);
}
sub installation_mode {
- my ($class, $newval) = @_;
- ($class->request_cache->{installation_mode} = $newval) if defined $newval;
- return $class->request_cache->{installation_mode}
+ my (undef, $newval) = @_;
+ (request_cache->{installation_mode} = $newval) if defined $newval;
+ return request_cache->{installation_mode}
|| INSTALLATION_MODE_INTERACTIVE;
}
sub installation_answers {
- my ($class, $filename) = @_;
+ my (undef, $filename) = @_;
if ($filename) {
my $s = new Safe;
$s->rdo($filename);
@@ -636,23 +608,23 @@ sub installation_answers {
die "Error evaluating $filename: $@" if $@;
# Now read the param back out from the sandbox
- $class->request_cache->{installation_answers} = $s->varglob('answer');
+ request_cache->{installation_answers} = $s->varglob('answer');
}
- return $class->request_cache->{installation_answers} || {};
+ return request_cache->{installation_answers} || {};
}
sub switch_to_shadow_db {
my $class = shift;
- if (!$class->request_cache->{dbh_shadow}) {
+ if (!request_cache->{dbh_shadow}) {
if ($class->get_param_with_override('shadowdb')) {
- $class->request_cache->{dbh_shadow} = Bugzilla::DB::connect_shadow();
+ request_cache->{dbh_shadow} = Bugzilla::DB::connect_shadow();
} else {
- $class->request_cache->{dbh_shadow} = $class->dbh_main;
+ request_cache->{dbh_shadow} = $class->dbh_main;
}
}
- $class->request_cache->{dbh} = $class->request_cache->{dbh_shadow};
+ request_cache->{dbh} = request_cache->{dbh_shadow};
# we have to return $class->dbh instead of {dbh} as
# {dbh_shadow} may be undefined if no shadow DB is used
# and no connection to the main DB has been established yet.
@@ -662,7 +634,7 @@ sub switch_to_shadow_db {
sub switch_to_main_db {
my $class = shift;
- $class->request_cache->{dbh} = $class->dbh_main;
+ request_cache->{dbh} = $class->dbh_main;
return $class->dbh_main;
}
@@ -696,7 +668,7 @@ sub log_user_request {
}
eval {
- local $class->request_cache->{dbh};
+ local request_cache->{dbh};
$class->switch_to_main_db();
$class->dbh->do("INSERT INTO user_request_log
(user_id, ip_address, user_agent, request_url,
@@ -708,13 +680,13 @@ sub log_user_request {
sub is_shadow_db {
my $class = shift;
- return $class->request_cache->{dbh} != $class->dbh_main;
+ return request_cache->{dbh} != $class->dbh_main;
}
sub fields {
- my ($class, $criteria) = @_;
+ my (undef, $criteria) = @_;
$criteria ||= {};
- my $cache = $class->request_cache;
+ my $cache = request_cache;
# We create an advanced cache for fields by type, so that we
# can avoid going back to the database for every fields() call.
@@ -761,29 +733,28 @@ sub fields {
}
sub active_custom_fields {
- my ($class, $params) = @_;
+ my (undef, $params) = @_;
my $cache_id = 'active_custom_fields';
if ($params) {
$cache_id .= ($params->{product} ? '_p' . $params->{product}->id : '') .
($params->{component} ? '_c' . $params->{component}->id : '');
$cache_id .= ':noext' if $params->{skip_extensions};
}
- if (!exists $class->request_cache->{$cache_id}) {
+ if (!exists request_cache->{$cache_id}) {
my $fields = Bugzilla::Field->match({ custom => 1, obsolete => 0, skip_extensions => 1 });
Bugzilla::Hook::process('active_custom_fields',
{ fields => \$fields, params => $params });
- $class->request_cache->{$cache_id} = $fields;
+ request_cache->{$cache_id} = $fields;
}
- return @{$class->request_cache->{$cache_id}};
+ return @{request_cache->{$cache_id}};
}
sub has_flags {
- my $class = shift;
- if (!defined $class->request_cache->{has_flags}) {
- $class->request_cache->{has_flags} = Bugzilla::Flag->any_exist;
+ if (!defined request_cache->{has_flags}) {
+ request_cache->{has_flags} = Bugzilla::Flag->any_exist;
}
- return $class->request_cache->{has_flags};
+ return request_cache->{has_flags};
}
sub local_timezone {
@@ -793,19 +764,13 @@ sub local_timezone {
# Send messages to syslog for the auditing systems (eg. mozdef) to pick up.
sub audit {
- my ($class, $message) = @_;
+ my (undef, $message) = @_;
state $logger = Log::Log4perl->get_logger("audit");
$logger->notice(encode_utf8($message));
}
-# This creates the request cache for non-mod_perl installations.
-# This is identical to Install::Util::_cache so that things loaded
-# into Install::Util::_cache during installation can be read out
-# of request_cache later in installation.
-use constant request_cache => Bugzilla::Install::Util::_cache();
-
sub clear_request_cache {
- my ($class, %option) = @_;
+ my (undef, %option) = @_;
my $request_cache = request_cache();
my @except = $option{except} ? @{ $option{except} } : ();
@@ -821,37 +786,10 @@ sub process_cache {
return $_process_cache;
}
-# BMO - Instrumentation
-
-sub metrics_enabled {
- if (defined $_[1]) {
- if (!$_[1]
- && $_[0]->request_cache->{metrics_enabled}
- && $_[0]->request_cache->{metrics})
- {
- $_[0]->request_cache->{metrics}->cancel();
- delete $_[0]->request_cache->{metrics};
- }
- $_[0]->request_cache->{metrics_enabled} = $_[1];
- }
- else {
- return $_[0]->request_cache->{metrics_enabled};
- }
-}
-
-sub metrics {
- return $_[0]->request_cache->{metrics} ||= Bugzilla::Metrics::Collector->new($_[1]);
-}
-
# This is a memcached wrapper, which provides cross-process and cross-system
# caching.
sub memcached {
- # BMO - use metrics subclass if required
- if (Bugzilla->metrics_enabled) {
- return $_[0]->request_cache->{memcached} ||= Bugzilla::Metrics::Memcached->_new();
- } else {
- return $_[0]->request_cache->{memcached} ||= Bugzilla::Memcached->_new();
- }
+ return request_cache->{memcached} ||= Bugzilla::Memcached->_new();
}
sub elastic {
@@ -877,20 +815,25 @@ sub check_rate_limit {
}
my $limit = join("/", @$limit);
Bugzilla->audit("[rate_limit] action=$action, ip=$ip, limit=$limit, name=$name");
- ThrowUserError("rate_limit") if $action eq 'block';
+ if ($action eq 'block') {
+ Bugzilla::ModPerl::BlockIP->block_ip($ip);
+ ThrowUserError("rate_limit");
+ }
}
}
}
+sub markdown_parser {
+ return request_cache->{markdown_parser}
+ ||= Bugzilla::Markdown::GFM::Parser->new( {extensions => [qw( autolink tagfilter table strikethrough)] } );
+}
+
# Private methods
# Per-process cleanup. Note that this is a plain subroutine, not a method,
# so we don't have $class available.
sub _cleanup {
- # BMO - finalise and report on metrics
- if (Bugzilla->metrics_enabled) {
- Bugzilla->metrics->finish();
- }
+ return if $^C;
# BMO - allow "end of request" processing
Bugzilla::Hook::process('request_cleanup');
@@ -901,7 +844,6 @@ sub _cleanup {
foreach my $dbh ($main, $shadow) {
next if !$dbh;
$dbh->bz_rollback_transaction() if $dbh->bz_in_transaction;
- $dbh->disconnect;
}
clear_request_cache();
@@ -1205,6 +1147,11 @@ of features, see C<OPTIONAL_MODULES> in C<Bugzilla::Install::Requirements>.
Feeds the provided message into our centralised auditing system.
+=item C<markdown_parser>
+
+Returns a L<Bugzilla::Markdown::GFM::Parser> with the default extensions
+loaded (autolink, tagfilter, table, and strikethrough).
+
=back
=head1 B<CACHING>
diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm
index 0bdb50c9a..9eac3a147 100644
--- a/Bugzilla/Attachment.pm
+++ b/Bugzilla/Attachment.pm
@@ -297,10 +297,8 @@ sub is_viewable {
# We assume we can view all text and image types.
return 1 if ($contenttype =~ /^(text|image)\//);
- # Mozilla can view XUL. Note the trailing slash on the Gecko detection to
- # avoid sending XUL to Safari.
- return 1 if (($contenttype =~ /^application\/vnd\.mozilla\./)
- && ($cgi->user_agent() =~ /Gecko\//));
+ # Modern browsers support PDF as well.
+ return 1 if ($contenttype eq 'application/pdf');
# If it's not one of the above types, we check the Accept: header for any
# types mentioned explicitly.
diff --git a/Bugzilla/Bloomfilter.pm b/Bugzilla/Bloomfilter.pm
index 0d329b2ea..ba1d6d6c3 100644
--- a/Bugzilla/Bloomfilter.pm
+++ b/Bugzilla/Bloomfilter.pm
@@ -13,7 +13,8 @@ use warnings;
use Bugzilla::Constants;
use Algorithm::BloomFilter;
-use File::Temp qw(tempfile);
+use File::Slurper qw(write_binary read_binary read_lines);
+use File::Spec::Functions qw(catfile);
sub _new_bloom_filter {
my ($n) = @_;
@@ -24,44 +25,37 @@ sub _new_bloom_filter {
}
sub _filename {
- my ($name) = @_;
+ my ($name, $type) = @_;
my $datadir = bz_locations->{datadir};
- return sprintf("%s/%s.bloom", $datadir, $name);
+
+ return catfile($datadir, "$name.$type");
}
sub populate {
- my ($class, $name, $items) = @_;
+ my ($class, $name) = @_;
my $memcached = Bugzilla->memcached;
+ my @items = read_lines(_filename($name, 'list'));
+ my $filter = _new_bloom_filter(@items + 0);
- my $filter = _new_bloom_filter(@$items + 0);
- foreach my $item (@$items) {
- $filter->add($item);
- }
-
- my ($fh, $filename) = tempfile( "${name}XXXXXX", DIR => bz_locations->{datadir}, UNLINK => 0);
- binmode $fh, ':bytes';
- print $fh $filter->serialize;
- close $fh;
- rename($filename, _filename($name)) or die "failed to rename $filename: $!";
+ $filter->add($_) foreach @items;
+ write_binary(_filename($name, 'bloom'), $filter->serialize);
$memcached->clear_bloomfilter({name => $name});
}
sub lookup {
my ($class, $name) = @_;
my $memcached = Bugzilla->memcached;
- my $filename = _filename($name);
+ my $filename = _filename($name, 'bloom');
my $filter_data = $memcached->get_bloomfilter( { name => $name } );
if (!$filter_data && -f $filename) {
- open my $fh, '<:bytes', $filename;
- local $/ = undef;
- $filter_data = <$fh>;
- close $fh;
+ $filter_data = read_binary($filename);
$memcached->set_bloomfilter({ name => $name, filter => $filter_data });
}
- return Algorithm::BloomFilter->deserialize($filter_data);
+ return Algorithm::BloomFilter->deserialize($filter_data) if $filter_data;
+ return undef;
}
1;
diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm
index 915405a0e..ebfc95d51 100644
--- a/Bugzilla/BugMail.pm
+++ b/Bugzilla/BugMail.pm
@@ -277,7 +277,7 @@ sub Send {
# BMO: never send emails to bugs or .tld addresses. this check needs to
# happen after the bugmail_recipients hook.
if ($user->email_enabled && $dep_ok &&
- ($user->login !~ /bugs$/) && ($user->login !~ /\.tld$/))
+ ($user->login !~ /\.(?:bugs|tld)$/))
{
# Don't show summaries for bugs the user can't access, and
# provide a hook for extensions such as SecureMail to filter
diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm
index b932116a2..a69bdd278 100644
--- a/Bugzilla/CGI.pm
+++ b/Bugzilla/CGI.pm
@@ -42,6 +42,11 @@ sub DEFAULT_CSP {
img_src => [ 'self', 'https://secure.gravatar.com', 'https://www.google-analytics.com' ],
style_src => [ 'self', 'unsafe-inline' ],
object_src => [ 'none' ],
+ connect_src => [
+ 'self',
+ # This is from extensions/OrangeFactor/web/js/orange_factor.js
+ 'https://treeherder.mozilla.org/api/failurecount/',
+ ],
form_action => [
'self',
# used in template/en/default/search/search-google.html.tmpl
@@ -69,7 +74,7 @@ sub SHOW_BUG_MODAL_CSP {
connect_src => [
'self',
# This is from extensions/OrangeFactor/web/js/orange_factor.js
- 'https://brasstacks.mozilla.com/orangefactor/api/count',
+ 'https://treeherder.mozilla.org/api/failurecount/',
],
frame_src => [ 'self', ],
worker_src => [ 'none', ],
@@ -590,6 +595,9 @@ sub header {
"skins/standard/fonts/MaterialIcons-Regular.woff2",
);
$headers{'-link'} = join(", ", map { sprintf('</static/v%s/%s>; rel="preload"; as="font"', Bugzilla->VERSION, $_) } @fonts);
+ if (Bugzilla->params->{google_analytics_tracking_id}) {
+ $headers{'-link'} .= ', <https://www.google-analytics.com>; rel="preconnect"; crossorigin';
+ }
}
return $self->SUPER::header(%headers) || "";
@@ -684,6 +692,8 @@ sub send_cookie {
$paramhash{'-secure'} = 1
if lc( $uri->scheme ) eq 'https';
+ $paramhash{'-samesite'} = 'Lax';
+
push(@{$self->{'Bugzilla_cookie_list'}}, $self->cookie(%paramhash));
}
diff --git a/Bugzilla/Config/Advanced.pm b/Bugzilla/Config/Advanced.pm
index 2eec11dbe..398f02701 100644
--- a/Bugzilla/Config/Advanced.pm
+++ b/Bugzilla/Config/Advanced.pm
@@ -36,43 +36,6 @@ use constant get_param_list => (
type => 'b',
default => 0
},
-
- {
- name => 'sentry_uri',
- type => 't',
- default => '',
- },
-
- {
- name => 'metrics_enabled',
- type => 'b',
- default => 0
- },
- {
- name => 'metrics_user_ids',
- type => 't',
- default => '3881,5038,5898,13647,20209,251051,373476,409787'
- },
- {
- name => 'metrics_elasticsearch_server',
- type => 't',
- default => '127.0.0.1:9200'
- },
- {
- name => 'metrics_elasticsearch_index',
- type => 't',
- default => 'bmo-metrics'
- },
- {
- name => 'metrics_elasticsearch_type',
- type => 't',
- default => 'timings'
- },
- {
- name => 'metrics_elasticsearch_ttl',
- type => 't',
- default => '1210000000' # 14 days
- },
);
1;
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index 65b37dced..0b0c857a0 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -695,9 +695,8 @@ sub _bz_locations {
# The script should really generate these graphs directly...
'webdotdir' => "$datadir/webdot",
'extensionsdir' => "$libpath/extensions",
+ 'logsdir' => "$libpath/logs",
'assetsdir' => "$datadir/assets",
- # error_reports store error/warnings destined for sentry
- 'error_reports' => "$libpath/error_reports",
'confdir' => $confdir,
};
}
diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm
index 15acfd0d9..ec0f058b9 100644
--- a/Bugzilla/DB.pm
+++ b/Bugzilla/DB.pm
@@ -8,13 +8,22 @@
package Bugzilla::DB;
use 5.10.1;
-use strict;
-use warnings;
+use Moo;
use DBI;
-
-# Inherit the DB class from DBI::db.
-use base qw(DBI::db);
+use DBIx::Connector;
+our %Connector;
+
+has 'dbh' => (
+ is => 'lazy',
+ handles => [
+ qw[
+ begin_work column_info commit disconnect do errstr get_info last_insert_id ping prepare
+ primary_key quote_identifier rollback selectall_arrayref selectall_hashref
+ selectcol_arrayref selectrow_array selectrow_arrayref selectrow_hashref table_info
+ ]
+ ],
+);
use Bugzilla::Constants;
use Bugzilla::Install::Requirements;
@@ -27,11 +36,14 @@ use Bugzilla::Error;
use Bugzilla::DB::Schema;
use Bugzilla::Version;
-use Bugzilla::Metrics::Mysql;
-
use List::Util qw(max);
use Storable qw(dclone);
+has [qw(dsn user pass attrs)] => (
+ is => 'ro',
+ required => 1,
+);
+
#####################################################################
# Constants
#####################################################################
@@ -89,7 +101,7 @@ use constant INDEX_DROPS_REQUIRE_FK_DROPS => 1;
sub quote {
my $self = shift;
- my $retval = $self->SUPER::quote(@_);
+ my $retval = $self->dbh->quote(@_);
trick_taint($retval) if defined $retval;
return $retval;
}
@@ -138,11 +150,6 @@ sub _connect {
# instantiate the correct DB specific module
- # BMO - enable instrumentation of db calls
- if (Bugzilla->metrics_enabled) {
- $pkg_module = 'Bugzilla::Metrics::Mysql';
- }
-
my $dbh = $pkg_module->new($params);
return $dbh;
@@ -206,7 +213,6 @@ sub bz_check_server_version {
my ($self, $db, $output) = @_;
my $sql_vers = $self->bz_server_version;
- $self->disconnect;
my $sql_want = $db->{db_version};
my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0;
@@ -262,7 +268,6 @@ sub bz_create_database {
}
}
- $dbh->disconnect;
}
# A helper for bz_create_database and bz_check_requirements.
@@ -271,6 +276,7 @@ sub _get_no_db_connection {
my $dbh;
my %connect_params = %{ Bugzilla->localconfig };
$connect_params{db_name} = '';
+ local %Connector = ();
my $conn_success = eval {
$dbh = _connect(\%connect_params);
};
@@ -1247,14 +1253,14 @@ sub bz_rollback_transaction {
# Subclass Helpers
#####################################################################
-sub db_new {
- my ($class, $params) = @_;
+sub _build_dbh {
+ my ($self) = @_;
my ($dsn, $user, $pass, $override_attrs) =
- @$params{qw(dsn user pass attrs)};
+ map { $self->$_ } qw(dsn user pass attrs);
# set up default attributes used to connect to the database
# (may be overridden by DB driver implementations)
- my $attributes = { RaiseError => 0,
+ my $attributes = { RaiseError => 1,
AutoCommit => 1,
PrintError => 0,
ShowErrorStatement => 1,
@@ -1272,20 +1278,16 @@ sub db_new {
$attributes->{$key} = $override_attrs->{$key};
}
}
+ my $class = ref $self;
+ if ($class->can('on_dbi_connected')) {
+ $attributes->{Callbacks} = {
+ connected => sub { $class->on_dbi_connected(@_); return },
+ }
+ }
- # connect using our known info to the specified db
- my $self = DBI->connect($dsn, $user, $pass, $attributes)
- or die "\nCan't connect to the database.\nError: $DBI::errstr\n"
- . " Is your database installed and up and running?\n Do you have"
- . " the correct username and password selected in localconfig?\n\n";
-
- # RaiseError was only set to 0 so that we could catch the
- # above "die" condition.
- $self->{RaiseError} = 1;
-
- bless ($self, $class);
+ my $connector = $Connector{"$user.$dsn"} //= DBIx::Connector->new($dsn, $user, $pass, $attributes);
- return $self;
+ return $connector->dbh;
}
#####################################################################
diff --git a/Bugzilla/DB/Mysql.pm b/Bugzilla/DB/Mysql.pm
index 727fe2316..d0b9724eb 100644
--- a/Bugzilla/DB/Mysql.pm
+++ b/Bugzilla/DB/Mysql.pm
@@ -22,10 +22,9 @@ For interface details see L<Bugzilla::DB> and L<DBI>.
package Bugzilla::DB::Mysql;
use 5.10.1;
-use strict;
-use warnings;
+use Moo;
-use base qw(Bugzilla::DB);
+extends qw(Bugzilla::DB);
use Bugzilla::Constants;
use Bugzilla::Install::Util qw(install_string);
@@ -43,7 +42,7 @@ use constant MAX_COMMENTS => 50;
use constant FULLTEXT_OR => '|';
-sub new {
+sub BUILDARGS {
my ($class, $params) = @_;
my ($user, $pass, $host, $dbname, $port, $sock) =
@$params{qw(db_user db_pass db_host db_name db_port db_sock)};
@@ -55,28 +54,20 @@ sub new {
my %attrs = (
mysql_enable_utf8 => Bugzilla->params->{'utf8'},
- # Needs to be explicitly specified for command-line processes.
- mysql_auto_reconnect => 1,
);
- my $self = $class->db_new({ dsn => $dsn, user => $user,
- pass => $pass, attrs => \%attrs });
+ return { dsn => $dsn, user => $user, pass => $pass, attrs => \%attrs };
+}
+
+sub on_dbi_connected {
+ my ($class, $dbh) = @_;
# This makes sure that if the tables are encoded as UTF-8, we
# return their data correctly.
- $self->do("SET NAMES utf8") if Bugzilla->params->{'utf8'};
-
- # all class local variables stored in DBI derived class needs to have
- # a prefix 'private_'. See DBI documentation.
- $self->{private_bz_tables_locked} = "";
-
- # Needed by TheSchwartz
- $self->{private_bz_dsn} = $dsn;
-
- bless ($self, $class);
+ $dbh->do("SET NAMES utf8") if Bugzilla->params->{'utf8'};
# Bug 321645 - disable MySQL strict mode, if set
- my ($var, $sql_mode) = $self->selectrow_array(
+ my ($var, $sql_mode) = $dbh->selectrow_array(
"SHOW VARIABLES LIKE 'sql\\_mode'");
if ($sql_mode) {
@@ -88,15 +79,13 @@ sub new {
split(/,/, $sql_mode));
if ($sql_mode ne $new_sql_mode) {
- $self->do("SET SESSION sql_mode = ?", undef, $new_sql_mode);
+ $dbh->do("SET SESSION sql_mode = ?", undef, $new_sql_mode);
}
}
# Allow large GROUP_CONCATs (largely for inserting comments
# into bugs_fulltext).
- $self->do('SET SESSION group_concat_max_len = 128000000');
-
- return $self;
+ $dbh->do('SET SESSION group_concat_max_len = 128000000');
}
# when last_insert_id() is supported on MySQL by lowest DBI/DBD version
diff --git a/Bugzilla/DB/Oracle.pm b/Bugzilla/DB/Oracle.pm
index 5c9ea68a3..a519bb796 100644
--- a/Bugzilla/DB/Oracle.pm
+++ b/Bugzilla/DB/Oracle.pm
@@ -22,10 +22,9 @@ For interface details see L<Bugzilla::DB> and L<DBI>.
package Bugzilla::DB::Oracle;
use 5.10.1;
-use strict;
-use warnings;
+use Moo;
-use base qw(Bugzilla::DB);
+extends qw(Bugzilla::DB);
use DBD::Oracle;
use DBD::Oracle qw(:ora_types);
@@ -47,7 +46,7 @@ use constant FULLTEXT_OR => ' OR ';
our $fulltext_label = 0;
-sub new {
+sub BUILDARGS {
my ($class, $params) = @_;
my ($user, $pass, $host, $dbname, $port) =
@$params{qw(db_user db_pass db_host db_name db_port)};
@@ -66,22 +65,20 @@ sub new {
LongReadLen => max(Bugzilla->params->{'maxattachmentsize'} || 0,
MIN_LONG_READ_LEN) * 1024,
};
- my $self = $class->db_new({ dsn => $dsn, user => $user,
- pass => $pass, attrs => $attrs });
- # Needed by TheSchwartz
- $self->{private_bz_dsn} = $dsn;
+ return { dsn => $dsn, user => $user, pass => $pass, attrs => $attrs };
+}
- bless ($self, $class);
+sub on_dbi_connected {
+ my ($class, $dbh) = @_;
# Set the session's default date format to match MySQL
- $self->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'");
- $self->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'");
- $self->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'")
+ $dbh->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'");
+ $dbh->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'");
+ $dbh->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'")
if Bugzilla->params->{'utf8'};
# To allow case insensitive query.
- $self->do("ALTER SESSION SET NLS_COMP='ANSI'");
- $self->do("ALTER SESSION SET NLS_SORT='BINARY_AI'");
- return $self;
+ $dbh->do("ALTER SESSION SET NLS_COMP='ANSI'");
+ $dbh->do("ALTER SESSION SET NLS_SORT='BINARY_AI'");
}
sub bz_last_key {
diff --git a/Bugzilla/DB/Pg.pm b/Bugzilla/DB/Pg.pm
index 8bcbc3262..0db349412 100644
--- a/Bugzilla/DB/Pg.pm
+++ b/Bugzilla/DB/Pg.pm
@@ -22,19 +22,18 @@ For interface details see L<Bugzilla::DB> and L<DBI>.
package Bugzilla::DB::Pg;
use 5.10.1;
-use strict;
-use warnings;
+use Moo;
use Bugzilla::Error;
use Bugzilla::Version;
use DBD::Pg;
# This module extends the DB interface via inheritance
-use base qw(Bugzilla::DB);
+extends qw(Bugzilla::DB);
use constant BLOB_TYPE => { pg_type => DBD::Pg::PG_BYTEA };
-sub new {
+sub BUILDARGS {
my ($class, $params) = @_;
my ($user, $pass, $host, $dbname, $port) =
@$params{qw(db_user db_pass db_host db_name db_port)};
@@ -55,18 +54,7 @@ sub new {
my $attrs = { pg_enable_utf8 => Bugzilla->params->{'utf8'} };
- my $self = $class->db_new({ dsn => $dsn, user => $user,
- pass => $pass, attrs => $attrs });
-
- # all class local variables stored in DBI derived class needs to have
- # a prefix 'private_'. See DBI documentation.
- $self->{private_bz_tables_locked} = "";
- # Needed by TheSchwartz
- $self->{private_bz_dsn} = $dsn;
-
- bless ($self, $class);
-
- return $self;
+ return { dsn => $dsn, user => $user, pass => $pass, attrs => $attrs }
}
# if last_insert_id is supported on PostgreSQL by lowest DBI/DBD version
diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index 3307464db..67ee9071c 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -398,7 +398,8 @@ use constant ABSTRACT_SCHEMA => {
DEFAULT => 'FALSE'},
type => {TYPE => 'INT2', NOTNULL => 1,
DEFAULT => '0'},
- extra_data => {TYPE => 'varchar(255)'}
+ extra_data => {TYPE => 'varchar(255)'},
+ is_markdown => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}
],
INDEXES => [
longdescs_bug_id_idx => ['bug_id'],
@@ -923,6 +924,8 @@ use constant ABSTRACT_SCHEMA => {
cryptpassword => {TYPE => 'varchar(128)'},
realname => {TYPE => 'varchar(255)', NOTNULL => 1,
DEFAULT => "''"},
+ nickname => {TYPE => 'varchar(255)', NOTNULL => 1,
+ DEFAULT => "''"},
disabledtext => {TYPE => 'MEDIUMTEXT', NOTNULL => 1,
DEFAULT => "''"},
disable_mail => {TYPE => 'BOOLEAN', NOTNULL => 1,
@@ -942,7 +945,10 @@ use constant ABSTRACT_SCHEMA => {
profiles_login_name_idx => {FIELDS => ['login_name'],
TYPE => 'UNIQUE'},
profiles_extern_id_idx => {FIELDS => ['extern_id'],
- TYPE => 'UNIQUE'}
+ TYPE => 'UNIQUE'},
+ profiles_nickname_idx => ['nickname'],
+ profiles_realname_ft_idx => {FIELDS => ['realname'],
+ TYPE => 'FULLTEXT'},
],
},
diff --git a/Bugzilla/DB/Sqlite.pm b/Bugzilla/DB/Sqlite.pm
index 87f45415b..3890d0795 100644
--- a/Bugzilla/DB/Sqlite.pm
+++ b/Bugzilla/DB/Sqlite.pm
@@ -8,10 +8,9 @@
package Bugzilla::DB::Sqlite;
use 5.10.1;
-use strict;
-use warnings;
+use Moo;
-use base qw(Bugzilla::DB);
+extends qw(Bugzilla::DB);
use Bugzilla::Constants;
use Bugzilla::Error;
@@ -69,7 +68,7 @@ sub _sqlite_position_ci {
# Constructor #
###############
-sub new {
+sub BUILDARGS {
my ($class, $params) = @_;
my $db_name = $params->{db_name};
@@ -97,10 +96,11 @@ sub new {
sqlite_unicode => Bugzilla->params->{'utf8'},
};
- my $self = $class->db_new({ dsn => $dsn, user => '',
- pass => '', attrs => $attrs });
- # Needed by TheSchwartz
- $self->{private_bz_dsn} = $dsn;
+ return { dsn => $dsn, user => '', pass => '', attrs => $attrs };
+}
+
+sub on_dbi_connected {
+ my ($class, $dbh) = @_;
my %pragmas = (
# Make sure that the sqlite file doesn't grow without bound.
@@ -122,23 +122,20 @@ sub new {
);
while (my ($name, $value) = each %pragmas) {
- $self->do("PRAGMA $name = $value");
+ $dbh->do("PRAGMA $name = $value");
}
- $self->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci);
- $self->sqlite_create_function('position', 2, \&_sqlite_position);
- $self->sqlite_create_function('iposition', 2, \&_sqlite_position_ci);
+ $dbh->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci);
+ $dbh->sqlite_create_function('position', 2, \&_sqlite_position);
+ $dbh->sqlite_create_function('iposition', 2, \&_sqlite_position_ci);
# SQLite has a "substr" function, but other DBs call it "SUBSTRING"
# so that's what we use, and I don't know of any way in SQLite to
# alias the SQL "substr" function to be called "SUBSTRING".
- $self->sqlite_create_function('substring', 3, \&CORE::substr);
- $self->sqlite_create_function('mod', 2, \&_sqlite_mod);
- $self->sqlite_create_function('now', 0, \&_sqlite_now);
- $self->sqlite_create_function('localtimestamp', 1, \&_sqlite_now);
- $self->sqlite_create_function('floor', 1, \&POSIX::floor);
-
- bless ($self, $class);
- return $self;
+ $dbh->sqlite_create_function('substring', 3, \&CORE::substr);
+ $dbh->sqlite_create_function('mod', 2, \&_sqlite_mod);
+ $dbh->sqlite_create_function('now', 0, \&_sqlite_now);
+ $dbh->sqlite_create_function('localtimestamp', 1, \&_sqlite_now);
+ $dbh->sqlite_create_function('floor', 1, \&POSIX::floor);
}
###############
diff --git a/Bugzilla/DaemonControl.pm b/Bugzilla/DaemonControl.pm
index 6586cc01b..2c6df1b87 100644
--- a/Bugzilla/DaemonControl.pm
+++ b/Bugzilla/DaemonControl.pm
@@ -81,7 +81,13 @@ sub run_cereal {
on_exception => on_exception( 'cereal', $exit_f ),
);
$exit_f->on_cancel( sub { $cereal->kill('TERM') } );
+ $exit_f->on_ready(
+ sub {
+ delete $ENV{LOG4PERL_STDERR_DISABLE};
+ }
+ );
$loop->add($cereal);
+ $ENV{LOG4PERL_STDERR_DISABLE} = 1;
return $exit_f;
}
diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm
index d67571848..9fcd16386 100644
--- a/Bugzilla/Error.pm
+++ b/Bugzilla/Error.pm
@@ -13,9 +13,10 @@ use warnings;
use base qw(Exporter);
-@Bugzilla::Error::EXPORT = qw(ThrowCodeError ThrowTemplateError ThrowUserError ThrowErrorPage);
+## no critic (Modules::ProhibitAutomaticExportation)
+our @EXPORT = qw( ThrowCodeError ThrowTemplateError ThrowUserError ThrowErrorPage);
+## use critic
-use Bugzilla::Sentry;
use Bugzilla::Constants;
use Bugzilla::WebService::Constants;
use Bugzilla::Util;
@@ -37,7 +38,7 @@ sub _in_eval {
}
sub _throw_error {
- my ($name, $error, $vars) = @_;
+ my ($name, $error, $vars, $logfunc) = @_;
$vars ||= {};
$vars->{error} = $error;
@@ -47,38 +48,6 @@ sub _throw_error {
my $dbh = eval { Bugzilla->dbh };
$dbh->bz_rollback_transaction() if ($dbh && $dbh->bz_in_transaction() && !_in_eval());
- my $datadir = bz_locations()->{'datadir'};
- # If a writable $datadir/errorlog exists, log error details there.
- if (-w "$datadir/errorlog") {
- require Data::Dumper;
- my $mesg = "";
- for (1..75) { $mesg .= "-"; };
- $mesg .= "\n[$$] " . time2str("%D %H:%M:%S ", time());
- $mesg .= "$name $error ";
- $mesg .= remote_ip();
- $mesg .= Bugzilla->user->login;
- $mesg .= (' actually ' . Bugzilla->sudoer->login) if Bugzilla->sudoer;
- $mesg .= "\n";
- my %params = Bugzilla->cgi->Vars;
- $Data::Dumper::Useqq = 1;
- for my $param (sort keys %params) {
- my $val = $params{$param};
- # obscure passwords
- $val = "*****" if $param =~ /password/i;
- # limit line length
- $val =~ s/^(.{512}).*$/$1\[CHOP\]/;
- $mesg .= "[$$] " . Data::Dumper->Dump([$val],["param($param)"]);
- }
- for my $var (sort keys %ENV) {
- my $val = $ENV{$var};
- $val = "*****" if $val =~ /password|http_pass/i;
- $mesg .= "[$$] " . Data::Dumper->Dump([$val],["env($var)"]);
- }
- open(ERRORLOGFID, ">>", "$datadir/errorlog");
- print ERRORLOGFID "$mesg\n";
- close ERRORLOGFID;
- }
-
my $template = Bugzilla->template;
my $message;
@@ -97,34 +66,24 @@ sub _throw_error {
message => \$message });
if ($Bugzilla::Template::is_processing) {
- $name =~ /^global\/(user|code)-error/;
- my $type = $1 // 'unknown';
+ my ($type) = $name =~ /^global\/(user|code)-error/;
+ $type //= 'unknown';
die Template::Exception->new("bugzilla.$type.$error", $vars);
}
if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) {
- if (sentry_should_notify($vars->{error})) {
- $vars->{maintainers_notified} = 1;
- $vars->{processed} = {};
- } else {
- $vars->{maintainers_notified} = 0;
- }
-
my $cgi = Bugzilla->cgi;
$cgi->close_standby_message('text/html', 'inline', 'error', 'html');
$template->process($name, $vars)
|| ThrowTemplateError($template->error());
print $cgi->multipart_final() if $cgi->{_multipart_in_progress};
-
- if ($vars->{maintainers_notified}) {
- sentry_handle_error($vars->{error}, $vars->{processed}->{error_message});
- }
+ $logfunc->("webpage error: $error");
}
elsif (Bugzilla->error_mode == ERROR_MODE_TEST) {
die Dumper($vars);
}
elsif (Bugzilla->error_mode == ERROR_MODE_DIE) {
- die("$message\n");
+ die "$message\n";
}
elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT
|| Bugzilla->error_mode == ERROR_MODE_JSON_RPC
@@ -141,6 +100,7 @@ sub _throw_error {
}
if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
+ $logfunc->("XMLRPC error: $error ($code)");
die SOAP::Fault->faultcode($code)->faultstring($message);
}
else {
@@ -150,6 +110,11 @@ sub _throw_error {
if (Bugzilla->error_mode == ERROR_MODE_REST) {
my %status_code_map = %{ REST_STATUS_CODE_MAP() };
$status_code = $status_code_map{$code} || $status_code_map{'_default'};
+ $logfunc->("REST error: $error (HTTP $status_code, internal code $code)");
+ }
+ else {
+ my $fake_code = 100000 + $code;
+ $logfunc->("JSONRPC error: $error ($fake_code)");
}
# Technically JSON-RPC isn't allowed to have error numbers
# higher than 999, but we do this to avoid conflicts with
@@ -170,22 +135,44 @@ sub _throw_error {
exit;
}
+
+sub _add_vars_to_logging_fields {
+ my ($vars) = @_;
+
+ foreach my $key (keys %$vars) {
+ Bugzilla::Logging->fields->{"var_$key"} = $vars->{$key};
+ }
+}
+
+sub _make_logfunc {
+ my ($type) = @_;
+ my $logger = Log::Log4perl->get_logger("Bugzilla.Error.$type");
+ return sub {
+ local $Log::Log4perl::caller_depth = $Log::Log4perl::caller_depth + 3;
+ if ($type eq 'User') {
+ $logger->warn(@_);
+ }
+ else {
+ $logger->error(@_);
+ }
+ };
+}
+
+
sub ThrowUserError {
- _throw_error("global/user-error.html.tmpl", @_);
+ my ($error, $vars) = @_;
+ my $logfunc = _make_logfunc('User');
+ _add_vars_to_logging_fields($vars);
+
+ _throw_error( 'global/user-error.html.tmpl', $error, $vars, $logfunc);
}
sub ThrowCodeError {
- my (undef, $vars) = @_;
-
- # Don't show function arguments, in case they contain
- # confidential data.
- local $Carp::MaxArgNums = -1;
- # Don't show the error as coming from Bugzilla::Error, show it
- # as coming from the caller.
- local $Carp::CarpInternal{'Bugzilla::Error'} = 1;
- $vars->{traceback} //= Carp::longmess();
+ my ($error, $vars) = @_;
+ my $logfunc = _make_logfunc('Code');
+ _add_vars_to_logging_fields($vars);
- _throw_error("global/code-error.html.tmpl", @_);
+ _throw_error( 'global/code-error.html.tmpl', $error, $vars, $logfunc );
}
sub ThrowTemplateError {
@@ -211,10 +198,12 @@ sub ThrowTemplateError {
# we never want to display this to the user
exit if $template_err =~ /\bModPerl::Util::exit\b/;
+ state $logger = Log::Log4perl->get_logger('Bugzilla.Error.Template');
+ $logger->error($template_err);
+
$vars->{'template_error_msg'} = $template_err;
$vars->{'error'} = "template_error";
- sentry_handle_error('error', $template_err);
$vars->{'template_error_msg'} =~ s/ at \S+ line \d+\.\s*$//;
my $template = Bugzilla->template;
diff --git a/Bugzilla/Extension.pm b/Bugzilla/Extension.pm
index a41ac9326..8e173c711 100644
--- a/Bugzilla/Extension.pm
+++ b/Bugzilla/Extension.pm
@@ -35,7 +35,11 @@ sub INC_HOOK {
my $first = 1;
untaint($real_file);
$INC{$fake_file} = $real_file;
- open my $fh, '<', $real_file or die "invalid file: $real_file";
+ my $found = open my $fh, '<', $real_file;
+ unless ($found) {
+ require Carp;
+ Carp::croak "Can't locate $fake_file while looking for $real_file in \@INC (\@INC contains: @INC)";
+ }
return sub {
no warnings;
if ( !$first ) {
diff --git a/Bugzilla/Group.pm b/Bugzilla/Group.pm
index 06491f6a0..7f684ea15 100644
--- a/Bugzilla/Group.pm
+++ b/Bugzilla/Group.pm
@@ -74,6 +74,11 @@ use constant UPDATE_COLUMNS => qw(
use constant GROUP_PARAMS => qw(chartgroup insidergroup timetrackinggroup
querysharegroup);
+
+sub DYNAMIC_COLUMNS {
+ return Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
+}
+
###############################
#### Accessors ######
###############################
diff --git a/Bugzilla/Install.pm b/Bugzilla/Install.pm
index 6d47a143f..14fe904eb 100644
--- a/Bugzilla/Install.pm
+++ b/Bugzilla/Install.pm
@@ -215,6 +215,10 @@ use constant SYSTEM_GROUPS => (
description => 'Can edit or disable users'
},
{
+ name => 'disableusers',
+ description => 'Can disable users'
+ },
+ {
name => 'creategroups',
description => 'Can create and destroy groups'
},
diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm
index 0d63f68b9..86edf8a30 100644
--- a/Bugzilla/Install/DB.pm
+++ b/Bugzilla/Install/DB.pm
@@ -710,6 +710,11 @@ sub update_table_definitions {
# 2014-07-27 LpSolit@gmail.com - Bug 1044561
_fix_user_api_keys_indexes();
+
+ # 2018-06-14 dylan@mozilla.com - Bug 1468818
+ $dbh->bz_add_column('longdescs', 'is_markdown',
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
+
# 2014-10-?? dkl@mozilla.com - Bug 1062940
$dbh->bz_alter_column('bugs', 'alias', { TYPE => 'varchar(40)' });
@@ -762,6 +767,13 @@ sub update_table_definitions {
$dbh->bz_add_column('components', 'triage_owner_id',
{TYPE => 'INT3'});
+ $dbh->bz_add_column('profiles', 'nickname',
+ {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"});
+ $dbh->bz_add_index('profiles', 'profiles_nickname_idx', [qw(nickname)]);
+
+ $dbh->bz_add_index('profiles', 'profiles_realname_ft_idx',
+ {TYPE => 'FULLTEXT', FIELDS => ['realname']});
+
################################################################
# New --TABLE-- changes should go *** A B O V E *** this point #
################################################################
diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm
index 493479d5e..d56c7f4c4 100644
--- a/Bugzilla/Install/Filesystem.pm
+++ b/Bugzilla/Install/Filesystem.pm
@@ -36,6 +36,7 @@ use Cwd ();
use File::Slurp;
use IO::File;
use POSIX ();
+use English qw(-no_match_vars $OSNAME);
use base qw(Exporter);
our @EXPORT = qw(
@@ -106,6 +107,7 @@ use constant HTTPD_ENV => qw(
LOCALCONFIG_ENV
BUGZILLA_UNSAFE_AUTH_DELEGATION
LOG4PERL_CONFIG_FILE
+ LOG4PERL_STDERR_DISABLE
USE_NYTPROF
NYTPROF_DIR
);
@@ -206,6 +208,8 @@ sub DIR_CGI_OVERWRITE { _group() ? 0770 : 0777 };
# (or their subdirectories) to the user, via the webserver.
sub DIR_ALSO_WS_SERVE { _suexec() ? 0001 : 0 };
+sub DIR_ALSO_WS_STICKY { $OSNAME eq 'linux' ? 02000 : 0 }
+
# This looks like a constant because it effectively is, but
# it has to call other subroutines and read the current filesystem,
# so it's defined as a sub. This is not exported, so it doesn't have
@@ -230,7 +234,7 @@ sub FILESYSTEM {
my $template_cache = bz_locations()->{'template_cache'};
my $graphsdir = bz_locations()->{'graphsdir'};
my $assetsdir = bz_locations()->{'assetsdir'};
- my $error_reports = bz_locations()->{'error_reports'};
+ my $logsdir = bz_locations()->{'logsdir'};
# We want to set the permissions the same for all localconfig files
# across all PROJECTs, so we do something special with $localconfig,
@@ -265,8 +269,6 @@ sub FILESYSTEM {
'runtests.pl' => { perms => OWNER_EXECUTE },
'jobqueue.pl' => { perms => OWNER_EXECUTE },
'migrate.pl' => { perms => OWNER_EXECUTE },
- 'sentry.pl' => { perms => WS_EXECUTE },
- 'metrics.pl' => { perms => WS_EXECUTE },
'Makefile.PL' => { perms => OWNER_EXECUTE },
'gen-cpanfile.pl' => { perms => OWNER_EXECUTE },
'jobqueue-worker.pl' => { perms => OWNER_EXECUTE },
@@ -315,8 +317,6 @@ sub FILESYSTEM {
# Writeable directories
$template_cache => { files => CGI_READ,
dirs => DIR_CGI_OVERWRITE },
- $error_reports => { files => CGI_READ,
- dirs => DIR_CGI_WRITE },
$attachdir => { files => CGI_WRITE,
dirs => DIR_CGI_WRITE },
$webdotdir => { files => WS_SERVE,
@@ -325,6 +325,8 @@ sub FILESYSTEM {
dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE },
"$datadir/db" => { files => CGI_WRITE,
dirs => DIR_CGI_WRITE },
+ $logsdir => { files => CGI_WRITE,
+ dirs => DIR_CGI_WRITE | DIR_ALSO_WS_STICKY },
$assetsdir => { files => WS_SERVE,
dirs => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE },
@@ -409,7 +411,7 @@ sub FILESYSTEM {
$webdotdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE,
$assetsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE,
$template_cache => DIR_CGI_WRITE,
- $error_reports => DIR_CGI_WRITE,
+ $logsdir => DIR_CGI_WRITE | DIR_ALSO_WS_STICKY,
# Directories that contain content served directly by the web server.
"$skinsdir/custom" => DIR_WS_SERVE,
"$skinsdir/contrib" => DIR_WS_SERVE,
@@ -544,8 +546,6 @@ sub FILESYSTEM {
contents => HT_DEFAULT_DENY },
"$datadir/.htaccess" => { perms => WS_SERVE,
contents => HT_DEFAULT_DENY },
- "$error_reports/.htaccess" => { perms => WS_SERVE,
- contents => HT_DEFAULT_DENY },
"$graphsdir/.htaccess" => { perms => WS_SERVE,
contents => HT_GRAPHS_DIR },
"$webdotdir/.htaccess" => { perms => WS_SERVE,
diff --git a/Bugzilla/Install/Localconfig.pm b/Bugzilla/Install/Localconfig.pm
index 85092f23a..39063ee63 100644
--- a/Bugzilla/Install/Localconfig.pm
+++ b/Bugzilla/Install/Localconfig.pm
@@ -126,6 +126,10 @@ use constant LOCALCONFIG_VARS => (
default => sub { dirname( bin_loc('diff') ) },
},
{
+ name => 'tct_bin',
+ default => sub { bin_loc('tct') },
+ },
+ {
name => 'site_wide_secret',
# 64 characters is roughly the equivalent of a 384-bit key, which
@@ -211,6 +215,7 @@ sub _read_localconfig_from_env {
else {
my $default = $var->{default};
$localconfig{$name} = ref($default) eq 'CODE' ? $default->() : $default;
+ untaint($localconfig{$name});
}
}
diff --git a/Bugzilla/JobQueue.pm b/Bugzilla/JobQueue.pm
index afb36673f..a78a4d0ae 100644
--- a/Bugzilla/JobQueue.pm
+++ b/Bugzilla/JobQueue.pm
@@ -59,7 +59,7 @@ sub new {
# to write to it.
my $self = $class->SUPER::new(
databases => [{
- dsn => Bugzilla->dbh_main->{private_bz_dsn},
+ dsn => Bugzilla->dbh_main->dsn,
user => $lc->{db_user},
pass => $lc->{db_pass},
prefix => 'ts_',
diff --git a/Bugzilla/Logging.pm b/Bugzilla/Logging.pm
index 769485c86..f334435fc 100644
--- a/Bugzilla/Logging.pm
+++ b/Bugzilla/Logging.pm
@@ -10,21 +10,30 @@ use 5.10.1;
use strict;
use warnings;
-use Log::Log4perl;
+use Log::Log4perl qw(:easy);
use Log::Log4perl::MDC;
-use File::Spec::Functions qw(rel2abs);
+use File::Spec::Functions qw(rel2abs catfile);
use Bugzilla::Constants qw(bz_locations);
use English qw(-no_match_vars $PROGRAM_NAME);
+use Taint::Util qw(untaint);
-sub is_interactive {
- return not exists $ENV{SERVER_SOFTWARE}
+sub logfile {
+ my ($class, $name) = @_;
+
+ my $file = rel2abs(catfile(bz_locations->{logsdir}, $name));
+ untaint($file);
+ return $file;
+}
+
+sub fields {
+ return Log::Log4perl::MDC->get_context->{fields} //= {};
}
BEGIN {
my $file = $ENV{LOG4PERL_CONFIG_FILE} // 'log4perl-syslog.conf';
Log::Log4perl::Logger::create_custom_level('NOTICE', 'WARN', 5, 2);
Log::Log4perl->init(rel2abs($file, bz_locations->{confdir}));
- Log::Log4perl->get_logger(__PACKAGE__)->trace("logging enabled in $PROGRAM_NAME");
+ TRACE("logging enabled in $PROGRAM_NAME");
}
# this is copied from Log::Log4perl's :easy handling,
diff --git a/Bugzilla/Mailer.pm b/Bugzilla/Mailer.pm
index 1dec3d4ff..c9a458b47 100644
--- a/Bugzilla/Mailer.pm
+++ b/Bugzilla/Mailer.pm
@@ -12,8 +12,9 @@ use strict;
use warnings;
use base qw(Exporter);
-@Bugzilla::Mailer::EXPORT = qw(MessageToMTA build_thread_marker);
+our @EXPORT = qw(MessageToMTA build_thread_marker); ## no critic (Modules::ProhibitAutomaticExportation)
+use Bugzilla::Logging;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Hook;
@@ -25,6 +26,8 @@ use Encode qw(encode);
use Encode::MIME::Header;
use Email::Address;
use Email::MIME;
+use Try::Tiny;
+
# Return::Value 1.666002 pollutes the error log with warnings about this
# deprecated module. We have to set NO_CLUCK = 1 before loading Email::Send
# to disable these warnings.
@@ -62,7 +65,7 @@ sub MessageToMTA {
$msg =~ s/(?:\015+)?\012/\015\012/msg;
}
- $email = new Email::MIME($msg);
+ $email = Email::MIME->new($msg);
}
# Ensure that we are not sending emails too quickly to recipients.
@@ -182,6 +185,25 @@ sub MessageToMTA {
Bugzilla::Hook::process('mailer_before_send',
{ email => $email, mailer_args => \@args });
+ try {
+ my $to = $email->header('to') or die qq{Unable to find "To:" address\n};
+ my @recipients = Email::Address->parse($to);
+ die qq{Unable to parse "To:" address - $to\n} unless @recipients;
+ die qq{Did not expect more than one "To:" address in $to\n} if @recipients > 1;
+ my $recipient = $recipients[0];
+ my $badhosts = Bugzilla::Bloomfilter->lookup("badhosts");
+ if ($badhosts && $badhosts->test($recipient->host)) {
+ WARN("Attempted to send email to address in badhosts: $to");
+ $email->header_set(to => '');
+ }
+ elsif ($recipient->host =~ /\.(?:bugs|tld)$/) {
+ WARN("Attempted to send email to fake address: $to");
+ $email->header_set(to => '');
+ }
+ } catch {
+ ERROR($_);
+ };
+
# Allow for extensions to to drop the bugmail by clearing the 'to' header
return if $email->header('to') eq '';
diff --git a/Bugzilla/Markdown/GFM.pm b/Bugzilla/Markdown/GFM.pm
new file mode 100644
index 000000000..f3f24fc6a
--- /dev/null
+++ b/Bugzilla/Markdown/GFM.pm
@@ -0,0 +1,92 @@
+package Bugzilla::Markdown::GFM;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Alien::libcmark_gfm;
+use FFI::Platypus;
+use FFI::Platypus::Buffer qw( scalar_to_buffer buffer_to_scalar );
+use Exporter qw(import);
+
+use Bugzilla::Markdown::GFM::SyntaxExtension;
+use Bugzilla::Markdown::GFM::SyntaxExtensionList;
+use Bugzilla::Markdown::GFM::Parser;
+use Bugzilla::Markdown::GFM::Node;
+
+our @EXPORT_OK = qw(cmark_markdown_to_html);
+
+my %OPTIONS = (
+ default => 0,
+ sourcepos => ( 1 << 1 ),
+ hardbreaks => ( 1 << 2 ),
+ safe => ( 1 << 3 ),
+ nobreaks => ( 1 << 4 ),
+ normalize => ( 1 << 8 ),
+ validate_utf8 => ( 1 << 9 ),
+ smart => ( 1 << 10 ),
+ github_pre_lang => ( 1 << 11 ),
+ liberal_html_tag => ( 1 << 12 ),
+ footnotes => ( 1 << 13 ),
+ strikethrough_double_tilde => ( 1 << 14 ),
+ table_prefer_style_attributes => ( 1 << 15 ),
+);
+
+my $FFI = FFI::Platypus->new(
+ lib => [grep { not -l $_ } Alien::libcmark_gfm->dynamic_libs],
+);
+
+$FFI->custom_type(
+ markdown_options_t => {
+ native_type => 'int',
+ native_to_perl => sub {
+ my ($options) = @_;
+ my $result = {};
+ foreach my $key (keys %OPTIONS) {
+ $result->{$key} = ($options & $OPTIONS{$key}) != 0;
+ }
+ return $result;
+ },
+ perl_to_native => sub {
+ my ($options) = @_;
+ my $result = 0;
+ foreach my $key (keys %OPTIONS) {
+ if ($options->{$key}) {
+ $result |= $OPTIONS{$key};
+ }
+ }
+ return $result;
+ }
+ }
+);
+
+$FFI->attach(cmark_markdown_to_html => ['opaque', 'int', 'markdown_options_t'] => 'string',
+ sub {
+ my $c_func = shift;
+ my($markdown, $markdown_length) = scalar_to_buffer $_[0];
+ return $c_func->($markdown, $markdown_length, $_[1]);
+ }
+);
+
+# This has to happen after something from the main lib is loaded
+$FFI->attach('core_extensions_ensure_registered' => [] => 'void');
+
+core_extensions_ensure_registered();
+
+Bugzilla::Markdown::GFM::SyntaxExtension->SETUP($FFI);
+Bugzilla::Markdown::GFM::SyntaxExtensionList->SETUP($FFI);
+Bugzilla::Markdown::GFM::Node->SETUP($FFI);
+Bugzilla::Markdown::GFM::Parser->SETUP($FFI);
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Markdown::GFM - Sets up the FFI to libcmark_gfm.
+
+=head1 DESCRIPTION
+
+This modules mainly just does setup work. See L<Bugzilla::Markdown::GFM::Parser>
+to actually render markdown to html.
diff --git a/Bugzilla/Markdown/GFM/Node.pm b/Bugzilla/Markdown/GFM/Node.pm
new file mode 100644
index 000000000..da5af1a68
--- /dev/null
+++ b/Bugzilla/Markdown/GFM/Node.pm
@@ -0,0 +1,33 @@
+package Bugzilla::Markdown::GFM::Node;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+sub SETUP {
+ my ($class, $FFI) = @_;
+
+ $FFI->custom_type(
+ markdown_node_t => {
+ native_type => 'opaque',
+ native_to_perl => sub {
+ bless \$_[0], $class if $_[0];
+ },
+ perl_to_native => sub { ${ $_[0] } },
+ }
+ );
+
+ $FFI->attach(
+ [ cmark_node_free => 'DESTROY' ],
+ [ 'markdown_node_t' ] => 'void'
+ );
+
+ $FFI->attach(
+ [ cmark_render_html => 'render_html' ],
+ [ 'markdown_node_t', 'markdown_options_t', 'markdown_syntax_extension_list_t'] => 'string',
+ );
+}
+
+1;
+
+__END__
diff --git a/Bugzilla/Markdown/GFM/Parser.pm b/Bugzilla/Markdown/GFM/Parser.pm
new file mode 100644
index 000000000..5307b49c1
--- /dev/null
+++ b/Bugzilla/Markdown/GFM/Parser.pm
@@ -0,0 +1,109 @@
+package Bugzilla::Markdown::GFM::Parser;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FFI::Platypus::Buffer qw( scalar_to_buffer buffer_to_scalar );
+
+sub new {
+ my ($class, $options) = @_;
+ my $extensions = delete $options->{extensions} // [];
+ my $parser = $class->_new($options);
+ $parser->{_options} = $options;
+
+ eval {
+ foreach my $name (@$extensions) {
+ my $extension = Bugzilla::Markdown::GFM::SyntaxExtension->find($name)
+ or die "unknown extension: $name";
+ $parser->attach_syntax_extension($extension);
+ }
+ };
+
+ return $parser;
+}
+
+sub render_html {
+ my ($self, $markdown) = @_;
+ $self->feed($markdown);
+ my $node = $self->finish;
+ return $node->render_html($self->{_options}, $self->get_syntax_extensions);
+}
+
+sub SETUP {
+ my ($class, $FFI) = @_;
+
+ $FFI->custom_type(
+ markdown_parser_t => {
+ native_type => 'opaque',
+ native_to_perl => sub {
+ bless { _pointer => $_[0] }, $class;
+ },
+ perl_to_native => sub { $_[0]->{_pointer} },
+ }
+ );
+
+ $FFI->attach(
+ [ cmark_parser_new => '_new' ],
+ [ 'markdown_options_t' ] => 'markdown_parser_t',
+ sub {
+ my $c_func = shift;
+ return $c_func->($_[1]);
+ }
+ );
+
+ $FFI->attach(
+ [ cmark_parser_free => 'DESTROY' ],
+ [ 'markdown_parser_t' ] => 'void'
+ );
+
+ $FFI->attach(
+ [ cmark_parser_feed => 'feed'],
+ ['markdown_parser_t', 'opaque', 'int'] => 'void',
+ sub {
+ my $c_func = shift;
+ $c_func->($_[0], scalar_to_buffer $_[1]);
+ }
+ );
+
+ $FFI->attach(
+ [ cmark_parser_finish => 'finish' ],
+ [ 'markdown_parser_t' ] => 'markdown_node_t',
+ );
+
+ $FFI->attach(
+ [ cmark_parser_attach_syntax_extension => 'attach_syntax_extension' ],
+ [ 'markdown_parser_t', 'markdown_syntax_extension_t' ] => 'void',
+ );
+
+ $FFI->attach(
+ [ cmark_parser_get_syntax_extensions => 'get_syntax_extensions' ],
+ [ 'markdown_parser_t' ] => 'markdown_syntax_extension_list_t',
+ );
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Markdown::GFM::Parser - Transforms markdown into HTML via libcmark_gfm.
+
+=head1 SYNOPSIS
+
+ use Bugzilla::Markdown::GFM;
+ use Bugzilla::Markdown::GFM::Parser;
+
+ my $parser = Bugzilla::Markdown::GFM::Parser->new({
+ extensions => [qw( autolink tagfilter table strikethrough )]
+ });
+
+ say $parser->render_html(<<'MARKDOWN');
+ # My header
+
+ This is **markdown**!
+
+ - list item 1
+ - list item 2
+ MARKDOWN
diff --git a/Bugzilla/Markdown/GFM/SyntaxExtension.pm b/Bugzilla/Markdown/GFM/SyntaxExtension.pm
new file mode 100644
index 000000000..56efa177a
--- /dev/null
+++ b/Bugzilla/Markdown/GFM/SyntaxExtension.pm
@@ -0,0 +1,31 @@
+package Bugzilla::Markdown::GFM::SyntaxExtension;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+sub SETUP {
+ my ($class, $FFI) = @_;
+
+ $FFI->custom_type(
+ markdown_syntax_extension_t => {
+ native_type => 'opaque',
+ native_to_perl => sub {
+ bless \$_[0], $class if $_[0];
+ },
+ perl_to_native => sub { $_[0] ? ${ $_[0] } : 0 },
+ }
+ );
+ $FFI->attach(
+ [ cmark_find_syntax_extension => 'find' ],
+ [ 'string' ] => 'markdown_syntax_extension_t',
+ sub {
+ my $c_func = shift;
+ return $c_func->($_[1]);
+ }
+ );
+}
+
+1;
+
+__END__
diff --git a/Bugzilla/Markdown/GFM/SyntaxExtensionList.pm b/Bugzilla/Markdown/GFM/SyntaxExtensionList.pm
new file mode 100644
index 000000000..06a9798c2
--- /dev/null
+++ b/Bugzilla/Markdown/GFM/SyntaxExtensionList.pm
@@ -0,0 +1,23 @@
+package Bugzilla::Markdown::GFM::SyntaxExtensionList;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+sub SETUP {
+ my ($class, $FFI) = @_;
+
+ $FFI->custom_type(
+ markdown_syntax_extension_list_t => {
+ native_type => 'opaque',
+ native_to_perl => sub {
+ bless \$_[0], $class if $_[0];
+ },
+ perl_to_native => sub { $_[0] ? ${ $_[0] } : 0 },
+ }
+ );
+}
+
+1;
+
+__END__
diff --git a/Bugzilla/Memcached.pm b/Bugzilla/Memcached.pm
index d34aaa595..40755aa29 100644
--- a/Bugzilla/Memcached.pm
+++ b/Bugzilla/Memcached.pm
@@ -227,11 +227,12 @@ sub should_rate_limit {
my $prefix = RATE_LIMIT_PREFIX . $name . ':';
my $memcached = $self->{memcached};
+ return 0 unless $name;
return 0 unless $memcached;
- $tries //= 3;
+ $tries //= 4;
- for (0 .. $tries) {
+ for my $try (1 .. $tries) {
my $now = time;
my ($key, @keys) = map { $prefix . ( $now - $_ ) } 0 .. $rate_seconds;
$memcached->add($key, 0, $rate_seconds+1);
@@ -240,8 +241,9 @@ sub should_rate_limit {
$tokens->{$key} = $cas->[1]++;
return 1 if sum(values %$tokens) >= $rate_max;
return 0 if $memcached->cas($key, @$cas, $rate_seconds+1);
+ WARN("retry for $prefix (try $try of $tries)");
}
- return 1;
+ return 0;
}
sub clear_all {
diff --git a/Bugzilla/Metrics/Collector.pm b/Bugzilla/Metrics/Collector.pm
deleted file mode 100644
index 53cbac819..000000000
--- a/Bugzilla/Metrics/Collector.pm
+++ /dev/null
@@ -1,171 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-package Bugzilla::Metrics::Collector;
-
-use strict;
-use warnings;
-use 5.10.1;
-
-# the reporter needs to be a constant and use'd here to ensure it's loaded at
-# compile time.
-use constant REPORTER => 'Bugzilla::Metrics::Reporter::ElasticSearch';
-use Bugzilla::Metrics::Reporter::ElasticSearch;
-
-# Debugging reporter
-#use constant REPORTER => 'Bugzilla::Metrics::Reporter::STDERR';
-#use Bugzilla::Metrics::Reporter::STDERR;
-
-use Bugzilla::Constants;
-use Cwd qw(abs_path);
-use File::Basename;
-use Time::HiRes qw(gettimeofday clock_gettime CLOCK_MONOTONIC);
-
-sub new {
- my ($class, $name) = @_;
- my $self = {
- root => undef,
- head => undef,
- time => scalar(gettimeofday()),
- };
- bless($self, $class);
- $self->_start_timer({ type => 'main', name => $name });
- return $self;
-}
-
-sub end {
- my ($self, $timer) = @_;
- my $is_head = $timer ? 0 : 1;
- $timer ||= $self->{head};
- $timer->{duration} += clock_gettime(CLOCK_MONOTONIC) - $timer->{start_time};
- $self->{head} = $self->{head}->{parent} if $is_head;
-}
-
-sub cancel {
- my ($self) = @_;
- delete $self->{head};
-}
-
-sub DESTROY {
- my ($self) = @_;
- $self->finish() if $self->{head};
-}
-
-sub finish {
- my ($self) = @_;
- $self->end($self->{root});
- delete $self->{head};
-
- my $user = Bugzilla->user;
- if ($ENV{MOD_PERL}) {
- require Apache2::RequestUtil;
- my $request = eval { Apache2::RequestUtil->request };
- my $headers = $request ? $request->headers_in() : {};
- $self->{env} = {
- referer => $headers->{Referer},
- request_method => $request->method,
- request_uri => basename($request->unparsed_uri),
- script_name => $request->uri,
- user_agent => $headers->{'User-Agent'},
- };
- }
- else {
- $self->{env} = {
- referer => $ENV{HTTP_REFERER},
- request_method => $ENV{REQUEST_METHOD},
- request_uri => $ENV{REQUEST_URI},
- script_name => basename($ENV{SCRIPT_NAME}),
- user_agent => $ENV{HTTP_USER_AGENT},
- };
- }
- $self->{env}->{name} = $self->{root}->{name};
- $self->{env}->{time} = $self->{time};
- $self->{env}->{user_id} = $user->id;
- $self->{env}->{login} = $user->login if $user->id;
-
- # remove passwords from request_uri
- $self->{env}->{request_uri} =~ s/\b((?:bugzilla_)?password=)(?:[^&]+|.+$)/$1x/gi;
-
- $self->report();
-}
-
-sub name {
- my ($self, $value) = @_;
- $self->{root}->{name} = $value if defined $value;
- return $self->{root}->{name};
-}
-
-sub db_start {
- my ($self) = @_;
- my $timer = $self->_start_timer({ type => 'db' });
-
- my @stack;
- my $i = 0;
- state $path = quotemeta(abs_path(bz_locations()->{cgi_path}) . '/');
- while (1) {
- my @caller = caller($i);
- last unless @caller;
- my $file = $caller[1];
- $file =~ s/^$path//o;
- push @stack, "$file:$caller[2]"
- unless substr($file, 0, 16) eq 'Bugzilla/Metrics';
- last if $file =~ /\.(?:tmpl|cgi)$/;
- $i++;
- }
- $timer->{stack} = \@stack;
-
- return $timer;
-}
-
-sub template_start {
- my ($self, $file) = @_;
- $self->_start_timer({ type => 'tmpl', file => $file });
-}
-
-sub memcached_start {
- my ($self, $key) = @_;
- $self->_start_timer({ type => 'memcached', key => $key });
-}
-
-sub memcached_end {
- my ($self, $hit) = @_;
- $self->{head}->{result} = $hit ? 'hit' : 'miss';
- $self->end();
-}
-
-sub resume {
- my ($self, $timer) = @_;
- $timer->{start_time} = clock_gettime(CLOCK_MONOTONIC);
- return $timer;
-}
-
-sub _start_timer {
- my ($self, $timer) = @_;
- $timer->{start_time} = $timer->{first_time} = clock_gettime(CLOCK_MONOTONIC);
- $timer->{duration} = 0;
- $timer->{children} = [];
-
- if ($self->{head}) {
- $timer->{parent} = $self->{head};
- push @{ $self->{head}->{children} }, $timer;
- }
- else {
- $timer->{parent} = undef;
- $self->{root} = $timer;
- }
- $self->{head} = $timer;
-
- return $timer;
-}
-
-sub report {
- my ($self) = @_;
- my $class = REPORTER;
- $class->DETACH ? $class->background($self) : $class->foreground($self);
-}
-
-1;
diff --git a/Bugzilla/Metrics/Memcached.pm b/Bugzilla/Metrics/Memcached.pm
deleted file mode 100644
index b640f9280..000000000
--- a/Bugzilla/Metrics/Memcached.pm
+++ /dev/null
@@ -1,24 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-package Bugzilla::Metrics::Memcached;
-
-use 5.10.1;
-use strict;
-use warnings;
-
-use base 'Bugzilla::Memcached';
-
-sub _get {
- my $self = shift;
- Bugzilla->metrics->memcached_start($_[0]);
- my $result = $self->SUPER::_get(@_);
- Bugzilla->metrics->memcached_end($result);
- return $result;
-}
-
-1;
diff --git a/Bugzilla/Metrics/Mysql.pm b/Bugzilla/Metrics/Mysql.pm
deleted file mode 100644
index 60dff78c8..000000000
--- a/Bugzilla/Metrics/Mysql.pm
+++ /dev/null
@@ -1,148 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-package Bugzilla::Metrics::Mysql;
-
-use 5.10.1;
-use strict;
-use warnings;
-
-use base 'Bugzilla::DB::Mysql';
-
-sub do {
- my ($self, @args) = @_;
- Bugzilla->metrics->db_start($args[0]);
- my $result = $self->SUPER::do(@args);
- Bugzilla->metrics->end();
- return $result;
-}
-
-sub selectall_arrayref {
- my ($self, @args) = @_;
- Bugzilla->metrics->db_start($args[0]);
- my $result = $self->SUPER::selectall_arrayref(@args);
- Bugzilla->metrics->end();
- return $result;
-}
-
-sub selectall_hashref {
- my ($self, @args) = @_;
- Bugzilla->metrics->db_start($args[0]);
- my $result = $self->SUPER::selectall_hashref(@args);
- Bugzilla->metrics->end();
- return $result;
-}
-
-sub selectcol_arrayref {
- my ($self, @args) = @_;
- Bugzilla->metrics->db_start($args[0]);
- my $result = $self->SUPER::selectcol_arrayref(@args);
- Bugzilla->metrics->end();
- return $result;
-}
-
-sub selectrow_array {
- my ($self, @args) = @_;
- Bugzilla->metrics->db_start($args[0]);
- my @result = $self->SUPER::selectrow_array(@args);
- Bugzilla->metrics->end();
- return wantarray ? @result : $result[0];
-}
-
-sub selectrow_arrayref {
- my ($self, @args) = @_;
- Bugzilla->metrics->db_start($args[0]);
- my $result = $self->SUPER::selectrow_arrayref(@args);
- Bugzilla->metrics->end();
- return $result;
-}
-
-sub selectrow_hashref {
- my ($self, @args) = @_;
- Bugzilla->metrics->db_start($args[0]);
- my $result = $self->SUPER::selectrow_hashref(@args);
- Bugzilla->metrics->end();
- return $result;
-}
-
-sub commit {
- my ($self, @args) = @_;
- Bugzilla->metrics->db_start('COMMIT');
- my $result = $self->SUPER::commit(@args);
- Bugzilla->metrics->end();
- return $result;
-}
-
-sub prepare {
- my ($self, @args) = @_;
- my $sth = $self->SUPER::prepare(@args);
- bless($sth, 'Bugzilla::Metrics::st');
- return $sth;
-}
-
-package Bugzilla::Metrics::st;
-
-use 5.10.1;
-use strict;
-use warnings;
-
-use base 'DBI::st';
-
-sub execute {
- my ($self, @args) = @_;
- $self->{private_timer} = Bugzilla->metrics->db_start();
- my $result = $self->SUPER::execute(@args);
- Bugzilla->metrics->end();
- return $result;
-}
-
-sub fetchrow_array {
- my ($self, @args) = @_;
- my $timer = $self->{private_timer};
- Bugzilla->metrics->resume($timer);
- my @result = $self->SUPER::fetchrow_array(@args);
- Bugzilla->metrics->end($timer);
- return wantarray ? @result : $result[0];
-}
-
-sub fetchrow_arrayref {
- my ($self, @args) = @_;
- my $timer = $self->{private_timer};
- Bugzilla->metrics->resume($timer);
- my $result = $self->SUPER::fetchrow_arrayref(@args);
- Bugzilla->metrics->end($timer);
- return $result;
-}
-
-sub fetchrow_hashref {
- my ($self, @args) = @_;
- my $timer = $self->{private_timer};
- Bugzilla->metrics->resume($timer);
- my $result = $self->SUPER::fetchrow_hashref(@args);
- Bugzilla->metrics->end($timer);
- return $result;
-}
-
-sub fetchall_arrayref {
- my ($self, @args) = @_;
- my $timer = $self->{private_timer};
- Bugzilla->metrics->resume($timer);
- my $result = $self->SUPER::fetchall_arrayref(@args);
- Bugzilla->metrics->end($timer);
- return $result;
-}
-
-sub fetchall_hashref {
- my ($self, @args) = @_;
- my $timer = $self->{private_timer};
- Bugzilla->metrics->resume($timer);
- my $result = $self->SUPER::fetchall_hashref(@args);
- Bugzilla->metrics->end($timer);
- return $result;
-}
-
-1;
diff --git a/Bugzilla/Metrics/Reporter.pm b/Bugzilla/Metrics/Reporter.pm
deleted file mode 100644
index 8216ea923..000000000
--- a/Bugzilla/Metrics/Reporter.pm
+++ /dev/null
@@ -1,103 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-package Bugzilla::Metrics::Reporter;
-
-use 5.10.1;
-use strict;
-use warnings;
-
-use Bugzilla::Constants;
-use File::Slurp;
-use File::Temp;
-use JSON;
-
-# most reporters should detach from the httpd process.
-# reporters which do not detach will block completion of the http response.
-use constant DETACH => 1;
-
-# class method to start the delivery script in the background
-sub background {
- my ($class, $collector) = @_;
-
- # we need to remove parent links to avoid looped structures, which
- # encode_json chokes on
- _walk_timers($collector->{root}, sub { delete $_[0]->{parent} });
-
- # serialisation
- my $json = encode_json({ env => $collector->{env}, times => $collector->{root} });
-
- # write to temp filename
- my $fh = File::Temp->new( UNLINK => 0 );
- if (!$fh) {
- warn "Failed to create temp file: $!\n";
- return;
- }
- binmode($fh, ':utf8');
- print $fh $json;
- close($fh) or die "$fh : $!";
- my $filename = $fh->filename;
-
- # spawn delivery worker
- my $command = bz_locations()->{'cgi_path'} . "/metrics.pl '$class' '$filename' &";
- $ENV{PATH} = '';
- system($command);
-}
-
-# run the reporter immediately
-sub foreground {
- my ($class, $collector) = @_;
- my $reporter = $class->new({ hashref => { env => $collector->{env}, times => $collector->{root} } });
- $reporter->report();
-}
-
-sub new {
- my ($invocant, $args) = @_;
- my $class = ref($invocant) || $invocant;
-
- # load from either a json_filename or hashref
- my $self;
- if ($args->{json_filename}) {
- $self = decode_json(read_file($args->{json_filename}, binmode => ':utf8'));
- unlink($args->{json_filename});
- }
- else {
- $self = $args->{hashref};
- }
- bless($self, $class);
-
- # remove redundant data
- $self->walk_timers(sub {
- my ($timer) = @_;
- $timer->{start_time} = delete $timer->{first_time};
- delete $timer->{children}
- if exists $timer->{children} && !scalar(@{ $timer->{children} });
- });
-
- return $self;
-}
-
-sub walk_timers {
- my ($self, $callback) = @_;
- _walk_timers($self->{times}, $callback, undef);
-}
-
-sub _walk_timers {
- my ($timer, $callback, $parent) = @_;
- $callback->($timer, $parent);
- if (exists $timer->{children}) {
- foreach my $child (@{ $timer->{children} }) {
- _walk_timers($child, $callback, $timer);
- }
- }
-}
-
-sub report {
- die "abstract method call";
-}
-
-1;
diff --git a/Bugzilla/Metrics/Reporter/ElasticSearch.pm b/Bugzilla/Metrics/Reporter/ElasticSearch.pm
deleted file mode 100644
index c61a1d750..000000000
--- a/Bugzilla/Metrics/Reporter/ElasticSearch.pm
+++ /dev/null
@@ -1,105 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-package Bugzilla::Metrics::Reporter::ElasticSearch;
-
-use 5.10.1;
-use strict;
-use warnings;
-
-use base 'Bugzilla::Metrics::Reporter';
-
-use constant DETACH => 1;
-
-sub report {
- my ($self) = @_;
-
- # build path array and flatten
- my @timers;
- $self->walk_timers(sub {
- my ($timer, $parent) = @_;
- $timer->{id} = scalar(@timers);
- if ($parent) {
- if (exists $timer->{children}) {
- if ($timer->{type} eq 'tmpl') {
- $timer->{node} = 'tmpl: ' . $timer->{file};
- }
- elsif ($timer->{type} eq 'db') {
- $timer->{node} = 'db';
- }
- else {
- $timer->{node} = '?';
- }
- }
- $timer->{path} = [ @{ $parent->{path} }, $parent->{node} ];
- $timer->{parent} = $parent->{id};
- }
- else {
- $timer->{path} = [ ];
- $timer->{node} = $timer->{name};
- }
- push @timers, $timer;
- });
-
- # calculate timer-only durations
- $self->walk_timers(sub {
- my ($timer) = @_;
- my $child_duration = 0;
- if (exists $timer->{children}) {
- foreach my $child (@{ $timer->{children} }) {
- $child_duration += $child->{duration};
- }
- }
- $timer->{this_duration} = $timer->{duration} - $child_duration;
- });
-
- # massage each timer
- my $start_time = $self->{times}->{start_time};
- foreach my $timer (@timers) {
- # remove node name and children
- delete $timer->{node};
- delete $timer->{children};
-
- # show relative times
- $timer->{start_time} = $timer->{start_time} - $start_time;
- delete $timer->{end_time};
-
- # show times in ms instead of fractional seconds
- foreach my $field (qw( start_time duration this_duration )) {
- $timer->{$field} = sprintf('%.4f', $timer->{$field} * 1000) * 1;
- }
- }
-
- # remove private data from env
- delete $self->{env}->{user_agent};
- delete $self->{env}->{referer};
-
- # throw at ES
- require ElasticSearch;
- my $es = ElasticSearch->new(
- servers => Bugzilla->params->{metrics_elasticsearch_server},
- transport => 'http',
- );
- # the ElasticSearch module queries the server for a list of nodes and
- # connects directly to a random node. that bypasses our load balancer so we
- # disable that by setting the server list directly.
- $es->transport->servers(Bugzilla->params->{metrics_elasticsearch_server});
- # as the discovered node list is lazy-loaded, increase _refresh_in so it
- # won't call ->refresh_servers()
- $es->transport->{_refresh_in} = 1;
- $es->index(
- index => Bugzilla->params->{metrics_elasticsearch_index},
- type => Bugzilla->params->{metrics_elasticsearch_type},
- ttl => Bugzilla->params->{metrics_elasticsearch_ttl},
- data => {
- env => $self->{env},
- times => \@timers,
- },
- );
-}
-
-1;
diff --git a/Bugzilla/Metrics/Reporter/STDERR.pm b/Bugzilla/Metrics/Reporter/STDERR.pm
deleted file mode 100644
index a61cdb25f..000000000
--- a/Bugzilla/Metrics/Reporter/STDERR.pm
+++ /dev/null
@@ -1,152 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-package Bugzilla::Metrics::Reporter::STDERR;
-
-use 5.10.1;
-use strict;
-use warnings;
-
-use base 'Bugzilla::Metrics::Reporter';
-
-use Data::Dumper;
-
-use constant DETACH => 0;
-
-sub report {
- my ($self) = @_;
-
- # count totals
- $self->{total} = $self->{times}->{duration};
- $self->{tmpl_count} = $self->{db_count} = $self->{mem_count} = 0;
- $self->{total_tmpl} = $self->{total_db} = $self->{mem_hits} = 0;
- $self->{mem_keys} = {};
- $self->_tally($self->{times});
-
- # calculate percentages
- $self->{other} = $self->{total} - $self->{total_tmpl} - $self->{total_db};
- if ($self->{total} * 1) {
- $self->{perc_tmpl} = $self->{total_tmpl} / $self->{total} * 100;
- $self->{perc_db} = $self->{total_db} / $self->{total} * 100;
- $self->{perc_other} = $self->{other} / $self->{total} * 100;
- } else {
- $self->{perc_tmpl} = 0;
- $self->{perc_db} = 0;
- $self->{perc_other} = 0;
- }
- if ($self->{mem_count}) {
- $self->{perc_mem} = $self->{mem_hits} / $self->{mem_count} * 100;
- } else {
- $self->{perm_mem} = 0;
- }
-
- # convert to ms and format
- foreach my $key (qw( total total_tmpl total_db other )) {
- $self->{$key} = sprintf("%.4f", $self->{$key} * 1000);
- }
- foreach my $key (qw( perc_tmpl perc_db perc_other perc_mem )) {
- $self->{$key} = sprintf("%.1f", $self->{$key});
- }
-
- # massage each timer
- my $start_time = $self->{times}->{start_time};
- $self->walk_timers(sub {
- my ($timer) = @_;
- delete $timer->{parent};
-
- # show relative times
- $timer->{start_time} = $timer->{start_time} - $start_time;
- delete $timer->{end_time};
-
- # show times in ms instead of fractional seconds
- foreach my $field (qw( start_time duration duration_this )) {
- $timer->{$field} = sprintf('%.4f', $timer->{$field} * 1000) * 1
- if exists $timer->{$field};
- }
- });
-
- if (0) {
- # dump timers to stderr
- local $Data::Dumper::Indent = 1;
- local $Data::Dumper::Terse = 1;
- local $Data::Dumper::Sortkeys = sub {
- my ($rh) = @_;
- return [ sort { $b cmp $a } keys %$rh ];
- };
- print STDERR Dumper($self->{env});
- print STDERR Dumper($self->{times});
- }
-
- # summary summary table too
- print STDERR <<EOF;
-total time: $self->{total}
- tmpl time: $self->{total_tmpl} ($self->{perc_tmpl}%) $self->{tmpl_count} hits
- db time: $self->{total_db} ($self->{perc_db}%) $self->{db_count} hits
-other time: $self->{other} ($self->{perc_other}%)
- memcached: $self->{perc_mem}% ($self->{mem_count} requests)
-EOF
- my $tmpls = $self->{tmpl};
- my $len = 0;
- foreach my $file (keys %$tmpls) {
- $len = length($file) if length($file) > $len;
- }
- foreach my $file (sort { $tmpls->{$b}->{count} <=> $tmpls->{$a}->{count} } keys %$tmpls) {
- my $tmpl = $tmpls->{$file};
- printf STDERR
- "%${len}s: %2s hits %8.4f total %8.4f avg\n",
- $file,
- $tmpl->{count},
- $tmpl->{duration} * 1000,
- $tmpl->{duration} * 1000 / $tmpl->{count}
- ;
- }
- my $keys = $self->{mem_keys};
- $len = 0;
- foreach my $key (keys %$keys) {
- $len = length($key) if length($key) > $len;
- }
- foreach my $key (sort { $keys->{$a} <=> $keys->{$b} or $a cmp $b } keys %$keys) {
- printf STDERR "%${len}s: %s\n", $key, $keys->{$key};
- }
-}
-
-sub _tally {
- my ($self, $timer) = @_;
- if (exists $timer->{children}) {
- foreach my $child (@{ $timer->{children} }) {
- $self->_tally($child);
- }
- }
-
- if ($timer->{type} eq 'db') {
- $timer->{duration_this} = $timer->{duration};
- $self->{total_db} += $timer->{duration};
- $self->{db_count}++;
-
- } elsif ($timer->{type} eq 'tmpl') {
- my $child_duration = 0;
- if (exists $timer->{children}) {
- foreach my $child (@{ $timer->{children} }) {
- $child_duration += $child->{duration};
- }
- }
- $timer->{duration_this} = $timer->{duration} - $child_duration;
-
- $self->{total_tmpl} += $timer->{duration} - $child_duration;
- $self->{tmpl_count}++;
- $self->{tmpl}->{$timer->{file}}->{count}++;
- $self->{tmpl}->{$timer->{file}}->{duration} += $timer->{duration};
-
- } elsif ($timer->{type} eq 'memcached') {
- $timer->{duration_this} = $timer->{duration};
- $self->{mem_count}++;
- $self->{mem_keys}->{$timer->{key}}++;
- $self->{mem_hits}++ if $timer->{result} eq 'hit';
- }
-}
-
-1;
diff --git a/Bugzilla/Metrics/Template.pm b/Bugzilla/Metrics/Template.pm
deleted file mode 100644
index 5d9af240e..000000000
--- a/Bugzilla/Metrics/Template.pm
+++ /dev/null
@@ -1,24 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-package Bugzilla::Metrics::Template;
-
-use 5.10.1;
-use strict;
-use warnings;
-
-use base 'Bugzilla::Template';
-
-sub process {
- my $self = shift;
- Bugzilla->metrics->template_start($_[0]);
- my $result = $self->SUPER::process(@_);
- Bugzilla->metrics->end();
- return $result;
-}
-
-1;
diff --git a/Bugzilla/Metrics/Template/Context.pm b/Bugzilla/Metrics/Template/Context.pm
deleted file mode 100644
index 278cfce1e..000000000
--- a/Bugzilla/Metrics/Template/Context.pm
+++ /dev/null
@@ -1,30 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-package Bugzilla::Metrics::Template::Context;
-
-use 5.10.1;
-use strict;
-use warnings;
-
-use base 'Bugzilla::Template::Context';
-
-sub process {
- my $self = shift;
-
- # we only want to measure files not template blocks
- if (ref($_[0]) || substr($_[0], -5) ne '.tmpl') {
- return $self->SUPER::process(@_);
- }
-
- Bugzilla->metrics->template_start($_[0]);
- my $result = $self->SUPER::process(@_);
- Bugzilla->metrics->end();
- return $result;
-}
-
-1;
diff --git a/Bugzilla/ModPerl.pm b/Bugzilla/ModPerl.pm
index 120dd8210..19cd1128f 100644
--- a/Bugzilla/ModPerl.pm
+++ b/Bugzilla/ModPerl.pm
@@ -74,7 +74,7 @@ __DATA__
# every process, and Perl has another. (Various Perl modules still use
# the built-in rand(), even though we never use it in Bugzilla itself,
# so we need to srand() both of them.)
-PerlChildInitHandler "sub { Bugzilla::RNG::srand(); srand(); }"
+PerlChildInitHandler "sub { Bugzilla::RNG::srand(); srand(); eval { Bugzilla->dbh->ping } }"
PerlInitHandler Bugzilla::ModPerl::Hostage
PerlAccessHandler Bugzilla::ModPerl::BlockIP
@@ -86,12 +86,6 @@ ErrorDocument 403 /errors/403.html
ErrorDocument 404 /errors/404.html
ErrorDocument 500 /errors/500.html
-<Location /helper>
- SetHandler perl-script
- PerlResponseHandler Plack::Handler::Apache2
- PerlSetVar psgi_app [% cgi_path %]/helper.psgi
-</Location>
-
<Directory "[% cgi_path %]">
AddHandler perl-script .cgi
# No need to PerlModule these because they're already defined in mod_perl.pl
diff --git a/Bugzilla/Object.pm b/Bugzilla/Object.pm
index 00afbe19f..eaafca219 100644
--- a/Bugzilla/Object.pm
+++ b/Bugzilla/Object.pm
@@ -44,6 +44,9 @@ use constant USE_MEMCACHED => 1;
# values, keywords, products, classifications, priorities, severities, etc.
use constant IS_CONFIG => 0;
+# When DYNAMIC_COLUMNS is true, _get_db_columns() will use the information schema.
+use constant DYNAMIC_COLUMNS => 0;
+
# This allows the JSON-RPC interface to return Bugzilla::Object instances
# as though they were hashes. In the future, this may be modified to return
# less information.
@@ -888,13 +891,19 @@ sub _get_db_columns {
my $cache = Bugzilla->request_cache;
my $cache_key = "object_${class}_db_columns";
return @{ $cache->{$cache_key} } if $cache->{$cache_key};
- # Currently you can only add new columns using object_columns, not
- # remove or modify existing columns, because removing columns would
- # almost certainly cause Bugzilla to function improperly.
- my @add_columns;
- Bugzilla::Hook::process('object_columns',
- { class => $class, columns => \@add_columns });
- my @columns = ($invocant->DB_COLUMNS, @add_columns);
+ my @columns;
+ if ($class->DYNAMIC_COLUMNS) {
+ @columns = Bugzilla->dbh->bz_table_columns_real($class->DB_TABLE);
+ }
+ else {
+ # Currently you can only add new columns using object_columns, not
+ # remove or modify existing columns, because removing columns would
+ # almost certainly cause Bugzilla to function improperly.
+ my @add_columns;
+ Bugzilla::Hook::process('object_columns',
+ { class => $class, columns => \@add_columns });
+ @columns = ($invocant->DB_COLUMNS, @add_columns);
+ }
$cache->{$cache_key} = \@columns;
return @{ $cache->{$cache_key} };
}
diff --git a/Bugzilla/Search/Quicksearch.pm b/Bugzilla/Search/Quicksearch.pm
index 6897d2219..6c0253e27 100644
--- a/Bugzilla/Search/Quicksearch.pm
+++ b/Bugzilla/Search/Quicksearch.pm
@@ -469,6 +469,11 @@ sub _handle_field_names {
$value = $2;
$value =~ s/\\(["'])/$1/g;
}
+ # If the value is a pair of matching quotes, the person wanted the empty string
+ elsif ($value =~ /^(["'])\1$/ || $translated eq 'resolution' && $value eq '---') {
+ $value = "";
+ $operator = "isempty";
+ }
addChart($translated, $operator, $value, $negate);
}
}
@@ -659,6 +664,9 @@ sub negateComparisonType {
if ($comparisonType eq 'anywords') {
return 'nowords';
}
+ elsif ($comparisonType eq 'isempty') {
+ return 'isnotempty';
+ }
return "not$comparisonType";
}
diff --git a/Bugzilla/Sentry.pm b/Bugzilla/Sentry.pm
deleted file mode 100644
index 0d7a9c980..000000000
--- a/Bugzilla/Sentry.pm
+++ /dev/null
@@ -1,374 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-package Bugzilla::Sentry;
-
-use 5.10.1;
-use strict;
-use warnings;
-
-use base qw(Exporter);
-our @EXPORT = qw(
- sentry_handle_error
- sentry_should_notify
-);
-
-use Carp;
-use DateTime;
-use File::Temp;
-use JSON ();
-use List::MoreUtils qw( any );
-use LWP::UserAgent;
-use Sys::Hostname;
-use URI;
-use URI::QueryParam;
-
-use Bugzilla::Constants;
-use Bugzilla::RNG qw(irand);
-use Bugzilla::Util;
-use Bugzilla::WebService::Constants;
-
-use constant CONFIG => {
- # 'codes' lists the code-errors which are sent to sentry
- codes => [qw(
- bug_error
- chart_datafile_corrupt
- chart_dir_nonexistent
- chart_file_open_fail
- illegal_content_type_method
- jobqueue_insert_failed
- ldap_bind_failed
- mail_send_error
- template_error
- token_generation_error
- param_must_be_numeric
- )],
-
- # any error/warning messages matching these regex's will not be logged or
- # sent to sentry
- ignore => [
- qr/^compiled template :\s*$/,
- qr/^Use of uninitialized value \$compiled in concatenation \(\.\) or string/,
- ],
-
- # any error/warning messages matching these regex's will be logged but not
- # sent to sentry
- sentry_ignore => [
- qr/Software caused connection abort/,
- qr/Could not check out .*\/cvsroot/,
- qr/Unicode character \S+ is illegal/,
- qr/Lost connection to MySQL server during query/,
- qr/Call me again when you have some data to chart/,
- qr/relative paths are not allowed/,
- qr/Illegal mix of collations for operation/,
- ],
-
- # (ab)use the logger to classify error/warning types
- logger => [
- {
- match => [
- qr/DBD::mysql/,
- qr/Can't connect to the database/,
- ],
- logger => 'database_error',
- },
- {
- match => [ qr/PatchReader/ ],
- logger => 'patchreader',
- },
- {
- match => [ qr/Use of uninitialized value/ ],
- logger => 'uninitialized_warning',
- },
- ],
-};
-
-sub sentry_generate_id {
- return sprintf('%04x%04x%04x%04x%04x%04x%04x%04x',
- irand(0xffff), irand(0xffff),
- irand(0xffff),
- irand(0x0fff) | 0x4000,
- irand(0x3fff) | 0x8000,
- irand(0xffff), irand(0xffff), irand(0xffff)
- );
-}
-
-sub sentry_should_notify {
- my $code_error = shift;
- return grep { $_ eq $code_error } @{ CONFIG->{codes} };
-}
-
-sub sentry_handle_error {
- my $level = shift;
- my @message = split(/\n/, shift);
- my $id = sentry_generate_id();
-
- my $is_error = $level eq 'error';
- if ($level ne 'error' && $level ne 'warning') {
- # it's a code-error
- return 0 unless sentry_should_notify($level);
- $is_error = 1;
- $level = 'error';
- }
-
- # build traceback
- my $traceback;
- {
- # for now don't show function arguments, in case they contain
- # confidential data. waiting on bug 700683
- #local $Carp::MaxArgLen = 256;
- #local $Carp::MaxArgNums = 0;
- local $Carp::MaxArgNums = -1;
- local $Carp::CarpInternal{'CGI::Carp'} = 1;
- local $Carp::CarpInternal{'Bugzilla::Error'} = 1;
- local $Carp::CarpInternal{'Bugzilla::Sentry'} = 1;
- $traceback = trim(Carp::longmess());
- }
-
- # strip timestamp
- foreach my $line (@message) {
- $line =~ s/^\[[^\]]+\] //;
- }
- my $message = join(" ", map { trim($_) } grep { $_ ne '' } @message);
-
- # message content filtering
- foreach my $re (@{ CONFIG->{ignore} }) {
- return 0 if $message =~ $re;
- }
-
- # determine logger
- my $logger;
- foreach my $config (@{ CONFIG->{logger} }) {
- foreach my $re (@{ $config->{match} }) {
- if ($message =~ $re) {
- $logger = $config->{logger};
- last;
- }
- }
- last if $logger;
- }
- $logger ||= $level;
-
- # don't send to sentry unless configured
- my $send_to_sentry = Bugzilla->params->{sentry_uri} ? 1 : 0;
-
- # web service filtering
- if ($send_to_sentry
- && (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT || Bugzilla->error_mode == ERROR_MODE_JSON_RPC))
- {
- my ($code) = $message =~ /^(-?\d+): /;
- if ($code
- && !($code == ERROR_UNKNOWN_FATAL || $code == ERROR_UNKNOWN_TRANSIENT))
- {
- $send_to_sentry = 0;
- }
- }
-
- # message content filtering
- if ($send_to_sentry) {
- foreach my $re (@{ CONFIG->{sentry_ignore} }) {
- if ($message =~ $re) {
- $send_to_sentry = 0;
- last;
- }
- }
- }
-
- # invalid boolean search errors need special handling
- if ($message =~ /selectcol_arrayref failed: syntax error/
- && $message =~ /IN BOOLEAN MODE/
- && $message =~ /Bugzilla\/Search\.pm/)
- {
- $send_to_sentry = 0;
- }
-
- # for now, don't send patchreader errors to sentry
- $send_to_sentry = 0
- if $logger eq 'patchreader';
-
- # log to apache's error_log
- if ($send_to_sentry) {
- _write_to_error_log("$message [#$id]", $is_error);
- } else {
- $traceback =~ s/\n/ /g;
- _write_to_error_log("$message $traceback", $is_error);
- }
-
- return 0 unless $send_to_sentry;
-
- my $user_data = undef;
- eval {
- my $user = Bugzilla->user;
- if ($user->id) {
- $user_data = {
- id => $user->login,
- name => $user->name,
- };
- }
- };
-
- my $uri = URI->new(Bugzilla->cgi->self_url);
- $uri->query(undef);
-
- # sanitise
-
- # sanitise these query-string params
- # names are checked as-is as well as prefixed by BUGZILLA_
- my @sanitise_params = qw( PASSWORD TOKEN API_KEY );
-
- # remove these ENV vars
- my @sanitise_vars = qw( HTTP_COOKIE HTTP_X_BUGZILLA_PASSWORD HTTP_X_BUGZILLA_API_KEY HTTP_X_BUGZILLA_TOKEN );
-
- foreach my $var (qw( QUERY_STRING REDIRECT_QUERY_STRING )) {
- next unless exists $ENV{$var};
- my @pairs = split('&', $ENV{$var});
- foreach my $pair (@pairs) {
- next unless $pair =~ /^([^=]+)=(.+)$/;
- my ($param, $value) = ($1, $2);
- if (any { uc($param) eq $_ || uc($param) eq "BUGZILLA_$_" } @sanitise_params) {
- $value = '*';
- }
- $pair = $param . '=' . $value;
- }
- $ENV{$var} = join('&', @pairs);
- }
- foreach my $var (qw( REQUEST_URI HTTP_REFERER )) {
- next unless exists $ENV{$var};
- my $uri = URI->new($ENV{$var});
- foreach my $param ($uri->query_param) {
- if (any { uc($param) eq $_ || uc($param) eq "BUGZILLA_$_" } @sanitise_params) {
- $uri->query_param($param, '*');
- }
- }
- $ENV{$var} = $uri->as_string;
- }
- foreach my $var (@sanitise_vars) {
- delete $ENV{$var};
- }
-
- my $now = DateTime->now();
- my $data = {
- event_id => $id,
- message => $message,
- timestamp => $now->iso8601(),
- level => $level,
- platform => 'Other',
- logger => $logger,
- server_name => hostname(),
- 'sentry.interfaces.User' => $user_data,
- 'sentry.interfaces.Http' => {
- url => $uri->as_string,
- method => $ENV{REQUEST_METHOD},
- query_string => $ENV{QUERY_STRING},
- env => \%ENV,
- },
- extra => {
- stacktrace => $traceback,
- },
- };
-
- my $fh = File::Temp->new(
- DIR => bz_locations()->{error_reports},
- TEMPLATE => $now->ymd('') . $now->hms('') . '-XXXX',
- SUFFIX => '.dump',
- UNLINK => 0,
-
- );
- if (!$fh) {
- warn "Failed to create dump file: $!\n";
- return;
- }
- print $fh JSON->new->utf8(1)->pretty(0)->allow_nonref(1)->encode($data);
- close($fh);
- return 1;
-}
-
-sub _write_to_error_log {
- my ($message, $is_error) = @_;
- if ($ENV{MOD_PERL}) {
- require Apache2::Log;
- if ($is_error) {
- Apache2::ServerRec::log_error($message);
- } else {
- Apache2::ServerRec::warn($message);
- }
- } else {
- print STDERR $message, "\n";
- }
-}
-
-# lifted from Bugzilla::Error
-sub _in_eval {
- my $in_eval = 0;
- for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) {
- last if $sub =~ /^ModPerl/;
- last if $sub =~ /^Bugzilla::Template/;
- $in_eval = 1 if $sub =~ /^\(eval\)/;
- }
- return $in_eval;
-}
-
-sub _sentry_die_handler {
- my $message = shift;
- $message =~ s/^undef error - //;
-
- # avoid recursion, and check for CGI::Carp::die failures
- my $in_cgi_carp_die = 0;
- for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) {
- return if $sub =~ /:_sentry_die_handler$/;
- $in_cgi_carp_die = 1 if $sub =~ /CGI::Carp::die$/;
- }
-
- return if $Bugzilla::Template::is_processing;
- return if _in_eval();
-
- # mod_perl overrides exit to call die with this string
- exit if $message =~ /\bModPerl::Util::exit\b/;
-
- my $nested_error = '';
- my $is_compilation_failure = $message =~ /\bcompilation (aborted|failed)\b/i;
-
- # if we are called via CGI::Carp::die chances are something is seriously
- # wrong, so skip trying to use ThrowTemplateError
- if (!$in_cgi_carp_die && !$is_compilation_failure) {
- eval {
- my $cgi = Bugzilla->cgi;
- $cgi->close_standby_message('text/html', 'inline', 'error', 'html');
- Bugzilla::Error::ThrowTemplateError($message);
- print $cgi->multipart_final() if $cgi->{_multipart_in_progress};
- };
- $nested_error = $@ if $@;
- }
-
- if ($is_compilation_failure ||
- $in_cgi_carp_die ||
- ($nested_error && $nested_error !~ /\bModPerl::Util::exit\b/)
- ) {
- sentry_handle_error('error', $message);
-
- # and call the normal error management
- # (ISE for web pages, error response for web services, etc)
- CORE::die($message);
- }
- exit;
-}
-
-sub install_sentry_handler {
- $SIG{__DIE__} = \&sentry_die_handler;
- $SIG{__WARN__} = sub {
- return if _in_eval();
- sentry_handle_error('warning', shift);
- };
-}
-
-BEGIN {
- if ($ENV{SCRIPT_NAME} || $ENV{MOD_PERL}) {
- install_sentry_handler();
- }
-}
-
-1;
diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm
index e6b4e551d..107c457c6 100644
--- a/Bugzilla/Template.pm
+++ b/Bugzilla/Template.pm
@@ -1052,10 +1052,7 @@ sub create {
$SHARED_PROVIDERS{$provider_key} ||= $provider_class->new($config);
$config->{LOAD_TEMPLATES} = [ $SHARED_PROVIDERS{$provider_key} ];
- # BMO - use metrics subclass
- local $Template::Config::CONTEXT = Bugzilla->metrics_enabled()
- ? 'Bugzilla::Metrics::Template::Context'
- : 'Bugzilla::Template::Context';
+ local $Template::Config::CONTEXT = 'Bugzilla::Template::Context';
Bugzilla::Hook::process('template_before_create', { config => $config });
my $template = $class->new($config)
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm
index c07454d7d..feb541c2e 100644
--- a/Bugzilla/WebService/Bug.pm
+++ b/Bugzilla/WebService/Bug.pm
@@ -35,6 +35,8 @@ use Bugzilla::Search::Quicksearch;
use List::Util qw(max);
use List::MoreUtils qw(uniq);
use Storable qw(dclone);
+use Types::Standard -all;
+use Type::Utils;
#############
# Constants #
@@ -656,9 +658,30 @@ sub possible_duplicates {
Bugzilla->switch_to_shadow_db();
- # Undo the array-ification that validate() does, for "summary".
- $params->{summary} || ThrowCodeError('param_required',
- { function => 'Bug.possible_duplicates', param => 'summary' });
+ state $params_type = Dict [
+ id => Optional [Int],
+ product => Optional [ ArrayRef [Str] ],
+ limit => Optional [Int],
+ summary => Optional [Str],
+ include_fields => Optional [ ArrayRef [Str] ],
+ Bugzilla_api_token => Optional [Str]
+ ];
+
+ ThrowCodeError( 'param_invalid', { function => 'Bug.possible_duplicates', param => 'A param' } )
+ if !$params_type->check($params);
+
+ my $summary;
+ if ($params->{id}) {
+ my $bug = Bugzilla::Bug->check({ id => $params->{id}, cache => 1 });
+ $summary = $bug->short_desc;
+ }
+ elsif ($params->{summary}) {
+ $summary = $params->{summary};
+ }
+ else {
+ ThrowCodeError('param_required',
+ { function => 'Bug.possible_duplicates', param => 'id or summary' });
+ }
my @products;
foreach my $name (@{ $params->{'product'} || [] }) {
@@ -667,8 +690,18 @@ sub possible_duplicates {
}
my $possible_dupes = Bugzilla::Bug->possible_duplicates(
- { summary => $params->{summary}, products => \@products,
- limit => $params->{limit} });
+ {
+ summary => $summary,
+ products => \@products,
+ limit => $params->{limit}
+ }
+ );
+
+ # If a bug id was used, remove the bug with the same id from the list.
+ if ($params->{id}) {
+ @$possible_dupes = grep { $_->id != $params->{id} } @$possible_dupes;
+ }
+
my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes;
$self->_add_update_tokens($params, $possible_dupes, \@hashes);
return { bugs => \@hashes };
diff --git a/Bugzilla/WebService/Server.pm b/Bugzilla/WebService/Server.pm
index ba9847abc..a76c4c48c 100644
--- a/Bugzilla/WebService/Server.pm
+++ b/Bugzilla/WebService/Server.pm
@@ -23,11 +23,6 @@ sub handle_login {
# Throw error if the supplied class does not exist or the method is private
ThrowCodeError('unknown_method', {method => $full_method}) if (!$class or $method =~ /^_/);
- # BMO - use the class and method as the name, instead of the cgi filename
- if (Bugzilla->metrics_enabled) {
- Bugzilla->metrics->name("$class $method");
- }
-
# We never want to create a new session unless the user is calling the
# login method. Setting dont_persist_session makes
# Bugzilla::Auth::_handle_login_result() skip calling persist_login().
diff --git a/Bugzilla/WebService/Server/REST/Resources/Bug.pm b/Bugzilla/WebService/Server/REST/Resources/Bug.pm
index 33cf43321..26aec011c 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Bug.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Bug.pm
@@ -34,6 +34,11 @@ sub _rest_resources {
method => 'get'
}
},
+ qr{^/bug/possible_duplicates$}, {
+ GET => {
+ method => 'possible_duplicates'
+ }
+ },
qr{^/bug/([^/]+)$}, {
GET => {
method => 'get',
diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm
index fce865e88..6bb73af01 100644
--- a/Bugzilla/WebService/Server/XMLRPC.pm
+++ b/Bugzilla/WebService/Server/XMLRPC.pm
@@ -11,6 +11,7 @@ use 5.10.1;
use strict;
use warnings;
+use Bugzilla::Logging;
use XMLRPC::Transport::HTTP;
use Bugzilla::WebService::Server;
if ($ENV{MOD_PERL}) {
@@ -32,8 +33,15 @@ BEGIN {
if ($type eq 'dateTime') {
# This is the XML-RPC implementation, see the README in Bugzilla/WebService/.
# Our "base" implementation is in Bugzilla::WebService::Server.
- $value = Bugzilla::WebService::Server->datetime_format_outbound($value);
- $value =~ s/-//g;
+ if (defined $value) {
+ $value = Bugzilla::WebService::Server->datetime_format_outbound($value);
+ $value =~ s/-//g;
+ }
+ else {
+ my ($pkg, $file, $line) = caller;
+ my $class = ref $self;
+ ERROR("$class->type($type, undef) called from $pkg ($file line $line)");
+ }
}
elsif ($type eq 'email') {
$type = 'string';
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e3c3bd4a4..ffe627d30 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,7 +12,7 @@ feedback for changes that would be required. All contributions should follow
this format, even those from core contributors.
Should you wish to work on an issue, please claim it first by commenting on
-the GitHub issue that you want to work on it. This is to prevent duplicated
+the bug that you want to work on it. This is to prevent duplicated
efforts from contributors on the same issue.
Head over to [BugsAhoy!](https://www.joshmatthews.net/bugsahoy/?bugzilla=1)
diff --git a/Dockerfile b/Dockerfile
index 35a5f22e9..009c41ee8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -26,6 +26,7 @@ COPY . .
RUN mv /opt/bmo/local /app && \
chown -R app:app /app && \
+ perl -I/app -I/app/local/lib/perl5 -c -E 'use Bugzilla; BEGIN { Bugzilla->extensions }' && \
perl -c /app/scripts/entrypoint.pl && \
setcap 'cap_net_bind_service=+ep' /usr/sbin/httpd
diff --git a/Log/Log4perl/Layout/Mozilla.pm b/Log/Log4perl/Layout/Mozilla.pm
index 67a070c54..b625c54f4 100644
--- a/Log/Log4perl/Layout/Mozilla.pm
+++ b/Log/Log4perl/Layout/Mozilla.pm
@@ -52,6 +52,7 @@ sub render {
);
my $mdc = Log::Log4perl::MDC->get_context;
+ my $fields = $mdc->{fields} // {};
my %out = (
EnvVersion => LOGGING_FORMAT_VERSION,
Hostname => $HOSTNAME,
@@ -60,12 +61,12 @@ sub render {
Severity => $Log::Log4perl::Level::SYSLOG{$priority},
Timestamp => time() * 1e9,
Type => $category,
- Fields => { msg => $msg, %$mdc },
+ Fields => { msg => $msg, %$fields },
);
my $json_text = $JSON->encode(\%out) . "\n";
if (length($json_text) > $self->max_json_length) {
- my $scary_msg = sprintf( "DANGER! LOG MESSAGE TOO BIG %d > %d", length($json_text), $self->max_json_length );
+ my $scary_msg = sprintf 'DANGER! LOG MESSAGE TOO BIG %d > %d', length($json_text), $self->max_json_length;
$out{Fields} = { remote_ip => $mdc->{remote_ip}, msg => $scary_msg };
$out{Severity} = 1; # alert
$json_text = $JSON->encode(\%out) . "\n";
diff --git a/Makefile.PL b/Makefile.PL
index 2c7b8fad7..7012c641d 100755
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -33,14 +33,16 @@ BEGIN {
# PREREQ_PM
my %requires = (
+ 'Alien::libcmark_gfm' => 0,
'Algorithm::BloomFilter' => '0.02',
- 'CGI' => '3.51',
+ 'CGI' => '4.31',
'CGI::Compile' => 0,
'CGI::Emulate::PSGI' => 0,
'CPAN::Meta::Prereqs' => '2.132830',
'CPAN::Meta::Requirements' => '2.121',
'Class::XSAccessor' => '1.18',
'DBI' => '1.614',
+ 'DBIx::Connector' => 0,
'Data::Password::passwdqc' => '0.08',
'Date::Format' => '2.23',
'DateTime' => '0.75',
@@ -49,7 +51,9 @@ my %requires = (
'Digest::SHA' => '5.47',
'Email::MIME' => '1.904',
'Email::Send' => '1.911',
+ 'FFI::Platypus' => 0,
'File::Slurp' => '9999.13',
+ 'File::Slurper' => '0.012',
'Future' => '0.34',
'HTML::Escape' => '1.10',
'IPC::System::Simple' => 0,
@@ -64,6 +68,7 @@ my %requires = (
'Math::Random::ISAAC' => '1.0.1',
'Module::Metadata' => '1.000033',
'Module::Runtime' => '0.014',
+ 'Mojolicious' => '7.71',
'Moo' => '2.002004',
'MooX::StrictConstructor' => '0.008',
'Mozilla::CA' => '20160104',
@@ -109,6 +114,16 @@ if ( $OSNAME eq 'linux' && -f '/etc/debian_version' ) {
}
my %optional_features = (
+ argon2 => {
+ description => 'Support hashing passwords with Argon2',
+ prereqs => {
+ runtime => {
+ requires => {
+ 'Crypt::Argon2' => '0.004',
+ },
+ },
+ },
+ },
smtp_auth => {
description => 'SMTP Authentication',
prereqs => { runtime => { requires => { 'Authen::SASL' => 0 } } },
@@ -423,13 +438,14 @@ META.json: Makefile.PL
\tmake distmeta 2>&1 /dev/null; mv */META.json .
META.yml: Makefile.PL
+1: hit EOF seeking end of quote/pattern starting at line 1 ending in /
\tmake distmeta 2>&1 /dev/null; mv */META.yml .
MAKE
}
sub is_bmo_feature {
local $_ = shift;
- return 1 if $OSNAME eq 'linux' && /^linux/;
+ return $OSNAME eq 'linux' if /^linux/;
return !m{
^
(?: pg
diff --git a/README.rst b/README.rst
index adeb1a18e..985be314e 100644
--- a/README.rst
+++ b/README.rst
@@ -80,6 +80,12 @@ or db is changed, do a full provision:
vagrant rsync && vagrant provision
+If you are using Visual Studio Code, these tasks are available from the editor's
+`Tasks menu`_. The update command can be run by simply hitting `Ctrl+Shift+B` on
+Windows/Linux or `Command+Shift+B` on macOS.
+
+.. _`Tasks menu`: https://code.visualstudio.com/docs/editor/tasks
+
Testing Auth delegation
-----------------------
@@ -377,6 +383,13 @@ LOG4PERL_CONFIG_FILE
.. _`Devel::NYTProf`: https://metacpan.org/pod/Devel::NYTProf
.. _`Log::Log4perl`: https://metacpan.org/pod/Log::Log4perl
+LOG4PERL_STDERR_DISABLE
+ Boolean. By default log messages are logged as plain text to `STDERR`.
+ Setting this to a true value disables this behavior.
+
+ Note: For programs that run using the `cereal` log aggregator, this environemnt
+ variable will be ignored.
+
Persistent Data Volume
----------------------
diff --git a/Vagrantfile b/Vagrantfile
index 8776ac08f..f63c707a0 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -22,6 +22,7 @@ RSYNC_ARGS = [
'--copy-links',
'--exclude=local/',
'--exclude=data/',
+ '--exclude=logs/',
'--exclude=template_cache/',
'--exclude=localconfig',
'--include=.git/'
@@ -38,7 +39,6 @@ Vagrant.configure('2') do |config|
config.vm.provision 'main', type: 'ansible_local', run: 'always' do |ansible|
ansible.playbook = 'vagrant_support/playbook.yml'
- ansible.version = 'latest'
ansible.extra_vars = {
WEB_IP: WEB_IP,
DB_IP: DB_IP,
@@ -51,7 +51,6 @@ Vagrant.configure('2') do |config|
if ARGV.include? '--provision-with'
config.vm.provision 'update', type: 'ansible_local', run: 'never' do |update|
update.playbook = 'vagrant_support/update.yml'
- update.version = 'latest'
end
end
diff --git a/admin.cgi b/admin.cgi
index d8fc0475d..801b26e20 100755
--- a/admin.cgi
+++ b/admin.cgi
@@ -25,6 +25,7 @@ print $cgi->header();
$user->in_group('admin')
|| $user->in_group('tweakparams')
|| $user->in_group('editusers')
+ || $user->in_group('disableusers')
|| $user->can_bless
|| (Bugzilla->params->{'useclassification'} && $user->in_group('editclassifications'))
|| $user->in_group('editcomponents')
diff --git a/conf/httpd.conf b/conf/httpd.conf
index 7fe859b59..539ab4231 100644
--- a/conf/httpd.conf
+++ b/conf/httpd.conf
@@ -86,6 +86,7 @@ Include /app/conf/env.conf
PerlSwitches -wT
PerlRequire /app/mod_perl.pl
+PerlSetEnv LOG4PERL_STDERR_DISABLE 1
DirectoryIndex index.cgi
DocumentRoot "/app"
<IfDefine HTTPD_IN_SUBDIR>
diff --git a/conf/log4perl-docker.conf b/conf/log4perl-docker.conf
new file mode 100644
index 000000000..1f5a033fd
--- /dev/null
+++ b/conf/log4perl-docker.conf
@@ -0,0 +1,14 @@
+log4perl.rootLogger = DEBUG, Cereal, Screen
+log4perl.appender.Cereal = Log::Log4perl::Appender::Socket
+log4perl.appender.Cereal.PeerAddr=127.0.0.1
+log4perl.appender.Cereal.PeerPort=5880
+log4perl.appender.Cereal.defer_connection=1
+log4perl.appender.Cereal.layout = Log::Log4perl::Layout::PatternLayout
+log4perl.appender.Cereal.layout.ConversionPattern = %d %6p | %c | %m{chomp}%n
+
+log4perl.filter.LOG_TO_STDERR = sub { not $ENV{LOG4PERL_STDERR_DISABLE} }
+log4perl.appender.Screen = Log::Log4perl::Appender::Screen
+log4perl.appender.Screen.Filter = LOG_TO_STDERR
+log4perl.appender.Screen.stderr = 1
+log4perl.appender.Screen.layout = Log::Log4perl::Layout::PatternLayout
+log4perl.appender.Screen.layout.ConversionPattern = %d %6p | %c | %m{chomp}%n
diff --git a/conf/log4perl-json.conf b/conf/log4perl-json.conf
index c5d7e2c1f..a9c4b6e1a 100644
--- a/conf/log4perl-json.conf
+++ b/conf/log4perl-json.conf
@@ -13,10 +13,12 @@ log4perl.appender.Cereal.layout.max_json_length = 16384
# The default is Bugzilla. This is the "Logger" field
# in https://wiki.mozilla.org/Firefox/Services/Logging#MozLog_JSON_schema
#and it might be useful to pass in different values for different jobs.
-log4perl.appender.Cereal.layout.name = Bugzilla
+log4perl.appender.Cereal.layout.name = CEREAL
-log4perl.filter.IS_INTERACTIVE = sub { Bugzilla::Logging::is_interactive() }
+log4perl.filter.LOG_TO_STDERR = sub { not $ENV{LOG4PERL_STDERR_DISABLE} }
log4perl.appender.Screen = Log::Log4perl::Appender::Screen
-log4perl.appender.Screen.Filter = IS_INTERACTIVE
-log4perl.appender.Screen.layout = Log::Log4perl::Layout::PatternLayout
-log4perl.appender.Screen.layout.ConversionPattern = %-5.5p [%d] [%c] %m{chomp} at %F line %L (%M)%n
+log4perl.appender.Screen.Filter = LOG_TO_STDERR
+log4perl.appender.Screen.stderr = 1
+log4perl.appender.Screen.layout = Log::Log4perl::Layout::Mozilla
+log4perl.appender.Screen.layout.max_json_length = 16384
+log4perl.appender.Screen.layout.name = STDERR
diff --git a/conf/log4perl-test.conf b/conf/log4perl-test.conf
index 65558ba4f..77fc00af8 100644
--- a/conf/log4perl-test.conf
+++ b/conf/log4perl-test.conf
@@ -4,15 +4,18 @@ log4perl.appender.Cereal.PeerAddr=127.0.0.1
log4perl.appender.Cereal.PeerPort=5880
log4perl.appender.Cereal.defer_connection=1
log4perl.appender.Cereal.layout = Log::Log4perl::Layout::PatternLayout
-log4perl.appender.Cereal.layout.ConversionPattern = %-5.5p [%d] [%c] %m{chomp} at %F line %L (%M)%n
+log4perl.appender.Cereal.layout.ConversionPattern = %d %6p | %c | %m{chomp}%n
-log4perl.filter.IS_INTERACTIVE = sub { Bugzilla::Logging::is_interactive() }
+log4perl.filter.LOG_TO_STDERR = sub { not $ENV{LOG4PERL_STDERR_DISABLE} }
log4perl.appender.Screen = Log::Log4perl::Appender::Screen
-log4perl.appender.Screen.Filter = IS_INTERACTIVE
+log4perl.appender.Screen.Filter = LOG_TO_STDERR
+log4perl.appender.Screen.stderr = 1
log4perl.appender.Screen.layout = Log::Log4perl::Layout::PatternLayout
-log4perl.appender.Screen.layout.ConversionPattern = %-5.5p [%d] [%c] %m{chomp} at %F line %L (%M)%n
+log4perl.appender.Screen.layout.ConversionPattern = %d %6p | %c | %m{chomp}%n
log4perl.appender.File = Log::Log4perl::Appender::File
log4perl.appender.File.layout = Log::Log4perl::Layout::Mozilla
log4perl.appender.File.filename = /app/bugzilla.log
log4perl.appender.File.mode = append
+log4perl.appender.File.syswrite = 1
+log4perl.appender.File.autoflush = 1
diff --git a/conf/log4perl-vagrant.conf b/conf/log4perl-vagrant.conf
new file mode 100644
index 000000000..d9c3e2753
--- /dev/null
+++ b/conf/log4perl-vagrant.conf
@@ -0,0 +1,24 @@
+log4perl.rootLogger = DEBUG, TextFile, JSONFile, Screen
+
+log4perl.appender.TextFile = Log::Log4perl::Appender::File
+log4perl.appender.TextFile.layout = Log::Log4perl::Layout::PatternLayout
+log4perl.appender.TextFile.filename = sub { Bugzilla::Logging->logfile("bugzilla.log") }
+log4perl.appender.TextFile.layout.ConversionPattern = %-5.5p [%d] [%c] %m{chomp} at %F line %L (%M)%n
+log4perl.appender.TextFile.create_at_logtime = true
+log4perl.appender.TextFile.owner = vagrant
+log4perl.appender.TextFile.group = apache
+
+log4perl.appender.JSONFile = Log::Log4perl::Appender::File
+log4perl.appender.JSONFile.layout = Log::Log4perl::Layout::Mozilla
+log4perl.appender.JSONFile.filename = sub { Bugzilla::Logging->logfile("bugzilla-json.log") }
+log4perl.appender.JSONFile.mode = append
+log4perl.appender.JSONFile.create_at_logtime = true
+log4perl.appender.JSONFile.owner = vagrant
+log4perl.appender.JSONFile.group = apache
+
+log4perl.filter.LOG_TO_STDERR = sub { not $ENV{LOG4PERL_STDERR_DISABLE} }
+log4perl.appender.Screen = Log::Log4perl::Appender::Screen
+log4perl.appender.Screen.Filter = LOG_TO_STDERR
+log4perl.appender.Screen.stderr = 1
+log4perl.appender.Screen.layout = Log::Log4perl::Layout::PatternLayout
+log4perl.appender.Screen.layout.ConversionPattern = %d %6p | %c | %m{chomp}%n
diff --git a/docker-compose.yml b/docker-compose.yml
index 5dfb6fad4..1ca6f5c90 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -17,7 +17,7 @@ services:
- /run
environment: &bmo_env
- LOCALCONFIG_ENV=1
- - LOG4PERL_CONFIG_FILE=log4perl-test.conf
+ - LOG4PERL_CONFIG_FILE=log4perl-docker.conf
- BUGZILLA_UNSAFE_AUTH_DELEGATION=1
- PORT=80
- BMO_db_host=bmo-db.vm
diff --git a/docs/en/rst/api/core/v1/bug.rst b/docs/en/rst/api/core/v1/bug.rst
index 3c7605527..53f637a67 100644
--- a/docs/en/rst/api/core/v1/bug.rst
+++ b/docs/en/rst/api/core/v1/bug.rst
@@ -1087,3 +1087,119 @@ This method can throw all the same errors as :ref:`rest_single_bug`, plus:
You did not specify a valid for the "file_name" argument.
* 604 (Summary Required)
You did not specify a value for the "summary" argument.
+
+.. _rest_possible_duplicates:
+
+Possible Duplicates
+-------------------
+
+Gets a list of possible duplicate bugs.
+
+**Request**
+
+To search by similar bug.
+
+.. code-block:: text
+
+ GET /rest/bug/possible_duplicates?id=1234567
+
+To search by a similar bug summary directly.
+
+.. code-block:: text
+
+ GET /rest/bug/possible_duplicates?summary=Similar+Bug+Summary
+
+======= ====== ================================================================
+name type description
+======= ====== ================================================================
+id int The id of a bug to find duplicates of.
+summary string A summary to search for duplicates of, only used if no bug id is
+ given.
+product string A product group to limit the search in.
+limit int Limit the number of results returned. If the limit
+ is more than zero and higher than the maximum limit
+ set by the administrator, then the maximum limit will
+ be used instead. If you set the limit equal to zero,
+ then all matching results will be returned instead.
+======= ====== ================================================================
+
+**Response**
+
+.. code-block:: js
+
+ {
+ "bugs": [
+ {
+ "alias": [],
+ "history": [
+ {
+ "when": "2014-09-23T19:12:17Z",
+ "who": "user@bugzilla.org",
+ "changes": [
+ {
+ "added": "P1",
+ "field_name": "priority",
+ "removed": "P2"
+ },
+ {
+ "removed": "blocker",
+ "field_name": "severity",
+ "added": "critical"
+ }
+ ]
+ },
+ {
+ "when": "2014-09-28T21:03:47Z",
+ "who": "user@bugzilla.org",
+ "changes": [
+ {
+ "added": "blocker?",
+ "removed": "",
+ "field_name": "flagtypes.name"
+ }
+ ]
+ }
+ ],
+ "id": 35
+ }
+ ]
+ }
+
+``bugs`` (array) Bug objects each containing the following items. If a bug id was
+used to query this endpoint, that bug will not be in the list returned.
+
+======= ===== =================================================================
+name type description
+======= ===== =================================================================
+id int The numeric ID of the bug.
+alias array The unique aliases of this bug. An empty array will be returned
+ if this bug has no aliases.
+history array An array of History objects.
+======= ===== =================================================================
+
+History object:
+
+======= ======== ==============================================================
+name type description
+======= ======== ==============================================================
+when datetime The date the bug activity/change happened.
+who string The login name of the user who performed the bug change.
+changes array An array of Change objects which contain all the changes that
+ happened to the bug at this time (as specified by ``when``).
+======= ======== ==============================================================
+
+Change object:
+
+============= ====== ==========================================================
+name type description
+============= ====== ==========================================================
+field_name string The name of the bug field that has changed.
+removed string The previous value of the bug field which has been
+ deleted by the change.
+added string The new value of the bug field which has been added
+ by the change.
+attachment_id int The ID of the attachment that was changed.
+ This only appears if the change was to an attachment,
+ otherwise ``attachment_id`` will not be present in this
+ object.
+============= ====== ========================================================== \ No newline at end of file
diff --git a/editusers.cgi b/editusers.cgi
index ae1f35835..beb9b3a4c 100755
--- a/editusers.cgi
+++ b/editusers.cgi
@@ -25,15 +25,18 @@ use Bugzilla::Group;
use Bugzilla::Token;
local our $user = Bugzilla->login(LOGIN_REQUIRED);
-my $cgi = Bugzilla->cgi;
-my $template = Bugzilla->template;
-my $dbh = Bugzilla->dbh;
-my $userid = $user->id;
-my $editusers = $user->in_group('editusers');
-local our $vars = {};
+
+my $cgi = Bugzilla->cgi;
+my $template = Bugzilla->template;
+my $dbh = Bugzilla->dbh;
+my $userid = $user->id;
+my $editusers = $user->in_group('editusers');
+my $disableusers = $user->in_group('disableusers');
+local our $vars = {};
# Reject access if there is no sense in continuing.
$editusers
+ || $disableusers
|| $user->can_bless()
|| ThrowUserError("auth_failure", {group => "editusers",
reason => "cant_bless",
@@ -50,6 +53,7 @@ my $token = $cgi->param('token');
# Prefill template vars with data used in all or nearly all templates
$vars->{'editusers'} = $editusers;
+$vars->{'disableusers'} = $disableusers;
mirrorListSelectionValues();
Bugzilla::Hook::process('admin_editusers_action',
@@ -233,7 +237,7 @@ if ($action eq 'search') {
# Lock tables during the check+update session.
$dbh->bz_start_transaction();
- $editusers || $user->can_see_user($otherUser)
+ $editusers || $disableusers || $user->can_see_user($otherUser)
|| ThrowUserError('auth_failure', {reason => "not_visible",
action => "modify",
object => "user"});
@@ -245,11 +249,8 @@ if ($action eq 'search') {
my $changes = {};
if ($editusers) {
$otherUser->set_login($cgi->param('login'));
- $otherUser->set_name($cgi->param('name'));
$otherUser->set_password($cgi->param('password'))
if $cgi->param('password');
- $otherUser->set_disabledtext($cgi->param('disabledtext'));
- $otherUser->set_disable_mail($cgi->param('disable_mail'));
$otherUser->set_extern_id($cgi->param('extern_id'))
if defined($cgi->param('extern_id'));
$otherUser->set_password_change_required($cgi->param('password_change_required'));
@@ -261,9 +262,16 @@ if ($action eq 'search') {
if ($user->in_group('bz_can_disable_mfa') && $otherUser->mfa && $cgi->param('mfa') eq '') {
$otherUser->set_mfa('');
}
- $changes = $otherUser->update();
}
+ if ($editusers || $disableusers) {
+ $otherUser->set_name($cgi->param('name'));
+ $otherUser->set_disabledtext($cgi->param('disabledtext'));
+ $otherUser->set_disable_mail($cgi->param('disable_mail'));
+ }
+
+ $changes = $otherUser->update();
+
# Update group settings.
my $sth_add_mapping = $dbh->prepare(
qq{INSERT INTO user_group_map (
@@ -849,7 +857,9 @@ sub edit_processing {
my $user = Bugzilla->user;
my $template = Bugzilla->template;
- $user->in_group('editusers') || $user->can_see_user($otherUser)
+ $user->in_group('editusers')
+ || $user->in_group('disableusers')
+ || $user->can_see_user($otherUser)
|| ThrowUserError('auth_failure', {reason => "not_visible",
action => "modify",
object => "user"});
diff --git a/extensions/BMO/Extension.pm b/extensions/BMO/Extension.pm
index f23ab723f..d2e62eccd 100644
--- a/extensions/BMO/Extension.pm
+++ b/extensions/BMO/Extension.pm
@@ -1213,7 +1213,7 @@ sub _attachment_fetch_github_pr_diff {
warn "Github fetch error: $pr_diff, " . $response->status_line;
return "Error retrieving Github pull request diff for " . $self->data;
}
- return $response->content;
+ return $response->decoded_content;
}
# redirect automatically to github urls
diff --git a/extensions/BMO/lib/Data.pm b/extensions/BMO/lib/Data.pm
index 4df05581c..30a32c95e 100644
--- a/extensions/BMO/lib/Data.pm
+++ b/extensions/BMO/lib/Data.pm
@@ -109,12 +109,11 @@ tie(%$cf_visible_in_products, "Tie::IxHash",
"Calendar" => [],
"Composer" => [],
"Core" => [],
+ "DevTools" => [],
"Directory" => [],
"External Software Affecting Firefox" => [],
"Firefox" => [],
"Firefox for Android" => [],
- "Firefox for Metro" => [],
- "Firefox OS" => [],
"JSS" => [],
"MailNews Core" => [],
"Mozilla Labs" => [],
@@ -133,6 +132,7 @@ tie(%$cf_visible_in_products, "Tie::IxHash",
"Testing" => [],
"Thunderbird" => [],
"Toolkit" => [],
+ "WebExtensions" => [],
},
qr/^cf_due_date$/ => {
"bugzilla.mozilla.org" => [],
diff --git a/extensions/BMO/template/en/default/account/create.html.tmpl b/extensions/BMO/template/en/default/account/create.html.tmpl
index 59ff822bd..fa897e0e8 100644
--- a/extensions/BMO/template/en/default/account/create.html.tmpl
+++ b/extensions/BMO/template/en/default/account/create.html.tmpl
@@ -156,10 +156,12 @@ function onSubmit() {
</tr>
<tr>
<td colspan="2">
+ <input type="checkbox" id="etiquette" value="agreed">
+ <label for="etiquette">
I have read <a href="page.cgi?id=etiquette.html">[% terms.Bugzilla %] Etiquette</a>
and the <a href="https://www.mozilla.org/about/governance/policies/participation/">Mozilla Community Participation Guidelines</a>
and agree to abide by them.
- <input type="checkbox" id="etiquette" value="agreed">
+ </label>
</td>
</tr>
<tr>
diff --git a/extensions/BMO/template/en/default/bug/create/comment-third-party-apps.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-third-party-apps.txt.tmpl
deleted file mode 100644
index 7a30902f2..000000000
--- a/extensions/BMO/template/en/default/bug/create/comment-third-party-apps.txt.tmpl
+++ /dev/null
@@ -1,22 +0,0 @@
-[%# 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 Bugzilla %]
-[% cgi = Bugzilla.cgi %]
-
-> Detailed description of the issue or enhancement request
-[%+ cgi.param('desc') %]
-
-> Is this issue reproducible?
-[%+ cgi.param('reproducible') %]
-
-> Device Information
-[%+ cgi.param('device_info') %]
-
-> Mozilla Reference Device
-[%+ cgi.param('reference_device') %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-dev-engagement-event.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-dev-engagement-event.html.tmpl
index 1197952fe..5478de00f 100644
--- a/extensions/BMO/template/en/default/bug/create/create-dev-engagement-event.html.tmpl
+++ b/extensions/BMO/template/en/default/bug/create/create-dev-engagement-event.html.tmpl
@@ -142,7 +142,7 @@
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
- <div id="developer_event_warning" class="bz_default_hidden">
+ <div id="developer_event_warning" class="warning bz_default_hidden">
The Developer Events Team only participates in developer events.
Form submission has been disabled.
</div>
diff --git a/extensions/BMO/template/en/default/bug/create/create-third-party-apps.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-third-party-apps.html.tmpl
deleted file mode 100644
index b6173e6e4..000000000
--- a/extensions/BMO/template/en/default/bug/create/create-third-party-apps.html.tmpl
+++ /dev/null
@@ -1,191 +0,0 @@
-[%# 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.
- #%]
-
-[% PROCESS global/variables.none.tmpl %]
-
-[% inline_css = BLOCK %]
-#third_party_form {
- width: 60%;
-}
-#third_party_form .required:after {
- content: " *";
- color: red;
-}
-#third_party_form .field_label {
- text-align: left;
- font-weight: bold;
-}
-#third_party_form .field_desc,
-#third_party_form .head_desc {
- word-wrap: normal;
-}
-#third_party_form .head_desc {
- font-size: 1.25em;
- padding-bottom: .5em;
-}
-#third_party_form .form_section {
- margin-bottom: 1em;
- padding-left: 2em;
-}
-.yui-calcontainer {
- z-index: 2;
-}
-[% END %]
-
-[% inline_javascript = BLOCK %]
- function validateAndSubmit() {
- var alert_text = '';
- $('label.required').each(function () {
- var id = $(this).attr('for');
- if (id && !isFilledOut(id)) {
- var desc = $(this).text() || id;
- alert_text += "Please select or enter a value for" +
- desc.replace(/[\r\n]+/, "").replace(/\s+/g, " ") + "\n";
- }
- });
- if (alert_text != '') {
- alert(alert_text);
- return false;
- }
- return true;
- }
-[% END %]
-
-[% PROCESS global/header.html.tmpl
- title = "Third Party Applications Issue Form"
- style = inline_css
- style_urls = [ 'skins/standard/enter_bug.css',
- 'skins/standard/attachment.css' ]
- javascript = inline_javascript
- javascript_urls = [ 'js/attachment.js',
- 'js/field.js',
- 'js/util.js',
- 'extensions/BMO/web/js/form_validate.js' ]
-%]
-
-<h2>Third Party Applications Issue Form</h2>
-
-<form method="post" action="post_bug.cgi" id="third_party_form"
- class="enter_bug_form" enctype="multipart/form-data"
- onSubmit="return validateAndSubmit();">
-<input type="hidden" name="format" value="third-party-apps">
-<input type="hidden" name="product" value="Marketplace">
-<input type="hidden" name="component" value="3rd Party Applications">
-<input type="hidden" name="rep_platform" value="All">
-<input type="hidden" name="op_sys" value="All">
-<input type="hidden" name="priority" value="--">
-<input type="hidden" name="version" value="unspecified">
-<input type="hidden" name="comment" id="comment" value="">
-<input type="hidden" name="status_whiteboard" id="status_whiteboard" value="">
-<input type="hidden" name="contenttypemethod" value="autodetect">
-<input type="hidden" name="token" value="[% token FILTER html %]">
-
-<div class="form_section">
- <label for="short_desc" class="field_label required">
- Summary
- </label>
- <div class="field_desc">
- Please enter the name of the application in brackets and a short summary of
- the issue. (ex. [App Name] Fails to launch at startup.
- </div>
- <input type="text" name="short_desc" id="short_desc" size="40" class="wide">
-</div>
-
-<div class="form_section">
- <label for="bug_file_loc" class="field_label required">
- Marketplace App URL
- </label>
- <div class="field_desc">
- Please copy the App listing page URL from Marketplace.
- </div>
- <input type="text" name="bug_file_loc" id="bug_file_loc" size="40" class="wide">
-</div>
-
-<div class="form_section">
- <label for="desc" class="field_label required">
- Detailed description of the issue or enhancement request
- </label>
- <div class="field_desc">
- Please enter a description of the issue with as much detail as possible.
- </div>
- <textarea name="desc" id="desc" rows="10" cols="60" class="wide"></textarea>
-</div>
-
-<div class="form_section">
- <label for="reproducible" class="field_label required">
- Is this issue reproducible?
- </label>
- <div class="field_desc">
- Please let us know if this issue is reproducible, steps to reproduction, and
- how often the issue presents itself.
- </div>
- <textarea name="reproducible" id="reproducible" rows="10" cols="60" class="wide"></textarea>
-</div>
-
-<div class="form_section">
- <label for="device_info" class="field_label required">
- Device Information
- </label>
- <div class="field_desc">
- Please let us know what device you were using when you experienced this
- issue. For FirefoxOS devices; please include the device make, model, and OS
- version at a minimum. RAM, Chipset, Screen size, and buildID is preferred
- if that information is available. (You can find BuildID by using this nav
- path on your FirefoxOS device; Settings—>Device info—>More info)
- </div>
- <textarea name="device_info" id="device_info" rows="10" cols="60" class="wide"></textarea>
-</div>
-
-<div class="form_section">
- <label for="reference_device" class="field_label required">
- Mozilla Reference Device
- </label>
- <div class="field_desc">
- Have you tried to reproduce this issue on a Mozilla reference device? If
- yes, please post your results, which device you were using, and OS version.
- </div>
- <textarea name="reference_device" id="reference_device" rows="10" cols="60" class="wide"></textarea>
-</div>
-
-<div class="form_section">
- <label class="field_label">
- Have additional materials?
- </label>
- <div class="field_desc">
- Please attach any additional information that will help us understand and
- diagnose this issue. Screenshots, testing documents, etc.
- </div>
- <table>
- <tr>
- <td>
- <label for="description" class="field_label">
- Description
- </label>
- </td>
- <td>
- <input type="text" name="description" id="description" size="40" class="wide">
- </td>
- </tr>
- <tr>
- <td>
- <label for="data" class="field_label">
- Filename
- </label>
- </th>
- <td>
- <input type="file" id="data" name="data" size="60">
- </td>
- </tr>
- </table>
-</div>
-
-<input type="submit" id="commit" value="Submit Request">
-
-</form>
-
-[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/created-fxos-betaprogram.html.tmpl b/extensions/BMO/template/en/default/bug/create/created-fxos-betaprogram.html.tmpl
deleted file mode 100644
index 145c976cd..000000000
--- a/extensions/BMO/template/en/default/bug/create/created-fxos-betaprogram.html.tmpl
+++ /dev/null
@@ -1,30 +0,0 @@
-[%# 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.
- #%]
-
-[% PROCESS global/variables.none.tmpl %]
-
-[% PROCESS global/header.html.tmpl
- title = "Firefox OS Beta Bug Submission"
-%]
-
-<h1>Thank you!</h1>
-
-<p>
- Thank you for submitting feedback about Firefox OS.
-</p>
-
-<p>
- We'll link any [% terms.bugs %] we file (or are already filed) as a result of
- this feedback to this report so you can be notified about their progress.
-</p>
-
-<p style="font-size: x-small">
- Reference: <a href="show_bug.cgi?id=[% id FILTER uri %]">#[% id FILTER html %]</a>
-</p>
-
-[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/custom_forms.none.tmpl b/extensions/BMO/template/en/default/bug/create/custom_forms.none.tmpl
index f0ddf0229..7b588f765 100644
--- a/extensions/BMO/template/en/default/bug/create/custom_forms.none.tmpl
+++ b/extensions/BMO/template/en/default/bug/create/custom_forms.none.tmpl
@@ -152,16 +152,6 @@ custom_forms = {
title => "Internet Public Policy Issue",
},
],
- "Marketplace" => [
- {
- link => "form.fxos.preload.app",
- title => "Firefox OS Pre-load App",
- },
- {
- link => "form.third.party",
- title => "Third Party Applications Issue Form",
- },
- ],
"Data Compliance" => [
{
link => "form.data.compliance",
diff --git a/extensions/BMO/template/en/default/email/bugmail.html.tmpl b/extensions/BMO/template/en/default/email/bugmail.html.tmpl
index d5cb3af9a..0b08e4a86 100644
--- a/extensions/BMO/template/en/default/email/bugmail.html.tmpl
+++ b/extensions/BMO/template/en/default/email/bugmail.html.tmpl
@@ -111,7 +111,10 @@
[% END %]
[% END %]
</ul>
+ Configure your email settings at
+ <a href="[% urlbase FILTER none %]userprefs.cgi?tab=email">[% urlbase FILTER none %]userprefs.cgi?tab=email</a>.
</div>
+
<div itemscope itemtype="http://schema.org/EmailMessage">
<div itemprop="action" itemscope itemtype="http://schema.org/ViewAction">
[%# Filtering of the URL param is not required & would break the URL when the comment anchor is set %]
diff --git a/extensions/BMO/template/en/default/global/choose-product.html.tmpl b/extensions/BMO/template/en/default/global/choose-product.html.tmpl
index 4329b716a..679d812e1 100644
--- a/extensions/BMO/template/en/default/global/choose-product.html.tmpl
+++ b/extensions/BMO/template/en/default/global/choose-product.html.tmpl
@@ -91,10 +91,22 @@
icon="firefox_ios.png"
%]
[% INCLUDE easyproduct
+ name="DevTools"
+ icon="devtools.png"
+ %]
+ [% INCLUDE easyproduct
+ name="WebExtensions"
+ icon="webextensions.png"
+ %]
+ [% INCLUDE easyproduct
name="Toolkit"
icon="component.png"
%]
[% INCLUDE easyproduct
+ name="Mozilla Localizations"
+ icon="localization.png"
+ %]
+ [% INCLUDE easyproduct
name="Thunderbird"
icon="thunderbird.png"
%]
@@ -103,10 +115,6 @@
icon="seamonkey.png"
%]
[% INCLUDE easyproduct
- name="Mozilla Localizations"
- icon="localization.png"
- %]
- [% INCLUDE easyproduct
name="Data Platform and Tools"
icon="sync.png"
%]
@@ -173,12 +181,14 @@
</section>
</div>
+[% IF NOT is_describe %]
<div id="guided">
<a href="enter_bug.cgi?format=guided">
<img src="extensions/BMO/web/images/guided.png" width="16" height="16">
Switch to the [% terms.Bugzilla %] Helper</a> |
<a href="page.cgi?id=custom_forms.html">Custom [% terms.bug %] entry forms</a>
</div>
+[% END %]
[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/hook/attachment/createformcontents-mimetypes.html.tmpl b/extensions/BMO/template/en/default/hook/attachment/createformcontents-mimetypes.html.tmpl
deleted file mode 100644
index 3dc727b87..000000000
--- a/extensions/BMO/template/en/default/hook/attachment/createformcontents-mimetypes.html.tmpl
+++ /dev/null
@@ -1,2 +0,0 @@
-[% mimetypes.push({type => "image/svg+xml", desc => "SVG image"}) %]
-[% mimetypes.push({type => "application/vnd.mozilla.xul+xml", desc => "XUL"}) %] \ No newline at end of file
diff --git a/extensions/BMO/template/en/default/pages/attachment_bounty_form.html.tmpl b/extensions/BMO/template/en/default/pages/attachment_bounty_form.html.tmpl
index faf32aa36..0f7966097 100644
--- a/extensions/BMO/template/en/default/pages/attachment_bounty_form.html.tmpl
+++ b/extensions/BMO/template/en/default/pages/attachment_bounty_form.html.tmpl
@@ -211,12 +211,12 @@ function validateAndSubmit() {
</div>
<div class="form_section">
- <label for="credit_2" class="field_label">Credit</label>
+ <label for="credit_2" class="field_label">Credit Twitter</label>
<input type="text" name="credit_2" id="credit_2" size="80" value="[% form.credit.1 FILTER html %]">
</div>
<div class="form_section">
- <label for="credit_3" class="field_label">Credit</label>
+ <label for="credit_3" class="field_label">Credit URL</label>
<input type="text" name="credit_3" id="credit_3" size="80" value="[% form.credit.2 FILTER html %]">
</div>
diff --git a/extensions/BMO/web/producticons/devtools.png b/extensions/BMO/web/producticons/devtools.png
new file mode 100644
index 000000000..9800f654b
--- /dev/null
+++ b/extensions/BMO/web/producticons/devtools.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/webextensions.png b/extensions/BMO/web/producticons/webextensions.png
new file mode 100644
index 000000000..7e5503d97
--- /dev/null
+++ b/extensions/BMO/web/producticons/webextensions.png
Binary files differ
diff --git a/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
index 38b6b572f..b9a42caf3 100644
--- a/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
+++ b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
@@ -83,12 +83,7 @@
[%# update last-visited %]
[% IF user.id && user.is_involved_in_bug(bug) %]
- $(function() {
- bugzilla_ajax({
- url: 'rest/bug_user_last_visit/[% bug.id FILTER none %]',
- type: 'POST'
- });
- });
+ document.addEventListener('DOMContentLoaded', () => show_new_changes_indicator(), { once: true });
[% END %]
[%# expose useful data to js %]
@@ -100,6 +95,7 @@
is_insider: [% user.is_insider ? "true" : "false" %],
is_timetracker: [% user.is_timetracker ? "true" : "false" %],
can_tag: [% user.can_tag_comments ? "true" : "false" %],
+ timezone: '[% user.timezone.name FILTER js %]',
settings: {
quote_replies: '[% user.settings.quote_replies.value FILTER js %]',
zoom_textareas: [% user.settings.zoom_textareas.value == "on" ? "true" : "false" %],
diff --git a/extensions/BugModal/template/en/default/bug_modal/user.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/user.html.tmpl
index cd05d053f..9eda7b936 100644
--- a/extensions/BugModal/template/en/default/bug_modal/user.html.tmpl
+++ b/extensions/BugModal/template/en/default/bug_modal/user.html.tmpl
@@ -46,7 +46,7 @@ END;
href="mailto:[% u.email FILTER html %]"
data-user-email="[% u.email FILTER html %]"
data-user-id="[% u.id FILTER html %]"
- data-show-edit="[% user.in_group('editusers') || user.bless_groups.size > 0 ? 1 : 0 %]"
+ data-show-edit="[% user.in_group('editusers') || user.in_group('disableusers') || user.bless_groups.size > 0 ? 1 : 0 %]"
title="[% u.identity FILTER html %]"
[% ELSE %]
href="user_profile?user_id=[% u.id FILTER none %]"
diff --git a/extensions/BugModal/web/bug_modal.css b/extensions/BugModal/web/bug_modal.css
index 65aeec86d..a8c469ad6 100644
--- a/extensions/BugModal/web/bug_modal.css
+++ b/extensions/BugModal/web/bug_modal.css
@@ -153,6 +153,12 @@ a.activity-ref {
font-weight: normal;
padding-right: 5px;
color: #666;
+ opacity: 1;
+ transition: all .2s;
+}
+
+.module-spinner[aria-expanded="true"] ~ .module-subtitle {
+ opacity: 0;
}
.module .fields-lhs {
@@ -532,6 +538,43 @@ td.flag-requestee {
text-align: right;
}
+.new-changes-link {
+ margin: 8px 0;
+ border-radius: 4px;
+ padding: 4px;
+ font-size: 12px;
+ text-align: center;
+ color: #FFF;
+ background: #277AC1;
+ cursor: pointer;
+}
+
+.new-changes-separator {
+ position: relative;
+ margin: 16px -8px;
+ height: 0;
+ border-top: 1px solid #C00;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ user-select: none;
+}
+
+.new-changes-separator span {
+ display: inline-block;
+ position: absolute;
+ top: -10px;
+ right: 16px;
+ border: 1px solid #CCC;
+ border-radius: 4px;
+ padding: 0 4px;
+ height: 16px;
+ font-size: 10px;
+ line-height: 16px;
+ text-transform: uppercase;
+ color: #C00;
+ background-color: #FFF;
+}
+
.change-set {
clear: both;
-webkit-box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
@@ -626,6 +669,11 @@ body.platform-Win32 .comment-text, body.platform-Win64 .comment-text {
width: 99% !important;
}
+.comment-text.bz_private {
+ color: darkred;
+ border: 1px dashed darkred;
+}
+
.comment-tags {
padding: 0 8px 2px 8px !important;
}
diff --git a/extensions/BugModal/web/bug_modal.js b/extensions/BugModal/web/bug_modal.js
index c6abb5d7a..4a770e66c 100644
--- a/extensions/BugModal/web/bug_modal.js
+++ b/extensions/BugModal/web/bug_modal.js
@@ -96,6 +96,54 @@ $(function() {
$('#editing').val('');
}
+ function saveBugComment(text) {
+ if (text.length < 1) return clearSavedBugComment();
+ if (text.length > 65535) return;
+ let key = `bug-modal-saved-comment-${BUGZILLA.bug_id}`;
+ let value = {
+ text: text,
+ savedAt: Date.now()
+ };
+ localStorage.setItem(key, JSON.stringify(value));
+ }
+
+ function clearSavedBugComment() {
+ let key = `bug-modal-saved-comment-${BUGZILLA.bug_id}`;
+ localStorage.removeItem(key);
+ }
+
+ function restoreSavedBugComment() {
+ expireSavedComments();
+ let key = `bug-modal-saved-comment-${BUGZILLA.bug_id}`;
+ let value = JSON.parse(localStorage.getItem(key));
+ if (value){
+ let commentBox = document.querySelector("textarea#comment");
+ commentBox.value = value['text'];
+ if (BUGZILLA.user.settings.autosize_comments) {
+ autosize.update(commentBox);
+ }
+ }
+ }
+
+ function expireSavedComments() {
+ const AGE_THRESHOLD = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds.
+ let expiredKeys = [];
+ for (let i = 0; i < localStorage.length; i++) {
+ let key = localStorage.key(i);
+ if (key.match(/^bug-modal-saved-comment-/)) {
+ let value = JSON.parse(localStorage.getItem(key));
+ let savedAt = value['savedAt'] || 0;
+ let age = Date.now() - savedAt;
+ if (age < 0 || age > AGE_THRESHOLD) {
+ expiredKeys.push(key);
+ }
+ }
+ }
+ expiredKeys.forEach((key) => {
+ localStorage.removeItem(key);
+ });
+ }
+
// expand/colapse module
$('.module-latch')
.click(function(event) {
@@ -309,7 +357,9 @@ $(function() {
// execCommand("copy") only works on selected text
$('#clip-container').show();
$('#clip').val(clipboardSummary()).select();
- document.execCommand("copy");
+ $('#floating-message-text')
+ .text(document.execCommand("copy") ? 'Bug summary copied!' : 'Couldn’t copy bug summary');
+ $('#floating-message').fadeIn(250).delay(2500).fadeOut();
$('#clip-container').hide();
});
}
@@ -509,6 +559,8 @@ $(function() {
keywords = data.keywords;
$('#keywords')
.devbridgeAutocomplete({
+ appendTo: $('#main-inner'),
+ forceFixPosition: true,
lookup: function(query, done) {
query = query.toLowerCase();
var matchStart =
@@ -586,6 +638,8 @@ $(function() {
.toArray()
.join(' ')
);
+
+ clearSavedBugComment();
})
.attr('disabled', false);
@@ -1272,9 +1326,18 @@ $(function() {
}
});
+ // Save comments in progress
+ $('#comment')
+ .on('input', function(event) {
+ saveBugComment(event.target.value);
+ });
+
// finally switch to edit mode if we navigate back to a page that was editing
$(window).on('pageshow', restoreEditMode);
+ $(window).on('pageshow', restoreSavedBugComment);
+ $(window).on('focus', restoreSavedBugComment);
restoreEditMode();
+ restoreSavedBugComment();
});
function confirmUnsafeURL(url) {
@@ -1283,6 +1346,93 @@ function confirmUnsafeURL(url) {
'The full URL is:\n\n' + url + '\n\nContinue?');
}
+function show_new_changes_indicator() {
+ const url = `rest/bug_user_last_visit/${BUGZILLA.bug_id}`;
+
+ // Get the last visited timestamp
+ bugzilla_ajax({ url }, data => {
+ // Save the current timestamp
+ bugzilla_ajax({ url, type: 'POST' });
+
+ if (!data[0] || !data[0].last_visit_ts) {
+ return;
+ }
+
+ const last_visit_ts = new Date(data[0].last_visit_ts);
+ const new_changes = [...document.querySelectorAll('main .change-set')].filter($change => {
+ // Exclude hidden CC changes
+ return $change.clientHeight > 0 &&
+ new Date($change.querySelector('[data-time]').getAttribute('data-time') * 1000) > last_visit_ts;
+ });
+
+ if (new_changes.length === 0) {
+ return;
+ }
+
+ const now = new Date();
+ const date_locale = document.querySelector('html').lang;
+ const date_options = {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+ hour12: false,
+ timeZone: BUGZILLA.user.timezone,
+ timeZoneName: 'short',
+ };
+
+ if (last_visit_ts.getFullYear() === now.getFullYear()) {
+ delete date_options.year;
+
+ if (last_visit_ts.getMonth() === now.getMonth() && last_visit_ts.getDate() === now.getDate()) {
+ delete date_options.month;
+ delete date_options.day;
+ }
+ }
+
+ const $link = document.createElement('div');
+ const $separator = document.createElement('div');
+ const comments_count = new_changes.filter($change => !!$change.querySelector('.comment')).length;
+ const changes_count = new_changes.length - comments_count;
+ const date_attr = last_visit_ts.toISOString();
+ const date_label = last_visit_ts.toLocaleString(date_locale, date_options);
+
+ // Insert a link
+ $link.className = 'new-changes-link';
+ $link.innerHTML =
+ (c => c === 0 ? '' : (c === 1 ? `${c} new comment` : `${c} new comments`))(comments_count) +
+ (comments_count > 0 && changes_count > 0 ? ', ' : '') +
+ (c => c === 0 ? '' : (c === 1 ? `${c} new change` : `${c} new changes`))(changes_count) +
+ ` since <time datetime="${date_attr}">${date_label}</time>`;
+ $link.addEventListener('click', () => {
+ $link.remove();
+ scroll_element_into_view($separator);
+ }, { once: true });
+ document.querySelector('#changeform').insertAdjacentElement('beforebegin', $link);
+
+ // Insert a separator
+ $separator.className = 'new-changes-separator';
+ $separator.innerHTML = '<span>New</span>';
+ new_changes[0].insertAdjacentElement('beforebegin', $separator);
+
+ // Remove the link once the separator goes into the viewport
+ if ('IntersectionObserver' in window) {
+ const observer = new IntersectionObserver(entries => entries.forEach(entry => {
+ if (entry.intersectionRatio > 0) {
+ observer.unobserve($separator);
+ $link.remove();
+ }
+ }), { root: document.querySelector('#bugzilla-body') });
+
+ observer.observe($separator);
+ }
+
+ // TODO: Enable auto-scroll once the modal page layout is optimized
+ // scroll_element_into_view($separator);
+ });
+}
+
// fix url after bug creation/update
if (history && history.replaceState) {
var href = document.location.href;
diff --git a/extensions/BugModal/web/comments.js b/extensions/BugModal/web/comments.js
index 04894506e..85c5a2368 100644
--- a/extensions/BugModal/web/comments.js
+++ b/extensions/BugModal/web/comments.js
@@ -355,6 +355,8 @@ $(function() {
$('#ctag-add')
.devbridgeAutocomplete({
+ appendTo: $('#main-inner'),
+ forceFixPosition: true,
serviceUrl: function(query) {
return 'rest/bug/comment/tags/' + encodeURIComponent(query);
},
diff --git a/extensions/BugModal/web/common_bug_modal.js b/extensions/BugModal/web/common_bug_modal.js
index dc91824f6..6cd658045 100644
--- a/extensions/BugModal/web/common_bug_modal.js
+++ b/extensions/BugModal/web/common_bug_modal.js
@@ -509,6 +509,8 @@ $(function() {
keywords = data.keywords;
$('#keywords')
.devbridgeAutocomplete({
+ appendTo: $('#main-inner'),
+ forceFixPosition: true,
lookup: function(query, done) {
query = query.toLowerCase();
var matchStart =
diff --git a/extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl b/extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl
index 36a8db6f2..5c372db3c 100644
--- a/extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl
+++ b/extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl
@@ -105,6 +105,7 @@
<div id="query_container">
<div class="query_heading"></div>
<div class="query_description"></div>
+ <span id="query_loading" class="items_found">Loading...</span>
<span id="query_count_refresh" class="bz_default_hidden">
<span class="items_found" id="query_bugs_found">0 [% terms.bugs %] found</span>
| <a class="refresh" href="javascript:void(0);" id="query_refresh">Refresh</a>
@@ -126,29 +127,29 @@
%]
</div>
- <div id="requestee_container">
- <div class="query_heading">
- Flags Requested of You
+ [% BLOCK requests_table %]
+ <div id="[% name FILTER html %]_container" class="requests">
+ <div class="query_heading">[% title FILTER html_light %]</div>
+ <span id="[% name FILTER html %]_loading" class="items_found">Loading...</span>
+ <span id="[% name FILTER html %]_count_refresh" class="bz_default_hidden">
+ <span class="items_found" id="[% name FILTER html %]_flags_found">0 reviews found</span>
+ | <a class="refresh" href="javascript:void(0);" id="[% name FILTER html %]_refresh">Refresh</a>
+ | <a class="buglist" href="javascript:void(0);" id="[% name FILTER html %]_buglist">Buglist</a>
+ </span>
+ <div id="[% name FILTER html %]_table"></div>
</div>
- <span id="requestee_count_refresh" class="bz_default_hidden">
- <span class="items_found" id="requestee_flags_found">0 flags found</span>
- | <a class="refresh" href="javascript:void(0);" id="requestee_refresh">Refresh</a>
- | <a class="buglist" href="javascript:void(0);" id="requestee_buglist">Buglist</a>
- </span>
- <div id="requestee_table"></div>
- </div>
+ [% END %]
- <div id="requester_container">
- <div class="query_heading">
- Flags You Have Requested
- </div>
- <span id="requester_count_refresh" class="bz_default_hidden">
- <span class="items_found" id="requester_flags_found">0 flags found</span>
- | <a class="refresh" href="javascript:void(0);" id="requester_refresh">Refresh</a>
- | <a class="buglist" href="javascript:void(0);" id="requester_buglist">Buglist</a>
- </span>
- <div id="requester_table"></div>
- </div>
+ [% ## no-008filter
+ # requires PhabBugz extension
+ IF Param('phabricator_enabled');
+ title = '<a href="' _ Param('phabricator_base_uri') _ '">Phabricator</a> Reviews Requested of You';
+ PROCESS requests_table name='reviews' title=title;
+ END;
+
+ PROCESS requests_table name='requestee' title='Flags Requested of You';
+ PROCESS requests_table name='requester' title='Flags You Have Requested';
+ %]
</div>
<div style="clear:both;"></div>
[% IF user.showmybugslink OR user.queries.size OR user.queries_subscribed.size %]
diff --git a/extensions/MyDashboard/web/js/flags.js b/extensions/MyDashboard/web/js/flags.js
index 95b256708..425e42e57 100644
--- a/extensions/MyDashboard/web/js/flags.js
+++ b/extensions/MyDashboard/web/js/flags.js
@@ -16,15 +16,18 @@ $(function () {
// Common
var counter = 0;
var dataSource = {
+ reviews: null,
requestee: null,
requester: null
};
var dataTable = {
+ reviews: null,
requestee: null,
requester: null
};
+ var hasReviews = !!document.getElementById('reviews_container');
- var updateFlagTable = function(type) {
+ var updateRequestsTable = function(type) {
if (!type) return;
counter = counter + 1;
@@ -32,32 +35,40 @@ $(function () {
var callback = {
success: function(e) {
if (e.response) {
+ Y.one('#' + type + '_loading').addClass('bz_default_hidden');
Y.one('#' + type + '_count_refresh').removeClass('bz_default_hidden');
Y.one("#" + type + "_flags_found").setHTML(
- e.response.results.length + ' flags found');
+ e.response.results.length +
+ ' request' + (e.response.results.length == 1 ? '' : 's') +
+ ' found');
dataTable[type].set('data', e.response.results);
}
},
failure: function(o) {
- if (o.error) {
- alert("Failed to load flag list from Bugzilla:\n\n" + o.error.message);
+ Y.one('#' + type + '_loading').addClass('bz_default_hidden');
+ Y.one('#' + type + '_count_refresh').removeClass('bz_default_hidden');
+ if (o.error && o.error.message) {
+ alert("Failed to load requests:\n\n" + o.error.message);
} else {
- alert("Failed to load flag list from Bugzilla.");
+ alert("Failed to load requests.");
}
}
};
+ var method = type === 'reviews' ? 'PhabBugz.needs_review' : 'MyDashboard.run_flag_query';
var json_object = {
version: "1.1",
- method: "MyDashboard.run_flag_query",
+ method: method,
id: counter,
- params: { type : type,
- Bugzilla_api_token : (BUGZILLA.api_token ? BUGZILLA.api_token : '')
+ params: {
+ type : type,
+ Bugzilla_api_token : (BUGZILLA.api_token ? BUGZILLA.api_token : '')
}
};
var stringified = Y.JSON.stringify(json_object);
+ Y.one('#' + type + '_loading').removeClass('bz_default_hidden');
Y.one('#' + type + '_count_refresh').addClass('bz_default_hidden');
dataTable[type].set('data', []);
@@ -86,14 +97,17 @@ $(function () {
};
var bugLinkFormatter = function(o) {
+ if (!o.data.bug_id) {
+ return '-';
+ }
var bug_closed = "";
if (o.data.bug_status == 'RESOLVED' || o.data.bug_status == 'VERIFIED') {
bug_closed = "bz_closed";
}
- return '<a href="show_bug.cgi?id=' + encodeURIComponent(o.value) +
+ return '<a href="show_bug.cgi?id=' + encodeURIComponent(o.data.bug_id) +
'" target="_blank" ' + 'title="' + Y.Escape.html(o.data.bug_status) + ' - ' +
- Y.Escape.html(o.data.bug_summary) + '" class="' + Y.Escape.html(bug_closed) +
- '">' + o.value + '</a>';
+ Y.Escape.html(o.data.bug_summary) + '" class="' + bug_closed +
+ '">' + o.data.bug_id + '</a>';
};
var updatedFormatter = function(o) {
@@ -124,6 +138,83 @@ $(function () {
}
};
+ var phabAuthorFormatter = function(o) {
+ return '<span title="' + Y.Escape.html(o.data.author_email) + '">' +
+ Y.Escape.html(o.data.author_name) + '</span>';
+ };
+
+ var phabRowFormatter = function(o) {
+ var row = o.cell.ancestor();
+
+ // space in the 'flags' tables is tight
+ // render requests as two rows - diff title on first row, columns
+ // on second
+
+ row.insert(
+ '<tr class="' + row.getAttribute('class') + '">' +
+ '<td class="yui3-datatable-cell" colspan="4">' +
+ '<a href="' + o.data.url + '" target="_blank">' +
+ Y.Escape.html('D' + o.data.id + ' - ' + o.data.title) +
+ '</a></td></tr>',
+ 'before');
+
+ o.cell.set('text', o.data.status == 'added' ? 'pending' : o.data.status);
+
+ return false;
+ };
+
+ // Reviews
+ if (hasReviews) {
+ dataSource.reviews = new Y.DataSource.IO({ source: 'jsonrpc.cgi' });
+ dataSource.reviews.on('error', function(e) {
+ console.log(e);
+ try {
+ var response = Y.JSON.parse(e.data.responseText);
+ if (response.error)
+ e.error.message = response.error.message;
+ } catch(ex) {
+ // ignore
+ }
+ });
+ dataTable.reviews = new Y.DataTable({
+ columns: [
+ { key: 'author_email', label: 'Requester', sortable: true,
+ formattter: phabAuthorFormatter, allowHTML: true },
+ { key: 'bug_id', label: 'Bug', sortable: true,
+ formatter: bugLinkFormatter, allowHTML: true },
+ { key: 'updated', label: 'Updated', sortable: true,
+ formatter: updatedFormatter, allowHTML: true }
+ ],
+ strings: {
+ emptyMessage: 'No review requests.',
+ }
+ });
+
+ dataTable.reviews.plug(Y.Plugin.DataTableSort);
+
+ dataTable.reviews.plug(Y.Plugin.DataTableDataSource, {
+ datasource: dataSource.reviews
+ });
+
+ dataSource.reviews.plug(Y.Plugin.DataSourceJSONSchema, {
+ schema: {
+ resultListLocator: 'result.result',
+ resultFields: [ 'author_email', 'author_name', 'bug_id',
+ 'bug_status', 'bug_summary', 'id', 'status', 'title',
+ 'updated', 'updated_fancy', 'url' ]
+ }
+ });
+
+ dataTable.reviews.render("#reviews_table");
+
+ Y.one('#reviews_refresh').on('click', function(e) {
+ updateRequestsTable('reviews');
+ });
+ Y.one('#reviews_buglist').on('click', function(e) {
+ loadBugList('reviews');
+ });
+ }
+
// Requestee
dataSource.requestee = new Y.DataSource.IO({ source: 'jsonrpc.cgi' });
dataSource.requestee.on('error', function(e) {
@@ -146,7 +237,7 @@ $(function () {
formatter: updatedFormatter, allowHTML: true }
],
strings: {
- emptyMessage: 'No flag data found.',
+ emptyMessage: 'No flags requested of you.',
}
});
@@ -167,7 +258,7 @@ $(function () {
dataTable.requestee.render("#requestee_table");
Y.one('#requestee_refresh').on('click', function(e) {
- updateFlagTable('requestee');
+ updateRequestsTable('requestee');
});
Y.one('#requestee_buglist').on('click', function(e) {
loadBugList('requestee');
@@ -196,7 +287,7 @@ $(function () {
formatter: updatedFormatter, allowHTML: true }
],
strings: {
- emptyMessage: 'No flag data found.',
+ emptyMessage: 'No requested flags found.',
}
});
@@ -214,19 +305,24 @@ $(function () {
}
});
- // Initial load
- Y.on("contentready", function (e) {
- updateFlagTable("requestee");
- }, "#requestee_table");
- Y.on("contentready", function (e) {
- updateFlagTable("requester");
- }, "#requester_table");
-
Y.one('#requester_refresh').on('click', function(e) {
- updateFlagTable('requester');
+ updateRequestsTable('requester');
});
Y.one('#requester_buglist').on('click', function(e) {
loadBugList('requester');
});
+
+ // Initial load
+ if (hasReviews) {
+ Y.on("contentready", function (e) {
+ updateRequestsTable('reviews');
+ }, "#reviews_table");
+ }
+ Y.on("contentready", function (e) {
+ updateRequestsTable("requestee");
+ }, "#requestee_table");
+ Y.on("contentready", function (e) {
+ updateRequestsTable("requester");
+ }, "#requester_table");
});
});
diff --git a/extensions/MyDashboard/web/js/query.js b/extensions/MyDashboard/web/js/query.js
index a95c0be61..e5e0979a1 100644
--- a/extensions/MyDashboard/web/js/query.js
+++ b/extensions/MyDashboard/web/js/query.js
@@ -77,6 +77,7 @@ $(function() {
var bugQueryCallback = {
success: function(e) {
if (e.response) {
+ Y.one('#query_loading').addClass('bz_default_hidden');
Y.one('#query_count_refresh').removeClass('bz_default_hidden');
Y.one("#query_container .query_description").setHTML(e.response.meta.description);
Y.one("#query_container .query_heading").setHTML(e.response.meta.heading);
@@ -100,6 +101,8 @@ $(function() {
}
},
failure: function(o) {
+ Y.one('#query_loading').addClass('bz_default_hidden');
+ Y.one('#query_count_refresh').removeClass('bz_default_hidden');
if (o.error) {
alert("Failed to load bug list from Bugzilla:\n\n" + o.error.message);
} else {
@@ -114,6 +117,7 @@ $(function() {
counter = counter + 1;
lastChangesCache = {};
+ Y.one('#query_loading').removeClass('bz_default_hidden');
Y.one('#query_count_refresh').addClass('bz_default_hidden');
bugQueryTable.set('data', []);
bugQueryTable.render("#query_table");
@@ -173,6 +177,9 @@ $(function() {
{ key: "bug_status", label: "Status", sortable: true },
{ key: "short_desc", label: "Summary", sortable: true },
],
+ strings: {
+ emptyMessage: 'Zarro Boogs found'
+ }
});
var last_changes_source = Y.one('#last-changes-template').getHTML(),
diff --git a/extensions/MyDashboard/web/styles/mydashboard.css b/extensions/MyDashboard/web/styles/mydashboard.css
index 1011a9143..ef34bb100 100644
--- a/extensions/MyDashboard/web/styles/mydashboard.css
+++ b/extensions/MyDashboard/web/styles/mydashboard.css
@@ -18,10 +18,13 @@
white-space: nowrap;
}
+#mydashboard .requests {
+ margin-bottom: 2em;
+}
+
.query_heading {
font-size: 18px;
font-weight: 600;
- margin: 5px 0;
color: rgb(72, 72, 72);
}
diff --git a/extensions/OpenGraph/template/en/default/hook/global/header-start.html.tmpl b/extensions/OpenGraph/template/en/default/hook/global/header-start.html.tmpl
index 2d7c3379f..51c388d42 100644
--- a/extensions/OpenGraph/template/en/default/hook/global/header-start.html.tmpl
+++ b/extensions/OpenGraph/template/en/default/hook/global/header-start.html.tmpl
@@ -13,6 +13,9 @@
[% IF bug %]
<meta property="og:description"
content="[% bug.bug_status FILTER html %] ([% bug.assigned_to.login FILTER email FILTER html %]) in [% bug.product FILTER html %] - [% bug.component FILTER html %]. Last updated [% bug.delta_ts FILTER time('%Y-%m-%d') %].">
-[% ELSE %]
-<meta property="og:image" content="[% urlbase FILTER none %]extensions/OpenGraph/web/moz-social-bw-rgb-1200x1200.png">
+[% ELSIF error_message %]
+<meta property="og:description" content="[% error_message FILTER txt FILTER html %]">
+[% END %]
+[% IF og_image %]
+<meta property="og:image" content="[% urlbase FILTER none %][% og_image FILTER html %]">
[% END %]
diff --git a/extensions/OrangeFactor/Extension.pm b/extensions/OrangeFactor/Extension.pm
index ab2f1d749..56dd5dc6e 100644
--- a/extensions/OrangeFactor/Extension.pm
+++ b/extensions/OrangeFactor/Extension.pm
@@ -17,6 +17,8 @@ use Bugzilla::User::Setting;
use Bugzilla::Constants;
use Bugzilla::Attachment;
+use DateTime;
+
our $VERSION = '1.0';
sub template_before_process {
@@ -38,6 +40,8 @@ sub template_before_process {
my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'};
if ($bug && grep($_->name eq 'intermittent-failure', @{ $bug->keyword_objects })) {
$vars->{'orange_factor'} = 1;
+ $vars->{'date_start'} = ( DateTime->now() - DateTime::Duration->new( days => 7 ) )->ymd();
+ $vars->{'date_end'} = DateTime->now->ymd();
}
}
diff --git a/extensions/OrangeFactor/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/OrangeFactor/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
index a41188a63..1eedab479 100644
--- a/extensions/OrangeFactor/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
+++ b/extensions/OrangeFactor/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
@@ -17,9 +17,14 @@
<td>
[% IF cgi.user_agent.match('(?i)gecko') %]
<canvas id="orange-graph" class="bz_default_hidden"></canvas>
- <span id="orange-count"></span>
+ <span id="orange-count"
+ style="display:none;"
+ data-date-start="[% date_start FILTER html %]"
+ data-date-end="[% date_end FILTER html %]"
+ data-bug-id="[% bug.bug_id FILTER html %]">
+ </span>
[% END %]
- (<a href="https://brasstacks.mozilla.com/orangefactor/?display=Bug&bugid=[% bug.bug_id FILTER uri %]"
+ (<a href="https://treeherder.mozilla.org/intermittent-failures.html#/bugdetails?startday=[% date_start FILTER uri %]&endday=[% date_end FILTER uri %]&tree=trunk&bug=[% bug.bug_id FILTER uri %]"
title="Click to load Orange Factor page for this [% terms.bug %]">link</a>)
</td>
</tr>
diff --git a/extensions/OrangeFactor/template/en/default/hook/bug_modal/edit-details_rhs.html.tmpl b/extensions/OrangeFactor/template/en/default/hook/bug_modal/edit-details_rhs.html.tmpl
index 30cd65cd2..d6ee5f127 100644
--- a/extensions/OrangeFactor/template/en/default/hook/bug_modal/edit-details_rhs.html.tmpl
+++ b/extensions/OrangeFactor/template/en/default/hook/bug_modal/edit-details_rhs.html.tmpl
@@ -16,10 +16,15 @@
%]
[% IF cgi.user_agent.match('(?i)gecko') %]
<canvas id="orange-graph" style="display:none;"></canvas>
- <span id="orange-count" style="display:none;"></span>
+ <span id="orange-count"
+ style="display:none;"
+ data-date-start="[% date_start FILTER html %]"
+ data-date-end="[% date_end FILTER html %]"
+ data-bug-id="[% bug.bug_id FILTER html %]">
+ </span>
[% END %]
<span id="orange-link">
- (<a href="https://brasstacks.mozilla.com/orangefactor/?display=Bug&bugid=[% bug.bug_id FILTER uri %]"
+ (<a href="https://treeherder.mozilla.org/intermittent-failures.html#/bugdetails?startday=[% date_start FILTER uri %]&endday=[% date_end FILTER uri %]&tree=trunk&bug=[% bug.bug_id FILTER uri %]"
title="Click to load Orange Factor page for this [% terms.bug %]">link</a>)
</span>
[% END %]
diff --git a/extensions/OrangeFactor/web/js/orange_factor.js b/extensions/OrangeFactor/web/js/orange_factor.js
index fa9411cf8..78fbb5eb3 100644
--- a/extensions/OrangeFactor/web/js/orange_factor.js
+++ b/extensions/OrangeFactor/web/js/orange_factor.js
@@ -8,21 +8,17 @@
$(function() {
'use strict';
- var dayMs = 24 * 60 * 60 * 1000;
- var limit = 7;
function getOrangeCount(data) {
- data = data.oranges;
- var total = 0,
- days = [],
- date = getCurrentDateMs() - limit * dayMs;
- for(var i = 0; i < limit; i++) {
- var iso = dateString(new Date(date));
- var count = data[iso] ? data[iso].orangecount : 0;
- days.push(count);
- total += count;
- date += dayMs;
- }
+ let days = [];
+ let total = 0;
+
+ data.forEach(entry => {
+ let failureCount = entry["failure_count"];
+ days.push(failureCount);
+ total += failureCount;
+ });
+
displayGraph(days);
displayCount(total);
}
@@ -53,39 +49,26 @@ $(function() {
$('#orange-count').text(count + ' failures on trunk in the past week');
}
- function dateString(date) {
- function norm(part) {
- return JSON.stringify(part).length == 2 ? part : '0' + part;
- }
- return date.getFullYear()
- + "-" + norm(date.getMonth() + 1)
- + "-" + norm(date.getDate());
- }
-
- function getCurrentDateMs() {
- var d = new Date;
- return d.getTime();
- };
-
function orangify() {
- $('#orange-count')
- .text('Loading...')
- .show();
- var bugId = document.forms['changeform'].id.value;
- var request = {
+ let $orangeCount = $('#orange-count');
+ let queryParams = $.param({
+ bug: $orangeCount.data('bug-id'),
+ startday: $orangeCount.data('date-start'),
+ endday: $orangeCount.data('date-end'),
+ tree: 'trunk'
+ });
+ let request = {
dataType: "json",
- xhrFields: {
- withCredentials: true
- },
- url: "https://brasstacks.mozilla.com/orangefactor/api/count?" +
- "bugid=" + encodeURIComponent(bugId) + "&tree=trunk"
+ url: `https://treeherder.mozilla.org/api/failurecount/?${queryParams}`
};
+
+ $orangeCount.text('Loading...').show();
$.ajax(request)
.done(function(data) {
getOrangeCount(data);
})
.fail(function() {
- $('#orange-count').text('Please sign into OrangeFactor first');
+ $orangeCount.text('Unable to load OrangeFactor at this time.');
});
}
diff --git a/extensions/PhabBugz/Extension.pm b/extensions/PhabBugz/Extension.pm
index ee96901a2..c857c60ab 100644
--- a/extensions/PhabBugz/Extension.pm
+++ b/extensions/PhabBugz/Extension.pm
@@ -18,11 +18,6 @@ use Bugzilla::Extension::PhabBugz::Feed;
our $VERSION = '0.01';
-BEGIN {
- *Bugzilla::User::phab_phid = sub { return $_[0]->{phab_phid}; };
- *Bugzilla::User::phab_review_status = sub { return $_[0]->{phab_review_status}; };
-}
-
sub config_add_panels {
my ($self, $args) = @_;
my $modules = $args->{panel_modules};
@@ -85,7 +80,7 @@ sub install_filesystem {
my $files = $args->{'files'};
my $extensionsdir = bz_locations()->{'extensionsdir'};
- my $scriptname = $extensionsdir . "/PhabBugz/bin/phabbugzd.pl";
+ my $scriptname = $extensionsdir . "/PhabBugz/bin/phabbugz_feed.pl";
$files->{$scriptname} = {
perms => Bugzilla::Install::Filesystem::WS_EXECUTE
diff --git a/extensions/PhabBugz/bin/update_project_members.pl b/extensions/PhabBugz/bin/update_project_members.pl
deleted file mode 100755
index fe62170a6..000000000
--- a/extensions/PhabBugz/bin/update_project_members.pl
+++ /dev/null
@@ -1,99 +0,0 @@
-#!/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 lib qw(. lib local/lib/perl5);
-
-use Bugzilla;
-BEGIN { Bugzilla->extensions() }
-
-use Bugzilla::Constants;
-use Bugzilla::Error;
-use Bugzilla::Group;
-
-use Bugzilla::Extension::PhabBugz::Project;
-use Bugzilla::Extension::PhabBugz::Util qw(
- get_phab_bmo_ids
-);
-
-Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
-
-my ($phab_uri, $phab_sync_groups);
-
-if (!Bugzilla->params->{phabricator_enabled}) {
- exit;
-}
-
-# Sanity checks
-unless ($phab_uri = Bugzilla->params->{phabricator_base_uri}) {
- ThrowUserError('invalid_phabricator_uri');
-}
-
-unless ($phab_sync_groups = Bugzilla->params->{phabricator_sync_groups}) {
- ThrowUserError('invalid_phabricator_sync_groups');
-}
-
-# Loop through each group and perform the following:
-#
-# 1. Load flattened list of group members
-# 2. Check to see if Phab project exists for 'bmo-<group_name>'
-# 3. Create if does not exist with locked down policy.
-# 4. Set project members to exact list
-# 5. Profit
-
-my $sync_groups = Bugzilla::Group->match({ name => [ split('[,\s]+', $phab_sync_groups) ] });
-
-foreach my $group (@$sync_groups) {
- # Create group project if one does not yet exist
- my $phab_project_name = 'bmo-' . $group->name;
- my $project = Bugzilla::Extension::PhabBugz::Project->new_from_query({
- name => $phab_project_name
- });
- if (!$project) {
- my $secure_revision = Bugzilla::Extension::PhabBugz::Project->new_from_query({
- name => 'secure-revision'
- });
- $project = Bugzilla::Extension::PhabBugz::Project->create({
- name => $phab_project_name,
- description => 'BMO Security Group for ' . $group->name,
- view_policy => $secure_revision->phid,
- edit_policy => $secure_revision->phid,
- join_policy => $secure_revision->phid
- });
- }
-
- if (my @group_members = get_group_members($group)) {
- $project->set_members(\@group_members);
- $project->update();
- }
-}
-
-sub get_group_members {
- my ($group) = @_;
- my $group_obj = ref $group ? $group : Bugzilla::Group->check({ name => $group });
- my $members_all = $group_obj->members_complete();
- my %users;
- foreach my $name (keys %$members_all) {
- foreach my $user (@{ $members_all->{$name} }) {
- $users{$user->id} = $user;
- }
- }
-
- # Look up the phab ids for these users
- my $phab_users = get_phab_bmo_ids({ ids => [ keys %users ] });
- foreach my $phab_user (@{ $phab_users }) {
- $users{$phab_user->{id}}->{phab_phid} = $phab_user->{phid};
- }
-
- # We only need users who have accounts in phabricator
- return grep { $_->phab_phid } values %users;
-} \ No newline at end of file
diff --git a/extensions/PhabBugz/lib/Config.pm b/extensions/PhabBugz/lib/Config.pm
index 85ba37e74..d4b71430b 100644
--- a/extensions/PhabBugz/lib/Config.pm
+++ b/extensions/PhabBugz/lib/Config.pm
@@ -31,11 +31,6 @@ sub get_param_list {
checker => \&check_urlbase
},
{
- name => 'phabricator_sync_groups',
- type => 't',
- default => '',
- },
- {
name => 'phabricator_api_key',
type => 't',
default => '',
diff --git a/extensions/PhabBugz/lib/Constants.pm b/extensions/PhabBugz/lib/Constants.pm
index 754130f0b..1692f8fb9 100644
--- a/extensions/PhabBugz/lib/Constants.pm
+++ b/extensions/PhabBugz/lib/Constants.pm
@@ -16,12 +16,16 @@ our @EXPORT = qw(
PHAB_AUTOMATION_USER
PHAB_ATTACHMENT_PATTERN
PHAB_CONTENT_TYPE
- PHAB_POLL_SECONDS
+ PHAB_FEED_POLL_SECONDS
+ PHAB_USER_POLL_SECONDS
+ PHAB_GROUP_POLL_SECONDS
);
use constant PHAB_ATTACHMENT_PATTERN => qr/^phabricator-D(\d+)/;
use constant PHAB_AUTOMATION_USER => 'phab-bot@bmo.tld';
use constant PHAB_CONTENT_TYPE => 'text/x-phabricator-request';
-use constant PHAB_POLL_SECONDS => 5;
+use constant PHAB_FEED_POLL_SECONDS => $ENV{PHAB_FEED_POLL} // 5;
+use constant PHAB_USER_POLL_SECONDS => $ENV{PHAB_USER_POLL} // 60;
+use constant PHAB_GROUP_POLL_SECONDS => $ENV{PHAB_GROUP_POLL} // 300;
1;
diff --git a/extensions/PhabBugz/lib/Feed.pm b/extensions/PhabBugz/lib/Feed.pm
index 074ecc0f9..c46d36c13 100644
--- a/extensions/PhabBugz/lib/Feed.pm
+++ b/extensions/PhabBugz/lib/Feed.pm
@@ -9,14 +9,21 @@ package Bugzilla::Extension::PhabBugz::Feed;
use 5.10.1;
+use IO::Async::Timer::Periodic;
+use IO::Async::Loop;
use List::Util qw(first);
use List::MoreUtils qw(any);
use Moo;
+use Scalar::Util qw(blessed);
+use Try::Tiny;
-use Bugzilla::Logging;
use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Field;
+use Bugzilla::Logging;
+use Bugzilla::Mailer;
use Bugzilla::Search;
-use Bugzilla::Util qw(diff_arrays with_writable_database with_readonly_database);
+use Bugzilla::Util qw(diff_arrays format_time with_writable_database with_readonly_database);
use Bugzilla::Extension::PhabBugz::Constants;
use Bugzilla::Extension::PhabBugz::Policy;
@@ -25,13 +32,9 @@ use Bugzilla::Extension::PhabBugz::User;
use Bugzilla::Extension::PhabBugz::Util qw(
add_security_sync_comments
create_revision_attachment
- edit_revision_policy
get_bug_role_phids
- get_phab_bmo_ids
- get_project_phid
get_security_sync_groups
is_attachment_phab_revision
- make_revision_public
request
set_phab_user
);
@@ -40,38 +43,85 @@ has 'is_daemon' => ( is => 'rw', default => 0 );
sub start {
my ($self) = @_;
- while (1) {
- my $ok = eval {
- if (Bugzilla->params->{phabricator_enabled}) {
+
+ # Query for new revisions or changes
+ my $feed_timer = IO::Async::Timer::Periodic->new(
+ first_interval => 0,
+ interval => PHAB_FEED_POLL_SECONDS,
+ reschedule => 'drift',
+ on_tick => sub {
+ try{
$self->feed_query();
- Bugzilla->_cleanup();
}
- 1;
- };
- ERROR( $@ // "unknown exception" ) unless $ok;
- sleep(PHAB_POLL_SECONDS);
- }
+ catch {
+ FATAL($_);
+ };
+ Bugzilla->_cleanup();
+ },
+ );
+
+ # Query for new users
+ my $user_timer = IO::Async::Timer::Periodic->new(
+ first_interval => 0,
+ interval => PHAB_USER_POLL_SECONDS,
+ reschedule => 'drift',
+ on_tick => sub {
+ try{
+ $self->user_query();
+ }
+ catch {
+ FATAL($_);
+ };
+ Bugzilla->_cleanup();
+ },
+ );
+
+ # Update project membership in Phabricator based on Bugzilla groups
+ my $group_timer = IO::Async::Timer::Periodic->new(
+ first_interval => 0,
+ interval => PHAB_GROUP_POLL_SECONDS,
+ reschedule => 'drift',
+ on_tick => sub {
+ try{
+ $self->group_query();
+ }
+ catch {
+ FATAL($_);
+ };
+ Bugzilla->_cleanup();
+ },
+ );
+
+ my $loop = IO::Async::Loop->new;
+ $loop->add($feed_timer);
+ $loop->add($user_timer);
+ $loop->add($group_timer);
+ $feed_timer->start;
+ $user_timer->start;
+ $group_timer->start;
+ $loop->run;
}
sub feed_query {
my ($self) = @_;
- my $dbh = Bugzilla->dbh;
+
+ local Bugzilla::Logging->fields->{type} = 'FEED';
# Ensure Phabricator syncing is enabled
if (!Bugzilla->params->{phabricator_enabled}) {
- INFO("PHABRICATOR SYNC DISABLED");
+ WARN("PHABRICATOR SYNC DISABLED");
return;
}
# PROCESS NEW FEED TRANSACTIONS
- INFO("FEED: Fetching new transactions");
+ INFO("Fetching new stories");
my $story_last_id = $self->get_last_id('feed');
# Check for new transctions (stories)
my $new_stories = $self->new_stories($story_last_id);
- INFO("FEED: No new stories") unless @$new_stories;
+ INFO("No new stories") unless @$new_stories;
# Process each story
foreach my $story_data (@$new_stories) {
@@ -81,25 +131,29 @@ sub feed_query {
my $object_phid = $story_data->{objectPHID};
my $story_text = $story_data->{text};
- DEBUG("STORY ID: $story_id");
- DEBUG("STORY PHID: $story_phid");
- DEBUG("AUTHOR PHID: $author_phid");
- DEBUG("OBJECT PHID: $object_phid");
+ TRACE("STORY ID: $story_id");
+ TRACE("STORY PHID: $story_phid");
+ TRACE("AUTHOR PHID: $author_phid");
+ TRACE("OBJECT PHID: $object_phid");
INFO("STORY TEXT: $story_text");
# Only interested in changes to revisions for now.
if ($object_phid !~ /^PHID-DREV/) {
- DEBUG("SKIPPING: Not a revision change");
+ INFO("SKIPPING: Not a revision change");
$self->save_last_id($story_id, 'feed');
next;
}
# Skip changes done by phab-bot user
- my $phab_users = get_phab_bmo_ids({ phids => [$author_phid] });
- if (@$phab_users) {
- my $user = Bugzilla::User->new({ id => $phab_users->[0]->{id}, cache => 1 });
- if ($user->login eq PHAB_AUTOMATION_USER) {
- DEBUG("SKIPPING: Change made by phabricator user");
+ my $phab_user = Bugzilla::Extension::PhabBugz::User->new_from_query(
+ {
+ phids => [ $author_phid ]
+ }
+ );
+
+ if ($phab_user && $phab_user->bugzilla_id) {
+ if ($phab_user->bugzilla_user->login eq PHAB_AUTOMATION_USER) {
+ INFO("SKIPPING: Change made by phabricator user");
$self->save_last_id($story_id, 'feed');
next;
}
@@ -111,15 +165,69 @@ sub feed_query {
$self->save_last_id($story_id, 'feed');
}
+ # Process any build targets as well.
+ my $dbh = Bugzilla->dbh;
+
+ INFO("Checking for revisions in draft mode");
+ my $build_targets = $dbh->selectall_arrayref(
+ "SELECT name, value FROM phabbugz WHERE name LIKE 'build_target_%'",
+ { Slice => {} }
+ );
+
+ my $delete_build_target = $dbh->prepare(
+ "DELETE FROM phabbugz WHERE name = ? AND VALUE = ?"
+ );
+
+ foreach my $target (@$build_targets) {
+ my ($revision_id) = ($target->{name} =~ /^build_target_(\d+)$/);
+ my $build_target = $target->{value};
+
+ next unless $revision_id && $build_target;
+
+ INFO("Processing revision $revision_id with build target $build_target");
+
+ my $revision =
+ Bugzilla::Extension::PhabBugz::Revision->new_from_query(
+ {
+ ids => [ int($revision_id) ]
+ }
+ );
+
+ with_writable_database {
+ $self->process_revision_change($revision, " created D" . $revision->id);
+ };
+
+ # Set the build target to a passing status to
+ # allow the revision to exit draft state
+ request( 'harbormaster.sendmessage', {
+ buildTargetPHID => $build_target,
+ type => 'pass'
+ } );
+
+ $delete_build_target->execute($target->{name}, $target->{value});
+ }
+}
+
+sub user_query {
+ my ( $self ) = @_;
+
+ local Bugzilla::Logging->fields->{type} = 'USERS';
+
+ # Ensure Phabricator syncing is enabled
+ if (!Bugzilla->params->{phabricator_enabled}) {
+ WARN("PHABRICATOR SYNC DISABLED");
+ return;
+ }
+
# PROCESS NEW USERS
- INFO("FEED: Fetching new users");
+ INFO("Fetching new users");
my $user_last_id = $self->get_last_id('user');
# Check for new users
my $new_users = $self->new_users($user_last_id);
- INFO("FEED: No new users") unless @$new_users;
+ INFO("No new users") unless @$new_users;
# Process each new user
foreach my $user_data (@$new_users) {
@@ -128,10 +236,10 @@ sub feed_query {
my $user_realname = $user_data->{fields}{realName};
my $object_phid = $user_data->{phid};
- DEBUG("USER ID: $user_id");
- DEBUG("USER LOGIN: $user_login");
- DEBUG("USER REALNAME: $user_realname");
- DEBUG("OBJECT PHID: $object_phid");
+ TRACE("ID: $user_id");
+ TRACE("LOGIN: $user_login");
+ TRACE("REALNAME: $user_realname");
+ TRACE("OBJECT PHID: $object_phid");
with_readonly_database {
$self->process_new_user($user_data);
@@ -140,26 +248,110 @@ sub feed_query {
}
}
+sub group_query {
+ my ($self) = @_;
+
+ local Bugzilla::Logging->fields->{type} = 'GROUPS';
+
+ # Ensure Phabricator syncing is enabled
+ if ( !Bugzilla->params->{phabricator_enabled} ) {
+ WARN("PHABRICATOR SYNC DISABLED");
+ return;
+ }
+
+ # PROCESS SECURITY GROUPS
+
+ INFO("Updating group memberships");
+
+ # Loop through each group and perform the following:
+ #
+ # 1. Load flattened list of group members
+ # 2. Check to see if Phab project exists for 'bmo-<group_name>'
+ # 3. Create if does not exist with locked down policy.
+ # 4. Set project members to exact list including phab-bot user
+ # 5. Profit
+
+ my $sync_groups = Bugzilla::Group->match( { isactive => 1, isbuggroup => 1 } );
+
+ # Load phab-bot Phabricator user to add as a member of each project group later
+ my $phab_bmo_user = Bugzilla::User->new( { name => PHAB_AUTOMATION_USER, cache => 1 } );
+ my $phab_user =
+ Bugzilla::Extension::PhabBugz::User->new_from_query(
+ {
+ ids => [ $phab_bmo_user->id ]
+ }
+ );
+
+ # secure-revision project that will be used for bmo group projects
+ my $secure_revision =
+ Bugzilla::Extension::PhabBugz::Project->new_from_query(
+ {
+ name => 'secure-revision'
+ }
+ );
+
+ foreach my $group (@$sync_groups) {
+ # Create group project if one does not yet exist
+ my $phab_project_name = 'bmo-' . $group->name;
+ my $project =
+ Bugzilla::Extension::PhabBugz::Project->new_from_query(
+ {
+ name => $phab_project_name
+ }
+ );
+
+ if ( !$project ) {
+ INFO("Project $phab_project_name not found. Creating.");
+ $project = Bugzilla::Extension::PhabBugz::Project->create(
+ {
+ name => $phab_project_name,
+ description => 'BMO Security Group for ' . $group->name,
+ view_policy => $secure_revision->phid,
+ edit_policy => $secure_revision->phid,
+ join_policy => $secure_revision->phid
+ }
+ );
+ }
+ else {
+ # Make sure that the group project permissions are set properly
+ INFO("Updating permissions on $phab_project_name");
+ $project->set_policy( 'view', $secure_revision->phid );
+ $project->set_policy( 'edit', $secure_revision->phid );
+ $project->set_policy( 'join', $secure_revision->phid );
+ }
+
+ # Make sure phab-bot also a member of the new project group so that it can
+ # make policy changes to the private revisions
+ INFO("Setting project members for " . $project->name);
+ my $set_members = $self->get_group_members( $group );
+ push @$set_members, $phab_user unless grep $_->phid eq $phab_user->phid, @$set_members;
+ $project->set_members( $set_members );
+ $project->update();
+ }
+}
+
sub process_revision_change {
my ($self, $revision_phid, $story_text) = @_;
# Load the revision from Phabricator
- my $revision = Bugzilla::Extension::PhabBugz::Revision->new_from_query({ phids => [ $revision_phid ] });
+ my $revision =
+ blessed $revision_phid
+ ? $revision_phid
+ : Bugzilla::Extension::PhabBugz::Revision->new_from_query({ phids => [ $revision_phid ] });
# NO BUG ID
if (!$revision->bug_id) {
if ($story_text =~ /\s+created\s+D\d+/) {
# If new revision and bug id was omitted, make revision public
- DEBUG("No bug associated with new revision. Marking public.");
- $revision->set_policy('view', 'public');
- $revision->set_policy('edit', 'users');
+ INFO("No bug associated with new revision. Marking public.");
+ $revision->make_public();
$revision->update();
INFO("SUCCESS");
return;
}
else {
- DEBUG("SKIPPING: No bug associated with revision change");
+ INFO("SKIPPING: No bug associated with revision change");
return;
}
}
@@ -180,11 +372,8 @@ sub process_revision_change {
# If bug is public then remove privacy policy
if (!@{ $bug->groups_in }) {
- DEBUG('Bug is public so setting view/edit public');
- $revision->set_policy('view', 'public');
- $revision->set_policy('edit', 'users');
- my $secure_project_phid = get_project_phid('secure-revision');
- $revision->remove_project($secure_project_phid);
+ INFO('Bug is public so setting view/edit public');
+ $revision->make_public();
}
# else bug is private.
else {
@@ -193,54 +382,51 @@ sub process_revision_change {
# If bug privacy groups do not have any matching synchronized groups,
# then leave revision private and it will have be dealt with manually.
if (!@set_groups) {
- DEBUG('No matching groups. Adding comments to bug and revision');
+ INFO('No matching groups. Adding comments to bug and revision');
add_security_sync_comments([$revision], $bug);
}
# Otherwise, we create a new custom policy containing the project
# groups that are mapped to bugzilla groups.
else {
- my @set_projects = map { "bmo-" . $_ } @set_groups;
+ my $set_project_names = [ map { "bmo-" . $_ } @set_groups ];
# If current policy projects matches what we want to set, then
# we leave the current policy alone.
my $current_policy;
if ($revision->view_policy =~ /^PHID-PLCY/) {
- DEBUG("Loading current policy: " . $revision->view_policy);
+ INFO("Loading current policy: " . $revision->view_policy);
$current_policy
= Bugzilla::Extension::PhabBugz::Policy->new_from_query({ phids => [ $revision->view_policy ]});
- my $current_projects = $current_policy->rule_projects;
- DEBUG("Current policy projects: " . join(", ", @$current_projects));
- my ($added, $removed) = diff_arrays($current_projects, \@set_projects);
+ my $current_project_names = [ map { $_->name } @{ $current_policy->rule_projects } ];
+ INFO("Current policy projects: " . join(", ", @$current_project_names));
+ my ($added, $removed) = diff_arrays($current_project_names, $set_project_names);
if (@$added || @$removed) {
- DEBUG('Project groups do not match. Need new custom policy');
- $current_policy= undef;
+ INFO('Project groups do not match. Need new custom policy');
+ $current_policy = undef;
}
else {
- DEBUG('Project groups match. Leaving current policy as-is');
+ INFO('Project groups match. Leaving current policy as-is');
}
}
if (!$current_policy) {
- DEBUG("Creating new custom policy: " . join(", ", @set_projects));
- my $new_policy = Bugzilla::Extension::PhabBugz::Policy->create(\@set_projects);
- $revision->set_policy('view', $new_policy->phid);
- $revision->set_policy('edit', $new_policy->phid);
+ INFO("Creating new custom policy: " . join(", ", @$set_project_names));
+ $revision->make_private($set_project_names);
}
- my $secure_project_phid = get_project_phid('secure-revision');
- $revision->add_project($secure_project_phid);
+ # Subscriber list of the private revision should always match
+ # the bug roles such as assignee, qa contact, and cc members.
+ my $subscribers = get_bug_role_phids($bug);
+ $revision->set_subscribers($subscribers);
}
-
- # Subscriber list of the private revision should always match
- # the bug roles such as assignee, qa contact, and cc members.
- my $subscribers = get_bug_role_phids($bug);
- $revision->set_subscribers($subscribers);
}
my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()");
- my $attachment = create_revision_attachment($bug, $revision, $timestamp);
-
+ INFO('Checking for revision attachment');
+ my $attachment = create_revision_attachment($bug, $revision, $timestamp, $revision->author->bugzilla_user);
+ INFO('Attachment ' . $attachment->id . ' created or already exists.');
+
# ATTACHMENT OBSOLETES
# fixup attachments on current bug
@@ -252,11 +438,11 @@ sub process_revision_change {
next if $attach_revision_id != $revision->id;
my $make_obsolete = $revision->status eq 'abandoned' ? 1 : 0;
- DEBUG('Updating obsolete status on attachmment ' . $attachment->id);
+ INFO('Updating obsolete status on attachmment ' . $attachment->id);
$attachment->set_is_obsolete($make_obsolete);
if ($revision->title ne $attachment->description) {
- DEBUG('Updating description on attachment ' . $attachment->id);
+ INFO('Updating description on attachment ' . $attachment->id);
$attachment->set_description($revision->title);
}
@@ -272,8 +458,8 @@ sub process_revision_change {
});
foreach my $attachment (@$other_attachments) {
$other_bugs{$attachment->bug_id}++;
- DEBUG('Updating obsolete status on attachment ' .
- $attachment->id . " for bug " . $attachment->bug_id);
+ INFO('Updating obsolete status on attachment ' .
+ $attachment->id . " for bug " . $attachment->bug_id);
$attachment->set_is_obsolete(1);
$attachment->update($timestamp);
}
@@ -281,17 +467,28 @@ sub process_revision_change {
# REVIEWER STATUSES
my (@accepted_phids, @denied_phids, @accepted_user_ids, @denied_user_ids);
- unless ($revision->status eq 'changes-planned' || $revision->status eq 'needs-review') {
- foreach my $reviewer (@{ $revision->reviewers }) {
- push(@accepted_phids, $reviewer->phab_phid) if $reviewer->phab_review_status eq 'accepted';
- push(@denied_phids, $reviewer->phab_phid) if $reviewer->phab_review_status eq 'rejected';
- }
+ foreach my $reviewer (@{ $revision->reviewers }) {
+ push(@accepted_phids, $reviewer->phid) if $reviewer->{phab_review_status} eq 'accepted';
+ push(@denied_phids, $reviewer->phid) if $reviewer->{phab_review_status} eq 'rejected';
}
- my $phab_users = get_phab_bmo_ids({ phids => \@accepted_phids });
- @accepted_user_ids = map { $_->{id} } @$phab_users;
- $phab_users = get_phab_bmo_ids({ phids => \@denied_phids });
- @denied_user_ids = map { $_->{id} } @$phab_users;
+ if ( @accepted_phids ) {
+ my $phab_users = Bugzilla::Extension::PhabBugz::User->match(
+ {
+ phids => \@accepted_phids
+ }
+ );
+ @accepted_user_ids = map { $_->bugzilla_user->id } grep { defined $_->bugzilla_user } @$phab_users;
+ }
+
+ if ( @denied_phids ) {
+ my $phab_users = Bugzilla::Extension::PhabBugz::User->match(
+ {
+ phids => \@denied_phids
+ }
+ );
+ @denied_user_ids = map { $_->bugzilla_user->id } grep { defined $_->bugzilla_user } @$phab_users;
+ }
my %reviewers_hash = map { $_->name => 1 } @{ $revision->reviewers };
@@ -299,19 +496,22 @@ sub process_revision_change {
my ($attach_revision_id) = ($attachment->filename =~ PHAB_ATTACHMENT_PATTERN);
next if $revision->id != $attach_revision_id;
- # Clear old flags if no longer accepted
+ # Clear old accepted review flags if no longer accepted
my (@denied_flags, @new_flags, @removed_flags, %accepted_done, $flag_type);
foreach my $flag (@{ $attachment->flags }) {
next if $flag->type->name ne 'review';
$flag_type = $flag->type if $flag->type->is_active;
+ next if $flag->status ne '+';
if (any { $flag->setter->id == $_ } @denied_user_ids) {
+ INFO('Denying review flag set by ' . $flag->setter->name);
push(@denied_flags, { id => $flag->id, setter => $flag->setter, status => 'X' });
}
if (any { $flag->setter->id == $_ } @accepted_user_ids) {
+ INFO('Skipping as review+ already set by ' . $flag->setter->name);
$accepted_done{$flag->setter->id}++;
}
- if ($flag->status eq '+'
- && !any { $flag->setter->id == $_ } (@accepted_user_ids, @denied_user_ids)) {
+ if (!any { $flag->setter->id == $_ } (@accepted_user_ids, @denied_user_ids)) {
+ INFO('Clearing review+ flag set by ' . $flag->setter->name);
push(@removed_flags, { id => $flag->id, setter => $flag->setter, status => 'X' });
}
}
@@ -322,6 +522,7 @@ sub process_revision_change {
foreach my $user_id (@accepted_user_ids) {
next if $accepted_done{$user_id};
my $user = Bugzilla::User->check({ id => $user_id, cache => 1 });
+ INFO('Setting new review+ flag for ' . $user->name);
push(@new_flags, { type_id => $flag_type->id, setter => $user, status => '+' });
}
@@ -344,6 +545,7 @@ sub process_revision_change {
if ($comment) {
$comment .= "\n" . Bugzilla->params->{phabricator_base_uri} . "D" . $revision->id;
+ INFO("Flag comment: $comment");
# Add transaction_id as anchor if one present
# $comment .= "#" . $params->{transaction_id} if $params->{transaction_id};
$bug->add_comment($comment, {
@@ -380,7 +582,7 @@ sub process_new_user {
my $phab_user = Bugzilla::Extension::PhabBugz::User->new($user_data);
if (!$phab_user->bugzilla_id) {
- DEBUG("SKIPPING: No bugzilla id associated with user");
+ WARN("SKIPPING: No bugzilla id associated with user");
return;
}
@@ -389,6 +591,55 @@ sub process_new_user {
# Pre setup before querying DB
my $old_user = set_phab_user();
+ # CHECK AND WARN FOR POSSIBLE USERNAME SQUATTING
+ INFO("Checking for username squatters");
+ my $dbh = Bugzilla->dbh;
+ my $regexp = $dbh->quote( ":?:" . quotemeta($phab_user->name) . "[[:>:]]" );
+ my $results = $dbh->selectall_arrayref( "
+ SELECT userid, login_name, realname
+ FROM profiles
+ WHERE userid != ? AND " . $dbh->sql_regexp( 'realname', $regexp ),
+ { Slice => {} },
+ $bug_user->id );
+ if (@$results) {
+ # The email client will display the Date: header in the desired timezone,
+ # so we can always use UTC here.
+ my $timestamp = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ $timestamp = format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC');
+
+ foreach my $row (@$results) {
+ WARN(
+ 'Possible username squatter: ',
+ 'phab user login: ' . $phab_user->name,
+ ' phab user realname: ' . $phab_user->realname,
+ ' bugzilla user id: ' . $row->{userid},
+ ' bugzilla login: ' . $row->{login_name},
+ ' bugzilla realname: ' . $row->{realname}
+ );
+
+ my $vars = {
+ date => $timestamp,
+ phab_user_login => $phab_user->name,
+ phab_user_realname => $phab_user->realname,
+ bugzilla_userid => $bug_user->id,
+ bugzilla_login => $bug_user->login,
+ bugzilla_realname => $bug_user->name,
+ squat_userid => $row->{userid},
+ squat_login => $row->{login_name},
+ squat_realname => $row->{realname}
+ };
+
+ my $message;
+ my $template = Bugzilla->template;
+ $template->process("admin/email/squatter-alert.txt.tmpl", $vars, \$message)
+ || ThrowTemplateError($template->error());
+
+ MessageToMTA($message);
+ }
+ }
+
+ # ADD SUBSCRIBERS TO REVSISIONS FOR CURRENT PRIVATE BUGS
+
my $params = {
f3 => 'OP',
j3 => 'OR',
@@ -432,8 +683,10 @@ sub process_new_user {
# the first value of each row should be the bug id
my @bug_ids = map { shift @$_ } @$data;
+ INFO("Updating subscriber values for old private bugs");
+
foreach my $bug_id (@bug_ids) {
- DEBUG("Processing bug $bug_id");
+ INFO("Processing bug $bug_id");
my $bug = Bugzilla::Bug->new({ id => $bug_id, cache => 1 });
@@ -442,7 +695,8 @@ sub process_new_user {
foreach my $attachment (@attachments) {
my ($revision_id) = ($attachment->filename =~ PHAB_ATTACHMENT_PATTERN);
- DEBUG("Processing revision D$revision_id");
+
+ INFO("Processing revision D$revision_id");
my $revision = Bugzilla::Extension::PhabBugz::Revision->new_from_query(
{ ids => [ int($revision_id) ] });
@@ -450,7 +704,7 @@ sub process_new_user {
$revision->add_subscriber($phab_user->phid);
$revision->update();
- DEBUG("Revision $revision_id updated");
+ INFO("Revision $revision_id updated");
}
}
@@ -467,7 +721,9 @@ sub new_stories {
my ( $self, $after ) = @_;
my $data = { view => 'text' };
$data->{after} = $after if $after;
+
my $result = request( 'feed.query_id', $data );
+
unless ( ref $result->{result}{data} eq 'ARRAY'
&& @{ $result->{result}{data} } )
{
@@ -487,7 +743,9 @@ sub new_users {
}
};
$data->{before} = $after if $after;
+
my $result = request( 'user.search', $data );
+
unless ( ref $result->{result}{data} eq 'ARRAY'
&& @{ $result->{result}{data} } )
{
@@ -504,7 +762,7 @@ sub get_last_id {
my $last_id = Bugzilla->dbh->selectrow_array( "
SELECT value FROM phabbugz WHERE name = ?", undef, $type_full );
$last_id ||= 0;
- DEBUG( "QUERY " . uc($type_full) . ": $last_id" );
+ TRACE(uc($type_full) . ": $last_id" );
return $last_id;
}
@@ -513,9 +771,34 @@ sub save_last_id {
# Store the largest last key so we can start from there in the next session
my $type_full = $type . "_last_id";
- DEBUG( "UPDATING " . uc($type_full) . ": $last_id" );
+ TRACE("UPDATING " . uc($type_full) . ": $last_id" );
Bugzilla->dbh->do( "REPLACE INTO phabbugz (name, value) VALUES (?, ?)",
undef, $type_full, $last_id );
}
+sub get_group_members {
+ my ( $self, $group ) = @_;
+
+ my $group_obj =
+ ref $group ? $group : Bugzilla::Group->check( { name => $group, cache => 1 } );
+
+ my $flat_list = join(',',
+ @{ Bugzilla::Group->flatten_group_membership( $group_obj->id ) } );
+
+ my $user_query = "
+ SELECT DISTINCT profiles.userid
+ FROM profiles, user_group_map AS ugm
+ WHERE ugm.user_id = profiles.userid
+ AND ugm.isbless = 0
+ AND ugm.group_id IN($flat_list)";
+ my $user_ids = Bugzilla->dbh->selectcol_arrayref($user_query);
+
+ # Return matching users in Phabricator
+ return Bugzilla::Extension::PhabBugz::User->match(
+ {
+ ids => $user_ids
+ }
+ );
+}
+
1;
diff --git a/extensions/PhabBugz/lib/Policy.pm b/extensions/PhabBugz/lib/Policy.pm
index 8162ac52c..a86c83036 100644
--- a/extensions/PhabBugz/lib/Policy.pm
+++ b/extensions/PhabBugz/lib/Policy.pm
@@ -41,7 +41,7 @@ has 'rules' => (
has 'rule_projects' => (
is => 'lazy',
- isa => ArrayRef[Str],
+ isa => ArrayRef[Object],
);
# {
@@ -88,7 +88,7 @@ sub new_from_query {
}
sub create {
- my ($class, $project_names) = @_;
+ my ($class, $projects) = @_;
my $data = {
objectType => 'DREV',
@@ -97,23 +97,19 @@ sub create {
{
action => 'allow',
rule => 'PhabricatorSubscriptionsSubscribersPolicyRule',
+ },
+ {
+ action => 'allow',
+ rule => 'PhabricatorDifferentialReviewersPolicyRule'
}
]
};
- if (@$project_names) {
- my $project_phids = [];
- foreach my $project_name (@$project_names) {
- my $project = Bugzilla::Extension::PhabBugz::Project->new_from_query({ name => $project_name });
- push @$project_phids, $project->phid if $project;
- }
-
- ThrowUserError('invalid_phabricator_sync_groups') unless @$project_phids;
-
+ if (@$projects) {
push @{ $data->{policy} }, {
action => 'allow',
- rule => 'PhabricatorProjectsPolicyRule',
- value => $project_phids,
+ rule => 'PhabricatorProjectsAllPolicyRule',
+ value => [ map { $_->phid } @$projects ],
};
}
else {
@@ -131,11 +127,9 @@ sub _build_rule_projects {
my ($self) = @_;
return [] unless $self->rules;
- my $rule = first { $_->{rule} eq 'PhabricatorProjectsPolicyRule'} @{ $self->rules };
+ my $rule = first { $_->{rule} =~ /PhabricatorProjects(?:All)?PolicyRule/ } @{ $self->rules };
return [] unless $rule;
return [
- map { $_->name }
- grep { $_ }
map { Bugzilla::Extension::PhabBugz::Project->new_from_query( { phids => [$_] } ) }
@{ $rule->{value} }
];
diff --git a/extensions/PhabBugz/lib/Project.pm b/extensions/PhabBugz/lib/Project.pm
index b0babc58b..c52e1a661 100644
--- a/extensions/PhabBugz/lib/Project.pm
+++ b/extensions/PhabBugz/lib/Project.pm
@@ -9,15 +9,14 @@ package Bugzilla::Extension::PhabBugz::Project;
use 5.10.1;
use Moo;
+use Scalar::Util qw(blessed);
use Types::Standard -all;
use Type::Utils;
use Bugzilla::Error;
use Bugzilla::Util qw(trim);
-use Bugzilla::Extension::PhabBugz::Util qw(
- request
- get_phab_bmo_ids
-);
+use Bugzilla::Extension::PhabBugz::User;
+use Bugzilla::Extension::PhabBugz::Util qw(request);
#########################
# Initialization #
@@ -47,7 +46,18 @@ sub new_from_query {
my $result = request( 'project.search', $data );
if ( exists $result->{result}{data} && @{ $result->{result}{data} } ) {
- return $class->new( $result->{result}{data}[0] );
+ # If name is used as a query param, we need to loop through and look
+ # for exact match as Conduit will tokenize the name instead of doing
+ # exact string match :( If name is not used, then return first one.
+ if ( exists $params->{name} ) {
+ foreach my $item ( @{ $result->{result}{data} } ) {
+ next if $item->{fields}{name} ne $params->{name};
+ return $class->new($item);
+ }
+ }
+ else {
+ return $class->new( $result->{result}{data}[0] );
+ }
}
}
@@ -157,7 +167,7 @@ sub create {
my $result = request( 'project.edit', $data );
return $class->new_from_query(
- { phids => $result->{result}{object}{phid} } );
+ { phids => [ $result->{result}{object}{phid} ] } );
}
sub update {
@@ -270,20 +280,20 @@ sub set_description {
sub add_member {
my ( $self, $member ) = @_;
$self->{add_members} ||= [];
- my $member_phid = blessed $member ? $member->phab_phid : $member;
+ my $member_phid = blessed $member ? $member->phid : $member;
push( @{ $self->{add_members} }, $member_phid );
}
sub remove_member {
my ( $self, $member ) = @_;
$self->{remove_members} ||= [];
- my $member_phid = blessed $member ? $member->phab_phid : $member;
+ my $member_phid = blessed $member ? $member->phid : $member;
push( @{ $self->{remove_members} }, $member_phid );
}
sub set_members {
my ( $self, $members ) = @_;
- $self->{set_members} = [ map { $_->phab_phid } @$members ];
+ $self->{set_members} = [ map { blessed $_ ? $_->phid : $_ } @$members ];
}
sub set_policy {
@@ -307,16 +317,13 @@ sub _build_members {
return [] if !@phids;
- my $users = get_phab_bmo_ids( { phids => \@phids } );
-
- my @members;
- foreach my $user (@$users) {
- my $member = Bugzilla::User->new( { id => $user->{id}, cache => 1 } );
- $member->{phab_phid} = $user->{phid};
- push( @members, $member );
- }
+ my $users = Bugzilla::Extension::PhabBugz::User->match(
+ {
+ phids => \@phids
+ }
+ );
- return \@members;
+ return [ map { $_->bugzilla_user } @$users ];
}
1;
diff --git a/extensions/PhabBugz/lib/Revision.pm b/extensions/PhabBugz/lib/Revision.pm
index 98c3196c2..900454220 100644
--- a/extensions/PhabBugz/lib/Revision.pm
+++ b/extensions/PhabBugz/lib/Revision.pm
@@ -17,10 +17,9 @@ use Type::Utils;
use Bugzilla::Bug;
use Bugzilla::Error;
use Bugzilla::Util qw(trim);
-use Bugzilla::Extension::PhabBugz::Util qw(
- get_phab_bmo_ids
- request
-);
+use Bugzilla::Extension::PhabBugz::Project;
+use Bugzilla::Extension::PhabBugz::User;
+use Bugzilla::Extension::PhabBugz::Util qw(request);
#########################
# Initialization #
@@ -37,12 +36,12 @@ has author_phid => ( is => 'ro', isa => Str );
has bug_id => ( is => 'ro', isa => Str );
has view_policy => ( is => 'ro', isa => Str );
has edit_policy => ( is => 'ro', isa => Str );
-has projects_raw => ( is => 'ro', isa => ArrayRef [Str] );
has subscriber_count => ( is => 'ro', isa => Int );
has bug => ( is => 'lazy', isa => Object );
has author => ( is => 'lazy', isa => Object );
has reviewers => ( is => 'lazy', isa => ArrayRef [Object] );
has subscribers => ( is => 'lazy', isa => ArrayRef [Object] );
+has projects => ( is => 'lazy', isa => ArrayRef [Object] );
has reviewers_raw => (
is => 'ro',
isa => ArrayRef [
@@ -62,6 +61,12 @@ has subscribers_raw => (
viewerIsSubscribed => Bool,
]
);
+has projects_raw => (
+ is => 'ro',
+ isa => Dict [
+ projectPHIDs => ArrayRef [Str]
+ ]
+);
sub new_from_query {
my ( $class, $params ) = @_;
@@ -93,18 +98,18 @@ sub new_from_query {
sub BUILDARGS {
my ( $class, $params ) = @_;
- $params->{title} = $params->{fields}->{title};
- $params->{summary} = $params->{fields}->{summary};
- $params->{status} = $params->{fields}->{status}->{value};
- $params->{creation_ts} = $params->{fields}->{dateCreated};
- $params->{modification_ts} = $params->{fields}->{dateModified};
- $params->{author_phid} = $params->{fields}->{authorPHID};
- $params->{bug_id} = $params->{fields}->{'bugzilla.bug-id'};
- $params->{view_policy} = $params->{fields}->{policy}->{view};
- $params->{edit_policy} = $params->{fields}->{policy}->{edit};
- $params->{reviewers_raw} = $params->{attachments}->{reviewers}->{reviewers};
- $params->{subscribers_raw} = $params->{attachments}->{subscribers};
- $params->{projects} = $params->{attachments}->{projects};
+ $params->{title} = $params->{fields}->{title};
+ $params->{summary} = $params->{fields}->{summary};
+ $params->{status} = $params->{fields}->{status}->{value};
+ $params->{creation_ts} = $params->{fields}->{dateCreated};
+ $params->{modification_ts} = $params->{fields}->{dateModified};
+ $params->{author_phid} = $params->{fields}->{authorPHID};
+ $params->{bug_id} = $params->{fields}->{'bugzilla.bug-id'};
+ $params->{view_policy} = $params->{fields}->{policy}->{view};
+ $params->{edit_policy} = $params->{fields}->{policy}->{edit};
+ $params->{reviewers_raw} = $params->{attachments}->{reviewers}->{reviewers};
+ $params->{subscribers_raw} = $params->{attachments}->{subscribers};
+ $params->{projects_raw} = $params->{attachments}->{projects};
$params->{subscriber_count} =
$params->{attachments}->{subscribers}->{subscriberCount};
@@ -284,12 +289,13 @@ sub _build_bug {
sub _build_author {
my ($self) = @_;
return $self->{author} if $self->{author};
- my $users = get_phab_bmo_ids( { phids => [ $self->author_phid ] } );
- if (@$users) {
- $self->{author} =
- new Bugzilla::User( { id => $users->[0]->{id}, cache => 1 } );
- $self->{author}->{phab_phid} = $self->author_phid;
- return $self->{author};
+ my $phab_user = Bugzilla::Extension::PhabBugz::User->new_from_query(
+ {
+ phids => [ $self->author_phid ]
+ }
+ );
+ if ($phab_user) {
+ return $self->{author} = $phab_user;
}
}
@@ -306,22 +312,22 @@ sub _build_reviewers {
return [] unless @phids;
- my $users = get_phab_bmo_ids( { phids => \@phids } );
+ my $users = Bugzilla::Extension::PhabBugz::User->match(
+ {
+ phids => \@phids
+ }
+ );
- my @reviewers;
foreach my $user (@$users) {
- my $reviewer = Bugzilla::User->new( { id => $user->{id}, cache => 1 } );
- $reviewer->{phab_phid} = $user->{phid};
foreach my $reviewer_data ( @{ $self->reviewers_raw } ) {
- if ( $reviewer_data->{reviewerPHID} eq $user->{phid} ) {
- $reviewer->{phab_review_status} = $reviewer_data->{status};
+ if ( $reviewer_data->{reviewerPHID} eq $user->phid ) {
+ $user->{phab_review_status} = $reviewer_data->{status};
last;
}
}
- push @reviewers, $reviewer;
}
- return \@reviewers;
+ return $self->{reviewers} = $users;
}
sub _build_subscribers {
@@ -335,19 +341,31 @@ sub _build_subscribers {
push @phids, $phid;
}
- my $users = get_phab_bmo_ids( { phids => \@phids } );
+ my $users = Bugzilla::Extension::PhabBugz::User->match(
+ {
+ phids => \@phids
+ }
+ );
- return [] unless @phids;
+ return $self->{subscribers} = $users;
+}
- my @subscribers;
- foreach my $user (@$users) {
- my $subscriber =
- Bugzilla::User->new( { id => $user->{id}, cache => 1 } );
- $subscriber->{phab_phid} = $user->{phid};
- push @subscribers, $subscriber;
+sub _build_projects {
+ my ($self) = @_;
+
+ return $self->{projects} if $self->{projects};
+ return [] unless $self->projects_raw->{projectPHIDs};
+
+ my @projects;
+ foreach my $phid ( @{ $self->projects_raw->{projectPHIDs} } ) {
+ push @projects, Bugzilla::Extension::PhabBugz::Project->new_from_query(
+ {
+ phids => [ $phid ]
+ }
+ );
}
- return \@subscribers;
+ return $self->{projects} = \@projects;
}
#########################
@@ -364,27 +382,27 @@ sub add_comment {
sub add_reviewer {
my ( $self, $reviewer ) = @_;
$self->{add_reviewers} ||= [];
- my $reviewer_phid = blessed $reviewer ? $reviewer->phab_phid : $reviewer;
+ my $reviewer_phid = blessed $reviewer ? $reviewer->phid : $reviewer;
push @{ $self->{add_reviewers} }, $reviewer_phid;
}
sub remove_reviewer {
my ( $self, $reviewer ) = @_;
$self->{remove_reviewers} ||= [];
- my $reviewer_phid = blessed $reviewer ? $reviewer->phab_phid : $reviewer;
+ my $reviewer_phid = blessed $reviewer ? $reviewer->phid : $reviewer;
push @{ $self->{remove_reviewers} }, $reviewer_phid;
}
sub set_reviewers {
my ( $self, $reviewers ) = @_;
- $self->{set_reviewers} = [ map { $_->phab_phid } @$reviewers ];
+ $self->{set_reviewers} = [ map { $_->phid } @$reviewers ];
}
sub add_subscriber {
my ( $self, $subscriber ) = @_;
$self->{add_subscribers} ||= [];
my $subscriber_phid =
- blessed $subscriber ? $subscriber->phab_phid : $subscriber;
+ blessed $subscriber ? $subscriber->phid : $subscriber;
push @{ $self->{add_subscribers} }, $subscriber_phid;
}
@@ -392,7 +410,7 @@ sub remove_subscriber {
my ( $self, $subscriber ) = @_;
$self->{remove_subscribers} ||= [];
my $subscriber_phid =
- blessed $subscriber ? $subscriber->phab_phid : $subscriber;
+ blessed $subscriber ? $subscriber->phid : $subscriber;
push @{ $self->{remove_subscribers} }, $subscriber_phid;
}
@@ -423,4 +441,50 @@ sub remove_project {
push @{ $self->{remove_projects} }, $project_phid;
}
+sub make_private {
+ my ( $self, $project_names ) = @_;
+
+ my $secure_revision_project =
+ Bugzilla::Extension::PhabBugz::Project->new_from_query(
+ {
+ name => 'secure-revision'
+ }
+ );
+
+ my @set_projects;
+ foreach my $name (@$project_names) {
+ my $set_project =
+ Bugzilla::Extension::PhabBugz::Project->new_from_query(
+ {
+ name => $name
+ }
+ );
+ push @set_projects, $set_project;
+ }
+
+ my $new_policy = Bugzilla::Extension::PhabBugz::Policy->create(\@set_projects);
+ $self->set_policy('view', $new_policy->phid);
+ $self->set_policy('edit', $new_policy->phid);
+
+ foreach my $project ($secure_revision_project, @set_projects) {
+ $self->add_project($project->phid);
+ }
+
+ return $self;
+}
+
+sub make_public {
+ my ( $self ) = @_;
+
+ $self->set_policy('view', 'public');
+ $self->set_policy('edit', 'users');
+
+ my @current_group_projects = grep { $_->name =~ /^(bmo-.*|secure-revision)$/ } @{ $self->projects };
+ foreach my $project (@current_group_projects) {
+ $self->remove_project($project->phid);
+ }
+
+ return $self;
+}
+
1;
diff --git a/extensions/PhabBugz/lib/User.pm b/extensions/PhabBugz/lib/User.pm
index 3b2d87e60..9d4e9eef4 100644
--- a/extensions/PhabBugz/lib/User.pm
+++ b/extensions/PhabBugz/lib/User.pm
@@ -116,14 +116,14 @@ sub match {
my ( $class, $params ) = @_;
# BMO id search takes precedence if bugzilla_ids is used.
- my $bugzilla_ids = delete $params->{bugzilla_ids};
+ my $bugzilla_ids = delete $params->{ids};
if ($bugzilla_ids) {
my $bugzilla_data =
$class->get_phab_bugzilla_ids( { ids => $bugzilla_ids } );
$params->{phids} = [ map { $_->{phid} } @$bugzilla_data ];
}
- return [] if !$params->{phids};
+ return [] if !@{ $params->{phids} };
# Look for BMO external user id in external-accounts attachment
my $data = {
@@ -177,6 +177,7 @@ sub get_phab_bugzilla_ids {
# Store new values in memcache for later retrieval
foreach my $user ( @{ $result->{result} } ) {
+ next if !$user->{phid};
$memcache->set(
{
key => "phab_user_bugzilla_id_" . $user->{id},
diff --git a/extensions/PhabBugz/lib/Util.pm b/extensions/PhabBugz/lib/Util.pm
index 844d8c0b5..d25f62f68 100644
--- a/extensions/PhabBugz/lib/Util.pm
+++ b/extensions/PhabBugz/lib/Util.pm
@@ -21,63 +21,26 @@ use Bugzilla::Extension::PhabBugz::Constants;
use JSON::XS qw(encode_json decode_json);
use List::Util qw(first);
use LWP::UserAgent;
+use Taint::Util qw(untaint);
+use Try::Tiny;
use base qw(Exporter);
our @EXPORT = qw(
- add_comment_to_revision
add_security_sync_comments
create_revision_attachment
- create_private_revision_policy
- create_project
- edit_revision_policy
get_attachment_revisions
get_bug_role_phids
- get_members_by_bmo_id
- get_members_by_phid
- get_phab_bmo_ids
- get_project_phid
- get_revisions_by_ids
- get_revisions_by_phids
+ get_needs_review
get_security_sync_groups
intersect
is_attachment_phab_revision
- make_revision_private
- make_revision_public
request
set_phab_user
- set_project_members
- set_revision_subscribers
);
-sub get_revisions_by_ids {
- my ($ids) = @_;
- return _get_revisions({ ids => $ids });
-}
-
-sub get_revisions_by_phids {
- my ($phids) = @_;
- return _get_revisions({ phids => $phids });
-}
-
-sub _get_revisions {
- my ($constraints) = @_;
-
- my $data = {
- queryKey => 'all',
- constraints => $constraints
- };
-
- my $result = request('differential.revision.search', $data);
-
- ThrowUserError('invalid_phabricator_revision_id')
- unless (exists $result->{result}{data} && @{ $result->{result}{data} });
-
- return $result->{result}{data};
-}
-
sub create_revision_attachment {
- my ( $bug, $revision, $timestamp ) = @_;
+ my ( $bug, $revision, $timestamp, $submitter ) = @_;
my $phab_base_uri = Bugzilla->params->{phabricator_base_uri};
ThrowUserError('invalid_phabricator_uri') unless $phab_base_uri;
@@ -97,22 +60,38 @@ sub create_revision_attachment {
($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()");
}
- my $attachment = Bugzilla::Attachment->create(
- {
- bug => $bug,
- creation_ts => $timestamp,
- data => $revision_uri,
- description => $revision->title,
- filename => 'phabricator-D' . $revision->id . '-url.txt',
- ispatch => 0,
- isprivate => 0,
- mimetype => PHAB_CONTENT_TYPE,
+ # If submitter, then switch to that user when creating attachment
+ my ($old_user, $attachment);
+ try {
+ if ($submitter) {
+ $old_user = Bugzilla->user;
+ $submitter->{groups} = [ Bugzilla::Group->get_all ]; # We need to always be able to add attachment
+ Bugzilla->set_user($submitter);
}
- );
- # Insert a comment about the new attachment into the database.
- $bug->add_comment($revision->summary, { type => CMT_ATTACHMENT_CREATED,
- extra_data => $attachment->id });
+ $attachment = Bugzilla::Attachment->create(
+ {
+ bug => $bug,
+ creation_ts => $timestamp,
+ data => $revision_uri,
+ description => $revision->title,
+ filename => 'phabricator-D' . $revision->id . '-url.txt',
+ ispatch => 0,
+ isprivate => 0,
+ mimetype => PHAB_CONTENT_TYPE,
+ }
+ );
+
+ # Insert a comment about the new attachment into the database.
+ $bug->add_comment($revision->summary, { type => CMT_ATTACHMENT_CREATED,
+ extra_data => $attachment->id });
+ }
+ catch {
+ die $_;
+ }
+ finally {
+ Bugzilla->set_user($old_user) if $old_user;
+ };
return $attachment;
}
@@ -132,321 +111,47 @@ sub get_bug_role_phids {
push(@bug_users, $bug->qa_contact) if $bug->qa_contact;
push(@bug_users, @{ $bug->cc_users }) if @{ $bug->cc_users };
- return get_members_by_bmo_id(\@bug_users);
-}
-
-sub create_private_revision_policy {
- my ($bug, $groups) = @_;
-
- my $data = {
- objectType => 'DREV',
- default => 'deny',
- policy => [
- {
- action => 'allow',
- rule => 'PhabricatorSubscriptionsSubscribersPolicyRule',
- }
- ]
- };
-
- if(scalar @$groups gt 0) {
- my $project_phids = [];
- foreach my $group (@$groups) {
- my $phid = get_project_phid('bmo-' . $group);
- push(@$project_phids, $phid) if $phid;
- }
-
- ThrowUserError('invalid_phabricator_sync_groups') unless @$project_phids;
-
- push(@{ $data->{policy} },
- {
- action => 'allow',
- rule => 'PhabricatorProjectsPolicyRule',
- value => $project_phids,
- }
- );
- }
- else {
- my $secure_revision = Bugzilla::Extension::PhabBugz::Project->new_from_query({
- name => 'secure-revision'
- });
- push(@{ $data->{policy} },
- {
- action => 'allow',
- value => $secure_revision->phid,
- }
- );
- }
-
- my $result = request('policy.create', $data);
- return $result->{result}{phid};
-}
-
-sub make_revision_public {
- my ($revision_phid) = @_;
- return request('differential.revision.edit', {
- transactions => [
- {
- type => 'view',
- value => 'public'
- },
- {
- type => 'edit',
- value => 'users'
- }
- ],
- objectIdentifier => $revision_phid
- });
-}
-
-sub make_revision_private {
- my ($revision_phid) = @_;
-
- my $secure_revision = Bugzilla::Extension::PhabBugz::Project->new_from_query({
- name => 'secure-revision'
- });
-
- return request('differential.revision.edit', {
- transactions => [
- {
- type => "view",
- value => $secure_revision->phid
- },
- {
- type => "edit",
- value => $secure_revision->phid
- }
- ],
- objectIdentifier => $revision_phid
- });
-}
-
-sub edit_revision_policy {
- my ($revision_phid, $policy_phid, $subscribers) = @_;
-
- my $data = {
- transactions => [
- {
- type => 'view',
- value => $policy_phid
- },
- {
- type => 'edit',
- value => $policy_phid
- }
- ],
- objectIdentifier => $revision_phid
- };
-
- if (@$subscribers) {
- push(@{ $data->{transactions} }, {
- type => 'subscribers.set',
- value => $subscribers
- });
- }
-
- return request('differential.revision.edit', $data);
-}
-
-sub set_revision_subscribers {
- my ($revision_phid, $subscribers) = @_;
-
- my $data = {
- transactions => [
- {
- type => 'subscribers.set',
- value => $subscribers
- }
- ],
- objectIdentifier => $revision_phid
- };
-
- return request('differential.revision.edit', $data);
-}
-
-sub add_comment_to_revision {
- my ($revision_phid, $comment) = @_;
-
- my $data = {
- transactions => [
- {
- type => 'comment',
- value => $comment
- }
- ],
- objectIdentifier => $revision_phid
- };
- return request('differential.revision.edit', $data);
-}
-
-sub get_project_phid {
- my $project = shift;
- my $memcache = Bugzilla->memcached;
-
- # Check memcache
- my $project_phid = $memcache->get_config({ key => "phab_project_phid_" . $project });
- if (!$project_phid) {
- my $data = {
- queryKey => 'all',
- constraints => {
- name => $project
- }
- };
-
- my $result = request('project.search', $data);
- return undef
- unless (exists $result->{result}{data} && @{ $result->{result}{data} });
-
- $project_phid = $result->{result}{data}[0]{phid};
- $memcache->set_config({ key => "phab_project_phid_" . $project, data => $project_phid });
- }
- return $project_phid;
-}
-
-sub create_project {
- my ($project, $description, $members) = @_;
-
- my $secure_revision = Bugzilla::Extension::PhabBugz::Project->new_from_query({
- name => 'secure-revision'
- });
-
- my $data = {
- transactions => [
- { type => 'name', value => $project },
- { type => 'description', value => $description },
- { type => 'edit', value => $secure_revision->phid }.
- { type => 'join', value => $secure_revision->phid },
- { type => 'view', value => $secure_revision->phid },
- { type => 'icon', value => 'group' },
- { type => 'color', value => 'red' }
- ]
- };
-
- my $result = request('project.edit', $data);
- return $result->{result}{object}{phid};
-}
-
-sub set_project_members {
- my ($project_id, $phab_user_ids) = @_;
-
- my $data = {
- objectIdentifier => $project_id,
- transactions => [
- { type => 'members.set', value => $phab_user_ids }
- ]
- };
-
- my $result = request('project.edit', $data);
- return $result->{result}{object}{phid};
-}
-
-sub get_members_by_bmo_id {
- my $users = shift;
-
- my $result = get_phab_bmo_ids({ ids => [ map { $_->id } @$users ] });
-
- my @phab_ids;
- foreach my $user (@$result) {
- push(@phab_ids, $user->{phid})
- if ($user->{phid} && $user->{phid} =~ /^PHID-USER/);
- }
-
- return \@phab_ids;
-}
-
-sub get_members_by_phid {
- my $phids = shift;
-
- my $result = get_phab_bmo_ids({ phids => $phids });
-
- my @bmo_ids;
- foreach my $user (@$result) {
- push(@bmo_ids, $user->{id})
- if ($user->{phid} && $user->{phid} =~ /^PHID-USER/);
- }
-
- return \@bmo_ids;
-}
-
-sub get_phab_bmo_ids {
- my ($params) = @_;
- my $memcache = Bugzilla->memcached;
-
- # Try to find the values in memcache first
- my @results;
- if ($params->{ids}) {
- my @bmo_ids = @{ $params->{ids} };
- for (my $i = 0; $i < @bmo_ids; $i++) {
- my $phid = $memcache->get({ key => "phab_user_bmo_id_" . $bmo_ids[$i] });
- if ($phid) {
- push(@results, {
- id => $bmo_ids[$i],
- phid => $phid
- });
- splice(@bmo_ids, $i, 1);
- }
- }
- $params->{ids} = \@bmo_ids;
- }
-
- if ($params->{phids}) {
- my @phids = @{ $params->{phids} };
- for (my $i = 0; $i < @phids; $i++) {
- my $bmo_id = $memcache->get({ key => "phab_user_phid_" . $phids[$i] });
- if ($bmo_id) {
- push(@results, {
- id => $bmo_id,
- phid => $phids[$i]
- });
- splice(@phids, $i, 1);
- }
+ my $phab_users =
+ Bugzilla::Extension::PhabBugz::User->match(
+ {
+ ids => [ map { $_->id } @bug_users ]
}
- $params->{phids} = \@phids;
- }
-
- my $result = request('bugzilla.account.search', $params);
-
- # Store new values in memcache for later retrieval
- foreach my $user (@{ $result->{result} }) {
- $memcache->set({ key => "phab_user_bmo_id_" . $user->{id},
- value => $user->{phid} });
- $memcache->set({ key => "phab_user_phid_" . $user->{phid},
- value => $user->{id} });
- push(@results, $user);
- }
+ );
- return \@results;
+ return [ map { $_->phid } @{ $phab_users } ];
}
sub is_attachment_phab_revision {
my ($attachment) = @_;
- return ($attachment->contenttype eq PHAB_CONTENT_TYPE
- && $attachment->attacher->login eq PHAB_AUTOMATION_USER) ? 1 : 0;
+ return $attachment->contenttype eq PHAB_CONTENT_TYPE;
}
sub get_attachment_revisions {
my $bug = shift;
- my $revisions;
-
my @attachments =
grep { is_attachment_phab_revision($_) } @{ $bug->attachments() };
- if (@attachments) {
- my @revision_ids;
- foreach my $attachment (@attachments) {
- my ($revision_id) =
- ( $attachment->filename =~ PHAB_ATTACHMENT_PATTERN );
- next if !$revision_id;
- push( @revision_ids, int($revision_id) );
- }
+ return unless @attachments;
- if (@revision_ids) {
- $revisions = get_revisions_by_ids( \@revision_ids );
- }
+ my @revision_ids;
+ foreach my $attachment (@attachments) {
+ my ($revision_id) =
+ ( $attachment->filename =~ PHAB_ATTACHMENT_PATTERN );
+ next if !$revision_id;
+ push( @revision_ids, int($revision_id) );
}
- return @$revisions;
+ return unless @revision_ids;
+
+ my @revisions;
+ foreach my $revision_id (@revision_ids) {
+ push @revisions, Bugzilla::Extension::PhabBugz::Revision->new_from_query({
+ ids => [ $revision_id ]
+ });
+ }
+
+ return \@revisions;
}
sub request {
@@ -478,7 +183,12 @@ sub request {
if $response->is_error;
my $result;
- my $result_ok = eval { $result = decode_json( $response->content); 1 };
+ my $result_ok = eval {
+ my $content = $response->content;
+ untaint($content);
+ $result = decode_json( $content );
+ 1;
+ };
if (!$result_ok || $result->{error_code}) {
ThrowCodeError('phabricator_api_error',
{ reason => 'JSON decode failure' }) if !$result_ok;
@@ -493,9 +203,8 @@ sub request {
sub get_security_sync_groups {
my $bug = shift;
- my $phab_sync_groups = Bugzilla->params->{phabricator_sync_groups}
- || ThrowUserError('invalid_phabricator_sync_groups');
- my $sync_group_names = [ split('[,\s]+', $phab_sync_groups) ];
+ my $sync_groups = Bugzilla::Group->match( { isactive => 1, isbuggroup => 1 } );
+ my $sync_group_names = [ map { $_->name } @$sync_groups ];
my $bug_groups = $bug->groups_in;
my $bug_group_names = [ map { $_->name } @$bug_groups ];
@@ -519,7 +228,7 @@ sub add_security_sync_comments {
my $phab_error_message = 'Revision is being made private due to unknown Bugzilla groups.';
foreach my $revision (@$revisions) {
- add_comment_to_revision( $revision->{phid}, $phab_error_message );
+ $revision->add_comment($phab_error_message);
}
my $num_revisions = scalar @$revisions;
@@ -536,4 +245,36 @@ sub add_security_sync_comments {
Bugzilla->set_user($old_user);
}
+sub get_needs_review {
+ my ($user) = @_;
+ $user //= Bugzilla->user;
+ return unless $user->id;
+
+ my $phab_user = Bugzilla::Extension::PhabBugz::User->new_from_query(
+ {
+ ids => [ $user->id ]
+ }
+ );
+
+ return [] unless $phab_user;
+
+ my $diffs = request(
+ 'differential.revision.search',
+ {
+ attachments => {
+ reviewers => 1,
+ },
+ constraints => {
+ reviewerPHIDs => [$phab_user->phid],
+ statuses => [qw( needs-review )],
+ },
+ order => 'newest',
+ }
+ );
+ ThrowCodeError('phabricator_api_error', { reason => 'Malformed Response' })
+ unless exists $diffs->{result}{data};
+
+ return $diffs->{result}{data};
+}
+
1;
diff --git a/extensions/PhabBugz/lib/WebService.pm b/extensions/PhabBugz/lib/WebService.pm
index 5b6310de6..0239ccf74 100644
--- a/extensions/PhabBugz/lib/WebService.pm
+++ b/extensions/PhabBugz/lib/WebService.pm
@@ -13,115 +13,44 @@ use warnings;
use base qw(Bugzilla::WebService);
-use Bugzilla::Attachment;
-use Bugzilla::Bug;
-use Bugzilla::BugMail;
use Bugzilla::Constants;
-use Bugzilla::Error;
-use Bugzilla::Extension::Push::Util qw(is_public);
use Bugzilla::User;
-use Bugzilla::Util qw(detaint_natural);
+use Bugzilla::Util qw(detaint_natural datetime_from time_ago trick_taint);
use Bugzilla::WebService::Constants;
use Bugzilla::Extension::PhabBugz::Constants;
use Bugzilla::Extension::PhabBugz::Util qw(
- add_security_sync_comments
- create_revision_attachment
- create_private_revision_policy
- edit_revision_policy
- get_bug_role_phids
- get_project_phid
- get_revisions_by_ids
- intersect
- is_attachment_phab_revision
- make_revision_public
- request
- get_security_sync_groups
+ get_needs_review
);
-use List::Util qw(first);
+use DateTime ();
+use List::Util qw(first uniq);
use List::MoreUtils qw(any);
use MIME::Base64 qw(decode_base64);
-use constant PUBLIC_METHODS => qw(
+use constant READ_ONLY => qw(
check_user_permission_for_bug
- obsolete_attachments
- revision
- update_reviewer_statuses
+ needs_review
);
-sub revision {
- my ($self, $params) = @_;
-
- # Phabricator only supports sending credentials via HTTP Basic Auth
- # so we exploit that function to pass in an API key as the password
- # of basic auth. BMO does not support basic auth but does support
- # use of API keys.
- my $http_auth = Bugzilla->cgi->http('Authorization');
- $http_auth =~ s/^Basic\s+//;
- $http_auth = decode_base64($http_auth);
- my ($login, $api_key) = split(':', $http_auth);
- $params->{'Bugzilla_login'} = $login;
- $params->{'Bugzilla_api_key'} = $api_key;
-
- my $user = Bugzilla->login(LOGIN_REQUIRED);
-
- # Prechecks
- _phabricator_precheck($user);
-
- unless (defined $params->{revision} && detaint_natural($params->{revision})) {
- ThrowCodeError('param_required', { param => 'revision' })
- }
-
- # Obtain more information about the revision from Phabricator
- my $revision_id = $params->{revision};
- my $revisions = get_revisions_by_ids([$revision_id]);
- my $revision = $revisions->[0];
-
- my $revision_phid = $revision->{phid};
- my $revision_title = $revision->{fields}{title} || 'Unknown Description';
- my $bug_id = $revision->{fields}{'bugzilla.bug-id'};
-
- my $bug = Bugzilla::Bug->new($bug_id);
-
- # If bug is public then remove privacy policy
- my $result;
- if (is_public($bug)) {
- $result = make_revision_public($revision_id);
- }
- # else bug is private
- else {
- my @set_groups = get_security_sync_groups($bug);
-
- # If bug privacy groups do not have any matching synchronized groups,
- # then leave revision private and it will have be dealt with manually.
- if (!@set_groups) {
- add_security_sync_comments($revisions, $bug);
- }
-
- my $policy_phid = create_private_revision_policy($bug, \@set_groups);
- my $subscribers = get_bug_role_phids($bug);
- $result = edit_revision_policy($revision_phid, $policy_phid, $subscribers);
- }
-
- my $attachment = create_revision_attachment($bug, $revision_id, $revision_title);
-
- Bugzilla::BugMail::Send($bug_id, { changer => $user });
-
- return {
- result => $result,
- attachment_id => $attachment->id,
- attachment_link => Bugzilla->localconfig->{urlbase} . "attachment.cgi?id=" . $attachment->id
- };
-}
+use constant PUBLIC_METHODS => qw(
+ check_user_permission_for_bug
+ needs_review
+ set_build_target
+);
sub check_user_permission_for_bug {
my ($self, $params) = @_;
my $user = Bugzilla->login(LOGIN_REQUIRED);
- # Prechecks
- _phabricator_precheck($user);
+ # Ensure PhabBugz is on
+ ThrowUserError('phabricator_not_enabled')
+ unless Bugzilla->params->{phabricator_enabled};
+
+ # Validate that the requesting user's email matches phab-bot
+ ThrowUserError('phabricator_unauthorized_user')
+ unless $user->login eq PHAB_AUTOMATION_USER;
# Validate that a bug id and user id are provided
ThrowUserError('phabricator_invalid_request_params')
@@ -136,184 +65,152 @@ sub check_user_permission_for_bug {
};
}
-sub update_reviewer_statuses {
+sub needs_review {
my ($self, $params) = @_;
-
+ ThrowUserError('phabricator_not_enabled')
+ unless Bugzilla->params->{phabricator_enabled};
my $user = Bugzilla->login(LOGIN_REQUIRED);
-
- # Prechecks
- _phabricator_precheck($user);
-
- my $revision_id = $params->{revision_id};
- unless (defined $revision_id && detaint_natural($revision_id)) {
- ThrowCodeError('param_required', { param => 'revision_id' })
- }
-
- my $bug_id = $params->{bug_id};
- unless (defined $bug_id && detaint_natural($bug_id)) {
- ThrowCodeError('param_required', { param => 'bug_id' })
+ my $dbh = Bugzilla->dbh;
+
+ my $reviews = get_needs_review();
+
+ my $authors = Bugzilla::Extension::PhabBugz::User->match({
+ phids => [
+ uniq
+ grep { defined }
+ map { $_->{fields}{authorPHID} }
+ @$reviews
+ ]
+ });
+
+ my %author_phab_to_id = map { $_->phid => $_->bugzilla_user->id } @$authors;
+ my %author_id_to_user = map { $_->bugzilla_user->id => $_->bugzilla_user } @$authors;
+
+ # bug data
+ my $visible_bugs = $user->visible_bugs([
+ uniq
+ grep { $_ }
+ map { $_->{fields}{'bugzilla.bug-id'} }
+ @$reviews
+ ]);
+
+ # get all bug statuses and summaries in a single query to avoid creation of
+ # many bug objects
+ my %bugs;
+ if (@$visible_bugs) {
+ #<<<
+ my $bug_rows =$dbh->selectall_arrayref(
+ 'SELECT bug_id, bug_status, short_desc ' .
+ ' FROM bugs ' .
+ ' WHERE bug_id IN (' . join(',', ('?') x @$visible_bugs) . ')',
+ { Slice => {} },
+ @$visible_bugs
+ );
+ #>>>
+ %bugs = map { $_->{bug_id} => $_ } @$bug_rows;
}
- my $accepted_user_ids = $params->{accepted_users};
- defined $accepted_user_ids
- || ThrowCodeError('param_required', { param => 'accepted_users' });
- $accepted_user_ids = [ split(':', $accepted_user_ids) ];
-
- my $denied_user_ids = $params->{denied_users};
- defined $denied_user_ids
- || ThrowCodeError('param_required', { param => 'denied_users' });
- $denied_user_ids = [ split(':', $denied_user_ids) ];
-
- my $bug = Bugzilla::Bug->check($bug_id);
-
- my @attachments =
- grep { is_attachment_phab_revision($_) } @{ $bug->attachments() };
-
- return { result => [] } if !@attachments;
-
- my $dbh = Bugzilla->dbh;
- my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
-
- my @updated_attach_ids;
- foreach my $attachment (@attachments) {
- my ($curr_revision_id) = ($attachment->filename =~ PHAB_ATTACHMENT_PATTERN);
- next if $revision_id != $curr_revision_id;
-
- # Clear old flags if no longer accepted
- my (@denied_flags, @new_flags, @removed_flags, %accepted_done, $flag_type);
- foreach my $flag (@{ $attachment->flags }) {
- next if $flag->type->name ne 'review';
- $flag_type = $flag->type if $flag->type->is_active;
- if (any { $flag->setter->id == $_ } @$denied_user_ids) {
- push(@denied_flags, { id => $flag->id, setter => $flag->setter, status => 'X' });
- }
- if (any { $flag->setter->id == $_ } @$accepted_user_ids) {
- $accepted_done{$flag->setter->id}++;
- }
- if ($flag->status eq '+'
- && !any { $flag->setter->id == $_ } (@$accepted_user_ids, @$denied_user_ids)) {
- push(@removed_flags, { id => $flag->id, setter => $flag->setter, status => 'X' });
- }
- }
-
- $flag_type ||= first { $_->name eq 'review' && $_->is_active } @{ $attachment->flag_types };
-
- # Create new flags
- foreach my $user_id (@$accepted_user_ids) {
- next if $accepted_done{$user_id};
- my $user = Bugzilla::User->check({ id => $user_id, cache => 1 });
- push(@new_flags, { type_id => $flag_type->id, setter => $user, status => '+' });
+ # build result
+ my $datetime_now = DateTime->now(time_zone => $user->timezone);
+ my @result;
+ foreach my $review (@$reviews) {
+ my $review_flat = {
+ id => $review->{id},
+ title => $review->{fields}{title},
+ url => Bugzilla->params->{phabricator_base_uri} . 'D' . $review->{id},
+ };
+
+ # show date in user's timezone
+ my $datetime = DateTime->from_epoch(
+ epoch => $review->{fields}{dateModified},
+ time_zone => 'UTC'
+ );
+ $datetime->set_time_zone($user->timezone);
+ $review_flat->{updated} = $datetime->strftime('%Y-%m-%d %T %Z');
+ $review_flat->{updated_fancy} = time_ago($datetime, $datetime_now);
+
+ # review requester
+ if (my $author = $author_id_to_user{$author_phab_to_id{ $review->{fields}{authorPHID} }}) {
+ $review_flat->{author_name} = $author->name;
+ $review_flat->{author_email} = $author->email;
}
-
- # Also add comment to for attachment update showing the user's name
- # that changed the revision.
- my $comment;
- foreach my $flag_data (@new_flags) {
- $comment .= $flag_data->{setter}->name . " has approved the revision.\n";
- }
- foreach my $flag_data (@denied_flags) {
- $comment .= $flag_data->{setter}->name . " has requested changes to the revision.\n";
- }
- foreach my $flag_data (@removed_flags) {
- $comment .= $flag_data->{setter}->name . " has been removed from the revision.\n";
+ else {
+ $review_flat->{author_name} = 'anonymous';
+ $review_flat->{author_email} = 'anonymous';
}
- if ($comment) {
- $comment .= "\n" . Bugzilla->params->{phabricator_base_uri} . "D" . $revision_id;
- # Add transaction_id as anchor if one present
- $comment .= "#" . $params->{transaction_id} if $params->{transaction_id};
- $bug->add_comment($comment, {
- isprivate => $attachment->isprivate,
- type => CMT_ATTACHMENT_UPDATED,
- extra_data => $attachment->id
- });
+ # referenced bug
+ if (my $bug_id = $review->{fields}{'bugzilla.bug-id'}) {
+ my $bug = $bugs{$bug_id};
+ $review_flat->{bug_id} = $bug_id;
+ $review_flat->{bug_status} = $bug->{bug_status};
+ $review_flat->{bug_summary} = $bug->{short_desc};
}
- $attachment->set_flags([ @denied_flags, @removed_flags ], \@new_flags);
- $attachment->update($timestamp);
- $bug->update($timestamp) if $comment;
-
- push(@updated_attach_ids, $attachment->id);
+ push @result, $review_flat;
}
- Bugzilla::BugMail::Send($bug_id, { changer => $user }) if @updated_attach_ids;
-
- return { result => \@updated_attach_ids };
+ return { result => \@result };
}
-sub obsolete_attachments {
- my ($self, $params) = @_;
+sub set_build_target {
+ my ( $self, $params ) = @_;
- my $user = Bugzilla->login(LOGIN_REQUIRED);
-
- # Prechecks
- _phabricator_precheck($user);
-
- my $revision_id = $params->{revision_id};
- unless (defined $revision_id && detaint_natural($revision_id)) {
- ThrowCodeError('param_required', { param => 'revision' })
- }
-
- my $bug_id= $params->{bug_id};
- unless (defined $bug_id && detaint_natural($bug_id)) {
- ThrowCodeError('param_required', { param => 'bug_id' })
- }
-
- my $make_obsolete = $params->{make_obsolete};
- unless (defined $make_obsolete) {
- ThrowCodeError('param_required', { param => 'make_obsolete' })
- }
- $make_obsolete = $make_obsolete ? 1 : 0;
-
- my $bug = Bugzilla::Bug->check($bug_id);
-
- my @attachments =
- grep { is_attachment_phab_revision($_) } @{ $bug->attachments() };
-
- return { result => [] } if !@attachments;
-
- my $dbh = Bugzilla->dbh;
- my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
-
- my @updated_attach_ids;
- foreach my $attachment (@attachments) {
- my ($curr_revision_id) = ($attachment->filename =~ PHAB_ATTACHMENT_PATTERN);
- next if $revision_id != $curr_revision_id;
+ # Phabricator only supports sending credentials via HTTP Basic Auth
+ # so we exploit that function to pass in an API key as the password
+ # of basic auth. BMO does not support basic auth but does support
+ # use of API keys.
+ my $http_auth = Bugzilla->cgi->http('Authorization');
+ $http_auth =~ s/^Basic\s+//;
+ $http_auth = decode_base64($http_auth);
+ my ($login, $api_key) = split(':', $http_auth);
+ $params->{'Bugzilla_login'} = $login;
+ $params->{'Bugzilla_api_key'} = $api_key;
- $attachment->set_is_obsolete($make_obsolete);
- $attachment->update($timestamp);
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
- push(@updated_attach_ids, $attachment->id);
- }
+ # Ensure PhabBugz is on
+ ThrowUserError('phabricator_not_enabled')
+ unless Bugzilla->params->{phabricator_enabled};
+
+ # Validate that the requesting user's email matches phab-bot
+ ThrowUserError('phabricator_unauthorized_user')
+ unless $user->login eq PHAB_AUTOMATION_USER;
- Bugzilla::BugMail::Send($bug_id, { changer => $user }) if @updated_attach_ids;
+ my $revision_id = $params->{revision_id};
+ my $build_target = $params->{build_target};
- return { result => \@updated_attach_ids };
-}
+ ThrowUserError('invalid_phabricator_revision_id')
+ unless detaint_natural($revision_id);
-sub _phabricator_precheck {
- my ($user) = @_;
+ ThrowUserError('invalid_phabricator_build_target')
+ unless $build_target =~ /^PHID-HMBT-[a-zA-Z0-9]+$/;
+ trick_taint($build_target);
- # Ensure PhabBugz is on
- ThrowUserError('phabricator_not_enabled')
- unless Bugzilla->params->{phabricator_enabled};
+ Bugzilla->dbh->do(
+ "INSERT INTO phabbugz (name, value) VALUES (?, ?)",
+ undef,
+ 'build_target_' . $revision_id,
+ $build_target
+ );
- # Validate that the requesting user's email matches phab-bot
- ThrowUserError('phabricator_unauthorized_user')
- unless $user->login eq PHAB_AUTOMATION_USER;
+ return { result => 1 };
}
sub rest_resources {
return [
- # Revision creation
- qr{^/phabbugz/revision/([^/]+)$}, {
+ # Set build target in Phabricator
+ qr{^/phabbugz/build_target/(\d+)/(PHID-HMBT-.*)$}, {
POST => {
- method => 'revision',
+ method => 'set_build_target',
params => sub {
- return { revision => $_[0] };
+ return {
+ revision_id => $_[0],
+ build_target => $_[1]
+ };
}
}
- },
+ },
# Bug permission checks
qr{^/phabbugz/check_bug/(\d+)/(\d+)$}, {
GET => {
@@ -323,17 +220,11 @@ sub rest_resources {
}
}
},
- # Update reviewer statuses
- qr{^/phabbugz/update_reviewer_statuses$}, {
- PUT => {
- method => 'update_reviewer_statuses',
- }
- },
- # Obsolete attachments
- qr{^/phabbugz/obsolete$}, {
- PUT => {
- method => 'obsolete_attachments',
- }
+ # Review requests
+ qw{^/phabbugz/needs_review$}, {
+ GET => {
+ method => 'needs_review',
+ },
}
];
}
diff --git a/extensions/PhabBugz/template/en/default/admin/email/squatter-alert.txt.tmpl b/extensions/PhabBugz/template/en/default/admin/email/squatter-alert.txt.tmpl
new file mode 100644
index 000000000..98e92a379
--- /dev/null
+++ b/extensions/PhabBugz/template/en/default/admin/email/squatter-alert.txt.tmpl
@@ -0,0 +1,34 @@
+[%# 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.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+From: [% Param('mailfrom') %]
+To: phabricator-admin@mozilla.com
+Subject: Possible Phabricator Username Squatter Alert
+Date: [% date %]
+X-Bugzilla-Type: squatter-alert
+
+Possible username squatter:
+
+Phabricator Account
+
+login: [% phab_user_login %]
+realname: [% phab_user_realname %]
+
+Bugzilla Account Matching Phabricator Account
+
+user id: [% bugzilla_userid %]
+login: [% bugzilla_login %]
+realname: [% bugzilla_realname %]
+
+Possible Bugzilla Account Squatting On
+
+user id: [% squat_userid %]
+login: [% squat_login %]
+realname: [% squat_realname %]
diff --git a/extensions/PhabBugz/template/en/default/admin/params/phabbugz.html.tmpl b/extensions/PhabBugz/template/en/default/admin/params/phabbugz.html.tmpl
index d67839cc8..1b5bdda4b 100644
--- a/extensions/PhabBugz/template/en/default/admin/params/phabbugz.html.tmpl
+++ b/extensions/PhabBugz/template/en/default/admin/params/phabbugz.html.tmpl
@@ -16,7 +16,6 @@
phabricator_enabled => 'Enable Phabricator Integration',
phabricator_base_uri => 'Phabricator Base URI',
phabricator_api_key => 'Phabricator User API Key',
- phabricator_sync_groups => 'Comma delimited list of Bugzilla groups to sync to Phabricator projects',
phabricator_auth_callback_url => 'Phabricator Auth Delegation URL',
phabricator_app_id => 'app_id for API Keys delegated to Phabricator',
}
diff --git a/extensions/PhabBugz/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/PhabBugz/template/en/default/hook/global/user-error-errors.html.tmpl
index 1457e3525..0274f72ce 100644
--- a/extensions/PhabBugz/template/en/default/hook/global/user-error-errors.html.tmpl
+++ b/extensions/PhabBugz/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -14,15 +14,18 @@
[% title = "Invalid Phabricator API Key" %]
You must provide a valid Phabricator API Key.
-[% ELSIF error == "invalid_phabricator_sync_groups" %]
- [% title = "Invalid Phabricator Sync Groups" %]
- You must provide a comma delimited list of security groups
- to sync with Phabricator.
+[% ELSIF error == "invalid_phabricator_projects" %]
+ [% title = "Invalid Phabricator Projects" %]
+ One or more Phabricator projects selected are invalid.
[% ELSIF error == "invalid_phabricator_revision_id" %]
[% title = "Invalid Phabricator Revision ID" %]
You must provide a valid Phabricator revision ID.
+[% ELSIF error == "invalid_phabricator_build_target" %]
+ [% title = "Invalid Phabricator Build Target" %]
+ You must provide a valid Phabricator Build Target PHID.
+
[% ELSIF error == "phabricator_not_enabled" %]
[% title = "Phabricator Support Not Enabled" %]
The Phabricator to Bugzilla library, PhabBugz,
diff --git a/extensions/ProdCompSearch/web/js/prod_comp_search.js b/extensions/ProdCompSearch/web/js/prod_comp_search.js
index 1b7e396a8..30351a3db 100644
--- a/extensions/ProdCompSearch/web/js/prod_comp_search.js
+++ b/extensions/ProdCompSearch/web/js/prod_comp_search.js
@@ -66,6 +66,8 @@ $(function() {
params.Bugzilla_api_token = BUGZILLA.api_token;
}
that.devbridgeAutocomplete({
+ appendTo: $('#main-inner'),
+ forceFixPosition: true,
serviceUrl: function(query) {
return 'rest/prod_comp_search/' + encodeURIComponent(query);
},
diff --git a/extensions/Push/lib/Backoff.pm b/extensions/Push/lib/Backoff.pm
index f0116a2a7..0436cdf14 100644
--- a/extensions/Push/lib/Backoff.pm
+++ b/extensions/Push/lib/Backoff.pm
@@ -19,6 +19,7 @@ use constant AUDIT_REMOVES => 0;
use constant USE_MEMCACHED => 0;
use Bugzilla;
+use Bugzilla::Logging;
use Bugzilla::Util;
#
@@ -67,9 +68,7 @@ sub reset {
my ($self) = @_;
$self->{next_attempt_ts} = Bugzilla->dbh->selectrow_array('SELECT NOW()');
$self->{attempts} = 0;
- Bugzilla->push_ext->logger->debug(
- sprintf("resetting backoff for %s", $self->connector)
- );
+ INFO( sprintf 'resetting backoff for %s', $self->connector );
}
sub inc {
@@ -82,8 +81,8 @@ sub inc {
$self->{next_attempt_ts} = $date;
$self->{attempts} = $attempts;
- Bugzilla->push_ext->logger->debug(
- sprintf("setting next attempt for %s to %s (attempt %s)", $self->connector, $date, $attempts)
+ INFO(
+ sprintf 'setting next attempt for %s to %s (attempt %s)', $self->connector, $date, $attempts
);
}
diff --git a/extensions/Push/lib/Config.pm b/extensions/Push/lib/Config.pm
index 337c2696d..2db95b972 100644
--- a/extensions/Push/lib/Config.pm
+++ b/extensions/Push/lib/Config.pm
@@ -11,7 +11,7 @@ use 5.10.1;
use strict;
use warnings;
-use Bugzilla;
+use Bugzilla::Logging;
use Bugzilla::Constants;
use Bugzilla::Extension::Push::Option;
use Crypt::CBC;
@@ -52,7 +52,6 @@ sub option {
sub load {
my ($self) = @_;
my $config = {};
- my $logger = Bugzilla->push_ext->logger;
# prime $config with defaults
foreach my $rh ($self->options) {
@@ -81,7 +80,7 @@ sub load {
# done, update self
foreach my $name (keys %$config) {
my $value = $self->option($name)->{type} eq 'password' ? '********' : $config->{$name};
- $logger->debug(sprintf("%s: set %s=%s\n", $self->{_name}, $name, $value || ''));
+ TRACE( sprintf "%s: set %s=%s\n", $self->{_name}, $name, $value || '' );
$self->{$name} = $config->{$name};
}
}
diff --git a/extensions/Push/lib/Connector/Phabricator.pm b/extensions/Push/lib/Connector/Phabricator.pm
index 5da64901a..e59ba6c0d 100644
--- a/extensions/Push/lib/Connector/Phabricator.pm
+++ b/extensions/Push/lib/Connector/Phabricator.pm
@@ -15,24 +15,18 @@ use base 'Bugzilla::Extension::Push::Connector::Base';
use Bugzilla::Bug;
use Bugzilla::Constants;
-use Bugzilla::Error;
-use Bugzilla::User;
use Bugzilla::Extension::PhabBugz::Constants;
+use Bugzilla::Extension::PhabBugz::Policy;
+use Bugzilla::Extension::PhabBugz::Project;
+use Bugzilla::Extension::PhabBugz::Revision;
use Bugzilla::Extension::PhabBugz::Util qw(
- add_comment_to_revision
add_security_sync_comments
- create_private_revision_policy
- edit_revision_policy
get_attachment_revisions
get_bug_role_phids
- get_project_phid
get_security_sync_groups
- intersect
- make_revision_public
- make_revision_private
- set_revision_subscribers
);
+
use Bugzilla::Extension::Push::Constants;
use Bugzilla::Extension::Push::Util qw(is_public);
@@ -76,72 +70,55 @@ sub send {
my @set_groups = get_security_sync_groups($bug);
- my @revisions = get_attachment_revisions($bug);
-
- if (!$is_public && !@set_groups) {
- foreach my $revision (@revisions) {
- Bugzilla->audit(sprintf(
- 'Making revision %s for bug %s private due to unkown Bugzilla groups: %s',
- $revision->{id},
- $bug->id,
- join(', ', @set_groups)
- ));
- make_revision_private( $revision->{phid} );
- }
-
- add_security_sync_comments(\@revisions, $bug);
-
- return PUSH_RESULT_OK;
- }
+ my $revisions = get_attachment_revisions($bug);
my $group_change =
($message->routing_key =~ /^(?:attachment|bug)\.modify:.*\bbug_group\b/)
? 1
: 0;
- my $subscribers;
- if ( !$is_public ) {
- $subscribers = get_bug_role_phids($bug);
- }
-
- my $secure_project_phid = get_project_phid('secure-revision');
-
- foreach my $revision (@revisions) {
- my $revision_phid = $revision->{phid};
-
- my $rev_obj = Bugzilla::Extension::PhabBugz::Revision->new_from_query({ phids => [ $revision_phid ] });
- my $revision_updated;
-
+ foreach my $revision (@$revisions) {
if ( $is_public && $group_change ) {
Bugzilla->audit(sprintf(
'Making revision %s public for bug %s',
- $revision->{id},
+ $revision->id,
$bug->id
));
- make_revision_public($revision_phid);
- $rev_obj->remove_project($secure_project_phid);
- $revision_updated = 1;
+ $revision->make_public();
+ }
+ elsif ( !$is_public && !@set_groups ) {
+ Bugzilla->audit(sprintf(
+ 'Making revision %s for bug %s private due to unkown Bugzilla groups: %s',
+ $revision->id,
+ $bug->id,
+ join(', ', @set_groups)
+ ));
+ $revision->make_private(['secure-revision']);
+ add_security_sync_comments([$revision], $bug);
}
elsif ( !$is_public && $group_change ) {
Bugzilla->audit(sprintf(
'Giving revision %s a custom policy for bug %s',
- $revision->{id},
+ $revision->id,
$bug->id
));
- my $policy_phid = create_private_revision_policy( $bug, \@set_groups );
- edit_revision_policy( $revision_phid, $policy_phid, $subscribers );
- $rev_obj->add_project($secure_project_phid);
- $revision_updated = 1;
+ my @set_project_names = map { "bmo-" . $_ } @set_groups;
+ $revision->make_private(\@set_project_names);
}
- elsif ( !$is_public && !$group_change ) {
+
+ # Subscriber list of the private revision should always match
+ # the bug roles such as assignee, qa contact, and cc members.
+ if (!$is_public) {
Bugzilla->audit(sprintf(
'Updating subscribers for %s for bug %s',
- $revision->{id},
+ $revision->id,
$bug->id
));
- set_revision_subscribers( $revision_phid, $subscribers );
+ my $subscribers = get_bug_role_phids($bug);
+ $revision->set_subscribers($subscribers) if $subscribers;
}
- $rev_obj->update() if $revision_updated;
+
+ $revision->update();
}
return PUSH_RESULT_OK;
diff --git a/extensions/Push/lib/Connectors.pm b/extensions/Push/lib/Connectors.pm
index 75a5083ff..d3c55d3ca 100644
--- a/extensions/Push/lib/Connectors.pm
+++ b/extensions/Push/lib/Connectors.pm
@@ -11,10 +11,12 @@ use 5.10.1;
use strict;
use warnings;
+use Bugzilla::Logging;
use Bugzilla::Extension::Push::Util;
use Bugzilla::Constants;
use Bugzilla::Util qw(trick_taint);
use File::Basename;
+use Try::Tiny;
sub new {
my ($class) = @_;
@@ -25,16 +27,15 @@ sub new {
$self->{objects} = {};
$self->{path} = bz_locations->{'extensionsdir'} . '/Push/lib/Connector';
- my $logger = Bugzilla->push_ext->logger;
foreach my $file (glob($self->{path} . '/*.pm')) {
my $name = basename($file);
$name =~ s/\.pm$//;
next if $name eq 'Base';
if (length($name) > 32) {
- $logger->info("Ignoring connector '$name': Name longer than 32 characters");
+ WARN("Ignoring connector '$name': Name longer than 32 characters");
}
push @{$self->{names}}, $name;
- $logger->debug("Found connector '$name'");
+ TRACE("Found connector '$name'");
}
return $self;
@@ -44,7 +45,6 @@ sub _load {
my ($self) = @_;
return if scalar keys %{$self->{objects}};
- my $logger = Bugzilla->push_ext->logger;
foreach my $name (@{$self->{names}}) {
next if exists $self->{objects}->{$name};
my $file = $self->{path} . "/$name.pm";
@@ -52,34 +52,30 @@ sub _load {
require $file;
my $package = "Bugzilla::Extension::Push::Connector::$name";
- $logger->debug("Loading connector '$name'");
+ TRACE("Loading connector '$name'");
my $old_error_mode = Bugzilla->error_mode;
Bugzilla->error_mode(ERROR_MODE_DIE);
- eval {
+ try {
my $connector = $package->new();
$connector->load_config();
$self->{objects}->{$name} = $connector;
+ } catch {
+ ERROR("Connector '$name' failed to load: " . clean_error($_));
};
- if ($@) {
- $logger->error("Connector '$name' failed to load: " . clean_error($@));
- }
Bugzilla->error_mode($old_error_mode);
}
}
sub stop {
my ($self) = @_;
- my $logger = Bugzilla->push_ext->logger;
foreach my $connector ($self->list) {
next unless $connector->enabled;
- $logger->debug("Stopping '" . $connector->name . "'");
- eval {
+ TRACE("Stopping '" . $connector->name . "'");
+ try {
$connector->stop();
+ } catch {
+ ERROR("Connector '" . $connector->name . "' failed to stop: " . clean_error($_));
};
- if ($@) {
- $logger->error("Connector '" . $connector->name . "' failed to stop: " . clean_error($@));
- $logger->debug("Connector '" . $connector->name . "' failed to stop: $@");
- }
}
}
diff --git a/extensions/Push/lib/Push.pm b/extensions/Push/lib/Push.pm
index 45b9b05dd..670b2aa56 100644
--- a/extensions/Push/lib/Push.pm
+++ b/extensions/Push/lib/Push.pm
@@ -11,6 +11,7 @@ use 5.10.1;
use strict;
use warnings;
+use Bugzilla::Logging;
use Bugzilla::Extension::Push::BacklogMessage;
use Bugzilla::Extension::Push::Config;
use Bugzilla::Extension::Push::Connectors;
@@ -107,7 +108,7 @@ sub push {
# if the connector is backlogged, push to the backlog queue
if ($is_backlogged) {
- $logger->debug("backlogged");
+ INFO('connector is backlogged');
my $backlog = Bugzilla::Extension::Push::BacklogMessage->create_from_message($message, $connector);
}
}
diff --git a/extensions/Review/Extension.pm b/extensions/Review/Extension.pm
index 576c31c7e..72f16e3b6 100644
--- a/extensions/Review/Extension.pm
+++ b/extensions/Review/Extension.pm
@@ -94,7 +94,7 @@ sub _user_is_active {
my ($self) = @_;
# never consider .bugs or .tld addresses as inactive.
- return 1 if $self->login =~ /bugs$/ || $self->login =~ /\.tld$/;
+ return 1 if $self->login =~ /\.(?:bugs|tld)$/;
return 1 unless Bugzilla->params->{max_reviewer_last_seen};
return 0 if !defined($self->last_seen_date);
diff --git a/extensions/SecureMail/Extension.pm b/extensions/SecureMail/Extension.pm
index 1595fe98c..2b5e1bdd6 100644
--- a/extensions/SecureMail/Extension.pm
+++ b/extensions/SecureMail/Extension.pm
@@ -27,6 +27,7 @@ use warnings;
use base qw(Bugzilla::Extension);
+use Bugzilla::Logging;
use Bugzilla::Attachment;
use Bugzilla::Comment;
use Bugzilla::Group;
@@ -35,6 +36,7 @@ use Bugzilla::User;
use Bugzilla::Util qw(trim trick_taint is_7bit_clean);
use Bugzilla::Error;
use Bugzilla::Mailer;
+use Bugzilla::Extension::SecureMail::TCT;
use Crypt::OpenPGP::Armour;
use Crypt::OpenPGP::KeyRing;
@@ -128,9 +130,12 @@ sub object_validators {
if ($value =~ /PUBLIC KEY/) {
# PGP keys must be ASCII-armoured.
- if (!Crypt::OpenPGP::Armour->unarmour($value)) {
- ThrowUserError('securemail_invalid_key',
- { errstr => Crypt::OpenPGP::Armour->errstr });
+ my $tct = Bugzilla::Extension::SecureMail::TCT->new(
+ public_key => $value,
+ command => Bugzilla->localconfig->{tct_bin},
+ );
+ unless ($tct->is_valid->get) {
+ ThrowUserError( 'securemail_invalid_key', { errstr => 'key is invalid or expired' } );
}
}
elsif ($value =~ /BEGIN CERTIFICATE/) {
@@ -471,8 +476,10 @@ sub _make_secure {
# PGP Encryption #
##################
- my $pubring = new Crypt::OpenPGP::KeyRing(Data => $key);
- my $pgp = new Crypt::OpenPGP(PubRing => $pubring);
+ my $tct = Bugzilla::Extension::SecureMail::TCT->new(
+ public_key => $key,
+ command => Bugzilla->localconfig->{tct_bin},
+ );
if (scalar $email->parts > 1) {
my $old_boundary = $email->{ct}{attributes}{boundary};
@@ -511,7 +518,7 @@ sub _make_secure {
disposition => 'inline',
encoding => '7bit',
},
- body => _pgp_encrypt($pgp, $to_encrypt, $bug_id)
+ body => _tct_encrypt($tct, $to_encrypt, $bug_id)
),
);
$email->parts_set(\@new_parts);
@@ -528,7 +535,7 @@ sub _make_secure {
if ($sanitise_subject) {
_insert_subject($email, $subject);
}
- $email->body_set(_pgp_encrypt($pgp, $email->body, $bug_id));
+ $email->body_set(_tct_encrypt($tct, $email->body, $bug_id));
}
}
@@ -604,33 +611,21 @@ sub _make_secure {
}
}
-sub _pgp_encrypt {
- my ($pgp, $text, $bug_id) = @_;
- # "@" matches every key in the public key ring, which is fine,
- # because there's only one key in our keyring.
- #
- # We use the CAST5 cipher because the Rijndael (AES) module doesn't
- # like us for some reason I don't have time to debug fully.
- # ("key must be an untainted string scalar")
- my $encrypted = $pgp->encrypt(
- Data => $text,
- Recipients => "@",
- Cipher => 'CAST5',
- Armour => 0
- );
- if (!defined $encrypted) {
- return 'Error during Encryption: ' . $pgp->errstr;
+sub _tct_encrypt {
+ my ($tct, $text, $bug_id) = @_;
+
+ my $comment = Bugzilla->localconfig->{urlbase} . ( $bug_id ? 'show_bug.cgi?id=' . $bug_id : '' );
+ my $encrypted;
+ my $ok = eval { $encrypted = $tct->encrypt( $text, $comment )->get; 1 };
+ if (!$ok) {
+ WARN("Error: $@");
+ $encrypted = "$comment\nOpenPGP Encryption failed. Check if your key is expired.";
}
- $encrypted = Crypt::OpenPGP::Armour->armour(
- Data => $encrypted,
- Object => 'MESSAGE',
- Headers => {
- Comment => Bugzilla->localconfig->{urlbase} . ($bug_id ? 'show_bug.cgi?id=' . $bug_id : ''),
- },
- );
- # until Crypt::OpenPGP makes the Version header optional we have to strip
- # it out manually (bug 1181406).
- $encrypted =~ s/\nVersion:[^\n]+//;
+ elsif (!$encrypted) {
+ WARN('message empty!');
+ $encrypted = "$comment\nOpenPGP Encryption failed for unknown reason.";
+ }
+
return $encrypted;
}
diff --git a/extensions/SecureMail/lib/TCT.pm b/extensions/SecureMail/lib/TCT.pm
new file mode 100644
index 000000000..3a16309c2
--- /dev/null
+++ b/extensions/SecureMail/lib/TCT.pm
@@ -0,0 +1,112 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::SecureMail::TCT;
+use 5.10.1;
+use Moo;
+
+use Bugzilla::DaemonControl qw( on_finish on_exception );
+use File::Temp;
+use Future::Utils qw(call);
+use Future;
+use IO::Async::Process;
+
+has 'public_key' => ( is => 'ro', required => 1 );
+has 'public_key_file' => ( is => 'lazy' );
+has 'is_valid' => ( is => 'lazy' );
+has 'command' => ( is => 'ro', default => 'tct' );
+
+sub _build_public_key_file {
+ my ($self) = @_;
+ my $fh = File::Temp->new(SUFFIX => '.pubkey');
+ $fh->print($self->public_key);
+ $fh->close;
+ return $fh;
+}
+
+sub _build_is_valid {
+ my ($self) = @_;
+
+ my $loop = IO::Async::Loop->new;
+ my $exit_f = $loop->new_future;
+ my ($stderr, $stdout);
+ my $process = IO::Async::Process->new(
+ command => [$self->command, 'check', '-k', $self->public_key_file ],
+ stderr => {
+ into => \$stderr,
+ },
+ stdout => {
+ into => \$stdout,
+ },
+ on_finish => on_finish($exit_f),
+ on_exception => on_exception($self->command, $exit_f),
+ );
+ $loop->add($process);
+
+ return $exit_f->then(
+ sub {
+ my ($rv) = @_;
+ Future->wrap($rv == 0);
+ }
+ );
+}
+
+sub encrypt {
+ my ($self, $input, $comment) = @_;
+ $self->is_valid->then(
+ sub {
+ my ($is_valid) = @_;
+ call {
+ die 'invalid public key!' unless $is_valid;
+
+ my $output;
+ my $loop = IO::Async::Loop->new;
+ my $exit_f = $loop->new_future;
+ my @command = ( $self->command, 'encrypt', '-k', $self->public_key_file );
+ push @command, '--comment', $comment if $comment;
+ my $process = IO::Async::Process->new(
+ command => \@command,
+ stdin => {
+ from => $input,
+ },
+ stdout => {
+ into => \$output,
+ },
+ on_finish => on_finish($exit_f),
+ on_exception => on_exception($self->command, $exit_f),
+ );
+ $loop->add($process);
+
+ return $exit_f->then(sub { Future->wrap($output) });
+ }
+ }
+ );
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Extension::SecureMail::TCT - An interface to the tct program
+
+=head1 SYNOPSIS
+
+ my $key = <<'PUBLIC_KEY';
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+
+ mQINBFakJSsBEACbDwHztgZaVhIb6f4PN0KbXv5BEciqKNbdVLgWQJyqgEMIwTF7
+ ...
+ o858gRM=
+ =t9lA
+ -----END PGP PUBLIC KEY BLOCK-----
+ PUBLIC_KEY
+
+ my $tct = Bugzilla::Extension::SecureMail::TCT->new(public_key => $key);
+ my $encrypted = $tct->encrypt("message", "comment goes here")->get;
+
diff --git a/extensions/SecureMail/template/en/default/hook/admin/users/userdata-end.html.tmpl b/extensions/SecureMail/template/en/default/hook/admin/users/userdata-end.html.tmpl
index a90266dae..e5e299ef9 100644
--- a/extensions/SecureMail/template/en/default/hook/admin/users/userdata-end.html.tmpl
+++ b/extensions/SecureMail/template/en/default/hook/admin/users/userdata-end.html.tmpl
@@ -6,7 +6,7 @@
# defined by the Mozilla Public License, v. 2.0.
#%]
-[% RETURN UNLESS otheruser.id %]
+[% RETURN UNLESS otheruser.id && user.in_group('editusers') %]
<tr>
<th>Has Secure Mail Key/Cert:</th>
@@ -14,7 +14,7 @@
[% otheruser.public_key ? "Yes" : "No" %]
</td>
</tr>
-
+
<tr>
<th>Member of Secure Mail Group:</th>
<td>
diff --git a/helper.psgi b/helper.psgi
deleted file mode 100644
index cc8c648a8..000000000
--- a/helper.psgi
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/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 Plack::Request;
-use Plack::Response;
-
-my $app = sub {
- my $env = shift;
- my $req = Plack::Request->new($env);
- my $res = Plack::Response->new(404);
- my $urlbase = Bugzilla->localconfig->{urlbase};
- my $path = $req->path;
-
- if ( $path eq '/quicksearch.html' ) {
- $res->redirect( $urlbase . 'page.cgi?id=quicksearch.html', 301 );
- }
- elsif ( $path eq '/bugwritinghelp.html') {
- $res->redirect( $urlbase . 'page.cgi?id=bug-writing.html', 301 );
- }
- elsif ( $path =~ m{^/(\d+)$}s ) {
- $res->redirect( $urlbase . "show_bug.cgi?id=$1", 301 );
- }
- else {
- $res->body('not found');
- }
- return $res->finalize;
-}; \ No newline at end of file
diff --git a/js/attachment.js b/js/attachment.js
index f967f64d3..6d6dae58d 100644
--- a/js/attachment.js
+++ b/js/attachment.js
@@ -93,6 +93,21 @@ function DataFieldHandler() {
}
}
}
+
+ // Check the current file size (in KB)
+ const file_size = field_data.files[0].size / 1024;
+ const max_size = BUGZILLA.param.maxattachmentsize;
+ const invalid = file_size > max_size;
+ const message = invalid ? `This file (<strong>${(file_size / 1024).toFixed(1)} MB</strong>) is larger than the ` +
+ `maximum allowed size (<strong>${(max_size / 1024).toFixed(1)} MB</strong>).<br>Please consider uploading it ` +
+ `to an online file storage and sharing the link in a bug comment instead.` : '';
+ const message_short = invalid ? 'File too large' : '';
+ const $error = document.querySelector('#data-error');
+
+ // Show an error message if the file is too large
+ $error.innerHTML = message;
+ field_data.setCustomValidity(message_short);
+ field_data.setAttribute('aria-invalid', invalid);
}
function clearAttachmentFields() {
diff --git a/js/comment-tagging.js b/js/comment-tagging.js
index 3897d46b8..44ca8b4ab 100644
--- a/js/comment-tagging.js
+++ b/js/comment-tagging.js
@@ -35,6 +35,8 @@ YAHOO.bugzilla.commentTagging = {
if (!can_edit) return;
$('#bz_ctag_add').devbridgeAutocomplete({
+ appendTo: $('#main-inner'),
+ forceFixPosition: true,
serviceUrl: function(query) {
return 'rest/bug/comment/tags/' + encodeURIComponent(query);
},
diff --git a/js/field.js b/js/field.js
index 85011342d..a5e204f8d 100644
--- a/js/field.js
+++ b/js/field.js
@@ -713,6 +713,8 @@ $(function() {
}
var options_user = {
+ appendTo: $('#main-inner'),
+ forceFixPosition: true,
serviceUrl: 'rest/elastic/suggest_users',
params: {
Bugzilla_api_token: BUGZILLA.api_token,
@@ -792,6 +794,8 @@ $(function() {
.each(function() {
var that = $(this);
that.devbridgeAutocomplete({
+ appendTo: $('#main-inner'),
+ forceFixPosition: true,
lookup: function(query, done) {
var values = BUGZILLA.autocomplete_values[that.data('values')];
query = query.toLowerCase();
diff --git a/js/global.js b/js/global.js
index 68aceb03f..d0396d6a8 100644
--- a/js/global.js
+++ b/js/global.js
@@ -219,7 +219,7 @@ const detect_blocked_gravatars = () => {
*/
const adjust_scroll_onload = () => {
if (location.hash) {
- const $target = document.querySelector(location.hash);
+ const $target = document.querySelector(CSS.escape(location.hash));
if ($target) {
window.setTimeout(() => scroll_element_into_view($target), 50);
diff --git a/metrics.pl b/metrics.pl
deleted file mode 100755
index b181ebf32..000000000
--- a/metrics.pl
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/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 lib qw(. lib local/lib/perl5);
-
-BEGIN { delete $ENV{SERVER_SOFTWARE}; }
-
-use Bugzilla;
-use Bugzilla::Constants;
-use POSIX qw(setsid nice);
-
-Bugzilla->metrics_enabled(0);
-Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
-nice(19);
-
-# grab reporter class and filename
-exit(1) unless my $reporter_class = shift;
-exit(1) unless my $filename = shift;
-
-# create reporter object and report
-eval "use $reporter_class";
-
-# detach
-if ($reporter_class->DETACH) {
- open(STDIN, '<', '/dev/null');
- open(STDOUT, '>', '/dev/null');
- open(STDERR, '>', '/dev/null');
- setsid();
-}
-
-# report
-exit(1) unless my $reporter = $reporter_class->new({ json_filename => $filename });
-$reporter->report();
diff --git a/mod_perl.pl b/mod_perl.pl
index a49af6b80..b5a038f0a 100644
--- a/mod_perl.pl
+++ b/mod_perl.pl
@@ -83,7 +83,7 @@ Bugzilla::CGI->compile(qw(:cgi :push));
# sharing with the other httpd processes.
my $limit = Bugzilla->localconfig->{apache_size_limit};
if ($OSNAME eq 'linux' && ! eval { require Linux::Smaps }) {
- warn "SizeLimit requires Linux::Smaps on linux. size limit set to 800MB";
+ WARN('SizeLimit requires Linux::Smaps on linux. size limit set to 800MB');
$limit = 800_000;
}
Apache2::SizeLimit->set_max_unshared_size($limit);
@@ -170,13 +170,11 @@ sub handler : method {
DB::enable_profile($file);
}
Bugzilla::init_page();
- my $start = Time::HiRes::time();
my $result = $class->SUPER::handler(@_);
if (Bugzilla::ModPerl::USE_NYTPROF) {
DB::disable_profile();
DB::finish_profile();
}
- warn "[request_time] ", Bugzilla->cgi->request_uri, " took ", Time::HiRes::time() - $start, " seconds to execute";
# When returning data from the REST api we must only return 200 or 304,
# which tells Apache not to append its error html documents to the
diff --git a/page.cgi b/page.cgi
index cd2e927cd..3784b92ed 100755
--- a/page.cgi
+++ b/page.cgi
@@ -65,11 +65,6 @@ if ($id) {
ThrowCodeError("bad_page_cgi_id", { "page_id" => $id });
}
- # BMO - append template filename to metrics data
- if (Bugzilla->metrics_enabled) {
- Bugzilla->metrics->name("page.cgi $id");
- }
-
my %vars = (
quicksearch_field_names => \&quicksearch_field_names,
);
diff --git a/scripts/bloomfilter-populate.pl b/scripts/bloomfilter-populate.pl
index c591a61b3..780e98bd0 100755
--- a/scripts/bloomfilter-populate.pl
+++ b/scripts/bloomfilter-populate.pl
@@ -14,8 +14,6 @@ use Bugzilla::Bloomfilter;
# set Bugzilla usage mode to USAGE_MODE_CMDLINE
Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
-my $name = shift @ARGV or die "usage: $0 \$name < list\n";
-my @lines = <STDIN>;
-chomp @lines;
-Bugzilla::Bloomfilter->populate($name, \@lines);
+my $name = shift @ARGV or die "usage: $0 \$name\n";
+Bugzilla::Bloomfilter->populate($name);
diff --git a/scripts/generate_conduit_data.pl b/scripts/generate_conduit_data.pl
index 50a7379f4..541afb52a 100755
--- a/scripts/generate_conduit_data.pl
+++ b/scripts/generate_conduit_data.pl
@@ -133,7 +133,6 @@ set_params(
password_check_on_login => 0,
phabricator_base_uri => 'http://phabricator.test/',
phabricator_enabled => 1,
- phabricator_sync_groups => 'core-security',
);
set_push_connector_options();
diff --git a/scripts/nagios_blocker_checker.pl b/scripts/nagios_blocker_checker.pl
index 8c4277507..01a7b7348 100755
--- a/scripts/nagios_blocker_checker.pl
+++ b/scripts/nagios_blocker_checker.pl
@@ -12,10 +12,10 @@ use warnings;
use lib qw(. lib local/lib/perl5);
use Bugzilla;
+use Bugzilla::Logging;
use Bugzilla::Constants;
use Bugzilla::Product;
use Bugzilla::User;
-use Bugzilla::Sentry;
use Getopt::Long;
use English qw(-no_match_vars);
@@ -141,7 +141,7 @@ try {
# nagios check does no good, we terminate if we stick around too long.
local $SIG{ALRM} = sub {
my $message = "$PROGRAM_NAME ran for longer than $config->{max_runtime} seconds and was auto-terminated.";
- sentry_handle_error('error', $message);
+ FATAL($message);
die "$message\n";
};
alarm($config->{max_runtime});
diff --git a/scripts/remove-non-public-data.pl b/scripts/remove-non-public-data.pl
index fd379af79..ce7948dd0 100755
--- a/scripts/remove-non-public-data.pl
+++ b/scripts/remove-non-public-data.pl
@@ -9,8 +9,17 @@
use 5.10.1;
use strict;
use warnings;
-use lib qw(. lib local/lib/perl5);
+use File::Basename qw(dirname);
+use File::Spec::Functions qw(catfile catdir rel2abs);
+use Cwd qw(realpath);
+BEGIN {
+ require lib;
+ my $dir = rel2abs(catdir(dirname(__FILE__), '..'));
+ lib->import($dir, catdir($dir, 'lib'), catdir($dir, qw(local lib perl5)));
+}
+
+use autodie;
use Bugzilla;
use Bugzilla::Constants;
use List::MoreUtils qw(any);
@@ -130,7 +139,8 @@ my $dbh = Bugzilla->dbh;
# run sanitiseme.pl
print "running sanitizeme.pl\n";
-system "'$RealBin/sanitizeme.pl' --execute";
+my $sanitizeme = catfile(realpath(dirname(__FILE__)), 'sanitizeme.pl');
+system $sanitizeme, '--execute';
if ($dbh->selectrow_array("SELECT COUNT(*) FROM bug_group_map")) {
die "sanitization failed\n";
@@ -164,7 +174,7 @@ foreach my $table (@tables) {
else {
print "dropping $table\n";
drop_referencing($table);
- $dbh->do("DROP TABLE $table");
+ $dbh->do("DROP TABLE IF EXISTS $table");
}
}
diff --git a/scripts/sanitizeme.pl b/scripts/sanitizeme.pl
index 48a618d12..31418d047 100755
--- a/scripts/sanitizeme.pl
+++ b/scripts/sanitizeme.pl
@@ -26,7 +26,6 @@ use strict;
use warnings;
use lib qw(. lib local/lib/perl5);
-
use Bugzilla;
use Bugzilla::Bug;
use Bugzilla::Constants;
diff --git a/scripts/undo.pl b/scripts/undo.pl
new file mode 100644
index 000000000..24d6f594b
--- /dev/null
+++ b/scripts/undo.pl
@@ -0,0 +1,203 @@
+#!/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 File::Basename qw(dirname);
+use File::Spec::Functions qw(catfile catdir rel2abs);
+use Cwd qw(realpath);
+BEGIN {
+ require lib;
+ my $dir = rel2abs(catdir(dirname(__FILE__), '..'));
+ lib->import($dir, catdir($dir, 'lib'), catdir($dir, qw(local lib perl5)));
+}
+
+use Bugzilla;
+BEGIN { Bugzilla->extensions };
+use Bugzilla::Extension::UserProfile::Util qw(tag_for_recount_from_bug);
+
+use Try::Tiny;
+
+# not involved with kmag
+my $query = q{
+ resolution = 'INACTIVE'
+ AND NOT (
+ triage_owner.login_name = 'mak77@bonardo.net'
+ AND bugs.priority IN ('P4', 'P5', '--')
+ )
+ AND NOT (
+ product.name = 'Toolkit' AND component.name = 'Add-ons Manager'
+ )
+ AND NOT (
+ product.name = 'Toolkit' AND component.name LIKE 'WebExtensions%'
+ )
+ AND NOT (
+ product.name = 'Core' AND component.name = 'XPConnect'
+ )
+};
+undo($query);
+
+sub undo {
+ my @args = @_;
+ my $changes = get_changes(@args);
+ my $comments = get_comments(@args);
+
+ my %action;
+ while ($_ = $changes->()) {
+ push @{ $action{$_->{bug_id}}{$_->{bug_when}}{remove_activities} }, { id => $_->{change_id} };
+ $action{ $_->{bug_id} }{ $_->{bug_when} }{change}{ $_->{field_name} } = {
+ replace => $_->{added},
+ with => $_->{removed},
+ };
+ }
+
+ while ($_ = $comments->()) {
+ push @{ $action{ $_->{bug_id} }{$_->{bug_when}}{remove_comments} }, {
+ id => $_->{comment_id},
+ };
+ }
+
+ my $dbh = Bugzilla->dbh;
+ my @bug_ids = reverse sort { $a <=> $b } keys %action;
+ say 'Found ', 0 + @bug_ids, ' bugs';
+ foreach my $bug_id (@bug_ids) {
+ $dbh->bz_start_transaction;
+ say "Fix $bug_id";
+ try {
+ my ($delta_ts) = $dbh->selectrow_array(
+ 'SELECT delta_ts FROM bugs WHERE bug_id = ?',
+ undef,
+ $bug_id);
+ my ($previous_last_ts) = $dbh->selectrow_array(
+ q{
+ SELECT MAX(bug_when) FROM (
+ SELECT bug_when FROM bugs_activity WHERE bug_id = ? AND bug_when < ?
+ UNION
+ SELECT bug_when FROM longdescs WHERE bug_id = ? AND bug_when < ?
+ UNION
+ SELECT creation_ts AS bug_when FROM bugs WHERE bug_id = ?
+ ) as changes ORDER BY bug_when DESC
+ },
+ undef,
+ $bug_id, $delta_ts,
+ $bug_id, $delta_ts,
+ $bug_id,
+ );
+ die 'cannot find previous last updated time' unless $previous_last_ts;
+ my $action = delete $action{$bug_id}{$delta_ts};
+ if (keys %{ $action{$bug_id}{$delta_ts}}) {
+ die "skipping because more than one change\n";
+ }
+ elsif (!$action) {
+ die "skipping because most recent change newer than automation change\n";
+ }
+ foreach my $field (keys %{ $action->{change} }) {
+ my $change = $action->{change}{$field};
+ if ($field eq 'cf_last_resolved' && !$change->{with}) {
+ $change->{with} = undef;
+ }
+ my $did = $dbh->do(
+ "UPDATE bugs SET $field = ? WHERE bug_id = ? AND $field = ?",
+ undef, $change->{with}, $bug_id, $change->{replace},
+ );
+ die "Failed to set $field to $change->{with}" unless $did;
+ }
+ $dbh->do('UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?', undef,
+ $previous_last_ts, $previous_last_ts, $bug_id );
+ my $del_comments = $dbh->prepare('DELETE FROM longdescs WHERE comment_id = ?');
+ my $del_activity = $dbh->prepare('DELETE FROM bugs_activity WHERE id = ?');
+ foreach my $comment (@{ $action->{remove_comments}}) {
+ $del_comments->execute($comment->{id}) or die "failed to delete comment $comment->{id}";
+ }
+ foreach my $activity (@{ $action->{remove_activities}}) {
+ $del_activity->execute($activity->{id}) or die "failed to delete comment $activity->{id}";
+ }
+ tag_for_recount_from_bug($bug_id);
+ $dbh->bz_commit_transaction;
+ sleep 1;
+ } catch {
+ chomp $_;
+ say "Error updating $bug_id: $_";
+ $dbh->bz_rollback_transaction;
+ };
+ }
+ say 'Done.';
+}
+
+sub get_changes {
+ my ($where, @bind) = @_;
+
+ my $sql = qq{
+ SELECT
+ BA.id AS change_id,
+ BA.bug_id,
+ FD.name AS field_name,
+ BA.removed,
+ BA.added,
+ BA.bug_when
+ FROM
+ bugs_activity AS BA
+ JOIN
+ fielddefs AS FD ON BA.fieldid = FD.id
+ JOIN
+ profiles AS changer ON changer.userid = BA.who
+ JOIN
+ (SELECT
+ bug_id
+ FROM
+ bugs
+ JOIN products AS product ON product.id = product_id
+ JOIN components AS component ON component.id = component_id
+ LEFT JOIN profiles AS triage_owner ON triage_owner.userid = component.triage_owner_id
+ WHERE
+ $where
+ ) target_bugs ON BA.bug_id = target_bugs.bug_id
+ WHERE
+ changer.login_name = 'automation\@bmo.tld'
+ AND BA.bug_when BETWEEN '2018-05-22' AND '2018-05-26'
+ };
+ my $sth = Bugzilla->dbh->prepare($sql);
+ $sth->execute(@bind);
+
+ return sub { $sth->fetchrow_hashref };
+}
+
+sub get_comments {
+ my ($where, @bind) = @_;
+
+ my $sql = qq{
+ SELECT
+ C.comment_id AS comment_id,
+ C.bug_id AS bug_id,
+ C.bug_when
+ FROM
+ longdescs AS C
+ JOIN
+ profiles AS commenter ON commenter.userid = C.who
+ JOIN
+ (SELECT
+ bug_id
+ FROM
+ bugs
+ JOIN products AS product ON product.id = product_id
+ JOIN components AS component ON component.id = component_id
+ LEFT JOIN profiles AS triage_owner ON triage_owner.userid = component.triage_owner_id
+ WHERE
+ $where
+ ) target_bugs ON C.bug_id = target_bugs.bug_id
+ WHERE
+ commenter.login_name = 'automation\@bmo.tld'
+ AND C.bug_when BETWEEN '2018-05-22' AND '2018-05-26'
+ };
+ my $sth = Bugzilla->dbh->prepare($sql);
+ $sth->execute(@bind);
+
+ return sub { $sth->fetchrow_hashref };
+}
diff --git a/sentry.pl b/sentry.pl
deleted file mode 100755
index ebb221cfd..000000000
--- a/sentry.pl
+++ /dev/null
@@ -1,94 +0,0 @@
-#!/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.
-
-#
-# report errors to sentry
-# expects a filename with a Data::Dumper serialised parameters
-# called by Bugzilla::Sentry
-#
-
-use 5.10.1;
-use strict;
-use warnings;
-use lib qw(. lib local/lib/perl5);
-
-BEGIN {
- delete $ENV{SERVER_SOFTWARE};
-
- use Bugzilla::Constants;
- exit(0) unless glob(bz_locations()->{error_reports} . '/*.dump');
-}
-
-use Bugzilla;
-use Fcntl qw(:flock);
-use File::Slurp qw(read_file);
-use HTTP::Request::Common;
-use LWP::UserAgent;
-use POSIX qw(nice);
-use URI;
-
-Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
-nice(19);
-
-exit(1) unless Bugzilla->params->{sentry_uri};
-my $uri = URI->new(Bugzilla->params->{sentry_uri});
-my $header = build_header($uri);
-exit(1) unless $header;
-
-my $ua = LWP::UserAgent->new(timeout => 10);
-if (my $proxy_url = Bugzilla->params->{proxy_url}) {
- $ua->proxy(['http', 'https'], $proxy_url);
-}
-
-flock(DATA, LOCK_EX);
-foreach my $file (glob(bz_locations()->{error_reports} . '/*.dump')) {
- eval {
- send_file($uri, $header, $file);
- };
-}
-
-sub build_header {
- my ($uri) = @_;
-
- # split the sentry uri
- return undef unless $uri->userinfo && $uri->path;
- my ($public_key, $secret_key) = split(/:/, $uri->userinfo);
- $uri->userinfo(undef);
- my $project_id = $uri->path;
- $project_id =~ s/^\///;
- $uri->path("/api/$project_id/store/");
-
- # build the header
- return {
- 'X-Sentry-Auth' => sprintf(
- "Sentry sentry_version=%s, sentry_timestamp=%s, sentry_key=%s, sentry_client=%s, sentry_secret=%s",
- '2.0',
- (time),
- $public_key,
- 'bmo/' . BUGZILLA_VERSION,
- $secret_key,
- ),
- 'Content-Type' => 'application/json'
- };
-}
-
-sub send_file {
- my ($uri, $header, $filename) = @_;
- # read data dump
- my $message = read_file($filename);
- unlink($filename);
-
- # and post to sentry
- my $request = POST $uri->canonical, %$header, Content => $message;
- my $response = $ua->request($request);
-}
-
-__DATA__
-this exists so the flock() code works.
-do not remove this data section.
diff --git a/ses/index.cgi b/ses/index.cgi
index 8abd98e24..e36956b1d 100755
--- a/ses/index.cgi
+++ b/ses/index.cgi
@@ -104,57 +104,40 @@ sub handle_notification {
sub process_bounce {
my ($notification) = @_;
- my $type = $notification->{bounce}->{bounceType};
- if ( $type eq 'Transient' ) {
+ # disable each account that is bouncing
+ foreach my $recipient ( @{ $notification->{bounce}->{bouncedRecipients} } ) {
+ my $address = $recipient->{emailAddress};
+ my $reason = sprintf '(%s) %s', $recipient->{action} // 'error', $recipient->{diagnosticCode} // 'unknown';
- # just log transient bounces
- foreach my $recipient ( @{ $notification->{bounce}->{bouncedRecipients} } ) {
- my $address = $recipient->{emailAddress};
- Bugzilla->audit("transient bounce for <$address>");
- }
- }
+ my $user = Bugzilla::User->new( { name => $address, cache => 1 } );
+ if ($user) {
- elsif ( $type eq 'Permanent' ) {
-
- # disable each account that is permanently bouncing
- foreach my $recipient ( @{ $notification->{bounce}->{bouncedRecipients} } ) {
- my $address = $recipient->{emailAddress};
- my $reason = sprintf '(%s) %s', $recipient->{action} // 'error', $recipient->{diagnosticCode} // 'unknown';
-
- my $user = Bugzilla::User->new( { name => $address, cache => 1 } );
- if ($user) {
-
- # never auto-disable admin accounts
- if ( $user->in_group('admin') ) {
- Bugzilla->audit("ignoring permanent bounce for admin <$address>: $reason");
- }
-
- else {
- my $template = Bugzilla->template_inner();
- my $vars = {
- mta => $notification->{bounce}->{reportingMTA} // 'unknown',
- reason => $reason,
- };
- my $disable_text;
- $template->process( 'admin/users/bounce-disabled.txt.tmpl', $vars, \$disable_text )
- || die $template->error();
-
- $user->set_disabledtext($disable_text);
- $user->set_disable_mail(1);
- $user->update();
- Bugzilla->audit( "permanent bounce for <$address> disabled userid-" . $user->id . ": $reason" );
- }
+ # never auto-disable admin accounts
+ if ( $user->in_group('admin') ) {
+ Bugzilla->audit("ignoring bounce for admin <$address>: $reason");
}
else {
- Bugzilla->audit("permanent bounce for <$address> has no user: $reason");
+ my $template = Bugzilla->template_inner();
+ my $vars = {
+ mta => $notification->{bounce}->{reportingMTA} // 'unknown',
+ reason => $reason,
+ };
+ my $disable_text;
+ $template->process( 'admin/users/bounce-disabled.txt.tmpl', $vars, \$disable_text )
+ || die $template->error();
+
+ $user->set_disabledtext($disable_text);
+ $user->set_disable_mail(1);
+ $user->update();
+ Bugzilla->audit( "bounce for <$address> disabled userid-" . $user->id . ": $reason" );
}
}
- }
- else {
- WARN("Unsupported bounce type: $type\n");
+ else {
+ Bugzilla->audit("bounce for <$address> has no user: $reason");
+ }
}
respond( 200 => 'OK' );
diff --git a/skins/standard/attachment.css b/skins/standard/attachment.css
index ace4b6781..401bce92b 100644
--- a/skins/standard/attachment.css
+++ b/skins/standard/attachment.css
@@ -38,6 +38,14 @@ table#attachment_flags td {
font-size: small;
}
+#data-error {
+ margin: 4px 0 0;
+}
+
+#data-error:empty {
+ margin: 0;
+}
+
/* Rules used to view patches in diff mode. */
.file_head {
diff --git a/skins/standard/global.css b/skins/standard/global.css
index 0b40fcb2a..48d79366a 100644
--- a/skins/standard/global.css
+++ b/skins/standard/global.css
@@ -1308,6 +1308,7 @@ hr {
}
#main-inner {
+ position: relative;
margin: 15px 0;
}
diff --git a/static/metricsgraphics/socorro-lens.html b/static/metricsgraphics/socorro-lens.html
index 9af061323..c3664db37 100644
--- a/static/metricsgraphics/socorro-lens.html
+++ b/static/metricsgraphics/socorro-lens.html
@@ -96,7 +96,7 @@
function getSignaturesFromURL(search, match) {
var index = search.indexOf("?s=");
- search = search.substring(index + 3);
+ search = search.substring(index + 3).replace(/\+/g, '%20');
var signatures = [];
if (search.indexOf("\\") !== -1) {
signatures = search.split("\\");
diff --git a/t/008filter.t b/t/008filter.t
index cae1e6880..443fb2b4f 100644
--- a/t/008filter.t
+++ b/t/008filter.t
@@ -147,6 +147,9 @@ sub directive_ok {
$directive =~ s/^\s*//;
$directive =~ s/\s*$//;
+ # Ignore blocks explicitly marked as ok
+ return 1 if $directive =~ /\b## no-008filter\b/;
+
# Empty directives are ok; they are usually line break helpers
return 1 if $directive eq '';
diff --git a/t/markdown.t b/t/markdown.t
new file mode 100644
index 000000000..0344706c9
--- /dev/null
+++ b/t/markdown.t
@@ -0,0 +1,73 @@
+# 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 lib qw( . lib local/lib/perl5 );
+use Bugzilla;
+use Test::More;
+
+my $parser = Bugzilla->markdown_parser;
+
+is(
+ $parser->render_html('# header'),
+ "<h1>header</h1>\n",
+ 'Simple header'
+);
+
+is(
+ $parser->render_html('`code snippet`'),
+ "<p><code>code snippet</code></p>\n",
+ 'Simple code snippet'
+);
+
+is(
+ $parser->render_html('http://bmo-web.vm'),
+ "<p><a href=\"http://bmo-web.vm\">http://bmo-web.vm</a></p>\n",
+ 'Autolink extension'
+);
+
+is(
+ $parser->render_html('<script>hijack()</script>'),
+ "&lt;script>hijack()&lt;/script>\n",
+ 'Tagfilter extension'
+);
+
+is(
+ $parser->render_html('~~strikethrough~~'),
+ "<p><del>strikethrough</del></p>\n",
+ 'Strikethrough extension'
+);
+
+my $table_markdown = <<'MARKDOWN';
+| Col1 | Col2 |
+| ---- |:----:|
+| val1 | val2 |
+MARKDOWN
+
+my $table_html = <<'HTML';
+<table>
+<thead>
+<tr>
+<th>Col1</th>
+<th align="center">Col2</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>val1</td>
+<td align="center">val2</td>
+</tr></tbody></table>
+HTML
+
+is(
+ $parser->render_html($table_markdown),
+ $table_html,
+ 'Table extension'
+);
+
+done_testing;
diff --git a/template/en/default/admin/admin.html.tmpl b/template/en/default/admin/admin.html.tmpl
index 62a246ceb..09fe00835 100644
--- a/template/en/default/admin/admin.html.tmpl
+++ b/template/en/default/admin/admin.html.tmpl
@@ -56,7 +56,7 @@
You can also automate this check by running <tt>sanitycheck.pl</tt> from a cron job.
A notification will be sent per email to the specified user if errors are detected.</dd>
- [% class = (user.in_group('editusers') || user.can_bless) ? "" : "forbidden" %]
+ [% class = (user.in_group('editusers') || user.in_group('disableusers') || user.can_bless) ? "" : "forbidden" %]
<dt id="users" class="[% class %]"><a href="editusers.cgi">Users</a></dt>
<dd class="[% class %]">Create new user accounts or edit existing ones. You can
also add and remove users from groups (also known as "user privileges").</dd>
diff --git a/template/en/default/admin/params/advanced.html.tmpl b/template/en/default/admin/params/advanced.html.tmpl
index 92c84d703..2fe59c533 100644
--- a/template/en/default/admin/params/advanced.html.tmpl
+++ b/template/en/default/admin/params/advanced.html.tmpl
@@ -25,7 +25,7 @@
%]
[% sts_desc = BLOCK %]
- Enables the sending of the
+ Enables the sending of the
<a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security">Strict-Transport-Security</a>
header along with HTTP responses on SSL connections. This adds greater
security to your SSL connections by forcing the browser to always
@@ -67,23 +67,4 @@
disable_bug_updates =>
"When enabled, all updates to $terms.bugs will be blocked.",
-
- sentry_uri =>
- "When set, important errors and warnings will be sent to the"
- _ " specified Sentry server. Enter the full API KEY URL."
- _ " eg <kbd>https://01234567890123456780123456780123:01234567890123456780123456780123@errormill.mozilla.org/10</kbd>.",
-
- metrics_enabled =>
- "Collect metrics for reporting to ElasticSearch",
- metrics_user_ids =>
- "Comma separated list of user_id's which trigger data collection and reporting."
- _ " eg <kbd>3881,5038,5898,13647,20209,251051,373476,409787</kbd>.",
- metrics_elasticsearch_server =>
- "Metrics ElasticSearch server and port. eg <kbd>127.0.0.1:9200</kbd>",
- metrics_elasticsearch_index =>
- "Metrics ElasticSearch index. eg <kbd>bmo-metrics</kbd>",
- metrics_elasticsearch_type =>
- "Metrics ElasticSearch type. eg <kbd>timings</kbd>",
- metrics_elasticsearch_ttl =>
- "The time to live for data in the ElasticSearch cluster, in milliseconds. eg <kbd>1210000000</kbd>",
} %]
diff --git a/template/en/default/admin/users/edit.html.tmpl b/template/en/default/admin/users/edit.html.tmpl
index 4eb62e763..de9826800 100644
--- a/template/en/default/admin/users/edit.html.tmpl
+++ b/template/en/default/admin/users/edit.html.tmpl
@@ -68,9 +68,10 @@ $(function() {
<form method="post" action="editusers.cgi">
<table class="main">
[% PROCESS admin/users/userdata.html.tmpl
- editform = 1
- editusers = editusers
- otheruser = otheruser
+ editform = 1
+ editusers = editusers
+ disableusers = disableusers
+ otheruser = otheruser
%]
[% IF groups.size %]
<tr>
@@ -125,43 +126,46 @@ $(function() {
</tr>
[% END %]
- <tr>
- <th>Product responsibilities:</th>
- <td>
- [% IF otheruser.product_responsibilities.size %]
- [% PROCESS admin/users/responsibilities.html.tmpl otheruser = otheruser %]
- [% ELSE %]
- <em>none</em>
- [% END %]
- </td>
- </tr>
-
- [% IF otheruser.groups_owned.size %]
+ [% IF editusers %]
<tr>
- <th>Groups Owned:</th>
+ <th>Product responsibilities:</th>
<td>
- [% can_edit_groups = user.in_group('creategroups') %]
- [% FOREACH group = otheruser.groups_owned %]
- [% IF can_edit_groups %]
- <a href="[% urlbase FILTER none %]editgroups.cgi?action=changeform&amp;group=[% group.id FILTER none %]">
+ [% IF otheruser.product_responsibilities.size %]
+ [% PROCESS admin/users/responsibilities.html.tmpl otheruser = otheruser %]
+ [% ELSE %]
+ <em>none</em>
+ [% END %]
+ </td>
+ </tr>
+
+ [% IF otheruser.groups_owned.size %]
+ <tr>
+ <th>Groups Owned:</th>
+ <td>
+ [% can_edit_groups = user.in_group('creategroups') %]
+ [% FOREACH group = otheruser.groups_owned %]
+ [% IF can_edit_groups %]
+ <a href="[% urlbase FILTER none %]editgroups.cgi?action=changeform&amp;group=[% group.id FILTER none %]">
+ [% END %]
+ [% group.name FILTER html %]
+ [% '</a>' IF can_edit_groups %]<br>
[% END %]
- [% group.name FILTER html %]
- [% '</a>' IF can_edit_groups %]<br>
+ </td>
+ </tr>
+ [% END %]
+
+ <tr>
+ <th>Last Login:</th>
+ <td>
+ [% IF otheruser.last_seen_date %]
+ [% otheruser.last_seen_date FILTER html %]
+ [% ELSE %]
+ <em>never</em>
[% END %]
</td>
</tr>
[% END %]
- <tr>
- <th>Last Login:</th>
- <td>
- [% IF otheruser.last_seen_date %]
- [% otheruser.last_seen_date FILTER html %]
- [% ELSE %]
- <em>never</em>
- [% END %]
- </td>
- </tr>
</table>
<p>
@@ -171,11 +175,10 @@ $(function() {
<input type="hidden" name="token" value="[% token FILTER html %]">
[% INCLUDE listselectionhiddenfields %]
- [% IF editusers %], [% ELSE %] or [% END %]
- <a href="editusers.cgi?action=activity&amp;userid=[% otheruser.id %]"
+ [% IF editusers %]
+ , <a href="editusers.cgi?action=activity&amp;userid=[% otheruser.id %]"
title="View Account History for '
[%- otheruser.login FILTER html %]'">View Account History</a>
- [% IF editusers %]
or <a href="editusers.cgi?action=admin_activity&amp;userid=[% otheruser.id %]"
title="View Account History for '
[%- otheruser.login FILTER html %]'">View Admin History</a>
diff --git a/template/en/default/admin/users/list.html.tmpl b/template/en/default/admin/users/list.html.tmpl
index 3ebfc2970..db425b40d 100644
--- a/template/en/default/admin/users/list.html.tmpl
+++ b/template/en/default/admin/users/list.html.tmpl
@@ -45,23 +45,24 @@
{name => 'last_seen_date'
heading => 'Last Login'
}
- {heading => 'Account History'
- content => 'View'
- contentlink => 'editusers.cgi?action=activity' _
- '&amp;userid=%%userid%%' _
- listselectionurlparams
- }
]
%]
[% IF editusers %]
[% columns.push({
+ heading => 'Account History'
+ content => 'View'
+ contentlink => 'editusers.cgi?action=activity' _
+ '&amp;userid=%%userid%%' _
+ listselectionurlparams
+ }
+ {
heading => 'Admin History'
content => 'View'
contentlink => 'editusers.cgi?action=admin_activity' _
'&amp;userid=%%userid%%' _
listselectionurlparams
- })
+ })
%]
[% END %]
diff --git a/template/en/default/admin/users/userdata.html.tmpl b/template/en/default/admin/users/userdata.html.tmpl
index 449a1b143..c4ab07010 100644
--- a/template/en/default/admin/users/userdata.html.tmpl
+++ b/template/en/default/admin/users/userdata.html.tmpl
@@ -54,7 +54,7 @@
<tr>
<th><label for="name">Real name:</label></th>
<td>
- [% IF editusers %]
+ [% IF editusers || disableusers %]
<input size="64" maxlength="255" name="name"
autocomplete="off"
id="name" value="[% otheruser.name FILTER html %]">
@@ -94,7 +94,9 @@
[% END %]
</td>
</tr>
+[% END %]
+[% IF editusers || disableusers %]
<tr>
<th><label for="disable_mail">[% terms.Bug %]mail Disabled:</label></th>
<td>
@@ -122,30 +124,31 @@
explain why.)
</td>
</tr>
- [% IF editform %]
- <tr>
- <th><label for="mfa">Two-factor Auth:</label></th>
- <td>
- [% IF user.in_group('bz_can_disable_mfa') %]
- [% IF otheruser.mfa %]
- <select name="mfa" value="mfa">
- <option value="">Disable</option>
- [% SWITCH otheruser.mfa %]
- [% CASE "TOTP" %]
- <option value="TOTP" selected>Enabled - TOTP</option>
- [% CASE "Duo" %]
- <option value="Duo" selected>Enabled - Duo Security</option>
- [% END %]
- </select>
- [% ELSE %]
- Disabled
- [% END %]
+[% END %]
+
+[% IF editform && editusers %]
+ <tr>
+ <th><label for="mfa">Two-factor Auth:</label></th>
+ <td>
+ [% IF user.in_group('bz_can_disable_mfa') %]
+ [% IF otheruser.mfa %]
+ <select name="mfa" value="mfa">
+ <option value="">Disable</option>
+ [% SWITCH otheruser.mfa %]
+ [% CASE "TOTP" %]
+ <option value="TOTP" selected>Enabled - TOTP</option>
+ [% CASE "Duo" %]
+ <option value="Duo" selected>Enabled - Duo Security</option>
+ [% END %]
+ </select>
[% ELSE %]
- [% otheruser.mfa ? "Enabled - " _ otheruser.mfa : "Disabled" FILTER html %]
+ Disabled
[% END %]
- </td>
- </tr>
- [% END %]
+ [% ELSE %]
+ [% otheruser.mfa ? "Enabled - " _ otheruser.mfa : "Disabled" FILTER html %]
+ [% END %]
+ </td>
+ </tr>
[% END %]
[% Hook.process('end') %]
diff --git a/template/en/default/attachment/createformcontents.html.tmpl b/template/en/default/attachment/createformcontents.html.tmpl
index 41a02a913..efb24e3e9 100644
--- a/template/en/default/attachment/createformcontents.html.tmpl
+++ b/template/en/default/attachment/createformcontents.html.tmpl
@@ -37,7 +37,8 @@
<em>Enter the path to the file on your computer</em> (or
<a id="attachment_data_controller">
paste text as attachment</a>).<br>
- <input type="file" id="data" name="data" size="50">
+ <input type="file" id="data" name="data" size="50" aria-errormessage="data-error" aria-invalid="false">
+ <div id="data-error" class="warning" aria-live="assertive"><div>
</td>
</tr>
<tr class="attachment_text_field">
@@ -69,7 +70,7 @@
[%# Reset this whenever the page loads so that the JS state is up to date %]
<script [% script_nonce FILTER none %]>
$(function() {
- $("#file").on("change", function() {
+ $("#data").on("change", function() {
DataFieldHandler();
// Fire event to keep take-bug in sync.
$("#ispatch").change();
@@ -119,6 +120,7 @@
{type => "image/gif", desc => "GIF image"},
{type => "image/jpeg", desc => "JPEG image"},
{type => "image/png", desc => "PNG image"},
+ {type => "image/svg+xml", desc => "SVG image"},
{type => "application/pdf", desc => "PDF document"},
{type => "application/octet-stream", desc => "binary file"}]
%]
diff --git a/template/en/default/attachment/show-multiple.html.tmpl b/template/en/default/attachment/show-multiple.html.tmpl
index c28d5dfd6..6a7bb4b9b 100644
--- a/template/en/default/attachment/show-multiple.html.tmpl
+++ b/template/en/default/attachment/show-multiple.html.tmpl
@@ -35,7 +35,7 @@
%]
[% IF hide_obsolete %]
<div id="hidden_obsolete_message">
- Obsolete attachments are hidden. To view all attachments (including obsolete)
+ Obsolete attachments are hidden. To view all attachments (including obsolete)
<a href="attachment.cgi?bugid=[% bug.id FILTER html %]&amp;action=viewall">click here</a>.
</div>
[% END %]
@@ -106,7 +106,7 @@
[% END %]
[% ELSE %]
<p><b>
- Attachment cannot be viewed because its MIME type is not text/*, image/*, or application/vnd.mozilla.*.
+ Attachment cannot be viewed because its MIME type is not text/*, image/*, or application/pdf.
<a href="attachment.cgi?id=[% a.id %]">Download the attachment instead</a>.
</b></p>
[% END %]
diff --git a/template/en/default/global/code-error.html.tmpl b/template/en/default/global/code-error.html.tmpl
index bf1ff5ad3..b37e7ca7b 100644
--- a/template/en/default/global/code-error.html.tmpl
+++ b/template/en/default/global/code-error.html.tmpl
@@ -532,9 +532,6 @@
admindocslinks = admindocslinks
%]
-[%# return the generated error_message for sentry %]
-[% processed.error_message = error_message %]
-
<p>
[% terms.Bugzilla %] has suffered an internal error:
</p>
@@ -543,13 +540,6 @@
[% error_message FILTER none %]
</p>
-[% IF maintainers_notified %]
-<p>
- The [% terms.Bugzilla %] maintainers have been notified of this error
- [#[% uid FILTER html %]].
-</p>
-[% END %]
-
[% IF variables %]
<pre>
Variables:
diff --git a/template/en/default/global/header.html.tmpl b/template/en/default/global/header.html.tmpl
index 1d304ad04..153137394 100644
--- a/template/en/default/global/header.html.tmpl
+++ b/template/en/default/global/header.html.tmpl
@@ -101,11 +101,12 @@
[% IF Bugzilla.cgi.should_block_referrer %]
<meta name="referrer" content="origin">
[% ELSE %]
- <meta name="referrer" content="origin-when-crossorigin">
+ <meta name="referrer" content="origin-when-cross-origin">
[% END %]
[%- js_BUGZILLA = {
param => {
+ maxattachmentsize => Param('maxattachmentsize'),
maxusermatches => Param('maxusermatches'),
splinter_base => Param('splinter_base'),
},
@@ -294,7 +295,7 @@
<li role="presentation">
<a href="report.cgi" role="menuitem" tabindex="-1">Reports</a>
</li>
- [% IF user.in_group('tweakparams') || user.in_group('editusers') || user.can_bless
+ [% IF user.in_group('tweakparams') || user.in_group('editusers') || user.can_bless || user.in_group('disableusers')
|| (Param('useclassification') && user.in_group('editclassifications'))
|| user.in_group('editcomponents') || user.in_group('admin') || user.in_group('creategroups')
|| user.in_group('editkeywords') || user.in_group('bz_canusewhines')
diff --git a/template/en/default/global/site-navigation.html.tmpl b/template/en/default/global/site-navigation.html.tmpl
index 06b0eaa92..510875422 100644
--- a/template/en/default/global/site-navigation.html.tmpl
+++ b/template/en/default/global/site-navigation.html.tmpl
@@ -69,22 +69,22 @@
[% END %]
[%# *** Bugzilla Administration Tools *** %]
- [% IF user.login %]
- [% '<link rel="Administration" title="Parameters"
+ [% IF user.login %]
+ [% '<link rel="Administration" title="Parameters"
href="editparams.cgi">' IF user.in_group('tweakparams') %]
- [% '<link rel="Administration" title="Users"
- href="editusers.cgi">' IF user.in_group('editusers') %]
+ [% '<link rel="Administration" title="Users"
+ href="editusers.cgi">' IF user.in_group('editusers') || user.in_group('disableusers') %]
[% '<link rel="Administration" title="Products" href="editproducts.cgi">'
IF user.in_group('editcomponents') || user.get_products_by_permission("editcomponents").size %]
- [% '<link rel="Administration" title="Flag Types"
+ [% '<link rel="Administration" title="Flag Types"
href="editflagtypes.cgi">' IF user.in_group('editcomponents') %]
- [% '<link rel="Administration" title="Groups"
+ [% '<link rel="Administration" title="Groups"
href="editgroups.cgi">' IF user.in_group('creategroups') %]
- [% '<link rel="Administration" title="Keywords"
+ [% '<link rel="Administration" title="Keywords"
href="editkeywords.cgi">' IF user.in_group('editkeywords') %]
- [% '<link rel="Administration" title="Whining"
+ [% '<link rel="Administration" title="Whining"
href="editwhines.cgi">' IF user.in_group('bz_canusewhines') %]
- [% '<link rel="Administration" title="Sanity Check"
+ [% '<link rel="Administration" title="Sanity Check"
href="sanitycheck.cgi">' IF user.in_group('editcomponents') %]
- [% END %]
+ [% END %]
[% END %]
diff --git a/template/en/default/global/user.html.tmpl b/template/en/default/global/user.html.tmpl
index caea27c46..876d12805 100644
--- a/template/en/default/global/user.html.tmpl
+++ b/template/en/default/global/user.html.tmpl
@@ -28,7 +28,7 @@
[% IF user.id %]
<a class="email" href="mailto:[% who.email FILTER html %]"
onclick="return show_usermenu([% who.id FILTER none %], '[% who.email FILTER js %]',
- [% user.in_group('editusers') || user.bless_groups.size > 0 ? "true" : "false" %]);"
+ [% user.in_group('editusers') || user.in_group('disableusers') || user.bless_groups.size > 0 ? "true" : "false" %]);"
title="[% who.identity FILTER html %]">
[%- END -%]
[% IF who %]
diff --git a/template/en/default/index.html.tmpl b/template/en/default/index.html.tmpl
index 177025686..2aff68615 100644
--- a/template/en/default/index.html.tmpl
+++ b/template/en/default/index.html.tmpl
@@ -30,6 +30,7 @@
header = "Main Page"
style_urls = [ 'skins/standard/index.css' ]
no_yui = 1
+ og_image = "extensions/OpenGraph/web/moz-social-bw-rgb-1200x1200.png"
%]
<div id="page-index">
diff --git a/template/en/default/setup/strings.txt.pl b/template/en/default/setup/strings.txt.pl
index 5fc860519..8726a8b13 100644
--- a/template/en/default/setup/strings.txt.pl
+++ b/template/en/default/setup/strings.txt.pl
@@ -181,6 +181,7 @@ For the "Difference Between Two Patches" feature to work, we need to know
what directory the "diff" bin is in. (You only need to set this if you
are using that feature of the Patch Viewer.)
END
+ localconfig_tct_bin => 'Path to tct (tocotrienol) a gpg replacement.',
localconfig_inbound_proxies => <<'END',
This is a list of IP addresses that we expect proxies to come from.
This can be '*' if only the load balancer can connect.
@@ -270,7 +271,7 @@ This is the max amount of unshared memory the apache process is allowed to use
before Apache::SizeLimit kills it. This is only applicable when run under mod_perl.
EOT
localconfig_shadowdb_user => <<EOT,
-The username used to authenticate to the shadow db.
+The username used to authenticate to the shadow db.
EOT
localconfig_shadowdb_pass => <<EOT,
The password used to authenticate to the shadow db.
diff --git a/vagrant_support/apache.j2 b/vagrant_support/apache.j2
index 773672fa1..3135d8c25 100644
--- a/vagrant_support/apache.j2
+++ b/vagrant_support/apache.j2
@@ -1,6 +1,8 @@
PerlSwitches -wT
PerlSetEnv USE_NYTPROF 0
PerlSetEnv BUGZILLA_UNSAFE_AUTH_DELEGATION 1
+PerlSetEnv LOG4PERL_CONFIG_FILE log4perl-vagrant.conf
+PerlSetEnv LOG4PERL_STDERR_DISABLE 1
PerlConfigRequire /vagrant/mod_perl.pl
<IfModule mpm_prefork_module>
diff --git a/vagrant_support/apache.yml b/vagrant_support/apache.yml
index c7159371c..5031187e3 100644
--- a/vagrant_support/apache.yml
+++ b/vagrant_support/apache.yml
@@ -10,5 +10,11 @@
service: name=httpd enabled=yes
when: LAZY == 0
+- name: ensure bugzilla.log has right permissions
+ file: path=/vagrant/logs/bugzilla.log state=touch owner=vagrant group=apache mode=0660
+
+- name: ensure bugzilla-json.log has right permissions
+ file: path=/vagrant/logs/bugzilla-json.log state=touch owner=vagrant group=apache mode=0660
+
- name: restart httpd
- service: name=httpd state=restarted
+ service: name=httpd state=restarted \ No newline at end of file
diff --git a/vagrant_support/checksetup.yml b/vagrant_support/checksetup.yml
index 580cc9dd9..455986148 100644
--- a/vagrant_support/checksetup.yml
+++ b/vagrant_support/checksetup.yml
@@ -22,6 +22,12 @@
mode: 0644
when: LAZY == 0
+- name: fix owner of /vagrant/template_cache
+ file: path=/vagrant/template_cache state=directory owner=vagrant group=apache recurse=yes
+
+- name: fix owner of /data
+ file: path=/data state=directory owner=vagrant group=apache recurse=yes
+
- name: run checksetup
become: false
shell: sg apache -c '/usr/local/bin/bmo-checksetup --no-templates'
diff --git a/vagrant_support/playbook.yml b/vagrant_support/playbook.yml
index f38c1fc06..c067eab05 100644
--- a/vagrant_support/playbook.yml
+++ b/vagrant_support/playbook.yml
@@ -50,6 +50,15 @@
group: root
mode: 0644
+ - name: 'add LOG4PERL_CONFIG_FILE'
+ lineinfile:
+ dest: /etc/environment
+ regexp: 'LOG4PERL_CONFIG_FILE='
+ line: 'LOG4PERL_CONFIG_FILE=log4perl-vagrant.conf'
+ owner: root
+ group: root
+ mode: 0644
+
- name: copy ntp.conf
copy:
src: ntp.conf
@@ -148,11 +157,19 @@
- ncurses-devel
- readline-devel
+ - name: fetch tct
+ unarchive:
+ src: https://github.com/dylanwh/tocotrienol/releases/download/1.0.6/tct-centos6.tar.xz
+ dest: /usr/local/bin
+ remote_src: yes
+ mode: '0755'
+
- name: fetch cpanm
get_url:
url: http://cpanmin.us
dest: /usr/local/bin/cpanm
mode: '0755'
+
- name: install more recent Apache2::SizeLimit
cpanm: name=Apache2::SizeLimit executable=/usr/local/bin/cpanm
@@ -207,9 +224,3 @@
LAZY: 0
- import_tasks: devtools.yml
- - name: fix owner of /vagrant/template_cache
- file: path=/vagrant/template_cache state=directory owner=vagrant group=apache recurse=yes
-
- - name: fix owner of /data
- file: path=/data state=directory owner=vagrant group=apache recurse=yes
-