View | Details | Raw Unified | Return to bug 40308
Collapse All | Expand All

(-)a/Koha/ExternalContent/OCLC.pm (-1 / +422 lines)
Line 0 Link Here
0
- 
1
package Koha::ExternalContent::OCLC;
2
3
# This file is part of Koha.
4
#
5
# Copyright (C) 2025 Dataly Tech
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
use Modern::Perl;
21
22
use C4::Context;
23
24
use Koha::Caches;
25
26
use Encode qw( decode );
27
use JSON   qw( from_json );
28
use LWP::UserAgent;
29
use MIME::Base64 qw( encode_base64 );
30
31
=head1 NAME
32
33
Koha::ExternalContent::OCLC
34
35
=head1 SYNOPSIS
36
37
A class that provides helper functions related to OCLC's APIs.
38
39
For more information on these APIs, please visit the following pages:
40
41
L<https://www.oclc.org/developer/api/oclc-apis.en.html>
42
43
L<https://developer.api.oclc.org/>
44
45
=head1 DESCRIPTION
46
47
This class contains common functions used when accessing OCLC's APIs.
48
49
Examples of such APIs include, but are not limited to:
50
51
=over
52
53
=item * Citations API
54
55
=item * Dewey Linked Data API
56
57
=item * Dewey Lookup API
58
59
=item * Discovery Library Profiles API
60
61
=item * Entity Search API
62
63
=item * FAST API
64
65
=item * New Titles API
66
67
=item * Suggestions API
68
69
=item * VIAF API
70
71
=item * WorldCat Discovery API
72
73
=item * WorldCat Entity Data
74
75
=item * WorldCat Metadata API
76
77
=back
78
79
Currently, this class has functions that allow use of the Dewey Linked Data and Dewey Lookup APIs.
80
81
=head1 FUNCTIONS
82
83
=head3 get_access_token
84
85
    my $access_token = get_access_token(
86
        {
87
            scope         => $scope,
88
            client_id     => $client_id,
89
            client_secret => $client_secret,
90
        }
91
    );
92
93
Requests an API access token from OCLC's OAuth 2.0 server, for a particular scope (such as C<deweyLinkedData>, C<WorldCatMetadataAPI>, etc.).
94
95
Tokens are valid for a specific time period, and to ensure re-usability within this timeframe they will be cached (in C<memcached>) using a unique key per scope and with an appropriate expiry set.
96
97
This request follows the Client Credentials Grant OAuth pattern, documented here: L<https://www.oclc.org/developer/api/keys/oauth/client-credentials-grant.en.html>
98
99
Essentially, an HTTP POST request to C<https://oauth.oclc.org/token> is performed, with the Base64-encoded Client ID and Client Secret components included in an C<Authorization: Basic> header.
100
101
WSKeys (or Web Service Keys) are documented here: L<https://www.oclc.org/developer/develop/authentication/what-is-a-wskey.en.html>
102
103
Instructions on how to request such a key can be found here: L<https://help.oclc.org/Librarian_Toolbox/OCLC_APIs/Get_started/Request_an_API_WSKey>
104
105
=head4 Parameters
106
107
=over 4
108
109
=item C<options> (required) - a hashref containing the following keys:
110
111
=over 8
112
113
=item C<scope> (string, required) - the service (i.e. scope) for which the client is requesting access (e.g. C<deweyLinkedData>, C<WorldCatMetadataAPI>, etc.)
114
115
=item C<client_id> (string, optional) - the Client ID component of your WSKey (falls back to the contents of the C<OCLCAPIWSKeyClientID> System Preference)
116
117
=item C<client_secret> (string, optional) - the Client Secret component of your WSKey (falls back to the contents of the C<OCLCAPIWSKeyClientSecret> System Preference)
118
119
=back
120
121
=back
122
123
=head4 Returns
124
125
=over 4
126
127
=item C<access_token> (string) - the API access token, or C<undef> if no token could be obtained
128
129
=item C<msg> (string) - the source of the token (API, cache), or the error message returned if no token could be obtained
130
131
=back
132
133
=head4 Related System Preferences
134
135
=over 4
136
137
=item C<OCLCAPIWSKeyClientID>
138
139
=item C<OCLCAPIWSKeyClientSecret>
140
141
=back
142
143
=cut
144
145
sub get_access_token {
146
147
    my ( $self, $args ) = @_;
148
149
    my $scope         = $args->{scope};
150
    my $client_id     = $args->{client_id}     ||= C4::Context->preference('OCLCAPIWSKeyClientID');
151
    my $client_secret = $args->{client_secret} ||= C4::Context->preference('OCLCAPIWSKeyClientSecret');
152
    my $cache         = Koha::Caches->get_instance;
153
154
    my $cached_access_token = $cache->get_from_cache( 'OCLC_access_token-' . $scope );
155
    if ($cached_access_token) {
156
        return ( $cached_access_token->{token}, 'cache' );
157
    } else {
158
        my $basic_auth = encode_base64( "$client_id:$client_secret", '' );
159
        my $ua         = LWP::UserAgent->new;
160
        my $method     = 'POST';
161
        my $uri        = "https://oauth.oclc.org/token?grant_type=client_credentials&scope=$scope";
162
        my $header     = [ 'Authorization' => "Basic $basic_auth" ];
163
        my $request    = HTTP::Request->new( $method, $uri, $header );
164
        my $response   = $ua->request($request);
165
        my $content    = Encode::decode( "utf8", $response->content );
166
        my $json       = from_json($content);
167
168
        if ( $response->is_success ) {
169
            $cache->set_in_cache(
170
                "OCLC_access_token-$scope",
171
                {
172
                    token      => $json->{access_token},
173
                    expires_at => $json->{expires_at},
174
                },
175
                { expiry => $json->{expires_in} }
176
            );
177
            return ( $json->{access_token}, 'API' );
178
        } else {
179
            return ( undef, $json->{code} . ' ' . $json->{message} );
180
        }
181
    }
182
183
}
184
185
=head3 get_ddc_uri
186
187
    my $ddc_uri = get_ddc_uri(
188
        {
189
            access_token => $access_token,
190
            ddc_number   => $ddc_number,
191
        }
192
    );
