# 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::BzAPI; use 5.10.1; use strict; use warnings; use base qw(Bugzilla::Extension); use Bugzilla::Extension::BzAPI::Constants; use Bugzilla::Extension::BzAPI::Util qw(fix_credentials filter_wants_nocache); use Bugzilla::PSGI qw(compile_cgi); use Bugzilla::Error; use Bugzilla::Util qw(trick_taint datetime_from); use Bugzilla::Constants; use Bugzilla::Install::Filesystem; use Bugzilla::WebService::Constants; use File::Basename; our $VERSION = '0.1'; ################ # Installation # ################ sub install_filesystem { my ($self, $args) = @_; my $files = $args->{'files'}; my $extensionsdir = bz_locations()->{'extensionsdir'}; my $scriptname = $extensionsdir . "/" . __PACKAGE__->NAME . "/bin/rest.cgi"; $files->{$scriptname} = { perms => Bugzilla::Install::Filesystem::WS_EXECUTE }; } ################## # Template Hooks # ################## sub template_before_process { my ($self, $args) = @_; my $vars = $args->{'vars'}; my $file = $args->{'file'}; if ($file =~ /config\.json\.tmpl$/) { $vars->{'initial_status'} = Bugzilla::Status->can_change_to; $vars->{'status_objects'} = [ Bugzilla::Status->get_all ]; } } ############## # Code Hooks # ############## sub bug_start_of_update { my ($self, $args) = @_; my $old_bug = $args->{old_bug}; my $params = Bugzilla->input_params; return if !Bugzilla->request_cache->{bzapi}; # Check for a mid-air collision. Currently this only works when updating # an individual bug and if last_changed_time is provided. Otherwise it # allows the changes. my $delta_ts = $params->{last_change_time} || ''; if ($delta_ts && exists $params->{ids} && @{ $params->{ids} } == 1) { _midair_check($delta_ts, $old_bug->delta_ts); } } sub object_end_of_set_all { my ($self, $args) = @_; my $object = $args->{object}; my $params = Bugzilla->input_params; return if !Bugzilla->request_cache->{bzapi}; return if !$object->isa('Bugzilla::Attachment'); # Check for a mid-air collision. Currently this only works when updating # an individual attachment and if last_changed_time is provided. Otherwise it # allows the changes. my $stash = Bugzilla->request_cache->{bzapi_stash} ||= {}; my $delta_ts = $stash->{last_change_time}; _midair_check($delta_ts, $object->modification_time) if $delta_ts; } sub _midair_check { my ($delta_ts, $old_delta_ts) = @_; my $delta_ts_z = datetime_from($delta_ts) || ThrowCodeError('invalid_timestamp', { timestamp => $delta_ts }); my $old_delta_tz_z = datetime_from($old_delta_ts); if ($old_delta_tz_z ne $delta_ts_z) { ThrowUserError('bzapi_midair_collision'); } } sub webservice_error_codes { my ($self, $args) = @_; my $error_map = $args->{error_map}; $error_map->{'bzapi_midair_collision'} = 400; } sub webservice_fix_credentials { my ($self, $args) = @_; my $rpc = $args->{rpc}; my $params = $args->{params}; return if !Bugzilla->request_cache->{bzapi}; fix_credentials($params); } sub webservice_rest_request { my ($self, $args) = @_; my $rpc = $args->{rpc}; my $params = $args->{params}; my $cache = Bugzilla->request_cache; return if !$cache->{bzapi}; # Stash certain values for later use $cache->{bzapi_rpc} = $rpc; # Internal websevice method being used $cache->{bzapi_rpc_method} = $rpc->path_info . "." . $rpc->bz_method_name; # Load the appropriate request handler based on path and type if (my $handler = _find_handler($rpc, 'request')) { &$handler($params); } } sub webservice_rest_response { my ($self, $args) = @_; my $rpc = $args->{rpc}; my $result = $args->{result}; my $response = $args->{response}; my $cache = Bugzilla->request_cache; # Stash certain values for later use $cache->{bzapi_rpc} ||= $rpc; return if !Bugzilla->request_cache->{bzapi} || ref $$result ne 'HASH'; if (exists $$result->{error}) { $$result->{documentation} = BZAPI_DOC; return; } # Load the appropriate response handler based on path and type if (my $handler = _find_handler($rpc, 'response')) { &$handler($result, $response); } # Add a Location header if a newly created resource # such as a bug or comment. if ($rpc->bz_success_code && $rpc->bz_success_code == STATUS_CREATED && $$result->{ref}) { $response->header("Location", $$result->{ref}); } } sub webservice_rest_resources { my ($self, $args) = @_; my $rpc = $args->{rpc}; my $resources = $args->{resources}; return if !Bugzilla->request_cache->{bzapi}; _add_resources($rpc, $resources); } sub webservice_status_code_map { my ($self, $args) = @_; my $status_code_map = $args->{status_code_map}; $status_code_map->{51} = STATUS_BAD_REQUEST; } sub psgi_builder { my ($self, $args) = @_; my $mount = $args->{mount}; $mount->{'bzapi'} = compile_cgi('extensions/BzAPI/bin/rest.cgi'); } ##################### # Utility Functions # ##################### sub _find_handler { my ($rpc, $type) = @_; my $path_info = $rpc->cgi->path_info; my $request_method = $rpc->request->method; my $module = $rpc->bz_class_name || ''; $module =~ s/^Bugzilla::WebService:://; my $cache = _preload_handlers(); return undef if !exists $cache->{$module}; # Make a copy of the handler array so # as to not alter the actual cached data. my @handlers = @{ $cache->{$module} }; while (my $regex = shift @handlers) { my $data = shift @handlers; next if ref $data ne 'HASH'; if ($path_info =~ $regex && exists $data->{$request_method} && exists $data->{$request_method}->{$type}) { return $data->{$request_method}->{$type}; } } return undef; } sub _add_resources { my ($rpc, $native_resources) = @_; my $cache = _preload_handlers(); foreach my $module (keys %$cache) { my $native_module = "Bugzilla::WebService::$module"; next if !$native_resources->{$native_module}; # Make a copy of the handler array so # as to not alter the actual cached data. my @handlers = @{ $cache->{$module} }; my @ext_resources = (); while (my $regex = shift @handlers) { my $data = shift @handlers; next if ref $data ne 'HASH'; my $new_data = {}; foreach my $request_method (keys %$data) { next if !exists $data->{$request_method}->{resource}; $new_data->{$request_method} = $data->{$request_method}->{resource}; } push(@ext_resources, $regex, $new_data); } # Places the new resources at the beginning of the list # so we can capture specific paths before the native resources unshift(@{$native_resources->{$native_module}}, @ext_resources); } } sub _resource_modules { my $extdir = bz_locations()->{extensionsdir}; return map { basename($_, '.pm') } glob("$extdir/" . __PACKAGE__->NAME . "/lib/Resources/*.pm"); } # preload all handlers into cache # since we don't want to parse all # this multiple times sub _preload_handlers { my $cache = Bugzilla->request_cache; if (!exists $cache->{rest_handlers}) { my $all_handlers = {}; foreach my $module (_resource_modules()) { my $resource_class = "Bugzilla::Extension::BzAPI::Resources::$module"; trick_taint($resource_class); eval("require $resource_class"); warn $@ if $@; next if ($@ || !$resource_class->can('rest_handlers')); my $handlers = $resource_class->rest_handlers; next if (ref $handlers ne 'ARRAY' || scalar @$handlers % 2 != 0); $all_handlers->{$module} = $handlers; } $cache->{rest_handlers} = $all_handlers; } return $cache->{rest_handlers}; } __PACKAGE__->NAME;