Commit ed3e015a authored by Tiago Mello's avatar Tiago Mello

Bug 593539: Refactor See Also to use separate modules for each type of URL

r/a=mkanat
parent c9c81ee7
......@@ -49,6 +49,7 @@ use Bugzilla::Component;
use Bugzilla::Group;
use Bugzilla::Status;
use Bugzilla::Comment;
use Bugzilla::BugUrl;
use List::MoreUtils qw(firstidx uniq);
use List::Util qw(min max first);
......@@ -930,6 +931,14 @@ sub update {
}
# See Also
foreach my $field_values (@{ $self->{added_see_also} || [] }) {
my $class = delete $field_values->{class};
$class->insert_create_data($field_values);
push @{ $self->see_also }, $field_values->{value};
}
delete $self->{added_see_also};
my ($removed_see, $added_see) =
diff_arrays($old_bug->see_also, $self->see_also);
......@@ -938,19 +947,13 @@ sub update {
. $dbh->sql_in('value', [('?') x @$removed_see]),
undef, $self->id, @$removed_see);
}
foreach my $url (@$added_see) {
$dbh->do('INSERT INTO bug_see_also (bug_id, value) VALUES (?,?)',
undef, $self->id, $url);
}
# If any changes were found, record it in the activity log
if (scalar @$removed_see || scalar @$added_see) {
$changes->{see_also} = [join(', ', @$removed_see),
join(', ', @$added_see)];
}
# Call update for the referenced bugs.
$_->update() foreach @{ $self->{see_also_update} || [] };
# Log bugs_activity items
# XXX Eventually, when bugs_activity is able to track the dupe_id,
# this code should go below the duplicates-table-updating code below.
......@@ -1193,9 +1196,9 @@ sub send_changes {
}
# Sending emails for the referenced bugs.
foreach my $ref_bug (@{ $self->{see_also_update} || [] }) {
foreach my $ref_bug_id (uniq @{ $self->{see_also_changes} || [] }) {
_send_bugmail({ forced => { changer => $user },
id => $ref_bug->id }, $vars);
id => $ref_bug_id }, $vars);
}
}
......@@ -2787,173 +2790,42 @@ sub remove_group {
}
sub add_see_also {
my ($self, $input, $skip_recursion) = @_;
my ($self, $input) = @_;
$input = trim($input);
if (!$input) {
ThrowCodeError('param_required',
{ function => 'add_see_also', param => '$input' });
}
# If a bug id/alias has been taken, then treat it
# as a link to the local Bugzilla.
my $local_bug_uri = correct_urlbase() . "show_bug.cgi?id=";
if ($input =~ m/^\w+$/) {
$input = $local_bug_uri . $input;
}
# We assume that the URL is an HTTP URL if there is no (something)://
# in front.
my $uri = new URI($input);
if (!$uri->scheme) {
# This works better than setting $uri->scheme('http'), because
# that creates URLs like "http:domain.com" and doesn't properly
# differentiate the path from the domain.
$uri = new URI("http://$input");
}
elsif ($uri->scheme ne 'http' && $uri->scheme ne 'https') {
ThrowUserError('bug_url_invalid', { url => $input, reason => 'http' });
}
# This stops the following edge cases from being accepted:
# * show_bug.cgi?id=1
# * /show_bug.cgi?id=1
# * http:///show_bug.cgi?id=1
if (!$uri->authority or $uri->path !~ m{/}) {
ThrowUserError('bug_url_invalid',
{ url => $input, reason => 'path_only' });
}
my $result;
# Launchpad URLs
if ($uri->authority =~ /launchpad.net$/) {
# Launchpad bug URLs can look like various things:
# https://bugs.launchpad.net/ubuntu/+bug/1234
# https://launchpad.net/bugs/1234
# All variations end with either "/bugs/1234" or "/+bug/1234"
if ($uri->path =~ m|bugs?/(\d+)$|) {
# This is the shortest standard URL form for Launchpad bugs,
# and so we reduce all URLs to this.
$result = "https://launchpad.net/bugs/$1";
}
else {
ThrowUserError('bug_url_invalid',
{ url => $input, reason => 'id' });
}
}
# Google Code URLs
elsif ($uri->authority =~ /^code.google.com$/i) {
# Google Code URLs only have one form:
# http(s)://code.google.com/p/PROJECT_NAME/issues/detail?id=1234
my $project_name;
if ($uri->path =~ m|^/p/([^/]+)/issues/detail$|) {
$project_name = $1;
} else {
ThrowUserError('bug_url_invalid',
{ url => $input });
}
my $bug_id = $uri->query_param('id');
detaint_natural($bug_id);
if (!$bug_id) {
ThrowUserError('bug_url_invalid',
{ url => $input, reason => 'id' });
}
# While Google Code URLs can be either HTTP or HTTPS,
# always go with the HTTP scheme, as that's the default.
$result = "http://code.google.com/p/" . $project_name .
"/issues/detail?id=" . $bug_id;
}
# Debian BTS URLs
elsif ($uri->authority =~ /^bugs.debian.org$/i) {
# Debian BTS URLs can look like various things:
# http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234
# http://bugs.debian.org/1234
my $bug_id;
if ($uri->path =~ m|^/(\d+)$|) {
$bug_id = $1;
}
elsif ($uri->path =~ /bugreport\.cgi$/) {
$bug_id = $uri->query_param('bug');
detaint_natural($bug_id);
}
if (!$bug_id) {
ThrowUserError('bug_url_invalid',
{ url => $input, reason => 'id' });
}
# This is the shortest standard URL form for Debian BTS URLs,
# and so we reduce all URLs to this.
$result = "http://bugs.debian.org/" . $bug_id;
}
# Bugzilla URLs
else {
if ($uri->path !~ /show_bug\.cgi$/) {
ThrowUserError('bug_url_invalid',
{ url => $input, reason => 'show_bug' });
}
my $bug_id = $uri->query_param('id');
# We don't currently allow aliases, because we can't check to see
# if somebody's putting both an alias link and a numeric ID link.
# When we start validating the URL by accessing the other Bugzilla,
# we can allow aliases.
detaint_natural($bug_id);
if (!$bug_id) {
ThrowUserError('bug_url_invalid',
{ url => $input, reason => 'id' });
}
my ($class, $uri) = Bugzilla::BugUrl->class_for($input);
# Make sure that "id" is the only query parameter.
$uri->query("id=$bug_id");
# And remove any # part if there is one.
$uri->fragment(undef);
my $uri_canonical = $uri->canonical;
$result = $uri_canonical->as_string;
# If this is a link to a local bug (treating the domain
# case-insensitively and ignoring http(s)://), then also update
# the other bug to point at this one.
my $canonical_local = URI->new($local_bug_uri)->canonical;
if (!$skip_recursion
and $canonical_local->authority eq $uri_canonical->authority
and $canonical_local->path eq $uri_canonical->path)
{
my $ref_bug = Bugzilla::Bug->check($bug_id);
if ($ref_bug->id == $self->id) {
ThrowUserError('see_also_self_reference');
}
my $product = $ref_bug->product_obj;
if (!Bugzilla->user->can_edit_product($product->id)) {
ThrowUserError("product_edit_denied",
{ product => $product->name });
}
my $ref_input = $local_bug_uri . $self->id;
if (!grep($ref_input, @{ $ref_bug->see_also })) {
$ref_bug->add_see_also($ref_input, 'skip recursion');
push @{ $self->{see_also_update} }, $ref_bug;
}
}
my $params = { value => $uri, bug_id => $self };
$class->check_required_create_fields($params);
}
my $field_values = $class->run_create_validators($params);
$uri = $field_values->{value};
$field_values->{value} = $uri->as_string;
$field_values->{class} = $class;
if (length($result) > MAX_BUG_URL_LENGTH) {
ThrowUserError('bug_url_too_long', { url => $result });
# If this is a link to a local bug then save the
# ref bug id for sending changes email.
if ($class->isa('Bugzilla::BugUrl::Bugzilla::Local')) {
my $ref_bug = $field_values->{ref_bug};
my $self_url = $class->local_uri . $self->id;
push @{ $self->{see_also_changes} }, $ref_bug->id
if !grep { $_ eq $self_url } @{ $ref_bug->see_also };
}
# We only add the new URI if it hasn't been added yet. URIs are
# case-sensitive, but most of our DBs are case-insensitive, so we do
# this check case-insensitively.
if (!grep { lc($_) eq lc($result) } @{ $self->see_also }) {
my $value = $uri->as_string;
if (!grep { lc($_) eq lc($value) } @{ $self->see_also }) {
my $privs;
my $can = $self->check_can_change_field('see_also', '', $result, \$privs);
my $can = $self->check_can_change_field('see_also', '', $value, \$privs);
if (!$can) {
ThrowUserError('illegal_change', { field => 'see_also',
newvalue => $result,
newvalue => $value,
privs => $privs });
}
push(@{ $self->see_also }, $result);
push @{ $self->{added_see_also} }, $field_values;
}
}
......
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Tiago Mello
# Portions created by Tiago Mello are Copyright (C) 2010
# Tiago Mello. All Rights Reserved.
#
# Contributor(s): Tiago Mello <timello@linux.vnet.ibm.com>
package Bugzilla::BugUrl;
use strict;
use base qw(Bugzilla::Object);
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Constants;
use URI::QueryParam;
###############################
#### Initialization ####
###############################
use constant DB_TABLE => 'bug_see_also';
use constant NAME_FIELD => 'value';
use constant LIST_ORDER => 'id';
use constant DB_COLUMNS => qw(
id
bug_id
value
);
# This must be strings with the names of the validations,
# instead of coderefs, because subclasses override these
# validators with their own.
use constant VALIDATORS => {
value => '_check_value',
bug_id => '_check_bug_id',
};
# This is the order we go through all of subclasses and
# pick the first one that should handle the url. New
# subclasses should be added at the end of the list.
use constant SUB_CLASSES => qw(
Bugzilla::BugUrl::Bugzilla::Local
Bugzilla::BugUrl::Bugzilla
Bugzilla::BugUrl::Launchpad
Bugzilla::BugUrl::Google
Bugzilla::BugUrl::Debian
);
###############################
#### Methods ####
###############################
sub new {
my $class = shift;
my $param = shift;
if (ref $param) {
my $bug_id = $param->{bug_id};
my $name = $param->{name} || $param->{value};
if (!defined $bug_id) {
ThrowCodeError('bad_arg',
{ argument => 'bug_id',
function => "${class}::new" });
}
if (!defined $name) {
ThrowCodeError('bad_arg',
{ argument => 'name',
function => "${class}::new" });
}
my $condition = 'bug_id = ? AND value = ?';
my @values = ($bug_id, $name);
$param = { condition => $condition, values => \@values };
}
unshift @_, $param;
return $class->SUPER::new(@_);
}
# This is an abstract method. It must be overridden
# in every subclass.
sub should_handle {
my ($class, $input) = @_;
ThrowCodeError('unknown_method',
{ method => "${class}::should_handle" });
}
sub class_for {
my ($class, $value) = @_;
my $uri = URI->new($value);
foreach my $subclass ($class->SUB_CLASSES) {
eval "use $subclass";
die $@ if $@;
return wantarray ? ($subclass, $uri) : $subclass
if $subclass->should_handle($uri);
}
ThrowUserError('bug_url_invalid', { url => $value,
reason => 'show_bug' });
}
sub _check_bug_id {
my ($class, $bug_id) = @_;
my $bug;
if (blessed $bug_id) {
# We got a bug object passed in, use it
$bug = $bug_id;
$bug->check_is_visible;
}
else {
# We got a bug id passed in, check it and get the bug object
$bug = Bugzilla::Bug->check({ id => $bug_id });
}
return $bug->id;
}
sub _check_value {
my ($class, $uri) = @_;
my $value = $uri->as_string;
if (!$value) {
ThrowCodeError('param_required',
{ function => 'add_see_also', param => '$value' });
}
# We assume that the URL is an HTTP URL if there is no (something)://
# in front.
if (!$uri->scheme) {
# This works better than setting $uri->scheme('http'), because
# that creates URLs like "http:domain.com" and doesn't properly
# differentiate the path from the domain.
$uri = new URI("http://$value");
}
elsif ($uri->scheme ne 'http' && $uri->scheme ne 'https') {
ThrowUserError('bug_url_invalid', { url => $value, reason => 'http' });
}
# This stops the following edge cases from being accepted:
# * show_bug.cgi?id=1
# * /show_bug.cgi?id=1
# * http:///show_bug.cgi?id=1
if (!$uri->authority or $uri->path !~ m{/}) {
ThrowUserError('bug_url_invalid',
{ url => $value, reason => 'path_only' });
}
if (length($uri->path) > MAX_BUG_URL_LENGTH) {
ThrowUserError('bug_url_too_long', { url => $uri->path });
}
return $uri;
}
1;
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Tiago Mello
# Portions created by Tiago Mello are Copyright (C) 2010
# Tiago Mello. All Rights Reserved.
#
# Contributor(s): Tiago Mello <timello@linux.vnet.ibm.com>
package Bugzilla::BugUrl::Bugzilla;
use strict;
use base qw(Bugzilla::BugUrl);
use Bugzilla::Error;
use Bugzilla::Util;
###############################
#### Methods ####
###############################
sub should_handle {
my ($class, $uri) = @_;
return ($uri->path =~ /show_bug\.cgi$/) ? 1 : 0;
}
sub _check_value {
my ($class, $uri) = @_;
$uri = $class->SUPER::_check_value($uri);
my $bug_id = $uri->query_param('id');
# We don't currently allow aliases, because we can't check to see
# if somebody's putting both an alias link and a numeric ID link.
# When we start validating the URL by accessing the other Bugzilla,
# we can allow aliases.
detaint_natural($bug_id);
if (!$bug_id) {
my $value = $uri->as_string;
ThrowUserError('bug_url_invalid', { url => $value, reason => 'id' });
}
# Make sure that "id" is the only query parameter.
$uri->query("id=$bug_id");
# And remove any # part if there is one.
$uri->fragment(undef);
return $uri;
}
1;
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Tiago Mello
# Portions created by Tiago Mello are Copyright (C) 2010
# Tiago Mello. All Rights Reserved.
#
# Contributor(s): Tiago Mello <timello@linux.vnet.ibm.com>
package Bugzilla::BugUrl::Bugzilla::Local;
use strict;
use base qw(Bugzilla::BugUrl::Bugzilla);
use Bugzilla::Error;
use Bugzilla::Util;
###############################
#### Initialization ####
###############################
use constant VALIDATOR_DEPENDENCIES => {
value => ['bug_id'],
};
###############################
#### Methods ####
###############################
sub insert_create_data {
my ($class, $field_values) = @_;
my $ref_bug = delete $field_values->{ref_bug};
my $url = $class->local_uri . $field_values->{bug_id};
my $bug_url = $class->SUPER::insert_create_data($field_values);
# Check if the ref bug has already the url and then,
# update the ref bug to point to the current bug.
if (!grep { $_ eq $url } @{ $ref_bug->see_also }) {
$class->SUPER::insert_create_data(
{ value => $url, bug_id => $ref_bug->id } );
}
return $bug_url;
}
sub should_handle {
my ($class, $uri) = @_;
return $uri->as_string =~ m/^\w+$/ ? 1 : 0;
my $canonical_local = URI->new($class->_local_uri)->canonical;
# Treating the domain case-insensitively and ignoring http(s)://
return ($canonical_local->authority eq $uri->canonical->authority
and $canonical_local->path eq $uri->canonical->path) ? 1 : 0;
}
sub _check_value {
my ($class, $uri, undef, $params) = @_;
# At this point we are going to treat any word as a
# bug id/alias to the local Bugzilla.
my $value = $uri->as_string;
if ($value =~ m/^\w+$/) {
$uri = new URI($class->local_uri . $value);
} else {
# It's not a word, then we have to check
# if it's a valid Bugzilla url.
$uri = $class->SUPER::_check_value($uri);
}
my $ref_bug_id = $uri->query_param('id');
my $ref_bug = Bugzilla::Bug->check($ref_bug_id);
my $self_bug_id = $params->{bug_id};
$params->{ref_bug} = $ref_bug;
if ($ref_bug->id == $self_bug_id) {
ThrowUserError('see_also_self_reference');
}
my $product = $ref_bug->product_obj;
if (!Bugzilla->user->can_edit_product($product->id)) {
ThrowUserError("product_edit_denied",
{ product => $product->name });
}
return $uri;
}
sub local_uri {
return correct_urlbase() . "show_bug.cgi?id=";
}
1;
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Tiago Mello
# Portions created by Tiago Mello are Copyright (C) 2010
# Tiago Mello. All Rights Reserved.
#
# Contributor(s): Tiago Mello <timello@linux.vnet.ibm.com>
package Bugzilla::BugUrl::Debian;
use strict;
use base qw(Bugzilla::BugUrl);
use Bugzilla::Error;
use Bugzilla::Util;
###############################
#### Methods ####
###############################
sub should_handle {
my ($class, $uri) = @_;
return ($uri->authority =~ /^bugs.debian.org$/i) ? 1 : 0;
}
sub _check_value {
my $class = shift;
my $uri = $class->SUPER::_check_value(@_);
# Debian BTS URLs can look like various things:
# http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234
# http://bugs.debian.org/1234
my $bug_id;
if ($uri->path =~ m|^/(\d+)$|) {
$bug_id = $1;
}
elsif ($uri->path =~ /bugreport\.cgi$/) {
$bug_id = $uri->query_param('bug');
detaint_natural($bug_id);
}
if (!$bug_id) {
ThrowUserError('bug_url_invalid',
{ url => $uri->path, reason => 'id' });
}
# This is the shortest standard URL form for Debian BTS URLs,
# and so we reduce all URLs to this.
return new URI("http://bugs.debian.org/" . $bug_id);
}
1;
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Tiago Mello
# Portions created by Tiago Mello are Copyright (C) 2010
# Tiago Mello. All Rights Reserved.
#
# Contributor(s): Tiago Mello <timello@linux.vnet.ibm.com>
package Bugzilla::BugUrl::Google;
use strict;
use base qw(Bugzilla::BugUrl);
use Bugzilla::Error;
use Bugzilla::Util;
###############################
#### Methods ####
###############################
sub should_handle {
my ($class, $uri) = @_;
return ($uri->authority =~ /^code.google.com$/i) ? 1 : 0;
}
sub _check_value {
my ($class, $uri) = @_;
$uri = $class->SUPER::_check_value($uri);
my $value = $uri->as_string;
# Google Code URLs only have one form:
# http(s)://code.google.com/p/PROJECT_NAME/issues/detail?id=1234
my $project_name;
if ($uri->path =~ m|^/p/([^/]+)/issues/detail$|) {
$project_name = $1;
} else {
ThrowUserError('bug_url_invalid', { url => $value });
}
my $bug_id = $uri->query_param('id');
detaint_natural($bug_id);
if (!$bug_id) {
ThrowUserError('bug_url_invalid', { url => $value, reason => 'id' });
}
# While Google Code URLs can be either HTTP or HTTPS,
# always go with the HTTP scheme, as that's the default.
$value = "http://code.google.com/p/" . $project_name .
"/issues/detail?id=" . $bug_id;
return new URI($value);
}
1;
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Tiago Mello
# Portions created by Tiago Mello are Copyright (C) 2010
# Tiago Mello. All Rights Reserved.
#
# Contributor(s): Tiago Mello <timello@linux.vnet.ibm.com>
package Bugzilla::BugUrl::Launchpad;
use strict;
use base qw(Bugzilla::BugUrl);
use Bugzilla::Error;
###############################
#### Methods ####
###############################
sub should_handle {
my ($class, $uri) = @_;
return ($uri->authority =~ /launchpad.net$/) ? 1 : 0;
}
sub _check_value {
my ($class, $uri) = @_;
$uri = $class->SUPER::_check_value($uri);
my $value = $uri->as_string;
# Launchpad bug URLs can look like various things:
# https://bugs.launchpad.net/ubuntu/+bug/1234
# https://launchpad.net/bugs/1234
# All variations end with either "/bugs/1234" or "/+bug/1234"
if ($uri->path =~ m|bugs?/(\d+)$|) {
# This is the shortest standard URL form for Launchpad bugs,
# and so we reduce all URLs to this.
$value = "https://launchpad.net/bugs/$1";
}
else {
ThrowUserError('bug_url_invalid', { url => $value, reason => 'id' });
}
return new URI($value);
}
1;
......@@ -645,6 +645,9 @@ sub update_table_definitions {
# 2010-10-09 LpSolit@gmail.com - Bug 451735
_fix_series_indexes();
$dbh->bz_add_column('bug_see_also', 'id',
{TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
################################################################
# New --TABLE-- changes should go *** A B O V E *** this point #
################################################################
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment