From df991072e89d1462149e26adf53129e1b4451983 Mon Sep 17 00:00:00 2001
From: Ere Maijala <ere.maijala@helsinki.fi>
Date: Thu, 29 Sep 2016 08:50:46 -0400
Subject: [PATCH] Add support for Shibboleth single log-out (#791) - Adds new
 database table for correlating external sessions to internal ones - Includes
 command-line tool for maintenance

---
 config/vufind/config.ini                      |   7 +
 config/vufind/permissions.ini                 |   8 +
 module/VuFind/config/module.config.php        |  57 ++++--
 .../4.0/001-add-external-session-table.sql    |  13 ++
 module/VuFind/sql/mysql.sql                   |  17 ++
 module/VuFind/sql/pgsql.sql                   |  16 ++
 module/VuFind/src/VuFind/Auth/Factory.php     |  14 ++
 module/VuFind/src/VuFind/Auth/Shibboleth.php  |  45 +++++
 ...ShibbolethLogoutNotificationController.php | 180 ++++++++++++++++++
 .../src/VuFind/Db/Row/ExternalSession.php     |  53 ++++++
 .../src/VuFind/Db/Table/ExternalSession.php   | 118 ++++++++++++
 .../src/VuFind/Session/AbstractBase.php       |   6 +-
 module/VuFindConsole/Module.php               |   2 +
 .../Controller/UtilController.php             |  27 +++
 14 files changed, 549 insertions(+), 14 deletions(-)
 create mode 100644 module/VuFind/sql/migrations/pgsql/4.0/001-add-external-session-table.sql
 create mode 100644 module/VuFind/src/VuFind/Controller/ShibbolethLogoutNotificationController.php
 create mode 100644 module/VuFind/src/VuFind/Db/Row/ExternalSession.php
 create mode 100644 module/VuFind/src/VuFind/Db/Table/ExternalSession.php

diff --git a/config/vufind/config.ini b/config/vufind/config.ini
index f5fe32f6965..91923724fb6 100644
--- a/config/vufind/config.ini
+++ b/config/vufind/config.ini
@@ -522,6 +522,13 @@ database          = mysql://root@localhost/vufind
 ; Server param with the identity provider entityID if a Shibboleth session exists.
 ; If omitted, Shib-Identity-Provider is used.
 ;idpserverparam = Shib-Identity-Provider
+; Optional: Session ID parameter for SAML2 single logout support. If omitted, single 
+; logout support is disabled. Note that if SLO support is enabled, Shibboleth session
+; ID's are tracked in external_session table which may need to be cleaned up with the 
+; expire_session_mappings command line utility. See 
+; https://vufind.org/wiki/configuration:shibboleth for more information on how
+; to configure the single logout support.   
+;session_id = Shib-Session-ID
 ; Optional: you may set attribute names and values to be used as a filter;
 ; users will only be logged into VuFind if they match these filters.
 ;userattribute_1       = entitlement
diff --git a/config/vufind/permissions.ini b/config/vufind/permissions.ini
index e480e9d1e45..2395196637a 100644
--- a/config/vufind/permissions.ini
+++ b/config/vufind/permissions.ini
@@ -131,3 +131,11 @@ permission = access.StaffViewTab
 ;ipRange[] = 1.2.3.1-1.2.3.254 ; for the IP-range of your university's network
 ;role = loggedin ; if you want to allow authenticated users to use Primo module
 ;permission = primoOnCampus.MYINSTITUTION
+
+; Example Shibboleth logout API access permission. 
+; See https://vufind.org/wiki/configuration:shibboleth for more information.
+;[api.ShibbolethLogoutNotification]
+;permission = access.api.ShibbolethLogoutNotification
+;require = ANY
+;ipRange[] = '127.0.0.1'
+;ipRange[] = '::1'
diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php
index 9a698d5a6fc..10ee1fafd90 100644
--- a/module/VuFind/config/module.config.php
+++ b/module/VuFind/config/module.config.php
@@ -57,6 +57,36 @@ $config = [
                         'action'     => 'Home',
                     ]
                 ]
