]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
bmo sync - 2020-08-23 (#49)
authorDylan William Hardison <dylan@hardison.net>
Mon, 24 Aug 2020 00:42:33 +0000 (20:42 -0400)
committerGitHub <noreply@github.com>
Mon, 24 Aug 2020 00:42:33 +0000 (20:42 -0400)
* 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 <html> 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 <dklawren@users.noreply.github.com>
Co-authored-by: byron jones <byron@glob.com.au>
Co-authored-by: Emma Humphries <emceeaich@users.noreply.github.com>
Co-authored-by: David Lawrence <dkl@mozilla.com>
Co-authored-by: Lisset Cuevas <lisset.cuevasj@gmail.com>
Co-authored-by: Michael Kohler <me@michaelkohler.info>
Co-authored-by: Tom Ritter <tom@ritter.vg>
72 files changed:
.vscode/tasks.json
Bugzilla.pm
Bugzilla/App/Plugin/OAuth2.pm
Bugzilla/Attachment.pm [changed mode: 0644->0755]
Bugzilla/BugUrl/GitLab.pm
Bugzilla/CGI.pm
Bugzilla/Constants.pm
Bugzilla/DB/Schema.pm
Bugzilla/Install/DB.pm
Bugzilla/Test/Util.pm
Bugzilla/User.pm
docker-compose.test.yml
extensions/BMO/Extension.pm [changed mode: 0644->0755]
extensions/BMO/template/en/default/bug/create/create-data-compliance.html.tmpl
extensions/BMO/template/en/default/hook/bug_modal/attachments-row.html.tmpl [changed mode: 0644->0755]
extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl
extensions/BugModal/template/en/default/bug_modal/attachments.html.tmpl [changed mode: 0644->0755]
extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl [changed mode: 0644->0755]
extensions/EditComments/Extension.pm
extensions/EditComments/lib/WebService.pm
extensions/EditComments/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl
extensions/EditComments/template/en/default/hook/bug_modal/activity_stream-comment_action.html.tmpl
extensions/EditComments/template/en/default/hook/bug_modal/header-end.html.tmpl
extensions/EditComments/template/en/default/hook/global/header-start.html.tmpl
extensions/EditComments/template/en/default/pages/comment-revisions.html.tmpl
extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl
extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl
extensions/Needinfo/template/en/default/hook/request/email-after_summary.txt.tmpl
extensions/Push/Extension.pm
extensions/Push/lib/Admin.pm
extensions/Push/lib/BacklogQueue.pm
extensions/Push/lib/Config.pm
extensions/Push/lib/Connector/Webhook.pm [new file with mode: 0644]
extensions/Push/lib/Connectors.pm
extensions/Push/template/en/default/hook/global/user-error-errors.html.tmpl
extensions/Push/template/en/default/pages/push_config.html.tmpl
extensions/Push/template/en/default/pages/push_queues.html.tmpl
extensions/Push/template/en/default/pages/webhooks_config.html.tmpl [new file with mode: 0644]
extensions/REMO/template/en/default/bug/create/comment-mozreps.txt.tmpl
extensions/REMO/template/en/default/bug/create/create-mozreps.html.tmpl
extensions/REMO/web/js/moz_reps.js
extensions/Review/Extension.pm
extensions/Review/lib/Util.pm
extensions/Splinter/template/en/default/pages/splinter.html.tmpl
extensions/Webhooks/Config.pm [new file with mode: 0644]
extensions/Webhooks/Extension.pm [new file with mode: 0644]
extensions/Webhooks/lib/Config.pm [new file with mode: 0644]
extensions/Webhooks/lib/Webhook.pm [new file with mode: 0644]
extensions/Webhooks/template/en/default/account/prefs/webhooks.html.tmpl [new file with mode: 0644]
extensions/Webhooks/template/en/default/admin/params/webhooks.html.tmpl [new file with mode: 0644]
extensions/Webhooks/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl [new file with mode: 0644]
extensions/Webhooks/template/en/default/hook/global/user-error-errors.html.tmpl [new file with mode: 0644]
extensions/Webhooks/template/en/default/pages/webhooks.html.tmpl [new file with mode: 0644]
js/attachment.js
js/util.js
scripts/build-bmo-push-data.pl
scripts/generate_bmo_data.pl
scripts/generate_conduit_data.pl
scripts/import_email.pl [new file with mode: 0644]
skins/standard/global.css
t/.perlcritic-history
t/mojo-example.t
t/mojo-oauth2.t
template/en/default/account/auth/confirm_scopes.html.tmpl
template/en/default/account/prefs/prefs.html.tmpl
template/en/default/admin/oauth/create.html.tmpl
template/en/default/admin/oauth/edit.html.tmpl
template/en/default/admin/users/responsibilities.html.tmpl
template/en/default/attachment/edit.html.tmpl
template/en/default/attachment/list.html.tmpl [changed mode: 0644->0755]
template/en/default/bug/create/create.html.tmpl
template/en/default/global/user-error.html.tmpl

index fc7423f0f293b65d8d13a48c4445b102b2d549e1..f9a7a806e840a2174a6c57f0593edb9d43539e92 100644 (file)
         "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": []
+    },
   ]
 }
index 99388f609cf6eabdd562d6bab10780ca9ea4cc95..42b56ec03f6ea1ab3008adb2e3bbcdbd7cb7103b 100644 (file)
@@ -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;
index 5e51f85b9d19bd27efea7e8f466d6eb6592b8fa3..aa3d6cb4e9b6657be8fd090eec69670968950f9b 100644 (file)
@@ -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});
old mode 100644 (file)
new mode 100755 (executable)
index 12d7bd2..bc1640d
@@ -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
index 8e546c1dce5fd62d8cdce6386c2205e6e816efc7..d2e75dcce036300ebabb47acc5a06941cab41b49 100644 (file)
@@ -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 {
index e4245a86221695fccfc18f6f3ed236ba7b30a56f..f3f69b8b882f11b957ff7252652a4e60974347f0 100644 (file)
@@ -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('</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';
-    }
-  }
   my $headers = $self->SUPER::header(%headers) || '';
   if ($self->server_software eq 'Bugzilla::App::CGI') {
     my $c = $Bugzilla::App::CGI::C;
index 0cd18bdf62c91c33875bfaccff677df4dea7719c..44ffefbea99f91f82b0b02b7042f3b5265378fe7 100644 (file)
@@ -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;
 }
 
index 47638cccbe9ccfa08488f6d3cc2f50cc7624161b..bc14d18f4b17a16277f9bf1cd09fffa0810cb0a6 100644 (file)
@@ -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'},
     ],
   },
 
