From 47507cf74635f05e6daaa2689a3e9fddcce4b418 Mon Sep 17 00:00:00 2001 From: Nick Clemens Date: Wed, 4 Dec 2019 11:39:41 +0000 Subject: [PATCH] Bug 17268: Advanced cataloging editor macros - add endpoint Bug 17268: Make shared macros use their own path Bug 17268: Update API to use shared instead of public Bug 17268: (follow-up) Update API field definition to boolean Signed-off-by: Andrew Fuerste-Henry Signed-off-by: Heather Hernandez Signed-off-by: Katrin Fischer --- Koha/AdvancedEditorMacro.pm | 57 +++ Koha/AdvancedEditorMacros.pm | 50 ++ Koha/REST/V1/AdvancedEditorMacro.pm | 397 ++++++++++++++++ api/v1/swagger/definitions.json | 3 + .../swagger/definitions/advancededitormacro.json | 26 + api/v1/swagger/parameters.json | 3 + api/v1/swagger/parameters/advancededitormacro.json | 9 + api/v1/swagger/paths.json | 12 + api/v1/swagger/paths/advancededitormacros.json | 521 +++++++++++++++++++++ api/v1/swagger/x-primitives.json | 5 + t/db_dependent/api/v1/advanced_editor_macros.t | 475 +++++++++++++++++++ 11 files changed, 1558 insertions(+) create mode 100644 Koha/AdvancedEditorMacro.pm create mode 100644 Koha/AdvancedEditorMacros.pm create mode 100644 Koha/REST/V1/AdvancedEditorMacro.pm create mode 100644 api/v1/swagger/definitions/advancededitormacro.json create mode 100644 api/v1/swagger/parameters/advancededitormacro.json create mode 100644 api/v1/swagger/paths/advancededitormacros.json create mode 100644 t/db_dependent/api/v1/advanced_editor_macros.t diff --git a/Koha/AdvancedEditorMacro.pm b/Koha/AdvancedEditorMacro.pm new file mode 100644 index 0000000000..d30fcc5f16 --- /dev/null +++ b/Koha/AdvancedEditorMacro.pm @@ -0,0 +1,57 @@ +package Koha::AdvancedEditorMacro; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Carp; + +use Koha::Database; + +use base qw(Koha::Object); + +=head1 NAME + +Koha::AdvancedEditorMacro - Koha Advanced Editor Macro Object class + +=head1 API + +=head2 Class methods + +=head3 to_api_mapping + +=cut + +sub to_api_mapping { + return { + id => 'macro_id', + macro => 'macro_text', + borrowernumber => 'patron_id', + }; +} + + +=head2 Internal methods + +=head3 _type + +=cut + +sub _type { + return 'AdvancedEditorMacro'; +} + +1; diff --git a/Koha/AdvancedEditorMacros.pm b/Koha/AdvancedEditorMacros.pm new file mode 100644 index 0000000000..67d44281a1 --- /dev/null +++ b/Koha/AdvancedEditorMacros.pm @@ -0,0 +1,50 @@ +package Koha::AdvancedEditorMacros; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Carp; + +use Koha::Database; + +use Koha::AdvancedEditorMacro; + +use base qw(Koha::Objects); + +=head1 NAME + +Koha::AdvancedEditorMacros - Koha Advanced Editor Macro Object set class + +=head1 API + +=head2 Class Methods + +=cut + +=head3 type + +=cut + +sub _type { + return 'AdvancedEditorMacro'; +} + +sub object_class { + return 'Koha::AdvancedEditorMacro'; +} + +1; diff --git a/Koha/REST/V1/AdvancedEditorMacro.pm b/Koha/REST/V1/AdvancedEditorMacro.pm new file mode 100644 index 0000000000..92f6730c2a --- /dev/null +++ b/Koha/REST/V1/AdvancedEditorMacro.pm @@ -0,0 +1,397 @@ +package Koha::REST::V1::AdvancedEditorMacro; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Controller'; +use Koha::AdvancedEditorMacros; + +use Try::Tiny; + +=head1 API + +=head2 Class Methods + +=cut + +=head3 list + +Controller function that handles listing Koha::AdvancedEditorMacro objects + +=cut + +sub list { + my $c = shift->openapi->valid_input or return; + my $patron = $c->stash('koha.user'); + return try { + my $macros_set = Koha::AdvancedEditorMacros->search({ -or => { shared => 1, borrowernumber => $patron->borrowernumber } }); + my $macros = $c->objects->search( $macros_set, \&_to_model, \&_to_api ); + return $c->render( status => 200, openapi => $macros ); + } + catch { + if ( $_->isa('DBIx::Class::Exception') ) { + return $c->render( status => 500, + openapi => { error => $_->{msg} } ); + } + else { + return $c->render( status => 500, + openapi => { error => "Something went wrong, check the logs. $_"} ); + } + }; + +} + +=head3 get + +Controller function that handles retrieving a single Koha::AdvancedEditorMacro + +=cut + +sub get { + my $c = shift->openapi->valid_input or return; + my $patron = $c->stash('koha.user'); + my $macro = Koha::AdvancedEditorMacros->find({ + id => $c->validation->param('advancededitormacro_id'), + }); + unless ($macro) { + return $c->render( status => 404, + openapi => { error => "Macro not found" } ); + } + if( $macro->shared ){ + return $c->render( status => 403, openapi => { + error => "This macro is shared, you must access it via advancededitormacros/shared" + }); + } + warn $macro->borrowernumber; + warn $patron->borrowernumber; + if( $macro->borrowernumber != $patron->borrowernumber ){ + return $c->render( status => 403, openapi => { + error => "You do not have permission to access this macro" + }); + } + + return $c->render( status => 200, openapi => $macro->to_api ); +} + +=head3 get_shared + +Controller function that handles retrieving a single Koha::AdvancedEditorMacro + +=cut + +sub get_shared { + my $c = shift->openapi->valid_input or return; + my $patron = $c->stash('koha.user'); + my $macro = Koha::AdvancedEditorMacros->find({ + id => $c->validation->param('advancededitormacro_id'), + }); + unless ($macro) { + return $c->render( status => 404, + openapi => { error => "Macro not found" } ); + } + unless( $macro->shared ){ + return $c->render( status => 403, openapi => { + error => "This macro is not shared, you must access it via advancededitormacros" + }); + } + return $c->render( status => 200, openapi => $macro->to_api ); +} + +=head3 add + +Controller function that handles adding a new Koha::AdvancedEditorMacro object + +=cut + +sub add { + my $c = shift->openapi->valid_input or return; + + if( defined $c->validation->param('body')->{shared} && $c->validation->param('body')->{shared} == 1 ){ + return $c->render( status => 403, + openapi => { error => "To create shared macros you must use advancededitor/shared" } ); + } + + return try { + my $macro = Koha::AdvancedEditorMacro->new( _to_model( $c->validation->param('body') ) ); + $macro->store; + $c->res->headers->location( $c->req->url->to_string . '/' . $macro->id ); + return $c->render( + status => 201, + openapi => $macro->to_api + ); + } + catch { handle_error($_) }; +} + +=head3 add_shared + +Controller function that handles adding a new shared Koha::AdvancedEditorMacro object + +=cut + +sub add_shared { + my $c = shift->openapi->valid_input or return; + + unless( defined $c->validation->param('body')->{shared} && $c->validation->param('body')->{shared} == 1 ){ + return $c->render( status => 403, + openapi => { error => "To create private macros you must use advancededitor" } ); + } + + return try { + my $macro = Koha::AdvancedEditorMacro->new( _to_model( $c->validation->param('body') ) ); + $macro->store; + $c->res->headers->location( $c->req->url->to_string . '/' . $macro->id ); + return $c->render( + status => 201, + openapi => $macro->to_api + ); + } + catch { handle_error($_) }; +} + +=head3 update + +Controller function that handles updating a Koha::AdvancedEditorMacro object + +=cut + +sub update { + my $c = shift->openapi->valid_input or return; + + my $macro = Koha::AdvancedEditorMacros->find( $c->validation->param('advancededitormacro_id') ); + + if ( not defined $macro ) { + return $c->render( status => 404, + openapi => { error => "Object not found" } ); + } + my $patron = $c->stash('koha.user'); + + if( $macro->shared == 1 || defined $c->validation->param('body')->{shared} && $c->validation->param('body')->{shared} == 1 ){ + return $c->render( status => 403, + openapi => { error => "To update a macro as shared you must use the advancededitormacros/shared endpoint" } ); + } else { + unless ( $macro->borrowernumber == $patron->borrowernumber ){ + return $c->render( status => 403, + openapi => { error => "You can only edit macros you own" } ); + } + } + + return try { + my $params = $c->req->json; + $macro->set( _to_model($params) ); + $macro->store(); + return $c->render( status => 200, openapi => $macro->to_api ); + } + catch { handle_error($_) }; +} + +=head3 update_shared + +Controller function that handles updating a shared Koha::AdvancedEditorMacro object + +=cut + +sub update_shared { + my $c = shift->openapi->valid_input or return; + + my $macro = Koha::AdvancedEditorMacros->find( $c->validation->param('advancededitormacro_id') ); + + if ( not defined $macro ) { + return $c->render( status => 404, + openapi => { error => "Object not found" } ); + } + + unless( $macro->shared == 1 || defined $c->validation->param('body')->{shared} && $c->validation->param('body')->{shared} == 1 ){ + return $c->render( status => 403, + openapi => { error => "You can only update shared macros using this endpoint" } ); + } + + return try { + my $params = $c->req->json; + $macro->set( _to_model($params) ); + $macro->store(); + return $c->render( status => 200, openapi => $macro->to_api ); + } + catch { handle_error($_) }; +} + +=head3 delete + +Controller function that handles deleting a Koha::AdvancedEditorMacro object + +=cut + +sub delete { + my $c = shift->openapi->valid_input or return; + + my $macro = Koha::AdvancedEditorMacros->find( $c->validation->param('advancededitormacro_id') ); + if ( not defined $macro ) { + return $c->render( status => 404, + openapi => { error => "Object not found" } ); + } + + my $patron = $c->stash('koha.user'); + if( $macro->shared == 1 ){ + return $c->render( status => 403, + openapi => { error => "You cannot delete shared macros using this endpoint" } ); + } else { + unless ( $macro->borrowernumber == $patron->borrowernumber ){ + return $c->render( status => 403, + openapi => { error => "You can only delete macros you own" } ); + } + } + + return try { + $macro->delete; + return $c->render( status => 200, openapi => "" ); + } + catch { handle_error($_) }; +} + +=head3 delete_shared + +Controller function that handles deleting a shared Koha::AdvancedEditorMacro object + +=cut + +sub delete_shared { + my $c = shift->openapi->valid_input or return; + + my $macro = Koha::AdvancedEditorMacros->find( $c->validation->param('advancededitormacro_id') ); + if ( not defined $macro ) { + return $c->render( status => 404, + openapi => { error => "Object not found" } ); + } + + unless( $macro->shared == 1 ){ + return $c->render( status => 403, + openapi => { error => "You can only delete shared macros using this endpoint" } ); + } + + return try { + $macro->delete; + return $c->render( status => 200, openapi => "" ); + } + catch { handle_error($_,$c) }; +} + +=head3 _handle_error + +Helper function that passes exception or error + +=cut + +sub _handle_error { + my ($err,$c) = @_; + if ( $err->isa('DBIx::Class::Exception') ) { + return $c->render( status => 500, + openapi => { error => $err->{msg} } ); + } + else { + return $c->render( status => 500, + openapi => { error => "Something went wrong, check the logs."} ); + } +}; + + +=head3 _to_api + +Helper function that maps a hashref of Koha::AdvancedEditorMacro attributes into REST api +attribute names. + +=cut + +sub _to_api { + my $macro = shift; + + # Rename attributes + foreach my $column ( keys %{ $Koha::REST::V1::AdvancedEditorMacro::to_api_mapping } ) { + my $mapped_column = $Koha::REST::V1::AdvancedEditorMacro::to_api_mapping->{$column}; + if ( exists $macro->{ $column } + && defined $mapped_column ) + { + # key /= undef + $macro->{ $mapped_column } = delete $macro->{ $column }; + } + elsif ( exists $macro->{ $column } + && !defined $mapped_column ) + { + # key == undef => to be deleted + delete $macro->{ $column }; + } + } + + return $macro; +} + +=head3 _to_model + +Helper function that maps REST api objects into Koha::AdvancedEditorMacros +attribute names. + +=cut + +sub _to_model { + my $macro = shift; + + foreach my $attribute ( keys %{ $Koha::REST::V1::AdvancedEditorMacro::to_model_mapping } ) { + my $mapped_attribute = $Koha::REST::V1::AdvancedEditorMacro::to_model_mapping->{$attribute}; + if ( exists $macro->{ $attribute } + && defined $mapped_attribute ) + { + # key /= undef + $macro->{ $mapped_attribute } = delete $macro->{ $attribute }; + } + elsif ( exists $macro->{ $attribute } + && !defined $mapped_attribute ) + { + # key == undef => to be deleted + delete $macro->{ $attribute }; + } + } + + if ( exists $macro->{shared} ) { + $macro->{shared} = ($macro->{shared}) ? 1 : 0; + } + + + return $macro; +} + +=head2 Global variables + +=head3 $to_api_mapping + +=cut + +our $to_api_mapping = { + id => 'macro_id', + macro => 'macro_text', + borrowernumber => 'patron_id', +}; + +=head3 $to_model_mapping + +=cut + +our $to_model_mapping = { + macro_id => 'id', + macro_text => 'macro', + patron_id => 'borrowernumber', +}; + +1; diff --git a/api/v1/swagger/definitions.json b/api/v1/swagger/definitions.json index 4b21d32f32..716e8703b6 100644 --- a/api/v1/swagger/definitions.json +++ b/api/v1/swagger/definitions.json @@ -47,6 +47,9 @@ "order": { "$ref": "definitions/order.json" }, + "advancededitormacro": { + "$ref": "definitions/advancededitormacro.json" + }, "patron": { "$ref": "definitions/patron.json" }, diff --git a/api/v1/swagger/definitions/advancededitormacro.json b/api/v1/swagger/definitions/advancededitormacro.json new file mode 100644 index 0000000000..46e6eebee9 --- /dev/null +++ b/api/v1/swagger/definitions/advancededitormacro.json @@ -0,0 +1,26 @@ +{ + "type": "object", + "properties": { + "macro_id": { + "$ref": "../x-primitives.json#/advancededitormacro_id" + }, + "name": { + "description": "macro name", + "type": "string" + }, + "macro_text": { + "description": "macro text", + "type": ["string", "null"] + }, + "patron_id": { + "description": "borrower number", + "type": ["integer", "null"] + }, + "shared": { + "description": "is macro shared", + "type": ["boolean", "null"] + } + }, + "additionalProperties": false, + "required": ["name", "macro_text", "patron_id", "shared"] +} diff --git a/api/v1/swagger/parameters.json b/api/v1/swagger/parameters.json index 9c3d7ebc96..cab2d53b47 100644 --- a/api/v1/swagger/parameters.json +++ b/api/v1/swagger/parameters.json @@ -2,6 +2,9 @@ "biblio_id_pp": { "$ref": "parameters/biblio.json#/biblio_id_pp" }, + "advancededitormacro_id_pp": { + "$ref": "parameters/advancededitormacro.json#/advancededitormacro_id_pp" + }, "patron_id_pp": { "$ref": "parameters/patron.json#/patron_id_pp" }, diff --git a/api/v1/swagger/parameters/advancededitormacro.json b/api/v1/swagger/parameters/advancededitormacro.json new file mode 100644 index 0000000000..482e0bc917 --- /dev/null +++ b/api/v1/swagger/parameters/advancededitormacro.json @@ -0,0 +1,9 @@ +{ + "advancededitormacro_id_pp": { + "name": "advancededitormacro_id", + "in": "path", + "description": "Advanced Editor Macro internal identifier", + "required": true, + "type": "integer" + } +} diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json index 1e4ec4a934..8e0af9bed8 100644 --- a/api/v1/swagger/paths.json +++ b/api/v1/swagger/paths.json @@ -68,6 +68,18 @@ "/checkouts/{checkout_id}/allows_renewal": { "$ref": "paths/checkouts.json#/~1checkouts~1{checkout_id}~1allows_renewal" }, + "/advancededitormacros": { + "$ref": "paths/advancededitormacros.json#/~1advancededitormacros" + }, + "/advancededitormacros/{advancededitormacro_id}": { + "$ref": "paths/advancededitormacros.json#/~1advancededitormacros~1{advancededitormacro_id}" + }, + "/advancededitormacros/shared": { + "$ref": "paths/advancededitormacros.json#/~1advancededitormacros~1shared" + }, + "/advancededitormacros/shared/{advancededitormacro_id}": { + "$ref": "paths/advancededitormacros.json#/~1advancededitormacros~1shared~1{advancededitormacro_id}" + }, "/patrons": { "$ref": "paths/patrons.json#/~1patrons" }, diff --git a/api/v1/swagger/paths/advancededitormacros.json b/api/v1/swagger/paths/advancededitormacros.json new file mode 100644 index 0000000000..7c7eb93aaa --- /dev/null +++ b/api/v1/swagger/paths/advancededitormacros.json @@ -0,0 +1,521 @@ +{ + "/advancededitormacros": { + "get": { + "x-mojo-to": "AdvancedEditorMacro#list", + "operationId": "listMacro", + "tags": ["advancededitormacro"], + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "name", + "in": "query", + "description": "Case insensative search on macro name", + "required": false, + "type": "string" + }, + { + "name": "macro_text", + "in": "query", + "description": "Case insensative search on macro text", + "required": false, + "type": "string" + }, + { + "name": "patron_id", + "in": "query", + "description": "Search on internal patron_id", + "required": false, + "type": "string" + }, + { + "name": "shared", + "in": "query", + "description": "Search on shared macros", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "A list of macros", + "schema": { + "type": "array", + "items": { + "$ref": "../definitions.json#/advancededitormacro" + } + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "editcatalogue": "advanced_editor" + } + } + }, + "post": { + "x-mojo-to": "AdvancedEditorMacro#add", + "operationId": "addAdvancedEditorMacro", + "tags": ["advancededitormacro"], + "parameters": [{ + "name": "body", + "in": "body", + "description": "A JSON object containing informations about the new macro", + "required": true, + "schema": { + "$ref": "../definitions.json#/advancededitormacro" + } + }], + "produces": [ + "application/json" + ], + "responses": { + "201": { + "description": "Macro added", + "schema": { + "$ref": "../definitions.json#/advancededitormacro" + } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "editcatalogue": "advanced_editor" + } + } + } + }, + "/advancededitormacros/shared": { + "post": { + "x-mojo-to": "AdvancedEditorMacro#add_shared", + "operationId": "addsharedAdvancedEditorMacro", + "tags": ["advancededitormacro"], + "parameters": [{ + "name": "body", + "in": "body", + "description": "A JSON object containing informations about the new macro", + "required": true, + "schema": { + "$ref": "../definitions.json#/advancededitormacro" + } + }], + "produces": [ + "application/json" + ], + "responses": { + "201": { + "description": "Macro added", + "schema": { + "$ref": "../definitions.json#/advancededitormacro" + } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "editcatalogue": "advanced_editor", + "editcatalogue": "create_shared_macros" + } + } + } + }, + "/advancededitormacros/{advancededitormacro_id}": { + "get": { + "x-mojo-to": "AdvancedEditorMacro#get", + "operationId": "getAdvancedEditorMacro", + "tags": ["advancededitormacros"], + "parameters": [{ + "$ref": "../parameters.json#/advancededitormacro_id_pp" + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A macro", + "schema": { + "$ref": "../definitions.json#/advancededitormacro" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "AdvancedEditorMacro not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "editcatalogue": "advanced_editor" + } + } + }, + "put": { + "x-mojo-to": "AdvancedEditorMacro#update", + "operationId": "updateAdvancedEditorMacro", + "tags": ["advancededitormacros"], + "parameters": [{ + "$ref": "../parameters.json#/advancededitormacro_id_pp" + }, { + "name": "body", + "in": "body", + "description": "An advanced editor macro object", + "required": true, + "schema": { + "$ref": "../definitions.json#/advancededitormacro" + } + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "An advanced editor macro", + "schema": { + "$ref": "../definitions.json#/advancededitormacro" + } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Macro not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "editcatalogue": "advanced_editor" + } + } + }, + "delete": { + "x-mojo-to": "AdvancedEditorMacro#delete", + "operationId": "deleteAdvancedEditorMacro", + "tags": ["advancededitormacros"], + "parameters": [{ + "$ref": "../parameters.json#/advancededitormacro_id_pp" + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Advanced editor macro deleted", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Macro not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "editcatalogue": "advanced_editor" + } + } + } + }, + "/advancededitormacros/shared/{advancededitormacro_id}": { + "get": { + "x-mojo-to": "AdvancedEditorMacro#get_shared", + "operationId": "getsharedAdvancedEditorMacro", + "tags": ["advancededitormacros"], + "parameters": [{ + "$ref": "../parameters.json#/advancededitormacro_id_pp" + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A macro", + "schema": { + "$ref": "../definitions.json#/advancededitormacro" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "AdvancedEditorMacro not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "editcatalogue": "advanced_editor" + } + } + }, + "put": { + "x-mojo-to": "AdvancedEditorMacro#update_shared", + "operationId": "updatesharedAdvancedEditorMacro", + "tags": ["advancededitormacros"], + "parameters": [{ + "$ref": "../parameters.json#/advancededitormacro_id_pp" + }, { + "name": "body", + "in": "body", + "description": "An advanced editor macro object", + "required": true, + "schema": { + "$ref": "../definitions.json#/advancededitormacro" + } + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "An advanced editor macro", + "schema": { + "$ref": "../definitions.json#/advancededitormacro" + } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Macro not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "editcatalogue": "advanced_editor", + "editcatalogue": "create_shared_macros" + } + } + }, + "delete": { + "x-mojo-to": "AdvancedEditorMacro#delete_shared", + "operationId": "deletesharedAdvancedEditorMacro", + "tags": ["advancededitormacros"], + "parameters": [{ + "$ref": "../parameters.json#/advancededitormacro_id_pp" + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Advanced editor macro deleted", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Macro not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "editcatalogue": "advanced_editor", + "editcatalogue": "delete_shared_macros" + } + } + } + } +} diff --git a/api/v1/swagger/x-primitives.json b/api/v1/swagger/x-primitives.json index 19bc8dc012..0594264be9 100644 --- a/api/v1/swagger/x-primitives.json +++ b/api/v1/swagger/x-primitives.json @@ -3,6 +3,11 @@ "type": "integer", "description": "Internal biblio identifier" }, + "advancededitormacro_id": { + "type": "integer", + "description": "Internal advanced editor macro identifier", + "readOnly": true + }, "patron_id": { "type": "integer", "description": "Internal patron identifier" diff --git a/t/db_dependent/api/v1/advanced_editor_macros.t b/t/db_dependent/api/v1/advanced_editor_macros.t new file mode 100644 index 0000000000..025c73fa2d --- /dev/null +++ b/t/db_dependent/api/v1/advanced_editor_macros.t @@ -0,0 +1,475 @@ +#!/usr/bin/env perl + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Test::More tests => 5; +use Test::Mojo; +use Test::Warn; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +use Koha::AdvancedEditorMacros; +use Koha::Database; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); + +my $t = Test::Mojo->new('Koha::REST::V1'); + +$schema->storage->txn_begin; + +subtest 'list() tests' => sub { + plan tests => 8; + + + my $patron_1 = $builder->build_object({ + class => 'Koha::Patrons', + value => { flags => 9 } + }); + my $patron_2 = $builder->build_object({ + class => 'Koha::Patrons', + }); + my $password = 'thePassword123'; + $patron_1->set_password({ password => $password, skip_validation => 1 }); + my $userid = $patron_1->userid; + + # Create test context + my $macro_1 = $builder->build_object({ class => 'Koha::AdvancedEditorMacros', value => + { + name => 'Test1', + macro => 'delete 100', + borrowernumber => $patron_1->borrowernumber, + } + }); + my $macro_2 = $builder->build_object({ class => 'Koha::AdvancedEditorMacros', value => + { + name => 'Test2', + macro => 'delete 100', + borrowernumber => $patron_1->borrowernumber, + shared=> 1, + } + }); + my $macro_3 = $builder->build_object({ class => 'Koha::AdvancedEditorMacros', value => + { + name => 'Test3', + macro => 'delete 100', + borrowernumber => $patron_2->borrowernumber, + } + }); + my $macro_4 = $builder->build_object({ class => 'Koha::AdvancedEditorMacros', value => + { + name => 'Test4', + macro => 'delete 100', + borrowernumber => $patron_2->borrowernumber, + shared => 1, + } + }); + + my $macros_index = Koha::AdvancedEditorMacros->search({ -or => { shared => 1, borrowernumber => $patron_1->borrowernumber } })->count-1; + ## Authorized user tests + # Make sure we are returned with the correct amount of macros + $t->get_ok( "//$userid:$password@/api/v1/advancededitormacros" ) + ->status_is( 200, 'SWAGGER3.2.2' ) + ->json_has('/' . $macros_index . '/macro_id') + ->json_hasnt('/' . ($macros_index + 1) . '/macro_id'); + + subtest 'query parameters' => sub { + + plan tests => 15; + $t->get_ok("//$userid:$password@/api/v1/advancededitormacros?name=" . $macro_2->name) + ->status_is(200) + ->json_has( [ $macro_2 ] ); + $t->get_ok("//$userid:$password@/api/v1/advancededitormacros?name=" . $macro_3->name) + ->status_is(200) + ->json_has( [ ] ); + $t->get_ok("//$userid:$password@/api/v1/advancededitormacros?macro_text=delete 100") + ->status_is(200) + ->json_has( [ $macro_1, $macro_2, $macro_4 ] ); + $t->get_ok("//$userid:$password@/api/v1/advancededitormacros?patron_id=" . $patron_1->borrowernumber) + ->status_is(200) + ->json_has( [ $macro_1, $macro_2 ] ); + $t->get_ok("//$userid:$password@/api/v1/advancededitormacros?shared=1") + ->status_is(200) + ->json_has( [ $macro_2, $macro_4 ] ); + }; + + # Warn on unsupported query parameter + $t->get_ok( "//$userid:$password@/api/v1/advancededitormacros?macro_blah=blah" ) + ->status_is(400) + ->json_is( [{ path => '/query/macro_blah', message => 'Malformed query string'}] ); + +}; + +subtest 'get() tests' => sub { + + plan tests => 15; + + my $patron = $builder->build_object({ + class => 'Koha::Patrons', + value => { flags => 1 } + }); + my $password = 'thePassword123'; + $patron->set_password({ password => $password, skip_validation => 1 }); + my $userid = $patron->userid; + + my $macro_1 = $builder->build_object( { class => 'Koha::AdvancedEditorMacros', value => { + shared => 1, + } + }); + my $macro_2 = $builder->build_object( { class => 'Koha::AdvancedEditorMacros', value => { + shared => 0, + } + }); + my $macro_3 = $builder->build_object( { class => 'Koha::AdvancedEditorMacros', value => { + borrowernumber => $patron->borrowernumber, + shared => 0, + } + }); + + $t->get_ok( "//$userid:$password@/api/v1/advancededitormacros/" . $macro_1->id ) + ->status_is( 403, 'Cannot get a shared macro via regular endpoint' ) + ->json_is( '/error' => 'This macro is shared, you must access it via advancededitormacros/shared' ); + + $t->get_ok( "//$userid:$password@/api/v1/advancededitormacros/shared/" . $macro_1->id ) + ->status_is( 200, 'Can get a shared macro via shared endpoint' ) + ->json_is( '' => Koha::REST::V1::AdvancedEditorMacro::_to_api( $macro_1->TO_JSON ), 'Macro correctly retrieved' ); + + $t->get_ok( "//$userid:$password@/api/v1/advancededitormacros/" . $macro_2->id ) + ->status_is( 403, 'Cannot access another users macro' ) + ->json_is( '/error' => 'You do not have permission to access this macro' ); + + $t->get_ok( "//$userid:$password@/api/v1/advancededitormacros/" . $macro_3->id ) + ->status_is( 200, 'Can get your own private macro' ) + ->json_is( '' => Koha::REST::V1::AdvancedEditorMacro::_to_api( $macro_3->TO_JSON ), 'Macro correctly retrieved' ); + + my $non_existent_code = $macro_1->id; + $macro_1->delete; + + $t->get_ok( "//$userid:$password@/api/v1/advancededitormacros/" . $non_existent_code ) + ->status_is(404) + ->json_is( '/error' => 'Macro not found' ); + +}; + +subtest 'add() tests' => sub { + + plan tests => 24; + + my $authorized_patron = $builder->build_object({ + class => 'Koha::Patrons', + value => { flags => 0 } + }); + $builder->build({ + source => 'UserPermission', + value => { + borrowernumber => $authorized_patron->borrowernumber, + module_bit => 9, + code => 'advanced_editor', + }, + }); + + my $password = 'thePassword123'; + $authorized_patron->set_password({ password => $password, skip_validation => 1 }); + my $auth_userid = $authorized_patron->userid; + + my $unauthorized_patron = $builder->build_object({ + class => 'Koha::Patrons', + value => { flags => 0 } + }); + $unauthorized_patron->set_password({ password => $password, skip_validation => 1 }); + my $unauth_userid = $unauthorized_patron->userid; + + my $macro = $builder->build_object({ + class => 'Koha::AdvancedEditorMacros', + value => { shared => 0 } + }); + my $macro_values = Koha::REST::V1::AdvancedEditorMacro::_to_api( $macro->TO_JSON ); + delete $macro_values->{macro_id}; + $macro->delete; + + # Unauthorized attempt to write + $t->post_ok( "//$unauth_userid:$password@/api/v1/advancededitormacros" => json => $macro_values ) + ->status_is(403); + + # Authorized attempt to write invalid data + my $macro_with_invalid_field = { %$macro_values }; + $macro_with_invalid_field->{'big_mac_ro'} = 'Mac attack'; + + $t->post_ok( "//$auth_userid:$password@/api/v1/advancededitormacros" => json => $macro_with_invalid_field ) + ->status_is(400) + ->json_is( + "/errors" => [ + { + message => "Properties not allowed: big_mac_ro.", + path => "/body" + } + ] + ); + + # Authorized attempt to write + $t->post_ok( "//$auth_userid:$password@/api/v1/advancededitormacros" => json => $macro_values ) + ->status_is( 201, 'SWAGGER3.2.1' ) + ->json_has( '/macro_id', 'We generated a new id' ) + ->json_is( '/name' => $macro_values->{name}, 'The name matches what we supplied' ) + ->json_is( '/macro_text' => $macro_values->{macro_text}, 'The text matches what we supplied' ) + ->json_is( '/patron_id' => $macro_values->{patron_id}, 'The borrower matches the borrower who submitted' ) + ->json_is( '/shared' => 0, 'The macro is not shared' ) + ->header_like( Location => qr|^\/api\/v1\/advancededitormacros\/d*|, 'Correct location' ); + + # save the library_id + my $macro_id = 999; + + # Authorized attempt to create with existing id + $macro_values->{macro_id} = $macro_id; + + $t->post_ok( "//$auth_userid:$password@/api/v1/advancededitormacros" => json => $macro_values ) + ->status_is(400) + ->json_is( '/errors' => [ + { + message => "Read-only.", + path => "/body/macro_id" + } + ] + ); + + $macro_values->{shared} = 1; + delete $macro_values->{macro_id}; + + # Unauthorized attempt to write a shared macro on private endpoint + $t->post_ok( "//$auth_userid:$password@/api/v1/advancededitormacros" => json => $macro_values ) + ->status_is(403); + # Unauthorized attempt to write a private macro on shared endpoint + $t->post_ok( "//$auth_userid:$password@/api/v1/advancededitormacros/shared" => json => $macro_values ) + ->status_is(403); + + $builder->build({ + source => 'UserPermission', + value => { + borrowernumber => $authorized_patron->borrowernumber, + module_bit => 9, + code => 'create_shared_macros', + }, + }); + + # Authorized attempt to write a shared macro on private endpoint + $t->post_ok( "//$auth_userid:$password@/api/v1/advancededitormacros" => json => $macro_values ) + ->status_is(403); + + # Authorized attempt to write a shared macro on shared endpoint + $t->post_ok( "//$auth_userid:$password@/api/v1/advancededitormacros/shared" => json => $macro_values ) + ->status_is(201); + +}; + +subtest 'update() tests' => sub { + plan tests => 32; + + my $authorized_patron = $builder->build_object({ + class => 'Koha::Patrons', + value => { flags => 0 } + }); + $builder->build({ + source => 'UserPermission', + value => { + borrowernumber => $authorized_patron->borrowernumber, + module_bit => 9, + code => 'advanced_editor', + }, + }); + + my $password = 'thePassword123'; + $authorized_patron->set_password({ password => $password, skip_validation => 1 }); + my $auth_userid = $authorized_patron->userid; + + my $unauthorized_patron = $builder->build_object({ + class => 'Koha::Patrons', + value => { flags => 0 } + }); + $unauthorized_patron->set_password({ password => $password, skip_validation => 1 }); + my $unauth_userid = $unauthorized_patron->userid; + + my $macro = $builder->build_object({ + class => 'Koha::AdvancedEditorMacros', + value => { borrowernumber => $authorized_patron->borrowernumber, shared => 0 } + }); + my $macro_2 = $builder->build_object({ + class => 'Koha::AdvancedEditorMacros', + value => { borrowernumber => $unauthorized_patron->borrowernumber, shared => 0 } + }); + my $macro_id = $macro->id; + my $macro_2_id = $macro_2->id; + my $macro_values = Koha::REST::V1::AdvancedEditorMacro::_to_api( $macro->TO_JSON ); + delete $macro_values->{macro_id}; + + # Unauthorized attempt to update + $t->put_ok( "//$unauth_userid:$password@/api/v1/advancededitormacros/$macro_id" + => json => { name => 'New unauthorized name change' } ) + ->status_is(403); + + # Attempt partial update on a PUT + my $macro_with_missing_field = { + name => "Call it macro-roni", + }; + + $t->put_ok( "//$auth_userid:$password@/api/v1/advancededitormacros/$macro_id" => json => $macro_with_missing_field ) + ->status_is(400) + ->json_has( "/errors" => + [ { message => "Missing property.", path => "/body/macro_text" } ] + ); + + my $macro_update = { + name => "Macro-update", + macro_text => "delete 100", + patron_id => $authorized_patron->borrowernumber, + shared => 0, + }; + + my $test = $t->put_ok( "//$auth_userid:$password@/api/v1/advancededitormacros/$macro_id" => json => $macro_update ) + ->status_is(200, 'Authorized user can update a macro') + ->json_is( '/macro_id' => $macro_id, 'We get the id back' ) + ->json_is( '/name' => $macro_update->{name}, 'We get the name back' ) + ->json_is( '/macro_text' => $macro_update->{macro_text}, 'We get the text back' ) + ->json_is( '/patron_id' => $macro_update->{patron_id}, 'We get the patron_id back' ) + ->json_is( '/shared' => $macro_update->{shared}, 'It should still not be shared' ); + + # Now try to make the macro shared + $macro_update->{shared} = 1; + + $t->put_ok( "//$auth_userid:$password@/api/v1/advancededitormacros/shared/$macro_id" => json => $macro_update ) + ->status_is(403, 'Cannot make your macro shared on private endpoint'); + $t->put_ok( "//$auth_userid:$password@/api/v1/advancededitormacros/shared/$macro_id" => json => $macro_update ) + ->status_is(403, 'Cannot make your macro shared without permission'); + + $builder->build({ + source => 'UserPermission', + value => { + borrowernumber => $authorized_patron->borrowernumber, + module_bit => 9, + code => 'create_shared_macros', + }, + }); + + $t->put_ok( "//$auth_userid:$password@/api/v1/advancededitormacros/$macro_id" => json => $macro_update ) + ->status_is(403, 'Cannot make your macro shared on the private endpoint'); + + $t->put_ok( "//$auth_userid:$password@/api/v1/advancededitormacros/shared/$macro_id" => json => $macro_update ) + ->status_is(200, 'Can update macro to shared with permission') + ->json_is( '/macro_id' => $macro_id, 'We get back the id' ) + ->json_is( '/name' => $macro_update->{name}, 'We get back the name' ) + ->json_is( '/macro_text' => $macro_update->{macro_text}, 'We get back the text' ) + ->json_is( '/patron_id' => $macro_update->{patron_id}, 'We get back our patron id' ) + ->json_is( '/shared' => 1, 'It is shared' ); + + # Authorized attempt to write invalid data + my $macro_with_invalid_field = { %$macro_update }; + $macro_with_invalid_field->{'big_mac_ro'} = 'Mac attack'; + + $t->put_ok( "//$auth_userid:$password@/api/v1/advancededitormacros/$macro_id" => json => $macro_with_invalid_field ) + ->status_is(400) + ->json_is( + "/errors" => [ + { + message => "Properties not allowed: big_mac_ro.", + path => "/body" + } + ] + ); + + my $non_existent_macro = $builder->build_object({class => 'Koha::AdvancedEditorMacros'}); + my $non_existent_code = $non_existent_macro->id; + $non_existent_macro->delete; + + $t->put_ok("//$auth_userid:$password@/api/v1/advancededitormacros/$non_existent_code" => json => $macro_update) + ->status_is(404); + + $t->put_ok("//$auth_userid:$password@/api/v1/advancededitormacros/$macro_2_id" => json => $macro_update) + ->status_is(403, "Cannot update other borrowers private macro"); +}; + +subtest 'delete() tests' => sub { + plan tests => 12; + + my $authorized_patron = $builder->build_object({ + class => 'Koha::Patrons', + value => { flags => 0 } + }); + $builder->build({ + source => 'UserPermission', + value => { + borrowernumber => $authorized_patron->borrowernumber, + module_bit => 9, + code => 'advanced_editor', + }, + }); + + my $password = 'thePassword123'; + $authorized_patron->set_password({ password => $password, skip_validation => 1 }); + my $auth_userid = $authorized_patron->userid; + + my $unauthorized_patron = $builder->build_object({ + class => 'Koha::Patrons', + value => { flags => 0 } + }); + $unauthorized_patron->set_password({ password => $password, skip_validation => 1 }); + my $unauth_userid = $unauthorized_patron->userid; + + my $macro = $builder->build_object({ + class => 'Koha::AdvancedEditorMacros', + value => { borrowernumber => $authorized_patron->borrowernumber, shared => 0 } + }); + my $macro_2 = $builder->build_object({ + class => 'Koha::AdvancedEditorMacros', + value => { borrowernumber => $unauthorized_patron->borrowernumber, shared => 0 } + }); + my $macro_id = $macro->id; + my $macro_2_id = $macro_2->id; + + # Unauthorized attempt to delete + $t->delete_ok( "//$unauth_userid:$password@/api/v1/advancededitormacros/$macro_2_id") + ->status_is(403, "Cannot delete macro without permission"); + + $t->delete_ok( "//$auth_userid:$password@/api/v1/advancededitormacros/$macro_id") + ->status_is(200, 'Can delete macro with permission'); + + $t->delete_ok( "//$auth_userid:$password@/api/v1/advancededitormacros/$macro_2_id") + ->status_is(403, 'Cannot delete other users macro with permission'); + + $macro_2->shared(1)->store(); + + $t->delete_ok( "//$auth_userid:$password@/api/v1/advancededitormacros/shared/$macro_2_id") + ->status_is(403, 'Cannot delete other users shared macro without permission'); + + $builder->build({ + source => 'UserPermission', + value => { + borrowernumber => $authorized_patron->borrowernumber, + module_bit => 9, + code => 'delete_shared_macros', + }, + }); + $t->delete_ok( "//$auth_userid:$password@/api/v1/advancededitormacros/$macro_2_id") + ->status_is(403, 'Cannot delete other users shared macro with permission on private endpoint'); + $t->delete_ok( "//$auth_userid:$password@/api/v1/advancededitormacros/shared/$macro_2_id") + ->status_is(200, 'Can delete other users shared macro with permission'); + +}; + +$schema->storage->txn_rollback; -- 2.11.0