# 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::API::1_0::Server;

use 5.14.0;
use strict;
use warnings;

use Bugzilla::API::1_0::Constants qw(API_AUTH_HEADERS);
use Bugzilla::API::1_0::Util qw(taint_data fix_credentials api_include_exclude);

use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Hook;
use Bugzilla::Util qw(datetime_from trick_taint);

use File::Basename qw(basename);
use File::Glob qw(:glob);
use List::MoreUtils qw(none uniq);
use MIME::Base64 qw(decode_base64 encode_base64);
use Moo;
use Scalar::Util qw(blessed);

extends 'Bugzilla::API::Server';

############
# Start up #
############

has api_version   => (is => 'ro', default => '1_0',  init_arg => undef);
has api_namespace => (is => 'ro', default => 'core', init_arg => undef);

sub _build_content_type {
    # Determine how the data should be represented. We do this early so
    # errors will also be returned with the proper content type.
    # If no accept header was sent or the content types specified were not
    # matched, we default to the first type in the whitelist.
    return $_[0]->_best_content_type(
        @{ $_[0]->constants->{REST_CONTENT_TYPE_WHITELIST} });
}

##################
# Public Methods #
##################

sub handle {
    my ($self)  = @_;

   # Using current path information, decide which class/method to
    # use to serve the request. Throw error if no resource was found
    # unless we were looking for OPTIONS
    if (!$self->_find_resource) {
        if ($self->request->method eq 'OPTIONS'
            && $self->api_options)
        {
            my $response = $self->response_header($self->constants->{STATUS_OK}, "");
            my $options_string = join(', ', @{ $self->api_options });
            $response->header('Allow' => $options_string,
                              'Access-Control-Allow-Methods' => $options_string);
            return $self->print_response($response);
        }

        ThrowUserError("rest_invalid_resource",
                       { path   => $self->cgi->path_info,
                         method => $self->request->method });
    }

    my $params = $self->_retrieve_json_params;
    $self->_params_check($params);

    fix_credentials($params);

    # Fix includes/excludes for each call
    api_include_exclude($params);

    # Set callback name if exists
    $self->callback($params->{'callback'}) if $params->{'callback'};

    Bugzilla->input_params($params);

    # Let's try to authenticate before executing
    $self->handle_login;

    # Execute the handler
    my $result = $self->_handle;

    # The result needs to be a valid JSON data structure
    # and not a undefined or scalar value.
    if (!ref $result
        || blessed($result)
        || (ref $result ne 'HASH' && ref $result ne 'ARRAY'))
    {
        $result = { result => $result };
    }

    $self->response($result);
}

sub response {
    my ($self, $result) = @_;

    # Error data needs to be formatted differently
    my $status_code;
    if (my $error = $self->return_error) {
        $status_code = delete $error->{status_code};
        $error->{documentation} = REST_DOC;
        $result = $error;
    }
    else {
        $status_code = $self->success_code;
    }

    Bugzilla::Hook::process('webservice_rest_result',
        { api => $self, result => \$result });

    # ETag support
    my $etag = $self->etag;
    $self->etag($result) if !$etag;

    # If accessing through web browser, then display in readable format
    my $content;
    if ($self->content_type eq 'text/html') {
        $result = $self->json->pretty->canonical->allow_nonref->encode($result);
        my $template = Bugzilla->template;
        $template->process("rest.html.tmpl", { result => $result }, \$content)
            || ThrowTemplateError($template->error());
    }
    else {
        $content = $self->json->encode($result);
    }

    if (my $callback = $self->callback) {
        # Prepend the response with /**/ in order to protect
        # against possible encoding attacks (e.g., affecting Flash).
        $content = "/**/$callback($content)";
    }

    my $response = $self->response_header($status_code, $content);

    Bugzilla::Hook::process('webservice_rest_response',
        { api => $self, response => $response });

    $self->print_response($response);
}