index 60ae614d0e56616bf0639b4b4953c2afca28baa5..b2155d057e972301a6c87a8ff87be65597e50645 100644 (file)
@@ -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 {
index 4d7d662ff9cba3365ab6e8a59a8afc5579381874..32ce2242bbf01196a8d3e078dc7a12e33bb55abc 100644 (file)
@@ -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";
index d3d989057c4ea3aa9499da16e6537a34bd013355..da7447442513c5e51cc7ae5425e95d601f1aebec 100644 (file)
@@ -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) {
index 1fb0655893f25f2b3bdf55d86ee3ddb46b5b2eec..de60c9589485be86a54f7dc4561384aa6d551baa 100644 (file)
@@ -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:
old mode 100644 (file)
new mode 100755 (executable)
index 7f31b6d..c46438f
@@ -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";
index 6e42709ca2ba7cd6c672752d5e59d50f7c9a30c5..a19f1215b3a75cb8e25cedcaac2f7efebb24054c 100644 (file)
     <option value="">-- Select --</option>
     <option value="Firefox">Firefox</option>
     <option value="Cloud Services">Cloud Services</option>
-    <option value="FxOS">FxOS</option>
     <option value="Foundation">Foundation</option>
     <option value="Engagement">Engagement</option>
     <option value="IT">IT</option>
old mode 100644 (file)
new mode 100755 (executable)
index f0a97b1..c8f887d
     [% END %]
   </td>
   <td class="attach-actions">
+    [% IF user.is_insider %]
     <a href="[% basepath FILTER none %]page.cgi?id=attachment_bounty_form.html&bug_id=[% bug.id FILTER none %]">
       Edit Bounty
     </a>
+    [% END %]
   </td>
 </tr>
index 6b379092b708c09b1182d3553dcfc34cf334169f..6352cf7d1c53c4ae28ccb28d0f77ae2a6538f3e0 100644 (file)
   [% IF comment.body || comment.count == 0 %]
     <[% comment_tag FILTER none %]
         class="comment-text [%= "markdown-body" IF comment.is_markdown %] [%= "bz_private" IF comment.is_private %]
-               [% "empty" IF !comment.body %]"
+               [% "empty" IF comment.body == "" %]"
         id="ct-[% comment.count FILTER none %]" data-comment-id="[% comment.id FILTER none %]"
         [% IF comment.is_markdown +%] data-ismarkdown="true" [% END ~%]
         [% IF comment.collapsed +%] style="display:none"[% END ~%]>
-      [%~ IF comment.body ~%]
+      [%~ IF comment.body != "" ~%]
         [%~ comment.body_full({ exclude_attachment => 1 }) FILTER renderMarkdown(bug, comment) ~%]
       [%~ ELSE ~%]
         <em>No description provided.</em>
old mode 100644 (file)
new mode 100755 (executable)
index c83885c..f391c19
 <table role="table" class="layout-table" id="attachments">
   [% FOREACH attachment IN bug.attachments %]
     [%
-      NEXT IF attachment.isprivate && !(user.is_insider || attachment.attacher.id == user.id);
+      NEXT IF attachment.isprivate
+        && !(user.is_insider
+              || attachment.attacher.id == user.id
+              || (attachment.is_bounty_attachment && user.id == bug.reporter.id));
       attachment_rendered = 0;
       Hook.process("row");
       NEXT IF attachment_rendered;
old mode 100644 (file)
new mode 100755 (executable)
index aa90993..504dbcd
@@ -48,7 +48,7 @@
   active_attachments = 0;
   obsolete_attachments = 0;
   FOREACH attachment IN bug.attachments;
-    NEXT IF attachment.isprivate && !(user.is_insider || attachment.attacher.id == user.id);
+    NEXT IF attachment.isprivate && !(user.is_insider || attachment.attacher.id == user.id || (attachment.is_bounty_attachment && bug.reporter.id == user.id)) ;
     IF attachment.isobsolete;
       obsolete_attachments = obsolete_attachments + 1;
     ELSE;
index 8244a84255766a128fcfee009bb12bcad3418abf..b3f09fdd41c101f04ceb6783871768857e5b9093 100644 (file)
@@ -98,13 +98,38 @@ sub page_before_template {
 
 BEGIN {
   no warnings 'redefine';
-  *Bugzilla::Comment::activity   = \&_get_activity;
-  *Bugzilla::Comment::edit_count = \&_edit_count;
+  *Bugzilla::Comment::edit_count          = \&_comment_edit_count;
+  *Bugzilla::Comment::is_editable_by      = \&_comment_is_editable_by;
+  *Bugzilla::Comment::activity            = \&_comment_get_activity;
+  *Bugzilla::User::can_edit_comments      = \&_user_can_edit_comments;
+  *Bugzilla::User::is_edit_comments_admin = \&_user_is_edit_comments_admin;
 }
 
-sub _edit_count { return $_[0]->{'edit_count'}; }
+sub _comment_edit_count { return $_[0]->{'edit_count'}; }
 
-sub _get_activity {
+sub _comment_is_editable_by {
+  my ($self, $user) = @_;
+  # Note: Does not verify that the bug is visible or editable by the user; the calling
+  # code needs to perform this validation at the bug level.
+
+  # Need to be able to edit comments via group membership
+  return 0 unless $user->can_edit_comments;
+
+  # Comment admins can edit any comment
+  return 1 if $user->is_edit_comments_admin;
+
+  # Can always edit your own comments
+  return 1 if $self->author->id == $user->id;
+
+  # Can edit comment 0 (description) on any bug, if enabled
+  return 1 if Bugzilla->params->{allow_global_initial_comment_editing}
+    && $self->count == 0;
+
+  # Otherwise not editable
+  return 0;
+}
+
+sub _comment_get_activity {
   my ($self, $activity_sort_order) = @_;
 
   return $self->{'activity'} if $self->{'activity'};
@@ -138,7 +163,6 @@ sub _get_activity {
       {
         author       => Bugzilla::User->new({id => $revision->{userid}, cache => 1}),
         created_time => $revision->{time},
-        old          => $revision->{old},
         revised_time => $current ? undef : $prev_rev->{time},
         new          => $current ? $self->body : $prev_rev->{old},
         is_hidden    => $current ? 0 : $prev_rev->{is_hidden},
@@ -175,6 +199,29 @@ sub _get_activity {
   return $self->{'activity'};
 }
 
+sub _user_can_edit_comments {
+  my ($self) = @_;
+  # Checks that edit-comments is enabled, and if the user is a member of a group
+  # controlling it, or if the user is an edit-comments-admin.
+
+  my $edit_comments_group = Bugzilla->params->{edit_comments_group};
+
+  return $self->{can_edit_comments} //=
+    $self->is_edit_comments_admin()
+    || ($edit_comments_group && $self->in_group($edit_comments_group));
+}
+
+sub _user_is_edit_comments_admin {
+  my ($self) = @_;
+  # Checks that edit-all-comments is enabled, and if the user is a member of a group
+  # controlling it.
+
+  my $edit_comments_admins_group = Bugzilla->params->{edit_comments_admins_group};
+
+  return $self->{is_edit_comments_admin} //=
+    $edit_comments_admins_group && $self->in_group($edit_comments_admins_group);
+}
+
 #########
 # Hooks #
 #########
@@ -190,13 +237,9 @@ sub object_columns {
 sub bug_end_of_update {
   my ($self, $args) = @_;
 
-  # Silently return if not in the proper group
-  # or if editing comments is disabled
-  my $user                = Bugzilla->user;
-  my $edit_comments_group = Bugzilla->params->{edit_comments_group};
-  return
-    unless $user->is_insider
-    || $edit_comments_group && $user->in_group($edit_comments_group);
+  # Silently return if not in the proper group or if editing comments is disabled
+  my $user = Bugzilla->user;
+  return unless $user->can_edit_comments();
 
   my $bug       = $args->{bug};
   my $timestamp = $args->{timestamp};
@@ -212,17 +255,17 @@ sub bug_end_of_update {
     my ($comment_obj) = grep($_->id == $comment_id, @{$bug->comments});
     next if (!$comment_obj || ($comment_obj->is_private && !$user->is_insider));
 
-# Insiders can edit any comment while unprivileged users can only edit their own comments
-    next unless $user->is_insider || $comment_obj->author->id == $user->id;
+    # Check that user can edit the comment.
+    next unless $comment_obj->is_editable_by($user);
 
     my $new_comment = $comment_obj->_check_thetext($params->{$param});
 
     my $old_comment = $comment_obj->body;
     next if $old_comment eq $new_comment;
 
-    # Insiders can hide comment revisions where needed
+    # edit_comments_admins_group members can hide comment revisions where needed
     my $is_hidden
-      = (  $user->is_insider
+      = (  $user->is_edit_comments_admin
         && defined $params->{"edit_comment_checkbox_$comment_id"}
         && $params->{"edit_comment_checkbox_$comment_id"} == 'on') ? 1 : 0;
 
@@ -260,6 +303,20 @@ sub config_modify_panels {
     default => 'editbugs',
     checker => \&check_group
     };
+  push @{$args->{panels}->{groupsecurity}->{params}},
+    {
+    name    => 'edit_comments_admins_group',
+    type    => 's',
+    choices => \&get_all_group_names,
+    default => 'admin',
+    checker => \&check_group
+    };
+  push @{$args->{panels}->{groupsecurity}->{params}},
+    {
+    name    => 'allow_global_initial_comment_editing',
+    type    => 'b',
+    default => 1
+    };
 }
 
 sub get_bug_activity {
@@ -267,9 +324,10 @@ sub get_bug_activity {
 
   return unless $args->{include_comment_activity};
 
-  my $list       = $args->{list};
-  my $starttime  = $args->{start_time};
-  my $is_insider = Bugzilla->user->is_insider;
+  my $list               = $args->{list};
+  my $starttime          = $args->{start_time};
+  my $is_insider         = Bugzilla->user->is_insider;
+  my $is_comments_admin  = Bugzilla->user->is_edit_comments_admin;
   my $hidden_placeholder = '(Hidden by Administrator)';
 
   my $query = "SELECT DISTINCT(c.comment_id) from longdescs_activity AS a
index fe677e1e5d4ea40f4feaed0b303cc46706c9ebd1..002fbe278fa21d4acb84f6ef8a5d76081274a6ee 100644 (file)
@@ -68,14 +68,7 @@ sub comments {
 # This should be migrated to the standard API method at /rest/bug/comment/(comment_id)
 sub update_comment {
   my ($self, $params) = @_;
-  my $user                = Bugzilla->login(LOGIN_REQUIRED);
-  my $edit_comments_group = Bugzilla->params->{edit_comments_group};
-
-  # Validate group membership
-  ThrowUserError('auth_failure',
-    {group => $edit_comments_group, action => 'view', object => 'editcomments'})
-    unless $user->is_insider
-    || $edit_comments_group && $user->in_group($edit_comments_group);
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
 
   my $comment_id
     = (defined $params->{comment_id} && $params->{comment_id} =~ /^(\d+)$/)
@@ -97,18 +90,18 @@ sub update_comment {
   ThrowUserError('comment_is_private', {id => $comment->id})
     unless $user->is_insider || !$comment->is_private;
 
-# Insiders can edit any comment while unprivileged users can only edit their own comments
-  ThrowUserError('auth_failure',
-    {group => 'insidergroup', action => 'view', object => 'editcomments'})
-    unless $user->is_insider || $comment->author->id == $user->id;
-
-  my $bug         = $comment->bug;
-  my $old_comment = $comment->body;
-  my $new_comment = $comment->_check_thetext($params->{new_comment});
+  my $bug = $comment->bug;
 
   # Validate bug visibility
   $bug->check_is_visible();
 
+  # Check if user can edit this comment.
+  ThrowUserError('auth_failure', {action => 'view', object => 'editcomments'})
+    unless $comment->is_editable_by($user);
+
+  my $old_comment = $comment->body;
+  my $new_comment = $comment->_check_thetext($params->{new_comment});
+
   # Make sure there is any change in the comment
   ThrowCodeError('param_no_changes',
     {function => 'EditComments.update_comment', param => 'new_comment'})
@@ -117,9 +110,9 @@ sub update_comment {
   my $dbh         = Bugzilla->dbh;
   my $change_when = $dbh->selectrow_array('SELECT NOW()');
 
-  # Insiders can hide comment revisions where needed
+  # edit_comments_admins_group members can hide comment revisions where needed
   my $is_hidden
-    = (  $user->is_insider
+    = (  $user->is_edit_comments_admin
       && defined $params->{is_hidden}
       && $params->{is_hidden} == 1) ? 1 : 0;
 
@@ -164,10 +157,10 @@ sub modify_revision {
   my ($self, $params) = @_;
   my $user = Bugzilla->login(LOGIN_REQUIRED);
 
-  # Only allow insiders to modify revisions
+  # Only allow edit_comments_admins_group members to modify revisions
   ThrowUserError('auth_failure',
-    {group => 'insidergroup', action => 'view', object => 'editcomments'})
-    unless $user->is_insider;
+    {group => 'edit_comments_admins_group', action => 'view', object => 'editcomments'})
+    unless $user->is_edit_comments_admin;
 
   my $comment_id
     = (defined $params->{comment_id} && $params->{comment_id} =~ /^(\d+)$/)
index c35dc0d98555e88956f0df120c2b54b488959551..e06028d053a7bf483cbfe01f43e217e7a5ababb6 100644 (file)
@@ -8,7 +8,14 @@
 
 [% IF panel.name == "groupsecurity" %]
   [% panel.param_descs.edit_comments_group =
-    'The name of the group of users who can edit their own comments. Leave it blank to disable comment editing. ' _
-    'Insiders can always edit any comment and hide revisions where needed.'
+    'The name of the group of users who can edit their own comments. ' _
+    'Leave it blank to disable comment editing.'
+  %]
+  [% panel.param_descs.edit_comments_admins_group =
+    'The name of the group of users who can edit any comments, and hide revisions where needed. ' _
+    'Leave it blank to disable comment editing administrators.'
+  %]
+  [% panel.param_descs.allow_global_initial_comment_editing =
+    'Allow anyone with the ability to edit comments to edit the description (comment 0) of any bug.'
   %]
 [% END -%]
index 490923b4b2894d53f8cbeb2b01a94da3338f1dae..7b3c17b86cd9a1df8444b1d9331ce48c7159ca13 100644 (file)
@@ -8,8 +8,7 @@
 
 [%
   RETURN UNLESS comment.body || comment.count == 0;
-  RETURN UNLESS user.is_insider
-    || Param('edit_comments_group') && user.in_group(Param('edit_comments_group')) && comment.author.id == user.id;
+  RETURN UNLESS comment.is_editable_by(user);
   RETURN UNLESS
     comment.type == constants.CMT_NORMAL
     || comment.type == constants.CMT_DUPE_OF
index b4854778591920e1487ef13345418f64f47ecda1..30e582286dd45755e257f3adc2b8129267d3a884 100644 (file)
@@ -10,8 +10,6 @@
   # Always load CSS to style the "Edited" revision link
   style_urls.push('extensions/EditComments/web/styles/inline-editor.css');
 
-  RETURN UNLESS user.is_insider
-    || Param('edit_comments_group') && user.in_group(Param('edit_comments_group'));
-
+  RETURN UNLESS user.can_edit_comments();
   javascript_urls.push('extensions/EditComments/web/js/inline-editor.js');
 %]
index d6af09a08f72196dbeb0195d0878b0152760b087..26d40e599b62a077d5a946e54e03ca0113a4a71c 100644 (file)
@@ -8,8 +8,7 @@
 
 [%
   RETURN UNLESS bug.defined;
-  RETURN UNLESS user.is_insider
-    || Param('edit_comments_group') && user.in_group(Param('edit_comments_group'));
+  RETURN UNLESS user.can_edit_comments();
 
   # Expose strings used in JavaScript
   js_BUGZILLA.string.InlineCommentEditor = {
@@ -24,7 +23,7 @@
     preview_error = 'Preview could not be loaded. Please try again later.',
     revision_count => [ '%d revision', '%d revisions' ],
     save => 'Update Comment',
-    save_error => 'Updated comment could not be saved. Please try again later.',
+    save_error => 'Updated comment could not be saved.',
     save_tooltip => 'Save the changes',
     saving => 'Saving…',
     toolbar => 'Comment Editor Toolbar',
index 9b4cb40254db464b8b56459c54297d095ee5e6ef..9ae06aa033f457d1878425ec0caa0bbce93d4c9d 100644 (file)
@@ -11,7 +11,7 @@
   PROCESS global/header.html.tmpl
     title           = "$terms.Bug $bug.id Comment $comment.count Edit History"
     style_urls      = ['extensions/EditComments/web/styles/revisions.css']
-    javascript_urls = (user.is_insider ? ['extensions/EditComments/web/js/revisions.js'] : [])
+    javascript_urls = (user.is_edit_comments_admin ? ['extensions/EditComments/web/js/revisions.js'] : [])
     generate_api_token = 1
 %]
 
   <article class="revision" data-comment-id="[% comment.id FILTER html %]" data-revised-time="[% a.revised_time FILTER html %]">
     <header>
       <div class="metadata">
-        [% a.old ? "Revision $rev_count" : "Original comment" FILTER html %]
+        [% rev_count == 0 ? "Original comment" : "Revision $rev_count" FILTER html %]
         by [% INCLUDE bug_modal/user.html.tmpl u=a.author %]
         on <time datetime="[% a.created_time FILTER html %]">[% a.created_time FILTER time %]</time>
       </div>
-      [% IF user.is_insider && a.revised_time %]
+      [% IF user.is_edit_comments_admin && a.revised_time %]
         <div class="actions">
           <label><input type="checkbox" name="is_hidden" [% "checked" IF a.is_hidden %]> Hide</label>
         </div>
@@ -40,7 +40,7 @@
     <div class="body">
       [% IF !user.is_insider && a.is_hidden %]
         <div class="hidden-comment">(Hidden by Administrator)</div>
-      [% ELSIF comment.count == 0 && !a.new %]
+      [% ELSIF comment.count == 0 && a.new == "" %]
         <div class="empty-comment">No description provided.</div>
       [% ELSE %]
         <pre class="bz_comment_text">[% a.new FILTER quoteUrls(bug) %]</pre>
index d202a3c780533019677d2e393297df0a4c944fd7..e177caf12e65efd453a5bc88270373866634cb13 100644 (file)
@@ -132,12 +132,6 @@ tabbed browsing or the location bar)
 
 <ul class="product-list">
 [% 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 %]
 </ul>
 
 <div id="prod_comp_search_main">
index 047d64abc8b99c57fc7c94b65bd4f86d52d0ae82..38affef3079131c0f7e410d60ab532e1adc1f236 100644 (file)
@@ -14,15 +14,37 @@ For [% terms.bugs %] in Firefox, the Mozilla Foundation's web browser.
 
 (<a href="https://www.mozilla.org/en-US/firefox/desktop/">more info</a>)
 [% 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.
+
+<i>If you are reporting a security issue that puts users at risk,
+<b><a href="/enter_bug.cgi?format=guided#h=dupes|Fenix|Security%3A%20Android">use
+this form</a>.</b></i>
+[% 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.
+
+<i>If you are reporting a security issue that puts users at risk,
+<b><a href="/enter_bug.cgi?format=guided#h=dupes|Focus|Security%3A iOS">use
+this form</a>.</b></i>
+[% 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 %]
index 6a302b76ac0e20ef025f25f568dd177856a685a2..66a6d1ad1689b3ef7036319015530a17664f43ac 100644 (file)
@@ -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
+---
 ---
index b8fa9d90e42a396dc57f2d37cb5dacb38b6577aa..265330a971ffa929b284cce28361c17bdf873252 100644 (file)
@@ -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');
+    }
+  }
 }
 
 #
index aa78b2d0310b35d8912bfb382fe26c9d957105dc..0369e2037a7ff9380264a3a0518e337ce8beb8cf 100644 (file)
@@ -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;
index 17d0a188f24a8a644d818ffdf46c38c527573c88..189b061676788befde02bfcdf66c6fd147558ba7 100644 (file)
@@ -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
 #
index f9f8fcb46bd3765c0353be7ba16d8e804e883392..3c45e2410d279d6092f4b7493fc9ee648cb03643 100644 (file)
@@ -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 (file)
index 0000000..ba74a9c
--- /dev/null
@@ -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;
index 3276759b955a1eea10f59f83382c0662b7d3e59a..b4851fa4f874de08a9bb90db8701ebc27fb67588 100644 (file)
@@ -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);
   }
 }
 
index 2b8a1c4e0122b4497c6e41431e08c170e67ec51b..4a6a5fbf4cb207f29a9d4e4d1f656b53a4029458 100644 (file)
@@ -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 %]
index 17f9506a82ccd85b73ca8569e8a127d7ef2fec82..82cad0c71fa01d8ad1f79a0a769d2674d78595fd 100644 (file)
@@ -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();
   <td width="100%">&nbsp;</td>
 </tr>
 
+[% IF Bugzilla.params.${'webhooks_enabled'} %]
+  <tr>
+    <td>&nbsp;</td>
+    <td colspan="2">
+      <a href="page.cgi?id=webhooks_config.html">See webhooks configurations</a>
+    </td>
+  </tr>
+[% END %]
+
 </table>
 
 </form>
@@ -106,7 +116,7 @@ var push_defaults = new Array();
             [% END %]
           >
             [% IF option.name != 'enabled' && !option.required %]
-              <option value="""
+              <option value=""
                [% ' selected' IF config.${option.name} == "" %]></option>
             [% END %]
             [% FOREACH value = option.values %]
index ae24c285094af989fc52b64e82f3ab1ee119e098..4538c94e9cf50cd86b1e2ccfc7f9351208dd7e6b 100644 (file)
@@ -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 (file)
index 0000000..c183902
--- /dev/null
@@ -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' ]
+%]
+
+<form method="POST" action="[% basepath FILTER none %]page.cgi">
+<input type="hidden" name="id" value="webhooks_config.html">
+<input type="hidden" name="save" value="1">
+<input type="hidden" id="token" name="token" value="[% issue_hash_token(['webhooks_config']) FILTER html %]">
+
+[% IF webhooks.size %]
+
+  <h3>
+    Current webhooks:
+  </h3>
+
+  <table id="webhooks_table" class="standard">
+  <thead>
+    <tr>
+      <th>Status</th>
+      <th>ID</th>
+      <th>User</th>
+      <th>Name</th>
+      <th>URL</th>
+      <th>Events</th>
+      <th>Product</th>
+      <th>Component</th>
+    </tr>
+  </thead>
+  <tbody>
+  [% FOREACH webhook IN webhooks %]
+    <tr>
+      <td>
+        [% connector = connectors.by_name('Webhook_' _ webhook.id)
+           config = connector.config
+        %]
+        <select name="[% connector.name FILTER html %].enabled"
+                id="[% connector.name FILTER html %]_enabled">
+          <option value="Enabled" [% 'selected' IF config.${'enabled'} == 'Enabled' %]>Enabled</option>
+          <option value="Disabled" [% 'selected' IF config.${'enabled'} == 'Disabled' %]>Disabled</option>
+        </select>
+      </td>
+      <td>[% webhook.id FILTER html %]</td>
+      <td>[% webhook.user.login_name FILTER html %]</td>
+      <td>[% webhook.name FILTER html %]</td>
+      <td>
+        <a href="[% webhook.url FILTER html %]">
+          [% webhook.url FILTER html %]
+        </a>
+      </td>
+      <td>[% webhook.event FILTER html %]</td>
+      <td>[% webhook.product.name FILTER html %]</td>
+      <td>[% webhook.component ? webhook.component.name : 'Any' FILTER html %]</td>
+    </tr>
+  [% END %]
+  </tbody>
+  </table>
+  <br>
+  <input id="submit" type="submit" value="Submit Changes">
+
+[% ELSE %]
+
+  <p>
+    <i>Do not exist any webhooks.</i>
+  </p>
+
+[% END %]
+
+</form>
+
+[% INCLUDE global/footer.html.tmpl %]
index 8e8ae7362b75b783ccef5792b62b167dc8930b54..e6a38ee1e49a37a362548b3f8882718862dabbac 100644 (file)
@@ -36,7 +36,7 @@ Languages:
 [%+ cgi.param('languages') %]
 
 [% END %]
-Mozillians.org Account URL:
+people.mozilla.org Profile URL:
 [%+ cgi.param('mozillian') %]
 
 References:
index db8c835f2a894789efe29ca8857c6597e3537cba..01a4369f4efdbb30336c688333ad5c3fbb3ecf10 100644 (file)
     </tr>
 
     <tr>
-      <th><label for="vouched" class="required">Do you have a vouched Mozillian account?</label></th>
+      <th><label for="pmo_account" class="required">Do you have a people.mozilla.org account?</label></th>
       <td>
-        <select id="vouched">
+        <select id="pmo_account">
           <option value="Yes">Yes</option>
           <option value="No">No</option>
         </select>
       </td>
     </tr>
 
-    <tr id="vouched_warning" style="display: none">
+    <tr id="pmo_warning" style="display: none">
       <td colspan="2">
-        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.
       </td>
     </tr>
 
     <tr>
-      <th><label class="required" for="mozillian">Mozillians.org Account URL:</label></th>
+      <th><label class="required" for="mozillian">people.mozilla.org Profile URL:</label></th>
       <td>
-        <input type="url" id="mozillian" name="mozillian" size="40" placeholder="https://mozillians.org/u/name">
+        <input type="url" id="mozillian" name="mozillian" size="40" placeholder="https://people.mozilla.org/p/name">
       </td>
     </tr>
 
     <tr>
       <th>
         <label for="information">
-          Is your Mozillians account filled with the <a href="https://wiki.mozilla.org/Reps/Application_Process/Selection_Criteria" target="_blank">required information</a>?
+          Is your people.mozilla.org profile filled with the <a href="https://wiki.mozilla.org/Reps/Application_Process/Selection_Criteria" target="_blank">required information</a>?
         </label>
       </th>
       <td>
 
     <tr id="information_warning" style="display: none">
       <td colspan="2">
-        Please fill out your Mozillians account according to the <a href="https://wiki.mozilla.org/Reps/Application_Process/Selection_Criteria" target="_blank">Selection Criteria</a>.
+        Please fill out your people.mozilla.org profile according to the <a href="https://wiki.mozilla.org/Reps/Application_Process/Selection_Criteria" target="_blank">Selection Criteria</a>.
       </td>
     </tr>
 
index 083172c7db7138998e4569f948d2ba1e1169eebb..68fb81590b0aedb3442a6f5db95aa111834c1c82 100644 (file)
@@ -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 {
index ca242024178f335e32e42d489338117dc81168f7..fa4900ff2a3a154e70e195068b73bdadbb0ee7f1 100644 (file)
@@ -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
index 61d4e91175b6d39c69232ac787c708ea66a35d5e..ca5bb9063fa90efc2f0e45a3c54bbcf0696db119 100644 (file)
@@ -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}});
 }
index 484bb7792591f28a8e63d13a9fb3c254db9c8288..15e45791cb21ffc8b7de7efe4f17eb280eb94ae1 100644 (file)
           [% 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 (file)
index 0000000..01d236b
--- /dev/null
@@ -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 (file)
index 0000000..1a02ea9
--- /dev/null
@@ -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 (file)
index 0000000..ca3e800
--- /dev/null
@@ -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 (file)
index 0000000..48eaa73
--- /dev/null
@@ -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 (file)
index 0000000..8b74601
--- /dev/null
@@ -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 %]
+
+<script [% script_nonce FILTER none %]>
+var useclassification = false;
+var first_load = true;
+var last_sel = [];
+var cpts = new Array();
+[% n = 0 %]
+[% FOREACH prod = selectable_products %]
+  cpts['[% n %]'] = [
+    [%- FOREACH comp = prod.components %]'[% comp.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+  [% n = n + 1 %]
+[% END %]
+</script>
+
+<script src="[% 'js/productform.js' FILTER version FILTER html %]"></script>
+
+<script [% script_nonce FILTER none %]>
+function onSelectProduct() {
+  var component = document.getElementById('component');
+  selectProduct(document.getElementById('product'), component);
+  // selectProduct only supports Any on both elements
+  // we only want it on component, so add it back in
+  try {
+    component.add(new Option('Any', ''), component.options[0]);
+  } catch(e) {
+    // support IE
+    component.add(new Option('Any', ''), 0);
+  }
+  document.getElementById('component').options[0].selected = true;
+}
+
+function onRemoveChange() {
+  var cbs = document.getElementById('webhooks_table').getElementsByTagName('input');
+  for (var i = 0, l = cbs.length; i < l; i++) {
+    if (cbs[i].checked) {
+      document.getElementById('remove').disabled = false;
+      return;
+    }
+  }
+  document.getElementById('remove').disabled = true;
+}
+
+window.onload = function() {
+  onSelectProduct();
+  onRemoveChange();
+};
+</script>
+
+<p>
+  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.<br>
+  Documentation about webhooks is available <a href="page.cgi?id=webhooks.html">here</a>.
+</p>
+
+<table border="0" cellpadding="3" cellspacing="0">
+<tr>
+  <th align="right">Name:</th>
+  <td><input type="text" name="name" id="name" maxlength="64"></td>
+</tr>
+<tr>
+  <th align="right">URL:</th>
+  <td><input type="text" name="url" id="url" maxlength="64"></td>
+</tr>
+</table>
+<h4>Events</h4>
+<p>Select the events you want to receive.</p>
+<p>
+  <input type="checkbox" id="create_event" name="create_event" value="1">
+  <label for="create_event">When a new [% terms.bug %] is created</label>
+<br>
+  <input type="checkbox" id="change_event" name="change_event" value="1">
+  <label for="change_event">When an existing [% terms.bug %] is modified</label>
+</p>
+<h4>Filters</h4>
+<p>
+  To receive all components in a product, select "Any".
+</p>
+<table border="0" cellpadding="3" cellspacing="0">
+<tr>
+  <th align="right">Product:</th>
+  <td>
+    <select name="add_product" id="product" onChange="onSelectProduct()">
+    [% FOREACH product IN selectable_products %]
+      <option>[% product.name FILTER html %]</option>
+    [% END %]
+    </select>
+  </td>
+</tr>
+<tr>
+  <th align="right">Component:</th>
+  <td>
+    <select name="add_component" id="component">
+      <option value="">Any</option>
+    [% FOREACH product IN selectable_products %]
+      [% FOREACH component IN product.components %]
+        <option>[% component.name FILTER html %]</option>
+      [% END %]
+    [% END %]
+    </select>
+  </td>
+</tr>
+<br>
+<tr>
+  <td>&nbsp;</td>
+  <td><input type="submit" id="add_webhook" name="add_webhook" value="Add"></td>
+</tr>
+</table>
+
+<hr>
+
+[% IF webhooks.size %]
+
+  <h3>
+    Your webhooks:
+  </h3>
+
+  <table id="webhooks_table" class="standard">
+  <thead>
+    <tr>
+      <th>Remove</th>
+      <th>ID</th>
+      <th>Name</th>
+      <th>URL</th>
+      <th>Events</th>
+      <th>Product</th>
+      <th>Component</th>
+      <th>Status</th>
+    </tr>
+  </thead>
+  <tbody>
+  [% FOREACH webhook IN webhooks %]
+    <tr>
+      <td>
+        <input type="checkbox" onChange="onRemoveChange()"
+               name="remove" value="[% webhook.id FILTER none %]">
+      </td>
+      <td>[% webhook.id FILTER html %]</td>
+      <td>[% webhook.name FILTER html %]</td>
+      <td>
+        <a href="[% webhook.url FILTER html %]">
+          [% webhook.url FILTER html %]
+        </a>
+      </td>
+      <td>[% webhook.event FILTER html %]</td>
+      <td>[% webhook.product.name FILTER html %]</td>
+      <td>[% webhook.component ? webhook.component.name : 'Any' FILTER html %]</td>
+      <td>
+        [% connector = connectors.by_name('Webhook_' _ webhook.id)
+           config = connector.config
+        %]
+        <select name="[% connector.name FILTER html %].enabled"
+                id="[% connector.name FILTER html %]_enabled">
+          <option value="Enabled" [% 'selected' IF config.${'enabled'} == 'Enabled' %]>Enabled</option>
+          <option value="Disabled" [% 'selected' IF config.${'enabled'} == 'Disabled' %]>Disabled</option>
+        </select>
+      </td>
+    </tr>
+  [% END %]
+  </tbody>
+  </table>
+  <br>
+  <input id="save_changes" type="submit" value="Save Changes">
+
+[% ELSE %]
+
+  <p>
+    <i>You do not have any webhooks.</i>
+  </p>
+
+[% 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 (file)
index 0000000..3e6e2a1
--- /dev/null
@@ -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 (file)
index 0000000..f4102f2
--- /dev/null
@@ -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 (file)
index 0000000..21f0748
--- /dev/null
@@ -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 (file)
index 0000000..15e5f66
--- /dev/null
@@ -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%
+            }
+            "
+%]
+
+<h1>[% terms.Bugzilla %] Webhooks</h1>
+
+<p>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.</p>
+
+<p>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.</p>
+
+<p>Examples of [% terms.Bugzilla %] webhooks could include:</p>
+<ul>
+  <li>Updating a copy of the [% terms.Bugzilla %] [% terms.bug %] in another system such as Jira</li>
+  <li>Sending a message to a chat server such as Matrix or Slack</li>
+</ul>
+
+<h2>Creating a webhook</h2>
+<p>To create a webhook follow the next steps:</p>
+<ol>
+  <li>Access to your [% terms.Bugzilla %] Account.</li>
+  <li>Go to your Preferences Panel > Webhooks.</li>
+  <li>Fill out all of the parameters:</li>
+    <h4>Name</h4>
+    <p>A name for the webhook, which should be descriptive (e.g. “Jira Webhook for New & Updated [% terms.Bugs %] in Core::Graphics”)</p>
+
+    <h4>URL</h4>
+    <p>The URL which will receive and process the webhook.</p>
+
+    <h4>Events</h4>
+    <p>The [% terms.bug %] events that will trigger your new webhook.</p>
+      <ul>
+        <li><b>When a new [% terms.bug %] is created</b></li>
+        <li><b>When an existing [% terms.bug %] is modified</b></li>
+      </ul>
+
+    <h4>Filters</h4>
+    <p>Properties of a [% terms.bug %] that specify which [% terms.bugs %] you will receive.</p>
+      <ul>
+        <li><b>Product</b>: name of the product of the [% terms.bugs %] that you want to receive.</li>
+        <li><b>Component</b>: 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 <b>Any</b>.</li>
+      </ul>
+    <br>
+  <li>Add the webhook.</li>
+</ol>
+
+<p>You can see your registered webhooks in the same panel.</p>
+<p>If you want to delete a webhook, select the webhook in “Your webhooks” table and click remove
+selected. You can select more than one.</p>
+
+<h2>Delivered webhook</h2>
+<p>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.</p>
+
+<p>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 %].</p>
+
+<p>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 %].</p>
+
+<h4>Public [% terms.Bug %] Request</h4>
+<xmp>{
+"webhook_id": 23,
+"webhook_name": "Example webhook",
+"event": {
+      "action": "create",
+      "time": "2020-07-24T20:11:22",
+      "user": {
+          "id": 1,
+            "login": "admin@bmo.test",
+            "real_name": "Vagrant User"
+      },
+      "changes": [
+          {
+            "field": "priority",
+            "removed": "P3",
+            "added": "P1"
+            }
+        ],
+},
+"[% terms.bug %]": {
+      "version": {
+            "name": "unspecified",
+            "id": 1
+      },
+      "qa_contact": null,
+      "product": {
+            "id": 1,
+            "name": "TestProduct"
+      },
+      "creation_time": "2020-07-23T00:01:45",
+      "resolution": "",
+      "flags": [],
+      "classification": "Unclassified",
+      "url": "",
+      "type": "enhancement",
+      "operating_system": "Linux",
+      "keywords": [],
+      "status": {
+            "name": "CONFIRMED",
+            "id": 2
+      },
+      "last_change_time": "2020-07-23T00:01:45",
+      "is_private": false,
+      "severity": "normal",
+      "summary": "Test",
+      "alias": "",
+      "reporter": {
+            "id": 1,
+            "real_name": "Vagrant User",
+            "login": "admin@bmo.test"
+      },
+      "assigned_to": {
+          "id": 1,
+            "login": "admin@bmo.test",
+            "real_name": "Vagrant User"
+        },
+      "platform": "PC",
+      "id": 11,
+      "priority": "P1",
+      "target_milestone": null,
+      "cf_user_story": "",
+      "cf_last_resolved": null,
+      "whiteboard": "",
+      "component": {
+            "id": 2,
+            "name": "TestComponent"
+      }
+   }
+}</xmp>
+
+<h4>Private [% terms.Bug %] Request</h4>
+<xmp>{
+"webhook_id": 23,
+"webhook_name": "Example webhook",
+"event": {
+      "action": "change",
+      "time": "2020-07-24T20:11:22",
+      "user": {
+          "id": 1,
+            "login": "admin@bmo.test",
+            "real_name": "Vagrant User"
+      },
+},
+"[% terms.bug %]": {
+      "id": 2,
+      "is_private": true
+      }
+}</xmp>
+
+<h4>Response</h4>
+<p>HTTP 200 OK. The request has succeeded.</p>
+
+<h4>Errors</h4>
+<p>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.</p>
+
+<p>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.</p>
+
+[% INCLUDE global/footer.html.tmpl %]
index 89d8d9dcae91592ac14b230911edd2c7bda5e5fa..739c0e47a4b8e17be2e0f7e10c42da604311aeb9 100644 (file)
@@ -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)
index 670fd6e380c68e422561ffe50b985cc65fa626e9..33dc2499fe8ab05904f753db0407946d82bc89b0 100644 (file)
@@ -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) {
index bef205f6e0acba751951b66270596f4e16091429..aa4567d1347e70676549acc77a9fd4bc3754c5e3 100755 (executable)
@@ -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<ul>";
+say $blog_fh '<li>no bugs</li>' if !@revisions;
 foreach my $revision (@revisions) {
   printf $blog_fh
     '<li>[<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=%s" target="_blank">%s</a>] %s</li>%s',
@@ -127,6 +128,7 @@ open my $email_fh, '>', 'email.push.txt';
 say $email_fh
   "the following changes have been pushed to bugzilla.mozilla.org:\n";
 say $email_fh "(tag: $tag_url)\n";
+say $email_fh 'no bugs' if !@revisions;
 foreach my $revision (@revisions) {
   printf $email_fh "https://bugzil.la/%s : %s\n", $revision->{bug_id},
     $revision->{summary};
@@ -139,6 +141,7 @@ open my $wiki_fh, '>', 'wiki.push.txt';
 say $wiki_fh 'https://wiki.mozilla.org/BMO/Recent_Changes';
 say $wiki_fh '== ' . DateTime->now->set_time_zone('UTC')->ymd('-') . " ==\n";
 say $wiki_fh "[$tag_url $tag]";
+say $wiki_fh '* no bugs' if !@revisions;
 foreach my $revision (@revisions) {
   printf $wiki_fh "* {{bug|%s}} %s\n", $revision->{bug_id}, $revision->{summary};
 }
@@ -176,6 +179,7 @@ sub fetch_bug {
 sub _get {
   my ($endpoint, $args) = @_;
   my $ua = LWP::UserAgent->new(agent => $PROGRAM_NAME);
+  $ua->ssl_opts(SSL_ca_file => Mozilla::CA::SSL_ca_file());
   $args //= {};
 
   if (exists $args->{include_fields} && ref($args->{include_fields})) {
index 30ae5a49e569988e85c0da402d8880d49b350ab8..c4860617b4370b0dfc07ec084f6912975e9d3c2a 100755 (executable)
@@ -498,7 +498,7 @@ my %set_params = (
   mail_delivery_method => 'Test',
   mailfrom             => '"Bugzilla@Mozilla" <bugzilla-daemon@mozilla.org>',
   maintainer           => 'bugzilla-admin@mozilla.org',
-  maxattachmentsize    => '10240',
+  maxattachmentsize    => '4096',
   maxusermatches       => '100',
   mostfreqthreshold    => '5',
   mybugstemplate       => 'buglist.cgi?bug_status=UNCONFIRMED&amp;bug_status=NEW'
index c1c63e2d30f7f37d91aa98ac45b23a08e236469f..441b97b40b11b6d1570c1d1cbf0585c5395105d2 100755 (executable)
@@ -197,7 +197,7 @@ if ($oauth_id && $oauth_secret) {
     undef, $oauth_id);
 
   my $scope_id = $dbh->selectrow_array(
-    'SELECT id FROM oauth2_scope WHERE description = \'user:read\'', undef);
+    'SELECT id FROM oauth2_scope WHERE name = \'user:read\'', undef);
 
   $dbh->do('REPLACE INTO oauth2_client_scope (client_id, scope_id) VALUES (?, ?)',
     undef, $client_data->{id}, $scope_id);
diff --git a/scripts/import_email.pl b/scripts/import_email.pl
new file mode 100644 (file)
index 0000000..8ce592e
--- /dev/null
@@ -0,0 +1,40 @@
+#!/bin/env 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.
+#
+# Import email messages from a mbox file and place in jobqueue
+
+use 5.10.1;
+use strict;
+use warnings;
+use lib qw(/app /app/local/lib/perl5);
+
+use Bugzilla;
+
+my $filename = shift;
+die "Need mbox filename\n" if !$filename;
+
+print "Processing $filename\n";
+
+open my $fh, '<:encoding(UTF-8)', $filename || die "Could not open mbox file: $!\n";
+
+my ($msg, $count);
+while (my $line = <$fh>) {
+  if ($line =~ /^From - /) {
+    if ($msg) {
+      Bugzilla->job_queue->insert('send_mail', {msg => $msg});
+      $count++;
+    }
+    $msg = undef;
+    next;
+  }
+  $msg .= $line;
+}
+
+close $fh || die "Could not close mbox file: $!\n";
+
+print "Imported $count emails\n";
index 59fa9f440ece0e2aec8ae636e87c931daf758f50..c04a58a12855cae2ce4cb88da5ee5f54c533fbdd 100644 (file)
@@ -973,23 +973,19 @@ input[type="radio"]:checked {
    * Fixed global header
    */
 
-  html,
-  body {
-    overflow-y: hidden; /* Disable bounce effect (Safari) */
-    height: 100%;
+  #wrapper {
+    width: 100%;
   }
 
-  #wrapper {
-    overflow: hidden;
+  #header {
+    height: 48px;
+    position: fixed;
     width: 100%;
-    height: 100%;
+    z-index: 5;
   }
 
   #bugzilla-body {
-    overflow-x: auto;
-    overflow-y: scroll;
-    -webkit-overflow-scrolling: touch; /* Enable momentum scrolling on iOS */
-    will-change: transform; /* Enable smooth scrolling (Safari) */
+    margin-top: 48px;
   }
 }
 
index 5ebdeacdb50749bd41d794c871ef071feb60edc2..7a764e703aafbf1ffdc22866f4bc03cd5ff0d730 100644 (file)
@@ -260,55 +260,15 @@ $VAR1 = [
             'Perl::Critic::Policy::InputOutput::ProhibitBacktickOperators' => 1,
             'Perl::Critic::Policy::Variables::ProhibitUnusedVariables' => 6,
             'Perl::Critic::Policy::BuiltinFunctions::RequireGlobFunction' => 0,
-            'Perl::Critic::Policy::Freenode::POSIXImports' => 0,
-            'Perl::Critic::Policy::Modules::RequireBarewordIncludes' => 2,
-            'Perl::Critic::Policy::Modules::ProhibitConditionalUseStatements' => 0,
-            'Perl::Critic::Policy::BuiltinFunctions::ProhibitStringySplit' => 28,
-            'Perl::Critic::Policy::Variables::RequireLexicalLoopIterators' => 6,
-            'Perl::Critic::Policy::Subroutines::ProhibitSubroutinePrototypes' => 8,
-            'Perl::Critic::Policy::ControlStructures::ProhibitLabelsWithSpecialBlockNames' => 0,
-            'Perl::Critic::Policy::InputOutput::ProhibitTwoArgOpen' => 0,
-            'Perl::Critic::Policy::ClassHierarchies::ProhibitExplicitISA' => 7,
-            'Perl::Critic::Policy::TestingAndDebugging::ProhibitNoWarnings' => 9,
-            'Perl::Critic::Policy::CodeLayout::RequireTrailingCommas' => 28,
-            'Perl::Critic::Policy::Freenode::OverloadOptions' => 1,
-            'Perl::Critic::Policy::Documentation::PodSpelling' => 81,
-            'Perl::Critic::Policy::Variables::ProhibitReusedNames' => 30,
-            'Perl::Critic::Policy::Freenode::Prototypes' => 5,
-            'Perl::Critic::Policy::Miscellanea::ProhibitUselessNoCritic' => 0,
-            'Perl::Critic::Policy::BuiltinFunctions::ProhibitVoidMap' => 4,
-            'Perl::Critic::Policy::ControlStructures::ProhibitMutatingListFunctions' => 1,
-            'Perl::Critic::Policy::ValuesAndExpressions::ProhibitLongChainsOfMethodCalls' => 1,
-            'Perl::Critic::Policy::Variables::ProtectPrivateVars' => 0
-          },
-          {
-            'Perl::Critic::Policy::ControlStructures::ProhibitUntilBlocks' => 1,
-            'Perl::Critic::Policy::Miscellanea::ProhibitTies' => 0,
-            'Perl::Critic::Policy::ControlStructures::ProhibitYadaOperator' => 0,
-            'Perl::Critic::Policy::Subroutines::ProhibitReturnSort' => 2,
-            'Perl::Critic::Policy::BuiltinFunctions::RequireBlockMap' => 21,
-            'Perl::Critic::Policy::Freenode::LoopOnHash' => 0,
-            'Perl::Critic::Policy::Freenode::IndirectObjectNotation' => 0,
-            'Perl::Critic::Policy::BuiltinFunctions::ProhibitUniversalIsa' => 0,
-            'Perl::Critic::Policy::Miscellanea::ProhibitUnrestrictedNoCritic' => 0,
-            'Perl::Critic::Policy::Modules::ProhibitMultiplePackages' => 7,
-            'Perl::Critic::Policy::RegularExpressions::ProhibitEscapedMetacharacters' => 268,
-            'Perl::Critic::Policy::ValuesAndExpressions::ProhibitEscapedCharacters' => 2,
-            'Perl::Critic::Policy::Freenode::Threads' => 0,
-            'Perl::Critic::Policy::ValuesAndExpressions::ProhibitCommaSeparatedStatements' => 1,
-            'Perl::Critic::Policy::Freenode::BarewordFilehandles' => 10,
-            'Perl::Critic::Policy::ValuesAndExpressions::ProhibitSpecialLiteralHeredocTerminator' => 0,
-            'Perl::Critic::Policy::BuiltinFunctions::ProhibitStringyEval' => 10,
-            'Perl::Critic::Policy::InputOutput::RequireCheckedOpen' => 4,
-            'Perl::Critic::Policy::ControlStructures::ProhibitDeepNests' => 6,
-            'Perl::Critic::Policy::InputOutput::ProhibitJoinedReadline' => 0,
-            'Perl::Critic::Policy::Freenode::WhileDiamondDefaultAssignment' => 6,
-            'Perl::Critic::Policy::RegularExpressions::ProhibitCaptureWithoutTest' => 29,
-            'Perl::Critic::Policy::TestingAndDebugging::ProhibitNoStrict' => 2,
-            'Perl::Critic::Policy::ControlStructures::ProhibitCStyleForLoops' => 9,
-            'Perl::Critic::Policy::ValuesAndExpressions::RequireNumberSeparators' => 34,
-            'Perl::Critic::Policy::Documentation::RequirePodAtEnd' => 17,
-            'Perl::Critic::Policy::InputOutput::RequireCheckedClose' => 23,
+            'Perl::Critic::Policy::TestingAndDebugging::RequireTestLabels' => 0,
+            'Perl::Critic::Policy::ClassHierarchies::ProhibitExplicitISA' => 0,
+            'Perl::Critic::Policy::Modules::ProhibitExcessMainComplexity' => 8,
+            'Perl::Critic::Policy::ValuesAndExpressions::ProhibitEscapedCharacters' => 1,
+            'Perl::Critic::Policy::Freenode::WarningsSwitch' => 19,
+            'Perl::Critic::Policy::RegularExpressions::ProhibitCaptureWithoutTest' => 0,
+            'Perl::Critic::Policy::Variables::ProhibitPunctuationVars' => 14,
+            'Perl::Critic::Policy::RegularExpressions::ProhibitFixedStringMatches' => 1,
+            'Perl::Critic::Policy::ClassHierarchies::ProhibitOneArgBless' => 0,
             'Perl::Critic::Policy::BuiltinFunctions::RequireSimpleSortBlock' => 0,
             'Perl::Critic::Policy::BuiltinFunctions::ProhibitSleepViaSelect' => 0,
             'Perl::Critic::Policy::Variables::ProhibitPunctuationVars' => 41,
@@ -416,10 +376,7 @@ $VAR1 = [
             'Perl::Critic::Policy::Freenode::Each' => 17,
             'Perl::Critic::Policy::Freenode::ConditionalImplicitReturn' => 5,
             'Perl::Critic::Policy::ValuesAndExpressions::ProhibitQuotesAsQuotelikeOperatorDelimiters' => 0,
-            'Perl::Critic::Policy::Freenode::EmptyReturn' => 53,
-            'Perl::Critic::Policy::Modules::RequireFilenameMatchesPackage' => 0,
-            'Perl::Critic::Policy::RegularExpressions::RequireBracesForMultiline' => 10,
-            'Perl::Critic::Policy::Subroutines::ProhibitUnusedPrivateSubroutines' => 56,
-            'Perl::Critic::Policy::InputOutput::ProhibitReadlineInForLoop' => 3
+            'Perl::Critic::Policy::Variables::RequireNegativeIndices' => 0,
+            'Perl::Critic::Policy::InputOutput::RequireBriefOpen' => 5,
           }
         ];
index fe58eac07e5d26a773fe14f3692c572400111178..79e9458f416af6a8ebeb361cf2b3ca8fb2a29260 100644 (file)
@@ -26,8 +26,7 @@ use Bugzilla::Test::MockLocalconfig (urlbase => 'http://bmo.test');
 use Bugzilla::Test::MockDB;
 
 # This redirects reads and writes from the config file (data/params)
-use Bugzilla::Test::MockParams (phabricator_enabled => 1,
-  announcehtml => 'Mojo::Test is awesome',);
+use Bugzilla::Test::MockParams (announcehtml => 'Mojo::Test is awesome');
 
 # Util provides a few functions more making mock data in the DB.
 use Bugzilla::Test::Util qw(create_user issue_api_key);
index 8113162781e62721fcf07bac43c078408091ea72..d28b92671c2f1b0afba0111d93d8fe8e6cd3d28d 100644 (file)
@@ -77,7 +77,7 @@ $t->post_ok(
     scope                  => 'user:read',
     redirect_uri           => '/oauth/redirect'
   }
-)->status_is(200)->text_is('title' => 'Confirm OAuth2 Scopes');
+)->status_is(200)->text_is('title' => 'Request for access to your account');
 
 # Get the csrf token to allow submitting the scope confirmation form
 my $csrf_token = $t->tx->res->dom->at('input[name=token]')->val;
index 0005ffc2c21c576f31789734992c903ce0b6fad1..4bdbf11466e33a81385a8328173b4fa4f35e89df 100644 (file)
@@ -7,7 +7,7 @@
   #%]
 
 [% PROCESS global/header.html.tmpl
-   title = "Confirm OAuth2 Scopes" %]
+   title = "Request for access to your account" %]
 
 <h1>[% title FILTER html %] </h1>
 <p>
 </p>
 
 <p>
-Scopes:
-<ul>
-  [% FOREACH scope = scopes %]
-    <li>
-      [% scope FILTER html %]
-    </li>
-  [% END %]
-</ul>
+  <ul>
+    [% FOREACH scope = scopes %]
+      <li>
+        [% scope.description FILTER html %]
+      </li>
+    [% END %]
+  </ul>
 </p>
 
 <p>Do you want this website to have the above access to your [% terms.Bugzilla %]
@@ -33,7 +32,7 @@ Scopes:
   <form action="/oauth/authorize" method="get">
     <input type="hidden" name="oauth_confirm_[% client.client_id FILTER html %]" value="1">
     <input type="hidden" name="token" value="[% token FILTER html %]">
-    <input type="submit" name="submit" value="Accept">
+    <input type="submit" name="submit" value="Allow">
     [% FOREACH field = c.req.params.names %]
       <input type="hidden" name="[% field FILTER html %]"
              value="[% c.param(field) FILTER html_linebreak %]">
index 762903e2429978b8c7867fc00287edf7726aae1f..6b97b08cfc4ea67ff09b0c8d1846d1ebdcbf8fda 100644 (file)
         addresses to confirm the change of email address.
       </p>
     [% END %]
+
+    [% IF webhooks_saved %]
+      Please allow up to 60 seconds for the change to be active.
+    [% END %]
   </div>
 [% END %]
 
index 4190a09d92d0313f1cc6ea5e69597b98374f50c1..147827a046d7025e9095a6a436deef6701e466b5 100644 (file)
@@ -29,7 +29,7 @@
                      name="scopes" type="checkbox" value="[% scope.id FILTER html %]">
             </td>
             <td>
-              [% scope.description FILTER html %]
+              [% scope.description FILTER html +%] ([%+ scope.name FILTER html %])
             </td>
           </tr>
         [% END %]
index 3d1f5570f61947b295bae3b7c2d00b2be90f7be5..899d63882fd26017410784e22f60af1fc006eb39 100644 (file)
@@ -36,7 +36,7 @@
                  [% ' checked="checked"' IF client.scopes.contains(scope.id) %]>
             </td>
             <td>
-              [% scope.description FILTER html %]
+              [% scope.description FILTER html +%] ([%+ scope.name FILTER html %])
             </td>
           </tr>
         [% END %]
index e5ef5bc106b5aca248a39109793fff4413084b38..4178c2832ba396374297fe0410630500b02e3b5e 100644 (file)
@@ -29,6 +29,7 @@
       <th>Component</th>
       <th>Default Assignee</th>
       <th>Default QA Contact</th>
+      <th>Triage Owner</th>
       <th>Default CC</th>
     </tr>
     [% FOREACH component = item.components %]
@@ -44,7 +45,7 @@
             </a>
           [% END %]
         </td>
-        [% FOREACH responsibility = ['default_assignee', 'default_qa_contact'] %]
+        [% FOREACH responsibility = ['default_assignee', 'default_qa_contact', 'triage_owner'] %]
           <td class="center">
             [% component.$responsibility.id == otheruser.id ? "X" : "&nbsp;" %]
           </td>
index ca634ddd4c1cce2b152d56ee9ebac6ddcfae7c7d..225e794ad8778b6e804a60fd949c6da1d133c02e 100644 (file)
           [% Hook.process('view') %]
           [% UNLESS custom_attachment_viewer %]
             <div>
-              <input type="hidden" name="markdown_off" value="1">
+              <input type="hidden" name="markdown_off" value="0">
               [% INCLUDE global/textarea.html.tmpl
                 id      = 'editFrame'
                 name    = 'comment'
old mode 100644 (file)
new mode 100755 (executable)
index 6ded063..73894b3
@@ -66,7 +66,7 @@ function toggle_display(link) {
 
   [% FOREACH attachment = attachments %]
     [% count = count + 1 %]
-    [% IF !attachment.isprivate || user.is_insider || attachment.attacher.id == user.id %]
+    [% IF !attachment.isprivate || user.is_insider || attachment.attacher.id == user.id || (attachment.is_bounty_attachment && user.id == bug.reporter.id) %]
       [% IF attachment.isobsolete %]
         [% obsolete_attachments = obsolete_attachments + 1 %]
       [% END %]
index 8815dec471a8c293db35978e289c3026b18838e1..0bd6abbc6fe9cedfac242e2f69e1335eab2917e9 100644 (file)
@@ -736,7 +736,9 @@ TUI_hide_default('expert_fields');
 [% any_flags_requesteeble = 0 %]
 [% FOREACH flag_type = product.flag_types.bug %]
   [% display_flags = 1 %]
-  [% 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 %]
   [% LAST IF display_flags && any_flags_requesteeable %]
 [% END %]
 
index 6aef1fb439310d1120df5b9e28051e233e12a84a..bec1d067f00be7aa5fee46bb32e5478c042d2fdd 100644 (file)
         <li>A b[% %]ug in a MantisBT installation.</li>
         <li>A b[% %]ug on sourceforge.net.</li>
         <li>An issue/pull request on github.com.</li>
+        <li>An issue/merge request on a GitLab system.</li>
         <li>A question on support.mozilla.org.</li>
         <li>An Aha feature on aha.io.</li>
         <li>An issue on webcompat.com.</li>