"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": []
+ },
]
}
use Bugzilla::Logging;
-our $VERSION = '20200603.1';
+our $VERSION = '20200805.1';
use Bugzilla::Auth;
use Bugzilla::Auth::Persist::Cookie;
if (!$args->{user_id}) {
return (user_id => Bugzilla->user->id);
}
+ return;
};
$app->helper(
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;
# 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});
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
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 {
$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;
'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;
}
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'},
],
},
# 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 {
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";
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) {
- selenium
bmo.db:
- image: mozillabteam/bmo-mysql:5.7
+ image: mysql:5.7
tmpfs:
- /tmp
logging:
shm_size: '512m'
ports:
- "5900:5900"
+
+volumes:
+ bmo-mysql-db:
}
sub _attachment_is_bounty_attachment {
+ # Keep this in sync with Bugzilla/Attachment.pm
my ($attachment) = @_;
return 0 unless $attachment->filename eq 'bugbounty.data';
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) = @_;
# 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 {
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";
<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>
[% 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>
[% 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>
<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;
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;
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'};
{
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},
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 #
#########
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};
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;
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 {
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
# 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+)$/)
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'})
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;
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+)$/)
[% 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 -%]
[%
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
# 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');
%]
[%
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 = {
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',
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>
<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>
<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">
(<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 %]
[% 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
+---
---
{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');
+ }
+ }
}
#
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 );
admin_config
admin_queues
admin_log
+ admin_webhooks
);
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();
$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;
return @result;
}
+sub delete {
+ my ($self) = @_;
+ Bugzilla->dbh->do("DELETE FROM push_backlog WHERE connector = ?",undef, $self->{connector});
+}
+
#
# backoff
#
use Bugzilla::Logging;
use Bugzilla::Constants;
+use Bugzilla::Extension::Webhooks::Webhook;
use Bugzilla::Extension::Push::Option;
use Crypt::CBC;
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);
}
}
--- /dev/null
+# 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;
use Bugzilla::Logging;
use Bugzilla::Extension::Push::Util;
+use Bugzilla::Extension::Webhooks::Webhook;
use Bugzilla::Constants;
use File::Basename;
use Try::Tiny;
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);
}
}
[% 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 %]
%]
[% FOREACH connector = connectors.list %]
+ [% NEXT IF connector.webhook_id %]
[% PROCESS options
name = connector.name
config = connector.config
<td width="100%"> </td>
</tr>
+[% IF Bugzilla.params.${'webhooks_enabled'} %]
+ <tr>
+ <td> </td>
+ <td colspan="2">
+ <a href="page.cgi?id=webhooks_config.html">See webhooks configurations</a>
+ </td>
+ </tr>
+[% END %]
+
</table>
</form>
[% END %]
>
[% IF option.name != 'enabled' && !option.required %]
- <option value="""
+ <option value=""
[% ' selected' IF config.${option.name} == "" %]></option>
[% END %]
[% FOREACH value = option.values %]
[% 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'
--- /dev/null
+[%# 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 %]
[%+ cgi.param('languages') %]
[% END %]
-Mozillians.org Account URL:
+people.mozilla.org Profile URL:
[%+ cgi.param('mozillian') %]
References:
</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>
}
}).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();
}).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;
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 {
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,
);
}
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});
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
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 => {}});
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}});
}
[% 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
--- /dev/null
+# 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;
--- /dev/null
+# 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;
--- /dev/null
+# 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;
--- /dev/null
+# 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;
--- /dev/null
+[%# 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> </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 %]
--- /dev/null
+[%# 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."
+ }
+%]
--- /dev/null
+[%# 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
+ }]) %]
--- /dev/null
+[%# 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 %]
--- /dev/null
+[%# 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 %]
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)
if (mode == 'edit') {
showElementById('editFrame');
showElementById('undoEditButton');
+ document.querySelector('input[name="markdown_off"]').value = 1;
} else if (mode == 'raw') {
showElementById('viewFrame');
if (patchviewerinstalled)
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) {
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;
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';
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}";
}
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',
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};
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};
}
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})) {
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&bug_status=NEW'
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);
--- /dev/null
+#!/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";
* 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;
}
}
'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,
'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,
}
];
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);
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;
#%]
[% 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 %]
<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 %]">
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 %]
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 %]
[% ' 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 %]
<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 %]
</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" : " " %]
</td>
[% 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'
[% 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 %]
[% 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 %]
<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>