sub print_response {
    my ($self, $response) = @_;

    # Access Control
    my @allowed_headers = qw(accept content-type origin x-requested-with);
    foreach my $header (keys %{ API_AUTH_HEADERS() }) {
        # We want to lowercase and replace _ with -
        my $translated_header = $header;
        $translated_header =~ tr/A-Z_/a-z\-/;
        push(@allowed_headers, $translated_header);
    }
    $response->header("Access-Control-Allow-Origin", "*");
    $response->header("Access-Control-Allow-Headers", join(', ', @allowed_headers));

    # Use $cgi->header properly instead of just printing text directly.
    # This fixes various problems, including sending Bugzilla's cookies
    # properly.
    my $headers = $response->headers;
    my @header_args;
    foreach my $name ($headers->header_field_names) {
        my @values = $headers->header($name);
        $name =~ s/-/_/g;
        foreach my $value (@values) {
            push(@header_args, "-$name", $value);
        }
    }

    # ETag support
    my $etag = $self->etag;
    if ($etag && $self->cgi->check_etag($etag)) {
        push(@header_args, "-ETag", $etag);
        print $self->cgi->header(-status => '304 Not Modified', @header_args);
    }
    else {
        push(@header_args, "-ETag", $etag) if $etag;
        print $self->cgi->header(-status => $response->code, @header_args);
        print $response->content;
    }
}

sub handle_login {
    my $self = shift;
    my $controller = $self->controller;
    my $method     = $self->method_name;

    return if ($controller->login_exempt($method)
               and !defined Bugzilla->input_params->{Bugzilla_login});

    Bugzilla->login();

    Bugzilla::Hook::process('webservice_before_call',
                            { rpc => $self, controller => $controller });
}

###################
# Private Methods #
###################

sub _handle {
    my ($self) = shift;
    my $method     = $self->method_name;
    my $controller = $self->controller;
    my $params     = Bugzilla->input_params;
    my $cache      = Bugzilla->request_cache;

    unless ($controller->can($method)) {
        return $self->return_error(302, "No such a method : '$method'.");
    }

    # Let Bugzilla::Error know we are inside an eval() for exceptions
    $cache->{in_eval} = 1;
    my $result = eval { $controller->$method($params) };
    $cache->{in_eval} = 0;

    return $self->return_error if $self->return_error;

    if ($@) {
        return $self->return_error(500, "Procedure error: $@");
    }

    # Set the ETag if not already set in the webservice methods.
    my $etag = $self->etag;
    if (!$etag && ref $result) {
        $self->etag($result);
    }

    return $result;
}

sub _params_check {
    my ($self, $params) = @_;
    my $method     = $self->method_name;
    my $controller = $self->controller;

    taint_data($params);

    # Now, convert dateTime fields on input.
    my @date_fields = @{ $controller->DATE_FIELDS->{$method} || [] };
    foreach my $field (@date_fields) {
        if (defined $params->{$field}) {
            my $value = $params->{$field};
            if (ref $value eq 'ARRAY') {
                $params->{$field} =
                    [ map { $self->datetime_format_inbound($_) } @$value ];
            }
            else {
                $params->{$field} = $self->datetime_format_inbound($value);
            }
        }
    }
    my @base64_fields = @{ $controller->BASE64_FIELDS->{$method} || [] };
    foreach my $field (@base64_fields) {
        if (defined $params->{$field}) {
            $params->{$field} = decode_base64($params->{$field});
        }
    }

    if ($self->request->method eq 'POST'
        || $self->request->method eq 'PUT') {
        # CSRF is possible via XMLHttpRequest when the Content-Type header
        # is not application/json (for example: text/plain or
        # application/x-www-form-urlencoded).
        # application/json is the single official MIME type, per RFC 4627.
        my $content_type = $self->cgi->content_type;
        # The charset can be appended to the content type, so we use a regexp.
        if ($content_type !~ m{^application/json(-rpc)?(;.*)?$}i) {
            ThrowUserError('json_rpc_illegal_content_type',
                            { content_type => $content_type });
        }
    }
    else {
        # When being called using GET, we don't allow calling
        # methods that can change data. This protects us against cross-site
        # request forgeries.
        if (!grep($_ eq $method, $controller->READ_ONLY)) {
            ThrowUserError('json_rpc_post_only',
                           { method => $self->method_name });
        }
    }

    # Only allowed methods to be used from our whitelist
    if (none { $_ eq $method} $controller->PUBLIC_METHODS) {
        ThrowCodeError('unknown_method', { method => $self->method_name });
    }
}

