bug_16034 Koha::ExternalContent::OverDrive - a wrapper around WebService::ILS::Overdr...
authorSrdjan <srdjan@catalyst.net.nz>
Tue, 8 Dec 2015 06:06:27 +0000 (19:06 +1300)
committerKyle M Hall <kyle@bywatersolutions.com>
Tue, 21 Feb 2017 19:58:20 +0000 (19:58 +0000)
* Using the upstream module for all the heavy lifting
* opac/external/overdrive/auth.pl - 3-legged authentication handler

Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Koha/ExternalContent.pm [new file with mode: 0644]
Koha/ExternalContent/OverDrive.pm [new file with mode: 0644]
Koha/Schema/Result/Borrower.pm
installer/data/mysql/atomicupdate/overdrive.sql [new file with mode: 0644]
installer/data/mysql/kohastructure.sql
opac/external/overdrive/auth.pl [new file with mode: 0755]
t/Koha_ExternalContent_OverDrive.t [new file with mode: 0755]

diff --git a/Koha/ExternalContent.pm b/Koha/ExternalContent.pm
new file mode 100644 (file)
index 0000000..7f5d8ed
--- /dev/null
@@ -0,0 +1,101 @@
+# Copyright 2014 Catalyst
+#
+# 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.
+
+package Koha::ExternalContent;
+
+use Modern::Perl;
+use Carp;
+use base qw(Class::Accessor);
+
+use Koha;
+use Koha::Patrons;
+use C4::Auth;
+
+__PACKAGE__->mk_accessors(qw(client koha_session_id koha_patron));
+
+=head1 NAME
+
+Koha::ExternalContent
+
+=head1 SYNOPSIS
+
+ use Koha::ExternalContent;
+ my $externalcontent = Koha::ExternalContent->new();
+
+=head1 DESCRIPTION
+
+Base class for interfacing with external content providers.
+
+Subclasses provide clients for particular systems. This class provides
+common methods for getting Koha patron.
+
+=head1 METHODS
+
+=cut
+
+sub agent_string {
+    return 'Koha/'.Koha::version();
+}
+
+sub new {
+    my $class     = shift;
+    my $params    = shift || {};
+    return bless $params, $class;
+}
+
+sub _koha_session {
+    my $self = shift;
+    my $session_id = $self->koha_session_id or return;
+    return C4::Auth::get_session($session_id);
+}
+
+sub get_from_koha_session {
+    my $self = shift;
+    my $key = shift or croak "No key";
+    my $session = $self->_koha_session or return;
+    return $session->param($key);
+}
+
+sub set_in_koha_session {
+    my $self = shift;
+    my $key = shift or croak "No key";
+    my $value = shift;
+    my $session = $self->_koha_session or croak "No Koha session";
+    return $session->param($key, $value);
+}
+
+sub koha_patron {
+    my $self = shift;
+
+    if (my $patron = $self->_koha_patron_accessor) {
+        return $patron;
+    }
+
+    my $id = $self->get_from_koha_session('number')
+      or die "No patron number in session";
+    my $patron = Koha::Patrons->find($id)
+      or die "Invalid patron number in session";
+    return $self->_koha_patron_accessor($patron);
+}
+
+=head1 AUTHOR
+
+CatalystIT
+
+=cut
+
+1;
diff --git a/Koha/ExternalContent/OverDrive.pm b/Koha/ExternalContent/OverDrive.pm
new file mode 100644 (file)
index 0000000..4cde086
--- /dev/null
@@ -0,0 +1,253 @@
+# Copyright 2014 Catalyst
+#
+# 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.
+
+package Koha::ExternalContent::OverDrive;
+
+use Modern::Perl;
+use Carp;
+
+use base qw(Koha::ExternalContent);
+use WebService::ILS::OverDrive::Patron;
+use C4::Context;
+use Koha::Logger;
+
+use constant logger => Koha::Logger->get();
+
+=head1 NAME
+
+Koha::ExternalContent::OverDrive
+
+=head1 SYNOPSIS
+
+    Register return url with OverDrive:
+      base app url + /cgi-bin/koha/external/overdrive/auth.pl
+
+    use Koha::ExternalContent::OverDrive;
+    my $od_client = Koha::ExternalContent::OverDrive->new();
+    my $od_auth_url = $od_client->auth_url($return_page_url);
+
+=head1 DESCRIPTION
+
+A (very) thin wrapper around C<WebService::ILS::OverDrive::Patron>
+
+Takes "OverDrive*" Koha preferences
+
+=cut
+
+sub new {
+    my $class  = shift;
+    my $params = shift || {};
+    $params->{koha_session_id} or croak "No koha_session_id";
+
+    my $self = $class->SUPER::new($params);
+    unless ($params->{client}) {
+        my $client_key     = C4::Context->preference('OverDriveClientKey')
+          or croak("OverDriveClientKey pref not set");
+        my $client_secret  = C4::Context->preference('OverDriveClientSecret')
+          or croak("OverDriveClientSecret pref not set");
+        my $library_id     = C4::Context->preference('OverDriveLibraryID')
+          or croak("OverDriveLibraryID pref not set");
+        my ($token, $token_type) = $self->get_token_from_koha_session();
+        $self->client( WebService::ILS::OverDrive::Patron->new(
+            client_id         => $client_key,
+            client_secret     => $client_secret,
+            library_id        => $library_id,
+            access_token      => $token,
+            access_token_type => $token_type,
+            user_agent_params => { agent => $class->agent_string }
+        ) );
+    }
+    return $self;
+}
+
+=head1 L<WebService::ILS::OverDrive::Patron> METHODS
+
+Methods used without mods:
+
+=over 4
+
+=item C<error_message()>
+
+=item C<patron()>
+
+=item C<checkouts()>
+
+=item C<holds()>
+
+=item C<checkout($id, $format)>
+
+=item C<checkout_download_url($id)>
+
+=item C<return($id)>
+
+=item C<place_hold($id)>
+
+=item C<remove_hold($id)>
+
+=back
+
+Methods with slightly moded interfaces:
+
+=head2 auth_url($page_url)
+
+  Input: url of the page from which OverDrive authentication was requested
+
+  Returns: Post OverDrive auth return handler url (see SYNOPSIS)
+
+=cut
+
+sub auth_url {
+    my $self = shift;
+    my $page_url = shift or croak "Page url not provided";
+
+    my ($return_url, $page) = $self->_return_url($page_url);
+    $self->set_return_page_in_koha_session($page);
+    return $self->client->auth_url($return_url);
+}
+
+=head2 auth_by_code($code, $base_url)
+
+  To be called in external/overdrive/auth.pl upon return from OverDrive auth
+
+=cut
+
+sub auth_by_code {
+    my $self = shift;
+    my $code = shift or croak "OverDrive auth code not provided";
+    my $base_url = shift or croak "App base url not provided";
+
+    my ($access_token, $access_token_type, $auth_token)
+      = $self->client->auth_by_code($code, $self->_return_url($base_url));
+    $access_token or die "Invalid OverDrive code returned";
+    $self->set_token_in_koha_session($access_token, $access_token_type);
+
+    $self->koha_patron->set({overdrive_auth_token => $auth_token})->store;
+    return $self->get_return_page_from_koha_session;
+}
+
+use constant AUTH_RETURN_HANDLER => "/cgi-bin/koha/external/overdrive/auth.pl";
+sub _return_url {
+    my $self = shift;
+    my $page_url = shift or croak "Page url not provided";
+
+    my ($base_url, $page) = ($page_url =~ m!^(https?://[^/]+)(.*)!);
+    my $return_url = $base_url.AUTH_RETURN_HANDLER;
+
+    return wantarray ? ($return_url, $page) : $return_url;
+}
+
+use constant RETURN_PAGE_SESSION_KEY => "overdrive.return_page";
+sub get_return_page_from_koha_session {
+    my $self = shift;
+    my $return_page = $self->get_from_koha_session(RETURN_PAGE_SESSION_KEY) || "";
+    $self->logger->debug("get_return_page_from_koha_session: $return_page");
+    return $return_page;
+}
+sub set_return_page_in_koha_session {
+    my $self = shift;
+    my $return_page = shift || "";
+    $self->logger->debug("set_return_page_in_koha_session: $return_page");
+    return $self->set_in_koha_session( RETURN_PAGE_SESSION_KEY, $return_page );
+}
+
+use constant ACCESS_TOKEN_SESSION_KEY => "overdrive.access_token";
+my $ACCESS_TOKEN_DELIMITER = ":";
+sub get_token_from_koha_session {
+    my $self = shift;
+    my ($token, $token_type)
+      = split $ACCESS_TOKEN_DELIMITER, $self->get_from_koha_session(ACCESS_TOKEN_SESSION_KEY) || "";
+    $self->logger->debug("get_token_from_koha_session: ".($token || "(none)"));
+    return ($token, $token_type);
+}
+sub set_token_in_koha_session {
+    my $self = shift;
+    my $token = shift || "";
+    my $token_type = shift || "";
+    $self->logger->debug("set_token_in_koha_session: $token $token_type");
+    return $self->set_in_koha_session(
+        ACCESS_TOKEN_SESSION_KEY,
+        join($ACCESS_TOKEN_DELIMITER, $token, $token_type)
+    );
+}
+
+=head1 OTHER METHODS
+
+=head2 is_logged_in()
+
+  Returns boolean
+
+=cut
+
+sub is_logged_in {
+    my $self = shift;
+    my ($token, $token_type) = $self->get_token_from_koha_session();
+    $token ||= $self->auth_by_saved_token;
+    return $token;
+}
+
+sub auth_by_saved_token {
+    my $self = shift;
+
+    my $koha_patron = $self->koha_patron;
+    if (my $auth_token = $koha_patron->overdrive_auth_token) {
+        my ($access_token, $access_token_type, $new_auth_token)
+          = $self->client->auth_by_token($auth_token);
+        $self->set_token_in_koha_session($access_token, $access_token_type);
+        $koha_patron->set({overdrive_auth_token => $new_auth_token})->store;
+        return $access_token;
+    }
+
+    return;
+}
+
+=head2 forget()
+
+  Removes stored OverDrive token
+
+=cut
+
+sub forget {
+    my $self = shift;
+
+    $self->set_token_in_koha_session("", "");
+    $self->koha_patron->set({overdrive_auth_token => undef})->store;
+}
+
+use vars qw{$AUTOLOAD};
+sub AUTOLOAD {
+    my $self = shift;
+    (my $method = $AUTOLOAD) =~ s/.*:://;
+    my $od = $self->client;
+    local $@;
+    my $ret = eval { $od->$method(@_) };
+    if ($@) {
+        if ( $od->is_access_token_error($@) && $self->auth_by_saved_token ) {
+            return $od->$method(@_);
+        }
+        die $@;
+    }
+    return $ret;
+}
+sub DESTROY { }
+
+=head1 AUTHOR
+
+CatalystIT
+
+=cut
+
+1;
index ac414b4..4241dd0 100644 (file)
@@ -616,6 +616,8 @@ __PACKAGE__->add_columns(
     datetime_undef_if_invalid => 1,
     is_nullable => 1,
   },
+  "overdrive_auth_token",
+  { data_type => "text", is_nullable => 1 },
 );
 
 =head1 PRIMARY KEY
diff --git a/installer/data/mysql/atomicupdate/overdrive.sql b/installer/data/mysql/atomicupdate/overdrive.sql
new file mode 100644 (file)
index 0000000..2e27d19
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE borrowers ADD overdrive_auth_token text default NULL AFTER lastseen;
index 7f405c9..117c14f 100644 (file)
@@ -1655,6 +1655,7 @@ CREATE TABLE `borrowers` ( -- this table includes information about your patrons
   `checkprevcheckout` varchar(7) NOT NULL default 'inherit', -- produce a warning for this patron if this item has previously been checked out to this patron if 'yes', not if 'no', defer to category setting if 'inherit'.
   `updated_on` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- time of last change could be useful for synchronization with external systems (among others)
   `lastseen` datetime default NULL, -- last time a patron has been seed (connected at the OPAC or staff interface)
+  overdrive_auth_token text default NULL, -- persist OverDrive auth token
   UNIQUE KEY `cardnumber` (`cardnumber`),
   PRIMARY KEY `borrowernumber` (`borrowernumber`),
   KEY `categorycode` (`categorycode`),
diff --git a/opac/external/overdrive/auth.pl b/opac/external/overdrive/auth.pl
new file mode 100755 (executable)
index 0000000..97604c7
--- /dev/null
@@ -0,0 +1,56 @@
+#!/usr/bin/perl
+
+# script to handle redirect back from OverDrive auth endpoint
+
+# Copyright 2015 Catalyst IT
+# 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, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+use CGI qw ( -utf8 );
+use URI;
+use URI::Escape;
+use C4::Auth qw(checkauth);
+use Koha::Logger;
+use Koha::ExternalContent::OverDrive;
+
+my $logger = Koha::Logger->get({ interface => 'opac' });
+my $cgi = new CGI;
+
+my ( $user, $cookie, $sessionID, $flags ) = checkauth( $cgi, 1, {}, 'opac' );
+my ($redirect_page, $error);
+if ($user && $sessionID) {
+    my $od = Koha::ExternalContent::OverDrive->new({ koha_session_id => $sessionID });
+    if ( my $auth_code = $cgi->param('code') ) {
+        my $base_url = $cgi->url(-base => 1);
+        local $@;
+        $redirect_page = eval { $od->auth_by_code($auth_code, $base_url) };
+        if ($@) {
+            $logger->error($@);
+            $error = $od->error_message($@);
+        }
+    }
+    else {
+        $error = "Missing OverDrive auth code";
+    }
+    $redirect_page ||= $od->get_return_page_from_koha_session;
+}
+else {
+    $error = "User not logged in";
+}
+$redirect_page ||= "/cgi-bin/koha/opac-user.pl";
+my $uri = URI->new($redirect_page);
+$uri->query_form( $uri->query_form, overdrive_tab => 1, overdrive_error => uri_escape($error || "") );
+print $cgi->redirect($redirect_page);
diff --git a/t/Koha_ExternalContent_OverDrive.t b/t/Koha_ExternalContent_OverDrive.t
new file mode 100755 (executable)
index 0000000..de70b3d
--- /dev/null
@@ -0,0 +1,35 @@
+use Modern::Perl;
+
+use t::lib::Mocks;
+use Test::More tests => 5;                      # last test to print
+
+local $@;
+eval { require WebService::ILS::OverDrive::Patron; }
+  or diag($@);
+SKIP: {
+    skip "cannot filnd WebService::ILS::OverDrive::Patron", 5 if $@;
+
+    use_ok('Koha::ExternalContent::OverDrive');
+
+    t::lib::Mocks::mock_preference('OverDriveClientKey', 'DUMMY');
+    t::lib::Mocks::mock_preference('OverDriveClientSecret', 'DUMMY');
+    t::lib::Mocks::mock_preference('OverDriveLibraryID', 'DUMMY');
+
+    my $client = Koha::ExternalContent::OverDrive->new({koha_session_id => 'DUMMY'});
+
+    my $user_agent_string = $client->user_agent->agent();
+    ok ($user_agent_string =~ m/^Koha/, 'User Agent string is set')
+      or diag("User Agent string: $user_agent_string");
+
+    my $base_url = "http://mykoha.org";
+    ok ($client->auth_url($base_url), 'auth_url()');
+    local $@;
+    eval { $client->auth_by_code("blah", $base_url) };
+    ok($@, "auth_by_code() dies with bogus credentials");
+    SKIP: {
+        skip "No exception", 1 unless $@;
+        my $error_message = $client->error_message($@);
+        ok($error_message =~ m/Authorization Failed/i, "error_message()")
+          or diag("Original:\n$@\nTurned into:\n$error_message");
+    }
+}