Bug 14440: get_template_and_user can not have an empty template_name (opac-ratings.pl)
[koha.git] / opac / oai.pl
1 #!/usr/bin/perl
2
3 # Copyright Biblibre 2008
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
21 use strict;
22 use warnings;
23
24 use CGI qw( :standard -oldstyle_urls -utf8 );
25 use vars qw( $GZIP );
26 use C4::Context;
27
28
29 BEGIN {
30     eval { require PerlIO::gzip };
31     $GZIP = ($@) ? 0 : 1;
32 }
33
34 unless ( C4::Context->preference('OAI-PMH') ) {
35     print
36         header(
37             -type       => 'text/plain; charset=utf-8',
38             -charset    => 'utf-8',
39             -status     => '404 OAI-PMH service is disabled',
40         ),
41         "OAI-PMH service is disabled";
42     exit;
43 }
44
45 my @encodings = http('HTTP_ACCEPT_ENCODING');
46 if ( $GZIP && grep { defined($_) && $_ eq 'gzip' } @encodings ) {
47     print header(
48         -type               => 'text/xml; charset=utf-8',
49         -charset            => 'utf-8',
50         -Content-Encoding   => 'gzip',
51     );
52     binmode( STDOUT, ":gzip" );
53 }
54 else {
55     print header(
56         -type       => 'text/xml; charset=utf-8',
57         -charset    => 'utf-8',
58     );
59 }
60
61 binmode STDOUT, ':encoding(UTF-8)';
62 my $repository = C4::OAI::Repository->new();
63
64 # __END__ Main Prog
65
66
67 #
68 # Extends HTTP::OAI::ResumptionToken
69 # A token is identified by:
70 # - metadataPrefix
71 # - from
72 # - until
73 # - offset
74 #
75 package C4::OAI::ResumptionToken;
76
77 use strict;
78 use warnings;
79 use HTTP::OAI;
80
81 use base ("HTTP::OAI::ResumptionToken");
82
83
84 sub new {
85     my ($class, %args) = @_;
86
87     my $self = $class->SUPER::new(%args);
88
89     my ($metadata_prefix, $offset, $from, $until, $set);
90     if ( $args{ resumptionToken } ) {
91         ($metadata_prefix, $offset, $from, $until, $set)
92             = split( '/', $args{resumptionToken} );
93     }
94     else {
95         $metadata_prefix = $args{ metadataPrefix };
96         $from = $args{ from } || '1970-01-01';
97         $until = $args{ until };
98         unless ( $until) {
99             my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime( time );
100             $until = sprintf( "%.4d-%.2d-%.2d", $year+1900, $mon+1,$mday );
101         }
102         #Add times to the arguments, when necessary, so they correctly match against the DB timestamps
103         $from .= 'T00:00:00Z' if length($from) == 10;
104         $until .= 'T23:59:59Z' if length($until) == 10;
105         $offset = $args{ offset } || 0;
106         $set = $args{set};
107     }
108
109     $self->{ metadata_prefix } = $metadata_prefix;
110     $self->{ offset          } = $offset;
111     $self->{ from            } = $from;
112     $self->{ until           } = $until;
113     $self->{ set             } = $set;
114     $self->{ from_arg        } = _strip_UTC_designators($from);
115     $self->{ until_arg       } = _strip_UTC_designators($until);
116
117     $self->resumptionToken(
118         join( '/', $metadata_prefix, $offset, $from, $until, $set ) );
119     $self->cursor( $offset );
120
121     return $self;
122 }
123
124 sub _strip_UTC_designators {
125     my ( $timestamp ) = @_;
126     $timestamp =~ s/T/ /g;
127     $timestamp =~ s/Z//g;
128     return $timestamp;
129 }
130
131 # __END__ C4::OAI::ResumptionToken
132
133
134
135 package C4::OAI::Identify;
136
137 use strict;
138 use warnings;
139 use HTTP::OAI;
140 use C4::Context;
141
142 use base ("HTTP::OAI::Identify");
143
144 sub new {
145     my ($class, $repository) = @_;
146
147     my ($baseURL) = $repository->self_url() =~ /(.*)\?.*/;
148     my $self = $class->SUPER::new(
149         baseURL             => $baseURL,
150         repositoryName      => C4::Context->preference("LibraryName"),
151         adminEmail          => C4::Context->preference("KohaAdminEmailAddress"),
152         MaxCount            => C4::Context->preference("OAI-PMH:MaxCount"),
153         granularity         => 'YYYY-MM-DD',
154         earliestDatestamp   => '0001-01-01',
155         deletedRecord       => 'no',
156     );
157
158     # FIXME - alas, the description element is not so simple; to validate
159     # against the OAI-PMH schema, it cannot contain just a string,
160     # but one or more elements that validate against another XML schema.
161     # For now, simply omitting it.
162     # $self->description( "Koha OAI Repository" );
163
164     $self->compression( 'gzip' );
165
166     return $self;
167 }
168
169 # __END__ C4::OAI::Identify
170
171
172
173 package C4::OAI::ListMetadataFormats;
174
175 use strict;
176 use warnings;
177 use HTTP::OAI;
178
179 use base ("HTTP::OAI::ListMetadataFormats");
180
181 sub new {
182     my ($class, $repository) = @_;
183
184     my $self = $class->SUPER::new();
185
186     if ( $repository->{ conf } ) {
187         foreach my $name ( @{ $repository->{ koha_metadata_format } } ) {
188             my $format = $repository->{ conf }->{ format }->{ $name };
189             $self->metadataFormat( HTTP::OAI::MetadataFormat->new(
190                 metadataPrefix    => $format->{metadataPrefix},
191                 schema            => $format->{schema},
192                 metadataNamespace => $format->{metadataNamespace}, ) );
193         }
194     }
195     else {
196         $self->metadataFormat( HTTP::OAI::MetadataFormat->new(
197             metadataPrefix    => 'oai_dc',
198             schema            => 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd',
199             metadataNamespace => 'http://www.openarchives.org/OAI/2.0/oai_dc/'
200         ) );
201         $self->metadataFormat( HTTP::OAI::MetadataFormat->new(
202             metadataPrefix    => 'marcxml',
203             schema            => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/ standards/marcxml/schema/MARC21slim.xsd',
204             metadataNamespace => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/ standards/marcxml/schema/MARC21slim'
205         ) );
206     }
207
208     return $self;
209 }
210
211 # __END__ C4::OAI::ListMetadataFormats
212
213
214
215 package C4::OAI::Record;
216
217 use strict;
218 use warnings;
219 use HTTP::OAI;
220 use HTTP::OAI::Metadata::OAI_DC;
221
222 use base ("HTTP::OAI::Record");
223
224 sub new {
225     my ($class, $repository, $marcxml, $timestamp, $setSpecs, %args) = @_;
226
227     my $self = $class->SUPER::new(%args);
228
229     $timestamp =~ s/ /T/, $timestamp .= 'Z';
230     $self->header( new HTTP::OAI::Header(
231         identifier  => $args{identifier},
232         datestamp   => $timestamp,
233     ) );
234
235     foreach my $setSpec (@$setSpecs) {
236         $self->header->setSpec($setSpec);
237     }
238
239     my $parser = XML::LibXML->new();
240     my $record_dom = $parser->parse_string( $marcxml );
241     my $format =  $args{metadataPrefix};
242     if ( $format ne 'marcxml' ) {
243         my %args = (
244             OPACBaseURL => "'" . C4::Context->preference('OPACBaseURL') . "'"
245         );
246         $record_dom = $repository->stylesheet($format)->transform($record_dom, %args);
247     }
248     $self->metadata( HTTP::OAI::Metadata->new( dom => $record_dom ) );
249
250     return $self;
251 }
252
253 # __END__ C4::OAI::Record
254
255
256
257 package C4::OAI::GetRecord;
258
259 use strict;
260 use warnings;
261 use HTTP::OAI;
262 use C4::OAI::Sets;
263
264 use base ("HTTP::OAI::GetRecord");
265
266
267 sub new {
268     my ($class, $repository, %args) = @_;
269
270     my $self = HTTP::OAI::GetRecord->new(%args);
271
272     my $dbh = C4::Context->dbh;
273     my $sth = $dbh->prepare("
274         SELECT marcxml, timestamp
275         FROM   biblioitems
276         WHERE  biblionumber=? " );
277     my $prefix = $repository->{koha_identifier} . ':';
278     my ($biblionumber) = $args{identifier} =~ /^$prefix(.*)/;
279     $sth->execute( $biblionumber );
280     my ($marcxml, $timestamp);
281     unless ( ($marcxml, $timestamp) = $sth->fetchrow ) {
282         return HTTP::OAI::Response->new(
283             requestURL  => $repository->self_url(),
284             errors      => [ new HTTP::OAI::Error(
285                 code    => 'idDoesNotExist',
286                 message => "There is no biblio record with this identifier",
287                 ) ] ,
288         );
289     }
290
291     my $oai_sets = GetOAISetsBiblio($biblionumber);
292     my @setSpecs;
293     foreach (@$oai_sets) {
294         push @setSpecs, $_->{spec};
295     }
296
297     #$self->header( HTTP::OAI::Header->new( identifier  => $args{identifier} ) );
298     $self->record( C4::OAI::Record->new(
299         $repository, $marcxml, $timestamp, \@setSpecs, %args ) );
300
301     return $self;
302 }
303
304 # __END__ C4::OAI::GetRecord
305
306
307
308 package C4::OAI::ListIdentifiers;
309
310 use strict;
311 use warnings;
312 use HTTP::OAI;
313 use C4::OAI::Sets;
314
315 use base ("HTTP::OAI::ListIdentifiers");
316
317
318 sub new {
319     my ($class, $repository, %args) = @_;
320
321     my $self = HTTP::OAI::ListIdentifiers->new(%args);
322
323     my $token = new C4::OAI::ResumptionToken( %args );
324     my $dbh = C4::Context->dbh;
325     my $set;
326     if(defined $token->{'set'}) {
327         $set = GetOAISetBySpec($token->{'set'});
328     }
329     my $max = $repository->{koha_max_count};
330     my $sql = "
331         SELECT biblioitems.biblionumber, biblioitems.timestamp
332         FROM biblioitems
333     ";
334     $sql .= " JOIN oai_sets_biblios ON biblioitems.biblionumber = oai_sets_biblios.biblionumber " if defined $set;
335     $sql .= " WHERE timestamp >= ? AND timestamp <= ? ";
336     $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set;
337     $sql .= "
338         LIMIT " . ($max+1) . "
339         OFFSET $token->{offset}
340     ";
341     my $sth = $dbh->prepare( $sql );
342     my @bind_params = ($token->{'from_arg'}, $token->{'until_arg'});
343     push @bind_params, $set->{'id'} if defined $set;
344     $sth->execute( @bind_params );
345
346     my $count = 0;
347     while ( my ($biblionumber, $timestamp) = $sth->fetchrow ) {
348         $count++;
349         if ( $count > $max ) {
350             $self->resumptionToken(
351                 new C4::OAI::ResumptionToken(
352                     metadataPrefix  => $token->{metadata_prefix},
353                     from            => $token->{from},
354                     until           => $token->{until},
355                     offset          => $token->{offset} + $max,
356                     set             => $token->{set}
357                 )
358             );
359             last;
360         }
361         $timestamp =~ s/ /T/, $timestamp .= 'Z';
362         $self->identifier( new HTTP::OAI::Header(
363             identifier => $repository->{ koha_identifier} . ':' . $biblionumber,
364             datestamp  => $timestamp,
365         ) );
366     }
367
368     return $self;
369 }
370
371 # __END__ C4::OAI::ListIdentifiers
372
373 package C4::OAI::Description;
374
375 use strict;
376 use warnings;
377 use HTTP::OAI;
378 use HTTP::OAI::SAXHandler qw/ :SAX /;
379
380 sub new {
381     my ( $class, %args ) = @_;
382
383     my $self = {};
384
385     if(my $setDescription = $args{setDescription}) {
386         $self->{setDescription} = $setDescription;
387     }
388     if(my $handler = $args{handler}) {
389         $self->{handler} = $handler;
390     }
391
392     bless $self, $class;
393     return $self;
394 }
395
396 sub set_handler {
397     my ( $self, $handler ) = @_;
398
399     $self->{handler} = $handler if $handler;
400
401     return $self;
402 }
403
404 sub generate {
405     my ( $self ) = @_;
406
407     g_data_element($self->{handler}, 'http://www.openarchives.org/OAI/2.0/', 'setDescription', {}, $self->{setDescription});
408
409     return $self;
410 }
411
412 # __END__ C4::OAI::Description
413
414 package C4::OAI::ListSets;
415
416 use strict;
417 use warnings;
418 use HTTP::OAI;
419 use C4::OAI::Sets;
420
421 use base ("HTTP::OAI::ListSets");
422
423 sub new {
424     my ( $class, $repository, %args ) = @_;
425
426     my $self = HTTP::OAI::ListSets->new(%args);
427
428     my $token = C4::OAI::ResumptionToken->new(%args);
429     my $sets = GetOAISets;
430     my $pos = 0;
431     foreach my $set (@$sets) {
432         if ($pos < $token->{offset}) {
433             $pos++;
434             next;
435         }
436         my @descriptions;
437         foreach my $desc (@{$set->{'descriptions'}}) {
438             push @descriptions, C4::OAI::Description->new(
439                 setDescription => $desc,
440             );
441         }
442         $self->set(
443             HTTP::OAI::Set->new(
444                 setSpec => $set->{'spec'},
445                 setName => $set->{'name'},
446                 setDescription => \@descriptions,
447             )
448         );
449         $pos++;
450         last if ($pos + 1 - $token->{offset}) > $repository->{koha_max_count};
451     }
452
453     $self->resumptionToken(
454         new C4::OAI::ResumptionToken(
455             metadataPrefix => $token->{metadata_prefix},
456             offset         => $pos
457         )
458     ) if ( $pos > $token->{offset} );
459
460     return $self;
461 }
462
463 # __END__ C4::OAI::ListSets;
464
465 package C4::OAI::ListRecords;
466
467 use strict;
468 use warnings;
469 use HTTP::OAI;
470 use C4::OAI::Sets;
471
472 use base ("HTTP::OAI::ListRecords");
473
474
475 sub new {
476     my ($class, $repository, %args) = @_;
477
478     my $self = HTTP::OAI::ListRecords->new(%args);
479
480     my $token = new C4::OAI::ResumptionToken( %args );
481     my $dbh = C4::Context->dbh;
482     my $set;
483     if(defined $token->{'set'}) {
484         $set = GetOAISetBySpec($token->{'set'});
485     }
486     my $max = $repository->{koha_max_count};
487     my $sql = "
488         SELECT biblioitems.biblionumber, biblioitems.marcxml, biblioitems.timestamp
489         FROM biblioitems
490     ";
491     $sql .= " JOIN oai_sets_biblios ON biblioitems.biblionumber = oai_sets_biblios.biblionumber " if defined $set;
492     $sql .= " WHERE timestamp >= ? AND timestamp <= ? ";
493     $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set;
494     $sql .= "
495         LIMIT " . ($max + 1) . "
496         OFFSET $token->{offset}
497     ";
498
499     my $sth = $dbh->prepare( $sql );
500     my @bind_params = ($token->{'from_arg'}, $token->{'until_arg'});
501     push @bind_params, $set->{'id'} if defined $set;
502     $sth->execute( @bind_params );
503
504     my $count = 0;
505     while ( my ($biblionumber, $marcxml, $timestamp) = $sth->fetchrow ) {
506         $count++;
507         if ( $count > $max ) {
508             $self->resumptionToken(
509                 new C4::OAI::ResumptionToken(
510                     metadataPrefix  => $token->{metadata_prefix},
511                     from            => $token->{from},
512                     until           => $token->{until},
513                     offset          => $token->{offset} + $max,
514                     set             => $token->{set}
515                 )
516             );
517             last;
518         }
519         my $oai_sets = GetOAISetsBiblio($biblionumber);
520         my @setSpecs;
521         foreach (@$oai_sets) {
522             push @setSpecs, $_->{spec};
523         }
524         $self->record( C4::OAI::Record->new(
525             $repository, $marcxml, $timestamp, \@setSpecs,
526             identifier      => $repository->{ koha_identifier } . ':' . $biblionumber,
527             metadataPrefix  => $token->{metadata_prefix}
528         ) );
529     }
530
531     return $self;
532 }
533
534 # __END__ C4::OAI::ListRecords
535
536
537
538 package C4::OAI::Repository;
539
540 use base ("HTTP::OAI::Repository");
541
542 use strict;
543 use warnings;
544
545 use HTTP::OAI;
546 use HTTP::OAI::Repository qw/:validate/;
547
548 use XML::SAX::Writer;
549 use XML::LibXML;
550 use XML::LibXSLT;
551 use YAML::Syck qw( LoadFile );
552 use CGI qw/:standard -oldstyle_urls/;
553
554 use C4::Context;
555 use C4::Biblio;
556
557
558 sub new {
559     my ($class, %args) = @_;
560     my $self = $class->SUPER::new(%args);
561
562     $self->{ koha_identifier      } = C4::Context->preference("OAI-PMH:archiveID");
563     $self->{ koha_max_count       } = C4::Context->preference("OAI-PMH:MaxCount");
564     $self->{ koha_metadata_format } = ['oai_dc', 'marcxml'];
565     $self->{ koha_stylesheet      } = { }; # Build when needed
566
567     # Load configuration file if defined in OAI-PMH:ConfFile syspref
568     if ( my $file = C4::Context->preference("OAI-PMH:ConfFile") ) {
569         $self->{ conf } = LoadFile( $file );
570         my @formats = keys %{ $self->{conf}->{format} };
571         $self->{ koha_metadata_format } =  \@formats;
572     }
573
574     # Check for grammatical errors in the request
575     my @errs = validate_request( CGI::Vars() );
576
577     # Is metadataPrefix supported by the respository?
578     my $mdp = param('metadataPrefix') || '';
579     if ( $mdp && !grep { $_ eq $mdp } @{$self->{ koha_metadata_format }} ) {
580         push @errs, new HTTP::OAI::Error(
581             code    => 'cannotDisseminateFormat',
582             message => "Dissemination as '$mdp' is not supported",
583         );
584     }
585
586     my $response;
587     if ( @errs ) {
588         $response = HTTP::OAI::Response->new(
589             requestURL  => self_url(),
590             errors      => \@errs,
591         );
592     }
593     else {
594         my %attr = CGI::Vars();
595         my $verb = delete( $attr{verb} );
596         if ( $verb eq 'ListSets' ) {
597             $response = C4::OAI::ListSets->new($self, %attr);
598         }
599         elsif ( $verb eq 'Identify' ) {
600             $response = C4::OAI::Identify->new( $self );
601         }
602         elsif ( $verb eq 'ListMetadataFormats' ) {
603             $response = C4::OAI::ListMetadataFormats->new( $self );
604         }
605         elsif ( $verb eq 'GetRecord' ) {
606             $response = C4::OAI::GetRecord->new( $self, %attr );
607         }
608         elsif ( $verb eq 'ListRecords' ) {
609             $response = C4::OAI::ListRecords->new( $self, %attr );
610         }
611         elsif ( $verb eq 'ListIdentifiers' ) {
612             $response = C4::OAI::ListIdentifiers->new( $self, %attr );
613         }
614     }
615
616     $response->set_handler( XML::SAX::Writer->new( Output => *STDOUT ) );
617     $response->generate;
618
619     bless $self, $class;
620     return $self;
621 }
622
623
624 sub stylesheet {
625     my ( $self, $format ) = @_;
626
627     my $stylesheet = $self->{ koha_stylesheet }->{ $format };
628     unless ( $stylesheet ) {
629         my $xsl_file = $self->{ conf }
630                        ? $self->{ conf }->{ format }->{ $format }->{ xsl_file }
631                        : ( C4::Context->config('intrahtdocs') .
632                          '/prog/en/xslt/' .
633                          C4::Context->preference('marcflavour') .
634                          'slim2OAIDC.xsl' );
635         my $parser = XML::LibXML->new();
636         my $xslt = XML::LibXSLT->new();
637         my $style_doc = $parser->parse_file( $xsl_file );
638         $stylesheet = $xslt->parse_stylesheet( $style_doc );
639         $self->{ koha_stylesheet }->{ $format } = $stylesheet;
640     }
641
642     return $stylesheet;
643 }
644
645
646
647 =head1 NAME
648
649 C4::OAI::Repository - Handles OAI-PMH requests for a Koha database.
650
651 =head1 SYNOPSIS
652
653   use C4::OAI::Repository;
654
655   my $repository = C4::OAI::Repository->new();
656
657 =head1 DESCRIPTION
658
659 This object extend HTTP::OAI::Repository object.
660 It accepts OAI-PMH HTTP requests and returns result.
661
662 This OAI-PMH server can operate in a simple mode and extended one.
663
664 In simple mode, repository configuration comes entirely from Koha system
665 preferences (OAI-PMH:archiveID and OAI-PMH:MaxCount) and the server returns
666 records in marcxml or dublin core format. Dublin core records are created from
667 koha marcxml records tranformed with XSLT. Used XSL file is located in
668 koha-tmpl/intranet-tmpl/prog/en/xslt directory and choosed based on marcflavour,
669 respecively MARC21slim2OAIDC.xsl for MARC21 and  MARC21slim2OAIDC.xsl for
670 UNIMARC.
671
672 In extende mode, it's possible to parameter other format than marcxml or Dublin
673 Core. A new syspref OAI-PMH:ConfFile specify a YAML configuration file which
674 list available metadata formats and XSL file used to create them from marcxml
675 records. If this syspref isn't set, Koha OAI server works in simple mode. A
676 configuration file koha-oai.conf can look like that:
677
678   ---
679   format:
680     vs:
681       metadataPrefix: vs
682       metadataNamespace: http://veryspecial.tamil.fr/vs/format-pivot/1.1/vs
683       schema: http://veryspecial.tamil.fr/vs/format-pivot/1.1/vs.xsd
684       xsl_file: /usr/local/koha/xslt/vs.xsl
685     marcxml:
686       metadataPrefix: marxml
687       metadataNamespace: http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim
688       schema: http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd
689     oai_dc:
690       metadataPrefix: oai_dc
691       metadataNamespace: http://www.openarchives.org/OAI/2.0/oai_dc/
692       schema: http://www.openarchives.org/OAI/2.0/oai_dc.xsd
693       xsl_file: /usr/local/koha/koha-tmpl/intranet-tmpl/xslt/UNIMARCslim2OAIDC.xsl
694
695 =cut
696
697
698