193
194
Requests a Linked Data ID (URI) for a particular DDC number from OCLC's Dewey Linked Data API.
195
196
The API is documented here: L<https://developer.api.oclc.org/dewey-ld>
197
198
=head4 Parameters
199
200
=over 4
201
202
=item C<options> (required) - a hashref containing the following keys:
203
204
=over 8
205
206
=item C<access_token> (string, required) - the access_token to use when making the API request
207
208
=item C<ddc_number> (string, required) - the DDC number to look up
209
210
=back
211
212
=back
213
214
=head4 Returns
215
216
=over 4
217
218
=item C<uri> (string) - the Linked Data ID (URI) associated with the DDC number requested, or C<undef> if no URI could be obtained
219
220
=item C<msg> (string) - the source of the URI (API, cache), or the error message returned if no token could be obtained
221
222
=back
223
224
=cut
225
226
sub get_ddc_uri {
227
228
    my ( $self, $args ) = @_;
229
230
    my $access_token = $args->{access_token};
231
    my $ddc_number   = $args->{ddc_number};
232
    my $cache        = Koha::Caches->get_instance;
233
234
    my $cached_uri = $cache->get_from_cache("$ddc_number-uri");
235
    if ($cached_uri) {
236
        return ( $cached_uri, 'cache' );
237
    } else {
238
        my $ua       = LWP::UserAgent->new;
239
        my $method   = 'GET';
240
        my $url      = "https://id.oclc.org/worldcat/ddc/api/id?ddc=$ddc_number";
241
        my $header   = [ 'Authorization' => "Bearer $access_token" ];
242
        my $request  = HTTP::Request->new( $method, $url, $header );
243
        my $response = $ua->request($request);
244
        my $content  = Encode::decode( "utf8", $response->content );
245
        my $json     = from_json($content);
246
247
        if ( $response->is_success ) {
248
            if ( $json->{$ddc_number} ) {
249
                $cache->set_in_cache( "$ddc_number-uri", $json->{$ddc_number} );
250
                return ( $json->{$ddc_number}, 'API' );
251
            } else {
252
                return ( undef, 'not found' );
253
            }
254
        } else {
255
            return ( undef, $json->{message} );
256
        }
257
    }
258
259
}
260
261
=head3 get_ddc_url
262
263
    my $ddc_url = get_ddc_url(
264
        {
265
            access_token => $access_token,
266
            ddc_number   => $ddc_number,
267
        }
268
    );