+            ],
+            'soap-shibboleth-logout-notification-handler' => [
+                'type' => 'Zend\Mvc\Router\Http\Literal',
+                'options' => [
+                    'route' => '/soap/shiblogout',
+                    'defaults' => [
+                        'controller' => 'ShibbolethLogoutNotification',
+                        'action' => 'index'
+                    ]
+                ],
+                'child_routes' => [
+                    'get' => [
+                        'type' => 'method',
+                        'options' => [
+                            'verb' => 'get',
+                            'defaults' => [
+                                'action' => 'get'
+                            ],
+                        ],
+                    ],
+                    'post' => [
+                        'type' => 'method',
+                        'options' => [
+                            'verb' => 'post',
+                            'defaults' => [
+                                'action' => 'post'
+                            ]
+                        ]
+                    ]
+                ]
             ]
         ],
     ],
@@ -98,6 +128,7 @@ $config = [
             'qrcode' => 'VuFind\Controller\QRCodeController',
             'records' => 'VuFind\Controller\RecordsController',
             'search' => 'VuFind\Controller\SearchController',
+            'shibbolethlogoutnotification' => 'VuFind\Controller\ShibbolethLogoutNotificationController',
             'summon' => 'VuFind\Controller\SummonController',
             'summonrecord' => 'VuFind\Controller\SummonrecordController',
             'tag' => 'VuFind\Controller\TagController',
@@ -225,17 +256,18 @@ $config = [
         ],
         // PostgreSQL sequence mapping
         'pgsql_seq_mapping'  => [
-            'comments'       => ['id', 'comments_id_seq'],
-            'oai_resumption' => ['id', 'oai_resumption_id_seq'],
-            'record'         => ['id', 'record_id_seq'],
-            'resource'       => ['id', 'resource_id_seq'],
-            'resource_tags'  => ['id', 'resource_tags_id_seq'],
-            'search'         => ['id', 'search_id_seq'],
-            'session'        => ['id', 'session_id_seq'],
-            'tags'           => ['id', 'tags_id_seq'],
-            'user'           => ['id', 'user_id_seq'],
-            'user_list'      => ['id', 'user_list_id_seq'],
-            'user_resource'  => ['id', 'user_resource_id_seq'],
+            'comments'         => ['id', 'comments_id_seq'],
+            'external_session' => ['id', 'external_session_id_seq'],
+            'oai_resumption'   => ['id', 'oai_resumption_id_seq'],
+            'record'           => ['id', 'record_id_seq'],
+            'resource'         => ['id', 'resource_id_seq'],
+            'resource_tags'    => ['id', 'resource_tags_id_seq'],
+            'search'           => ['id', 'search_id_seq'],
+            'session'          => ['id', 'session_id_seq'],
+            'tags'             => ['id', 'tags_id_seq'],
+            'user'             => ['id', 'user_id_seq'],
+            'user_list'        => ['id', 'user_list_id_seq'],
+            'user_resource'    => ['id', 'user_resource_id_seq'],
         ],
         // This section contains service manager configurations for all VuFind
         // pluggable components:
@@ -247,13 +279,13 @@ $config = [
                     'facebook' => 'VuFind\Auth\Factory::getFacebook',
                     'ils' => 'VuFind\Auth\Factory::getILS',
                     'multiils' => 'VuFind\Auth\Factory::getMultiILS',
+                    'shibboleth' => 'VuFind\Auth\Factory::getShibboleth'
                 ],
                 'invokables' => [
                     'cas' => 'VuFind\Auth\CAS',
                     'database' => 'VuFind\Auth\Database',
                     'ldap' => 'VuFind\Auth\LDAP',
                     'multiauth' => 'VuFind\Auth\MultiAuth',
-                    'shibboleth' => 'VuFind\Auth\Shibboleth',
                     'sip2' => 'VuFind\Auth\SIP2',
                 ],
                 'aliases' => [
@@ -344,6 +376,7 @@ $config = [
                 'invokables' => [
                     'changetracker' => 'VuFind\Db\Table\ChangeTracker',
                     'comments' => 'VuFind\Db\Table\Comments',
+                    'externalsession' => 'VuFind\Db\Table\ExternalSession',
                     'oairesumption' => 'VuFind\Db\Table\OaiResumption',
                     'record' => 'VuFind\Db\Table\Record',
                     'search' => 'VuFind\Db\Table\Search',
diff --git a/module/VuFind/sql/migrations/pgsql/4.0/001-add-external-session-table.sql b/module/VuFind/sql/migrations/pgsql/4.0/001-add-external-session-table.sql
new file mode 100644
index 00000000000..4ebcb8f70d6
--- /dev/null
+++ b/module/VuFind/sql/migrations/pgsql/4.0/001-add-external-session-table.sql
@@ -0,0 +1,13 @@
+--
+-- Table structure for table external_session
+--
+
+CREATE TABLE external_session (
+id SERIAL,
+session_id varchar(128) NOT NULL,
+external_session_id varchar(255) NOT NULL,
+created timestamp NOT NULL default '1970-01-01 00:00:00',
+PRIMARY KEY (id),
+UNIQUE (session_id)
+);
+CREATE INDEX external_session_id on external_session(external_session_id);
diff --git a/module/VuFind/sql/mysql.sql b/module/VuFind/sql/mysql.sql
index 9029d54fa5f..5fe37443422 100644
--- a/module/VuFind/sql/mysql.sql
+++ b/module/VuFind/sql/mysql.sql
@@ -145,6 +145,23 @@ CREATE TABLE `session` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 /*!40101 SET character_set_client = @saved_cs_client */;
 
+--
+-- Table structure for table `external_session`
+--
+
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `external_session` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `session_id` varchar(128) NOT NULL,
+  `external_session_id` varchar(255) NOT NULL,
+  `created` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `session_id` (`session_id`),
+  KEY `external_session_id` (`external_session_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
 --
 -- Table structure for table `tags`
 --
diff --git a/module/VuFind/sql/pgsql.sql b/module/VuFind/sql/pgsql.sql
index 94b949066dd..a63e2bb0425 100644
--- a/module/VuFind/sql/pgsql.sql
+++ b/module/VuFind/sql/pgsql.sql
@@ -188,6 +188,22 @@ UNIQUE (session_id)
 );
 CREATE INDEX last_used_idx on session(last_used);
 
+--
+-- Table structure for table external_session
+--
+
+DROP TABLE IF EXISTS "external_session";
+
+CREATE TABLE external_session (
+id SERIAL,
+session_id varchar(128) NOT NULL,
+external_session_id varchar(255) NOT NULL,
+created timestamp NOT NULL default '1970-01-01 00:00:00',
+PRIMARY KEY (id),
+UNIQUE (session_id)
+);
+CREATE INDEX external_session_id on external_session(external_session_id);
+
 --
 -- Table structure for table change_tracker
 --
diff --git a/module/VuFind/src/VuFind/Auth/Factory.php b/module/VuFind/src/VuFind/Auth/Factory.php
index bf6740eed94..28acc1ceb60 100644
--- a/module/VuFind/src/VuFind/Auth/Factory.php
+++ b/module/VuFind/src/VuFind/Auth/Factory.php
@@ -167,4 +167,18 @@ class Factory
             $sm->getServiceLocator()->get('VuFind\ILSAuthenticator')
         );
     }
+
+    /**
+     * Construct the Shibboleth plugin.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return Shibboleth
+     */
+    public static function getShibboleth(ServiceManager $sm)
+    {
+        return new Shibboleth(
+            $sm->getServiceLocator()->get('VuFind\SessionManager')
+        );
+    }
 }
diff --git a/module/VuFind/src/VuFind/Auth/Shibboleth.php b/module/VuFind/src/VuFind/Auth/Shibboleth.php
index f872f781786..51bbc5c1e20 100644
--- a/module/VuFind/src/VuFind/Auth/Shibboleth.php
+++ b/module/VuFind/src/VuFind/Auth/Shibboleth.php
@@ -5,6 +5,7 @@
  * PHP version 5
  *
  * Copyright (C) Villanova University 2014.
+ * Copyright (C) The National Library of Finland 2016.
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License version 2,
@@ -25,6 +26,7 @@
  * @author   Jochen Lienhard <lienhard@ub.uni-freiburg.de>
  * @author   Bernd Oberknapp <bo@ub.uni-freiburg.de>
  * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org Main Page
  */
@@ -40,6 +42,7 @@ use VuFind\Exception\Auth as AuthException;
  * @author   Jochen Lienhard <lienhard@ub.uni-freiburg.de>
  * @author   Bernd Oberknapp <bo@ub.uni-freiburg.de>
  * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org Main Page
  */
@@ -47,6 +50,23 @@ class Shibboleth extends AbstractBase
 {
     const DEFAULT_IDPSERVERPARAM = 'Shib-Identity-Provider';
 
+    /**
+     * Session manager
+     *
+     * @var \Zend\Session\ManagerInterface
+     */
+    protected $sessionManager;
+
+    /**
+     * Constructor
+     *
+     * @param \Zend\Session\ManagerInterface $sessionManager Session manager
+     */
+    public function __construct(\Zend\Session\ManagerInterface $sessionManager)
+    {
+        $this->sessionManager = $sessionManager;
+    }
+
     /**
      * Validate configuration parameters.  This is a support method for getConfig(),
      * so the configuration MUST be accessed using $this->config; do not call
@@ -88,12 +108,20 @@ class Shibboleth extends AbstractBase
         $shib = $this->getConfig()->Shibboleth;
         $username = $request->getServer()->get($shib->username);
         if (empty($username)) {
+            $this->debug(
+                "No username attribute ({$shib->username}) present in request: "
+                . print_r($request->getServer()->toArray(), true)
+            );
             throw new AuthException('authentication_error_admin');
         }
 
         // Check if required attributes match up:
         foreach ($this->getRequiredAttributes() as $key => $value) {
             if (!preg_match('/' . $value . '/', $request->getServer()->get($key))) {
+                $this->debug(
+                    "Attribute '$key' does not match required value '$value' in"
+                    . ' request: ' . print_r($request->getServer()->toArray(), true)
+                );
                 throw new AuthException('authentication_error_denied');
             }
         }
@@ -136,6 +164,23 @@ class Shibboleth extends AbstractBase
             );
         }
 
+        // Add session id mapping to external_session table for single logout support
+        if (isset($shib->session_id)) {
+            $shibSessionId = $request->getServer()->get($shib->session_id);
+            if (null !== $shibSessionId) {
+                $localSessionId = $this->sessionManager->getId();
+                $externalSession = $this->getDbTableManager()
+                    ->get('ExternalSession');
+                $externalSession->addSessionMapping(
+                    $localSessionId, $shibSessionId
+                );
+                $this->debug(
+                    "Cached Shibboleth session id '$shibSessionId' for local session"
+                    . " '$localSessionId'"
+                );
+            }
+        }
+
         // Save and return the user object:
         $user->save();
         return $user;
diff --git a/module/VuFind/src/VuFind/Controller/ShibbolethLogoutNotificationController.php b/module/VuFind/src/VuFind/Controller/ShibbolethLogoutNotificationController.php
new file mode 100644
index 00000000000..ddd874843c3
--- /dev/null
+++ b/module/VuFind/src/VuFind/Controller/ShibbolethLogoutNotificationController.php
@@ -0,0 +1,180 @@
+<?php
+/**
+ * Shibboleth Logout Notification API Controller
+ *
+ * PHP version 5
+ *
+ * Copyright (C) The National Library of Finland 2016.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program 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 this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Controller
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Site
+ */
+namespace VuFind\Controller;
+
+use Zend\Stdlib\ResponseInterface as Response;
+
+/**
+ * Handles Shibboleth back-channel logout notifications.
+ *
+ * @category VuFind
+ * @package  Controller
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Site
+ */
+class ShibbolethLogoutNotificationController extends AbstractBase
+{
+    /**
+     * Constructor
+     */
+    public function __construct()
+    {
+        $this->accessPermission = 'access.api.ShibbolethLogoutNotification';
+        $this->accessDeniedBehavior = 'exception';
+    }
+
+    /**
+     * GET method handler for the logout handler
+     *
+     * @return Response
+     */
+    public function getAction()
+    {
+        $this->disableSessionWrites();
+        $response = $this->getResponse();
+        $response->getHeaders()->addHeaderLine(
+            'Content-Type', 'application/wsdl+xml'
+        );
+        $response->setContent($this->getWsdl());
+        return $response;
+    }
+
+    /**
+     * POST method handler for the logout handler
+     *
+     * @return Response
+     */
+    public function postAction()
+    {
+        $this->disableSessionWrites();
+        list($uri) = explode('?', $this->getRequest()->getUriString());
+        $soapServer = new \Zend\Soap\Server(
+            'data://text/plain;base64,' . base64_encode($this->getWsdl())
+        );
+        $soapServer->setReturnResponse(true);
+        $soapServer->setObject($this);
+        $soapResponse = $soapServer->handle();
+        if ($soapResponse instanceof \SoapFault) {
+            $soapResponse = (string)$soapResponse;
+        }
+        $response = $this->getResponse();
+        $response->getHeaders()->addHeaderLine('Content-Type', 'text/xml');
+        $response->setContent($soapResponse);
+        return $response;
+    }
+
+    /**
+     * Logout notification handler
+     *
+     * @param string $sessionId External session id
+     *
+     * @return void
+     */
+    public function logoutNotification($sessionId)
+    {
+        $table = $this->getTable('ExternalSession');
+        $row = $table->getByExternalSessionId(trim($sessionId));
+        if (empty($row)) {
+            return;
+        }
+        $sessionManager = $this->getServiceLocator()->get('VuFind\SessionManager');
+        $handler = $sessionManager->getSaveHandler();
+        $handler->destroy($row['session_id']);
+        return;
+    }
+
+    /**
+     * Get WSDL for the service
+     *
+     * @return string
+     */
+    protected function getWsdl()
+    {
+        list($uri) = explode('?', $this->getRequest()->getUriString());
+        return <<<EOT
+<?xml version ="1.0" encoding ="UTF-8" ?>
+<definitions name="LogoutNotification"
+  targetNamespace="urn:mace:shibboleth:2.0:sp:notify"
+  xmlns:notify="urn:mace:shibboleth:2.0:sp:notify"
+  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+  xmlns="http://schemas.xmlsoap.org/wsdl/">
+
+    <types>
+       <schema targetNamespace="urn:mace:shibboleth:2.0:sp:notify"
+           xmlns="http://www.w3.org/2000/10/XMLSchema"
+           xmlns:notify="urn:mace:shibboleth:2.0:sp:notify">
+
+            <simpleType name="string">
+                <restriction base="string">
+                    <minLength value="1"/>
+                </restriction>
+            </simpleType>
+
+            <element name="OK" type="notify:OKType"/>
+            <complexType name="OKType">
+                <sequence/>
+            </complexType>
+
+        </schema>
+    </types>
+
+    <message name="getLogoutNotificationRequest">
+        <part name="SessionID" type="notify:string"/>
+    </message>
+
+    <message name="getLogoutNotificationResponse" >
+        <part name="OK"/>
+    </message>
+
+    <portType name="LogoutNotificationPortType">
+        <operation name="LogoutNotification">
+            <input message="getLogoutNotificationRequest"/>
+            <output message="getLogoutNotificationResponse"/>
+        </operation>
+    </portType>
+
+    <binding name="LogoutNotificationBinding"
+        type="notify:LogoutNotificationPortType">
+        <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
+        <operation name="LogoutNotification">
+            <soap:operation
+                soapAction="urn:xmethods-logout-notification#LogoutNotification"/>
+        </operation>
+    </binding>
+
+    <service name="LogoutNotificationService">
+          <port name="LogoutNotificationPort"
+            binding="notify:LogoutNotificationBinding">
+            <soap:address location="$uri"/>
+          </port>
+    </service>
+</definitions>
+EOT;
+    }
+}
diff --git a/module/VuFind/src/VuFind/Db/Row/ExternalSession.php b/module/VuFind/src/VuFind/Db/Row/ExternalSession.php
new file mode 100644
index 00000000000..07452cfbe24
--- /dev/null
+++ b/module/VuFind/src/VuFind/Db/Row/ExternalSession.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * Row Definition for external_session
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ * Copyrught (C) The National Library of Finland 2016.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program 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 this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Db_Row
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Site
+ */
+namespace VuFind\Db\Row;
+
+/**
+ * Row Definition for external_session
+ *
+ * @category VuFind
+ * @package  Db_Row
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Site
+ */
+class ExternalSession extends RowGateway
+{
+    /**
+     * Constructor
+     *
+     * @param \Zend\Db\Adapter\Adapter $adapter Database adapter
+     */
+    public function __construct($adapter)
+    {
+        parent::__construct('id', 'external_session', $adapter);
+    }
+}
diff --git a/module/VuFind/src/VuFind/Db/Table/ExternalSession.php b/module/VuFind/src/VuFind/Db/Table/ExternalSession.php
new file mode 100644
index 00000000000..3e2763ff15a
--- /dev/null
+++ b/module/VuFind/src/VuFind/Db/Table/ExternalSession.php
@@ -0,0 +1,118 @@
+<?php
+/**
+ * Table Definition for external_session
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ * Copyright (C) The National Library of Finland 2016.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program 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 this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Db_Table
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Page
+ */
+namespace VuFind\Db\Table;
+
+/**
+ * Table Definition for external_session
+ *
+ * @category VuFind
+ * @package  Db_Table
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Site
+ */
+class ExternalSession extends Gateway
+{
+    use ExpirationTrait;
+
+    /**
+     * Constructor
+     */
+    public function __construct()
+    {
+        parent::__construct('external_session', 'VuFind\Db\Row\ExternalSession');
+    }
+
+    /**
+     * Add a mapping between local and external session id's
+     *
+     * @param string $localSessionId    Local (VuFind) session id
+     * @param string $externalSessionId External session id
+     *
+     * @return void
+     */
+    public function addSessionMapping($localSessionId, $externalSessionId)
+    {
+        $this->destroySession($localSessionId);
+        $row = $this->createRow();
+        $row->session_id = $localSessionId;
+        $row->external_session_id = $externalSessionId;
+        $row->created = date('Y-m-d H:i:s');
+        $row->save();
+    }
+
+    /**
+     * Retrieve an object from the database based on an external session ID
+     *
+     * @param string $sid External session ID to retrieve
+     *
+     * @return \VuFind\Db\Row\ExternalSession
+     */
+    public function getByExternalSessionId($sid)
+    {
+        return $this->select(['external_session_id' => $sid])->current();
+    }
+
+    /**
+     * Destroy data for the given session ID.
+     *
+     * @param string $sid Session ID to erase
+     *
+     * @return void
+     */
+    public function destroySession($sid)
+    {
+        $this->delete(['session_id' => $sid]);
+    }
+
+    /**
+     * Update the select statement to find records to delete.
+     *
+     * @param Select $select  Select clause
+     * @param int    $daysOld Age in days of an "expired" record.
+     * @param int    $idFrom  Lowest id of rows to delete.
+     * @param int    $idTo    Highest id of rows to delete.
+     *
+     * @return void
+     */
+    protected function expirationCallback($select, $daysOld, $idFrom = null,
+        $idTo = null
+    ) {
+        $expireDate = date('Y-m-d', time() - $daysOld * 24 * 60 * 60);
+        $where = $select->where->lessThan('created', $expireDate);
+        if (null !== $idFrom) {
+            $where->and->greaterThanOrEqualTo('id', $idFrom);
+        }
+        if (null !== $idTo) {
+            $where->and->lessThanOrEqualTo('id', $idTo);
+        }
+    }
+}
diff --git a/module/VuFind/src/VuFind/Session/AbstractBase.php b/module/VuFind/src/VuFind/Session/AbstractBase.php
index d8231664bab..26b545dab8c 100644
--- a/module/VuFind/src/VuFind/Session/AbstractBase.php
+++ b/module/VuFind/src/VuFind/Session/AbstractBase.php
@@ -133,8 +133,10 @@ abstract class AbstractBase implements SaveHandlerInterface,
      */
     public function destroy($sess_id)
     {
-        $table = $this->getTable('Search');
-        $table->destroySession($sess_id);
+        $searchTable = $this->getTable('Search');
+        $searchTable->destroySession($sess_id);
+        $sessionTable = $this->getTable('ExternalSession');
+        $sessionTable->destroySession($sess_id);
         return true;
     }
 
diff --git a/module/VuFindConsole/Module.php b/module/VuFindConsole/Module.php
index 016f39db18a..822f6c2b32b 100644
--- a/module/VuFindConsole/Module.php
+++ b/module/VuFindConsole/Module.php
@@ -98,6 +98,8 @@ class Module implements \Zend\ModuleManager\Feature\ConsoleUsageProviderInterfac
             'util deletes' => 'Tool for deleting Solr records',
             'util expire_searches' => 'Database search table cleanup',
             'util expire_sessions' => 'Database session table cleanup',
+            'util expire_external_sessions'
+                => 'Database external_session table cleanup',
             'util index_reserves' => 'Solr reserves indexer',
             'util optimize' => 'Solr optimize tool',
             'util sitemap' => 'XML sitemap generator',
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/UtilController.php b/module/VuFindConsole/src/VuFindConsole/Controller/UtilController.php
index c6f4ecdb024..532f0f8ce3f 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/UtilController.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/UtilController.php
@@ -540,6 +540,33 @@ class UtilController extends AbstractBase
         );
     }
 
+    /**
+     * Command-line tool to clear unwanted entries
+     * from external_session database table.
+     *
+     * @return \Zend\Console\Response
+     */
+    public function expireExternalSessionsAction()
+    {
+        $this->consoleOpts->addRules(
+            [
+                'h|help' => 'Get help',
+                'batch=i' => 'Batch size',
+                'sleep=i' => 'Sleep interval between batches'
+            ]
+        );
+
+        if ($this->consoleOpts->getOption('h')) {
+            return $this->expirationHelp('external sessions');
+        }
+
+        return $this->expire(
+            'ExternalSession',
+            '%%count%% expired external sessions deleted.',
+            'No expired external sessions to delete.'
+        );
+    }
+
     /**
      * Command-line tool to delete suppressed records from the index.
      *
-- 
GitLab