Bug 32336: Fix encoding of MARCXML output for REST API (UNIMARC)
[koha.git] / Koha / REST / V1 / Biblios.pm
1 package Koha::REST::V1::Biblios;
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18 use Modern::Perl;
19
20 use Mojo::Base 'Mojolicious::Controller';
21
22 use Koha::Biblios;
23 use Koha::Ratings;
24 use Koha::RecordProcessor;
25 use C4::Biblio qw( DelBiblio AddBiblio ModBiblio );
26 use C4::Search qw( FindDuplicate );
27
28 use List::MoreUtils qw( any );
29 use MARC::Record::MiJ;
30
31 use Try::Tiny qw( catch try );
32
33 =head1 API
34
35 =head2 Methods
36
37 =head3 get
38
39 Controller function that handles retrieving a single biblio object
40
41 =cut
42
43 sub get {
44     my $c = shift->openapi->valid_input or return;
45
46     my $attributes;
47     $attributes = { prefetch => [ 'metadata' ] } # don't prefetch metadata if not needed
48         unless $c->req->headers->accept =~ m/application\/json/;
49
50     my $biblio = Koha::Biblios->find( { biblionumber => $c->validation->param('biblio_id') }, $attributes );
51
52     unless ( $biblio ) {
53         return $c->render(
54             status  => 404,
55             openapi => {
56                 error => "Object not found."
57             }
58         );
59     }
60
61     return try {
62
63         if ( $c->req->headers->accept =~ m/application\/json/ ) {
64             return $c->render(
65                 status => 200,
66                 json   => $biblio->to_api
67             );
68         }
69         else {
70             my $record = $biblio->metadata->record;
71             my $marcflavour = C4::Context->preference("marcflavour");
72
73             $c->respond_to(
74                 marcxml => {
75                     status => 200,
76                     format => 'marcxml',
77                     text   => $record->as_xml_record($marcflavour),
78                 },
79                 mij => {
80                     status => 200,
81                     format => 'mij',
82                     data   => $record->to_mij
83                 },
84                 marc => {
85                     status => 200,
86                     format => 'marc',
87                     text   => $record->as_usmarc
88                 },
89                 txt => {
90                     status => 200,
91                     format => 'text/plain',
92                     text   => $record->as_formatted
93                 },
94                 any => {
95                     status  => 406,
96                     openapi => [
97                         "application/json",
98                         "application/marcxml+xml",
99                         "application/marc-in-json",
100                         "application/marc",
101                         "text/plain"
102                     ]
103                 }
104             );
105         }
106     }
107     catch {
108         $c->unhandled_exception($_);
109     };
110 }
111
112 =head3 delete
113
114 Controller function that handles deleting a biblio object
115
116 =cut
117
118 sub delete {
119     my $c = shift->openapi->valid_input or return;
120
121     my $biblio = Koha::Biblios->find( $c->validation->param('biblio_id') );
122
123     if ( not defined $biblio ) {
124         return $c->render(
125             status  => 404,
126             openapi => { error => "Object not found" }
127         );
128     }
129
130     return try {
131         my $error = DelBiblio( $biblio->id );
132
133         if ($error) {
134             return $c->render(
135                 status  => 409,
136                 openapi => { error => $error }
137             );
138         }
139         else {
140             return $c->render( status => 204, openapi => "" );
141         }
142     }
143     catch {
144         $c->unhandled_exception($_);
145     };
146 }
147
148 =head3 get_public
149
150 Controller function that handles retrieving a single biblio object
151
152 =cut
153
154 sub get_public {
155     my $c = shift->openapi->valid_input or return;
156
157     my $biblio = Koha::Biblios->find(
158         { biblionumber => $c->validation->param('biblio_id') },
159         { prefetch     => ['metadata'] } );
160
161     unless ($biblio) {
162         return $c->render(
163             status  => 404,
164             openapi => {
165                 error => "Object not found."
166             }
167         );
168     }
169
170     return try {
171
172         my $record = $biblio->metadata->record;
173
174         my $opachiddenitems_rules = C4::Context->yaml_preference('OpacHiddenItems');
175         my $patron = $c->stash('koha.user');
176
177         # Check if the biblio should be hidden for unprivileged access
178         # unless there's a logged in user, and there's an exception for it's
179         # category
180         unless ( $patron and $patron->category->override_hidden_items ) {
181             if ( $biblio->hidden_in_opac({ rules => $opachiddenitems_rules }) )
182             {
183                 return $c->render(
184                     status  => 404,
185                     openapi => {
186                         error => "Object not found."
187                     }
188                 );
189             }
190         }
191
192         my $marcflavour = C4::Context->preference("marcflavour");
193
194         my $record_processor = Koha::RecordProcessor->new({
195             filters => 'ViewPolicy',
196             options => {
197                 interface => 'opac',
198                 frameworkcode => $biblio->frameworkcode
199             }
200         });
201         # Apply framework's filtering to MARC::Record object
202         $record_processor->process($record);
203
204         $c->respond_to(
205             marcxml => {
206                 status => 200,
207                 format => 'marcxml',
208                 text   => $record->as_xml_record($marcflavour),
209             },
210             mij => {
211                 status => 200,
212                 format => 'mij',
213                 data   => $record->to_mij
214             },
215             marc => {
216                 status => 200,
217                 format => 'marc',
218                 text   => $record->as_usmarc
219             },
220             txt => {
221                 status => 200,
222                 format => 'text/plain',
223                 text   => $record->as_formatted
224             },
225             any => {
226                 status  => 406,
227                 openapi => [
228                     "application/marcxml+xml",
229                     "application/marc-in-json",
230                     "application/marc",
231                     "text/plain"
232                 ]
233             }
234         );
235     }
236     catch {
237         $c->unhandled_exception($_);
238     };
239 }
240
241 =head3 get_items
242
243 Controller function that handles retrieving biblio's items
244
245 =cut
246
247 sub get_items {
248     my $c = shift->openapi->valid_input or return;
249
250     my $biblio = Koha::Biblios->find( { biblionumber => $c->validation->param('biblio_id') }, { prefetch => ['items'] } );
251
252     unless ( $biblio ) {
253         return $c->render(
254             status  => 404,
255             openapi => {
256                 error => "Object not found."
257             }
258         );
259     }
260
261     return try {
262
263         my $items_rs = $biblio->items;
264         my $items    = $c->objects->search( $items_rs );
265         return $c->render(
266             status  => 200,
267             openapi => $items
268         );
269     }
270     catch {
271         $c->unhandled_exception($_);
272     };
273 }
274
275 =head3 get_checkouts
276
277 List Koha::Checkout objects
278
279 =cut
280
281 sub get_checkouts {
282     my $c = shift->openapi->valid_input or return;
283
284     my $checked_in = delete $c->validation->output->{checked_in};
285
286     try {
287         my $biblio = Koha::Biblios->find( $c->validation->param('biblio_id') );
288
289         unless ($biblio) {
290             return $c->render(
291                 status  => 404,
292                 openapi => { error => 'Object not found' }
293             );
294         }
295
296         my $checkouts =
297           ($checked_in)
298           ? $c->objects->search( $biblio->old_checkouts )
299           : $c->objects->search( $biblio->current_checkouts );
300
301         return $c->render(
302             status  => 200,
303             openapi => $checkouts
304         );
305     }
306     catch {
307         $c->unhandled_exception($_);
308     };
309 }
310
311 =head3 pickup_locations
312
313 Method that returns the possible pickup_locations for a given biblio
314 used for building the dropdown selector
315
316 =cut
317
318 sub pickup_locations {
319     my $c = shift->openapi->valid_input or return;
320
321     my $biblio_id = $c->validation->param('biblio_id');
322     my $biblio = Koha::Biblios->find( $biblio_id );
323
324     unless ($biblio) {
325         return $c->render(
326             status  => 404,
327             openapi => { error => "Biblio not found" }
328         );
329     }
330
331     my $patron_id = delete $c->validation->output->{patron_id};
332     my $patron    = Koha::Patrons->find( $patron_id );
333
334     unless ($patron) {
335         return $c->render(
336             status  => 400,
337             openapi => { error => "Patron not found" }
338         );
339     }
340
341     return try {
342
343         my $pl_set = $biblio->pickup_locations( { patron => $patron } );
344
345         my @response = ();
346         if ( C4::Context->preference('AllowHoldPolicyOverride') ) {
347
348             my $libraries_rs = Koha::Libraries->search( { pickup_location => 1 } );
349             my $libraries    = $c->objects->search($libraries_rs);
350
351             @response = map {
352                 my $library = $_;
353                 $library->{needs_override} = (
354                     any { $_->branchcode eq $library->{library_id} }
355                     @{$pl_set->as_list}
356                   )
357                   ? Mojo::JSON->false
358                   : Mojo::JSON->true;
359                 $library;
360             } @{$libraries};
361         }
362         else {
363
364             my $pickup_locations = $c->objects->search($pl_set);
365             @response = map { $_->{needs_override} = Mojo::JSON->false; $_; } @{$pickup_locations};
366         }
367
368         return $c->render(
369             status  => 200,
370             openapi => \@response
371         );
372     }
373     catch {
374         $c->unhandled_exception($_);
375     };
376 }
377
378 =head3 get_items_public
379
380 Controller function that handles retrieving biblio's items, for unprivileged
381 access.
382
383 =cut
384
385 sub get_items_public {
386     my $c = shift->openapi->valid_input or return;
387
388     my $biblio = Koha::Biblios->find( { biblionumber => $c->validation->param('biblio_id') }, { prefetch => ['items'] } );
389
390     unless ( $biblio ) {
391         return $c->render(
392             status  => 404,
393             openapi => {
394                 error => "Object not found."
395             }
396         );
397     }
398
399     return try {
400
401         my $patron = $c->stash('koha.user');
402
403         my $items_rs = $biblio->items->filter_by_visible_in_opac({ patron => $patron });
404         my $items    = $c->objects->search( $items_rs );
405         return $c->render(
406             status  => 200,
407             openapi => $items
408         );
409     }
410     catch {
411         $c->unhandled_exception($_);
412     };
413 }
414
415 =head3 set_rating
416
417 Set rating for the logged in user
418
419 =cut
420
421
422 sub set_rating {
423     my $c = shift->openapi->valid_input or return;
424
425     my $biblio = Koha::Biblios->find( $c->validation->param('biblio_id') );
426
427     unless ($biblio) {
428         return $c->render(
429             status  => 404,
430             openapi => {
431                 error => "Object not found."
432             }
433         );
434     }
435
436     my $patron = $c->stash('koha.user');
437     unless ($patron) {
438         return $c->render(
439             status => 403,
440             openapi =>
441                 { error => "Cannot rate. Reason: must be logged-in" }
442         );
443     }
444
445     my $body   = $c->validation->param('body');
446     my $rating_value = $body->{rating};
447
448     return try {
449
450         my $rating = Koha::Ratings->find(
451             {
452                 biblionumber   => $biblio->biblionumber,
453                 borrowernumber => $patron->borrowernumber,
454             }
455         );
456         $rating->delete if $rating;
457
458         if ( $rating_value ) { # Cannot set to 0 from the UI
459             $rating = Koha::Rating->new(
460                 {
461                     biblionumber   => $biblio->biblionumber,
462                     borrowernumber => $patron->borrowernumber,
463                     rating_value   => $rating_value,
464                 }
465             )->store;
466         };
467         my $ratings =
468           Koha::Ratings->search( { biblionumber => $biblio->biblionumber } );
469         my $average = $ratings->get_avg_rating;
470
471         return $c->render(
472             status  => 200,
473             openapi => {
474                 rating  => $rating && $rating->in_storage ? $rating->rating_value : undef,
475                 average => $average,
476                 count   => $ratings->count
477             },
478         );
479     }
480     catch {
481         $c->unhandled_exception($_);
482     };
483 }
484
485 =head3 add
486
487 Controller function that handles creating a biblio object
488
489 =cut
490
491 sub add {
492     my $c = shift->openapi->valid_input or return;
493
494     try {
495         my $headers = $c->req->headers;
496
497         my $flavour = $headers->header('x-marc-schema');
498         $flavour //= C4::Context->preference('marcflavour');
499
500         my $record;
501
502         my $frameworkcode = $headers->header('x-framework-id');
503         my $content_type  = $headers->content_type;
504
505         if ( $content_type =~ m/application\/marcxml\+xml/ ) {
506             $record = MARC::Record->new_from_xml( $c->req->body, 'UTF-8', $flavour );
507         }
508         elsif ( $content_type =~ m/application\/marc-in-json/ ) {
509             $record = MARC::Record->new_from_mij_structure( $c->req->json );
510         }
511         elsif ( $content_type =~ m/application\/marc/ ) {
512             $record = MARC::Record->new_from_usmarc( $c->req->body );
513         }
514         else {
515             return $c->render(
516                 status  => 406,
517                 openapi => [
518                     "application/marcxml+xml",
519                     "application/marc-in-json",
520                     "application/marc"
521                 ]
522             );
523         }
524
525         my ( $duplicatebiblionumber, $duplicatetitle );
526             ( $duplicatebiblionumber, $duplicatetitle ) = FindDuplicate($record);
527
528         my $confirm_not_duplicate = $headers->header('x-confirm-not-duplicate');
529
530         return $c->render(
531             status  => 400,
532             openapi => {
533                 error => "Duplicate biblio $duplicatebiblionumber",
534             }
535         ) unless !$duplicatebiblionumber || $confirm_not_duplicate;
536
537         my ( $biblionumber, $oldbibitemnum );
538             ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
539
540         $c->render(
541             status  => 200,
542             openapi => { id => $biblionumber }
543         );
544     }
545     catch {
546         $c->unhandled_exception($_);
547     };
548 }
549
550 =head3 update
551
552 Controller function that handles modifying an biblio object
553
554 =cut
555
556 sub update {
557     my $c = shift->openapi->valid_input or return;
558
559     my $biblio_id = $c->param('biblio_id');
560     my $biblio    = Koha::Biblios->find($biblio_id);
561
562     if ( ! defined $biblio ) {
563         return $c->render(
564             status  => 404,
565             openapi => { error => "Object not found" }
566         );
567     }
568
569     try {
570         my $headers = $c->req->headers;
571
572         my $flavour = $headers->header('x-marc-schema');
573         $flavour //= C4::Context->preference('marcflavour');
574
575         my $frameworkcode = $headers->header('x-framework-id') || $biblio->frameworkcode;
576
577         my $content_type = $headers->content_type;
578
579         my $record;
580
581         if ( $content_type =~ m/application\/marcxml\+xml/ ) {
582             $record = MARC::Record->new_from_xml( $c->req->body, 'UTF-8', $flavour );
583         }
584         elsif ( $content_type =~ m/application\/marc-in-json/ ) {
585             $record = MARC::Record->new_from_mij_structure( $c->req->json );
586         }
587         elsif ( $content_type =~ m/application\/marc/ ) {
588             $record = MARC::Record->new_from_usmarc( $c->req->body );
589         }
590         else {
591             return $c->render(
592                 status  => 406,
593                 openapi => [
594                     "application/json",
595                     "application/marcxml+xml",
596                     "application/marc-in-json",
597                     "application/marc"
598                 ]
599             );
600         }
601
602         ModBiblio( $record, $biblio_id, $frameworkcode );
603
604         $c->render(
605             status  => 200,
606             openapi => { id => $biblio_id }
607         );
608     }
609     catch {
610         $c->unhandled_exception($_);
611     };
612 }
613
614 =head3 list
615
616 Controller function that handles retrieving a single biblio object
617
618 =cut
619
620 sub list {
621     my $c = shift->openapi->valid_input or return;
622
623     my $attributes;
624     $attributes =
625       { prefetch => ['metadata'] }    # don't prefetch metadata if not needed
626       unless $c->req->headers->accept =~ m/application\/json/;
627
628     my $biblios = $c->objects->search_rs( Koha::Biblios->new );
629
630     return try {
631
632         if ( $c->req->headers->accept =~ m/application\/json(;.*)?$/ ) {
633             return $c->render(
634                 status => 200,
635                 json   => $c->objects->to_api( $biblios ),
636             );
637         }
638         elsif (
639             $c->req->headers->accept =~ m/application\/marcxml\+xml(;.*)?$/ )
640         {
641             $c->res->headers->add( 'Content-Type', 'application/marcxml+xml' );
642             return $c->render(
643                 status => 200,
644                 text   => $biblios->print_collection('marcxml')
645             );
646         }
647         elsif (
648             $c->req->headers->accept =~ m/application\/marc-in-json(;.*)?$/ )
649         {
650             $c->res->headers->add( 'Content-Type', 'application/marc-in-json' );
651             return $c->render(
652                 status => 200,
653                 data   => $biblios->print_collection('mij')
654             );
655         }
656         elsif ( $c->req->headers->accept =~ m/application\/marc(;.*)?$/ ) {
657             $c->res->headers->add( 'Content-Type', 'application/marc' );
658             return $c->render(
659                 status => 200,
660                 text   => $biblios->print_collection('marc')
661             );
662         }
663         elsif ( $c->req->headers->accept =~ m/text\/plain(;.*)?$/ ) {
664             return $c->render(
665                 status => 200,
666                 text   => $biblios->print_collection('txt')
667             );
668         }
669         else {
670             return $c->render(
671                 status  => 406,
672                 openapi => [
673                     "application/json",         "application/marcxml+xml",
674                     "application/marc-in-json", "application/marc",
675                     "text/plain"
676                 ]
677             );
678         }
679     }
680     catch {
681         $c->unhandled_exception($_);
682     };
683 }
684
685 1;