From ed3e015a1c21061ed9f30cfc3fe3c3e83c0d2fb1 Mon Sep 17 00:00:00 2001
From: Tiago Mello <timello@gmail.com>
Date: Mon, 20 Dec 2010 20:49:10 -0200
Subject: [PATCH] Bug 593539: Refactor See Also to use separate modules for
 each type of URL r/a=mkanat

---
 Bugzilla/Bug.pm                   | 192 +++++-------------------------
 Bugzilla/BugUrl.pm                | 173 +++++++++++++++++++++++++++
 Bugzilla/BugUrl/Bugzilla.pm       |  61 ++++++++++
 Bugzilla/BugUrl/Bugzilla/Local.pm | 105 ++++++++++++++++
 Bugzilla/BugUrl/Debian.pm         |  62 ++++++++++
 Bugzilla/BugUrl/Google.pm         |  64 ++++++++++
 Bugzilla/BugUrl/Launchpad.pm      |  58 +++++++++
 Bugzilla/Install/DB.pm            |   3 +
 8 files changed, 558 insertions(+), 160 deletions(-)
 create mode 100644 Bugzilla/BugUrl.pm
 create mode 100644 Bugzilla/BugUrl/Bugzilla.pm
 create mode 100644 Bugzilla/BugUrl/Bugzilla/Local.pm
 create mode 100644 Bugzilla/BugUrl/Debian.pm
 create mode 100644 Bugzilla/BugUrl/Google.pm
 create mode 100644 Bugzilla/BugUrl/Launchpad.pm

diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index 9d41d5c7e..fd0111578 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -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;
     }
 }
 
diff --git a/Bugzilla/BugUrl.pm b/Bugzilla/BugUrl.pm
new file mode 100644
index 000000000..521ee8193
--- /dev/null
+++ b/Bugzilla/BugUrl.pm
@@ -0,0 +1,173 @@
+# -*- 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;
diff --git a/Bugzilla/BugUrl/Bugzilla.pm b/Bugzilla/BugUrl/Bugzilla.pm
new file mode 100644
index 000000000..3957afc9d
--- /dev/null
+++ b/Bugzilla/BugUrl/Bugzilla.pm
@@ -0,0 +1,61 @@
+# -*- 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;
diff --git a/Bugzilla/BugUrl/Bugzilla/Local.pm b/Bugzilla/BugUrl/Bugzilla/Local.pm
new file mode 100644
index 000000000..22812d085
--- /dev/null
+++ b/Bugzilla/BugUrl/Bugzilla/Local.pm
@@ -0,0 +1,105 @@
+# -*- 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;
diff --git a/Bugzilla/BugUrl/Debian.pm b/Bugzilla/BugUrl/Debian.pm
new file mode 100644
index 000000000..90d61a69a
--- /dev/null
+++ b/Bugzilla/BugUrl/Debian.pm
@@ -0,0 +1,62 @@
+# -*- 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;
diff --git a/Bugzilla/BugUrl/Google.pm b/Bugzilla/BugUrl/Google.pm
new file mode 100644
index 000000000..4f6321e74
--- /dev/null
+++ b/Bugzilla/BugUrl/Google.pm
@@ -0,0 +1,64 @@
+# -*- 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;
diff --git a/Bugzilla/BugUrl/Launchpad.pm b/Bugzilla/BugUrl/Launchpad.pm
new file mode 100644
index 000000000..bb16b3b30
--- /dev/null
+++ b/Bugzilla/BugUrl/Launchpad.pm
@@ -0,0 +1,58 @@
+# -*- 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;
diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm
index 47c8873fa..9cbd66f42 100644
--- a/Bugzilla/Install/DB.pm
+++ b/Bugzilla/Install/DB.pm
@@ -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 #
     ################################################################
-- 
2.24.1