sub _retrieve_json_params {
    my $self = shift;

    # Make a copy of the current input_params rather than edit directly
    my $params = {};
    %{$params} = %{ Bugzilla->input_params };

    # First add any parameters we were able to pull out of the path
    # based on the resource regexp and combine with the normal URL
    # parameters.
    if (my $api_params = $self->api_params) {
        foreach my $param (keys %$api_params) {
            # If the param does not already exist or if the
            # rest param is a single value, add it to the
            # global params.
            if (!exists $params->{$param} || !ref $api_params->{$param}) {
                $params->{$param} = $api_params->{$param};
            }
            # If param is a list then add any extra values to the list
            elsif (ref $api_params->{$param}) {
                my @extra_values = ref $params->{$param}
                                   ? @{ $params->{$param} }
                                   : ($params->{$param});
                $params->{$param}
                    = [ uniq (@{ $api_params->{$param} }, @extra_values) ];
            }
        }
    }

    # Any parameters passed in in the body of a non-GET request will override
    # any parameters pull from the url path. Otherwise non-unique keys are
    # combined.
    if ($self->request->method ne 'GET') {
        my $extra_params = {};
        # We do this manually because CGI.pm doesn't understand JSON strings.
        my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'};
        if ($json) {
            eval { $extra_params = $self->json->decode($json); };
            if ($@) {
                ThrowUserError('json_rpc_invalid_params', { err_msg  => $@ });
            }
        }

        # Allow parameters in the query string if request was non-GET.
        # Note: parameters in query string body override any matching
        # parameters in the request body.
        foreach my $param ($self->cgi->url_param()) {
            $extra_params->{$param} = $self->cgi->url_param($param);
        }

        %{$params} = (%{$params}, %{$extra_params}) if %{$extra_params};
    }

    return $params;
}

sub _find_resource {
    my ($self) = @_;
    my $api_version     = $self->api_version;
    my $api_ext_version = $self->api_ext_version;
    my $api_namespace   = $self->api_namespace;
    my $api_path        = $self->api_path;
    my $request_method  = $self->request->method;
    my $resource_found  = 0;

    my $resource_modules;
    if ($api_ext_version) {
        $resource_modules = File::Spec->catdir(bz_locations()->{extensionsdir},
            $api_namespace, 'API', $api_ext_version, 'Resource', '*.pm');
    }
    else {
        $resource_modules = File::Spec->catdir(bz_locations()->{cgi_path},
            'Bugzilla','API', $api_version, 'Resource', '*.pm');
    }

    # Load in the WebService modules from the appropriate version directory
    # and then call $module->REST_RESOURCES to get the resources array ref.
    foreach my $module_file (bsd_glob($resource_modules)) {
        # Create a controller object
        trick_taint($module_file);
        my $module_basename = basename($module_file, '.pm');
        eval { require "$module_file"; } || die $@;
        my $module_class = "Bugzilla::API::${api_version}::Resource::${module_basename}";
        my $controller = $module_class->new;
        next if !$controller || !$controller->can('REST_RESOURCES');

        # The resource data for each module needs to be an array ref with an
        # even number of elements to work correctly.
        my $this_resources = $controller->REST_RESOURCES;
        next if (ref $this_resources ne 'ARRAY' || scalar @$this_resources % 2 != 0);

        while (my ($regex, $options_data) = splice(@$this_resources, 0, 2)) {
            next if ref $options_data ne 'HASH';

            if (my @matches = ($self->api_path =~ $regex)) {
                # If a specific path is accompanied by a OPTIONS request
                # method, the user is asking for a list of possible request
                # methods for a specific path.
                $self->api_options([ keys %$options_data ]);

                if ($options_data->{$request_method}) {
                    my $resource_data = $options_data->{$request_method};

                    # The method key/value can be a simple scalar method name
                    # or a anonymous subroutine so we execute it here.
                    my $method = ref $resource_data->{method} eq 'CODE'
                                 ? $resource_data->{method}->($self)
                                 : $resource_data->{method};
                    $self->method_name($method);

                    # Pull out any parameters parsed from the URL path
                    # and store them for use by the method.
                    if ($resource_data->{params}) {
                        $self->api_params($resource_data->{params}->(@matches));
                    }

                    # If a special success code is needed for this particular
                    # method, then store it for later when generating response.
                    if ($resource_data->{success_code}) {
                        $self->success_code($resource_data->{success_code});
                    }

                    # Stash away for later
                    $self->controller($controller);

                    # No need to look further
                    $resource_found = 1;
                    last;
                }
            }
        }
        last if $resource_found;
    }

    return $resource_found;
}

1;

__END__

=head1 NAME

Bugzilla::API::1_0::Server - The API 1.0 Interface to Bugzilla

=head1 DESCRIPTION

This documentation describes version 1.0 of the Bugzilla API. This
module inherits from L<Bugzilla::API::Server> and overrides specific
methods to make this version distinct from other versions of the API.
New versions of the API may make breaking changes by implementing
these methods in a different way.

=head1 SEE ALSO

L<Bugzilla::API::Server>

=head1 B<Methods in need of POD>

=over

=item api_namespace

=item api_version

=item handle

=item response

=item print_response

=item handle_login

=back

