From: Dylan William Hardison
Date: Mon, 24 Aug 2020 00:42:33 +0000 (-0400)
Subject: bmo sync - 2020-08-23 (#49)
X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=cdcbc710f970ec25290da322317d03fa7f5c3ea4;p=thirdparty%2Fbugzilla.git
bmo sync - 2020-08-23 (#49)
* Bug 1646559 - Phabricator to BMO OAuth2 authentication fails to work properly due to CSP protections
* Bumped version to 20200624.1
* no bug - fix broken build script when no new commits to master since last prod deploy
* no bug - remove warning "Odd number of elements"
The callback jwt_claims is called in list context.
Without an explicit return, when the if (!...) does not match it will
return a single false item. This causes the "Odd number of elements..."
perl warning.
Adding a `return` will prevent the warning and is also probably the
intent of the code.
* Bug 1645768 - Please add 'See Also' support for GitLab
* no bug - Updated tasks.json to include some admin tasks
* no bug - Fix build data script to help find the Mozilla CA cert
* Bug 1651591 - remove preloading of fonts and ga; r=dkl
Font preloading has been broken for more than a year, and doesn't appear
to work correctly even when provided URLs which exist; removed.
google-analytics likely does little to improve page load time
performance against all the other assets, is loaded async, and exists
outside of the GoogleAnalytics making it easy to miss when updating that
extension (eg. the preloading doesn't honour DNT); also removed.
* Bug 1535000 - Allow anyone with edit-comments to edit any bug's comment 0
* Bug 1652863 - setting the needinfo flag when filing a new bug in Core or Toolkit does not cause the textbox for user information to pop up
* Bumped version 20200722.1
* Bug 1647642 - when commenting on patch or reviewing one, bugzilla clears other (review, ui-review) flags
* Bumped version to 20200723.1
* Bug 1643526 - Attachment comments don't render markdown, but their preview does
* Bug 1654456 - needinfo? request email enhancements (#1594)
* Start WIP PR
* Modify default need info text
* Fix breaking bugwords.t
* You have to fix all the instances of the word.
* Adding conditional for needinfo to reporter
* fix failing bugwords.t again
* Don't divert people with questions to bmo team
* don't repeat bug url
* fix terms.bug
* Remove 'to see question,' question is in email
* Keep the link to the bug
* move text into a conditional branch
* link to needinfo docs
* Bug 1654370 - Remove remaining code that references Firefox OS from BMO code base
* Bug 1655808: send users in guided bug flow to GitHub for Android and iOS bug reports (#1600)
* Move all the products to the product template
* Link mobile products to GitHub
* Better boilerplate text
* Fix failing terms test
* no bug - Use standard docker mysql for docker-compose instead of bmo-mysql
* no bug - Updated docker-compose.test.yml for mysql settings in CircleCI environment.
* Bumped version to 20200805.1
* Bug 1657542 - During recent bmo deployment, emails were delivered to a file instead of SES which caused interruption of email service
* Bug 1658622 - "product responsibilities" on editusers should include Triage Owner
* Bug 1588661 - Design for Webhooks
* Bug 1659177 - Replace mozillians.org with people.mozilla.org in Reps Mentorship Form
* Bug 1649841 - Include data-review? requests in notification count
* Bug 1658317 - Make scopes more descriptive and user friendly when authenticating to BMO using OAuth2
* Bug 1656609: Make the scrolling element
* Bug 1657778 - Offer link to Bugzilla for filing security issues in Fenix and iOS
* no bug - Show Bounty Attachments to the Bug Reporter
This will allow most bug bounty recipients to view the amount of their bounty. It will not show it to reporters if we filed the bug for them, however those are less liekly to be repeat filers.
* Bug 1658846 - Allow users to enable and disable their webhooks
Co-authored-by: dklawren
Co-authored-by: byron jones
Co-authored-by: Emma Humphries
Co-authored-by: David Lawrence
Co-authored-by: Lisset Cuevas
Co-authored-by: Michael Kohler
Co-authored-by: Tom Ritter
---
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index fc7423f0f..f9a7a806e 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -19,6 +19,20 @@
"isDefault": true
},
"problemMatcher": []
- }
+ },
+ {
+ "label": "Docker: Generate new cpanfile and cpanfile.snapshot",
+ "type": "shell",
+ "command": "docker build -t bmo-cpanfile -f Dockerfile.cpanfile . ; docker run -it -v $(pwd):/app/result bmo-cpanfile cp cpanfile cpanfile.snapshot /app/result",
+ "group": "none",
+ "problemMatcher": []
+ },
+ {
+ "label": "Docker: Generate mozillabteam/bmo-perl-slim image",
+ "type": "shell",
+ "command": "docker build -t mozillabteam/bmo-perl-slim:$(date +%Y%m%d.1) -f Dockerfile.bmo-slim .",
+ "group": "none",
+ "problemMatcher": []
+ },
]
}
diff --git a/Bugzilla.pm b/Bugzilla.pm
index 99388f609..42b56ec03 100644
--- a/Bugzilla.pm
+++ b/Bugzilla.pm
@@ -13,7 +13,7 @@ use warnings;
use Bugzilla::Logging;
-our $VERSION = '20200603.1';
+our $VERSION = '20200805.1';
use Bugzilla::Auth;
use Bugzilla::Auth::Persist::Cookie;
diff --git a/Bugzilla/App/Plugin/OAuth2.pm b/Bugzilla/App/Plugin/OAuth2.pm
index 5e51f85b9..aa3d6cb4e 100644
--- a/Bugzilla/App/Plugin/OAuth2.pm
+++ b/Bugzilla/App/Plugin/OAuth2.pm
@@ -40,6 +40,7 @@ sub register {
if (!$args->{user_id}) {
return (user_id => Bugzilla->user->id);
}
+ return;
};
$app->helper(
@@ -81,6 +82,7 @@ sub _resource_owner_confirm_scopes {
my (%args) = @_;
my ($c, $client_id, $scopes_ref)
= @args{qw/ mojo_controller client_id scopes /};
+ my $dbh = Bugzilla->dbh;
$c->bugzilla->login(LOGIN_REQUIRED) || return undef;
@@ -90,12 +92,18 @@ sub _resource_owner_confirm_scopes {
# access last time, we check [again] with the user for access
if (!defined $is_allowed) {
my $client
- = Bugzilla->dbh->selectrow_hashref(
- 'SELECT * FROM oauth2_client WHERE client_id = ?',
+ = $dbh->selectrow_hashref('SELECT * FROM oauth2_client WHERE client_id = ?',
undef, $client_id);
+ my $scopes = $dbh->selectall_arrayref(
+ 'SELECT * FROM oauth2_scope WHERE name IN ('
+ . join(',', map { $dbh->quote($_) } @{$scopes_ref}) . ')',
+ {Slice => {}}
+ );
+
+
my $vars = {
client => $client,
- scopes => $scopes_ref,
+ scopes => $scopes,
token => scalar issue_session_token('oauth_confirm_scopes')
};
$c->stash(%{$vars});
diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm
old mode 100644
new mode 100755
index 12d7bd267..bc1640d02
--- a/Bugzilla/Attachment.pm
+++ b/Bugzilla/Attachment.pm
@@ -542,13 +542,20 @@ sub get_attachments_by_bug {
my $dbh = Bugzilla->dbh;
# By default, private attachments are not accessible, unless the user
- # is in the insider group or submitted the attachment.
+ # is in the insider group, submitted the attachment, or it's a bounty
+ # attachment and they reported the bug.
my $and_restriction = '';
my @values = ($bug->id);
unless ($user->is_insider) {
- $and_restriction = 'AND (isprivate = 0 OR submitter_id = ?)';
+ $and_restriction = 'AND (isprivate = 0 OR submitter_id = ?';
push(@values, $user->id);
+ if ($user->id == $bug->reporter->id) {
+ # Keep these conditions in sync with _attachment_is_bounty_attachment
+ # in extensions/BMO/Extension.pm
+ $and_restriction .= " OR (filename = 'bugbounty.data' AND mimetype = 'text/plain')";
+ }
+ $and_restriction .= ')';
}
# BMO - allow loading of just non-obsolete attachments
diff --git a/Bugzilla/BugUrl/GitLab.pm b/Bugzilla/BugUrl/GitLab.pm
index 8e546c1dc..d2e75dcce 100644
--- a/Bugzilla/BugUrl/GitLab.pm
+++ b/Bugzilla/BugUrl/GitLab.pm
@@ -20,9 +20,9 @@ use base qw(Bugzilla::BugUrl);
sub should_handle {
my ($class, $uri) = @_;
-# GitLab issue URLs can have the form:
-# https://gitlab.com/projectA/subprojectB/subprojectC/../issues/53
- return ($uri->path =~ m!^/.*/issues/\d+$!) ? 1 : 0;
+ # GitLab issue and merge request URLs can have the form:
+ # https://gitlab.com/projectA/subprojectB/subprojectC/../(issues|merge_requests)/53
+ return ($uri->path =~ m!^/.*/(issues|merge_requests)/\d+$!) ? 1 : 0;
}
sub _check_value {
diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm
index e4245a862..f3f69b8b8 100644
--- a/Bugzilla/CGI.pm
+++ b/Bugzilla/CGI.pm
@@ -410,26 +410,6 @@ sub header {
$self->{_header_done} = 1;
- if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
- my @fonts = (
- "skins/standard/fonts/FiraMono-Regular.woff2?v=3.202",
- "skins/standard/fonts/FiraSans-Bold.woff2?v=4.203",
- "skins/standard/fonts/FiraSans-Italic.woff2?v=4.203",
- "skins/standard/fonts/FiraSans-Regular.woff2?v=4.203",
- "skins/standard/fonts/FiraSans-SemiBold.woff2?v=4.203",
- "skins/standard/fonts/MaterialIcons-Regular.woff2",
- );
- $headers{'-link'} = join(
- ", ",
- map {
- sprintf('; rel="preload"; as="font"', Bugzilla->VERSION, $_)
- } @fonts
- );
- if (Bugzilla->params->{google_analytics_tracking_id}) {
- $headers{'-link'}
- .= ', ; rel="preconnect"; crossorigin';
- }
- }
my $headers = $self->SUPER::header(%headers) || '';
if ($self->server_software eq 'Bugzilla::App::CGI') {
my $c = $Bugzilla::App::CGI::C;
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index 0cd18bdf6..44ffefbea 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -789,6 +789,11 @@ sub DEFAULT_CSP {
'https://github.com/login';
}
+ # This is for Mozilla Phabricator and authentication
+ if (Bugzilla->params->{phabricator_enabled}) {
+ push @{$policy{form_action}}, Bugzilla->params->{phabricator_base_uri};
+ }
+
return %policy;
}
diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index 47638cccb..bc14d18f4 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -1851,7 +1851,12 @@ use constant ABSTRACT_SCHEMA => {
oauth2_scope => {
FIELDS => [
id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
- description => {TYPE => 'varchar(255)', NOTNULL => 1},
+ name => {TYPE => 'varchar(255)', NOTNULL => 1},
+ description => {TYPE => 'TINYTEXT', NOTNULL => 1},
+ ],
+ INDEXES => [
+ oauth2_scope_idx =>
+ {FIELDS => ['name'], TYPE => 'UNIQUE'},
],
},
diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm
index 60ae614d0..b2155d057 100644
--- a/Bugzilla/Install/DB.pm
+++ b/Bugzilla/Install/DB.pm
@@ -4262,8 +4262,19 @@ sub _populate_oauth2_scopes {
# if there are no scopes, then we're creating a database from scratch
my ($scope_count) = $dbh->selectrow_array('SELECT COUNT(*) FROM oauth2_scope');
- return if $scope_count;
- $dbh->do("INSERT INTO oauth2_scope (id, description) VALUES (1, 'user:read')");
+ if (!$scope_count) {
+ $dbh->do(
+ "INSERT INTO oauth2_scope (id, name, description) VALUES " .
+ "(1, 'user:read', 'View basic account information such as email address.')"
+ );
+ }
+
+ # Bug 1658317 - dkl@mozilla - Update column names if this is an existing DB
+ if (!$dbh->bz_column_info('oauth2_scope', 'name')) {
+ $dbh->bz_rename_column("oauth2_scope", "description", "name");
+ $dbh->bz_add_column('oauth2_scope', 'description',
+ {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "'Needs Description'"});
+ }
}
sub _add_oauth2_jwt_support {
diff --git a/Bugzilla/Test/Util.pm b/Bugzilla/Test/Util.pm
index 4d7d662ff..32ce2242b 100644
--- a/Bugzilla/Test/Util.pm
+++ b/Bugzilla/Test/Util.pm
@@ -59,7 +59,7 @@ sub create_oauth_client {
foreach my $scope (@{$scopes}) {
my $scope_id
- = $dbh->selectrow_array('SELECT id FROM oauth2_scope WHERE description = ?',
+ = $dbh->selectrow_array('SELECT id FROM oauth2_scope WHERE name = ?',
undef, $scope);
if (!$scope_id) {
die "Scope $scope not found";
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index d3d989057..da7447442 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -2032,8 +2032,9 @@ sub product_responsibilities {
ON components.id = component_cc.component_id
WHERE components.initialowner = ?
OR components.initialqacontact = ?
+ OR components.triage_owner_id = ?
OR component_cc.user_id = ?',
- {Slice => {}}, ($self->id, $self->id, $self->id)
+ {Slice => {}}, ($self->id, $self->id, $self->id, $self->id)
);
unless ($list) {
diff --git a/docker-compose.test.yml b/docker-compose.test.yml
index 1fb065589..de60c9589 100644
--- a/docker-compose.test.yml
+++ b/docker-compose.test.yml
@@ -43,7 +43,7 @@ services:
- selenium
bmo.db:
- image: mozillabteam/bmo-mysql:5.7
+ image: mysql:5.7
tmpfs:
- /tmp
logging:
@@ -62,3 +62,6 @@ services:
shm_size: '512m'
ports:
- "5900:5900"
+
+volumes:
+ bmo-mysql-db:
diff --git a/extensions/BMO/Extension.pm b/extensions/BMO/Extension.pm
old mode 100644
new mode 100755
index 7f31b6d4a..c46438f99
--- a/extensions/BMO/Extension.pm
+++ b/extensions/BMO/Extension.pm
@@ -344,6 +344,7 @@ sub bounty_attachment {
}
sub _attachment_is_bounty_attachment {
+ # Keep this in sync with Bugzilla/Attachment.pm
my ($attachment) = @_;
return 0 unless $attachment->filename eq 'bugbounty.data';
@@ -2236,18 +2237,6 @@ sub _file_child_bug {
Bugzilla->error_mode($old_error_mode);
}
-sub _pre_fxos_feature {
- my ($self, $args) = @_;
- my $cgi = Bugzilla->cgi;
- my $user = Bugzilla->user;
- my $params = $args->{params};
-
- $params->{keywords} = 'foxfood';
- $params->{keywords} .= ',feature'
- if ($cgi->param('feature_type') // '') eq 'new';
- $params->{bug_status} = $user->in_group('canconfirm') ? 'NEW' : 'UNCONFIRMED';
-}
-
sub _add_attachment {
my ($self, $args, $attachment_args) = @_;
@@ -2368,9 +2357,6 @@ sub bug_before_create {
# map renamed groups
$params->{groups} = [_map_groups($params->{groups})];
}
- if ((Bugzilla->cgi->param('format') // '') eq 'fxos-feature') {
- $self->_pre_fxos_feature($args);
- }
}
sub _map_groups {
@@ -2641,19 +2627,6 @@ sub _split_crash_signature {
extract_multiple($crash_signature, [sub { extract_bracketed($_[0], '[]') }])];
}
-sub enter_bug_entrydefaultvars {
- my ($self, $args) = @_;
- my $vars = $args->{vars};
- my $cgi = Bugzilla->cgi;
- return unless my $format = $cgi->param('format');
-
- if ($format eq 'fxos-feature') {
- $vars->{feature_type} = $cgi->param('feature_type');
- $vars->{description} = $cgi->param('description');
- $vars->{discussion} = $cgi->param('discussion');
- }
-}
-
sub _fetch_product_version_file {
my ($product, $cache_only) = @_;
my $key = "${product}_versions";
diff --git a/extensions/BMO/template/en/default/bug/create/create-data-compliance.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-data-compliance.html.tmpl
index 6e42709ca..a19f1215b 100644
--- a/extensions/BMO/template/en/default/bug/create/create-data-compliance.html.tmpl
+++ b/extensions/BMO/template/en/default/bug/create/create-data-compliance.html.tmpl
@@ -120,7 +120,6 @@
-
diff --git a/extensions/BMO/template/en/default/hook/bug_modal/attachments-row.html.tmpl b/extensions/BMO/template/en/default/hook/bug_modal/attachments-row.html.tmpl
old mode 100644
new mode 100755
index f0a97b1f0..c8f887d4e
--- a/extensions/BMO/template/en/default/hook/bug_modal/attachments-row.html.tmpl
+++ b/extensions/BMO/template/en/default/hook/bug_modal/attachments-row.html.tmpl
@@ -24,8 +24,10 @@
[% END %]
diff --git a/extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl b/extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl
index d202a3c78..e177caf12 100644
--- a/extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl
+++ b/extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl
@@ -132,12 +132,6 @@ tabbed browsing or the location bar)
[% INCLUDE 'guided/products.html.tmpl' %]
-[% WRAPPER product_block
- name="Other Products"
- icon="other.png"
- onclick="guided.setStep('otherProducts')" %]
-Other Mozilla products which aren't listed here
-[% END %]
diff --git a/extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl b/extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl
index 047d64abc..38affef30 100644
--- a/extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl
+++ b/extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl
@@ -14,15 +14,37 @@ For [% terms.bugs %] in Firefox, the Mozilla Foundation's web browser.
(more info)
[% END %]
-[% INCLUDE product_block
+[% WRAPPER product_block
name="Firefox for Android"
icon="firefox_android.png"
+ onclick="document.location='https://github.com/mozilla-mobile/fenix/issues/'"
%]
-[% INCLUDE product_block
+Firefox for Android is the Firefox mobile experience developed for Android. [% terms.Bugs %] for
+this product are tracked on GitHub.
+
+If you are reporting a security issue that puts users at risk,
+use
+this form.
+[% END %]
+[% WRAPPER product_block
name="Firefox for iOS"
icon="firefox_ios.png"
+ onclick="document.location='https://github.com/mozilla-mobile/firefox-ios/issues'"
%]
+Firefox for iOS is the Firefox mobile experience developed for the iOS platform. [% terms.Bugs %] for
+this product are tracked on GitHub.
+
+If you are reporting a security issue that puts users at risk,
+use
+this form.
+[% END %]
[% INCLUDE product_block
name="Thunderbird"
icon="thunderbird.png"
%]
+[% WRAPPER product_block
+ name="Other Products"
+ icon="other.png"
+ onclick="guided.setStep('otherProducts')" %]
+Other Mozilla products which aren't listed here
+[% END %]
diff --git a/extensions/Needinfo/template/en/default/hook/request/email-after_summary.txt.tmpl b/extensions/Needinfo/template/en/default/hook/request/email-after_summary.txt.tmpl
index 6a302b76a..66a6d1ad1 100644
--- a/extensions/Needinfo/template/en/default/hook/request/email-after_summary.txt.tmpl
+++ b/extensions/Needinfo/template/en/default/hook/request/email-after_summary.txt.tmpl
@@ -8,6 +8,28 @@
[% RETURN UNLESS flag && flag.type.name == 'needinfo' && flag.status == '?' %]
---
---- This request has set a needinfo flag on the [% terms.bug %].
---- You can clear it by logging in and replying in a comment.
+--- Hello from [% terms.Bugzilla %], the [% terms.bug %] tracker for Mozilla and Firefox:
+---
+--- Someone has asked you for information about this [% terms.bug %].
+---
+--- Please go to [% urlbase %]show_bug.cgi?id=[% bug.bug_id %] to respond
+--- to the question below.
+[% IF flag.requestee.login_name == bug.reporter.login_name %]
+---
+--- Since you reported this [% terms.bug %], then the person asking the
+--- question needs more information from you to understand it.
+---
+--- Responding to the question will enable developers to take further
+--- action on this [% terms.bug %].
+[% ELSE %]
+---
+--- Please log into [% terms.Bugzilla %] and respond as soon as possible
+--- so developers may take action on this [% terms.bug %].
+[% END %]
+---
+--- If you have questions about responding to needinfo requests, please
+--- see https://wiki.mozilla.org/BMO/UserGuide#Needinfo_Flag.
+---
+--- Thank you
+---
---
diff --git a/extensions/Push/Extension.pm b/extensions/Push/Extension.pm
index b8fa9d90e..265330a97 100644
--- a/extensions/Push/Extension.pm
+++ b/extensions/Push/Extension.pm
@@ -461,6 +461,16 @@ sub page_before_template {
{group => 'admin', action => 'access', object => 'administrative_pages'});
admin_log($vars);
}
+ elsif ($page eq 'webhooks_config.html') {
+ if (Bugzilla->params->{webhooks_enabled}){
+ Bugzilla->user->in_group('admin')
+ || ThrowUserError('auth_failure',
+ {group => 'admin', action => 'access', object => 'administrative_pages'});
+ admin_webhooks($vars);
+ }else{
+ ThrowUserError('webhooks_disabled');
+ }
+ }
}
#
diff --git a/extensions/Push/lib/Admin.pm b/extensions/Push/lib/Admin.pm
index aa78b2d03..0369e2037 100644
--- a/extensions/Push/lib/Admin.pm
+++ b/extensions/Push/lib/Admin.pm
@@ -14,6 +14,7 @@ use warnings;
use Bugzilla;
use Bugzilla::Error;
use Bugzilla::Extension::Push::Util;
+use Bugzilla::Extension::Webhooks::Webhook;
use Bugzilla::Token qw(check_hash_token);
use Bugzilla::Util qw(trim detaint_natural );
@@ -22,6 +23,7 @@ our @EXPORT = qw(
admin_config
admin_queues
admin_log
+ admin_webhooks
);
sub admin_config {
@@ -36,7 +38,9 @@ sub admin_config {
$dbh->bz_start_transaction();
_update_config_from_form('global', $push->config);
foreach my $connector ($push->connectors->list) {
- _update_config_from_form($connector->name, $connector->config);
+ if ($connector->name !~ /\QWebhook\E/) {
+ _update_config_from_form($connector->name, $connector->config);
+ }
}
$push->set_config_last_modified();
$dbh->bz_commit_transaction();
@@ -123,4 +127,37 @@ sub admin_log {
$vars->{push} = $push;
}
+sub admin_webhooks {
+ my ($vars) = @_;
+ my $push = Bugzilla->push_ext;
+ my $input = Bugzilla->input_params;
+ my @webhooks = Bugzilla::Extension::Webhooks::Webhook->get_all;
+
+ if ($input->{save}) {
+ my $token = $input->{token};
+ check_hash_token($token, ['webhooks_config']);
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
+ foreach my $connector ($push->connectors->list) {
+ if ($connector->name =~ /\QWebhook\E/) {
+ _update_webhook_status($connector->name, $connector->config);
+ }
+ }
+ $push->set_config_last_modified();
+ $dbh->bz_commit_transaction();
+ $vars->{message} = 'push_config_updated';
+ }
+
+ $vars->{push} = $push;
+ $vars->{connectors} = $push->connectors;
+ $vars->{webhooks} = \@webhooks;
+}
+
+sub _update_webhook_status {
+ my ($name, $config) = @_;
+ my $input = Bugzilla->input_params;
+ $config->{enabled} = trim($input->{$name . ".enabled"});
+ $config->update();
+}
+
1;
diff --git a/extensions/Push/lib/BacklogQueue.pm b/extensions/Push/lib/BacklogQueue.pm
index 17d0a188f..189b06167 100644
--- a/extensions/Push/lib/BacklogQueue.pm
+++ b/extensions/Push/lib/BacklogQueue.pm
@@ -78,6 +78,11 @@ sub list {
return @result;
}
+sub delete {
+ my ($self) = @_;
+ Bugzilla->dbh->do("DELETE FROM push_backlog WHERE connector = ?",undef, $self->{connector});
+}
+
#
# backoff
#
diff --git a/extensions/Push/lib/Config.pm b/extensions/Push/lib/Config.pm
index f9f8fcb46..3c45e2410 100644
--- a/extensions/Push/lib/Config.pm
+++ b/extensions/Push/lib/Config.pm
@@ -13,6 +13,7 @@ use warnings;
use Bugzilla::Logging;
use Bugzilla::Constants;
+use Bugzilla::Extension::Webhooks::Webhook;
use Bugzilla::Extension::Push::Option;
use Crypt::CBC;
@@ -188,7 +189,8 @@ sub _validate_config {
die join("\n", @errors) if @errors;
if ($self->{_name} ne 'global') {
- my $class = 'Bugzilla::Extension::Push::Connector::' . $self->{_name};
+ my $name = $self->{_name} =~ /\QWebhook\E/ ? 'Webhook' : $self->{_name};
+ my $class = 'Bugzilla::Extension::Push::Connector::' . $name;
$class->options_validate($config);
}
}
diff --git a/extensions/Push/lib/Connector/Webhook.pm b/extensions/Push/lib/Connector/Webhook.pm
new file mode 100644
index 000000000..ba74a9c1d
--- /dev/null
+++ b/extensions/Push/lib/Connector/Webhook.pm
@@ -0,0 +1,158 @@
+# 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::Push::Connector::Webhook;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use base 'Bugzilla::Extension::Push::Connector::Base';
+
+use Bugzilla;
+use Bugzilla::Bug;
+use Bugzilla::Constants;
+use Bugzilla::Attachment;
+use Bugzilla::Extension::Webhooks::Webhook;
+use Bugzilla::Extension::Push::Constants;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Util ();
+
+use JSON qw(decode_json encode_json);
+use LWP::UserAgent;
+use List::MoreUtils qw(any);
+use Try::Tiny;
+
+sub new {
+ my ($class,$webhook_id) = @_;
+ my $self = {};
+ bless($self, $class);
+ $self->{name} = 'Webhook_' . $webhook_id;
+ $self->{webhook_id} = $webhook_id;
+ $self->init();
+ return $self;
+}
+
+sub load_config {
+ my ($self) = @_;
+ my $config
+ = Bugzilla::Extension::Push::Config->new($self->name, $self->options);
+ $config->option('enabled')->{'default'} = 'Enabled';
+ $config->load();
+ $self->{config} = $config;
+}
+
+sub save {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $push = Bugzilla->push_ext;
+ $dbh->bz_start_transaction();
+ $self->config->update();
+ $push->set_config_last_modified();
+ $dbh->bz_commit_transaction();
+}
+
+sub should_send {
+ my ($self, $message) = @_;
+
+ return 0 unless Bugzilla->params->{webhooks_enabled};
+
+ my $webhook = Bugzilla::Extension::Webhooks::Webhook->new($self->{webhook_id});
+ my $event = $webhook->event;
+ my $product = $webhook->product_name;
+ my $component = $webhook->component_name ? $webhook->component_name : 'any';
+
+ my $data = $message->payload_decoded;
+ my $bug_data = $self->_get_bug_data($data) || return 0;
+
+ my $bug = Bugzilla::Bug->new({id => $bug_data->{id}, cache => 1});
+
+ if ($product eq $bug->product
+ && ($component eq $bug->component || $component eq 'any'))
+ {
+ if ($event =~ /create/ && $message->routing_key eq 'bug.create') {
+ return 1;
+ }elsif ($event =~ /change/ && $message->routing_key =~ /\Qbug.modify\E/) {
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+sub send {
+ my ($self, $message) = @_;
+
+ try {
+ my $webhook = Bugzilla::Extension::Webhooks::Webhook->new($self->{webhook_id});
+
+ my $payload = $message->payload_decoded;
+ $payload->{webhook_name} = $webhook->name;
+ $payload->{webhook_id} = $webhook->id;
+
+ my $bug_data = $self->_get_bug_data($payload);
+ my $is_private = $bug_data->{is_private};
+ if ($is_private){
+ delete @{$payload}{bug};
+ if($payload->{event}->{action} eq 'modify'){
+ delete @{$payload->{event}}{changes};
+ }
+ $payload->{bug}->{id} = $bug_data->{id};
+ $payload->{bug}->{is_private} = $is_private;
+ }
+ delete @{$payload->{event}}{qw(routing_key change_set target)};
+
+ my $headers = HTTP::Headers->new(Content_Type => 'application/json');
+ my $request
+ = HTTP::Request->new('POST', $webhook->url, $headers, encode_json($payload));
+ my $resp = $self->_user_agent->request($request);
+ if ($resp->code != 200) {
+ die "Expected HTTP 200 response, got " . $resp->code;
+ }else{
+ return PUSH_RESULT_OK;
+ }
+ }
+ catch{
+ return (PUSH_RESULT_TRANSIENT, clean_error($_));
+ };
+
+}
+
+# Private methods
+
+sub _get_bug_data {
+ my ($self, $data) = @_;
+ my $target = $data->{event}->{target};
+ if ($target eq 'bug') {
+ return $data->{bug};
+ }
+ elsif (exists $data->{$target}->{bug}) {
+ return $data->{$target}->{bug};
+ }
+ else {
+ return;
+ }
+}
+
+sub _user_agent {
+ my ($self) = @_;
+
+ my $ua = LWP::UserAgent->new(agent => 'Bugzilla');
+ $ua->timeout(10);
+ $ua->protocols_allowed(['http', 'https']);
+
+ if (my $proxy_url = Bugzilla->params->{proxy_url}) {
+ $ua->proxy(['http', 'https'], $proxy_url);
+ }
+ else {
+ $ua->env_proxy();
+ }
+
+ return $ua;
+}
+
+1;
diff --git a/extensions/Push/lib/Connectors.pm b/extensions/Push/lib/Connectors.pm
index 3276759b9..b4851fa4f 100644
--- a/extensions/Push/lib/Connectors.pm
+++ b/extensions/Push/lib/Connectors.pm
@@ -13,6 +13,7 @@ use warnings;
use Bugzilla::Logging;
use Bugzilla::Extension::Push::Util;
+use Bugzilla::Extension::Webhooks::Webhook;
use Bugzilla::Constants;
use File::Basename;
use Try::Tiny;
@@ -53,15 +54,35 @@ sub _load {
TRACE("Loading connector '$name'");
my $old_error_mode = Bugzilla->error_mode;
Bugzilla->error_mode(ERROR_MODE_DIE);
- try {
- my $connector = $package->new();
- $connector->load_config();
- $self->{objects}->{$name} = $connector;
+ if ($name eq 'Webhook' && Bugzilla->params->{webhooks_enabled}) {
+ my @webhooks = Bugzilla::Extension::Webhooks::Webhook->get_all;
+ if (@webhooks){
+ foreach my $webhook (@webhooks) {
+ my $webhook_name = $name . '_' . $webhook->{id};
+ next if exists $self->{objects}->{$webhook_name};
+ try {
+ TRACE("Loading connector '$webhook_name'");
+ my $connector = $package->new($webhook->{id});
+ $connector->load_config();
+ $self->{objects}->{$webhook_name} = $connector;
+ }
+ catch {
+ ERROR("Connector '$webhook_name' failed to load: " . clean_error($_));
+ };
+ Bugzilla->error_mode($old_error_mode);
+ }
+ }
+ }elsif ($name ne 'Webhook') {
+ try {
+ my $connector = $package->new();
+ $connector->load_config();
+ $self->{objects}->{$name} = $connector;
+ }
+ catch {
+ ERROR("Connector '$name' failed to load: " . clean_error($_));
+ };
+ Bugzilla->error_mode($old_error_mode);
}
- catch {
- ERROR("Connector '$name' failed to load: " . clean_error($_));
- };
- Bugzilla->error_mode($old_error_mode);
}
}
diff --git a/extensions/Push/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Push/template/en/default/hook/global/user-error-errors.html.tmpl
index 2b8a1c4e0..4a6a5fbf4 100644
--- a/extensions/Push/template/en/default/hook/global/user-error-errors.html.tmpl
+++ b/extensions/Push/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -8,4 +8,11 @@
[% IF error == "push_error" %]
[% error_message FILTER html %]
+
+[% ELSIF error == "webhooks_disabled" %]
+ The webhooks feature is not available.
+ [% IF user.in_group('admin') %]
+ If you would like to enable this feature, please go to webhooks section in parameters panel.
+ [% END %]
+
[% END %]
diff --git a/extensions/Push/template/en/default/pages/push_config.html.tmpl b/extensions/Push/template/en/default/pages/push_config.html.tmpl
index 17f9506a8..82cad0c71 100644
--- a/extensions/Push/template/en/default/pages/push_config.html.tmpl
+++ b/extensions/Push/template/en/default/pages/push_config.html.tmpl
@@ -41,6 +41,7 @@ var push_defaults = new Array();
%]
[% FOREACH connector = connectors.list %]
+ [% NEXT IF connector.webhook_id %]
[% PROCESS options
name = connector.name
config = connector.config
@@ -67,6 +68,15 @@ var push_defaults = new Array();
@@ -106,7 +116,7 @@ var push_defaults = new Array();
[% END %]
>
[% IF option.name != 'enabled' && !option.required %]
-
[% END %]
[% FOREACH value = option.values %]
diff --git a/extensions/Push/template/en/default/pages/push_queues.html.tmpl b/extensions/Push/template/en/default/pages/push_queues.html.tmpl
index ae24c2850..4538c94e9 100644
--- a/extensions/Push/template/en/default/pages/push_queues.html.tmpl
+++ b/extensions/Push/template/en/default/pages/push_queues.html.tmpl
@@ -22,6 +22,7 @@
[% FOREACH connector = push.connectors.list %]
[% NEXT UNLESS connector.enabled %]
+ [% NEXT IF !connector.backlog.count && connector.webhook_id %]
[% PROCESS show_queue
queue = connector.backlog
title = connector.name _ ' Backlog'
diff --git a/extensions/Push/template/en/default/pages/webhooks_config.html.tmpl b/extensions/Push/template/en/default/pages/webhooks_config.html.tmpl
new file mode 100644
index 000000000..c183902e0
--- /dev/null
+++ b/extensions/Push/template/en/default/pages/webhooks_config.html.tmpl
@@ -0,0 +1,80 @@
+[%# 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/header.html.tmpl
+ title = "Push Administration: Configuration: Webhoooks"
+ javascript_urls = [ 'extensions/Push/web/admin.js' ]
+ style_urls = [ 'extensions/Push/web/admin.css' ]
+%]
+
+
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/REMO/template/en/default/bug/create/comment-mozreps.txt.tmpl b/extensions/REMO/template/en/default/bug/create/comment-mozreps.txt.tmpl
index 8e8ae7362..e6a38ee1e 100644
--- a/extensions/REMO/template/en/default/bug/create/comment-mozreps.txt.tmpl
+++ b/extensions/REMO/template/en/default/bug/create/comment-mozreps.txt.tmpl
@@ -36,7 +36,7 @@ Languages:
[%+ cgi.param('languages') %]
[% END %]
-Mozillians.org Account URL:
+people.mozilla.org Profile URL:
[%+ cgi.param('mozillian') %]
References:
diff --git a/extensions/REMO/template/en/default/bug/create/create-mozreps.html.tmpl b/extensions/REMO/template/en/default/bug/create/create-mozreps.html.tmpl
index db8c835f2..01a4369f4 100644
--- a/extensions/REMO/template/en/default/bug/create/create-mozreps.html.tmpl
+++ b/extensions/REMO/template/en/default/bug/create/create-mozreps.html.tmpl
@@ -115,32 +115,32 @@
-
+
-
-
+
- You require a vouched Mozillians account to apply to the Reps Program.
+ You require a people.mozilla.org account to apply to the Reps Program.
-
+
-
+
@@ -153,7 +153,7 @@
- Please fill out your Mozillians account according to the Selection Criteria.
+ Please fill out your people.mozilla.org profile according to the Selection Criteria.
diff --git a/extensions/REMO/web/js/moz_reps.js b/extensions/REMO/web/js/moz_reps.js
index 083172c7d..68fb81590 100644
--- a/extensions/REMO/web/js/moz_reps.js
+++ b/extensions/REMO/web/js/moz_reps.js
@@ -31,13 +31,13 @@ $(document).ready(function() {
}
}).change();
- $("#vouched").change(function(evt) {
+ $("#pmo_account").change(function(evt) {
if (this.value === 'Yes') {
- $('#vouched_warning').hide();
+ $('#pmo_warning').hide();
$('#submit').prop("disabled", false);
}
else {
- $('#vouched_warning').show();
+ $('#pmo_warning').show();
$('#submit').prop("disabled", true);
}
}).change();
@@ -63,7 +63,7 @@ $(document).ready(function() {
}).change();
$('#tmRequestForm').submit(function (event) {
- var mozillian_re = /^https?:\/\/(www\.)?mozillians.org\/([^\/]+\/)?u\/[^\/]+\/?$/i;
+ var mozillian_re = /^https?:\/\/people.mozilla.org\/([^\/]+\/)?p\/[^\/]+\/?$/i;
var errors = [];
var missing = false;
@@ -75,7 +75,7 @@ $(document).ready(function() {
if (id == 'mozillian') {
if (!value.match(mozillian_re)) {
input.addClass("missing");
- errors.push("The Mozillian Account URL is invalid");
+ errors.push("The people.mozilla.org Profile URL is invalid");
event.preventDefault();
}
else {
diff --git a/extensions/Review/Extension.pm b/extensions/Review/Extension.pm
index ca2420241..fa4900ff2 100644
--- a/extensions/Review/Extension.pm
+++ b/extensions/Review/Extension.pm
@@ -117,7 +117,7 @@ sub _user_review_count {
INNER JOIN flagtypes ON flagtypes.id = flags.type_id
WHERE flags.requestee_id = ?
AND "
- . $dbh->sql_in('flagtypes.name', ["'review'", "'feedback'"]), undef,
+ . $dbh->sql_in('flagtypes.name', ["'review'", "'feedback'", "'data-review'"]), undef,
$self->id,
);
}
@@ -485,13 +485,17 @@ sub _is_countable_flag {
my $type_name = $object->type->name;
return
$type_name eq 'review'
+ || $type_name eq 'data-review'
|| $type_name eq 'feedback'
|| $type_name eq 'needinfo';
}
sub _check_requestee {
my ($flag) = @_;
- return unless $flag->type->name eq 'review' || $flag->type->name eq 'feedback';
+ return
+ unless $flag->type->name eq 'review'
+ || $flag->type->name eq 'data-review'
+ || $flag->type->name eq 'feedback';
if ($flag->requestee->reviews_blocked) {
ThrowUserError('reviews_blocked',
{requestee => $flag->requestee, flagtype => $flag->type->name});
@@ -518,7 +522,8 @@ sub _log_flag_state_activity {
sub _adjust_request_count {
my ($flag, $add) = @_;
return unless my $requestee_id = $flag->requestee_id;
- my $field = $flag->type->name . '_request_count';
+ my $field = $flag->type->name;
+ $field = ($field eq 'data-review' ? 'review' : $field) . '_request_count';
# update the current user's object so things are display correctly on the
# post-processing page
diff --git a/extensions/Review/lib/Util.pm b/extensions/Review/lib/Util.pm
index 61d4e9117..ca5bb9063 100644
--- a/extensions/Review/lib/Util.pm
+++ b/extensions/Review/lib/Util.pm
@@ -30,7 +30,7 @@ sub rebuild_review_counters {
INNER JOIN profiles ON profiles.userid = flags.requestee_id
INNER JOIN flagtypes ON flagtypes.id = flags.type_id
WHERE flags.status = '?'
- AND flagtypes.name IN ('review', 'feedback', 'needinfo')
+ AND flagtypes.name IN ('review', 'data-review', 'feedback', 'needinfo')
GROUP BY flags.requestee_id, flagtypes.name
", {Slice => {}});
@@ -71,7 +71,7 @@ sub _update_profile {
SET review_request_count = ?,
feedback_request_count = ?,
needinfo_request_count = ?
- WHERE userid = ?", undef, $data->{review} || 0, $data->{feedback} || 0,
+ WHERE userid = ?", undef, ($data->{review} || 0) + ($data->{'data-review'} || 0), $data->{feedback} || 0,
$data->{needinfo} || 0, $data->{id});
Bugzilla->memcached->clear({table => 'profiles', id => $data->{id}});
}
diff --git a/extensions/Splinter/template/en/default/pages/splinter.html.tmpl b/extensions/Splinter/template/en/default/pages/splinter.html.tmpl
index 484bb7792..15e45791c 100644
--- a/extensions/Splinter/template/en/default/pages/splinter.html.tmpl
+++ b/extensions/Splinter/template/en/default/pages/splinter.html.tmpl
@@ -218,7 +218,9 @@
[% any_flags_requesteeble = 0 %]
[% FOREACH flag_type = flag_types %]
[% NEXT UNLESS flag_type.is_active %]
- [% SET any_flags_requesteeble = 1 IF flag_type.is_requestable && flag_type.is_requesteeble %]
+ [% IF flag_type.is_requestable && flag_type.is_requesteeble %]
+ [% SET any_flags_requesteeble = 1 %]
+ [% END %]
[% END %]
[% IF flag_types.size > 0 %]
[% PROCESS "flag/list.html.tmpl" bug_id = bug_id
diff --git a/extensions/Webhooks/Config.pm b/extensions/Webhooks/Config.pm
new file mode 100644
index 000000000..01d236bfe
--- /dev/null
+++ b/extensions/Webhooks/Config.pm
@@ -0,0 +1,16 @@
+# 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::Webhooks;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use constant NAME => 'Webhooks';
+
+__PACKAGE__->NAME;
diff --git a/extensions/Webhooks/Extension.pm b/extensions/Webhooks/Extension.pm
new file mode 100644
index 000000000..1a02ea9bf
--- /dev/null
+++ b/extensions/Webhooks/Extension.pm
@@ -0,0 +1,240 @@
+# 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::Webhooks;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Component;
+use Bugzilla::Product;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::User;
+use Bugzilla::Logging;
+use Bugzilla::Extension::Webhooks::Webhook;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Util;
+use Try::Tiny;
+
+#
+# installation
+#
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ my $dbh = Bugzilla->dbh;
+ $args->{'schema'}->{'webhooks'} = {
+ FIELDS => [
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1,},
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',}
+ },
+ name => {TYPE => 'VARCHAR(64)', NOTNULL => 1,},
+ url => {TYPE => 'VARCHAR(64)', NOTNULL => 1,},
+ event => {TYPE => 'VARCHAR(64)', NOTNULL => 1,},
+ product_id => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE',}
+ },
+ component_id => {
+ TYPE => 'INT2',
+ NOTNULL => 0,
+ REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE',}
+ }
+ ],
+ INDEXES => [
+ webhooks_userid_name_idx => {FIELDS => ['user_id', 'name'], TYPE => 'UNIQUE',},
+ ],
+ };
+}
+
+sub db_sanitize {
+ my $dbh = Bugzilla->dbh;
+ print "Deleting webhooks...\n";
+ $dbh->do("DELETE FROM webhooks");
+}
+
+#
+# preferences
+#
+
+sub user_preferences {
+ my ($self, $args) = @_;
+
+ return unless Bugzilla->params->{webhooks_enabled}
+ && Bugzilla->user->in_group(Bugzilla->params->{"webhooks_group"});
+ return unless $args->{'current_tab'} eq 'webhooks';
+
+ my $input = Bugzilla->input_params;
+ my $user = Bugzilla->user;
+ my $push = Bugzilla->push_ext;
+ my $vars = $args->{vars};
+
+ if ($args->{'save_changes'}) {
+
+ if ($input->{'add_webhook'}) {
+
+ # add webhook
+
+ my $params = {user_id => $user->id,};
+
+ if ($input->{name} eq '') {
+ ThrowUserError('webhooks_define_name');
+ }
+ else {
+ $params->{name} = $input->{name};
+ }
+
+ if ($input->{url} eq '') {
+ ThrowUserError('webhooks_define_url');
+ }
+ else {
+ $params->{url} = $input->{url};
+ }
+
+ if ($input->{create_event} && $input->{change_event}) {
+ $params->{event} = 'create,change';
+ }
+ elsif ($input->{create_event}) {
+ $params->{event} = 'create';
+ }
+ elsif ($input->{change_event}) {
+ $params->{event} = 'change';
+ }
+ else {
+ ThrowUserError('webhooks_select_event');
+ }
+
+ my $product_name = $input->{add_product};
+ my $product = Bugzilla::Product->check({name => $product_name, cache => 1});
+ $params->{product_id} = $product->id;
+
+ if (my $component_name = $input->{add_component}) {
+ my $component = Bugzilla::Component->check({
+ name => $component_name, product => $product, cache => 1});
+ $params->{component_id} = $component->id;
+ }
+
+ my $new_webhook = Bugzilla::Extension::Webhooks::Webhook->create($params);
+
+ create_push_connector($new_webhook->{id});
+
+ }
+ else {
+
+ # remove webhook(s)
+
+ my $ids = ref($input->{remove}) ? $input->{remove} : [$input->{remove}];
+ my $dbh = Bugzilla->dbh;
+ my $push = Bugzilla->push_ext;
+
+ my $webhooks = Bugzilla::Extension::Webhooks::Webhook->match(
+ {id => $ids, user_id => $user->id});
+ $dbh->bz_start_transaction;
+ foreach my $webhook (@$webhooks) {
+ delete_backlog_queue($webhook->id);
+ $webhook->remove_from_db();
+ }
+ $push->set_config_last_modified();
+ $dbh->bz_commit_transaction();
+
+ # save change(s)
+
+ $webhooks = Bugzilla::Extension::Webhooks::Webhook->match(
+ {user_id => $user->id});
+ $dbh->bz_start_transaction;
+ foreach my $webhook (@$webhooks) {
+ my $connector = $push->connectors->by_name('Webhook_' . $webhook->id);
+ my $config = $connector->config;
+ my $status = trim($input->{$connector->name . ".enabled"});
+ if ( $status eq 'Enabled' || $status eq 'Disabled' ){
+ $config->{enabled} = $status;
+ $config->update();
+ }else{
+ ThrowUserError('webhooks_invalid_option');
+ }
+ }
+ $push->set_config_last_modified();
+ $dbh->bz_commit_transaction();
+ }
+
+ }
+
+ $vars->{webhooks} = [
+ sort {
+ $a->product_name cmp $b->product_name
+ || $a->component_name cmp $b->component_name
+ } @{Bugzilla::Extension::Webhooks::Webhook->match({
+ user_id => $user->id,
+ })
+ }
+ ];
+ $vars->{push} = $push;
+ $vars->{connectors} = $push->connectors;
+ $vars->{webhooks_saved} = 1;
+
+ ${$args->{handled}} = 1;
+}
+
+#
+# admin
+#
+
+sub config_add_panels {
+ my ($self, $args) = @_;
+ my $modules = $args->{panel_modules};
+ $modules->{Webhooks} = "Bugzilla::Extension::Webhooks::Config";
+}
+
+#
+# templates
+#
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ return if Bugzilla->params->{webhooks_enabled}
+ && Bugzilla->user->in_group(Bugzilla->params->{"webhooks_group"});
+ my ($vars, $file) = @$args{qw(vars file)};
+ return unless $file eq 'account/prefs/tabs.html.tmpl';
+ @{$vars->{tabs}} = grep { $_->{name} ne 'webhooks' } @{$vars->{tabs}};
+}
+
+#
+# push connector
+#
+
+sub create_push_connector {
+ my ($webhook_id) = @_;
+ my $webhook_name = 'Webhoook_' . $webhook_id;
+ my $package = "Bugzilla::Extension::Push::Connector::Webhook";
+ try {
+ my $connector = $package->new($webhook_id);
+ $connector->load_config();
+ $connector->save();
+ }
+ catch {
+ ERROR("Connector '$webhook_name' failed to load: " . clean_error($_));
+ };
+}
+
+sub delete_backlog_queue {
+ my ($webhook_id) = @_;
+ my $push = Bugzilla->push_ext;
+ my $webhook_name = 'Webhook_' . $webhook_id;
+ my $connector = $push->connectors->by_name($webhook_name);
+ my $queue = $connector->backlog;
+ $queue->delete();
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/Webhooks/lib/Config.pm b/extensions/Webhooks/lib/Config.pm
new file mode 100644
index 000000000..ca3e800c0
--- /dev/null
+++ b/extensions/Webhooks/lib/Config.pm
@@ -0,0 +1,33 @@
+# 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::Webhooks::Config;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::Config::Common;
+
+sub get_param_list {
+ my ($class) = @_;
+
+ my @param_list = (
+ {name => 'webhooks_enabled', type => 'b', default => '0',},
+
+ {
+ name => 'webhooks_group',
+ type => 's',
+ choices => \&get_all_group_names,
+ default => 'admin',
+ checker => \&check_group
+ },
+ );
+ return @param_list;
+}
+
+1;
diff --git a/extensions/Webhooks/lib/Webhook.pm b/extensions/Webhooks/lib/Webhook.pm
new file mode 100644
index 000000000..48eaa738c
--- /dev/null
+++ b/extensions/Webhooks/lib/Webhook.pm
@@ -0,0 +1,107 @@
+# 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::Webhooks::Webhook;
+
+use base qw(Bugzilla::Object);
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::User;
+use Bugzilla::Product;
+use Bugzilla::Component;
+use Bugzilla::Error;
+
+use constant DB_TABLE => 'webhooks';
+
+use constant DB_COLUMNS => qw(
+ id
+ name
+ url
+ user_id
+ event
+ product_id
+ component_id
+);
+
+use constant LIST_ORDER => 'id';
+
+use constant UPDATE_COLUMNS => ();
+
+use constant VALIDATORS => {
+ user_id => \&_check_user,
+};
+use constant VALIDATOR_DEPENDENCIES => {component_id => ['product_id'],};
+
+use constant AUDIT_CREATES => 0;
+use constant AUDIT_UPDATES => 0;
+use constant AUDIT_REMOVES => 0;
+use constant USE_MEMCACHED => 0;
+
+# getters
+
+sub user {
+ my ($self) = @_;
+ return Bugzilla::User->new({id => $self->{user_id}, cache => 1});
+}
+
+sub id {
+ return $_[0]->{id};
+}
+
+sub name {
+ return $_[0]->{name};
+}
+
+sub url {
+ return $_[0]->{url};
+}
+
+sub event {
+ return $_[0]->{event};
+}
+
+sub product_id {
+ return $_[0]->{product_id};
+}
+
+sub component_id {
+ return $_[0]->{component_id};
+}
+
+sub product {
+ my ($self) = @_;
+ return $self->{product} ||=
+ Bugzilla::Product->new({id => $self->{product_id}, cache => 1});
+}
+
+sub product_name {
+ my ($self) = @_;
+ return $self->{product_name} ||= $self->{product_id} ? $self->product->name : '';
+}
+
+sub component {
+ my ($self) = @_;
+ return $self->{component} ||= $self->{component_id}
+ ? Bugzilla::Component->new({id => $self->{component_id}, cache => 1}) : undef;
+}
+
+sub component_name {
+ my ($self) = @_;
+ return $self->{component_name} ||= $self->{component_id} ? $self->component->name : '';
+}
+
+# validators
+
+sub _check_user {
+ my ($class, $user) = @_;
+ $user || ThrowCodeError('param_required', {param => 'user'});
+}
+
+1;
diff --git a/extensions/Webhooks/template/en/default/account/prefs/webhooks.html.tmpl b/extensions/Webhooks/template/en/default/account/prefs/webhooks.html.tmpl
new file mode 100644
index 000000000..8b746012d
--- /dev/null
+++ b/extensions/Webhooks/template/en/default/account/prefs/webhooks.html.tmpl
@@ -0,0 +1,181 @@
+[%# 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.
+ #%]
+
+[%# initialize product to component mapping #%]
+
+[% SET selectable_products = user.get_selectable_products %]
+[% SET dont_show_button = 1 %]
+
+
+
+
+
+
+
+
+ It will be sent a POST request with the information of the [% terms.bugs %] that match with the events and filters selected to your URL.
+ Documentation about webhooks is available here.
+
+
+
+
+
Name:
+
+
+
+
URL:
+
+
+
+
Events
+
Select the events you want to receive.
+
+
+
+
+
+
+
+
Filters
+
+ To receive all components in a product, select "Any".
+
+
+
+
Product:
+
+
+ [% FOREACH product IN selectable_products %]
+
+ [% END %]
+
+
+
+
+
Component:
+
+
+
+ [% FOREACH product IN selectable_products %]
+ [% FOREACH component IN product.components %]
+
+ [% END %]
+ [% END %]
+
+
+
+[% END %]
diff --git a/extensions/Webhooks/template/en/default/admin/params/webhooks.html.tmpl b/extensions/Webhooks/template/en/default/admin/params/webhooks.html.tmpl
new file mode 100644
index 000000000..3e6e2a1af
--- /dev/null
+++ b/extensions/Webhooks/template/en/default/admin/params/webhooks.html.tmpl
@@ -0,0 +1,19 @@
+[%# 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.
+ #%]
+
+[%
+ title = "Webhooks"
+ desc = "Webhooks feature flag"
+%]
+
+[%
+ param_descs = {
+ webhooks_enabled => "Enable or disable the webhooks feature.",
+ webhooks_group => "The name of the group of users who can create webhooks."
+ }
+%]
diff --git a/extensions/Webhooks/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl b/extensions/Webhooks/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
new file mode 100644
index 000000000..f4102f20c
--- /dev/null
+++ b/extensions/Webhooks/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
@@ -0,0 +1,14 @@
+[%# 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.
+ #%]
+
+[% CALL tabs.import([{
+ name => "webhooks",
+ label => "Webhooks",
+ link => "userprefs.cgi?tab=webhooks",
+ saveable => 1
+ }]) %]
diff --git a/extensions/Webhooks/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Webhooks/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..21f074806
--- /dev/null
+++ b/extensions/Webhooks/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,25 @@
+[%# 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.
+ #%]
+
+[% IF error == "webhooks_select_event" %]
+ [% title = "Select at least one event" %]
+ You didn't select any event. Select at least one.
+
+[% ELSIF error == "webhooks_define_name" %]
+ [% title = "Define a name" %]
+ You didn't define a name for your webhook. Define a name to identify your webhook.
+
+[% ELSIF error == "webhooks_define_url" %]
+ [% title = "Define a URL" %]
+ You didn't define a URL for your webhook. Define one.
+
+[% ELSIF error == "webhooks_invalid_option" %]
+ [% title = "Invalid option" %]
+ The option value specified is invalid.
+
+[% END %]
diff --git a/extensions/Webhooks/template/en/default/pages/webhooks.html.tmpl b/extensions/Webhooks/template/en/default/pages/webhooks.html.tmpl
new file mode 100644
index 000000000..15e5f66a2
--- /dev/null
+++ b/extensions/Webhooks/template/en/default/pages/webhooks.html.tmpl
@@ -0,0 +1,190 @@
+[%# 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/header.html.tmpl
+ title = "Webhooks Documentation"
+ style = "#bugzilla-body li {
+ margin: 5px
+ }
+ h4 {
+ margin-bottom: 0px
+ }
+ .heading {
+ font-weight: bold
+ }
+ #main-inner{
+ margin-left: 20%;
+ margin-right: 20%;
+ margin-bottom: 5%
+ }
+ "
+%]
+
+
[% terms.Bugzilla %] Webhooks
+
+
A webhook is a custom callback defined by events. Is triggered when those events happen, and a POST
+request is sent to a defined URL.
+
+
In the case of [% terms.Bugzilla %], a webhook can be triggered by a change to, or creation of a [% terms.bug %]. The
+parameters of the [% terms.bug %] are exposed to the webhook handler which makes a callback (over HTTP) to another
+web application.
+
+
Examples of [% terms.Bugzilla %] webhooks could include:
+
+
Updating a copy of the [% terms.Bugzilla %] [% terms.bug %] in another system such as Jira
+
Sending a message to a chat server such as Matrix or Slack
+
+
+
Creating a webhook
+
To create a webhook follow the next steps:
+
+
Access to your [% terms.Bugzilla %] Account.
+
Go to your Preferences Panel > Webhooks.
+
Fill out all of the parameters:
+
Name
+
A name for the webhook, which should be descriptive (e.g. âJira Webhook for New & Updated [% terms.Bugs %] in Core::Graphicsâ)
+
+
URL
+
The URL which will receive and process the webhook.
+
+
Events
+
The [% terms.bug %] events that will trigger your new webhook.
+
+
When a new [% terms.bug %] is created
+
When an existing [% terms.bug %] is modified
+
+
+
Filters
+
Properties of a [% terms.bug %] that specify which [% terms.bugs %] you will receive.
+
+
Product: name of the product of the [% terms.bugs %] that you want to receive.
+
Component: name of the component of the [% terms.bugs %] that you want to receive.
+ If you want to receive the [% terms.bugs %] of all the components of a product, select Any.
+
+
+
Add the webhook.
+
+
+
You can see your registered webhooks in the same panel.
+
If you want to delete a webhook, select the webhook in âYour webhooksâ table and click remove
+selected. You can select more than one.
+
+
Delivered webhook
+
When a webhook is triggered the HTTP POST of a fixed JSON structure payload that is delivered
+contains the webhook_id, webhook_name and the information about the [% terms.bug %] that matches the event and filters.
+
+
If the [% terms.bug %] is private, only the [% terms.bug %] id and some other basic information is sent and the external system will
+need to query BMO over the REST API to get the actual details of the [% terms.bug %].
+
+
The webhooks will be called in the same order as the events triggering them and will be one request per new
+[% terms.bug %] and per changed [% terms.bug %]. The "changes" parameter will be sent only when the triggered event is changed and will
+content every change made in the [% terms.bug %].
If the response is not 200, the [% terms.Bugzilla %] system will retry 4 attempts, waiting 5s, 10s, 15s and
+20s between each attempt. If none of those attempts is successful, the system will continue trying
+every 15 minutes.
+
+
If a message is stuck without a successful attempt, the next messages that trigger the webhook
+will be stored in a queue in the order that were triggered and will be delivered in that order when
+the first message in the queue is delivered.
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/js/attachment.js b/js/attachment.js
index 89d8d9dca..739c0e47a 100644
--- a/js/attachment.js
+++ b/js/attachment.js
@@ -177,6 +177,7 @@ function switchToMode(mode, patchviewerinstalled)
if (current_mode == 'edit') {
hideElementById('editFrame');
hideElementById('undoEditButton');
+ document.querySelector('input[name="markdown_off"]').value = 0;
} else if (current_mode == 'raw') {
hideElementById('viewFrame');
if (patchviewerinstalled)
@@ -195,6 +196,7 @@ function switchToMode(mode, patchviewerinstalled)
if (mode == 'edit') {
showElementById('editFrame');
showElementById('undoEditButton');
+ document.querySelector('input[name="markdown_off"]').value = 1;
} else if (mode == 'raw') {
showElementById('viewFrame');
if (patchviewerinstalled)
diff --git a/js/util.js b/js/util.js
index 670fd6e38..33dc2499f 100644
--- a/js/util.js
+++ b/js/util.js
@@ -450,11 +450,9 @@ Bugzilla.API = class API {
reject(new Bugzilla.Error({ name: 'TimeoutError', message: 'Request Timeout' }));
}, 30000);
- /** @throws {AbortError} */
- fetch(request, init).then(response => {
- /** @throws {SyntaxError} */
- return response.ok ? response.json() : { error: true };
- }).then(result => {
+ fetch(request, init)
+ .then(response => response.json())
+ .then(result => {
const { error, code, message } = result;
if (error) {
diff --git a/scripts/build-bmo-push-data.pl b/scripts/build-bmo-push-data.pl
index bef205f6e..aa4567d13 100755
--- a/scripts/build-bmo-push-data.pl
+++ b/scripts/build-bmo-push-data.pl
@@ -21,6 +21,7 @@ use IPC::System::Simple qw(runx capture);
use JSON::MaybeXS qw(decode_json);
use LWP::Simple qw(get);
use LWP::UserAgent;
+use Mozilla::CA;
use MIME::Base64 qw(decode_base64);
use URI::QueryParam;
use URI;
@@ -45,48 +46,46 @@ foreach my $line (@log) {
next;
}
- my @bug_ids;
+ my $bug_id;
if ($message =~ /\bBug (\d+)/i) {
- push @bug_ids, $1;
+ $bug_id = $1;
}
-
- if (!@bug_ids) {
+ else {
warn "skipping $line (no bug)\n";
next;
}
- foreach my $bug_id (@bug_ids) {
- my $duplicate = 0;
- foreach my $revisions (@revisions) {
- if ($revisions->{bug_id} == $bug_id) {
- $duplicate = 1;
- last;
- }
+ my $duplicate = 0;
+ foreach my $revisions (@revisions) {
+ if ($revisions->{bug_id} == $bug_id) {
+ $duplicate = 1;
+ last;
}
- next if $duplicate;
+ }
+ next if $duplicate;
- my $bug = fetch_bug($bug_id);
- if ($bug->{status} eq 'RESOLVED' && $bug->{resolution} ne 'FIXED') {
- next;
- }
- if ($bug->{summary} =~ /\bbackport\s+(?:upstream\s+)?bug\s+(\d+)/i) {
- my $upstream = $1;
- $bug->{summary} = fetch_bug($upstream)->{summary};
- }
- push @revisions,
- {hash => $revision, bug_id => $bug_id, summary => $bug->{summary},};
+ my $bug = fetch_bug($bug_id);
+ if ($bug->{status} eq 'RESOLVED' && $bug->{resolution} ne 'FIXED') {
+ next;
+ }
+ if ($bug->{summary} =~ /\bbackport\s+(?:upstream\s+)?bug\s+(\d+)/i) {
+ my $upstream = $1;
+ $bug->{summary} = fetch_bug($upstream)->{summary};
}
+ push @revisions,
+ {hash => $revision, bug_id => $bug_id, summary => $bug->{summary},};
}
+
if (!@revisions) {
- die
- "no new revisions. make sure you run this script before production is updated.\n";
+ warn
+ "no new revisions. make sure you run this script before deployment to production.\n";
}
else {
@revisions = reverse @revisions;
}
-my $first_revision = $revisions[0]->{hash};
-my $last_revision = $revisions[-1]->{hash};
+my $first_revision = @revisions ? $revisions[0]->{hash} : $prod_tag;
+my $last_revision = @revisions ? $revisions[-1]->{hash} : 'HEAD';
say "write tag.txt";
open my $tag_fh, '>', 'tag.txt';
@@ -99,6 +98,7 @@ open my $bug_fh, '>', 'bug.push.txt';
say $bug_fh
'https://bugzilla.mozilla.org/enter_bug.cgi?product=bugzilla.mozilla.org&component=Infrastructure&short_desc=push+updated+bugzilla.mozilla.org+live';
say $bug_fh "revisions: $first_revision - $last_revision";
+say $bug_fh 'no bugs' if !@revisions;
foreach my $revision (@revisions) {
say $bug_fh "bug $revision->{bug_id} : $revision->{summary}";
}
@@ -110,6 +110,7 @@ open my $blog_fh, '>', 'blog.push.txt';
say $blog_fh "[release tag]($tag_url)\n";
say $blog_fh
"the following changes have been pushed to bugzilla.mozilla.org:\n
";
+say $blog_fh '
no bugs
' if !@revisions;
foreach my $revision (@revisions) {
printf $blog_fh
'