# 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::BugUrl;

use 5.14.0;
use strict;
use warnings;

use parent qw(Bugzilla::Object);

use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Constants;
use Bugzilla::Hook;

use URI;
use URI::QueryParam;

###############################
####    Initialization     ####
###############################

use constant DB_TABLE   => 'bug_see_also';
use constant NAME_FIELD => 'value';
use constant LIST_ORDER => 'id';
# See Also is tracked in bugs_activity.
use constant AUDIT_CREATES => 0;
use constant AUDIT_UPDATES => 0;
use constant AUDIT_REMOVES => 0;

use constant DB_COLUMNS => qw(
    id
    bug_id
    value
    class
);

# 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',
    class  => \&_check_class,
};

# 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
    Bugzilla::BugUrl::JIRA
    Bugzilla::BugUrl::Trac
    Bugzilla::BugUrl::MantisBT
    Bugzilla::BugUrl::SourceForge
    Bugzilla::BugUrl::GitHub
);

###############################
####      Accessors      ######
###############################

sub class  { return $_[0]->{class}  }
sub bug_id { return $_[0]->{bug_id} }

###############################
####        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(@_);
}

sub _do_list_select {
    my $class = shift;
    my $objects = $class->SUPER::_do_list_select(@_);

    foreach my $object (@$objects) {
        eval "use " . $object->class;
        # If the class cannot be loaded, then we build a generic object.
        bless $object, ($@ ? 'Bugzilla::BugUrl' : $object->class);
    }

    return $objects
}

# 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 @sub_classes = $class->SUB_CLASSES;
    Bugzilla::Hook::process("bug_url_sub_classes",
        { sub_classes => \@sub_classes });

    my $uri = URI->new($value);
    foreach my $subclass (@sub_classes) {
        eval "use $subclass";
        die $@ if $@;
        return wantarray ? ($subclass, $uri) : $subclass
            if $subclass->should_handle($uri);
    }

    ThrowUserError('bug_url_invalid', { url => $value });
}

sub _check_class {
    my ($class, $subclass) = @_;
    eval "use $subclass"; die $@ if $@;
    return $subclass;
}

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;

=head1 B<Methods in need of POD>

=over

=item should_handle

=item class_for

=item class

=item bug_id

=back