269
270
Requests a Linked Data URL for a particular DDC number from OCLC's Dewey Linked Data API.
271
272
The API is documented here: L<https://developer.api.oclc.org/dewey-ld>
273
274
=head4 Parameters
275
276
=over 4
277
278
=item C<options> (required) - a hashref containing the following keys:
279
280
=over 8
281
282
=item C<access_token> (string, required) - the access_token to use when making the API request
283
284
=item C<ddc_number> (string, required) - the DDC number to look up
285
286
=back
287
288
=back
289
290
=head4 Returns
291
292
=over 4
293
294
=item C<url> (string) - the Linked Data URL associated with the DDC number requested, or C<undef> if no URL could be obtained
295
296
=item C<msg> (string) - the source of the URL (API, cache), or the error message returned if no token could be obtained
297
298
=back
299
300
=cut
301
302
sub get_ddc_url {
303
304
    my ( $self, $args ) = @_;
305
306
    my $access_token = $args->{access_token};
307
    my $ddc_number   = $args->{ddc_number};
308
    my $cache        = Koha::Caches->get_instance;
309
310
    my $cached_url = $cache->get_from_cache("$ddc_number-url");
311
    if ($cached_url) {
312
        return ( $cached_url, 'cache' );
313
    } else {
314
        my $ua       = LWP::UserAgent->new;
315
        my $method   = 'GET';
316
        my $url      = "https://id.oclc.org/worldcat/ddc/api/url?ddc=$ddc_number";
317
        my $header   = [ 'Authorization' => "Bearer $access_token" ];
318
        my $request  = HTTP::Request->new( $method, $url, $header );
319
        my $response = $ua->request($request);
320
        my $content  = Encode::decode( "utf8", $response->content );
321
        my $json     = from_json($content);
322
323
        if ( $response->is_success ) {
324
            $cache->set_in_cache( "$ddc_number-url", $json->{$ddc_number} );
325
            return ( $json->{$ddc_number}, 'API' );
326
        } else {
327
            return ( undef, $json->{message} );
328
        }
329
    }
330
331
}
332
333
=head3 get_ddc_description
334
335
    my $ddc_description = get_ddc_description(
336
        {
337
            access_token => $access_token,
338
            ddc_uri      => $ddc_uri,
339
          [ language     => $language, ]
340
        }
341
    );
342
343
Requests a Linked Data Description for a particular DDC number from OCLC's Dewey Linked Data API.
344
345
The API is documented here: L<https://developer.api.oclc.org/dewey-ld>
346
347
=head4 Parameters
348
349
=over 4
350
351
=item C<options> (required) - a hashref containing the following keys:
352
353
=over 8
354
355
=item C<access_token> (string, required) - the access_token to use when making the API request
356
357
=item C<ddc_uri> (string, required) - the DDC URI to look up
358
359
=item C<language> (string, optional) - the language of the description to obtain (falls back to the setting of the C<OCLCDeweyLinkedDataLanguage> System Preference)
360
361
=back
362
363
=back
364
365
=head4 Returns
366
367
=over 4
368
369
=item C<description> (string) - the Linked Data Description associated with the DDC URI requested, or C<undef> if no Description could be obtained
370
371
=item C<msg> (string) - the source of the LD Description (API, cache), or the error message returned if no token could be obtained
372
373
=back
374
375
=head4 Related System Preferences
376
377
=over 4
378
379
=item C<OCLCDeweyLinkedDataLanguage>
380
381
=back
382
383
=cut
384
385
sub get_ddc_description {
386
387
    my ( $self, $args ) = @_;
388
389
    my $access_token = $args->{access_token};
390
    my $ddc_uri      = $args->{ddc_uri};
391
    my $language     = $args->{language} ||= C4::Context->preference('OCLCDeweyLinkedDataLanguage');
392
    my $cache        = Koha::Caches->get_instance;
393
394
    my $cached_description = $cache->get_from_cache("$ddc_uri-description-$language");
395
    if ($cached_description) {
396
        return ( $cached_description, 'cache' );
397
    } else {
398
        my $ua       = LWP::UserAgent->new;
399
        my $method   = 'GET';
400
        my $url      = "https://id.oclc.org/worldcat/ddc/$ddc_uri";
401
        my $header   = [ 'Authorization' => "Bearer $access_token" ];
402
        my $request  = HTTP::Request->new( $method, $url, $header );
403
        my $response = $ua->request($request);
404
        my $content  = Encode::decode( "utf8", $response->content );
405
        my $json     = from_json($content);
406
407
        if ( $response->is_success ) {
408
409
            # Some DDC numbers (e.g. 348.481) do not have descriptions in all
410
            # possible languages. We take that into account here.
411
            my $description = $json->{'prefLabel'}->{$language} || "No description found for language code '$language'";
412
            $cache->set_in_cache( "$ddc_uri-description-$language", $description );
413
            return ( $description, 'API' );
414
415
        } else {
416
            return ( undef, $json->{message} );
417
        }
418
    }
419
420
}
421
422
1;

Return to bug 40308