From 1afe1514bc5160ba53467abb1f173e1aa17a2090 Mon Sep 17 00:00:00 2001
From: Joe Atzberger <ohiocore@gmail.com>
Date: Wed, 6 Mar 2013 11:36:01 -0500
Subject: [PATCH] Polaris driver and config, squashed

---
 config/vufind/Polaris.ini                     |  42 ++
 config/vufind/config.ini                      |   8 +-
 module/VuFind/config/module.config.php        |   1 +
 .../VuFind/src/VuFind/ILS/Driver/Polaris.php  | 525 ++++++++++++++++++
 .../src/VuFindTest/ILS/Driver/PolarisTest.php |  67 +++
 5 files changed, 639 insertions(+), 4 deletions(-)
 create mode 100644 config/vufind/Polaris.ini
 create mode 100644 module/VuFind/src/VuFind/ILS/Driver/Polaris.php
 create mode 100644 module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PolarisTest.php

diff --git a/config/vufind/Polaris.ini b/config/vufind/Polaris.ini
new file mode 100644
index 00000000000..203b326f3ad
--- /dev/null
+++ b/config/vufind/Polaris.ini
@@ -0,0 +1,42 @@
+[PAPI]
+ws_host    = 'http://example.catalog.org';
+ws_app     = '/PAPIService/REST/public/v1/1033/100/1/';
+ws_api_key = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXX';
+ws_api_id  = 'YOUR_ID_HERE';
+ws_requestingorgid = '';
+
+
+[Holds]
+; If a colon separated list of item statuses is provided here, only matching items
+; will show hold / recall links.  Skip this setting to allow all statuses.
+;valid_hold_statuses = Charged:Renewed:Overdue
+
+; non_borrowable - If a colon-separated list of ids from the ITEM_TYPES table is
+; provided, matching items will be excluded from holds / recalls.  Skip this setting
+; to allow links for all item types.
+;non_borrowable = 2:4:23:10
+
+; HMACKeys - A list of hold form element names that will be analyzed for consistency
+; during hold form processing. Most users should not need to change this setting.
+HMACKeys = item_id:holdtype:level
+
+; defaultRequiredDate - A colon-separated list used to set the default "not required
+; after" date for holds in the format days:months:years
+; e.g. 0:1:0 will set a "not required after" date of 1 month from the current date
+defaultRequiredDate = 0:1:0
+
+; extraHoldFields - A colon-separated list used to display extra visible fields in the
+; place holds form. Supported values are "comments", "requiredByDate" and 
+; "pickUpLocation"  
+extraHoldFields = pickUpLocation
+
+; A Pick Up Location Code used to pre-select the pick up location drop down list and
+; provide a default option if others are not available. Must correspond with one of 
+; the Location IDs returned by getPickUpLocations()
+defaultPickUpLocation = "15"
+
+; The maximum number of holding items to generate request links for. The process of
+; checking the API for a valid hold is intensive. Any hold items above this this 
+; limit will have their hold status checked via ajax or when a user clicks on the
+; link. Use "0" to check all items via ajax. Default is 15.
+holdCheckLimit = 15
diff --git a/config/vufind/config.ini b/config/vufind/config.ini
index e2954cfcd2e..09655bcff3e 100644
--- a/config/vufind/config.ini
+++ b/config/vufind/config.ini
@@ -96,10 +96,10 @@ lifetime                    = 3600 ; Session lasts for 1 hour
 ;       access only), HorizonXMLAPI (more features via API), Innovative, Koha,
 ;       MultiBackend (to chain together multiple drivers in a consortial setting),
 ;       NewGenLib, NoILS (for users without an ILS, or to disable ILS functionality
-;       during maintenance), Unicorn (which also applies to SirsiDynix Symphony),
-;       Virtua, Voyager (for Voyager 6+), VoyagerRestful (for Voyager 7+ w/ RESTful
-;       web services), XCNCIP (for XC NCIP Toolkit v1.x), XCNCIP2 (for XC NCIP Tookit
-;       v2.x)
+;       during maintenance), Polaris, Unicorn (which also applies to SirsiDynix
+;       Symphony), Virtua, Voyager (for Voyager 6+), VoyagerRestful (for Voyager 7+
+;       w/ RESTful web services), XCNCIP (for XC NCIP Toolkit v1.x), XCNCIP2 (for
+;       XC NCIP Tookit v2.x)
 ; Note: Unicorn users should visit the vufind-unicorn project for more details:
 ;       http://code.google.com/p/vufind-unicorn/
 ;
diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php
index cd5f26a2ba0..915bcc1cca7 100644
--- a/module/VuFind/config/module.config.php
+++ b/module/VuFind/config/module.config.php
@@ -537,6 +537,7 @@ $config = array(
                     'koha' => 'VuFind\ILS\Driver\Koha',
                     'newgenlib' => 'VuFind\ILS\Driver\NewGenLib',
                     'pica' => 'VuFind\ILS\Driver\PICA',
+                    'polaris' => 'VuFind\ILS\Driver\Polaris',
                     'sample' => 'VuFind\ILS\Driver\Sample',
                     'symphony' => 'VuFind\ILS\Driver\Symphony',
                     'virtua' => 'VuFind\ILS\Driver\Virtua',
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Polaris.php b/module/VuFind/src/VuFind/ILS/Driver/Polaris.php
new file mode 100644
index 00000000000..d3931fc13a9
--- /dev/null
+++ b/module/VuFind/src/VuFind/ILS/Driver/Polaris.php
@@ -0,0 +1,525 @@
+<?php
+/**
+ * Polaris ILS Driver (POCA)
+ *
+ * PHP version 5
+ *
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  ILS_Drivers
+ * @author   BookSite <vufind-tech@lists.sourceforge.net>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/building_an_ils_driver Wiki
+ */
+namespace VuFind\ILS\Driver;
+use VuFind\Config\Reader as ConfigReader,
+    VuFind\Exception\ILS as ILSException;
+
+/**
+ * VuFind Connector for Polaris
+ *
+ * Based on Polaris 1.4 API
+ *
+ * @category VuFind2
+ * @package  ILS_Drivers
+ * @author   BookSite <vufind-tech@lists.sourceforge.net>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/building_an_ils_driver Wiki
+ */
+class Polaris extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface
+{
+    protected $ws_host;
+    protected $ws_app;
+    protected $ws_api_id;
+    protected $ws_api_key;
+    protected $ws_requestingorgid;
+
+    /**
+     * HTTP service
+     *
+     * @var \VuFindHttp\HttpServiceInterface
+     */
+    protected $httpService = null;
+
+    /**
+     * Set the HTTP service to be used for HTTP requests.
+     *
+     * @param HttpServiceInterface $service HTTP service
+     *
+     * @return void
+     */
+    public function setHttpService(\VuFindHttp\HttpServiceInterface $service)
+    {
+        $this->httpService = $service;
+    }
+
+    /**
+     * Initialize the driver.
+     *
+     * Validate configuration and perform all resource-intensive tasks needed to
+     * make the driver active.
+     *
+     * @throws ILSException
+     * @return void
+     */
+    public function init()
+    {
+        if (empty($this->config) || !isset($this->config['PAPI'])) {
+            throw new ILSException('Configuration needs to be set.');
+        }
+
+        // Define Polaris PAPI parameters
+        $this->ws_host    = $this->config['PAPI']['ws_host'];
+        $this->ws_app     = $this->config['PAPI']['ws_app'];
+        $this->ws_api_id  = $this->config['PAPI']['ws_api_id'];
+        $this->ws_api_key = $this->config['PAPI']['ws_api_key'];
+        $this->ws_requestingorgid    = $this->config['PAPI']['ws_requestingorgid'];
+        $this->defaultPickUpLocation
+            = isset($this->config['Holds']['defaultPickUpLocation'])
+            ? $this->config['Holds']['defaultPickUpLocation'] : null;
+    }
+
+    /**
+     * Make Request
+     *
+     * Makes a request to the Polaris Restful API
+     *
+     * @param string $api_query      Query string for request
+     * @param string $http_method    HTTP method (default = GET)
+     * @param string $patronpassword Patron password (optional)
+     * @param bool   $json           Optional JSON attachment
+     *
+     * @throws ILSException
+     * @return obj
+     */
+    protected function makeRequest($api_query, $http_method="GET",
+        $patronpassword = "", $json = false
+    ) {
+        // TODO, just make this for this one call
+        date_default_timezone_set('GMT');
+        $date = date("D, d M Y H:i:s T");
+
+        $url = $this->ws_host . $this->ws_app . $api_query;
+
+        $signature_text = $http_method.$url.$date.$patronpassword;
+        $signature = base64_encode(
+            hash_hmac('sha1', $signature_text, $this->ws_api_key, true)
+        );
+
+        $auth_token = "PWS {$this->ws_api_id}:$signature";
+        $http_headers = array(
+            "Content-type: application/json",
+            "Accept: application/json",
+            "PolarisDate: $date",
+            "Authorization: $auth_token"
+        );
+
+        try {
+            $client = $this->httpService->createClient($url);
+
+            // Attach JSON if necessary
+            if ($json !== false) {
+                $json_data = json_encode($json);
+                $client->setRawBody($json_data);
+                $client->setEncType('application/json');
+            }
+
+            $client->setHeaders($http_headers);
+            $client->setMethod($http_method);
+            $result = $client->send();
+        } catch (\Exception $e) {
+            throw new ILSException($e->getMessage());
+        }
+
+        if (!$result->isSuccess()) {
+            throw new ILSException('HTTP error');
+        }
+
+        return json_decode($result->getBody());
+    }
+
+    /**
+     * return human-readable date from text like Date(1360051200000-0800)
+     *
+     * @param string $jsontime Input
+     *
+     * @return string
+     */
+    public function formatJSONTime($jsontime)
+    {
+        preg_match('/Date\((\d+)\-(\d){2}(\d){2}\)/', $jsontime, $matches);
+        $matchestmp = intval($matches[1]/1000);
+        $date = gmdate("n-j-Y", $matchestmp);
+        return $date;
+    }
+
+    /**
+     * Get Patron Holds
+     *
+     * This is responsible for retrieving all holds by a specific patron.
+     *
+     * @param array $patron The patron array from patronLogin
+     *
+     * @return mixed                Array of the patron's holds on success.
+     */
+    public function getMyHolds($patron)
+    {
+        $holds = array();
+        $response = $this->makeRequest(
+            "patron/{$patron['cat_username']}/holdrequests/active", 'GET',
+            $patron['cat_password']
+        );
+        $holds_response_array = $response->PatronHoldRequestsGetRows;
+        foreach ($holds_response_array as $holds_response) {
+
+            $create = $this->formatJSONTime($holds_response->ActivationDate);
+            $expire = $this->formatJSONTime($holds_response->ExpirationDate);
+            $holds[] = array(
+                'id'             => $holds_response->BibID,
+                'location' => $holds_response->PickupBranchName,
+                'reqnum'     => $holds_response->HoldRequestID,
+                'expire'     => $expire,
+                'create'     => $create,
+                'position' => $holds_response->QueuePosition,
+                'title'      => $holds_response->Title,
+            );
+
+        }
+        return $holds;
+    }
+
+    /**
+     * Get Status
+     *
+     * This is responsible for retrieving the status information of a certain
+     * record.
+     *
+     * @param string $id The record id to retrieve the holdings for
+     *
+     * @return mixed     On success, an associative array with the following keys:
+     * id, availability (boolean), status, location, reserve, callnumber.
+     */
+    public function getStatus($id)
+    {
+        $holding = array();
+        $response = $this->makeRequest("bib/$id/holdings");
+        $holdings_response_array = $response->BibHoldingsGetRows;
+
+        $copy_count = 0;
+        foreach ($holdings_response_array as $holdings_response) {
+            //$holdings_response = $holdings_response_array[0];
+            $copy_count++;
+
+            $availability = 0;
+            if (($holdings_response->CircStatus == 'In')
+                || ($holdings_response->CircStatus == 'Just Returned')
+                || ($holdings_response->CircStatus == 'On Shelf')
+            ) {
+                $availability = 1;
+            }
+
+            $duedate = '';
+            if ($holdings_response->DueDate) {
+                $duedate = date("n-j-Y", strtotime($holdings_response->DueDate));
+            }
+
+            $holding[] = array(
+                'availability' => $availability,
+                'id'                 => $id,
+                'status'         => $holdings_response->CircStatus,
+                'location'   => $holdings_response->LocationName,
+                //'reserve'      => 'No',
+                'callnumber' => $holdings_response->CallNumber,
+                'duedate'    => $duedate,
+                //'number'   => $holdings_response->ItemsIn,
+                'number'         => $copy_count,
+                'barcode'    => $holdings_response->Barcode,
+            );
+
+        }
+        return $holding;
+    }
+
+    /**
+     * Get Statuses
+     *
+     * This is responsible for retrieving the status information for a
+     * collection of records.
+     *
+     * @param array $ids The array of record ids to retrieve the status for
+     *
+     * @return mixed         An array of getStatus() return values on success.
+     */
+    public function getStatuses($ids)
+    {
+        $items = array();
+        $count = 0;
+        foreach ($ids as $id) {
+            $items[$count] = $this->getStatus($id);
+            $count++;
+        }
+        return $items;
+    }
+
+    /**
+     * Public Function which retrieves renew, hold and cancel settings from the
+     * driver ini file.
+     *
+     * @param string $function The name of the feature to be checked
+     *
+     * @return array An array with key-value pairs.
+     */
+    public function getConfig($function)
+    {
+        if (isset($this->config[$function]) ) {
+            $functionConfig = $this->config[$function];
+        } else {
+            $functionConfig = false;
+        }
+        return $functionConfig;
+    }
+
+    /**
+     * Get Holding
+     *
+     * This is responsible for retrieving the holding information of a certain
+     * record.
+     *
+     * @param string $id     The record id to retrieve the holdings for
+     * @param array  $patron Patron data
+     *
+     * @return mixed         On success, an associative array with the following
+     * keys: id, availability (boolean), status, location, reserve, callnumber,
+     * duedate, number, barcode.
+     */
+    public function getHolding($id, $patron = false)
+    {
+        return $this->getStatus($id);
+    }
+
+    /**
+     * Place Hold
+     *
+     * Attempts to place a hold or recall on a particular item and returns
+     * an array with result details.
+     *
+     * @param array $holdDetails An array of item and patron data
+     *
+     * @return mixed An array of data on the request including
+     * whether or not it was successful and a system message (if available)
+     */
+    public function placeHold($holdDetails)
+    {
+        $api_query = 'holdrequest';
+        $http_method = 'POST';
+
+        // what do workstation & userid really mean in this context?
+        $workstationid = '1';
+        $userid = '1';
+
+        // all activations are for now(), for now.
+        // microtime is msec or sec?? seems to have changed
+        $activationdate = '/Date(' . intval(microtime(true) * 1000) .')/';
+
+        $jsonrequest = array(
+            'PatronID' => $holdDetails['patron']['id'],
+            'BibID'      => $holdDetails['id'],
+            'ItemBarcode'  => '',
+            'VolumeNumber' => '',
+            'Designation'  => '',
+            'PickupOrgID'       => $holdDetails['pickUpLocation'],
+            'IsBorrowByMail'    => '0',
+            'PatronNotes'       => $holdDetails['comment'],
+            'ActivationDate'    => $activationdate,
+            'WorkstationID'     => $workstationid,
+            'UserID'                    => $userid,
+            'RequestingOrgID' => $this->ws_requestingorgid,
+            'TargetGUID'            => '',
+        );
+
+        $response = $this->makeRequest('holdrequest', 'POST', '', $jsonrequest);
+
+        if ($response->StatusValue == 1) {
+            return array('success' => true,  'sysMessage' => $response->Message);
+        } else {
+            return array('success' => false, 'sysMessage' => $response->Message);
+        }
+
+    }
+
+    /**
+     * Get Pick Up Locations
+     *
+     * This is responsible for gettting a list of valid library locations for
+     * holds / recall retrieval
+     *
+     * @param array $patron      Patron information returned by the patronLogin
+     * method.
+     * @param array $holdDetails Optional array, only passed in when getting a list
+     * in the context of placing a hold; contains most of the same values passed to
+     * placeHold, minus the patron data.    May be used to limit the pickup options
+     * or may be ignored.  The driver must not add new options to the return array
+     * based on this data or other areas of VuFind may behave incorrectly.
+     *
+     * @throws ILSException
+     * @return array             An array of associative arrays with locationID
+     * and locationDisplay keys
+     */
+    public function getPickUpLocations($patron = false, $holdDetails = null)
+    {
+        if (isset($this->ws_pickUpLocations)) {
+            // hardcoded pickup locations in the .ini file? or...
+            foreach ($this->ws_pickUpLocations as $code => $library) {
+                $locations[] = array(
+                    'locationID'            => $code,
+                    'locationDisplay' => $library
+                );
+            }
+        } else {
+            // we get them from the API
+            $response = $this->makeRequest("organizations/branch");
+            $locations_response_array = $response->OrganizationsGetRows;
+            foreach ($locations_response_array as $location_response) {
+                $locations[] = array(
+                    'locationID'            => $location_response->OrganizationID,
+                    'locationDisplay' => $location_response->Name,
+                );
+            }
+        }
+        return $locations;
+    }
+
+    /**
+     * Get Default Pick Up Location
+     *
+     * Returns the default pick up location set in VoyagerRestful.ini
+     *
+     * @param array $patron      Patron information returned by the patronLogin
+     * method.
+     * @param array $holdDetails Optional array, only passed in when getting a list
+     * in the context of placing a hold; contains most of the same values passed to
+     * placeHold, minus the patron data.    May be used to limit the pickup options
+     * or may be ignored.
+     *
+     * @return string           The default pickup location for the patron.
+     */
+    public function getDefaultPickUpLocation($patron = false, $holdDetails = null)
+    {
+        return $this->defaultPickUpLocation;
+    }
+
+    /**
+     * Get Purchase History
+     *
+     * This is responsible for retrieving the acquisitions history data for the
+     * specific record (usually recently received issues of a serial).
+     *
+     * @param string $id The record id to retrieve the info for
+     *
+     * @return mixed         An array with the acquisitions data on success.
+     */
+    public function getPurchaseHistory($id)
+    {
+        return array();
+    }
+
+    /**
+     * Get New Items
+     *
+     * Retrieve the IDs of items recently added to the catalog.
+     *
+     * @param int $page    Page number of results to retrieve (counting starts at 1)
+     * @param int $limit   The size of each page of results to retrieve
+     * @param int $daysOld The maximum age of records to retrieve in days (max. 30)
+     * @param int $fundId  optional fund ID to use for limiting results (use a value
+     * returned by getFunds, or exclude for no limit); note that "fund" may be a
+     * misnomer - if funds are not an appropriate way to limit your new item
+     * results, you can return a different set of values from getFunds. The
+     * important thing is that this parameter supports an ID returned by getFunds,
+     * whatever that may mean.
+     *
+     * @return array             Associative array with 'count' and 'results' keys
+     */
+    public function getNewItems($page, $limit, $daysOld, $fundId = null)
+    {
+        return array('count' => 0, 'results' => array());
+    }
+
+    /**
+     * Find Reserves
+     *
+     * Obtain information on course reserves.
+     *
+     * @param string $course ID from getCourses (empty string to match all)
+     * @param string $inst   ID from getInstructors (empty string to match all)
+     * @param string $dept   ID from getDepartments (empty string to match all)
+     *
+     * @return mixed An array of associative arrays representing reserve items.
+     */
+    public function findReserves($course, $inst, $dept)
+    {
+        return array();
+    }
+
+    /**
+     * Patron Login
+     *
+     * This is responsible for authenticating a patron against the catalog.
+     *
+     * @param string $username The patron username
+     * @param string $password The patron password
+     *
+     * @return mixed           Associative array of patron info on successful login,
+     * null on unsuccessful login.
+     */
+    public function patronLogin($username, $password)
+    {
+        // username == barcode
+        $response = $this->makeRequest("patron/$username", "GET", "$password");
+
+        if (!$response->ValidPatron) {
+            return null;
+        }
+
+        $user = array();
+
+        $user['id']                     = $response->PatronID;
+        $user['firstname']      = null;
+        $user['lastname']       = null;
+        $user['cat_username'] = $response->Barcode;
+        $user['cat_password'] = $password;
+        $user['email']              = null;
+        $user['major']              = null;
+        $user['college']            = null;
+
+        return $user;
+    }
+
+    /**
+     * Get Patron Profile
+     *
+     * This is responsible for retrieving the profile for a specific patron.
+     *
+     * @param array $userinfo The patron array
+     *
+     * @throws ILSException
+     * @return array          Array of the patron's profile data on success.
+     */
+    public function getMyProfile($userinfo)
+    {
+        return $userinfo;
+    }
+}
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PolarisTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PolarisTest.php
new file mode 100644
index 00000000000..ec0f7e86ab5
--- /dev/null
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PolarisTest.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * ILS driver test
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2011.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Tests
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://www.vufind.org  Main Page
+ */
+namespace VuFindTest\ILS\Driver;
+use VuFind\ILS\Driver\Polaris;
+
+/**
+ * ILS driver test
+ *
+ * @category VuFind2
+ * @package  Tests
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://www.vufind.org  Main Page
+ */
+class PolarisTest extends \VuFindTest\Unit\TestCase
+{
+    /**
+     * Driver object
+     *
+     * @var Polaris
+     */
+    protected $driver;
+
+    /**
+     * Constructor
+     */
+    public function __construct()
+    {
+        $this->driver = new Polaris();
+    }
+
+    /**
+     * Test that driver complains about missing configuration.
+     *
+     * @return void
+     */
+    public function testMissingConfiguration()
+    {
+        $this->setExpectedException('VuFind\Exception\ILS');
+        $this->driver->init();
+    }
+}
\ No newline at end of file
-- 
GitLab