From a51452701d6e047e19ac0f564a4518585c4c0a11 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Wed, 11 Jul 2018 00:19:48 +0100 Subject: [PATCH 01/35] Rename class to SIP2Client and adopt PSR-2 formatting and PSR-4 autoloading Change to MIT with permission of original author Improve composer.json with addition targets for testing and formatting Add additional documentation files --- .editorconfig | 15 + .gitattributes | 11 + .gitignore | 4 + .scrutinizer.yml | 23 + .styleci.yml | 1 + .travis.yml | 37 ++ CHANGELOG.md | 27 ++ CONTRIBUTING.md | 32 ++ ISSUE_TEMPLATE.md | 27 ++ LICENSE.md | 22 + PULL_REQUEST_TEMPLATE.md | 43 ++ README.md | 125 ++++-- composer.json | 54 ++- phpunit.xml.dist | 29 ++ sip2.class.php | 858 ------------------------------------- src/SIP2Client.php | 883 +++++++++++++++++++++++++++++++++++++++ 16 files changed, 1285 insertions(+), 906 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .scrutinizer.yml create mode 100644 .styleci.yml create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 ISSUE_TEMPLATE.md create mode 100644 LICENSE.md create mode 100644 PULL_REQUEST_TEMPLATE.md create mode 100644 phpunit.xml.dist delete mode 100644 sip2.class.php create mode 100644 src/SIP2Client.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cd8eb86 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3286141 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/phpunit.xml.dist export-ignore +/.scrutinizer.yml export-ignore +/tests export-ignore +/docs export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a16e58b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build +composer.lock +vendor +.idea diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..aea6609 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,23 @@ +filter: + excluded_paths: [tests/*] + +checks: + php: + remove_extra_empty_lines: true + remove_php_closing_tag: true + remove_trailing_whitespace: true + fix_use_statements: + remove_unused: true + preserve_multiple: false + preserve_blanklines: true + order_alphabetically: true + fix_php_opening_tag: true + fix_linefeed: true + fix_line_ending: true + fix_identation_4spaces: true + fix_doc_comments: true + +tools: + external_code_coverage: + timeout: 600 + runs: 3 diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..247a09c --- /dev/null +++ b/.styleci.yml @@ -0,0 +1 @@ +preset: psr2 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e3731a8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,37 @@ +dist: trusty +language: php + +php: + - 5.6 + - 7.0 + - 7.1 + - 7.2 + - hhvm + +# This triggers builds to run on the new TravisCI infrastructure. +# See: http://docs.travis-ci.com/user/workers/container-based-infrastructure/ +sudo: false + +## Cache composer +cache: + directories: + - $HOME/.composer/cache + +matrix: + include: + - php: 5.6 + env: 'COMPOSER_FLAGS="--prefer-stable --prefer-lowest"' + +before_script: + - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-dist + +script: + - vendor/bin/phpcs --standard=psr2 src/ + - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover + +after_script: + - | + if [[ "$TRAVIS_PHP_VERSION" != 'hhvm' && "$TRAVIS_PHP_VERSION" != '7.0' ]]; then + wget https://scrutinizer-ci.com/ocular.phar + php ocular.phar code-coverage:upload --format=php-clover coverage.clover + fi diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b04c0c1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog + +All notable changes to `php-sip2` will be documented in this file. + +Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. + +## 2.0.0 - 2018-07-xx + +### Added +- MIT License adopted - prior releases were GPL + +### Deprecated +- Nothing + +### Fixed +- Nothing + +### Removed +- Nothing + +### Security +- Nothing + + +## 1.0.0 - 2015-11-03 + +- First release, GPL licensed \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d0f82b7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +We accept contributions via Pull Requests on [Github](https://github.com/lordelph/php-sip2). + + +## Pull Requests + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - Check the code style with ``$ composer check-style`` and fix it with ``$ composer fix-style``. + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. + +- **Create feature branches** - Don't ask us to pull from your master branch. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + + +## Running Tests + +``` bash +$ composer test +``` + + +**Happy coding**! diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..5b48c57 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,27 @@ + + +## Detailed description + +Provide a detailed description of the change or addition you are proposing. + +Make it clear if the issue is a bug, an enhancement or just a question. + +## Context + +Why is this change important to you? How would you use it? + +How can it benefit other users? + +## Possible implementation + +Not obligatory, but suggest an idea for implementing addition or change. + +## Your environment + +Include as many relevant details about the environment you experienced the bug in and how to reproduce it. + +* Version used (e.g. PHP 5.6, HHVM 3): +* Operating system and version (e.g. Ubuntu 16.04, Windows 7): +* Link to your project: +* ... +* ... diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..980d5a5 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,22 @@ +# The MIT License (MIT) + +Copyright (c) 2015 John Wohlers +Copyright (c) 2018 Paul Dixon + +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..86246b3 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,43 @@ + + +## Description + +Describe your changes in detail. + +## Motivation and context + +Why is this change required? What problem does it solve? + +If it fixes an open issue, please link to the issue here (if you write `fixes #num` +or `closes #num`, the issue will be automatically closed when the pull is accepted.) + +## How has this been tested? + +Please describe in detail how you tested your changes. + +Include details of your testing environment, and the tests you ran to +see how your change affects other areas of the code, etc. + +## Screenshots (if appropriate) + +## Types of changes + +What types of changes does your code introduce? Put an `x` in all the boxes that apply: +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) + +## Checklist: + +Go over all the following points, and put an `x` in all the boxes that apply. + +Please, please, please, don't send your pull request until all of the boxes are ticked. Once your pull request is created, it will trigger a build on our [continuous integration](http://www.phptherightway.com/#continuous-integration) server to make sure your [tests and code style pass](https://help.github.com/articles/about-required-status-checks/). + +- [ ] I have read the **[CONTRIBUTING](CONTRIBUTING.md)** document. +- [ ] My pull request addresses exactly one patch/feature. +- [ ] I have created a branch for this patch/feature. +- [ ] Each individual commit in the pull request is meaningful. +- [ ] I have added tests to cover my changes. +- [ ] If my change requires a change to the documentation, I have updated it accordingly. + +If you're unsure about any of these, don't hesitate to ask. We're here to help! diff --git a/README.md b/README.md index 8f63e93..abd0323 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,97 @@ -# SIP2 communication library for PHP +# php-sip2 -PHP class library to facilitate communication with Integrated Library System (ILS) servers via 3M's SIP2. +[![Latest Version on Packagist][ico-version]][link-packagist] +[![Software License][ico-license]](LICENSE.md) +[![Build Status][ico-travis]][link-travis] +[![Coverage Status][ico-scrutinizer]][link-scrutinizer] +[![Quality Score][ico-code-quality]][link-code-quality] +[![Total Downloads][ico-downloads]][link-downloads] -## Composer Installation +PHP client library to facilitate communication with Integrated Library System (ILS) servers via 3M's SIP2. -To install this package, run this command: -```sh -composer require cap60552/php-sip2 +## ToDo +- abstract socket into separate class https://github.com/clue/php-socket-raw +- make socket factory which can be given to SIP2Client +- now can make unit tests which simulate socket connections + + +## Install + +Via Composer + +``` bash +$ composer require lordelph/php-sip2 +``` + +## Usage + +``` php +// create object +$mysip = new lordelph\SIP2\SIP2Client; + +// Set host name +$mysip->hostname = 'server.example.com'; +$mysip->port = 6002; + +// Identify a patron +$mysip->patron = '101010101'; +$mysip->patronpwd = '010101'; + +// connect to SIP server +$result = $mysip->connect(); + +// Get Charged Items Raw response +$in = $mysip->msgPatronInformation('charged'); + +// parse the raw response into an array +$result = $mysip->parsePatronInfoResponse( $mysip->get_message($in) ); ``` -## General Installation -Copy the sip2.class.php file to a location in your php_include path. - -## General Usage - - // create object - $mysip = new sip2; - - // Set host name - $mysip->hostname = 'server.example.com'; - $mysip->port = 6002; - - // Identify a patron - $mysip->patron = '101010101'; - $mysip->patronpwd = '010101'; - - // connect to SIP server - $result = $mysip->connect(); - - // Get Charged Items Raw response - $in = $mysip->msgPatronInformation('charged'); - - // parse the raw response into an array - $result = $mysip->parsePatronInfoResponse( $mysip->get_message($in) ); - -## Contribution - -Feel free to contribute! \ No newline at end of file +## Change log + +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. + +## Testing + +``` bash +$ composer test +``` + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +## Security + +If you discover any security related issues, please email paul@elphin.com instead of using the +issue tracker. + +## Credits + +- [John Wohlers][link-author1] +- [Paul Dixon][link-author2] +- [All Contributors][link-contributors] + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. + +Note that prior to v2.0.0, the GPL licence was used. The original author, John Wohlers, kindly +agreed to allow the MIT license terms. + +[ico-version]: https://img.shields.io/packagist/v/lordelph/php-sip2.svg?style=flat-square +[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square +[ico-travis]: https://img.shields.io/travis/lordelph/php-sip2/master.svg?style=flat-square +[ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/lordelph/php-sip2.svg?style=flat-square +[ico-code-quality]: https://img.shields.io/scrutinizer/g/lordelph/php-sip2.svg?style=flat-square +[ico-downloads]: https://img.shields.io/packagist/dt/lordelph/php-sip2.svg?style=flat-square + +[link-packagist]: https://packagist.org/packages/lordelph/php-sip2 +[link-travis]: https://travis-ci.org/lordelph/php-sip2 +[link-scrutinizer]: https://scrutinizer-ci.com/g/lordelph/php-sip2/code-structure +[link-code-quality]: https://scrutinizer-ci.com/g/lordelph/php-sip2 +[link-downloads]: https://packagist.org/packages/lordelph/php-sip2 +[link-author1]: https://github.com/cap60552 +[link-author2]: https://github.com/lordelph +[link-contributors]: ../../contributors diff --git a/composer.json b/composer.json index 00f77be..e5f020f 100644 --- a/composer.json +++ b/composer.json @@ -1,23 +1,49 @@ { - "name": "cap60552/php-sip2", - "description": "PHP class library to facilitate communication with Integrated Library System (ILS) servers via 3M's SIP2.", - "type": "library", + "name": "lordelph/php-sip2", + "type": "library", + "description": "Communicate with Integrated Library System (ILS) servers via 3M's SIP2", + "keywords": [ + "lordelph", + "php-sip2" + ], + "homepage": "https://github.com/lordelph/php-sip2", + "license": "MIT", "authors": [ { - "name": "John Wohlers", - "email": "john@wohlershome.net", - "role": "Maintainer" + "name": "Paul Dixon", + "email": "paul@elphin.com", + "homepage": "https://github.com/lordelph", + "role": "Developer" } ], - "homepage": "https://github.com/cap60552/php-sip2", - "license": "GPL-3.0", - "support": { - "issues": "https://github.com/cap60552/php-sip2/issues", - "source": "https://github.com/cap60552/php-sip2", - "docs": "https://github.com/cap60552/php-sip2/tree/master/doc" + "require": { + "php" : "~5.6|~7.0" + }, + "require-dev": { + "phpunit/phpunit" : ">=5.4.3", + "squizlabs/php_codesniffer": "^2.3" }, "autoload": { - "classmap": ["/"] + "psr-4": { + "lordelph\\SIP2\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "lordelph\\SIP2\\": "tests" + } + }, + "scripts": { + "test": "phpunit", + "check-style": "phpcs -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src tests", + "fix-style": "phpcbf -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src tests" + }, + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "config": { + "sort-packages": true } } - diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..1249077 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + tests + + + + + src/ + + + + + + + + + + diff --git a/sip2.class.php b/sip2.class.php deleted file mode 100644 index 5a37d1f..0000000 --- a/sip2.class.php +++ /dev/null @@ -1,858 +0,0 @@ - -* @licence http://opensource.org/licenses/gpl-3.0.html -* @copyright John Wohlers -* @version 1.0.1 -* @link https://github.com/cap60552/php-sip2/ -*/ - -/** -* -* TODO -* - Clean up variable names, check for consistency -* - Add better i18n support, including functions to handle the SIP2 language definitions -* -*/ - - -class sip2 -{ - - /* Public variables for configuration */ - public $hostname; - public $port = 6002; /* default sip2 port for Sirsi */ - public $library = ''; - public $language = '001'; /* 001= english */ - - /* Patron ID */ - public $patron = ''; /* AA */ - public $patronpwd = ''; /* AD */ - - /* Terminal password */ - public $AC = ''; /*AC */ - - /* Maximum number of resends allowed before get_message gives up */ - public $maxretry = 3; - - /* Terminators - * - * From page 15 of SPI2 v2 docs: - * All messages must end in a carriage return (hexadecimal 0d). - * - * Technically $msgTerminator should be set to \r only. However some vendors mistakenly require the \r\n. - * - * TODO: Create a function to set this, rather than exposing the variable as public. - */ - public $msgTerminator = "\r\n"; - - public $fldTerminator = '|'; - - /* Login Variables */ - public $UIDalgorithm = 0; /* 0 = unencrypted, default */ - public $PWDalgorithm = 0; /* undefined in documentation */ - public $scLocation = ''; /* Location Code */ - - /* Debug */ - public $debug = false; - - /* Public variables used for building messages */ - public $AO = 'WohlersSIP'; - public $AN = 'SIPCHK'; - - /* Private variable to hold socket connection */ - private $socket; - - /* Sequence number counter */ - private $seq = -1; - - /* resend counter */ - private $retry = 0; - - /* Work area for building a message */ - private $msgBuild = ''; - private $noFixed = false; - - function msgPatronStatusRequest() - { - /* Server Response: Patron Status Response message. */ - $this->_newMessage('23'); - $this->_addFixedOption($this->language, 3); - $this->_addFixedOption($this->_datestamp(), 18); - $this->_addVarOption('AO',$this->AO); - $this->_addVarOption('AA',$this->patron); - $this->_addVarOption('AC',$this->AC); - $this->_addVarOption('AD',$this->patronpwd); - return $this->_returnMessage(); - } - - function msgCheckout($item, $nbDateDue ='', $scRenewal='N', $itmProp ='', $fee='N', $noBlock='N', $cancel='N') - { - /* Checkout an item (11) - untested */ - $this->_newMessage('11'); - $this->_addFixedOption($scRenewal, 1); - $this->_addFixedOption($noBlock, 1); - $this->_addFixedOption($this->_datestamp(), 18); - if ($nbDateDue != '') { - /* override default date due */ - $this->_addFixedOption($this->_datestamp($nbDateDue), 18); - } else { - /* send a blank date due to allow ACS to use default date due computed for item */ - $this->_addFixedOption('', 18); - } - $this->_addVarOption('AO',$this->AO); - $this->_addVarOption('AA',$this->patron); - $this->_addVarOption('AB',$item); - $this->_addVarOption('AC',$this->AC); - $this->_addVarOption('CH',$itmProp, true); - $this->_addVarOption('AD',$this->patronpwd, true); - $this->_addVarOption('BO',$fee, true); /* Y or N */ - $this->_addVarOption('BI',$cancel, true); /* Y or N */ - - return $this->_returnMessage(); - } - - function msgCheckin($item, $itmReturnDate, $itmLocation = '', $itmProp = '', $noBlock='N', $cancel = '') - { - /* Check-in an item (09) - untested */ - if ($itmLocation == '') { - /* If no location is specified, assume the default location of the SC, behaviour suggested by spec*/ - $itmLocation = $this->scLocation; - } - - $this->_newMessage('09'); - $this->_addFixedOption($noBlock, 1); - $this->_addFixedOption($this->_datestamp(), 18); - $this->_addFixedOption($this->_datestamp($itmReturnDate), 18); - $this->_addVarOption('AP',$itmLocation); - $this->_addVarOption('AO',$this->AO); - $this->_addVarOption('AB',$item); - $this->_addVarOption('AC',$this->AC); - $this->_addVarOption('CH',$itmProp, true); - $this->_addVarOption('BI',$cancel, true); /* Y or N */ - - return $this->_returnMessage(); - } - - function msgBlockPatron($message, $retained='N') - { - /* Blocks a patron, and responds with a patron status response (01) - untested */ - $this->_newMessage('01'); - $this->_addFixedOption($retained, 1); /* Y if card has been retained */ - $this->_addFixedOption($this->_datestamp(), 18); - $this->_addVarOption('AO',$this->AO); - $this->_addVarOption('AL',$message); - $this->_addVarOption('AA',$this->AA); - $this->_addVarOption('AC',$this->AC); - - return $this->_returnMessage(); - } - - function msgSCStatus($status = 0, $width = 80, $version = 2) - { - /* selfcheck status message, this should be sent immediately after login - untested */ - /* status codes, from the spec: - * 0 SC unit is OK - * 1 SC printer is out of paper - * 2 SC is about to shut down - */ - - if ($version > 3) { - $version = 2; - } - if ($status < 0 || $status > 2) { - $this->_debugmsg( "SIP2: Invalid status passed to msgSCStatus" ); - return false; - } - $this->_newMessage('99'); - $this->_addFixedOption($status, 1); - $this->_addFixedOption($width, 3); - $this->_addFixedOption(sprintf("%03.2f",$version), 4); - return $this->_returnMessage(); - } - - function msgRequestACSResend () - { - /* Used to request a resend due to CRC mismatch - No sequence number is used */ - $this->_newMessage('97'); - return $this->_returnMessage(false); - } - - function msgLogin($sipLogin, $sipPassword) - { - /* Login (93) - untested */ - $this->_newMessage('93'); - $this->_addFixedOption($this->UIDalgorithm, 1); - $this->_addFixedOption($this->PWDalgorithm, 1); - $this->_addVarOption('CN',$sipLogin); - $this->_addVarOption('CO',$sipPassword); - $this->_addVarOption('CP',$this->scLocation, true); - return $this->_returnMessage(); - - } - - function msgPatronInformation($type, $start = '1', $end = '5') - { - - /* - * According to the specification: - * Only one category of items should be requested at a time, i.e. it would take 6 of these messages, - * each with a different position set to Y, to get all the detailed information about a patron's items. - */ - $summary['none'] = ' '; - $summary['hold'] = 'Y '; - $summary['overdue'] = ' Y '; - $summary['charged'] = ' Y '; - $summary['fine'] = ' Y '; - $summary['recall'] = ' Y '; - $summary['unavail'] = ' Y'; - - /* Request patron information */ - $this->_newMessage('63'); - $this->_addFixedOption($this->language, 3); - $this->_addFixedOption($this->_datestamp(), 18); - $this->_addFixedOption(sprintf("%-10s",$summary[$type]), 10); - $this->_addVarOption('AO',$this->AO); - $this->_addVarOption('AA',$this->patron); - $this->_addVarOption('AC',$this->AC, true); - $this->_addVarOption('AD',$this->patronpwd, true); - $this->_addVarOption('BP',$start, true); /* old function version used padded 5 digits, not sure why */ - $this->_addVarOption('BQ',$end, true); /* old function version used padded 5 digits, not sure why */ - return $this->_returnMessage(); - } - - function msgEndPatronSession() - { - /* End Patron Session, should be sent before switching to a new patron. (35) - untested */ - - $this->_newMessage('35'); - $this->_addFixedOption($this->_datestamp(), 18); - $this->_addVarOption('AO',$this->AO); - $this->_addVarOption('AA',$this->patron); - $this->_addVarOption('AC',$this->AC, true); - $this->_addVarOption('AD',$this->patronpwd, true); - return $this->_returnMessage(); - } - - /* Fee paid function should go here */ - function msgFeePaid ($feeType, $pmtType, $pmtAmount, $curType = 'USD', $feeId = '', $transId = '') - { - /* Fee payment function (37) - untested */ - /* Fee Types: */ - /* 01 other/unknown */ - /* 02 administrative */ - /* 03 damage */ - /* 04 overdue */ - /* 05 processing */ - /* 06 rental*/ - /* 07 replacement */ - /* 08 computer access charge */ - /* 09 hold fee */ - - /* Value Payment Type */ - /* 00 cash */ - /* 01 VISA */ - /* 02 credit card */ - - if (!is_numeric($feeType) || $feeType > 99 || $feeType < 1) { - /* not a valid fee type - exit */ - $this->_debugmsg( "SIP2: (msgFeePaid) Invalid fee type: {$feeType}"); - return false; - } - - if (!is_numeric($pmtType) || $pmtType > 99 || $pmtType < 0) { - /* not a valid payment type - exit */ - $this->_debugmsg( "SIP2: (msgFeePaid) Invalid payment type: {$pmtType}"); - return false; - } - - $this->_newMessage('37'); - $this->_addFixedOption($this->_datestamp(), 18); - $this->_addFixedOption(sprintf('%02d', $feeType), 2); - $this->_addFixedOption(sprintf('%02d', $pmtType), 2); - $this->_addFixedOption($curType, 3); - $this->_addVarOption('BV',$pmtAmount); /* due to currency format localization, it is up to the programmer to properly format their payment amount */ - $this->_addVarOption('AO',$this->AO); - $this->_addVarOption('AA',$this->patron); - $this->_addVarOption('AC',$this->AC, true); - $this->_addVarOption('AD',$this->patronpwd, true); - $this->_addVarOption('CG',$feeId, true); - $this->_addVarOption('BK',$transId, true); - - return $this->_returnMessage(); - } - - function msgItemInformation($item) - { - - $this->_newMessage('17'); - $this->_addFixedOption($this->_datestamp(), 18); - $this->_addVarOption('AO',$this->AO); - $this->_addVarOption('AB',$item); - $this->_addVarOption('AC',$this->AC, true); - return $this->_returnMessage(); - - } - - function msgItemStatus ($item, $itmProp = '') - { - /* Item status update function (19) - untested */ - - $this->_newMessage('19'); - $this->_addFixedOption($this->_datestamp(), 18); - $this->_addVarOption('AO',$this->AO); - $this->_addVarOption('AB',$item); - $this->_addVarOption('AC',$this->AC, true); - $this->_addVarOption('CH',$itmProp); - return $this->_returnMessage(); - } - - function msgPatronEnable () - { - /* Patron Enable function (25) - untested */ - /* This message can be used by the SC to re-enable cancelled patrons. It should only be used for system testing and validation. */ - $this->_newMessage('25'); - $this->_addFixedOption($this->_datestamp(), 18); - $this->_addVarOption('AO',$this->AO); - $this->_addVarOption('AA',$this->patron); - $this->_addVarOption('AC',$this->AC, true); - $this->_addVarOption('AD',$this->patronpwd, true); - return $this->_returnMessage(); - - } - - function msgHold($mode, $expDate = '', $holdtype = '', $item = '', $title = '', $fee='N', $pkupLocation = '') - { - /* mode validity check */ - /* - * - remove hold - * + place hold - * * modify hold - */ - if (strpos('-+*',$mode) === false) { - /* not a valid mode - exit */ - $this->_debugmsg( "SIP2: Invalid hold mode: {$mode}"); - return false; - } - - if ($holdtype != '' && ($holdtype < 1 || $holdtype > 9)) { - /* - * Valid hold types range from 1 - 9 - * 1 other - * 2 any copy of title - * 3 specific copy - * 4 any copy at a single branch or location - */ - $this->_debugmsg( "SIP2: Invalid hold type code: {$holdtype}"); - return false; - } - - $this->_newMessage('15'); - $this->_addFixedOption($mode, 1); - $this->_addFixedOption($this->_datestamp(), 18); - if ($expDate != '') { - /* hold expiration date, due to the use of the datestamp function, we have to check here for empty value. when datestamp is passed an empty value it will generate a current datestamp */ - $this->_addVarOption('BW', $this->_datestamp($expDate), true); /*spec says this is fixed field, but it behaves like a var field and is optional... */ - } - $this->_addVarOption('BS',$pkupLocation, true); - $this->_addVarOption('BY',$holdtype, true); - $this->_addVarOption('AO',$this->AO); - $this->_addVarOption('AA',$this->patron); - $this->_addVarOption('AD',$this->patronpwd, true); - $this->_addVarOption('AB',$item, true); - $this->_addVarOption('AJ',$title, true); - $this->_addVarOption('AC',$this->AC, true); - $this->_addVarOption('BO',$fee, true); /* Y when user has agreed to a fee notice */ - - return $this->_returnMessage(); - - } - - function msgRenew($item = '', $title = '', $nbDateDue = '', $itmProp = '', $fee= 'N', $noBlock = 'N', $thirdParty = 'N') - { - /* renew a single item (29) - untested */ - $this->_newMessage('29'); - $this->_addFixedOption($thirdParty, 1); - $this->_addFixedOption($noBlock, 1); - $this->_addFixedOption($this->_datestamp(), 18); - if ($nbDateDue != '') { - /* override default date due */ - $this->_addFixedOption($this->_datestamp($nbDateDue), 18); - } else { - /* send a blank date due to allow ACS to use default date due computed for item */ - $this->_addFixedOption('', 18); - } - $this->_addVarOption('AO',$this->AO); - $this->_addVarOption('AA',$this->patron); - $this->_addVarOption('AD',$this->patronpwd, true); - $this->_addVarOption('AB',$item, true); - $this->_addVarOption('AJ',$title, true); - $this->_addVarOption('AC',$this->AC, true); - $this->_addVarOption('CH',$itmProp, true); - $this->_addVarOption('BO',$fee, true); /* Y or N */ - - return $this->_returnMessage(); - } - - function msgRenewAll($fee = 'N') - { - /* renew all items for a patron (65) - untested */ - $this->_newMessage('65'); - $this->_addVarOption('AO',$this->AO); - $this->_addVarOption('AA',$this->patron); - $this->_addVarOption('AD',$this->patronpwd, true); - $this->_addVarOption('AC',$this->AC, true); - $this->_addVarOption('BO',$fee, true); /* Y or N */ - - return $this->_returnMessage(); - } - - function parsePatronStatusResponse($response) - { - $result['fixed'] = - array( - 'PatronStatus' => substr($response, 2, 14), - 'Language' => substr($response, 16, 3), - 'TransactionDate' => substr($response, 19, 18), - ); - - $result['variable'] = $this->_parsevariabledata($response, 37); - return $result; - } - - function parseCheckoutResponse($response) - { - $result['fixed'] = - array( - 'Ok' => substr($response,2,1), - 'RenewalOk' => substr($response,3,1), - 'Magnetic' => substr($response,4,1), - 'Desensitize' => substr($response,5,1), - 'TransactionDate' => substr($response,6,18), - ); - - $result['variable'] = $this->_parsevariabledata($response, 24); - return $result; - - } - - function parseCheckinResponse($response) - { - $result['fixed'] = - array( - 'Ok' => substr($response,2,1), - 'Resensitize' => substr($response,3,1), - 'Magnetic' => substr($response,4,1), - 'Alert' => substr($response,5,1), - 'TransactionDate' => substr($response,6,18), - ); - - $result['variable'] = $this->_parsevariabledata($response, 24); - return $result; - - } - - function parseACSStatusResponse($response) - { - $result['fixed'] = - array( - 'Online' => substr($response, 2, 1), - 'Checkin' => substr($response, 3, 1), /* is Checkin by the SC allowed ?*/ - 'Checkout' => substr($response, 4, 1), /* is Checkout by the SC allowed ?*/ - 'Renewal' => substr($response, 5, 1), /* renewal allowed? */ - 'PatronUpdate' => substr($response, 6, 1), /* is patron status updating by the SC allowed ? (status update ok)*/ - 'Offline' => substr($response, 7, 1), - 'Timeout' => substr($response, 8, 3), - 'Retries' => substr($response, 11, 3), - 'TransactionDate' => substr($response, 14, 18), - 'Protocol' => substr($response, 32, 4), - ); - - $result['variable'] = $this->_parsevariabledata($response, 36); - return $result; - } - - function parseLoginResponse($response) - { - $result['fixed'] = - array( - 'Ok' => substr($response, 2, 1), - ); - $result['variable'] = array(); - return $result; - } - - function parsePatronInfoResponse($response) - { - - $result['fixed'] = - array( - 'PatronStatus' => substr($response, 2, 14), - 'Language' => substr($response, 16, 3), - 'TransactionDate' => substr($response, 19, 18), - 'HoldCount' => intval (substr($response, 37, 4)), - 'OverdueCount' => intval (substr($response, 41, 4)), - 'ChargedCount' => intval (substr($response, 45, 4)), - 'FineCount' => intval (substr($response, 49, 4)), - 'RecallCount' => intval (substr($response, 53, 4)), - 'UnavailableCount' => intval (substr($response, 57, 4)) - ); - - $result['variable'] = $this->_parsevariabledata($response, 61); - return $result; - } - - function parseEndSessionResponse($response) - { - /* Response example: 36Y20080228 145537AOWOHLERS|AAX00000000|AY9AZF474 */ - - $result['fixed'] = - array( - 'EndSession' => substr($response, 2, 1), - 'TransactionDate' => substr($response, 3, 18), - ); - - - $result['variable'] = $this->_parsevariabledata($response, 21); - - return $result; - } - - function parseFeePaidResponse($response) - { - $result['fixed'] = - array( - 'PaymentAccepted' => substr($response, 2, 1), - 'TransactionDate' => substr($response, 3, 18), - ); - - $result['variable'] = $this->_parsevariabledata($response, 21); - return $result; - - } - - function parseItemInfoResponse($response) - { - $result['fixed'] = - array( - 'CirculationStatus' => intval (substr($response, 2, 2)), - 'SecurityMarker' => intval (substr($response, 4, 2)), - 'FeeType' => intval (substr($response, 6, 2)), - 'TransactionDate' => substr($response, 8, 18), - ); - - $result['variable'] = $this->_parsevariabledata($response, 26); - - return $result; - } - - function parseItemStatusResponse($response) - { - $result['fixed'] = - array( - 'PropertiesOk' => substr($response, 2, 1), - 'TransactionDate' => substr($response, 3, 18), - ); - - $result['variable'] = $this->_parsevariabledata($response, 21); - return $result; - - } - - function parsePatronEnableResponse($response) - { - $result['fixed'] = - array( - 'PatronStatus' => substr($response, 2, 14), - 'Language' => substr($response, 16, 3), - 'TransactionDate' => substr($response, 19, 18), - ); - - $result['variable'] = $this->_parsevariabledata($response, 37); - return $result; - - } - - function parseHoldResponse($response) - { - - $result['fixed'] = - array( - 'Ok' => substr($response, 2, 1), - 'available' => substr($response, 3, 1), - 'TransactionDate' => substr($response, 4, 18), - 'ExpirationDate' => substr($response, 22, 18) - ); - - - $result['variable'] = $this->_parsevariabledata($response, 40); - - return $result; - } - - - function parseRenewResponse($response) - { - /* Response Example: 300NUU20080228 222232AOWOHLERS|AAX00000241|ABM02400028262|AJFolksongs of Britain and Ireland|AH5/23/2008,23:59|CH|AFOverride required to exceed renewal limit.|AY1AZCDA5 */ - $result['fixed'] = - array( - 'Ok' => substr($response, 2, 1), - 'RenewalOk' => substr($response, 3, 1), - 'Magnetic' => substr($response, 4, 1), - 'Desensitize' => substr($response, 5, 1), - 'TransactionDate' => substr($response, 6, 18), - ); - - - $result['variable'] = $this->_parsevariabledata($response, 24); - - return $result; - } - - function parseRenewAllResponse($response) - { - $result['fixed'] = - array( - 'Ok' => substr($response, 2, 1), - 'Renewed' => substr($response, 3, 4), - 'Unrenewed' => substr($response, 7, 4), - 'TransactionDate' => substr($response, 11, 18), - ); - - - $result['variable'] = $this->_parsevariabledata($response, 29); - - return $result; - } - - - - - function get_message ($message) - { - /* sends the current message, and gets the response */ - $result = ''; - $terminator = ''; - $nr = ''; - - $this->_debugmsg('SIP2: Sending SIP2 request...'); - socket_write($this->socket, $message, strlen($message)); - - $this->_debugmsg('SIP2: Request Sent, Reading response'); - - while ($terminator != "\x0D" && $nr !== FALSE) { - $nr = socket_recv($this->socket,$terminator,1,0); - $result = $result . $terminator; - } - - $this->_debugmsg("SIP2: {$result}"); - - /* test message for CRC validity */ - if ($this->_check_crc($result)) { - /* reset the retry counter on successful send */ - $this->retry=0; - $this->_debugmsg("SIP2: Message from ACS passed CRC check"); - } else { - /* CRC check failed, request a resend */ - $this->retry++; - if ($this->retry < $this->maxretry) { - /* try again */ - $this->_debugmsg("SIP2: Message failed CRC check, retrying ({$this->retry})"); - - $this->get_message($message); - } else { - /* give up */ - $this->_debugmsg("SIP2: Failed to get valid CRC after {$this->maxretry} retries."); - return false; - } - } - return $result; - } - - function connect() - { - - /* Socket Communications */ - $this->_debugmsg( "SIP2: --- BEGIN SIP communication ---"); - - /* Get the IP address for the target host. */ - $address = gethostbyname($this->hostname); - - /* Create a TCP/IP socket. */ - $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); - - /* check for actual truly false result using ===*/ - if ($this->socket === false) { - $this->_debugmsg( "SIP2: socket_create() failed: reason: " . socket_strerror($this->socket)); - return false; - } else { - $this->_debugmsg( "SIP2: Socket Created" ); - } - $this->_debugmsg( "SIP2: Attempting to connect to '$address' on port '{$this->port}'..."); - - /* open a connection to the host */ - $result = socket_connect($this->socket, $address, $this->port); - if (!$result) { - $this->_debugmsg("SIP2: socket_connect() failed.\nReason: ($result) " . socket_strerror($result)); - } else { - $this->_debugmsg( "SIP2: --- SOCKET READY ---" ); - } - /* return the result from the socket connect */ - return $result; - - } - - function disconnect () - { - /* Close the socket */ - socket_close($this->socket); - } - - /* Core local utility functions */ - function _datestamp($timestamp = '') - { - /* generate a SIP2 compatible datestamp */ - /* From the spec: - * YYYYMMDDZZZZHHMMSS. - * All dates and times are expressed according to the ANSI standard X3.30 for date and X3.43 for time. - * The ZZZZ field should contain blanks (code $20) to represent local time. To represent universal time, - * a Z character(code $5A) should be put in the last (right hand) position of the ZZZZ field. - * To represent other time zones the appropriate character should be used; a Q character (code $51) - * should be put in the last (right hand) position of the ZZZZ field to represent Atlantic Standard Time. - * When possible local time is the preferred format. - */ - if ($timestamp != '') { - /* Generate a proper date time from the date provided */ - return date('Ymd His', $timestamp); - } else { - /* Current Date/Time */ - return date('Ymd His'); - } - } - - function _parsevariabledata($response, $start) - { - - $result = array(); - $result['Raw'] = explode("|", substr($response,$start,-7)); - foreach ($result['Raw'] as $item) { - $field = substr($item,0,2); - $value = substr($item,2); - /* SD returns some odd values on occasion, Unable to locate the purpose in spec, so I strip from - * the parsed array. Orig values will remain in ['raw'] element - */ - $clean = trim($value, "\x00..\x1F"); - if (trim($clean) <> '') { - $result[$field][] = $clean; - } - } - $result['AZ'][] = substr($response,-5); - - return ($result); - } - - function _crc($buf) - { - /* Calculate CRC */ - $sum = 0; - - $len = strlen($buf); - for ($n = 0; $n < $len; $n++) { - $sum = $sum + ord(substr($buf, $n, 1)); - } - - $crc = ($sum & 0xFFFF) * -1; - - /* 2008.03.15 - Fixed a bug that allowed the checksum to be larger then 4 digits */ - return substr(sprintf ("%4X", $crc), -4, 4); - } /* end crc */ - - function _getseqnum() - { - /* Get a sequence number for the AY field */ - /* valid numbers range 0-9 */ - $this->seq++; - if ($this->seq > 9 ) { - $this->seq = 0; - } - return ($this->seq); - } - - function _debugmsg($message) - { - /* custom debug function, why repeat the check for the debug flag in code... */ - if ($this->debug) { - trigger_error( $message, E_USER_NOTICE); - } - } - - function _check_crc($message) - { - /* test the received message's CRC by generating our own CRC from the message */ - $test = preg_split('/(.{4})$/',trim($message),2,PREG_SPLIT_DELIM_CAPTURE); - - if ($this->_crc($test[0]) == $test[1]) { - return true; - } else { - return false; - } - } - - function _newMessage($code) - { - /* resets the msgBuild variable to the value of $code, and clears the flag for fixed messages */ - $this->noFixed = false; - $this->msgBuild = $code; - } - - function _addFixedOption($value, $len) - { - /* adds a fixed length option to the msgBuild IF no variable options have been added. */ - if ( $this->noFixed ) { - return false; - } else { - $this->msgBuild .= sprintf("%{$len}s", substr($value,0,$len)); - return true; - } - } - - function _addVarOption($field, $value, $optional = false) - { - /* adds a variable length option to the message, and also prevents adding additional fixed fields */ - if ($optional == true && $value == '') { - /* skipped */ - $this->_debugmsg( "SIP2: Skipping optional field {$field}"); - } else { - $this->noFixed = true; /* no more fixed for this message */ - $this->msgBuild .= $field . substr($value, 0, 255) . $this->fldTerminator; - } - return true; - } - - function _returnMessage($withSeq = true, $withCrc = true) - { - /* Finalizes the message and returns it. Message will remain in msgBuild until newMessage is called */ - if ($withSeq) { - $this->msgBuild .= 'AY' . $this->_getseqnum(); - } - if ($withCrc) { - $this->msgBuild .= 'AZ'; - $this->msgBuild .= $this->_crc($this->msgBuild); - } - $this->msgBuild .= $this->msgTerminator; - - return $this->msgBuild; - } - -} - -?> diff --git a/src/SIP2Client.php b/src/SIP2Client.php new file mode 100644 index 0000000..2f4d2b5 --- /dev/null +++ b/src/SIP2Client.php @@ -0,0 +1,883 @@ + + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @version 2.0.0 + * @link https://github.com/cap60552/php-sip2/ + */ + +/** + * + * TODO + * - Clean up variable names, check for consistency + * - Add better i18n support, including functions to handle the SIP2 language definitions + * + */ + +class SIP2Client +{ + + /* Public variables for configuration */ + public $hostname; + public $port = 6002; /* default sip2 port for Sirsi */ + public $library = ''; + public $language = '001'; /* 001= english */ + + /* Patron ID */ + public $patron = ''; /* AA */ + public $patronpwd = ''; /* AD */ + + /* Terminal password */ + public $AC = ''; /*AC */ + + /* Maximum number of resends allowed before getMessage gives up */ + public $maxretry = 3; + + /* Terminators + * + * From page 15 of SPI2 v2 docs: + * All messages must end in a carriage return (hexadecimal 0d). + * + * Technically $msgTerminator should be set to \r only. However some vendors mistakenly require the \r\n. + * + * TODO: Create a function to set this, rather than exposing the variable as public. + */ + public $msgTerminator = "\r\n"; + + public $fldTerminator = '|'; + + /* Login Variables */ + public $UIDalgorithm = 0; /* 0 = unencrypted, default */ + public $PWDalgorithm = 0; /* undefined in documentation */ + public $scLocation = ''; /* Location Code */ + + /* Debug */ + public $debug = false; + + /* Public variables used for building messages */ + public $AO = 'WohlersSIP'; + public $AN = 'SIPCHK'; + public $AA = ''; + + /* Private variable to hold socket connection */ + private $socket; + + /* Sequence number counter */ + private $seq = -1; + + /* resend counter */ + private $retry = 0; + + /* Work area for building a message */ + private $msgBuild = ''; + private $noFixed = false; + + public function msgPatronStatusRequest() + { + /* Server Response: Patron Status Response message. */ + $this->newMessage('23'); + $this->addFixedOption($this->language, 3); + $this->addFixedOption($this->datestamp(), 18); + $this->addVarOption('AO', $this->AO); + $this->addVarOption('AA', $this->patron); + $this->addVarOption('AC', $this->AC); + $this->addVarOption('AD', $this->patronpwd); + return $this->returnMessage(); + } + + public function msgCheckout( + $item, + $nbDateDue = '', + $scRenewal = 'N', + $itmProp = '', + $fee = 'N', + $noBlock = 'N', + $cancel = 'N' + ) { + + /* Checkout an item (11) - untested */ + $this->newMessage('11'); + $this->addFixedOption($scRenewal, 1); + $this->addFixedOption($noBlock, 1); + $this->addFixedOption($this->datestamp(), 18); + if ($nbDateDue != '') { + /* override default date due */ + $this->addFixedOption($this->datestamp($nbDateDue), 18); + } else { + /* send a blank date due to allow ACS to use default date due computed for item */ + $this->addFixedOption('', 18); + } + $this->addVarOption('AO', $this->AO); + $this->addVarOption('AA', $this->patron); + $this->addVarOption('AB', $item); + $this->addVarOption('AC', $this->AC); + $this->addVarOption('CH', $itmProp, true); + $this->addVarOption('AD', $this->patronpwd, true); + $this->addVarOption('BO', $fee, true); /* Y or N */ + $this->addVarOption('BI', $cancel, true); /* Y or N */ + + return $this->returnMessage(); + } + + public function msgCheckin($item, $itmReturnDate, $itmLocation = '', $itmProp = '', $noBlock = 'N', $cancel = '') + { + /* Check-in an item (09) - untested */ + if ($itmLocation == '') { + /* If no location is specified, assume the default location of the SC, behaviour suggested by spec*/ + $itmLocation = $this->scLocation; + } + + $this->newMessage('09'); + $this->addFixedOption($noBlock, 1); + $this->addFixedOption($this->datestamp(), 18); + $this->addFixedOption($this->datestamp($itmReturnDate), 18); + $this->addVarOption('AP', $itmLocation); + $this->addVarOption('AO', $this->AO); + $this->addVarOption('AB', $item); + $this->addVarOption('AC', $this->AC); + $this->addVarOption('CH', $itmProp, true); + $this->addVarOption('BI', $cancel, true); /* Y or N */ + + return $this->returnMessage(); + } + + public function msgBlockPatron($message, $retained = 'N') + { + /* Blocks a patron, and responds with a patron status response (01) - untested */ + $this->newMessage('01'); + $this->addFixedOption($retained, 1); /* Y if card has been retained */ + $this->addFixedOption($this->datestamp(), 18); + $this->addVarOption('AO', $this->AO); + $this->addVarOption('AL', $message); + $this->addVarOption('AA', $this->AA); + $this->addVarOption('AC', $this->AC); + + return $this->returnMessage(); + } + + public function msgSCStatus($status = 0, $width = 80, $version = 2) + { + /* selfcheck status message, this should be sent immediately after login - untested */ + /* status codes, from the spec: + * 0 SC unit is OK + * 1 SC printer is out of paper + * 2 SC is about to shut down + */ + + if ($version > 3) { + $version = 2; + } + if ($status < 0 || $status > 2) { + $this->debugMsg("SIP2: Invalid status passed to msgSCStatus"); + return false; + } + $this->newMessage('99'); + $this->addFixedOption($status, 1); + $this->addFixedOption($width, 3); + $this->addFixedOption(sprintf("%03.2f", $version), 4); + return $this->returnMessage(); + } + + public function msgRequestACSResend() + { + /* Used to request a resend due to CRC mismatch - No sequence number is used */ + $this->newMessage('97'); + return $this->returnMessage(false); + } + + public function msgLogin($sipLogin, $sipPassword) + { + /* Login (93) - untested */ + $this->newMessage('93'); + $this->addFixedOption($this->UIDalgorithm, 1); + $this->addFixedOption($this->PWDalgorithm, 1); + $this->addVarOption('CN', $sipLogin); + $this->addVarOption('CO', $sipPassword); + $this->addVarOption('CP', $this->scLocation, true); + return $this->returnMessage(); + } + + public function msgPatronInformation($type, $start = '1', $end = '5') + { + + /* + * According to the specification: + * Only one category of items should be requested at a time, i.e. it would take 6 of these messages, + * each with a different position set to Y, to get all the detailed information about a patron's items. + */ + $summary['none'] = ' '; + $summary['hold'] = 'Y '; + $summary['overdue'] = ' Y '; + $summary['charged'] = ' Y '; + $summary['fine'] = ' Y '; + $summary['recall'] = ' Y '; + $summary['unavail'] = ' Y'; + + /* Request patron information */ + $this->newMessage('63'); + $this->addFixedOption($this->language, 3); + $this->addFixedOption($this->datestamp(), 18); + $this->addFixedOption(sprintf("%-10s", $summary[$type]), 10); + $this->addVarOption('AO', $this->AO); + $this->addVarOption('AA', $this->patron); + $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('AD', $this->patronpwd, true); + /* old function version used padded 5 digits, not sure why */ + $this->addVarOption('BP', $start, true); + /* old function version used padded 5 digits, not sure why */ + $this->addVarOption('BQ', $end, true); + return $this->returnMessage(); + } + + public function msgEndPatronSession() + { + /* End Patron Session, should be sent before switching to a new patron. (35) - untested */ + + $this->newMessage('35'); + $this->addFixedOption($this->datestamp(), 18); + $this->addVarOption('AO', $this->AO); + $this->addVarOption('AA', $this->patron); + $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('AD', $this->patronpwd, true); + return $this->returnMessage(); + } + + /* Fee paid function should go here */ + public function msgFeePaid($feeType, $pmtType, $pmtAmount, $curType = 'USD', $feeId = '', $transId = '') + { + /* Fee payment function (37) - untested */ + /* Fee Types: */ + /* 01 other/unknown */ + /* 02 administrative */ + /* 03 damage */ + /* 04 overdue */ + /* 05 processing */ + /* 06 rental*/ + /* 07 replacement */ + /* 08 computer access charge */ + /* 09 hold fee */ + + /* Value Payment Type */ + /* 00 cash */ + /* 01 VISA */ + /* 02 credit card */ + + if (!is_numeric($feeType) || $feeType > 99 || $feeType < 1) { + /* not a valid fee type - exit */ + $this->debugMsg("SIP2: (msgFeePaid) Invalid fee type: {$feeType}"); + return false; + } + + if (!is_numeric($pmtType) || $pmtType > 99 || $pmtType < 0) { + /* not a valid payment type - exit */ + $this->debugMsg("SIP2: (msgFeePaid) Invalid payment type: {$pmtType}"); + return false; + } + + $this->newMessage('37'); + $this->addFixedOption($this->datestamp(), 18); + $this->addFixedOption(sprintf('%02d', $feeType), 2); + $this->addFixedOption(sprintf('%02d', $pmtType), 2); + $this->addFixedOption($curType, 3); + + // due to currency format localization, it is up to the programmer + // to properly format their payment amount + $this->addVarOption('BV', $pmtAmount); + $this->addVarOption('AO', $this->AO); + $this->addVarOption('AA', $this->patron); + $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('AD', $this->patronpwd, true); + $this->addVarOption('CG', $feeId, true); + $this->addVarOption('BK', $transId, true); + + return $this->returnMessage(); + } + + public function msgItemInformation($item) + { + + $this->newMessage('17'); + $this->addFixedOption($this->datestamp(), 18); + $this->addVarOption('AO', $this->AO); + $this->addVarOption('AB', $item); + $this->addVarOption('AC', $this->AC, true); + return $this->returnMessage(); + } + + public function msgItemStatus($item, $itmProp = '') + { + /* Item status update function (19) - untested */ + + $this->newMessage('19'); + $this->addFixedOption($this->datestamp(), 18); + $this->addVarOption('AO', $this->AO); + $this->addVarOption('AB', $item); + $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('CH', $itmProp); + return $this->returnMessage(); + } + + public function msgPatronEnable() + { + /* Patron Enable function (25) - untested */ + /* This message can be used by the SC to re-enable cancelled patrons. + It should only be used for system testing and validation. */ + $this->newMessage('25'); + $this->addFixedOption($this->datestamp(), 18); + $this->addVarOption('AO', $this->AO); + $this->addVarOption('AA', $this->patron); + $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('AD', $this->patronpwd, true); + return $this->returnMessage(); + } + + public function msgHold( + $mode, + $expDate = '', + $holdtype = '', + $item = '', + $title = '', + $fee = 'N', + $pkupLocation = '' + ) { + + /* mode validity check */ + /* + * - remove hold + * + place hold + * * modify hold + */ + if (strpos('-+*', $mode) === false) { + /* not a valid mode - exit */ + $this->debugMsg("SIP2: Invalid hold mode: {$mode}"); + return false; + } + + if ($holdtype != '' && ($holdtype < 1 || $holdtype > 9)) { + /* + * Valid hold types range from 1 - 9 + * 1 other + * 2 any copy of title + * 3 specific copy + * 4 any copy at a single branch or location + */ + $this->debugMsg("SIP2: Invalid hold type code: {$holdtype}"); + return false; + } + + $this->newMessage('15'); + $this->addFixedOption($mode, 1); + $this->addFixedOption($this->datestamp(), 18); + if ($expDate != '') { + // hold expiration date, due to the use of the datestamp function, we have to check here for + // empty value. when datestamp is passed an empty value it will generate a current datestamp. + // Also, spec says this is fixed field, but it behaves like a var field and is optional... + $this->addVarOption('BW', $this->datestamp($expDate), true); + } + $this->addVarOption('BS', $pkupLocation, true); + $this->addVarOption('BY', $holdtype, true); + $this->addVarOption('AO', $this->AO); + $this->addVarOption('AA', $this->patron); + $this->addVarOption('AD', $this->patronpwd, true); + $this->addVarOption('AB', $item, true); + $this->addVarOption('AJ', $title, true); + $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('BO', $fee, true); /* Y when user has agreed to a fee notice */ + + return $this->returnMessage(); + } + + public function msgRenew( + $item = '', + $title = '', + $nbDateDue = '', + $itmProp = '', + $fee = 'N', + $noBlock = 'N', + $thirdParty = 'N' + ) { + + /* renew a single item (29) - untested */ + $this->newMessage('29'); + $this->addFixedOption($thirdParty, 1); + $this->addFixedOption($noBlock, 1); + $this->addFixedOption($this->datestamp(), 18); + if ($nbDateDue != '') { + /* override default date due */ + $this->addFixedOption($this->datestamp($nbDateDue), 18); + } else { + /* send a blank date due to allow ACS to use default date due computed for item */ + $this->addFixedOption('', 18); + } + $this->addVarOption('AO', $this->AO); + $this->addVarOption('AA', $this->patron); + $this->addVarOption('AD', $this->patronpwd, true); + $this->addVarOption('AB', $item, true); + $this->addVarOption('AJ', $title, true); + $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('CH', $itmProp, true); + $this->addVarOption('BO', $fee, true); /* Y or N */ + + return $this->returnMessage(); + } + + public function msgRenewAll($fee = 'N') + { + /* renew all items for a patron (65) - untested */ + $this->newMessage('65'); + $this->addVarOption('AO', $this->AO); + $this->addVarOption('AA', $this->patron); + $this->addVarOption('AD', $this->patronpwd, true); + $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('BO', $fee, true); /* Y or N */ + + return $this->returnMessage(); + } + + public function parsePatronStatusResponse($response) + { + $result['fixed'] = + array( + 'PatronStatus' => substr($response, 2, 14), + 'Language' => substr($response, 16, 3), + 'TransactionDate' => substr($response, 19, 18), + ); + + $result['variable'] = $this->parseVariableData($response, 37); + return $result; + } + + public function parseCheckoutResponse($response) + { + $result['fixed'] = + array( + 'Ok' => substr($response, 2, 1), + 'RenewalOk' => substr($response, 3, 1), + 'Magnetic' => substr($response, 4, 1), + 'Desensitize' => substr($response, 5, 1), + 'TransactionDate' => substr($response, 6, 18), + ); + + $result['variable'] = $this->parseVariableData($response, 24); + return $result; + } + + public function parseCheckinResponse($response) + { + $result['fixed'] = + array( + 'Ok' => substr($response, 2, 1), + 'Resensitize' => substr($response, 3, 1), + 'Magnetic' => substr($response, 4, 1), + 'Alert' => substr($response, 5, 1), + 'TransactionDate' => substr($response, 6, 18), + ); + + $result['variable'] = $this->parseVariableData($response, 24); + return $result; + } + + public function parseACSStatusResponse($response) + { + $result['fixed'] = + [ + 'Online' => substr($response, 2, 1), + // is Checkin by the SC allowed ? + 'Checkin' => substr($response, 3, 1), + // is Checkout by the SC allowed ? + 'Checkout' => substr($response, 4, 1), + // renewal allowed? */ + 'Renewal' => substr($response, 5, 1), + //is patron status updating by the SC allowed ? (status update ok) + 'PatronUpdate' => substr($response, 6, 1), + 'Offline' => substr($response, 7, 1), + 'Timeout' => substr($response, 8, 3), + 'Retries' => substr($response, 11, 3), + 'TransactionDate' => substr($response, 14, 18), + 'Protocol' => substr($response, 32, 4), + ]; + + $result['variable'] = $this->parseVariableData($response, 36); + return $result; + } + + public function parseLoginResponse($response) + { + $result['fixed'] = + [ + 'Ok' => substr($response, 2, 1), + ]; + $result['variable'] = array(); + return $result; + } + + public function parsePatronInfoResponse($response) + { + + $result['fixed'] = + [ + 'PatronStatus' => substr($response, 2, 14), + 'Language' => substr($response, 16, 3), + 'TransactionDate' => substr($response, 19, 18), + 'HoldCount' => intval(substr($response, 37, 4)), + 'OverdueCount' => intval(substr($response, 41, 4)), + 'ChargedCount' => intval(substr($response, 45, 4)), + 'FineCount' => intval(substr($response, 49, 4)), + 'RecallCount' => intval(substr($response, 53, 4)), + 'UnavailableCount' => intval(substr($response, 57, 4)) + ]; + + $result['variable'] = $this->parseVariableData($response, 61); + return $result; + } + + public function parseEndSessionResponse($response) + { + /* Response example: 36Y20080228 145537AOWOHLERS|AAX00000000|AY9AZF474 */ + + $result['fixed'] = + [ + 'EndSession' => substr($response, 2, 1), + 'TransactionDate' => substr($response, 3, 18), + ]; + + + $result['variable'] = $this->parseVariableData($response, 21); + + return $result; + } + + public function parseFeePaidResponse($response) + { + $result['fixed'] = + [ + 'PaymentAccepted' => substr($response, 2, 1), + 'TransactionDate' => substr($response, 3, 18), + ]; + + $result['variable'] = $this->parseVariableData($response, 21); + return $result; + } + + public function parseItemInfoResponse($response) + { + $result['fixed'] = + [ + 'CirculationStatus' => intval(substr($response, 2, 2)), + 'SecurityMarker' => intval(substr($response, 4, 2)), + 'FeeType' => intval(substr($response, 6, 2)), + 'TransactionDate' => substr($response, 8, 18), + ]; + + $result['variable'] = $this->parseVariableData($response, 26); + + return $result; + } + + public function parseItemStatusResponse($response) + { + $result['fixed'] = + [ + 'PropertiesOk' => substr($response, 2, 1), + 'TransactionDate' => substr($response, 3, 18), + ]; + + $result['variable'] = $this->parseVariableData($response, 21); + return $result; + } + + public function parsePatronEnableResponse($response) + { + $result['fixed'] = + [ + 'PatronStatus' => substr($response, 2, 14), + 'Language' => substr($response, 16, 3), + 'TransactionDate' => substr($response, 19, 18), + ]; + + $result['variable'] = $this->parseVariableData($response, 37); + return $result; + } + + public function parseHoldResponse($response) + { + + $result['fixed'] = + [ + 'Ok' => substr($response, 2, 1), + 'available' => substr($response, 3, 1), + 'TransactionDate' => substr($response, 4, 18), + 'ExpirationDate' => substr($response, 22, 18) + ]; + + + $result['variable'] = $this->parseVariableData($response, 40); + + return $result; + } + + + public function parseRenewResponse($response) + { + /* Response Example: + 300NUU20080228 222232AOWOHLERS|AAX00000241|ABM02400028262| + AJFolksongs of Britain and Ireland|AH5/23/2008,23:59|CH| + AFOverride required to exceed renewal limit.|AY1AZCDA5 + */ + $result['fixed'] = + [ + 'Ok' => substr($response, 2, 1), + 'RenewalOk' => substr($response, 3, 1), + 'Magnetic' => substr($response, 4, 1), + 'Desensitize' => substr($response, 5, 1), + 'TransactionDate' => substr($response, 6, 18), + ]; + + + $result['variable'] = $this->parseVariableData($response, 24); + + return $result; + } + + public function parseRenewAllResponse($response) + { + $result['fixed'] = + [ + 'Ok' => substr($response, 2, 1), + 'Renewed' => substr($response, 3, 4), + 'Unrenewed' => substr($response, 7, 4), + 'TransactionDate' => substr($response, 11, 18), + ]; + + + $result['variable'] = $this->parseVariableData($response, 29); + + return $result; + } + + + public function getMessage($message) + { + /* sends the current message, and gets the response */ + $result = ''; + $terminator = ''; + $nr = ''; + + $this->debugMsg('SIP2: Sending SIP2 request...'); + socket_write($this->socket, $message, strlen($message)); + + $this->debugMsg('SIP2: Request Sent, Reading response'); + + while ($terminator != "\x0D" && $nr !== false) { + $nr = socket_recv($this->socket, $terminator, 1, 0); + $result = $result . $terminator; + } + + $this->debugMsg("SIP2: {$result}"); + + /* test message for CRC validity */ + if ($this->checkCRC($result)) { + /* reset the retry counter on successful send */ + $this->retry = 0; + $this->debugMsg("SIP2: Message from ACS passed CRC check"); + } else { + /* CRC check failed, request a resend */ + $this->retry++; + if ($this->retry < $this->maxretry) { + /* try again */ + $this->debugMsg("SIP2: Message failed CRC check, retrying ({$this->retry})"); + + $this->getMessage($message); + } else { + /* give up */ + $this->debugMsg("SIP2: Failed to get valid CRC after {$this->maxretry} retries."); + return false; + } + } + return $result; + } + + public function connect() + { + + /* Socket Communications */ + $this->debugMsg("SIP2: --- BEGIN SIP communication ---"); + + /* Get the IP address for the target host. */ + $address = gethostbyname($this->hostname); + + /* Create a TCP/IP socket. */ + $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + + /* check for actual truly false result using ===*/ + if ($this->socket === false) { + $this->debugMsg("SIP2: socket_create() failed: reason: " . socket_strerror($this->socket)); + return false; + } else { + $this->debugMsg("SIP2: Socket Created"); + } + $this->debugMsg("SIP2: Attempting to connect to '$address' on port '{$this->port}'..."); + + /* open a connection to the host */ + $result = socket_connect($this->socket, $address, $this->port); + if (!$result) { + $this->debugMsg("SIP2: socket_connect() failed.\nReason: ($result) " . socket_strerror($result)); + } else { + $this->debugMsg("SIP2: --- SOCKET READY ---"); + } + /* return the result from the socket connect */ + return $result; + } + + public function disconnect() + { + /* Close the socket */ + socket_close($this->socket); + } + + /* Core local utility functions */ + private function datestamp($timestamp = '') + { + /* generate a SIP2 compatible datestamp */ + /* From the spec: + * YYYYMMDDZZZZHHMMSS. + * All dates and times are expressed according to the ANSI standard X3.30 for date and X3.43 for time. + * The ZZZZ field should contain blanks (code $20) to represent local time. To represent universal time, + * a Z character(code $5A) should be put in the last (right hand) position of the ZZZZ field. + * To represent other time zones the appropriate character should be used; a Q character (code $51) + * should be put in the last (right hand) position of the ZZZZ field to represent Atlantic Standard Time. + * When possible local time is the preferred format. + */ + if ($timestamp != '') { + /* Generate a proper date time from the date provided */ + return date('Ymd His', $timestamp); + } else { + /* Current Date/Time */ + return date('Ymd His'); + } + } + + private function parseVariableData($response, $start) + { + + $result = array(); + $result['Raw'] = explode("|", substr($response, $start, -7)); + foreach ($result['Raw'] as $item) { + $field = substr($item, 0, 2); + $value = substr($item, 2); + /* SD returns some odd values on occasion, Unable to locate the purpose in spec, so I strip from + * the parsed array. Orig values will remain in ['raw'] element + */ + $clean = trim($value, "\x00..\x1F"); + if (trim($clean) <> '') { + $result[$field][] = $clean; + } + } + $result['AZ'][] = substr($response, -5); + + return ($result); + } + + private function crc($buf) + { + /* Calculate CRC */ + $sum = 0; + + $len = strlen($buf); + for ($n = 0; $n < $len; $n++) { + $sum = $sum + ord(substr($buf, $n, 1)); + } + + $crc = ($sum & 0xFFFF) * -1; + + /* 2008.03.15 - Fixed a bug that allowed the checksum to be larger then 4 digits */ + return substr(sprintf("%4X", $crc), -4, 4); + } /* end crc */ + + private function getSeqNumber() + { + /* Get a sequence number for the AY field */ + /* valid numbers range 0-9 */ + $this->seq++; + if ($this->seq > 9) { + $this->seq = 0; + } + return ($this->seq); + } + + private function debugMsg($message) + { + /* custom debug function, why repeat the check for the debug flag in code... */ + if ($this->debug) { + trigger_error($message, E_USER_NOTICE); + } + } + + private function checkCRC($message) + { + /* test the received message's CRC by generating our own CRC from the message */ + $test = preg_split('/(.{4})$/', trim($message), 2, PREG_SPLIT_DELIM_CAPTURE); + + if ($this->crc($test[0]) == $test[1]) { + return true; + } else { + return false; + } + } + + private function newMessage($code) + { + /* resets the msgBuild variable to the value of $code, and clears the flag for fixed messages */ + $this->noFixed = false; + $this->msgBuild = $code; + } + + private function addFixedOption($value, $len) + { + /* adds a fixed length option to the msgBuild IF no variable options have been added. */ + if ($this->noFixed) { + return false; + } else { + $this->msgBuild .= sprintf("%{$len}s", substr($value, 0, $len)); + return true; + } + } + + private function addVarOption($field, $value, $optional = false) + { + /* adds a variable length option to the message, and also prevents adding additional fixed fields */ + if ($optional == true && $value == '') { + /* skipped */ + $this->debugMsg("SIP2: Skipping optional field {$field}"); + } else { + $this->noFixed = true; /* no more fixed for this message */ + $this->msgBuild .= $field . substr($value, 0, 255) . $this->fldTerminator; + } + return true; + } + + private function returnMessage($withSeq = true, $withCrc = true) + { + /* Finalizes the message and returns it. Message will remain in msgBuild until newMessage is called */ + if ($withSeq) { + $this->msgBuild .= 'AY' . $this->getSeqNumber(); + } + if ($withCrc) { + $this->msgBuild .= 'AZ'; + $this->msgBuild .= $this->crc($this->msgBuild); + } + $this->msgBuild .= $this->msgTerminator; + + return $this->msgBuild; + } +} From beda827c3bb86338beb53ed15c8e9d6f5c9f0518 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Wed, 11 Jul 2018 10:46:26 +0100 Subject: [PATCH 02/35] Refactored SIP2Client to use a socket factory to facilitate testing --- composer.json | 3 +- src/SIP2Client.php | 81 +++++++++++++++++++++++++++++----------------- 2 files changed, 53 insertions(+), 31 deletions(-) diff --git a/composer.json b/composer.json index e5f020f..b790bfc 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ } ], "require": { - "php" : "~5.6|~7.0" + "php": "~5.6|~7.0", + "clue/socket-raw": "^1.3" }, "require-dev": { "phpunit/phpunit" : ">=5.4.3", diff --git a/src/SIP2Client.php b/src/SIP2Client.php index 2f4d2b5..c2a6f0d 100644 --- a/src/SIP2Client.php +++ b/src/SIP2Client.php @@ -16,6 +16,9 @@ * @link https://github.com/cap60552/php-sip2/ */ +use Socket\Raw\Factory; +use \Socket\Raw\Socket; + /** * * TODO @@ -69,7 +72,7 @@ class SIP2Client public $AN = 'SIPCHK'; public $AA = ''; - /* Private variable to hold socket connection */ + /** @var Socket */ private $socket; /* Sequence number counter */ @@ -82,6 +85,31 @@ class SIP2Client private $msgBuild = ''; private $noFixed = false; + private $socketFactory; + + /** + * Allows an alternative socket factory to be injected. The allows us to + * mock socket connections for testing + * + * @param Factory $factory + */ + public function setSocketFactory(Factory $factory) + { + $this->socketFactory = $factory; + } + + /** + * Get the current socket factory, creating a default on if necessary + * @return Factory + */ + private function getSocketFactory() + { + if (is_null($this->socketFactory)) { + $this->socketFactory = new Factory(); //@codeCoverageIgnore + } + return $this->socketFactory; + } + public function msgPatronStatusRequest() { /* Server Response: Patron Status Response message. */ @@ -671,15 +699,20 @@ public function getMessage($message) /* sends the current message, and gets the response */ $result = ''; $terminator = ''; - $nr = ''; $this->debugMsg('SIP2: Sending SIP2 request...'); - socket_write($this->socket, $message, strlen($message)); + $this->socket->write($message); $this->debugMsg('SIP2: Request Sent, Reading response'); - while ($terminator != "\x0D" && $nr !== false) { - $nr = socket_recv($this->socket, $terminator, 1, 0); + while ($terminator != "\x0D") { + try { + $terminator = $this->socket->recv(1, 0); + } + catch (\Exception $e) { + break; + } + //$nr = socket_recv($this->socket, $terminator, 1, 0); $result = $result . $terminator; } @@ -713,36 +746,23 @@ public function connect() /* Socket Communications */ $this->debugMsg("SIP2: --- BEGIN SIP communication ---"); - /* Get the IP address for the target host. */ - $address = gethostbyname($this->hostname); - - /* Create a TCP/IP socket. */ - $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); - - /* check for actual truly false result using ===*/ - if ($this->socket === false) { - $this->debugMsg("SIP2: socket_create() failed: reason: " . socket_strerror($this->socket)); + $address = $this->hostname . ':' . $this->port; + try { + $this->socket = $this->getSocketFactory()->createClient($address); + } + catch (\Exception $e) { + $this->debugMsg("SIP2Client: Failed to connect: ".$e->getMessage()); return false; - } else { - $this->debugMsg("SIP2: Socket Created"); } - $this->debugMsg("SIP2: Attempting to connect to '$address' on port '{$this->port}'..."); - /* open a connection to the host */ - $result = socket_connect($this->socket, $address, $this->port); - if (!$result) { - $this->debugMsg("SIP2: socket_connect() failed.\nReason: ($result) " . socket_strerror($result)); - } else { - $this->debugMsg("SIP2: --- SOCKET READY ---"); - } - /* return the result from the socket connect */ - return $result; + $this->debugMsg("SIP2: --- SOCKET READY ---"); + return true; } public function disconnect() { - /* Close the socket */ - socket_close($this->socket); + $this->socket->close(); + $this->socket = null; } /* Core local utility functions */ @@ -783,7 +803,7 @@ private function parseVariableData($response, $start) $result[$field][] = $clean; } } - $result['AZ'][] = substr($response, -5); + $result['AZ'][] = trim(substr($response, -5)); return ($result); } @@ -802,7 +822,7 @@ private function crc($buf) /* 2008.03.15 - Fixed a bug that allowed the checksum to be larger then 4 digits */ return substr(sprintf("%4X", $crc), -4, 4); - } /* end crc */ + } private function getSeqNumber() { @@ -831,6 +851,7 @@ private function checkCRC($message) if ($this->crc($test[0]) == $test[1]) { return true; } else { + //echo "Expected SRC was ".$this->crc($test[0])." but found ".$test[1]."\n"; return false; } } From 25f3fcde91a74c182d9f2550f1239feb5a56d7e7 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Wed, 11 Jul 2018 10:47:17 +0100 Subject: [PATCH 03/35] Add basic unit test which demonstrates mocking server responses --- tests/SIP2ClientTest.php | 146 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 tests/SIP2ClientTest.php diff --git a/tests/SIP2ClientTest.php b/tests/SIP2ClientTest.php new file mode 100644 index 0000000..5e4b2dc --- /dev/null +++ b/tests/SIP2ClientTest.php @@ -0,0 +1,146 @@ +setSocketFactory($this->createMockSIP2Server($responses)); + + $client->hostname = 'server.example.com'; + $client->port = 6002; + $client->patron = '101010101'; + $client->patronpwd = '010101'; + + $ok = $client->connect(); + $this->assertTrue($ok); + + $msg = $client->msgPatronInformation('none'); + $this->assertNotEmpty($msg); + + $response = $client->getMessage($msg); + $this->assertNotEmpty($response); + + $info = $client->parsePatronInfoResponse($response); + $this->assertArrayHasKey('fixed', $info); + $this->assertArrayHasKey('variable', $info); + $this->assertArrayHasKey('Raw', $info['variable']); + + //check the fixed data + $this->assertFixedMetadata(' ', $info, 'PatronStatus'); + $this->assertFixedMetadata('000', $info, 'Language'); + $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); + $this->assertFixedMetadata(0, $info, 'HoldCount'); + $this->assertFixedMetadata(10, $info, 'ChargedCount'); + $this->assertFixedMetadata(0, $info, 'FineCount'); + $this->assertFixedMetadata(0, $info, 'RecallCount'); + $this->assertFixedMetadata(9, $info, 'UnavailableCount'); + + //check variable data + $this->assertVariableMetadata('Example City Library', $info, 'AO'); + $this->assertVariableMetadata('1381380', $info, 'AA'); + $this->assertVariableMetadata('Mr Joe Tester', $info, 'AE'); + $this->assertVariableMetadata('9999', $info, 'BZ'); + $this->assertVariableMetadata('9999', $info, 'CA'); + $this->assertVariableMetadata('9999', $info, 'CB'); + $this->assertVariableMetadata('Y', $info, 'BL'); + $this->assertVariableMetadata('Y', $info, 'CQ'); + $this->assertVariableMetadata('0.00', $info, 'BV'); + $this->assertVariableMetadata('joe.tester@example.com', $info, 'BE'); + $this->assertVariableMetadata('0', $info, 'AY'); + $this->assertVariableMetadata("CF82", $info, 'AZ'); + } + + /** + * This helper creates a socket factory we can pass to the client. The factory returns a mock + * socket which will return a sequence of responses after each write() to the socket. This allows + * us to easily simulate SIP2 server responses + * + * @param array $responses + * @return \Socket\Raw\Factory + */ + private function createMockSIP2Server(array $responses) + { + $socket = $this->prophesize(\Socket\Raw\Socket::class); + + //our mock socket will send each given response in sequence after each write() call + $socket->responses = $responses; + $socket->responseIdx = -1; + $socket->responseOffset = 0; + $socket->responseLength = 0; + + $socket->write(Argument::type('string'))->will(function ($args) use ($socket) { + //printf("write(%s)\n", $args[0]); + + //next call to recv will start returning out next canned response + $socket->responseIdx++; + if ($socket->responseIdx >= count($socket->responses)) { + throw new \LogicException( + 'Mock client has no response for write #' . ($socket->responseIdx + 1) . ':' . $args[0] + ); + } + $socket->responseOffset = 0; + $socket->responseLength = strlen($socket->responses[$socket->responseIdx]); + return true; + }); + + $socket->recv(1, 0)->will(function ($args) use ($socket) { + if ($socket->responseOffset >= $socket->responseLength) { + throw new \LogicException('Client is reading past prophesized response'); + } + //return next char of response + return $socket->responses[$socket->responseIdx][$socket->responseOffset++]; + }); + + + //our factory just returns our mock + $factory = $this->prophesize(\Socket\Raw\Factory::class); + $factory->createClient(Argument::type('string'))->willReturn($socket->reveal()); + + return $factory->reveal(); + } + + /** + * Checks fixed data in patron info is present and valid + * @param string $expected expected value + * @param array $info info array returned from parsePatronInfoResponse + * @param string $name name of element + */ + private function assertFixedMetadata($expected, array $info, $name) + { + $this->assertArrayHasKey($name, $info['fixed']); + $this->assertEquals($expected, $info['fixed'][$name]); + } + + /** + * Checks variable data in patron info is present and valid + * @param string|array $expected expected value - for multi-valued responses you can pass an array here + * @param array $info info array returned from parsePatronInfoResponse + * @param string $name name of element + */ + private function assertVariableMetadata($expected, array $info, $name) + { + $this->assertArrayHasKey($name, $info['variable']); + if (is_string($expected)) { + $expected = [$expected]; + } + + $valueCount = count($expected); + $this->assertCount($valueCount, $info['variable'][$name]); + for ($i = 0; $i < $valueCount; $i++) { + $this->assertEquals($expected[$i], $info['variable'][$name][$i]); + } + } +} From d1b603891df447f616f65e49aeab6e0389164482 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Wed, 11 Jul 2018 10:48:13 +0100 Subject: [PATCH 04/35] Corrected phpunit.xml to bring it up to date --- phpunit.xml.dist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1249077..4f294dd 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,7 +10,7 @@ processIsolation="false" stopOnFailure="false"> - + tests @@ -22,7 +22,7 @@ - + From 3152fe052680f2f326b47dfba7cda99bac852069 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Wed, 18 Jul 2018 10:27:18 +0100 Subject: [PATCH 05/35] Refactor and expand test suite --- tests/ACSResendTest.php | 20 ++++ ...entTest.php => AbstractSIP2ClientTest.php} | 91 ++++++--------- tests/BlockPatronTest.php | 41 +++++++ tests/CRCTest.php | 39 +++++++ tests/CheckinTest.php | 97 ++++++++++++++++ tests/CheckoutTest.php | 107 ++++++++++++++++++ tests/HoldTest.php | 40 +++++++ tests/LoginTest.php | 26 +++++ tests/PatronInfoTest.php | 58 ++++++++++ tests/PatronStatusTest.php | 51 +++++++++ tests/SCStatusTest.php | 49 ++++++++ 11 files changed, 561 insertions(+), 58 deletions(-) create mode 100644 tests/ACSResendTest.php rename tests/{SIP2ClientTest.php => AbstractSIP2ClientTest.php} (51%) create mode 100644 tests/BlockPatronTest.php create mode 100644 tests/CRCTest.php create mode 100644 tests/CheckinTest.php create mode 100644 tests/CheckoutTest.php create mode 100644 tests/HoldTest.php create mode 100644 tests/LoginTest.php create mode 100644 tests/PatronInfoTest.php create mode 100644 tests/PatronStatusTest.php create mode 100644 tests/SCStatusTest.php diff --git a/tests/ACSResendTest.php b/tests/ACSResendTest.php new file mode 100644 index 0000000..4238984 --- /dev/null +++ b/tests/ACSResendTest.php @@ -0,0 +1,20 @@ +makeResponse("96")]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + $client->connect(); + + $msg = $client->msgRequestACSResend(); + $this->assertEquals("97AZFEF5", trim($msg)); + } +} diff --git a/tests/SIP2ClientTest.php b/tests/AbstractSIP2ClientTest.php similarity index 51% rename from tests/SIP2ClientTest.php rename to tests/AbstractSIP2ClientTest.php index 5e4b2dc..26094f1 100644 --- a/tests/SIP2ClientTest.php +++ b/tests/AbstractSIP2ClientTest.php @@ -4,63 +4,38 @@ use Prophecy\Argument; -class SIP2ClientTest extends \PHPUnit\Framework\TestCase +abstract class AbstractSIP2ClientTest extends \PHPUnit\Framework\TestCase { - public function testBasicPatronInfo() + /** + * Make a valid response by adding sequence number and CRC + * @param $str + * @return string + */ + protected function makeResponse($str) { - //our mock socket will return these responses in sequence after each write() to the socket - //here we simulate a basic patron information request - $responses = [ - "64 00020180711 185645000000000010000000000009AOExample City Library|" . - "AA1381380|AEMr Joe Tester|BZ9999|CA9999|CB9999|BLY|CQY|BV0.00|" . - "BEjoe.tester@example.com|AY0AZCF82\x0D" - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->hostname = 'server.example.com'; - $client->port = 6002; - $client->patron = '101010101'; - $client->patronpwd = '010101'; - - $ok = $client->connect(); - $this->assertTrue($ok); - - $msg = $client->msgPatronInformation('none'); - $this->assertNotEmpty($msg); - - $response = $client->getMessage($msg); - $this->assertNotEmpty($response); - - $info = $client->parsePatronInfoResponse($response); - $this->assertArrayHasKey('fixed', $info); - $this->assertArrayHasKey('variable', $info); - $this->assertArrayHasKey('Raw', $info['variable']); - - //check the fixed data - $this->assertFixedMetadata(' ', $info, 'PatronStatus'); - $this->assertFixedMetadata('000', $info, 'Language'); - $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); - $this->assertFixedMetadata(0, $info, 'HoldCount'); - $this->assertFixedMetadata(10, $info, 'ChargedCount'); - $this->assertFixedMetadata(0, $info, 'FineCount'); - $this->assertFixedMetadata(0, $info, 'RecallCount'); - $this->assertFixedMetadata(9, $info, 'UnavailableCount'); + //add sequence number and intro for checksum + $str .= 'AY0AZ'; + //add checksum + $str .= $this->crc($str); + //add terminator + $str .= "\x0D"; + return $str; + } - //check variable data - $this->assertVariableMetadata('Example City Library', $info, 'AO'); - $this->assertVariableMetadata('1381380', $info, 'AA'); - $this->assertVariableMetadata('Mr Joe Tester', $info, 'AE'); - $this->assertVariableMetadata('9999', $info, 'BZ'); - $this->assertVariableMetadata('9999', $info, 'CA'); - $this->assertVariableMetadata('9999', $info, 'CB'); - $this->assertVariableMetadata('Y', $info, 'BL'); - $this->assertVariableMetadata('Y', $info, 'CQ'); - $this->assertVariableMetadata('0.00', $info, 'BV'); - $this->assertVariableMetadata('joe.tester@example.com', $info, 'BE'); - $this->assertVariableMetadata('0', $info, 'AY'); - $this->assertVariableMetadata("CF82", $info, 'AZ'); + /** + * Calc SIP2 CRC value + * @param $buffer + * @return string + */ + private function crc($buffer) + { + $sum = 0; + $len = strlen($buffer); + for ($n = 0; $n < $len; $n++) { + $sum = $sum + ord($buffer[$n]); + } + $crc = ($sum & 0xFFFF) * -1; + return substr(sprintf("%4X", $crc), -4, 4); } /** @@ -71,7 +46,7 @@ public function testBasicPatronInfo() * @param array $responses * @return \Socket\Raw\Factory */ - private function createMockSIP2Server(array $responses) + protected function createMockSIP2Server(array $responses) { $socket = $this->prophesize(\Socket\Raw\Socket::class); @@ -84,7 +59,7 @@ private function createMockSIP2Server(array $responses) $socket->write(Argument::type('string'))->will(function ($args) use ($socket) { //printf("write(%s)\n", $args[0]); - //next call to recv will start returning out next canned response + //next call to recv will start returning our next canned response $socket->responseIdx++; if ($socket->responseIdx >= count($socket->responses)) { throw new \LogicException( @@ -118,7 +93,7 @@ private function createMockSIP2Server(array $responses) * @param array $info info array returned from parsePatronInfoResponse * @param string $name name of element */ - private function assertFixedMetadata($expected, array $info, $name) + protected function assertFixedMetadata($expected, array $info, $name) { $this->assertArrayHasKey($name, $info['fixed']); $this->assertEquals($expected, $info['fixed'][$name]); @@ -130,7 +105,7 @@ private function assertFixedMetadata($expected, array $info, $name) * @param array $info info array returned from parsePatronInfoResponse * @param string $name name of element */ - private function assertVariableMetadata($expected, array $info, $name) + protected function assertVariableMetadata($expected, array $info, $name) { $this->assertArrayHasKey($name, $info['variable']); if (is_string($expected)) { diff --git a/tests/BlockPatronTest.php b/tests/BlockPatronTest.php new file mode 100644 index 0000000..a16b051 --- /dev/null +++ b/tests/BlockPatronTest.php @@ -0,0 +1,41 @@ +makeResponse("24". + "xxxYYYYxxxYYYY". + "ENG". + "20180711 185645". + "AO1234|". + "AApatron|". + "AEJoe|". + "BLY|". + "CQY|". + "BHGBP|". + "BV1.23|". + "AFmessage|". + "AGprint|") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgBlockPatron('Blocked', 'Y'); + $response = $client->getMessage($msg); + + //no need to fully test this response as other tests do it + $info = $client->parsePatronStatusResponse($response); + $this->assertFixedMetadata('xxxYYYYxxxYYYY', $info, 'PatronStatus'); + $this->assertFixedMetadata('ENG', $info, 'Language'); + $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); + } +} diff --git a/tests/CRCTest.php b/tests/CRCTest.php new file mode 100644 index 0000000..80f5f8c --- /dev/null +++ b/tests/CRCTest.php @@ -0,0 +1,39 @@ +assertEquals('EB80', $this->invokeMethod($client, 'crc', [$in])); + + $in = '09N20160419 12171320160419 121713APReading Room 1|AO830|AB830$28170815|AC|AY2AZ'; + $this->assertEquals('EB7C', $this->invokeMethod($client, 'crc', [$in])); + } + + /** + * Call protected/private method of a class. + * + * @param object &$object Instantiated object that we will run method on. + * @param string $methodName Method name to call + * @param array $parameters Array of parameters to pass into method. + * + * @return mixed Method return. + * @throws \ReflectionException + */ + private function invokeMethod(&$object, $methodName, array $parameters = []) + { + $reflection = new \ReflectionClass(get_class($object)); + $method = $reflection->getMethod($methodName); + $method->setAccessible(true); + + return $method->invokeArgs($object, $parameters); + } +} diff --git a/tests/CheckinTest.php b/tests/CheckinTest.php new file mode 100644 index 0000000..b9c6920 --- /dev/null +++ b/tests/CheckinTest.php @@ -0,0 +1,97 @@ +makeResponse("12" . + "1" . + "Y" . + "U" . + "N" . + "20180711 185645" . + "AO1234|" . + "ABitem|" . + "AQloc|" . + "AJtitle|" . + "CLsort|" . + "AApatron|" . + "CKmda|" . + "CHprop|" . + "AFmessage|" . + "AGprint|") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgCheckin( + 'mybook', + strtotime('2018-07-11 11:22:33'), + 'loc', + 'prop', + 'Y', + 'N' + ); + $response = $client->getMessage($msg); + + $info = $client->parseCheckinResponse($response); + + $this->assertFixedMetadata('1', $info, 'Ok'); + $this->assertFixedMetadata('Y', $info, 'Resensitize'); + $this->assertFixedMetadata('U', $info, 'Magnetic'); + $this->assertFixedMetadata('N', $info, 'Alert'); + $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); + + $this->assertVariableMetadata('1234', $info, 'AO'); + $this->assertVariableMetadata('item', $info, 'AB'); + $this->assertVariableMetadata('loc', $info, 'AQ'); + $this->assertVariableMetadata('title', $info, 'AJ'); + $this->assertVariableMetadata('sort', $info, 'CL'); + $this->assertVariableMetadata('patron', $info, 'AA'); + $this->assertVariableMetadata('mda', $info, 'CK'); + $this->assertVariableMetadata('prop', $info, 'CH'); + $this->assertVariableMetadata('message', $info, 'AF'); + $this->assertVariableMetadata('print', $info, 'AG'); + } + + public function testExampleOLIBCheckin() + { + //here we mock an example response from the OCLC OLIB SIP server + //http://www.oclc.org/support/help/olib/900/Content/System/Supported%20SIP2%20Messages.htm#11 + //Note that the example gives the CRC as E777 but we calculate it as E6C0 + $responses = [ + $this->makeResponse("101YUN20110217 075306". + "AOMAIN|AB111111|AQ|AJThe 7 pillars of wisdom|AAJSMITH|CK001|") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgCheckin('mybook', '', '', 'prop', 'Y', 'N'); + $response = $client->getMessage($msg); + + $info = $client->parseCheckinResponse($response); + + $this->assertFixedMetadata('1', $info, 'Ok'); + $this->assertFixedMetadata('Y', $info, 'Resensitize'); + $this->assertFixedMetadata('U', $info, 'Magnetic'); + $this->assertFixedMetadata('N', $info, 'Alert'); + $this->assertFixedMetadata('20110217 075306', $info, 'TransactionDate'); + + $this->assertVariableMetadata('MAIN', $info, 'AO'); + $this->assertVariableMetadata('111111', $info, 'AB'); + $this->assertVariableMetadata('The 7 pillars of wisdom', $info, 'AJ'); + $this->assertVariableMetadata('JSMITH', $info, 'AA'); + $this->assertVariableMetadata('001', $info, 'CK'); + } +} diff --git a/tests/CheckoutTest.php b/tests/CheckoutTest.php new file mode 100644 index 0000000..cb7ad74 --- /dev/null +++ b/tests/CheckoutTest.php @@ -0,0 +1,107 @@ +makeResponse("12" . + "1" . + "Y" . + "N" . + "Y" . + "20180711 185645" . + "AO1234|" . + "AApatron|" . + "ABitem|" . + "AJtitle|" . + "AH20180711 185645|" . + "BT01|" . + "CIN|" . + "BHGBP|" . + "BV1.23|" . + "CKmda|" . + "CHprop|" . + "BKxyz|" . + "AFmessage|" . + "AGprint|") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgCheckout( + 'mybook', + strtotime('2018-07-11 11:22:33'), + 'N', + 'prop', + 'Y', + 'N', + 'N' + ); + $response = $client->getMessage($msg); + + $info = $client->parseCheckoutResponse($response); + + $this->assertFixedMetadata('1', $info, 'Ok'); + $this->assertFixedMetadata('Y', $info, 'RenewalOk'); + $this->assertFixedMetadata('N', $info, 'Magnetic'); + $this->assertFixedMetadata('Y', $info, 'Desensitize'); + $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); + + $this->assertVariableMetadata('1234', $info, 'AO'); + $this->assertVariableMetadata('patron', $info, 'AA'); + $this->assertVariableMetadata('item', $info, 'AB'); + $this->assertVariableMetadata('title', $info, 'AJ'); + $this->assertVariableMetadata('20180711 185645', $info, 'AH'); + $this->assertVariableMetadata('01', $info, 'BT'); + $this->assertVariableMetadata('N', $info, 'CI'); + $this->assertVariableMetadata('GBP', $info, 'BH'); + $this->assertVariableMetadata('1.23', $info, 'BV'); + $this->assertVariableMetadata('mda', $info, 'CK'); + $this->assertVariableMetadata('prop', $info, 'CH'); + $this->assertVariableMetadata('xyz', $info, 'BK'); + $this->assertVariableMetadata('message', $info, 'AF'); + $this->assertVariableMetadata('print', $info, 'AG'); + } + + public function testExampleOLIBCheckout() + { + //here we mock an example response from the OCLC OLIB SIP server + //http://www.oclc.org/support/help/olib/900/Content/System/Supported%20SIP2%20Messages.htm#11 + //Note that the example gives the CRC as DC91 but we calculate it as DD61 + $responses = [ + $this->makeResponse("121NUY20101014 121215AOMAIN|AH20101104 120000|AAJSMITH|AB111111|" . + "AJMarmalade and jam making for dummies|BV1.30|AF|") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgCheckout('mybook', '', 'N', 'prop', 'Y', 'N', 'N'); + $response = $client->getMessage($msg); + + $info = $client->parseCheckoutResponse($response); + + $this->assertFixedMetadata('1', $info, 'Ok'); + $this->assertFixedMetadata('N', $info, 'RenewalOk'); + $this->assertFixedMetadata('U', $info, 'Magnetic'); + $this->assertFixedMetadata('Y', $info, 'Desensitize'); + $this->assertFixedMetadata('20101014 121215', $info, 'TransactionDate'); + + $this->assertVariableMetadata('MAIN', $info, 'AO'); + $this->assertVariableMetadata('20101104 120000', $info, 'AH'); + $this->assertVariableMetadata('JSMITH', $info, 'AA'); + $this->assertVariableMetadata('111111', $info, 'AB'); + $this->assertVariableMetadata('Marmalade and jam making for dummies', $info, 'AJ'); + $this->assertVariableMetadata('1.30', $info, 'BV'); + } +} diff --git a/tests/HoldTest.php b/tests/HoldTest.php new file mode 100644 index 0000000..191fe44 --- /dev/null +++ b/tests/HoldTest.php @@ -0,0 +1,40 @@ +makeResponse("161Y20180711 185645BW20180711 185645" . + "BR1|BSLibrary|AO123|AApatron|ABitem|AJtitle|AFthankyou|AGprint|") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgHold('+', strtotime('2018-07-11 11:22:33'), 1, 'Item', 'Title', 'N', 'Loc'); + $response = $client->getMessage($msg); + + $info = $client->parseHoldResponse($response); + + $this->assertFixedMetadata('1', $info, 'Ok'); + $this->assertFixedMetadata('Y', $info, 'available'); + $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); + $this->assertFixedMetadata('20180711 185645', $info, 'ExpirationDate'); + + $this->assertVariableMetadata('1', $info, 'BR'); + $this->assertVariableMetadata('Library', $info, 'BS'); + $this->assertVariableMetadata('123', $info, 'AO'); + $this->assertVariableMetadata('patron', $info, 'AA'); + $this->assertVariableMetadata('item', $info, 'AB'); + $this->assertVariableMetadata('title', $info, 'AJ'); + $this->assertVariableMetadata('thankyou', $info, 'AF'); + $this->assertVariableMetadata('print', $info, 'AG'); + } +} diff --git a/tests/LoginTest.php b/tests/LoginTest.php new file mode 100644 index 0000000..434e847 --- /dev/null +++ b/tests/LoginTest.php @@ -0,0 +1,26 @@ +makeResponse("941") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgLogin('username', 'password'); + $response = $client->getMessage($msg); + + $info = $client->parseLoginResponse($response); + $this->assertFixedMetadata('1', $info, 'Ok'); + } +} diff --git a/tests/PatronInfoTest.php b/tests/PatronInfoTest.php new file mode 100644 index 0000000..ab5bfe3 --- /dev/null +++ b/tests/PatronInfoTest.php @@ -0,0 +1,58 @@ +makeResponse("64 00020180711 185645000000000010000000000009" . + "AOExample City Library|AA1381380|AEMr Joe Tester|BZ9999|CA9999|CB9999|BLY|CQY|BV0.00|" . + "BEjoe.tester@example.com|") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $ok = $client->connect(); + $this->assertTrue($ok); + + $msg = $client->msgPatronInformation('none'); + $this->assertNotEmpty($msg); + + $response = $client->getMessage($msg); + $this->assertNotEmpty($response); + + $info = $client->parsePatronInfoResponse($response); + $this->assertArrayHasKey('fixed', $info); + $this->assertArrayHasKey('variable', $info); + $this->assertArrayHasKey('Raw', $info['variable']); + + //check the fixed data + $this->assertFixedMetadata(' ', $info, 'PatronStatus'); + $this->assertFixedMetadata('000', $info, 'Language'); + $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); + $this->assertFixedMetadata(0, $info, 'HoldCount'); + $this->assertFixedMetadata(10, $info, 'ChargedCount'); + $this->assertFixedMetadata(0, $info, 'FineCount'); + $this->assertFixedMetadata(0, $info, 'RecallCount'); + $this->assertFixedMetadata(9, $info, 'UnavailableCount'); + + //check variable data + $this->assertVariableMetadata('Example City Library', $info, 'AO'); + $this->assertVariableMetadata('1381380', $info, 'AA'); + $this->assertVariableMetadata('Mr Joe Tester', $info, 'AE'); + $this->assertVariableMetadata('9999', $info, 'BZ'); + $this->assertVariableMetadata('9999', $info, 'CA'); + $this->assertVariableMetadata('9999', $info, 'CB'); + $this->assertVariableMetadata('Y', $info, 'BL'); + $this->assertVariableMetadata('Y', $info, 'CQ'); + $this->assertVariableMetadata('0.00', $info, 'BV'); + $this->assertVariableMetadata('joe.tester@example.com', $info, 'BE'); + $this->assertVariableMetadata('0', $info, 'AY'); + $this->assertVariableMetadata("CF82", $info, 'AZ'); + } +} diff --git a/tests/PatronStatusTest.php b/tests/PatronStatusTest.php new file mode 100644 index 0000000..998081a --- /dev/null +++ b/tests/PatronStatusTest.php @@ -0,0 +1,51 @@ +makeResponse("24". + "xxxYYYYxxxYYYY". + "ENG". + "20180711 185645". + "AO1234|". + "AApatron|". + "AEJoe|". + "BLY|". + "CQY|". + "BHGBP|". + "BV1.23|". + "AFmessage|". + "AGprint|") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgPatronStatusRequest(); + $response = $client->getMessage($msg); + + $info = $client->parsePatronStatusResponse($response); + + $this->assertFixedMetadata('xxxYYYYxxxYYYY', $info, 'PatronStatus'); + $this->assertFixedMetadata('ENG', $info, 'Language'); + $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); + + $this->assertVariableMetadata('1234', $info, 'AO'); + $this->assertVariableMetadata('patron', $info, 'AA'); + $this->assertVariableMetadata('Joe', $info, 'AE'); + $this->assertVariableMetadata('Y', $info, 'BL'); + $this->assertVariableMetadata('Y', $info, 'CQ'); + $this->assertVariableMetadata('GBP', $info, 'BH'); + $this->assertVariableMetadata('1.23', $info, 'BV'); + $this->assertVariableMetadata('message', $info, 'AF'); + $this->assertVariableMetadata('print', $info, 'AG'); + } +} diff --git a/tests/SCStatusTest.php b/tests/SCStatusTest.php new file mode 100644 index 0000000..793e3be --- /dev/null +++ b/tests/SCStatusTest.php @@ -0,0 +1,49 @@ +makeResponse("98". + "Y". + "N". + "Y". + "N". + "Y". + "N". + "123". + "456". + "20180711 185645". + "2.00". + "AOinstitution|") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgSCStatus(0, 80, 2); + $response = $client->getMessage($msg); + + $info = $client->parseACSStatusResponse($response); + + $this->assertFixedMetadata('Y', $info, 'Online'); + $this->assertFixedMetadata('N', $info, 'Checkin'); + $this->assertFixedMetadata('Y', $info, 'Checkout'); + $this->assertFixedMetadata('N', $info, 'Renewal'); + $this->assertFixedMetadata('Y', $info, 'PatronUpdate'); + $this->assertFixedMetadata('N', $info, 'Offline'); + $this->assertFixedMetadata('123', $info, 'Timeout'); + $this->assertFixedMetadata('456', $info, 'Retries'); + $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); + $this->assertFixedMetadata('2.00', $info, 'Protocol'); + + $this->assertVariableMetadata('institution', $info, 'AO'); + } +} From 068883bb0fb83bd794c1d4b53e66e4b462c5a9a9 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Wed, 18 Jul 2018 10:28:54 +0100 Subject: [PATCH 06/35] Ensure parseHoldResponse copes with optional elements --- src/SIP2Client.php | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/SIP2Client.php b/src/SIP2Client.php index c2a6f0d..1377583 100644 --- a/src/SIP2Client.php +++ b/src/SIP2Client.php @@ -202,12 +202,13 @@ public function msgSCStatus($status = 0, $width = 80, $version = 2) * 2 SC is about to shut down */ - if ($version > 3) { - $version = 2; - } + $version = min(2, $version); + if ($status < 0 || $status > 2) { + //@codeCoverageIgnoreStart $this->debugMsg("SIP2: Invalid status passed to msgSCStatus"); return false; + //@codeCoverageIgnoreEnd } $this->newMessage('99'); $this->addFixedOption($status, 1); @@ -644,12 +645,17 @@ public function parseHoldResponse($response) [ 'Ok' => substr($response, 2, 1), 'available' => substr($response, 3, 1), - 'TransactionDate' => substr($response, 4, 18), - 'ExpirationDate' => substr($response, 22, 18) + 'TransactionDate' => substr($response, 4, 18) ]; + //expiration date is optional an indicated by BW + $variableOffset=22; + if (substr($response, 22, 2) === 'BW') { + $result['fixed']['ExpirationDate'] = substr($response, 24, 18); + $variableOffset=42; + } - $result['variable'] = $this->parseVariableData($response, 40); + $result['variable'] = $this->parseVariableData($response, $variableOffset); return $result; } @@ -708,8 +714,7 @@ public function getMessage($message) while ($terminator != "\x0D") { try { $terminator = $this->socket->recv(1, 0); - } - catch (\Exception $e) { + } catch (\Exception $e) { break; } //$nr = socket_recv($this->socket, $terminator, 1, 0); @@ -749,8 +754,7 @@ public function connect() $address = $this->hostname . ':' . $this->port; try { $this->socket = $this->getSocketFactory()->createClient($address); - } - catch (\Exception $e) { + } catch (\Exception $e) { $this->debugMsg("SIP2Client: Failed to connect: ".$e->getMessage()); return false; } From bc217136d15afe7fb637e653b5bad6545de6d442 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Wed, 18 Jul 2018 10:29:50 +0100 Subject: [PATCH 07/35] Corrected method name --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index abd0323..8536589 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ $result = $mysip->connect(); $in = $mysip->msgPatronInformation('charged'); // parse the raw response into an array -$result = $mysip->parsePatronInfoResponse( $mysip->get_message($in) ); +$result = $mysip->parsePatronInfoResponse( $mysip->getMessage($in) ); ``` ## Change log From fb76caec66ec810e7eee7a0c6bcb7a1cc3d1c69d Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Wed, 18 Jul 2018 10:47:29 +0100 Subject: [PATCH 08/35] Add tests for fee-paid and end-patron-session --- tests/EndPatronSessionTest.php | 41 ++++++++++++++++++++++++++ tests/FeePaidTest.php | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 tests/EndPatronSessionTest.php create mode 100644 tests/FeePaidTest.php diff --git a/tests/EndPatronSessionTest.php b/tests/EndPatronSessionTest.php new file mode 100644 index 0000000..2ffb398 --- /dev/null +++ b/tests/EndPatronSessionTest.php @@ -0,0 +1,41 @@ +makeResponse("36". + "Y". + "20180711 185645". + "AO1234|". + "AApatron|". + "AEJoe|". + "AFmessage|". + "AGprint|") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgEndPatronSession(); + $response = $client->getMessage($msg); + + $info = $client->parseEndSessionResponse($response); + + $this->assertFixedMetadata('Y', $info, 'EndSession'); + $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); + + $this->assertVariableMetadata('1234', $info, 'AO'); + $this->assertVariableMetadata('patron', $info, 'AA'); + $this->assertVariableMetadata('Joe', $info, 'AE'); + $this->assertVariableMetadata('message', $info, 'AF'); + $this->assertVariableMetadata('print', $info, 'AG'); + } +} diff --git a/tests/FeePaidTest.php b/tests/FeePaidTest.php new file mode 100644 index 0000000..fceca54 --- /dev/null +++ b/tests/FeePaidTest.php @@ -0,0 +1,53 @@ +makeResponse("36". + "Y". + "20180711 185645". + "AO1234|". + "AApatron|". + "BK5555|". + "AFmessage|". + "AGprint|") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgFeePaid(4, 0, '1.30', 'GBP', 'xxx', 'yyy'); + $response = $client->getMessage($msg); + + $info = $client->parseFeePaidResponse($response); + + $this->assertFixedMetadata('Y', $info, 'PaymentAccepted'); + $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); + + $this->assertVariableMetadata('1234', $info, 'AO'); + $this->assertVariableMetadata('patron', $info, 'AA'); + $this->assertVariableMetadata('5555', $info, 'BK'); + $this->assertVariableMetadata('message', $info, 'AF'); + $this->assertVariableMetadata('print', $info, 'AG'); + } + + public function testBadFeeType() + { + $client = new SIP2Client; + $this->assertFalse($client->msgFeePaid(100, 0, '1.30')); + } + + public function testBadPaymentType() + { + $client = new SIP2Client; + $this->assertFalse($client->msgFeePaid(2, 100, '1.30')); + } +} From 9656efd321ece9a9eeb031102b7541798fca0f2d Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Wed, 18 Jul 2018 11:00:58 +0100 Subject: [PATCH 09/35] Complete coverage of hold message generation --- tests/HoldTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/HoldTest.php b/tests/HoldTest.php index 191fe44..2482230 100644 --- a/tests/HoldTest.php +++ b/tests/HoldTest.php @@ -37,4 +37,16 @@ public function testHold() $this->assertVariableMetadata('thankyou', $info, 'AF'); $this->assertVariableMetadata('print', $info, 'AG'); } + + public function testBadMode() + { + $client = new SIP2Client; + $this->assertFalse($client->msgHold('X')); + } + + public function testBadHoldType() + { + $client = new SIP2Client; + $this->assertFalse($client->msgHold('+', '', 999)); + } } From b8b1cb0e8947e528e1856be954199788f9a9ef4b Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Thu, 19 Jul 2018 13:12:13 +0100 Subject: [PATCH 10/35] Full test coverage --- tests/ACSResendTest.php | 4 ++ tests/AbstractSIP2ClientTest.php | 7 +++ tests/CRCFailureTest.php | 54 ++++++++++++++++++++++ tests/ConnectionFailureTest.php | 37 +++++++++++++++ tests/ItemInformationTest.php | 75 +++++++++++++++++++++++++++++++ tests/ItemStatusTest.php | 41 +++++++++++++++++ tests/LoginTest.php | 2 + tests/PatronEnableTest.php | 47 +++++++++++++++++++ tests/RenewAllTest.php | 50 +++++++++++++++++++++ tests/RenewTest.php | 77 ++++++++++++++++++++++++++++++++ tests/SequencingTest.php | 28 ++++++++++++ 11 files changed, 422 insertions(+) create mode 100644 tests/CRCFailureTest.php create mode 100644 tests/ConnectionFailureTest.php create mode 100644 tests/ItemInformationTest.php create mode 100644 tests/ItemStatusTest.php create mode 100644 tests/PatronEnableTest.php create mode 100644 tests/RenewAllTest.php create mode 100644 tests/RenewTest.php create mode 100644 tests/SequencingTest.php diff --git a/tests/ACSResendTest.php b/tests/ACSResendTest.php index 4238984..bc14405 100644 --- a/tests/ACSResendTest.php +++ b/tests/ACSResendTest.php @@ -2,6 +2,10 @@ namespace lordelph\SIP2; +/** + * Tests the ACS Resend request + * @package lordelph\SIP2 + */ class ACSResendTest extends AbstractSIP2ClientTest { public function testResend() diff --git a/tests/AbstractSIP2ClientTest.php b/tests/AbstractSIP2ClientTest.php index 26094f1..d4bf91f 100644 --- a/tests/AbstractSIP2ClientTest.php +++ b/tests/AbstractSIP2ClientTest.php @@ -4,6 +4,12 @@ use Prophecy\Argument; +/** + * AbstractSIP2ClientTest provides a mock socket which can return a sequence of canned responses, and + * helpers to assist with asserting the content of a parsed response + * + * @package lordelph\SIP2 + */ abstract class AbstractSIP2ClientTest extends \PHPUnit\Framework\TestCase { /** @@ -79,6 +85,7 @@ protected function createMockSIP2Server(array $responses) return $socket->responses[$socket->responseIdx][$socket->responseOffset++]; }); + $socket->close()->willReturn(true); //our factory just returns our mock $factory = $this->prophesize(\Socket\Raw\Factory::class); diff --git a/tests/CRCFailureTest.php b/tests/CRCFailureTest.php new file mode 100644 index 0000000..bee3e23 --- /dev/null +++ b/tests/CRCFailureTest.php @@ -0,0 +1,54 @@ +makeResponse("941") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgLogin('username', 'password'); + $response = $client->getMessage($msg); + + $info = $client->parseLoginResponse($response); + $this->assertFixedMetadata('1', $info, 'Ok'); + } + + public function testCRCFailureAbort() + { + //our mock socket will return these responses in sequence after each write() to the socket + //here we simulate a continued failure to provide a valid response, leading us to abort after + //3 retries + $responses = [ + "940AY0AZ1234\r", + "940AY0AZ1234\r", + "940AY0AZ1234\r", + "940AY0AZ1234\r", + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgLogin('username', 'password'); + $response = $client->getMessage($msg); + $this->assertFalse($response); + } +} diff --git a/tests/ConnectionFailureTest.php b/tests/ConnectionFailureTest.php new file mode 100644 index 0000000..a419acf --- /dev/null +++ b/tests/ConnectionFailureTest.php @@ -0,0 +1,37 @@ +setSocketFactory($this->createUnconnectableMockSIP2Server()); + + $ok = $client->connect(); + $this->assertFalse($ok); + } + + + /** + * This provides a socket factory which will always fail to connect + * @return \Socket\Raw\Factory + */ + protected function createUnconnectableMockSIP2Server() + { + //our factory will always fail to connect... + $factory = $this->prophesize(\Socket\Raw\Factory::class); + $factory->createClient(Argument::type('string'))->will(function () { + throw new \Exception('Test connection failure'); + }); + + return $factory->reveal(); + } +} diff --git a/tests/ItemInformationTest.php b/tests/ItemInformationTest.php new file mode 100644 index 0000000..38da626 --- /dev/null +++ b/tests/ItemInformationTest.php @@ -0,0 +1,75 @@ +makeResponse("18" . + "01" . + "02" . + "03" . + "20180711 185645" . + "CF3|" . + "AH20180711 185645|" . + "CJ20180711 185645|" . + "CM20180711 185645|" . + "AB1565921879|" . + "AJPerl 5 desktop reference|" . + "BGBR1|" . + "BHGBP|" . + "BV1.23|" . + "CK001|" . + "AQBR2|" . + "APBR3|" . + "CHprop|" . + "AFmessage|" . + "AGprint|") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgItemInformation('item'); + $response = $client->getMessage($msg); + + $info = $client->parseItemInfoResponse($response); + + $this->assertFixedMetadata('01', $info, 'CirculationStatus'); + $this->assertFixedMetadata('02', $info, 'SecurityMarker'); + $this->assertFixedMetadata('03', $info, 'FeeType'); + $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); + + $this->assertVariableMetadata('3', $info, 'CF'); + $this->assertVariableMetadata('20180711 185645', $info, 'AH'); + $this->assertVariableMetadata('20180711 185645', $info, 'CJ'); + $this->assertVariableMetadata('20180711 185645', $info, 'CM'); + $this->assertVariableMetadata('1565921879', $info, 'AB'); + $this->assertVariableMetadata('Perl 5 desktop reference', $info, 'AJ'); + $this->assertVariableMetadata('BR1', $info, 'BG'); + $this->assertVariableMetadata('GBP', $info, 'BH'); + $this->assertVariableMetadata('1.23', $info, 'BV'); + $this->assertVariableMetadata('001', $info, 'CK'); + $this->assertVariableMetadata('BR2', $info, 'AQ'); + $this->assertVariableMetadata('BR3', $info, 'AP'); + $this->assertVariableMetadata('prop', $info, 'CH'); + $this->assertVariableMetadata('message', $info, 'AF'); + $this->assertVariableMetadata('print', $info, 'AG'); + } +} diff --git a/tests/ItemStatusTest.php b/tests/ItemStatusTest.php new file mode 100644 index 0000000..b40b29c --- /dev/null +++ b/tests/ItemStatusTest.php @@ -0,0 +1,41 @@ +makeResponse("20" . + "1" . + "20180711 185645" . + "AB1565921879|" . + "AJPerl 5 desktop reference|" . + "CHprop|" . + "AFmessage|" . + "AGprint|") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgItemStatus('item', 'prop'); + $response = $client->getMessage($msg); + + $info = $client->parseItemStatusResponse($response); + + $this->assertFixedMetadata('1', $info, 'PropertiesOk'); + $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); + + $this->assertVariableMetadata('1565921879', $info, 'AB'); + $this->assertVariableMetadata('Perl 5 desktop reference', $info, 'AJ'); + $this->assertVariableMetadata('prop', $info, 'CH'); + $this->assertVariableMetadata('message', $info, 'AF'); + $this->assertVariableMetadata('print', $info, 'AG'); + } +} diff --git a/tests/LoginTest.php b/tests/LoginTest.php index 434e847..a223fb2 100644 --- a/tests/LoginTest.php +++ b/tests/LoginTest.php @@ -22,5 +22,7 @@ public function testLogin() $info = $client->parseLoginResponse($response); $this->assertFixedMetadata('1', $info, 'Ok'); + + $client->disconnect(); } } diff --git a/tests/PatronEnableTest.php b/tests/PatronEnableTest.php new file mode 100644 index 0000000..c860ee1 --- /dev/null +++ b/tests/PatronEnableTest.php @@ -0,0 +1,47 @@ +makeResponse("26" . + "XXXXyyyXXXXyyy" . + "ENG". + "20180711 185645" . + "AOinstitution|" . + "AApatron|" . + "AEJoe Tester|" . + "BLY|". + "CQY|". + "AFmessage|" . + "AGprint|") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgPatronEnable(); + $response = $client->getMessage($msg); + + $info = $client->parsePatronEnableResponse($response); + + $this->assertFixedMetadata('XXXXyyyXXXXyyy', $info, 'PatronStatus'); + $this->assertFixedMetadata('ENG', $info, 'Language'); + $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); + + $this->assertVariableMetadata('institution', $info, 'AO'); + $this->assertVariableMetadata('patron', $info, 'AA'); + $this->assertVariableMetadata('Joe Tester', $info, 'AE'); + $this->assertVariableMetadata('Y', $info, 'BL'); + $this->assertVariableMetadata('Y', $info, 'CQ'); + $this->assertVariableMetadata('message', $info, 'AF'); + $this->assertVariableMetadata('print', $info, 'AG'); + } +} diff --git a/tests/RenewAllTest.php b/tests/RenewAllTest.php new file mode 100644 index 0000000..c4429f9 --- /dev/null +++ b/tests/RenewAllTest.php @@ -0,0 +1,50 @@ +makeResponse("66" . + "1" . + "0002". + "0003". + "20180711 185645" . + "AOinstitution|" . + "BMbook 1|". + "BMbook 2|". + "BNbook 3|". + "BNbook 4|". + "BNbook 5|". + "AFmessage|" . + "AGprint|") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgRenewAll('Y'); + $response = $client->getMessage($msg); + + $info = $client->parseRenewAllResponse($response); + + $this->assertFixedMetadata('1', $info, 'Ok'); + $this->assertFixedMetadata('0002', $info, 'Renewed'); + $this->assertFixedMetadata('0003', $info, 'Unrenewed'); + $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); + + $this->assertVariableMetadata('institution', $info, 'AO'); + + $this->assertVariableMetadata(['book 1', 'book 2'], $info, 'BM'); + $this->assertVariableMetadata(['book 3', 'book 4', 'book 5'], $info, 'BN'); + + $this->assertVariableMetadata('message', $info, 'AF'); + $this->assertVariableMetadata('print', $info, 'AG'); + } +} diff --git a/tests/RenewTest.php b/tests/RenewTest.php new file mode 100644 index 0000000..403948c --- /dev/null +++ b/tests/RenewTest.php @@ -0,0 +1,77 @@ +makeResponse("30" . + "1" . + "Y". + "U". + "N". + "20180711 185645" . + "AOinstitution|" . + "AApatron|" . + "AB1565921879|" . + "AJPerl 5 desktop reference|" . + "AH20180711 185645|" . + "BT01|". + "CIY|". + "BHGBP|". + "BV1.23|". + "CK001|". + "CHprop|". + "BK1234|". + "AFmessage|" . + "AGprint|") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect(); + + $msg = $client->msgRenew( + '1565921879', + 'Perl 5 desktop reference', + strtotime('2018-07-11 11:22:33'), + 'prop', + 'N', + 'N', + 'N' + ); + $response = $client->getMessage($msg); + + $info = $client->parseRenewResponse($response); + + $this->assertFixedMetadata('1', $info, 'Ok'); + $this->assertFixedMetadata('Y', $info, 'RenewalOk'); + $this->assertFixedMetadata('U', $info, 'Magnetic'); + $this->assertFixedMetadata('N', $info, 'Desensitize'); + $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); + + $this->assertVariableMetadata('institution', $info, 'AO'); + $this->assertVariableMetadata('patron', $info, 'AA'); + $this->assertVariableMetadata('1565921879', $info, 'AB'); + $this->assertVariableMetadata('Perl 5 desktop reference', $info, 'AJ'); + $this->assertVariableMetadata('20180711 185645', $info, 'AH'); + $this->assertVariableMetadata('01', $info, 'BT'); + $this->assertVariableMetadata('Y', $info, 'CI'); + $this->assertVariableMetadata('GBP', $info, 'BH'); + $this->assertVariableMetadata('1.23', $info, 'BV'); + $this->assertVariableMetadata('001', $info, 'CK'); + $this->assertVariableMetadata('prop', $info, 'CH'); + $this->assertVariableMetadata('1234', $info, 'BK'); + $this->assertVariableMetadata('message', $info, 'AF'); + $this->assertVariableMetadata('print', $info, 'AG'); + + //for coverage, build message with empty date + $msg = $client->msgRenew('123', 'Test Book', ''); + $this->assertNotEmpty($msg); + } +} diff --git a/tests/SequencingTest.php b/tests/SequencingTest.php new file mode 100644 index 0000000..b46c3b5 --- /dev/null +++ b/tests/SequencingTest.php @@ -0,0 +1,28 @@ +setSocketFactory($this->createMockSIP2Server([])); + + for ($s=0; $s<=11; $s++) { + $msg = $client->msgLogin('uu', 'pp'); + //9300CNuu|COpp|AY1AZF9E9 + + $lastSep=strrpos($msg, '|'); + $ay=substr($msg, $lastSep+1, 2); + $seq=substr($msg, $lastSep+3, 1); + + $this->assertEquals('AY', $ay); + $this->assertEquals($s % 10, intval($seq)); + } + } +} From 3317b346afbbae73cf77508c9d05f114c1dba650 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Thu, 19 Jul 2018 13:12:57 +0100 Subject: [PATCH 11/35] Fix getMessage to return retried response and minor test coverage adjustments --- src/SIP2Client.php | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/SIP2Client.php b/src/SIP2Client.php index 1377583..c58e700 100644 --- a/src/SIP2Client.php +++ b/src/SIP2Client.php @@ -712,12 +712,14 @@ public function getMessage($message) $this->debugMsg('SIP2: Request Sent, Reading response'); while ($terminator != "\x0D") { + //@codeCoverageIgnoreStart try { $terminator = $this->socket->recv(1, 0); } catch (\Exception $e) { break; } - //$nr = socket_recv($this->socket, $terminator, 1, 0); + //@codeCoverageIgnoreEnd + $result = $result . $terminator; } @@ -735,7 +737,7 @@ public function getMessage($message) /* try again */ $this->debugMsg("SIP2: Message failed CRC check, retrying ({$this->retry})"); - $this->getMessage($message); + $result = $this->getMessage($message); } else { /* give up */ $this->debugMsg("SIP2: Failed to get valid CRC after {$this->maxretry} retries."); @@ -839,6 +841,10 @@ private function getSeqNumber() return ($this->seq); } + /** + * @param $message + * @codeCoverageIgnore + */ private function debugMsg($message) { /* custom debug function, why repeat the check for the debug flag in code... */ @@ -871,11 +877,13 @@ private function addFixedOption($value, $len) { /* adds a fixed length option to the msgBuild IF no variable options have been added. */ if ($this->noFixed) { - return false; - } else { - $this->msgBuild .= sprintf("%{$len}s", substr($value, 0, $len)); - return true; + //@codeCoverageIgnoreStart + throw new \LogicException('Cannot add fixed options after variable options'); + //@codeCoverageIgnoreEnd } + + $this->msgBuild .= sprintf("%{$len}s", substr($value, 0, $len)); + return true; } private function addVarOption($field, $value, $optional = false) From 05231c77f88aa6f544405197e862c7bc0886bd70 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Thu, 19 Jul 2018 21:55:56 +0100 Subject: [PATCH 12/35] Add ability to bind to specific outbound IP --- src/SIP2Client.php | 20 +++++++++++++-- tests/AbstractSIP2ClientTest.php | 8 +++++- tests/BindingTest.php | 44 ++++++++++++++++++++++++++++++++ tests/ConnectionFailureTest.php | 15 ++++++++--- 4 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 tests/BindingTest.php diff --git a/src/SIP2Client.php b/src/SIP2Client.php index c58e700..eece03c 100644 --- a/src/SIP2Client.php +++ b/src/SIP2Client.php @@ -36,6 +36,13 @@ class SIP2Client public $library = ''; public $language = '001'; /* 001= english */ + /** + * @var string IP (or IP:port) to bind outbound connnections to + * Using this is only necessary on a machine which has multiple outbound connections and its important + * to control which one is used (normally because the remote SIP2 service is firewalled to particular IPs + */ + public $bindTo = ''; + /* Patron ID */ public $patron = ''; /* AA */ public $patronpwd = ''; /* AD */ @@ -752,11 +759,19 @@ public function connect() /* Socket Communications */ $this->debugMsg("SIP2: --- BEGIN SIP communication ---"); - $address = $this->hostname . ':' . $this->port; + + $this->socket = $this->getSocketFactory()->createFromString($address, $scheme); + try { - $this->socket = $this->getSocketFactory()->createClient($address); + if (!empty($this->bindTo)) { + $this->socket->bind($this->bindTo); + } + + $this->socket->connect($address); } catch (\Exception $e) { + $this->socket->close(); + $this->socket = null; $this->debugMsg("SIP2Client: Failed to connect: ".$e->getMessage()); return false; } @@ -765,6 +780,7 @@ public function connect() return true; } + public function disconnect() { $this->socket->close(); diff --git a/tests/AbstractSIP2ClientTest.php b/tests/AbstractSIP2ClientTest.php index d4bf91f..9a00e0f 100644 --- a/tests/AbstractSIP2ClientTest.php +++ b/tests/AbstractSIP2ClientTest.php @@ -87,9 +87,15 @@ protected function createMockSIP2Server(array $responses) $socket->close()->willReturn(true); + $socket->connect(Argument::type('string'))->willReturn(true); + $socket->bind(Argument::type('string'))->willReturn(true); + //our factory just returns our mock $factory = $this->prophesize(\Socket\Raw\Factory::class); - $factory->createClient(Argument::type('string'))->willReturn($socket->reveal()); + $factory->createFromString( + Argument::type('string'), + Argument::any() + )->willReturn($socket->reveal()); return $factory->reveal(); } diff --git a/tests/BindingTest.php b/tests/BindingTest.php new file mode 100644 index 0000000..9a5babe --- /dev/null +++ b/tests/BindingTest.php @@ -0,0 +1,44 @@ +bindTo = '1.2.3.4'; + $client->setSocketFactory($this->createBindingTestMockSIP2Server()); + + $ok = $client->connect(); + $this->assertTrue($ok); + } + + /** + * This provides a socket factory which will verify the bind method is called + * @return \Socket\Raw\Factory + */ + protected function createBindingTestMockSIP2Server() + { + $socket = $this->prophesize(\Socket\Raw\Socket::class); + $socket->connect(Argument::type('string'))->willReturn(true); + + //we verify bind gets called... + $socket->bind(Argument::type('string'))->shouldBeCalled()->willReturn(true); + + //our factory will always fail to connect... + $factory = $this->prophesize(\Socket\Raw\Factory::class); + $factory->createFromString( + Argument::type('string'), + Argument::any() + )->willReturn($socket->reveal()); + + return $factory->reveal(); + } +} diff --git a/tests/ConnectionFailureTest.php b/tests/ConnectionFailureTest.php index a419acf..d57b45a 100644 --- a/tests/ConnectionFailureTest.php +++ b/tests/ConnectionFailureTest.php @@ -19,19 +19,26 @@ public function testCRCFailureRetry() $this->assertFalse($ok); } - /** * This provides a socket factory which will always fail to connect * @return \Socket\Raw\Factory */ protected function createUnconnectableMockSIP2Server() { - //our factory will always fail to connect... - $factory = $this->prophesize(\Socket\Raw\Factory::class); - $factory->createClient(Argument::type('string'))->will(function () { + $socket = $this->prophesize(\Socket\Raw\Socket::class); + $socket->connect(Argument::type('string'))->will(function () { throw new \Exception('Test connection failure'); }); + $socket->close()->willReturn(true); + + //our factory will always fail to connect... + $factory = $this->prophesize(\Socket\Raw\Factory::class); + $factory->createFromString( + Argument::type('string'), + Argument::any() + )->willReturn($socket->reveal()); + return $factory->reveal(); } } From 37141eaa83662f255e34104986f475939947ee4d Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Thu, 19 Jul 2018 22:33:22 +0100 Subject: [PATCH 13/35] Add support for PSR-3 logger --- composer.json | 6 ++++- src/SIP2Client.php | 60 ++++++++++++++++++++++++---------------------- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/composer.json b/composer.json index b790bfc..70b66aa 100644 --- a/composer.json +++ b/composer.json @@ -18,12 +18,16 @@ ], "require": { "php": "~5.6|~7.0", - "clue/socket-raw": "^1.3" + "clue/socket-raw": "^1.3", + "psr/log": "^1.0" }, "require-dev": { "phpunit/phpunit" : ">=5.4.3", "squizlabs/php_codesniffer": "^2.3" }, + "suggest": { + "psr/log-implementation": "A PSR-3 compatible logger is recommended for troubleshooting" + }, "autoload": { "psr-4": { "lordelph\\SIP2\\": "src" diff --git a/src/SIP2Client.php b/src/SIP2Client.php index eece03c..7b270ee 100644 --- a/src/SIP2Client.php +++ b/src/SIP2Client.php @@ -16,6 +16,10 @@ * @link https://github.com/cap60552/php-sip2/ */ +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use Socket\Raw\Factory; use \Socket\Raw\Socket; @@ -27,8 +31,9 @@ * */ -class SIP2Client +class SIP2Client implements LoggerAwareInterface { + use LoggerAwareTrait; /* Public variables for configuration */ public $hostname; @@ -94,6 +99,17 @@ class SIP2Client private $socketFactory; + /** + * Constructor allows you to provide a PSR-3 logger, but you can also use the setLogger method + * later on + * + * @param LoggerInterface|null $logger + */ + public function __construct(LoggerInterface $logger = null) + { + $this->logger = $logger ?? new NullLogger(); + } + /** * Allows an alternative socket factory to be injected. The allows us to * mock socket connections for testing @@ -213,7 +229,7 @@ public function msgSCStatus($status = 0, $width = 80, $version = 2) if ($status < 0 || $status > 2) { //@codeCoverageIgnoreStart - $this->debugMsg("SIP2: Invalid status passed to msgSCStatus"); + $this->logger->error("SIP2: Invalid status passed to msgSCStatus"); return false; //@codeCoverageIgnoreEnd } @@ -310,13 +326,13 @@ public function msgFeePaid($feeType, $pmtType, $pmtAmount, $curType = 'USD', $fe if (!is_numeric($feeType) || $feeType > 99 || $feeType < 1) { /* not a valid fee type - exit */ - $this->debugMsg("SIP2: (msgFeePaid) Invalid fee type: {$feeType}"); + $this->logger->error("SIP2: (msgFeePaid) Invalid fee type: {$feeType}"); return false; } if (!is_numeric($pmtType) || $pmtType > 99 || $pmtType < 0) { /* not a valid payment type - exit */ - $this->debugMsg("SIP2: (msgFeePaid) Invalid payment type: {$pmtType}"); + $this->logger->error("SIP2: (msgFeePaid) Invalid payment type: {$pmtType}"); return false; } @@ -395,7 +411,7 @@ public function msgHold( */ if (strpos('-+*', $mode) === false) { /* not a valid mode - exit */ - $this->debugMsg("SIP2: Invalid hold mode: {$mode}"); + $this->logger->error("SIP2: Invalid hold mode: {$mode}"); return false; } @@ -407,7 +423,7 @@ public function msgHold( * 3 specific copy * 4 any copy at a single branch or location */ - $this->debugMsg("SIP2: Invalid hold type code: {$holdtype}"); + $this->logger->error("SIP2: Invalid hold type code: {$holdtype}"); return false; } @@ -713,10 +729,10 @@ public function getMessage($message) $result = ''; $terminator = ''; - $this->debugMsg('SIP2: Sending SIP2 request...'); + $this->logger->debug('SIP2: Sending SIP2 request...'); $this->socket->write($message); - $this->debugMsg('SIP2: Request Sent, Reading response'); + $this->logger->debug('SIP2: Request Sent, Reading response'); while ($terminator != "\x0D") { //@codeCoverageIgnoreStart @@ -730,24 +746,24 @@ public function getMessage($message) $result = $result . $terminator; } - $this->debugMsg("SIP2: {$result}"); + $this->logger->info("SIP2: result={$result}"); /* test message for CRC validity */ if ($this->checkCRC($result)) { /* reset the retry counter on successful send */ $this->retry = 0; - $this->debugMsg("SIP2: Message from ACS passed CRC check"); + $this->logger->debug("SIP2: Message from ACS passed CRC check"); } else { /* CRC check failed, request a resend */ $this->retry++; if ($this->retry < $this->maxretry) { /* try again */ - $this->debugMsg("SIP2: Message failed CRC check, retrying ({$this->retry})"); + $this->logger->warning("SIP2: Message failed CRC check, retrying ({$this->retry})"); $result = $this->getMessage($message); } else { /* give up */ - $this->debugMsg("SIP2: Failed to get valid CRC after {$this->maxretry} retries."); + $this->logger->error("SIP2: Failed to get valid CRC after {$this->maxretry} retries."); return false; } } @@ -758,7 +774,7 @@ public function connect() { /* Socket Communications */ - $this->debugMsg("SIP2: --- BEGIN SIP communication ---"); + $this->logger->debug("SIP2: --- BEGIN SIP communication ---"); $address = $this->hostname . ':' . $this->port; $this->socket = $this->getSocketFactory()->createFromString($address, $scheme); @@ -772,11 +788,11 @@ public function connect() } catch (\Exception $e) { $this->socket->close(); $this->socket = null; - $this->debugMsg("SIP2Client: Failed to connect: ".$e->getMessage()); + $this->logger->error("SIP2Client: Failed to connect: ".$e->getMessage()); return false; } - $this->debugMsg("SIP2: --- SOCKET READY ---"); + $this->logger->debug("SIP2: --- SOCKET READY ---"); return true; } @@ -857,18 +873,6 @@ private function getSeqNumber() return ($this->seq); } - /** - * @param $message - * @codeCoverageIgnore - */ - private function debugMsg($message) - { - /* custom debug function, why repeat the check for the debug flag in code... */ - if ($this->debug) { - trigger_error($message, E_USER_NOTICE); - } - } - private function checkCRC($message) { /* test the received message's CRC by generating our own CRC from the message */ @@ -907,7 +911,7 @@ private function addVarOption($field, $value, $optional = false) /* adds a variable length option to the message, and also prevents adding additional fixed fields */ if ($optional == true && $value == '') { /* skipped */ - $this->debugMsg("SIP2: Skipping optional field {$field}"); + $this->logger->debug("SIP2: Skipping optional field {$field}"); } else { $this->noFixed = true; /* no more fixed for this message */ $this->msgBuild .= $field . substr($value, 0, 255) . $this->fldTerminator; From dc34ab5023605ab7c57ad9591187c4d97da75130 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Thu, 19 Jul 2018 23:19:07 +0100 Subject: [PATCH 14/35] Rename some member variables for clarity and improve comments on each --- src/SIP2Client.php | 165 +++++++++++++++++++++++++-------------------- 1 file changed, 93 insertions(+), 72 deletions(-) diff --git a/src/SIP2Client.php b/src/SIP2Client.php index 7b270ee..04fd2e6 100644 --- a/src/SIP2Client.php +++ b/src/SIP2Client.php @@ -24,22 +24,21 @@ use \Socket\Raw\Socket; /** - * - * TODO - * - Clean up variable names, check for consistency - * - Add better i18n support, including functions to handle the SIP2 language definitions - * + * SIP2Client provides a simple client for SIP2 library services */ - class SIP2Client implements LoggerAwareInterface { use LoggerAwareTrait; - /* Public variables for configuration */ + //----------------------------------------------------- + // connection configuration + //----------------------------------------------------- + + /** @var string hostname or IP address to connect to */ public $hostname; - public $port = 6002; /* default sip2 port for Sirsi */ - public $library = ''; - public $language = '001'; /* 001= english */ + + /** @var int port number */ + public $port = 6002; /** * @var string IP (or IP:port) to bind outbound connnections to @@ -48,55 +47,77 @@ class SIP2Client implements LoggerAwareInterface */ public $bindTo = ''; - /* Patron ID */ - public $patron = ''; /* AA */ - public $patronpwd = ''; /* AD */ + /** @var int maximum number of resends in the event of CRC failure */ + public $maxretry = 3; - /* Terminal password */ - public $AC = ''; /*AC */ + //----------------------------------------------------- + // patron credentials + //----------------------------------------------------- - /* Maximum number of resends allowed before getMessage gives up */ - public $maxretry = 3; + /** @var string patron identifier / barcode */ + public $patron = ''; + + /** @var string patron password / pin */ + public $patronpwd = ''; + + //----------------------------------------------------- + // request options + //----------------------------------------------------- - /* Terminators - * - * From page 15 of SPI2 v2 docs: - * All messages must end in a carriage return (hexadecimal 0d). - * - * Technically $msgTerminator should be set to \r only. However some vendors mistakenly require the \r\n. - * - * TODO: Create a function to set this, rather than exposing the variable as public. - */ + /** @var string language code - 001 is English */ + public $language = '001'; + + /** + * @var string terminator for requests. This should be just \r (0x0d) according to docs, but some vendors + * require \r\n + */ public $msgTerminator = "\r\n"; + /** @var string variable length field terminator */ public $fldTerminator = '|'; - /* Login Variables */ - public $UIDalgorithm = 0; /* 0 = unencrypted, default */ - public $PWDalgorithm = 0; /* undefined in documentation */ - public $scLocation = ''; /* Location Code */ + /** @var int encryption algorithm for user id using during login 0=unencrypted */ + public $uidAlgorithm = 0; + + /** @var int encryption algorithm for user password using during login (no docs for this) */ + public $passwordAlgorithm = 0; - /* Debug */ - public $debug = false; + /** @var string Default location used in some request messages */ + public $location = ''; - /* Public variables used for building messages */ - public $AO = 'WohlersSIP'; - public $AN = 'SIPCHK'; - public $AA = ''; + /** @var string Institution ID */ + public $institutionId = 'WohlersSIP'; - /** @var Socket */ - private $socket; + /** @var string Patron identifier */ + public $patronId = ''; + + /** @var string Terminal password */ + public $terminalPassword = ''; + + //----------------------------------------------------- + // internal request building + //----------------------------------------------------- - /* Sequence number counter */ + /** @var int sequence counter for AY */ private $seq = -1; - /* resend counter */ + /** @var int resend counter */ private $retry = 0; - /* Work area for building a message */ + /** @var string request is built up here */ private $msgBuild = ''; + + /** @var bool tracks when a variable field is used to prevent further fixed fields */ private $noFixed = false; + //----------------------------------------------------- + // internal socket handling + //----------------------------------------------------- + + /** @var Socket */ + private $socket; + + /** @var Factory injectable factory for creating socket connections */ private $socketFactory; /** @@ -139,9 +160,9 @@ public function msgPatronStatusRequest() $this->newMessage('23'); $this->addFixedOption($this->language, 3); $this->addFixedOption($this->datestamp(), 18); - $this->addVarOption('AO', $this->AO); + $this->addVarOption('AO', $this->institutionId); $this->addVarOption('AA', $this->patron); - $this->addVarOption('AC', $this->AC); + $this->addVarOption('AC', $this->terminalPassword); $this->addVarOption('AD', $this->patronpwd); return $this->returnMessage(); } @@ -168,10 +189,10 @@ public function msgCheckout( /* send a blank date due to allow ACS to use default date due computed for item */ $this->addFixedOption('', 18); } - $this->addVarOption('AO', $this->AO); + $this->addVarOption('AO', $this->institutionId); $this->addVarOption('AA', $this->patron); $this->addVarOption('AB', $item); - $this->addVarOption('AC', $this->AC); + $this->addVarOption('AC', $this->terminalPassword); $this->addVarOption('CH', $itmProp, true); $this->addVarOption('AD', $this->patronpwd, true); $this->addVarOption('BO', $fee, true); /* Y or N */ @@ -185,7 +206,7 @@ public function msgCheckin($item, $itmReturnDate, $itmLocation = '', $itmProp = /* Check-in an item (09) - untested */ if ($itmLocation == '') { /* If no location is specified, assume the default location of the SC, behaviour suggested by spec*/ - $itmLocation = $this->scLocation; + $itmLocation = $this->location; } $this->newMessage('09'); @@ -193,9 +214,9 @@ public function msgCheckin($item, $itmReturnDate, $itmLocation = '', $itmProp = $this->addFixedOption($this->datestamp(), 18); $this->addFixedOption($this->datestamp($itmReturnDate), 18); $this->addVarOption('AP', $itmLocation); - $this->addVarOption('AO', $this->AO); + $this->addVarOption('AO', $this->institutionId); $this->addVarOption('AB', $item); - $this->addVarOption('AC', $this->AC); + $this->addVarOption('AC', $this->terminalPassword); $this->addVarOption('CH', $itmProp, true); $this->addVarOption('BI', $cancel, true); /* Y or N */ @@ -208,10 +229,10 @@ public function msgBlockPatron($message, $retained = 'N') $this->newMessage('01'); $this->addFixedOption($retained, 1); /* Y if card has been retained */ $this->addFixedOption($this->datestamp(), 18); - $this->addVarOption('AO', $this->AO); + $this->addVarOption('AO', $this->institutionId); $this->addVarOption('AL', $message); - $this->addVarOption('AA', $this->AA); - $this->addVarOption('AC', $this->AC); + $this->addVarOption('AA', $this->patronId); + $this->addVarOption('AC', $this->terminalPassword); return $this->returnMessage(); } @@ -251,11 +272,11 @@ public function msgLogin($sipLogin, $sipPassword) { /* Login (93) - untested */ $this->newMessage('93'); - $this->addFixedOption($this->UIDalgorithm, 1); - $this->addFixedOption($this->PWDalgorithm, 1); + $this->addFixedOption($this->uidAlgorithm, 1); + $this->addFixedOption($this->passwordAlgorithm, 1); $this->addVarOption('CN', $sipLogin); $this->addVarOption('CO', $sipPassword); - $this->addVarOption('CP', $this->scLocation, true); + $this->addVarOption('CP', $this->location, true); return $this->returnMessage(); } @@ -280,9 +301,9 @@ public function msgPatronInformation($type, $start = '1', $end = '5') $this->addFixedOption($this->language, 3); $this->addFixedOption($this->datestamp(), 18); $this->addFixedOption(sprintf("%-10s", $summary[$type]), 10); - $this->addVarOption('AO', $this->AO); + $this->addVarOption('AO', $this->institutionId); $this->addVarOption('AA', $this->patron); - $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('AC', $this->terminalPassword, true); $this->addVarOption('AD', $this->patronpwd, true); /* old function version used padded 5 digits, not sure why */ $this->addVarOption('BP', $start, true); @@ -297,9 +318,9 @@ public function msgEndPatronSession() $this->newMessage('35'); $this->addFixedOption($this->datestamp(), 18); - $this->addVarOption('AO', $this->AO); + $this->addVarOption('AO', $this->institutionId); $this->addVarOption('AA', $this->patron); - $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('AC', $this->terminalPassword, true); $this->addVarOption('AD', $this->patronpwd, true); return $this->returnMessage(); } @@ -345,9 +366,9 @@ public function msgFeePaid($feeType, $pmtType, $pmtAmount, $curType = 'USD', $fe // due to currency format localization, it is up to the programmer // to properly format their payment amount $this->addVarOption('BV', $pmtAmount); - $this->addVarOption('AO', $this->AO); + $this->addVarOption('AO', $this->institutionId); $this->addVarOption('AA', $this->patron); - $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('AC', $this->terminalPassword, true); $this->addVarOption('AD', $this->patronpwd, true); $this->addVarOption('CG', $feeId, true); $this->addVarOption('BK', $transId, true); @@ -360,9 +381,9 @@ public function msgItemInformation($item) $this->newMessage('17'); $this->addFixedOption($this->datestamp(), 18); - $this->addVarOption('AO', $this->AO); + $this->addVarOption('AO', $this->institutionId); $this->addVarOption('AB', $item); - $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('AC', $this->terminalPassword, true); return $this->returnMessage(); } @@ -372,9 +393,9 @@ public function msgItemStatus($item, $itmProp = '') $this->newMessage('19'); $this->addFixedOption($this->datestamp(), 18); - $this->addVarOption('AO', $this->AO); + $this->addVarOption('AO', $this->institutionId); $this->addVarOption('AB', $item); - $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('AC', $this->terminalPassword, true); $this->addVarOption('CH', $itmProp); return $this->returnMessage(); } @@ -386,9 +407,9 @@ public function msgPatronEnable() It should only be used for system testing and validation. */ $this->newMessage('25'); $this->addFixedOption($this->datestamp(), 18); - $this->addVarOption('AO', $this->AO); + $this->addVarOption('AO', $this->institutionId); $this->addVarOption('AA', $this->patron); - $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('AC', $this->terminalPassword, true); $this->addVarOption('AD', $this->patronpwd, true); return $this->returnMessage(); } @@ -438,12 +459,12 @@ public function msgHold( } $this->addVarOption('BS', $pkupLocation, true); $this->addVarOption('BY', $holdtype, true); - $this->addVarOption('AO', $this->AO); + $this->addVarOption('AO', $this->institutionId); $this->addVarOption('AA', $this->patron); $this->addVarOption('AD', $this->patronpwd, true); $this->addVarOption('AB', $item, true); $this->addVarOption('AJ', $title, true); - $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('AC', $this->terminalPassword, true); $this->addVarOption('BO', $fee, true); /* Y when user has agreed to a fee notice */ return $this->returnMessage(); @@ -471,12 +492,12 @@ public function msgRenew( /* send a blank date due to allow ACS to use default date due computed for item */ $this->addFixedOption('', 18); } - $this->addVarOption('AO', $this->AO); + $this->addVarOption('AO', $this->institutionId); $this->addVarOption('AA', $this->patron); $this->addVarOption('AD', $this->patronpwd, true); $this->addVarOption('AB', $item, true); $this->addVarOption('AJ', $title, true); - $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('AC', $this->terminalPassword, true); $this->addVarOption('CH', $itmProp, true); $this->addVarOption('BO', $fee, true); /* Y or N */ @@ -487,10 +508,10 @@ public function msgRenewAll($fee = 'N') { /* renew all items for a patron (65) - untested */ $this->newMessage('65'); - $this->addVarOption('AO', $this->AO); + $this->addVarOption('AO', $this->institutionId); $this->addVarOption('AA', $this->patron); $this->addVarOption('AD', $this->patronpwd, true); - $this->addVarOption('AC', $this->AC, true); + $this->addVarOption('AC', $this->terminalPassword, true); $this->addVarOption('BO', $fee, true); /* Y or N */ return $this->returnMessage(); From fa03dbe0f2d3a5d729789890d3e4b12cb4824672 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sun, 22 Jul 2018 18:22:53 +0100 Subject: [PATCH 15/35] Add documentation for all public methods --- src/SIP2Client.php | 428 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 346 insertions(+), 82 deletions(-) diff --git a/src/SIP2Client.php b/src/SIP2Client.php index 04fd2e6..5d30b5a 100644 --- a/src/SIP2Client.php +++ b/src/SIP2Client.php @@ -13,7 +13,6 @@ * @licence https://opensource.org/licenses/MIT * @copyright John Wohlers * @version 2.0.0 - * @link https://github.com/cap60552/php-sip2/ */ use Psr\Log\LoggerAwareInterface; @@ -21,10 +20,13 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Socket\Raw\Factory; -use \Socket\Raw\Socket; +use Socket\Raw\Socket; /** * SIP2Client provides a simple client for SIP2 library services + * + * In the specification, and the comments below, 'SC' (or Self Check) denotes the client, and ACS (or Automated + * Circulation System) denotes the server. */ class SIP2Client implements LoggerAwareInterface { @@ -67,7 +69,7 @@ class SIP2Client implements LoggerAwareInterface /** @var string language code - 001 is English */ public $language = '001'; - /** + /** * @var string terminator for requests. This should be just \r (0x0d) according to docs, but some vendors * require \r\n */ @@ -78,7 +80,7 @@ class SIP2Client implements LoggerAwareInterface /** @var int encryption algorithm for user id using during login 0=unencrypted */ public $uidAlgorithm = 0; - + /** @var int encryption algorithm for user password using during login (no docs for this) */ public $passwordAlgorithm = 0; @@ -104,7 +106,7 @@ class SIP2Client implements LoggerAwareInterface /** @var int resend counter */ private $retry = 0; - /** @var string request is built up here */ + /** @var string request is built up here */ private $msgBuild = ''; /** @var bool tracks when a variable field is used to prevent further fixed fields */ @@ -143,7 +145,7 @@ public function setSocketFactory(Factory $factory) } /** - * Get the current socket factory, creating a default on if necessary + * Get the current socket factory, creating a default one if necessary * @return Factory */ private function getSocketFactory() @@ -154,6 +156,13 @@ private function getSocketFactory() return $this->socketFactory; } + /** + * This message is used by the client to request patron information from the SIP2 server. The service must + * respond to this command with a Patron Status Response message. + * @return string + * + * @see SIP2Client::parsePatronStatusResponse() + */ public function msgPatronStatusRequest() { /* Server Response: Patron Status Response message. */ @@ -167,6 +176,22 @@ public function msgPatronStatusRequest() return $this->returnMessage(); } + /** + * This message is used by the SC to request to check out an item, and also to cancel a Checkin request that did + * not successfully complete. The ACS must respond to this command with a Checkout Response message. + * + * @param string $item item identifier + * @param string $nbDateDue unix timestamp of due date (can be blank to let service decide) + * @param string $scRenewal renewal policy, either Y or N + * @param string $itmProp item properties + * @param string $fee fee acknowledge, either Y or N + * @param string $noBlock no block, either Y or N + * @param string $cancel Y or N - used to cancel an incomplete checkin + * + * @return string + * + * @see SIP2Client::parseCheckoutResponse() + */ public function msgCheckout( $item, $nbDateDue = '', @@ -177,7 +202,6 @@ public function msgCheckout( $cancel = 'N' ) { - /* Checkout an item (11) - untested */ $this->newMessage('11'); $this->addFixedOption($scRenewal, 1); $this->addFixedOption($noBlock, 1); @@ -201,9 +225,21 @@ public function msgCheckout( return $this->returnMessage(); } + /** + * This message is used by the SC to request to check in an item, and also to cancel a Checkout request that did not + * successfully complete. The ACS must respond to this command with a Checkin Response message. + * @param string $item item identifier + * @param string $itmReturnDate unix timestamp of return date + * @param string $itmLocation item location + * @param string $itmProp item properties + * @param string $noBlock no block, either Y or N + * @param string $cancel Y or N - used to cancel an incomplete checkout + * @return string + * + * @see SIP2Client::parseCheckinResponse() + */ public function msgCheckin($item, $itmReturnDate, $itmLocation = '', $itmProp = '', $noBlock = 'N', $cancel = '') { - /* Check-in an item (09) - untested */ if ($itmLocation == '') { /* If no location is specified, assume the default location of the SC, behaviour suggested by spec*/ $itmLocation = $this->location; @@ -223,9 +259,20 @@ public function msgCheckin($item, $itmReturnDate, $itmLocation = '', $itmProp = return $this->returnMessage(); } + /** + * This message requests that the patron card be blocked by the ACS. This is, for example, sent when the patron is + * detected tampering with the SC or when a patron forgets to take their card. The ACS should invalidate the + * patron’s card and respond with a Patron Status Response message. The ACS could also notify the library staff + * that the card has been blocked. + * + * @param string $message blocked card message + * @param string $retained Y/N indicating whether card was retained + * @return string + * + * @see SIP2Client::parsePatronStatusResponse() + */ public function msgBlockPatron($message, $retained = 'N') { - /* Blocks a patron, and responds with a patron status response (01) - untested */ $this->newMessage('01'); $this->addFixedOption($retained, 1); /* Y if card has been retained */ $this->addFixedOption($this->datestamp(), 18); @@ -237,15 +284,22 @@ public function msgBlockPatron($message, $retained = 'N') return $this->returnMessage(); } + /** + * The SC status message sends SC status to the ACS. It requires an ACS Status Response message reply from the ACS. + * This message will be the first message sent by the SC to the ACS once a connection has been established + * (exception: the Login Message may be sent first to login to an ACS server program). The ACS will respond with a + * message that establishes some of the rules to be followed by the SC and establishes some parameters needed for + * further communication. + * + * @param int $status 0=OK, 1=out of paper, 2=shutting down + * @param int $width print width + * @param int $version version number X.YY + * @return bool|string + * + * @see SIP2Client::parseACSStatusResponse() + */ public function msgSCStatus($status = 0, $width = 80, $version = 2) { - /* selfcheck status message, this should be sent immediately after login - untested */ - /* status codes, from the spec: - * 0 SC unit is OK - * 1 SC printer is out of paper - * 2 SC is about to shut down - */ - $version = min(2, $version); if ($status < 0 || $status > 2) { @@ -261,16 +315,33 @@ public function msgSCStatus($status = 0, $width = 80, $version = 2) return $this->returnMessage(); } + /** + * This message requests the ACS to re-transmit its last message. It is sent by the SC to the ACS when the checksum + * in a received message does not match the value calculated by the SC. The ACS should respond by re-transmitting + * its last message, This message should never include a “sequence number” field, even when error detection is + * enabled, but would include a “checksum” field since checksums are in use. + * + * @return string + */ public function msgRequestACSResend() { - /* Used to request a resend due to CRC mismatch - No sequence number is used */ $this->newMessage('97'); return $this->returnMessage(false); } + /** + * This message can be used to login to an ACS server program. The ACS should respond with the Login Response + * message. Whether to use this message or to use some other mechanism to login to the ACS is configurable on the + * SC. When this message is used, it will be the first message sent to the ACS. + * + * @param string $sipLogin username + * @param string $sipPassword password + * @return string + * + * @see SIP2Client::parseLoginResponse() + */ public function msgLogin($sipLogin, $sipPassword) { - /* Login (93) - untested */ $this->newMessage('93'); $this->addFixedOption($this->uidAlgorithm, 1); $this->addFixedOption($this->passwordAlgorithm, 1); @@ -280,14 +351,25 @@ public function msgLogin($sipLogin, $sipPassword) return $this->returnMessage(); } + /** + * This message is a superset of the Patron Status Request message. It should be used to request patron information. + * The ACS should respond with the Patron Information Response message. + * + * @param string $type one of none,hold,overdue,charged,fine,recall or unavail + * @param string $start item + * @param string $end item + * @return string + * + * @see SIP2Client::parsePatronInfoResponse() + */ public function msgPatronInformation($type, $start = '1', $end = '5') { - /* * According to the specification: * Only one category of items should be requested at a time, i.e. it would take 6 of these messages, * each with a different position set to Y, to get all the detailed information about a patron's items. */ + $summary = []; $summary['none'] = ' '; $summary['hold'] = 'Y '; $summary['overdue'] = ' Y '; @@ -312,10 +394,16 @@ public function msgPatronInformation($type, $start = '1', $end = '5') return $this->returnMessage(); } + /** + * This message will be sent when a patron has completed all of their transactions. The ACS may, upon receipt of + * this command, close any open files or deallocate data structures pertaining to that patron. The ACS should + * respond with an End Session Response message. + * @return string + * + * @see SIP2Client::parseEndSessionResponse() + */ public function msgEndPatronSession() { - /* End Patron Session, should be sent before switching to a new patron. (35) - untested */ - $this->newMessage('35'); $this->addFixedOption($this->datestamp(), 18); $this->addVarOption('AO', $this->institutionId); @@ -325,34 +413,41 @@ public function msgEndPatronSession() return $this->returnMessage(); } - /* Fee paid function should go here */ + /** + * This message can be used to notify the ACS that a fee has been collected from the patron. The ACS should record + * this information in their database and respond with a Fee Paid Response message. + * + * @param string $feeType fee type + * 01 other/unknown + * 02 administrative + * 03 damage + * 04 overdue + * 05 processing + * 06 rental + * 07 replacement + * 08 computer access charge + * 09 hold fee + * @param string $pmtType payment type + * 00 cash + * 01 visa + * 02 credit card + * @param string $pmtAmount payment amount + * @param string $curType currency 3-letter code following ISO Standard 4217:1995 + * @param string $feeId Identifies a specific fee, possibly in combination with fee type. + * @param string $transId transaction identifier + * + * @return bool|string + * + * @see SIP2Client::parseFeePaidResponse() + */ public function msgFeePaid($feeType, $pmtType, $pmtAmount, $curType = 'USD', $feeId = '', $transId = '') { - /* Fee payment function (37) - untested */ - /* Fee Types: */ - /* 01 other/unknown */ - /* 02 administrative */ - /* 03 damage */ - /* 04 overdue */ - /* 05 processing */ - /* 06 rental*/ - /* 07 replacement */ - /* 08 computer access charge */ - /* 09 hold fee */ - - /* Value Payment Type */ - /* 00 cash */ - /* 01 VISA */ - /* 02 credit card */ - if (!is_numeric($feeType) || $feeType > 99 || $feeType < 1) { - /* not a valid fee type - exit */ $this->logger->error("SIP2: (msgFeePaid) Invalid fee type: {$feeType}"); return false; } if (!is_numeric($pmtType) || $pmtType > 99 || $pmtType < 0) { - /* not a valid payment type - exit */ $this->logger->error("SIP2: (msgFeePaid) Invalid payment type: {$pmtType}"); return false; } @@ -376,9 +471,17 @@ public function msgFeePaid($feeType, $pmtType, $pmtAmount, $curType = 'USD', $fe return $this->returnMessage(); } + /** + * This message may be used to request item information. The ACS should respond with the Item Information Response + * message. + * + * @param string $item item identifier + * @return string + * + * @see SIP2Client::parseItemInfoResponse() + */ public function msgItemInformation($item) { - $this->newMessage('17'); $this->addFixedOption($this->datestamp(), 18); $this->addVarOption('AO', $this->institutionId); @@ -387,10 +490,19 @@ public function msgItemInformation($item) return $this->returnMessage(); } + /** + * This message can be used to send item information to the ACS, without having to do a Checkout or Checkin + * operation. The item properties could be stored on the ACS’s database. The ACS should respond with an Item + * Status Update Response message. + * + * @param string $item item identifier + * @param string $itmProp item properties + * @return string + * + * @see SIP2Client::parseItemStatusResponse() + */ public function msgItemStatus($item, $itmProp = '') { - /* Item status update function (19) - untested */ - $this->newMessage('19'); $this->addFixedOption($this->datestamp(), 18); $this->addVarOption('AO', $this->institutionId); @@ -400,6 +512,14 @@ public function msgItemStatus($item, $itmProp = '') return $this->returnMessage(); } + /** + * This message can be used by the SC to re-enable canceled patrons. It should only be used for system testing and + * validation. The ACS should respond with a Patron Enable Response message. + * + * @return string + * + * @see SIP2Client::parsePatronEnableResponse() + */ public function msgPatronEnable() { /* Patron Enable function (25) - untested */ @@ -414,36 +534,42 @@ public function msgPatronEnable() return $this->returnMessage(); } + /** + * This message is used to create, modify, or delete a hold. The ACS should respond with a Hold Response message. + * Either or both of the “item identifier” and “title identifier” fields must be present for the message to + * be useful. + * + * @param string $mode one of '+', '-' or '*' to denote add, delete or change + * @param string $expDate unix timestamp expiration date + * @param string $holdtype optional single digit, one of following values: + * 1 other + * 2 any copy of title + * 3 specific copy + * 4 any copy at a single branch or location + * @param string $item item identifier + * @param string $title item title + * @param string $feeAcknowledged Y/N to indicate if fee has been acknowledged + * @param string $pickupLocation pickup location + * @return bool|string + * + * @see SIP2Client::parseHoldResponse() + */ public function msgHold( $mode, $expDate = '', $holdtype = '', $item = '', $title = '', - $fee = 'N', - $pkupLocation = '' + $feeAcknowledged = 'N', + $pickupLocation = '' ) { - /* mode validity check */ - /* - * - remove hold - * + place hold - * * modify hold - */ if (strpos('-+*', $mode) === false) { - /* not a valid mode - exit */ $this->logger->error("SIP2: Invalid hold mode: {$mode}"); return false; } if ($holdtype != '' && ($holdtype < 1 || $holdtype > 9)) { - /* - * Valid hold types range from 1 - 9 - * 1 other - * 2 any copy of title - * 3 specific copy - * 4 any copy at a single branch or location - */ $this->logger->error("SIP2: Invalid hold type code: {$holdtype}"); return false; } @@ -452,12 +578,9 @@ public function msgHold( $this->addFixedOption($mode, 1); $this->addFixedOption($this->datestamp(), 18); if ($expDate != '') { - // hold expiration date, due to the use of the datestamp function, we have to check here for - // empty value. when datestamp is passed an empty value it will generate a current datestamp. - // Also, spec says this is fixed field, but it behaves like a var field and is optional... $this->addVarOption('BW', $this->datestamp($expDate), true); } - $this->addVarOption('BS', $pkupLocation, true); + $this->addVarOption('BS', $pickupLocation, true); $this->addVarOption('BY', $holdtype, true); $this->addVarOption('AO', $this->institutionId); $this->addVarOption('AA', $this->patron); @@ -465,22 +588,36 @@ public function msgHold( $this->addVarOption('AB', $item, true); $this->addVarOption('AJ', $title, true); $this->addVarOption('AC', $this->terminalPassword, true); - $this->addVarOption('BO', $fee, true); /* Y when user has agreed to a fee notice */ + $this->addVarOption('BO', $feeAcknowledged, true); return $this->returnMessage(); } + /** + * This message is used to renew an item. The ACS should respond with a Renew Response message. Either or both of + * the “item identifier” and “title identifier” fields must be present for the message to be useful. + * + * @param string $item item identifier + * @param string $title item title + * @param string $nbDateDue unix timestamp of no block due date + * @param string $itemProperties item properties + * @param string $feeAcknowledged Y/N if fee acknowledged + * @param string $noBlock Y/N if no blocking permitted - see specification + * @param string $thirdParty Y/N if third party renewals allowed + * @return string + * + * @see SIP2Client::parseRenewResponse() + */ public function msgRenew( $item = '', $title = '', $nbDateDue = '', - $itmProp = '', - $fee = 'N', + $itemProperties = '', + $feeAcknowledged = 'N', $noBlock = 'N', $thirdParty = 'N' ) { - /* renew a single item (29) - untested */ $this->newMessage('29'); $this->addFixedOption($thirdParty, 1); $this->addFixedOption($noBlock, 1); @@ -498,27 +635,43 @@ public function msgRenew( $this->addVarOption('AB', $item, true); $this->addVarOption('AJ', $title, true); $this->addVarOption('AC', $this->terminalPassword, true); - $this->addVarOption('CH', $itmProp, true); - $this->addVarOption('BO', $fee, true); /* Y or N */ + $this->addVarOption('CH', $itemProperties, true); + $this->addVarOption('BO', $feeAcknowledged, true); /* Y or N */ return $this->returnMessage(); } - public function msgRenewAll($fee = 'N') + /** + * This message is used to renew all items that the patron has checked out. The ACS should respond with a Renew All + * Response message. + * + * @param string $feeAcknowledged + * @return string + * + * @see SIP2Client::parseRenewAllResponse() + */ + public function msgRenewAll($feeAcknowledged = 'N') { - /* renew all items for a patron (65) - untested */ $this->newMessage('65'); $this->addVarOption('AO', $this->institutionId); $this->addVarOption('AA', $this->patron); $this->addVarOption('AD', $this->patronpwd, true); $this->addVarOption('AC', $this->terminalPassword, true); - $this->addVarOption('BO', $fee, true); /* Y or N */ + $this->addVarOption('BO', $feeAcknowledged, true); /* Y or N */ return $this->returnMessage(); } + /** + * Parse response from a Patron Status request + * @param string $response ACS response + * @return array with 'fixed' and 'variable' keys + * + * @see SIP2Client::msgPatronStatusRequest() + */ public function parsePatronStatusResponse($response) { + $result = []; $result['fixed'] = array( 'PatronStatus' => substr($response, 2, 14), @@ -530,8 +683,16 @@ public function parsePatronStatusResponse($response) return $result; } + /** + * Parse response from a Checkout request + * @param string $response ACS response + * @return array with 'fixed' and 'variable' keys + * + * @see SIP2Client::msgCheckout() + */ public function parseCheckoutResponse($response) { + $result = []; $result['fixed'] = array( 'Ok' => substr($response, 2, 1), @@ -545,8 +706,16 @@ public function parseCheckoutResponse($response) return $result; } + /** + * Parse response from a Checkin request + * @param string $response ACS response + * @return array with 'fixed' and 'variable' keys + * + * @see SIP2Client::msgCheckin() + */ public function parseCheckinResponse($response) { + $result = []; $result['fixed'] = array( 'Ok' => substr($response, 2, 1), @@ -560,8 +729,16 @@ public function parseCheckinResponse($response) return $result; } + /** + * Parse response from a SC Status request + * @param string $response ACS response + * @return array with 'fixed' and 'variable' keys + * + * @see SIP2Client::msgSCStatus() + */ public function parseACSStatusResponse($response) { + $result = []; $result['fixed'] = [ 'Online' => substr($response, 2, 1), @@ -584,8 +761,16 @@ public function parseACSStatusResponse($response) return $result; } + /** + * Parse response from a Login request + * @param string $response ACS response + * @return array with 'fixed' and 'variable' keys + * + * @see SIP2Client::msgSCStatus() + */ public function parseLoginResponse($response) { + $result = []; $result['fixed'] = [ 'Ok' => substr($response, 2, 1), @@ -594,9 +779,16 @@ public function parseLoginResponse($response) return $result; } + /** + * Parse response from a Patron Information request + * @param string $response ACS response + * @return array with 'fixed' and 'variable' keys + * + * @see SIP2Client::msgPatronInformation() + */ public function parsePatronInfoResponse($response) { - + $result = []; $result['fixed'] = [ 'PatronStatus' => substr($response, 2, 14), @@ -614,10 +806,17 @@ public function parsePatronInfoResponse($response) return $result; } + /** + * Parse response from a End Session request + * @param string $response ACS response + * @return array with 'fixed' and 'variable' keys + * + * @see SIP2Client::msgPatronInformation() + */ public function parseEndSessionResponse($response) { /* Response example: 36Y20080228 145537AOWOHLERS|AAX00000000|AY9AZF474 */ - + $result = []; $result['fixed'] = [ 'EndSession' => substr($response, 2, 1), @@ -630,8 +829,16 @@ public function parseEndSessionResponse($response) return $result; } + /** + * Parse response from a Fee Paid request + * @param string $response ACS response + * @return array with 'fixed' and 'variable' keys + * + * @see SIP2Client::msgFeePaid() + */ public function parseFeePaidResponse($response) { + $result = []; $result['fixed'] = [ 'PaymentAccepted' => substr($response, 2, 1), @@ -642,8 +849,16 @@ public function parseFeePaidResponse($response) return $result; } + /** + * Parse response from a Item Information request + * @param string $response ACS response + * @return array with 'fixed' and 'variable' keys + * + * @see SIP2Client::msgItemInformation() + */ public function parseItemInfoResponse($response) { + $result = []; $result['fixed'] = [ 'CirculationStatus' => intval(substr($response, 2, 2)), @@ -657,8 +872,16 @@ public function parseItemInfoResponse($response) return $result; } + /** + * Parse response from a Item Status request + * @param string $response ACS response + * @return array with 'fixed' and 'variable' keys + * + * @see SIP2Client::msgItemStatus() + */ public function parseItemStatusResponse($response) { + $result = []; $result['fixed'] = [ 'PropertiesOk' => substr($response, 2, 1), @@ -669,8 +892,16 @@ public function parseItemStatusResponse($response) return $result; } + /** + * Parse response from a Patron Enable request + * @param string $response ACS response + * @return array with 'fixed' and 'variable' keys + * + * @see SIP2Client::msgPatronEnable() + */ public function parsePatronEnableResponse($response) { + $result = []; $result['fixed'] = [ 'PatronStatus' => substr($response, 2, 14), @@ -682,9 +913,16 @@ public function parsePatronEnableResponse($response) return $result; } + /** + * Parse response from a Hold request + * @param string $response ACS response + * @return array with 'fixed' and 'variable' keys + * + * @see SIP2Client::msgHold() + */ public function parseHoldResponse($response) { - + $result = []; $result['fixed'] = [ 'Ok' => substr($response, 2, 1), @@ -693,10 +931,10 @@ public function parseHoldResponse($response) ]; //expiration date is optional an indicated by BW - $variableOffset=22; + $variableOffset = 22; if (substr($response, 22, 2) === 'BW') { $result['fixed']['ExpirationDate'] = substr($response, 24, 18); - $variableOffset=42; + $variableOffset = 42; } $result['variable'] = $this->parseVariableData($response, $variableOffset); @@ -705,6 +943,13 @@ public function parseHoldResponse($response) } + /** + * Parse response from a Renew request + * @param string $response ACS response + * @return array with 'fixed' and 'variable' keys + * + * @see SIP2Client::msgRenew() + */ public function parseRenewResponse($response) { /* Response Example: @@ -712,6 +957,7 @@ public function parseRenewResponse($response) AJFolksongs of Britain and Ireland|AH5/23/2008,23:59|CH| AFOverride required to exceed renewal limit.|AY1AZCDA5 */ + $result = []; $result['fixed'] = [ 'Ok' => substr($response, 2, 1), @@ -727,8 +973,16 @@ public function parseRenewResponse($response) return $result; } + /** + * Parse response from a Renew All request + * @param string $response ACS response + * @return array with 'fixed' and 'variable' keys + * + * @see SIP2Client::msgRenewAll() + */ public function parseRenewAllResponse($response) { + $result = []; $result['fixed'] = [ 'Ok' => substr($response, 2, 1), @@ -743,7 +997,12 @@ public function parseRenewAllResponse($response) return $result; } - + /** + * Send a request to ACS and obtain raw response + * + * @param string $message generated by one of the msg*() methods + * @return bool|string false in event of failure, otherwise a string containing ACS response + */ public function getMessage($message) { /* sends the current message, and gets the response */ @@ -791,9 +1050,12 @@ public function getMessage($message) return $result; } + /** + * Connect to ACS via SIP2 + * @return bool returns true if connection is established + */ public function connect() { - /* Socket Communications */ $this->logger->debug("SIP2: --- BEGIN SIP communication ---"); $address = $this->hostname . ':' . $this->port; @@ -809,7 +1071,7 @@ public function connect() } catch (\Exception $e) { $this->socket->close(); $this->socket = null; - $this->logger->error("SIP2Client: Failed to connect: ".$e->getMessage()); + $this->logger->error("SIP2Client: Failed to connect: " . $e->getMessage()); return false; } @@ -817,7 +1079,9 @@ public function connect() return true; } - + /** + * Disconnect from ACS + */ public function disconnect() { $this->socket->close(); From c0c6c6f786ead1b0bd5bf8a57cf8719f755e6a0d Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sun, 22 Jul 2018 18:31:32 +0100 Subject: [PATCH 16/35] Ensure composer and CI only target php7+ --- .travis.yml | 7 ------- composer.json | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index e3731a8..8c087ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,9 @@ dist: trusty language: php php: - - 5.6 - 7.0 - 7.1 - 7.2 - - hhvm # This triggers builds to run on the new TravisCI infrastructure. # See: http://docs.travis-ci.com/user/workers/container-based-infrastructure/ @@ -17,11 +15,6 @@ cache: directories: - $HOME/.composer/cache -matrix: - include: - - php: 5.6 - env: 'COMPOSER_FLAGS="--prefer-stable --prefer-lowest"' - before_script: - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-dist diff --git a/composer.json b/composer.json index 70b66aa..118bc43 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ } ], "require": { - "php": "~5.6|~7.0", + "php": "~7.0", "clue/socket-raw": "^1.3", "psr/log": "^1.0" }, From dda41b6d5eb094e64acf07269f84a5604f1a9386 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sun, 22 Jul 2018 18:40:19 +0100 Subject: [PATCH 17/35] Only send one code coverage run to scrutinizer --- .scrutinizer.yml | 2 +- .travis.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.scrutinizer.yml b/.scrutinizer.yml index aea6609..e2ef079 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -20,4 +20,4 @@ checks: tools: external_code_coverage: timeout: 600 - runs: 3 + runs: 1 diff --git a/.travis.yml b/.travis.yml index 8c087ff..50f9347 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ script: after_script: - | - if [[ "$TRAVIS_PHP_VERSION" != 'hhvm' && "$TRAVIS_PHP_VERSION" != '7.0' ]]; then + if [[ "$TRAVIS_PHP_VERSION" == '7.0' ]]; then wget https://scrutinizer-ci.com/ocular.phar php ocular.phar code-coverage:upload --format=php-clover coverage.clover fi From 4dcd04dee8f065a6fda9e3e02f4686f377cd8c83 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sun, 22 Jul 2018 20:43:11 +0100 Subject: [PATCH 18/35] Added experimental outline of broken out request class --- src/AbstractSIP2Request.php | 121 ++++++++++++++++++++++++++++++++++++ src/CheckoutRequest.php | 51 +++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 src/AbstractSIP2Request.php create mode 100644 src/CheckoutRequest.php diff --git a/src/AbstractSIP2Request.php b/src/AbstractSIP2Request.php new file mode 100644 index 0000000..c69b271 --- /dev/null +++ b/src/AbstractSIP2Request.php @@ -0,0 +1,121 @@ +noFixed = false; + $this->msgBuild = $code; + } + + protected function addFixedOption($value, $len) + { + /* adds a fixed length option to the msgBuild IF no variable options have been added. */ + if ($this->noFixed) { + //@codeCoverageIgnoreStart + throw new \LogicException('Cannot add fixed options after variable options'); + //@codeCoverageIgnoreEnd + } + + $this->msgBuild .= sprintf("%{$len}s", substr($value, 0, $len)); + return true; + } + + protected function addVarOption($field, $value, $optional = false) + { + /* adds a variable length option to the message, and also prevents adding additional fixed fields */ + if ($optional == true && $value == '') { + /* skipped */ + //$this->logger->debug("SIP2: Skipping optional field {$field}"); + } else { + $this->noFixed = true; /* no more fixed for this message */ + $this->msgBuild .= $field . substr($value, 0, 255) . $this->fldTerminator; + } + return true; + } + + protected function returnMessage($withSeq = true, $withCrc = true) + { + /* Finalizes the message and returns it. Message will remain in msgBuild until newMessage is called */ + if ($withSeq) { + $this->msgBuild .= 'AY' . self::getSeqNumber(); + } + if ($withCrc) { + $this->msgBuild .= 'AZ'; + $this->msgBuild .= $this->crc($this->msgBuild); + } + $this->msgBuild .= $this->msgTerminator; + + return $this->msgBuild; + } + + /* Core local utility functions */ + protected function datestamp($timestamp = '') + { + /* generate a SIP2 compatible datestamp */ + /* From the spec: + * YYYYMMDDZZZZHHMMSS. + * All dates and times are expressed according to the ANSI standard X3.30 for date and X3.43 for time. + * The ZZZZ field should contain blanks (code $20) to represent local time. To represent universal time, + * a Z character(code $5A) should be put in the last (right hand) position of the ZZZZ field. + * To represent other time zones the appropriate character should be used; a Q character (code $51) + * should be put in the last (right hand) position of the ZZZZ field to represent Atlantic Standard Time. + * When possible local time is the preferred format. + */ + if ($timestamp != '') { + /* Generate a proper date time from the date provided */ + return date('Ymd His', $timestamp); + } else { + /* Current Date/Time */ + return date('Ymd His'); + } + } + + protected function crc($buf) + { + /* Calculate CRC */ + $sum = 0; + + $len = strlen($buf); + for ($n = 0; $n < $len; $n++) { + $sum = $sum + ord(substr($buf, $n, 1)); + } + + $crc = ($sum & 0xFFFF) * -1; + + /* 2008.03.15 - Fixed a bug that allowed the checksum to be larger then 4 digits */ + return substr(sprintf("%4X", $crc), -4, 4); + } + + protected static function getSeqNumber() + { + /* Get a sequence number for the AY field */ + /* valid numbers range 0-9 */ + self::$seq++; + if (self::$seq > 9) { + self::$seq = 0; + } + return self::$seq; + } +} \ No newline at end of file diff --git a/src/CheckoutRequest.php b/src/CheckoutRequest.php new file mode 100644 index 0000000..cee32ab --- /dev/null +++ b/src/CheckoutRequest.php @@ -0,0 +1,51 @@ +newMessage('11'); + $this->addFixedOption($this->scRenewal, 1); + $this->addFixedOption($this->noBlock, 1); + $this->addFixedOption($this->datestamp(), 18); + if ($this->nbDateDue != '') { + /* override default date due */ + $this->addFixedOption($this->datestamp($this->nbDateDue), 18); + } else { + /* send a blank date due to allow ACS to use default date due computed for item */ + $this->addFixedOption('', 18); + } + $this->addVarOption('AO', $this->institutionId); + $this->addVarOption('AA', $this->patron); + $this->addVarOption('AB', $this->itemIdentifer); + $this->addVarOption('AC', $this->terminalPassword); + $this->addVarOption('CH', $this->itemProperties, true); + $this->addVarOption('AD', $this->patronpwd, true); + $this->addVarOption('BO', $this->fee, true); /* Y or N */ + $this->addVarOption('BI', $this->cancel, true); /* Y or N */ + + return $this->returnMessage(); + } +} \ No newline at end of file From 2e83c232e925eee6f172af8db3a425c3f48896e8 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sun, 22 Jul 2018 20:43:53 +0100 Subject: [PATCH 19/35] Flesh out changelog and readme for v2 --- CHANGELOG.md | 8 ++++-- README.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b04c0c1..27c3de2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,15 +8,19 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - MIT License adopted - prior releases were GPL +- PSR-2 formatting/naming conventions, including change of classname from sip2 to SIP2Client +- Support for binding to particular interface +- Full unit tests ### Deprecated - Nothing ### Fixed -- Nothing +- Ensure parseHoldResponse copes with optional elements +- Ensure getMessage properly handles retries ### Removed -- Nothing +- debug flag, replaced with PSR-3 logger support ### Security - Nothing diff --git a/README.md b/README.md index 8536589..0f953a1 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,15 @@ PHP client library to facilitate communication with Integrated Library System (ILS) servers via 3M's SIP2. -## ToDo -- abstract socket into separate class https://github.com/clue/php-socket-raw -- make socket factory which can be given to SIP2Client -- now can make unit tests which simulate socket connections +This is derived from [cap60552/php-sip2](https://github.com/cap60552/php-sip2) by John Wohlers, +with following improvements: +* Full unit tests +* PSR-2 formatting conventions +* PSR-3 logging +* PSR-4 auto-loading +* Ability to bind to specific interface when making requests +* MIT license (with consent of original author who used GPL) ## Install @@ -24,11 +28,32 @@ Via Composer $ composer require lordelph/php-sip2 ``` +## Migration from v1.0 + +If you want to switch to using this class from [cap60552/php-sip2](https://github.com/cap60552/php-sip2), +you should only need to change instantations of `sip2` to `SIP2Client` and ensure you include the class with +`use lordelph\SIP2\SIP2Client` + +```php +#before +$mysip = new sip2; + +#after +use lordelph\SIP2\SIP2Client; + +$mysip = new SIP2Client; + + +``` + ## Usage ``` php +use lordelph\SIP2\SIP2Client; + + // create object -$mysip = new lordelph\SIP2\SIP2Client; +$mysip = new SIP2Client; // Set host name $mysip->hostname = 'server.example.com'; @@ -41,13 +66,44 @@ $mysip->patronpwd = '010101'; // connect to SIP server $result = $mysip->connect(); -// Get Charged Items Raw response -$in = $mysip->msgPatronInformation('charged'); +// build a request for patron information +$request = $mysip->msgPatronInformation('charged'); + +// send that request and obtain a raw response +$response = $mysip->getMessage($request) // parse the raw response into an array -$result = $mysip->parsePatronInfoResponse( $mysip->getMessage($in) ); +$result = $mysip->parsePatronInfoResponse($response); +``` + +## Binding to a specific local outbound address + +If connecting to a SIP2 service over the internet, such services will usually be tightly firewalled +to specific IPs. If your client software is running on a machine with multiple outbound interfaces, +you may wish to pick the specific interface so that the SIP2 server sees the correct IP. + +To do this, specify the IP with `bindTo` public member variable *before* calling `connect()`: + + +``` php +use lordelph\SIP2\SIP2Client; + + +// create object +$mysip = new SIP2Client; + +// Set host name +$mysip->hostname = 'server.example.com'; +$mysip->port = 6002; + +//ensure outbound connections go from this IP address +$mysip->bindTo = '1.2.3.4'; + +// connect to SIP server +$result = $mysip->connect(); ``` + ## Change log Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. From 2cc0134e2c369770e37ed15355a6b772f2799ec3 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Mon, 23 Jul 2018 08:40:13 +0100 Subject: [PATCH 20/35] Expand migration section --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0f953a1..72f806c 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ $ composer require lordelph/php-sip2 ## Migration from v1.0 If you want to switch to using this class from [cap60552/php-sip2](https://github.com/cap60552/php-sip2), -you should only need to change instantations of `sip2` to `SIP2Client` and ensure you include the class with +you need to change instantations of `sip2` to `SIP2Client` and ensure you include the class with `use lordelph\SIP2\SIP2Client` ```php @@ -42,10 +42,10 @@ $mysip = new sip2; use lordelph\SIP2\SIP2Client; $mysip = new SIP2Client; - - ``` +Also, the `get_message` method is now `getMessage` + ## Usage ``` php From dc8038dc7bf6c2bd65134b5c58302af4fba06ed1 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Thu, 26 Jul 2018 12:10:08 +0100 Subject: [PATCH 21/35] Refactor parsing/request building into separate classes --- src/CheckoutRequest.php | 51 - src/Exception/LogicException.php | 10 + src/Exception/RuntimeException.php | 11 + src/Exception/SIP2ClientException.php | 12 + src/Request/BlockPatronRequest.php | 39 + src/Request/CheckInRequest.php | 49 + src/Request/CheckOutRequest.php | 59 + src/Request/EndPatronSessionRequest.php | 35 + src/Request/FeePaidRequest.php | 72 ++ src/Request/HoldRequest.php | 77 ++ src/Request/ItemInformationRequest.php | 31 + src/Request/ItemStatusUpdateRequest.php | 35 + src/Request/LoginRequest.php | 39 + src/Request/PatronEnableRequest.php | 34 + src/Request/PatronInformationRequest.php | 66 + src/Request/PatronStatusRequest.php | 37 + src/Request/RenewAllRequest.php | 36 + src/Request/RenewRequest.php | 57 + src/Request/RequestACSResendRequest.php | 18 + src/Request/SCStatusRequest.php | 35 + .../SIP2Request.php} | 76 +- src/Response/ACSStatusResponse.php | 68 ++ src/Response/CheckInResponse.php | 61 + src/Response/CheckOutResponse.php | 69 ++ src/Response/EndSessionResponse.php | 39 + src/Response/FeePaidResponse.php | 41 + src/Response/HoldResponse.php | 70 ++ src/Response/ItemInformationResponse.php | 68 ++ src/Response/ItemStatusUpdateResponse.php | 41 + src/Response/LoginResponse.php | 20 + src/Response/PatronEnableResponse.php | 49 + src/Response/PatronInformationResponse.php | 96 ++ src/Response/PatronStatusResponse.php | 53 + src/Response/RenewAllResponse.php | 47 + src/Response/RenewResponse.php | 69 ++ src/Response/SIP2Response.php | 202 ++++ src/SIP2Client.php | 1070 +---------------- src/SIP2Message.php | 158 +++ tests/ACSResendTest.php | 24 - tests/AbstractSIP2ClientTest.php | 6 + tests/BindingTest.php | 44 - tests/BlockPatronTest.php | 41 - tests/CRCFailureTest.php | 54 - tests/CRCTest.php | 39 - tests/CheckinTest.php | 97 -- tests/CheckoutTest.php | 107 -- tests/ConnectionFailureTest.php | 44 - tests/EndPatronSessionTest.php | 41 - tests/FeePaidTest.php | 53 - tests/HoldTest.php | 52 - tests/ItemInformationTest.php | 75 -- tests/ItemStatusTest.php | 41 - tests/LoginTest.php | 28 - tests/PatronEnableTest.php | 47 - tests/PatronInfoTest.php | 58 - tests/PatronStatusTest.php | 51 - tests/RenewAllTest.php | 50 - tests/RenewTest.php | 77 -- tests/Request/BlockPatronRequestTest.php | 22 + tests/Request/CheckInRequestTest.php | 26 + tests/Request/CheckOutRequestTest.php | 27 + tests/Request/EndPatronSessionRequestTest.php | 21 + tests/Request/FeePaidRequestTest.php | 26 + tests/Request/HoldRequestTest.php | 24 + tests/Request/ItemInformationRequestTest.php | 22 + tests/Request/ItemStatusUpdateRequestTest.php | 23 + tests/Request/LoginRequestTest.php | 21 + tests/Request/PatronEnableRequestTest.php | 23 + .../Request/PatronInformationRequestTest.php | 27 + tests/Request/PatronStatusRequestTest.php | 22 + tests/Request/RenewAllRequestTest.php | 22 + tests/Request/RenewRequestTest.php | 26 + tests/Request/RequestACSResendRequestTest.php | 15 + tests/Request/SCStatusRequestTest.php | 15 + tests/Request/SIP2RequestTest.php | 52 + tests/Response/ACSStatusResponseTest.php | 41 + tests/Response/CheckInResponseTest.php | 48 + tests/Response/CheckOutResponseTest.php | 56 + tests/Response/EndSessionResponseTest.php | 37 + tests/Response/FeePaidResponseTest.php | 33 + tests/Response/HoldResponseTest.php | 33 + .../Response/ItemInformationResponseTest.php | 57 + .../Response/ItemStatusUpdateResponseTest.php | 33 + tests/Response/LoginResponseTest.php | 19 + tests/Response/PatronEnableResponseTest.php | 39 + .../PatronInformationResponseTest.php | 44 + tests/Response/PatronStatusResponseTest.php | 43 + tests/Response/RenewAllResponseTest.php | 48 + tests/Response/RenewResponseTest.php | 56 + tests/Response/SIP2ResponseTest.php | 87 ++ tests/SCStatusTest.php | 49 - tests/SIP2ClientTest.php | 171 +++ tests/SequencingTest.php | 28 - 93 files changed, 3244 insertions(+), 2221 deletions(-) delete mode 100644 src/CheckoutRequest.php create mode 100644 src/Exception/LogicException.php create mode 100644 src/Exception/RuntimeException.php create mode 100644 src/Exception/SIP2ClientException.php create mode 100644 src/Request/BlockPatronRequest.php create mode 100644 src/Request/CheckInRequest.php create mode 100644 src/Request/CheckOutRequest.php create mode 100644 src/Request/EndPatronSessionRequest.php create mode 100644 src/Request/FeePaidRequest.php create mode 100644 src/Request/HoldRequest.php create mode 100644 src/Request/ItemInformationRequest.php create mode 100644 src/Request/ItemStatusUpdateRequest.php create mode 100644 src/Request/LoginRequest.php create mode 100644 src/Request/PatronEnableRequest.php create mode 100644 src/Request/PatronInformationRequest.php create mode 100644 src/Request/PatronStatusRequest.php create mode 100644 src/Request/RenewAllRequest.php create mode 100644 src/Request/RenewRequest.php create mode 100644 src/Request/RequestACSResendRequest.php create mode 100644 src/Request/SCStatusRequest.php rename src/{AbstractSIP2Request.php => Request/SIP2Request.php} (61%) create mode 100644 src/Response/ACSStatusResponse.php create mode 100644 src/Response/CheckInResponse.php create mode 100644 src/Response/CheckOutResponse.php create mode 100644 src/Response/EndSessionResponse.php create mode 100644 src/Response/FeePaidResponse.php create mode 100644 src/Response/HoldResponse.php create mode 100644 src/Response/ItemInformationResponse.php create mode 100644 src/Response/ItemStatusUpdateResponse.php create mode 100644 src/Response/LoginResponse.php create mode 100644 src/Response/PatronEnableResponse.php create mode 100644 src/Response/PatronInformationResponse.php create mode 100644 src/Response/PatronStatusResponse.php create mode 100644 src/Response/RenewAllResponse.php create mode 100644 src/Response/RenewResponse.php create mode 100644 src/Response/SIP2Response.php create mode 100644 src/SIP2Message.php delete mode 100644 tests/ACSResendTest.php delete mode 100644 tests/BindingTest.php delete mode 100644 tests/BlockPatronTest.php delete mode 100644 tests/CRCFailureTest.php delete mode 100644 tests/CRCTest.php delete mode 100644 tests/CheckinTest.php delete mode 100644 tests/CheckoutTest.php delete mode 100644 tests/ConnectionFailureTest.php delete mode 100644 tests/EndPatronSessionTest.php delete mode 100644 tests/FeePaidTest.php delete mode 100644 tests/HoldTest.php delete mode 100644 tests/ItemInformationTest.php delete mode 100644 tests/ItemStatusTest.php delete mode 100644 tests/LoginTest.php delete mode 100644 tests/PatronEnableTest.php delete mode 100644 tests/PatronInfoTest.php delete mode 100644 tests/PatronStatusTest.php delete mode 100644 tests/RenewAllTest.php delete mode 100644 tests/RenewTest.php create mode 100644 tests/Request/BlockPatronRequestTest.php create mode 100644 tests/Request/CheckInRequestTest.php create mode 100644 tests/Request/CheckOutRequestTest.php create mode 100644 tests/Request/EndPatronSessionRequestTest.php create mode 100644 tests/Request/FeePaidRequestTest.php create mode 100644 tests/Request/HoldRequestTest.php create mode 100644 tests/Request/ItemInformationRequestTest.php create mode 100644 tests/Request/ItemStatusUpdateRequestTest.php create mode 100644 tests/Request/LoginRequestTest.php create mode 100644 tests/Request/PatronEnableRequestTest.php create mode 100644 tests/Request/PatronInformationRequestTest.php create mode 100644 tests/Request/PatronStatusRequestTest.php create mode 100644 tests/Request/RenewAllRequestTest.php create mode 100644 tests/Request/RenewRequestTest.php create mode 100644 tests/Request/RequestACSResendRequestTest.php create mode 100644 tests/Request/SCStatusRequestTest.php create mode 100644 tests/Request/SIP2RequestTest.php create mode 100644 tests/Response/ACSStatusResponseTest.php create mode 100644 tests/Response/CheckInResponseTest.php create mode 100644 tests/Response/CheckOutResponseTest.php create mode 100644 tests/Response/EndSessionResponseTest.php create mode 100644 tests/Response/FeePaidResponseTest.php create mode 100644 tests/Response/HoldResponseTest.php create mode 100644 tests/Response/ItemInformationResponseTest.php create mode 100644 tests/Response/ItemStatusUpdateResponseTest.php create mode 100644 tests/Response/LoginResponseTest.php create mode 100644 tests/Response/PatronEnableResponseTest.php create mode 100644 tests/Response/PatronInformationResponseTest.php create mode 100644 tests/Response/PatronStatusResponseTest.php create mode 100644 tests/Response/RenewAllResponseTest.php create mode 100644 tests/Response/RenewResponseTest.php create mode 100644 tests/Response/SIP2ResponseTest.php delete mode 100644 tests/SCStatusTest.php create mode 100644 tests/SIP2ClientTest.php delete mode 100644 tests/SequencingTest.php diff --git a/src/CheckoutRequest.php b/src/CheckoutRequest.php deleted file mode 100644 index cee32ab..0000000 --- a/src/CheckoutRequest.php +++ /dev/null @@ -1,51 +0,0 @@ -newMessage('11'); - $this->addFixedOption($this->scRenewal, 1); - $this->addFixedOption($this->noBlock, 1); - $this->addFixedOption($this->datestamp(), 18); - if ($this->nbDateDue != '') { - /* override default date due */ - $this->addFixedOption($this->datestamp($this->nbDateDue), 18); - } else { - /* send a blank date due to allow ACS to use default date due computed for item */ - $this->addFixedOption('', 18); - } - $this->addVarOption('AO', $this->institutionId); - $this->addVarOption('AA', $this->patron); - $this->addVarOption('AB', $this->itemIdentifer); - $this->addVarOption('AC', $this->terminalPassword); - $this->addVarOption('CH', $this->itemProperties, true); - $this->addVarOption('AD', $this->patronpwd, true); - $this->addVarOption('BO', $this->fee, true); /* Y or N */ - $this->addVarOption('BI', $this->cancel, true); /* Y or N */ - - return $this->returnMessage(); - } -} \ No newline at end of file diff --git a/src/Exception/LogicException.php b/src/Exception/LogicException.php new file mode 100644 index 0000000..5224619 --- /dev/null +++ b/src/Exception/LogicException.php @@ -0,0 +1,10 @@ + ['type' => 'YN', 'default' => 'N'], + 'InstitutionId' => [], + 'Message' => ['default' => ''], + 'PatronIdentifier' => [], + 'TerminalPassword' => ['default' => ''], + ]; + + public function getMessageString($withSeq = true, $withCrc = true): string + { + $this->newMessage('01'); + $this->addFixedOption($this->getVariable('CardRetained'), 1); + $this->addFixedOption($this->datestamp(), 18); + $this->addVarOption('AO', $this->getVariable('InstitutionId')); + $this->addVarOption('AL', $this->getVariable('Message')); + $this->addVarOption('AA', $this->getVariable('PatronIdentifier')); + $this->addVarOption('AC', $this->getVariable('TerminalPassword')); + + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/Request/CheckInRequest.php b/src/Request/CheckInRequest.php new file mode 100644 index 0000000..4e53595 --- /dev/null +++ b/src/Request/CheckInRequest.php @@ -0,0 +1,49 @@ + ['type' => 'YN', 'default' => 'N'], + 'ItemReturnDate' => ['type' => 'timestamp', 'default' => ''], + 'ItemLocation' => ['default' => ''], + 'InstitutionId' => [], + 'ItemIdentifier' => [], + 'TerminalPassword' => ['default' => ''], + 'ItemProperties' => ['default' => ''], + 'Cancel' => ['type' => 'YN', 'default' => 'N'], + ]; + + public function getMessageString($withSeq = true, $withCrc = true): string + { + $this->newMessage('09'); + $this->addFixedOption($this->getVariable('NoBlock'), 1); + $this->addFixedOption($this->datestamp(), 18); + $this->addFixedOption($this->getVariable('ItemReturnDate'), 18); + $this->addVarOption('AP', $this->getVariable('ItemLocation')); + $this->addVarOption('AO', $this->getVariable('InstitutionId')); + $this->addVarOption('AB', $this->getVariable('ItemIdentifier')); + $this->addVarOption('AC', $this->getVariable('TerminalPassword')); + $this->addVarOption('CH', $this->getVariable('ItemProperties'), true); + $this->addVarOption('BI', $this->getVariable('Cancel'), true); + + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/Request/CheckOutRequest.php b/src/Request/CheckOutRequest.php new file mode 100644 index 0000000..1a483b8 --- /dev/null +++ b/src/Request/CheckOutRequest.php @@ -0,0 +1,59 @@ + ['type' => 'YUN', 'default' => 'N'], + 'NoBlock' => ['type' => 'YN', 'default' => 'N'], + 'NBDateDue' => ['type' => 'timestamp', 'default' => ''], + 'InstitutionId' => [], + 'PatronIdentifier' => [], + 'ItemIdentifier' => [], + 'TerminalPassword' => ['default' => ''], + 'ItemProperties' => ['default' => ''], + 'PatronPassword' => ['default' => ''], + 'FeeAcknowledged' => ['type' => 'YN', 'default' => 'N'], + 'Cancel' => ['type' => 'YN', 'default' => 'N'], + ]; + + public function getMessageString($withSeq = true, $withCrc = true): string + { + $this->newMessage('11'); + $this->addFixedOption($this->getVariable('SCRenewal'), 1); + $this->addFixedOption($this->getVariable('NoBlock'), 1); + $this->addFixedOption($this->datestamp(), 18); + $this->addFixedOption($this->getVariable('NBDateDue'), 18); + + $this->addVarOption('AO', $this->getVariable('InstitutionId')); + $this->addVarOption('AA', $this->getVariable('PatronIdentifier')); + $this->addVarOption('AB', $this->getVariable('ItemIdentifier')); + $this->addVarOption('AC', $this->getVariable('TerminalPassword')); + $this->addVarOption('CH', $this->getVariable('ItemProperties'), true); + $this->addVarOption('AD', $this->getVariable('PatronPassword'), true); + $this->addVarOption('BO', $this->getVariable('FeeAcknowledged'), true); + $this->addVarOption('BI', $this->getVariable('Cancel'), true); + + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/Request/EndPatronSessionRequest.php b/src/Request/EndPatronSessionRequest.php new file mode 100644 index 0000000..0752fd0 --- /dev/null +++ b/src/Request/EndPatronSessionRequest.php @@ -0,0 +1,35 @@ + [], + 'PatronIdentifier' => [], + 'TerminalPassword' => ['default' => ''], + 'PatronPassword' => ['default' => ''], + ]; + + public function getMessageString($withSeq = true, $withCrc = true): string + { + $this->newMessage('35'); + $this->addFixedOption($this->datestamp(), 18); + $this->addVarOption('AO', $this->getVariable('InstitutionId')); + $this->addVarOption('AA', $this->getVariable('PatronIdentifier')); + $this->addVarOption('AC', $this->getVariable('TerminalPassword'), true); + $this->addVarOption('AD', $this->getVariable('PatronPassword'), true); + + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/Request/FeePaidRequest.php b/src/Request/FeePaidRequest.php new file mode 100644 index 0000000..19b3bcd --- /dev/null +++ b/src/Request/FeePaidRequest.php @@ -0,0 +1,72 @@ + ['type' => 'nn'], + 'PaymentType' => ['type' => 'nn'], + 'CurrencyType' => ['default' => 'USD'], + 'PaymentAmount' => [], + 'InstitutionId' => [], + 'PatronIdentifier' => [], + 'TerminalPassword' => ['default' => ''], + 'PatronPassword' => ['default' => ''], + 'FeeIdentifier' => ['default' => ''], + 'TransactionIdentifier' => ['default' => ''], + ]; + + public function getMessageString($withSeq = true, $withCrc = true): string + { + $this->newMessage('37'); + $this->addFixedOption($this->datestamp(), 18); + $this->addFixedOption(sprintf('%02d', $this->getVariable('FeeType')), 2); + $this->addFixedOption(sprintf('%02d', $this->getVariable('PaymentType')), 2); + $this->addFixedOption($this->getVariable('CurrencyType'), 3); + + // due to currency format localization, it is up to the programmer + // to properly format their payment amount + $this->addVarOption('BV', $this->getVariable('PaymentAmount')); + $this->addVarOption('AO', $this->getVariable('InstitutionId')); + $this->addVarOption('AA', $this->getVariable('PatronIdentifier')); + $this->addVarOption('AC', $this->getVariable('TerminalPassword')); + $this->addVarOption('AD', $this->getVariable('PatronPassword'), true); + $this->addVarOption('CG', $this->getVariable('FeeIdentifier'), true); + $this->addVarOption('BK', $this->getVariable('TransactionIdentifier'), true); + + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/Request/HoldRequest.php b/src/Request/HoldRequest.php new file mode 100644 index 0000000..58b5ab1 --- /dev/null +++ b/src/Request/HoldRequest.php @@ -0,0 +1,77 @@ + ['type' => 'regex/[\-\+\*]/'], + 'ExpiryDate' => ['type' => 'timestamp', 'default' => ''], + 'PickupLocation' => ['default' => ''], + 'HoldType' => ['type' => 'n', 'default' => ''], + 'InstitutionId' => [], + 'PatronIdentifier' => [], + 'PatronPassword' => ['default' => ''], + 'ItemIdentifier' => ['default' => ''], + 'ItemTitle' => ['default' => ''], + 'TerminalPassword' => ['default' => ''], + 'FeeAcknowledged' => ['type' => 'YN', 'default' => 'N'], + ]; + + public function getMessageString($withSeq = true, $withCrc = true): string + { + $this->newMessage('15'); + $this->addFixedOption($this->getVariable('HoldMode'), 1); + $this->addFixedOption($this->datestamp(), 18); + + $this->addVarOption('BW', $this->getVariable('ExpiryDate'), true); + $this->addVarOption('BS', $this->getVariable('PickupLocation'), true); + $this->addVarOption('BY', $this->getVariable('HoldType'), true); + $this->addVarOption('AO', $this->getVariable('InstitutionId')); + $this->addVarOption('AA', $this->getVariable('PatronIdentifier')); + $this->addVarOption('AD', $this->getVariable('PatronPassword'), true); + $this->addVarOption('AB', $this->getVariable('ItemIdentifier'), true); + $this->addVarOption('AJ', $this->getVariable('ItemTitle'), true); + $this->addVarOption('AC', $this->getVariable('TerminalPassword'), true); + $this->addVarOption('BO', $this->getVariable('FeeAcknowledged'), true); + + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/Request/ItemInformationRequest.php b/src/Request/ItemInformationRequest.php new file mode 100644 index 0000000..ff6236b --- /dev/null +++ b/src/Request/ItemInformationRequest.php @@ -0,0 +1,31 @@ + [], + 'ItemIdentifier' => [], + 'TerminalPassword' => ['default' => ''], + ]; + + public function getMessageString($withSeq = true, $withCrc = true): string + { + $this->newMessage('17'); + $this->addFixedOption($this->datestamp(), 18); + $this->addVarOption('AO', $this->getVariable('InstitutionId')); + $this->addVarOption('AB', $this->getVariable('ItemIdentifier')); + $this->addVarOption('AC', $this->getVariable('TerminalPassword')); + + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/Request/ItemStatusUpdateRequest.php b/src/Request/ItemStatusUpdateRequest.php new file mode 100644 index 0000000..98e98d8 --- /dev/null +++ b/src/Request/ItemStatusUpdateRequest.php @@ -0,0 +1,35 @@ + [], + 'ItemIdentifier' => [], + 'TerminalPassword' => ['default' => ''], + 'ItemProperties' => ['default' => ''], + ]; + + public function getMessageString($withSeq = true, $withCrc = true): string + { + $this->newMessage('19'); + $this->addFixedOption($this->datestamp(), 18); + $this->addVarOption('AO', $this->getVariable('InstitutionId')); + $this->addVarOption('AB', $this->getVariable('ItemIdentifier')); + $this->addVarOption('AC', $this->getVariable('TerminalPassword')); + $this->addVarOption('CH', $this->getVariable('ItemProperties'), true); + + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/Request/LoginRequest.php b/src/Request/LoginRequest.php new file mode 100644 index 0000000..63e9194 --- /dev/null +++ b/src/Request/LoginRequest.php @@ -0,0 +1,39 @@ + ['type' => 'n', 'default' => '0'], + 'PasswordAlgorithm' => ['type' => 'n', 'default' => '0'], + 'SIPLogin' => [], + 'SIPPassword' => [], + 'Location' => ['default' => ''] + ]; + + public function getMessageString($withSeq = true, $withCrc = true): string + { + $this->newMessage('93'); + $this->addFixedOption($this->getVariable('UserIdAlgorithm'), 1); + $this->addFixedOption($this->getVariable('PasswordAlgorithm'), 1); + $this->addVarOption('CN', $this->getVariable('SIPLogin')); + $this->addVarOption('CO', $this->getVariable('SIPPassword')); + $this->addVarOption('CP', $this->getVariable('Location'), true); + + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/Request/PatronEnableRequest.php b/src/Request/PatronEnableRequest.php new file mode 100644 index 0000000..ed00e6c --- /dev/null +++ b/src/Request/PatronEnableRequest.php @@ -0,0 +1,34 @@ + [], + 'PatronIdentifier' => [], + 'TerminalPassword' => ['default' => ''], + 'PatronPassword' => ['default' => ''], + ]; + + public function getMessageString($withSeq = true, $withCrc = true): string + { + $this->newMessage('25'); + $this->addFixedOption($this->datestamp(), 18); + $this->addVarOption('AO', $this->getVariable('InstitutionId')); + $this->addVarOption('AA', $this->getVariable('PatronIdentifier')); + $this->addVarOption('AC', $this->getVariable('TerminalPassword'), true); + $this->addVarOption('AD', $this->getVariable('PatronPassword'), true); + + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/Request/PatronInformationRequest.php b/src/Request/PatronInformationRequest.php new file mode 100644 index 0000000..017b1e6 --- /dev/null +++ b/src/Request/PatronInformationRequest.php @@ -0,0 +1,66 @@ + ['type' => 'nnn', 'default' => '001'], + 'Type' => ['default' => 'none'], + 'InstitutionId' => [], + 'PatronIdentifier' => [], + 'TerminalPassword' => ['default' => ''], + 'PatronPassword' => ['default' => ''], + 'Start' => ['default' => '1'], + 'End' => ['default' => '5'], + ]; + + public function getMessageString($withSeq = true, $withCrc = true): string + { + /* + * According to the specification: + * Only one category of items should be requested at a time, i.e. it would take 6 of these messages, + * each with a different position set to Y, to get all the detailed information about a patron's items. + */ + $summary = []; + $summary['none'] = ' '; + $summary['hold'] = 'Y '; + $summary['overdue'] = ' Y '; + $summary['charged'] = ' Y '; + $summary['fine'] = ' Y '; + $summary['recall'] = ' Y '; + $summary['unavail'] = ' Y'; + + $type = $this->getVariable('Type'); + + $this->newMessage('63'); + $this->addFixedOption($this->getVariable('Language'), 3); + $this->addFixedOption($this->datestamp(), 18); + $this->addFixedOption(sprintf("%-10s", $summary[$type]), 10); + $this->addVarOption('AO', $this->getVariable('InstitutionId')); + $this->addVarOption('AA', $this->getVariable('PatronIdentifier')); + $this->addVarOption('AC', $this->getVariable('TerminalPassword')); + $this->addVarOption('AD', $this->getVariable('PatronPassword'), true); + /* old function version used padded 5 digits, not sure why */ + $this->addVarOption('BP', $this->getVariable('Start'), true); + /* old function version used padded 5 digits, not sure why */ + $this->addVarOption('BQ', $this->getVariable('End'), true); + + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/Request/PatronStatusRequest.php b/src/Request/PatronStatusRequest.php new file mode 100644 index 0000000..8a6c403 --- /dev/null +++ b/src/Request/PatronStatusRequest.php @@ -0,0 +1,37 @@ + ['type' => 'nnn', 'default' => '001'], + 'InstitutionId' => [], + 'PatronIdentifier' => [], + 'TerminalPassword' => ['default' => ''], + 'PatronPassword' => ['default' => ''], + ]; + + public function getMessageString($withSeq = true, $withCrc = true): string + { + $this->newMessage('23'); + $this->addFixedOption($this->getVariable('Language'), 3); + $this->addFixedOption($this->datestamp(), 18); + $this->addVarOption('AO', $this->getVariable('InstitutionId')); + $this->addVarOption('AA', $this->getVariable('PatronIdentifier')); + $this->addVarOption('AC', $this->getVariable('TerminalPassword')); + $this->addVarOption('AD', $this->getVariable('PatronPassword'), true); + + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/Request/RenewAllRequest.php b/src/Request/RenewAllRequest.php new file mode 100644 index 0000000..9bad9fa --- /dev/null +++ b/src/Request/RenewAllRequest.php @@ -0,0 +1,36 @@ + [], + 'PatronIdentifier' => [], + 'PatronPassword' => ['default' => ''], + 'TerminalPassword' => ['default' => ''], + 'FeeAcknowledged' => ['type' => 'YN', 'default' => 'N'], + ]; + + public function getMessageString($withSeq = true, $withCrc = true): string + { + $this->newMessage('65'); + $this->addVarOption('AO', $this->getVariable('InstitutionId')); + $this->addVarOption('AA', $this->getVariable('PatronIdentifier')); + $this->addVarOption('AD', $this->getVariable('PatronPassword'), true); + $this->addVarOption('AC', $this->getVariable('TerminalPassword'), true); + $this->addVarOption('BO', $this->getVariable('FeeAcknowledged'), true); + + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/Request/RenewRequest.php b/src/Request/RenewRequest.php new file mode 100644 index 0000000..bc37827 --- /dev/null +++ b/src/Request/RenewRequest.php @@ -0,0 +1,57 @@ + ['type' => 'YN', 'default' => 'N'], + 'NoBlock' => ['type' => 'YN', 'default' => 'N'], + 'NBDateDue' => ['type' => 'timestamp', 'default' => ''], + 'InstitutionId' => [], + 'PatronIdentifier' => [], + 'PatronPassword' => ['default' => ''], + 'ItemIdentifier' => ['default' => ''], + 'ItemTitle' => ['default' => ''], + 'TerminalPassword' => ['default' => ''], + 'ItemProperties' => ['default' => ''], + 'FeeAcknowledged' => ['type' => 'YN', 'default' => 'N'], + ]; + + public function getMessageString($withSeq = true, $withCrc = true): string + { + $this->newMessage('29'); + $this->addFixedOption($this->getVariable('ThirdParty'), 1); + $this->addFixedOption($this->getVariable('NoBlock'), 1); + $this->addFixedOption($this->datestamp(), 18); + $this->addFixedOption($this->getVariable('NBDateDue'), 18); + + $this->addVarOption('AO', $this->getVariable('InstitutionId')); + $this->addVarOption('AA', $this->getVariable('PatronIdentifier')); + $this->addVarOption('AD', $this->getVariable('PatronPassword'), true); + $this->addVarOption('AB', $this->getVariable('ItemIdentifier'), true); + $this->addVarOption('AJ', $this->getVariable('ItemTitle'), true); + $this->addVarOption('AC', $this->getVariable('TerminalPassword'), true); + $this->addVarOption('CH', $this->getVariable('ItemProperties'), true); + $this->addVarOption('BO', $this->getVariable('FeeAcknowledged'), true); + + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/Request/RequestACSResendRequest.php b/src/Request/RequestACSResendRequest.php new file mode 100644 index 0000000..09487d4 --- /dev/null +++ b/src/Request/RequestACSResendRequest.php @@ -0,0 +1,18 @@ +newMessage('97'); + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/Request/SCStatusRequest.php b/src/Request/SCStatusRequest.php new file mode 100644 index 0000000..fe58ff4 --- /dev/null +++ b/src/Request/SCStatusRequest.php @@ -0,0 +1,35 @@ + ['default' => '0'], + 'Width' => ['default' => '80'], + 'Version' => ['default' => '2'] + ]; + + public function getMessageString($withSeq = true, $withCrc = true): string + { + $this->newMessage('99'); + $this->addFixedOption($this->getVariable('Status'), 1); + $this->addFixedOption($this->getVariable('Width'), 3); + $this->addFixedOption(sprintf("%03.2f", $this->getVariable('Version')), 4); + + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/AbstractSIP2Request.php b/src/Request/SIP2Request.php similarity index 61% rename from src/AbstractSIP2Request.php rename to src/Request/SIP2Request.php index c69b271..82ee04b 100644 --- a/src/AbstractSIP2Request.php +++ b/src/Request/SIP2Request.php @@ -1,8 +1,17 @@ msgBuild .= 'AZ'; - $this->msgBuild .= $this->crc($this->msgBuild); + $this->msgBuild .= self::crc($this->msgBuild); } $this->msgBuild .= $this->msgTerminator; @@ -71,42 +99,6 @@ protected function returnMessage($withSeq = true, $withCrc = true) } /* Core local utility functions */ - protected function datestamp($timestamp = '') - { - /* generate a SIP2 compatible datestamp */ - /* From the spec: - * YYYYMMDDZZZZHHMMSS. - * All dates and times are expressed according to the ANSI standard X3.30 for date and X3.43 for time. - * The ZZZZ field should contain blanks (code $20) to represent local time. To represent universal time, - * a Z character(code $5A) should be put in the last (right hand) position of the ZZZZ field. - * To represent other time zones the appropriate character should be used; a Q character (code $51) - * should be put in the last (right hand) position of the ZZZZ field to represent Atlantic Standard Time. - * When possible local time is the preferred format. - */ - if ($timestamp != '') { - /* Generate a proper date time from the date provided */ - return date('Ymd His', $timestamp); - } else { - /* Current Date/Time */ - return date('Ymd His'); - } - } - - protected function crc($buf) - { - /* Calculate CRC */ - $sum = 0; - - $len = strlen($buf); - for ($n = 0; $n < $len; $n++) { - $sum = $sum + ord(substr($buf, $n, 1)); - } - - $crc = ($sum & 0xFFFF) * -1; - - /* 2008.03.15 - Fixed a bug that allowed the checksum to be larger then 4 digits */ - return substr(sprintf("%4X", $crc), -4, 4); - } protected static function getSeqNumber() { @@ -118,4 +110,4 @@ protected static function getSeqNumber() } return self::$seq; } -} \ No newline at end of file +} diff --git a/src/Response/ACSStatusResponse.php b/src/Response/ACSStatusResponse.php new file mode 100644 index 0000000..5a733e3 --- /dev/null +++ b/src/Response/ACSStatusResponse.php @@ -0,0 +1,68 @@ + [], + 'Checkin' => [], + 'Checkout' => [], + 'Renewal' => [], + 'PatronUpdate' => [], + 'Offline' => [], + 'Timeout' => [], + 'Retries' => [], + 'TransactionDate' => [], + 'Protocol' => [], + ]; + + //variable part of the response allowed to contain these... + protected $allowedVariables=[ + self::AO_INSTITUTION_ID, + self::AM_LIBRARY_NAME, + self::BX_SUPPORTED_MESSAGES, + self::AN_TERMINAL_LOCATION, + self::AF_SCREEN_MESSAGE, + self::AG_PRINT_LINE, + self::AY_SEQUENCE_NUMBER + ]; + + public function __construct($raw) + { + $this->setVariable('Online', substr($raw, 2, 1)); + $this->setVariable('Checkin', substr($raw, 3, 1)); + $this->setVariable('Checkout', substr($raw, 4, 1)); + $this->setVariable('Renewal', substr($raw, 5, 1)); + $this->setVariable('PatronUpdate', substr($raw, 6, 1)); + $this->setVariable('Offline', substr($raw, 7, 1)); + $this->setVariable('Timeout', substr($raw, 8, 3)); + $this->setVariable('Retries', substr($raw, 11, 3)); + $this->setVariable('TransactionDate', substr($raw, 14, 18)); + $this->setVariable('Protocol', substr($raw, 32, 4)); + + $this->parseVariableData($raw, 36); + } +} diff --git a/src/Response/CheckInResponse.php b/src/Response/CheckInResponse.php new file mode 100644 index 0000000..261e5c9 --- /dev/null +++ b/src/Response/CheckInResponse.php @@ -0,0 +1,61 @@ + [], + 'Resensitize' => [], + 'Magnetic' => [], + 'Alert' => [], + 'TransactionDate' => [] + ]; + + //variable part of the response allowed to contain these... + protected $allowedVariables=[ + self::AO_INSTITUTION_ID, + self::AB_ITEM_IDENTIFIER, + self::AQ_PERMANENT_LOCATION, + self::AJ_TITLE_IDENTIFIER, + self::CL_SORT_BIN, + self::AA_PATRON_IDENTIFIER, + self::CK_MEDIA_TYPE, + self::CH_ITEM_PROPERTIES, + self::AF_SCREEN_MESSAGE, + self::AG_PRINT_LINE, + self::AY_SEQUENCE_NUMBER + ]; + + public function __construct($raw) + { + $this->setVariable('Ok', substr($raw, 2, 1)); + $this->setVariable('Resensitize', substr($raw, 3, 1)); + $this->setVariable('Magnetic', substr($raw, 4, 1)); + $this->setVariable('Alert', substr($raw, 5, 1)); + $this->setVariable('TransactionDate', substr($raw, 6, 18)); + + $this->parseVariableData($raw, 24); + } +} diff --git a/src/Response/CheckOutResponse.php b/src/Response/CheckOutResponse.php new file mode 100644 index 0000000..9939c66 --- /dev/null +++ b/src/Response/CheckOutResponse.php @@ -0,0 +1,69 @@ + [], + 'RenewalOk' => [], + 'Magnetic' => [], + 'Desensitize' => [], + 'TransactionDate' => [] + ]; + + //variable part of the response allowed to contain these... + protected $allowedVariables=[ + self::AO_INSTITUTION_ID, + self::AA_PATRON_IDENTIFIER, + self::AB_ITEM_IDENTIFIER, + self::AJ_TITLE_IDENTIFIER, + self::AH_DUE_DATE, + self::BT_FEE_TYPE, + self::CI_SECURITY_INHIBIT, + self::BH_CURRENCY_TYPE, + self::BV_FEE_AMOUNT, + self::CK_MEDIA_TYPE, + self::CH_ITEM_PROPERTIES, + self::BK_TRANSACTION_ID, + self::AF_SCREEN_MESSAGE, + self::AG_PRINT_LINE, + self::AY_SEQUENCE_NUMBER + ]; + + public function __construct($raw) + { + $this->setVariable('Ok', substr($raw, 2, 1)); + $this->setVariable('RenewalOk', substr($raw, 3, 1)); + $this->setVariable('Magnetic', substr($raw, 4, 1)); + $this->setVariable('Desensitize', substr($raw, 5, 1)); + $this->setVariable('TransactionDate', substr($raw, 6, 18)); + + $this->parseVariableData($raw, 24); + } +} diff --git a/src/Response/EndSessionResponse.php b/src/Response/EndSessionResponse.php new file mode 100644 index 0000000..2b7ca79 --- /dev/null +++ b/src/Response/EndSessionResponse.php @@ -0,0 +1,39 @@ + [], + 'TransactionDate' => [] + ]; + + //variable part of the response allowed to contain these... + protected $allowedVariables=[ + self::AO_INSTITUTION_ID, + self::AA_PATRON_IDENTIFIER, + self::AF_SCREEN_MESSAGE, + self::AG_PRINT_LINE, + self::AY_SEQUENCE_NUMBER + ]; + + public function __construct($raw) + { + $this->setVariable('EndSession', substr($raw, 2, 1)); + $this->setVariable('TransactionDate', substr($raw, 3, 18)); + $this->parseVariableData($raw, 21); + } +} diff --git a/src/Response/FeePaidResponse.php b/src/Response/FeePaidResponse.php new file mode 100644 index 0000000..aa079ae --- /dev/null +++ b/src/Response/FeePaidResponse.php @@ -0,0 +1,41 @@ + [], + 'TransactionDate' => [] + ]; + + //variable part of the response allowed to contain these... + protected $allowedVariables=[ + self::AO_INSTITUTION_ID, + self::AA_PATRON_IDENTIFIER, + self::BK_TRANSACTION_ID, + self::AF_SCREEN_MESSAGE, + self::AG_PRINT_LINE, + self::AY_SEQUENCE_NUMBER + ]; + + public function __construct($raw) + { + $this->setVariable('PaymentAccepted', substr($raw, 2, 1)); + $this->setVariable('TransactionDate', substr($raw, 3, 18)); + $this->parseVariableData($raw, 21); + } +} diff --git a/src/Response/HoldResponse.php b/src/Response/HoldResponse.php new file mode 100644 index 0000000..dbdc5e4 --- /dev/null +++ b/src/Response/HoldResponse.php @@ -0,0 +1,70 @@ + [], + 'Available' => [], + 'TransactionDate' => [] + ]; + + //variable part of the response allowed to contain these... + protected $allowedVariables=[ + self::BW_EXPIRATION_DATE, + self::BR_QUEUE_POSITION, + self::BS_PICKUP_LOCATION, + self::AO_INSTITUTION_ID, + self::AA_PATRON_IDENTIFIER, + self::AB_ITEM_IDENTIFIER, + self::AJ_TITLE_IDENTIFIER, + self::AF_SCREEN_MESSAGE, + self::AG_PRINT_LINE, + self::AY_SEQUENCE_NUMBER + ]; + + public function __construct($raw) + { + /* + * $result = []; + $result['fixed'] = + [ + 'Ok' => substr($response, 2, 1), + 'available' => substr($response, 3, 1), + 'TransactionDate' => substr($response, 4, 18) + ]; + + //expiration date is optional an indicated by BW + $variableOffset = 22; + if (substr($response, 22, 2) === 'BW') { + $result['fixed']['ExpirationDate'] = substr($response, 24, 18); + $variableOffset = 42; + } + + $result['variable'] = $this->parseVariableData($response, $variableOffset); + */ + $this->setVariable('Ok', substr($raw, 2, 1)); + $this->setVariable('Available', substr($raw, 3, 1)); + $this->setVariable('TransactionDate', substr($raw, 4, 18)); + $this->parseVariableData($raw, 22); + } +} diff --git a/src/Response/ItemInformationResponse.php b/src/Response/ItemInformationResponse.php new file mode 100644 index 0000000..75a9276 --- /dev/null +++ b/src/Response/ItemInformationResponse.php @@ -0,0 +1,68 @@ + [], + 'SecurityMarker' => [], + 'FeeType' => [], + 'TransactionDate' => [] + ]; + + //variable part of the response allowed to contain these... + protected $allowedVariables=[ + self::CF_HOLD_QUEUE_LENGTH, + self::AH_DUE_DATE, + self::CJ_RECALL_DATE, + self::CM_HOLD_PICKUP_DATE, + self::AB_ITEM_IDENTIFIER, + self::AJ_TITLE_IDENTIFIER, + self::BG_OWNER, + self::BH_CURRENCY_TYPE, + self::BV_FEE_AMOUNT, + self::CK_MEDIA_TYPE, + self::AQ_PERMANENT_LOCATION, + self::AP_CURRENT_LOCATION, + self::CH_ITEM_PROPERTIES, + self::AF_SCREEN_MESSAGE, + self::AG_PRINT_LINE, + self::AY_SEQUENCE_NUMBER + ]; + + public function __construct($raw) + { + $this->setVariable('CirculationStatus', substr($raw, 2, 2)); + $this->setVariable('SecurityMarker', substr($raw, 4, 2)); + $this->setVariable('FeeType', substr($raw, 6, 2)); + $this->setVariable('TransactionDate', substr($raw, 8, 18)); + + $this->parseVariableData($raw, 26); + } +} diff --git a/src/Response/ItemStatusUpdateResponse.php b/src/Response/ItemStatusUpdateResponse.php new file mode 100644 index 0000000..ea68fa4 --- /dev/null +++ b/src/Response/ItemStatusUpdateResponse.php @@ -0,0 +1,41 @@ + [], + 'TransactionDate' => [], + ]; + + //variable part of the response allowed to contain these... + protected $allowedVariables=[ + self::AB_ITEM_IDENTIFIER, + self::AJ_TITLE_IDENTIFIER, + self::CH_ITEM_PROPERTIES, + self::AF_SCREEN_MESSAGE, + self::AG_PRINT_LINE, + self::AY_SEQUENCE_NUMBER + ]; + + public function __construct($raw) + { + $this->setVariable('PropertiesOk', substr($raw, 2, 1)); + $this->setVariable('TransactionDate', substr($raw, 3, 18)); + $this->parseVariableData($raw, 21); + } +} diff --git a/src/Response/LoginResponse.php b/src/Response/LoginResponse.php new file mode 100644 index 0000000..01b1478 --- /dev/null +++ b/src/Response/LoginResponse.php @@ -0,0 +1,20 @@ + ['type' => 'n'], + ]; + + public function __construct($raw) + { + $this->setVariable('Ok', substr($raw, 2, 1)); + } +} diff --git a/src/Response/PatronEnableResponse.php b/src/Response/PatronEnableResponse.php new file mode 100644 index 0000000..6726708 --- /dev/null +++ b/src/Response/PatronEnableResponse.php @@ -0,0 +1,49 @@ + [], + 'Language' => [], + 'TransactionDate' => [] + ]; + + //variable part of the response allowed to contain these... + protected $allowedVariables=[ + self::AO_INSTITUTION_ID, + self::AA_PATRON_IDENTIFIER, + self::AE_PERSONAL_NAME, + self::BL_VALID_PATRON, + self::CQ_VALID_PATRON_PASSWORD, + self::AF_SCREEN_MESSAGE, + self::AG_PRINT_LINE, + self::AY_SEQUENCE_NUMBER + ]; + + public function __construct($raw) + { + $this->setVariable('PatronStatus', substr($raw, 2, 14)); + $this->setVariable('Language', substr($raw, 16, 3)); + $this->setVariable('TransactionDate', substr($raw, 19, 18)); + + $this->parseVariableData($raw, 37); + } +} diff --git a/src/Response/PatronInformationResponse.php b/src/Response/PatronInformationResponse.php new file mode 100644 index 0000000..06d2091 --- /dev/null +++ b/src/Response/PatronInformationResponse.php @@ -0,0 +1,96 @@ + [], + 'Language' => [], + 'TransactionDate' => [], + 'HoldCount' => [], + 'OverdueCount' => [], + 'ChargedCount' => [], + 'FineCount' => [], + 'RecallCount' => [], + 'UnavailableCount' => [], + ]; + + //variable part of the response allowed to contain these... + protected $allowedVariables=[ + self::AO_INSTITUTION_ID, + self::AA_PATRON_IDENTIFIER, + self::AE_PERSONAL_NAME, + self::BZ_HOLD_ITEMS_LIMIT, + self::CA_OVERDUE_ITEMS_LIMIT, + self::CB_CHARGED_ITEMS_LIMIT, + self::BL_VALID_PATRON, + self::CQ_VALID_PATRON_PASSWORD, + self::BH_CURRENCY_TYPE, + self::BV_FEE_AMOUNT, + self::CC_FEE_LIMIT, + self::AS_HOLD_ITEMS, + self::AT_OVERDUE_ITEMS, + self::AU_CHARGED_ITEMS, + self::AV_FINE_ITEMS, + self::BU_RECALL_ITEMS, + self::CD_UNAVAILABLE_HOLD_ITEMS, + self::BD_HOME_ADDRESS, + self::BE_EMAIL_ADDRESS, + self::BF_HOME_PHONE_NUMBER, + self::AF_SCREEN_MESSAGE, + self::AG_PRINT_LINE, + self::AY_SEQUENCE_NUMBER + ]; + + public function __construct($raw) + { + $this->setVariable('PatronStatus', substr($raw, 2, 14)); + $this->setVariable('Language', substr($raw, 16, 3)); + $this->setVariable('TransactionDate', substr($raw, 19, 18)); + $this->setVariable('HoldCount', substr($raw, 37, 4)); + $this->setVariable('OverdueCount', substr($raw, 41, 4)); + $this->setVariable('ChargedCount', substr($raw, 45, 4)); + $this->setVariable('FineCount', substr($raw, 49, 4)); + $this->setVariable('RecallCount', substr($raw, 53, 4)); + $this->setVariable('UnavailableCount', substr($raw, 57, 4)); + + $this->parseVariableData($raw, 61); + } +} diff --git a/src/Response/PatronStatusResponse.php b/src/Response/PatronStatusResponse.php new file mode 100644 index 0000000..52240f8 --- /dev/null +++ b/src/Response/PatronStatusResponse.php @@ -0,0 +1,53 @@ + [], + 'Language' => [], + 'TransactionDate' => [] + ]; + + //variable part of the response allowed to contain these... + protected $allowedVariables=[ + self::AO_INSTITUTION_ID, + self::AA_PATRON_IDENTIFIER, + self::AE_PERSONAL_NAME, + self::BL_VALID_PATRON, + self::CQ_VALID_PATRON_PASSWORD, + self::BH_CURRENCY_TYPE, + self::BV_FEE_AMOUNT, + self::AF_SCREEN_MESSAGE, + self::AG_PRINT_LINE, + self::AY_SEQUENCE_NUMBER + ]; + + public function __construct($raw) + { + $this->setVariable('PatronStatus', substr($raw, 2, 14)); + $this->setVariable('Language', substr($raw, 16, 3)); + $this->setVariable('TransactionDate', substr($raw, 19, 18)); + + $this->parseVariableData($raw, 37); + } +} diff --git a/src/Response/RenewAllResponse.php b/src/Response/RenewAllResponse.php new file mode 100644 index 0000000..1867ca2 --- /dev/null +++ b/src/Response/RenewAllResponse.php @@ -0,0 +1,47 @@ + [], + 'Renewed' => [], + 'Unrenewed' => [], + 'TransactionDate' => [] + ]; + + //variable part of the response allowed to contain these... + protected $allowedVariables=[ + self::AO_INSTITUTION_ID, + self::BM_RENEWED_ITEMS, + self::BN_UNRENEWED_ITEMS, + self::AF_SCREEN_MESSAGE, + self::AG_PRINT_LINE, + self::AY_SEQUENCE_NUMBER + ]; + + public function __construct($raw) + { + $this->setVariable('Ok', substr($raw, 2, 1)); + $this->setVariable('Renewed', substr($raw, 3, 4)); + $this->setVariable('Unrenewed', substr($raw, 7, 4)); + $this->setVariable('TransactionDate', substr($raw, 11, 18)); + $this->parseVariableData($raw, 29); + } +} diff --git a/src/Response/RenewResponse.php b/src/Response/RenewResponse.php new file mode 100644 index 0000000..5d4647c --- /dev/null +++ b/src/Response/RenewResponse.php @@ -0,0 +1,69 @@ + [], + 'RenewalOk' => [], + 'Magnetic' => [], + 'Desensitize' => [], + 'TransactionDate' => [] + ]; + + //variable part of the response allowed to contain these... + protected $allowedVariables=[ + self::AO_INSTITUTION_ID, + self::AA_PATRON_IDENTIFIER, + self::AB_ITEM_IDENTIFIER, + self::AJ_TITLE_IDENTIFIER, + self::AH_DUE_DATE, + self::BT_FEE_TYPE, + self::CI_SECURITY_INHIBIT, + self::BH_CURRENCY_TYPE, + self::BV_FEE_AMOUNT, + self::CK_MEDIA_TYPE, + self::CH_ITEM_PROPERTIES, + self::BK_TRANSACTION_ID, + self::AF_SCREEN_MESSAGE, + self::AG_PRINT_LINE, + self::AY_SEQUENCE_NUMBER + ]; + + public function __construct($raw) + { + $this->setVariable('Ok', substr($raw, 2, 1)); + $this->setVariable('RenewalOk', substr($raw, 3, 1)); + $this->setVariable('Magnetic', substr($raw, 4, 1)); + $this->setVariable('Desensitize', substr($raw, 5, 1)); + $this->setVariable('TransactionDate', substr($raw, 6, 18)); + + $this->parseVariableData($raw, 24); + } +} diff --git a/src/Response/SIP2Response.php b/src/Response/SIP2Response.php new file mode 100644 index 0000000..f1b3b34 --- /dev/null +++ b/src/Response/SIP2Response.php @@ -0,0 +1,202 @@ + CheckInResponse::class, + '12' => CheckOutResponse::class, + '16' => HoldResponse::class, + '18' => ItemInformationResponse::class, + '20' => ItemStatusUpdateResponse::class, + '24' => PatronStatusResponse::class, + '26' => PatronEnableResponse::class, + '30' => RenewResponse::class, + '36' => EndSessionResponse::class, + '38' => FeePaidResponse::class, + '64' => PatronInformationResponse::class, + '66' => RenewAllResponse::class, + '94' => LoginResponse::class, + '98' => ACSStatusResponse::class, + ]; + + /** @var array maps SIP2 variable code names to a definition */ + private static $mapCodeToVarDef = [ + self::AA_PATRON_IDENTIFIER => ['name' => 'PatronIdentifier', 'default'=>''], + self::AB_ITEM_IDENTIFIER => ['name' => 'ItemIdentifier', 'default'=>''], + self::AE_PERSONAL_NAME => ['name' => 'PersonalName', 'default'=>''], + self::AF_SCREEN_MESSAGE => ['name' => 'ScreenMessage', 'type' => 'array', 'default'=>[]], + self::AG_PRINT_LINE => ['name' => 'PrintLine', 'type' => 'array', 'default'=>[]], + self::AH_DUE_DATE => ['name' => 'DueDate', 'default'=>''], + self::AJ_TITLE_IDENTIFIER => ['name' => 'TitleIdentifier', 'default'=>''], + self::AM_LIBRARY_NAME => ['name' => 'LibraryName', 'default'=>''], + self::AN_TERMINAL_LOCATION => ['name' => 'TerminalLocation', 'default'=>''], + self::AO_INSTITUTION_ID => ['name' => 'InstitutionId', 'default'=>''], + self::AP_CURRENT_LOCATION => ['name' => 'CurrentLocation', 'default'=>''], + self::AQ_PERMANENT_LOCATION => ['name' => 'PermanentLocation', 'default'=>''], + self::AS_HOLD_ITEMS => ['name' => 'HoldItems', 'type' => 'array', 'default'=>[]], + self::AT_OVERDUE_ITEMS => ['name' => 'OverdueItems', 'type' => 'array', 'default'=>[]], + self::AU_CHARGED_ITEMS => ['name' => 'ChargedItems', 'type' => 'array', 'default'=>[]], + self::AV_FINE_ITEMS => ['name' => 'FineItems', 'type' => 'array', 'default'=>[]], + self::AY_SEQUENCE_NUMBER => ['name' => 'SequenceNumber', 'default'=>''], + self::BD_HOME_ADDRESS => ['name' => 'HomeAddress', 'default'=>''], + self::BE_EMAIL_ADDRESS => ['name' => 'EmailAddress', 'default'=>''], + self::BF_HOME_PHONE_NUMBER => ['name' => 'HomePhoneNumber', 'default'=>''], + self::BG_OWNER => ['name' => 'Owner', 'default'=>''], + self::BH_CURRENCY_TYPE => ['name' => 'CurrencyType', 'default'=>''], + self::BK_TRANSACTION_ID => ['name' => 'TransactionId', 'default'=>''], + self::BL_VALID_PATRON => ['name' => 'ValidPatron', 'default'=>''], + self::BM_RENEWED_ITEMS => ['name' => 'RenewedItems', 'type' => 'array', 'default'=>[]], + self::BN_UNRENEWED_ITEMS => ['name' => 'UnrenewedItems', 'type' => 'array', 'default'=>[]], + self::BR_QUEUE_POSITION => ['name' => 'QueuePosition', 'default'=>''], + self::BS_PICKUP_LOCATION => ['name' => 'PickupLocation', 'default'=>''], + self::BT_FEE_TYPE => ['name' => 'FeeType', 'default'=>''], + self::BU_RECALL_ITEMS => ['name' => 'RecallItems', 'type' => 'array', 'default'=>[]], + self::BV_FEE_AMOUNT => ['name' => 'FeeAmount', 'default'=>''], + self::BW_EXPIRATION_DATE => ['name' => 'ExpirationDate', 'default'=>''], + self::BX_SUPPORTED_MESSAGES => ['name' => 'SupportedMessages', 'default'=>''], + self::BZ_HOLD_ITEMS_LIMIT => ['name' => 'HoldItemsLimit', 'default'=>''], + self::CA_OVERDUE_ITEMS_LIMIT => ['name' => 'OverdueItemsLimit', 'default'=>''], + self::CB_CHARGED_ITEMS_LIMIT => ['name' => 'ChargedItemsLimit', 'default'=>''], + self::CC_FEE_LIMIT => ['name' => 'FeeLimit', 'default'=>''], + self::CD_UNAVAILABLE_HOLD_ITEMS => ['name' => 'UnavailableHoldItems', 'type' => 'array', 'default'=>[]], + self::CF_HOLD_QUEUE_LENGTH => ['name' => 'HoldQueueLength', 'default'=>''], + self::CH_ITEM_PROPERTIES => ['name' => 'ItemProperties', 'default'=>''], + self::CI_SECURITY_INHIBIT => ['name' => 'SecurityInhibit', 'default'=>''], + self::CJ_RECALL_DATE => ['name' => 'RecallDate', 'default'=>''], + self::CK_MEDIA_TYPE => ['name' => 'MediaType', 'default'=>''], + self::CL_SORT_BIN => ['name' => 'SortBin', 'default'=>''], + self::CM_HOLD_PICKUP_DATE => ['name' => 'HoldPickupDate', 'default'=>''], + self::CQ_VALID_PATRON_PASSWORD => ['name' => 'ValidPatronPassword', 'default'=>''] + ]; + + protected $allowedVariables = []; + + public static function parse($raw): SIP2Response + { + if (empty($raw) || !self::checkCRC($raw)) { + throw new LogicException("Empty string or bad CRC not expected here");//@codeCoverageIgnore + } + + $type = substr($raw, 0, 2); + if (!isset(self::$mapResponseToClass[$type])) { + throw new RuntimeException("Unexpected SIP2 response $type"); + } + + //good to go + $className = self::$mapResponseToClass[$type]; + return new $className($raw); + } + + public static function checkCRC($raw) + { + $test = preg_split('/(.{4})$/', trim($raw), 2, PREG_SPLIT_DELIM_CAPTURE); + return self::crc($test[0]) == $test[1]; + } + + protected function parseVariableData($response, $start) + { + //init allowed variables + foreach ($this->allowedVariables as $code) { + if (!isset(self::$mapCodeToVarDef[$code])) { + throw new LogicException("Unexpected $code in allowed variables"); //@codeCoverageIgnore + } + $name = self::$mapCodeToVarDef[$code]['name']; + if (!$this->hasVariable($name)) { + //add a definition for this variable + $this->var[$name] = self::$mapCodeToVarDef[$code]; + } + } + + $items = explode("|", substr($response, $start, -7)); + + foreach ($items as $item) { + $field = substr($item, 0, 2); + + //expected? + if (!in_array($field, $this->allowedVariables)) { + //we tolerate unexpected values and treat them as array types + //named after the code if we don't have a definition for it + if (!isset(self::$mapCodeToVarDef[$field])) { + self::$mapCodeToVarDef[$field]=[ + 'name' => $field, + 'type' => 'array' + ]; + $name=$field; + } else { + $name = self::$mapCodeToVarDef[$field]['name']; + } + $this->var[$name] = self::$mapCodeToVarDef[$field]; + } + + $value = substr($item, 2); + /* SD returns some odd values on occasion, Unable to locate the purpose in spec, so I strip from + * the parsed array. Orig values will remain in ['raw'] element + */ + $clean = trim($value, "\x00..\x1F"); + if ($clean!='') { + $name = self::$mapCodeToVarDef[$field]['name']; + $this->addVariable($name, $clean); + } + } + } +} diff --git a/src/SIP2Client.php b/src/SIP2Client.php index 5d30b5a..055190d 100644 --- a/src/SIP2Client.php +++ b/src/SIP2Client.php @@ -15,6 +15,9 @@ * @version 2.0.0 */ +use lordelph\SIP2\Exception\RuntimeException; +use lordelph\SIP2\Request\SIP2Request; +use lordelph\SIP2\Response\SIP2Response; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerInterface; @@ -36,19 +39,6 @@ class SIP2Client implements LoggerAwareInterface // connection configuration //----------------------------------------------------- - /** @var string hostname or IP address to connect to */ - public $hostname; - - /** @var int port number */ - public $port = 6002; - - /** - * @var string IP (or IP:port) to bind outbound connnections to - * Using this is only necessary on a machine which has multiple outbound connections and its important - * to control which one is used (normally because the remote SIP2 service is firewalled to particular IPs - */ - public $bindTo = ''; - /** @var int maximum number of resends in the event of CRC failure */ public $maxretry = 3; @@ -96,21 +86,6 @@ class SIP2Client implements LoggerAwareInterface /** @var string Terminal password */ public $terminalPassword = ''; - //----------------------------------------------------- - // internal request building - //----------------------------------------------------- - - /** @var int sequence counter for AY */ - private $seq = -1; - - /** @var int resend counter */ - private $retry = 0; - - /** @var string request is built up here */ - private $msgBuild = ''; - - /** @var bool tracks when a variable field is used to prevent further fixed fields */ - private $noFixed = false; //----------------------------------------------------- // internal socket handling @@ -124,7 +99,10 @@ class SIP2Client implements LoggerAwareInterface /** * Constructor allows you to provide a PSR-3 logger, but you can also use the setLogger method - * later on + * later on. + * + * You can also specific the IP address you want to bind to, which is useful if you have multiple local + * IPs, but you want the remote SIP2 service to see a specific IP address * * @param LoggerInterface|null $logger */ @@ -156,860 +134,26 @@ private function getSocketFactory() return $this->socketFactory; } - /** - * This message is used by the client to request patron information from the SIP2 server. The service must - * respond to this command with a Patron Status Response message. - * @return string - * - * @see SIP2Client::parsePatronStatusResponse() - */ - public function msgPatronStatusRequest() - { - /* Server Response: Patron Status Response message. */ - $this->newMessage('23'); - $this->addFixedOption($this->language, 3); - $this->addFixedOption($this->datestamp(), 18); - $this->addVarOption('AO', $this->institutionId); - $this->addVarOption('AA', $this->patron); - $this->addVarOption('AC', $this->terminalPassword); - $this->addVarOption('AD', $this->patronpwd); - return $this->returnMessage(); - } - - /** - * This message is used by the SC to request to check out an item, and also to cancel a Checkin request that did - * not successfully complete. The ACS must respond to this command with a Checkout Response message. - * - * @param string $item item identifier - * @param string $nbDateDue unix timestamp of due date (can be blank to let service decide) - * @param string $scRenewal renewal policy, either Y or N - * @param string $itmProp item properties - * @param string $fee fee acknowledge, either Y or N - * @param string $noBlock no block, either Y or N - * @param string $cancel Y or N - used to cancel an incomplete checkin - * - * @return string - * - * @see SIP2Client::parseCheckoutResponse() - */ - public function msgCheckout( - $item, - $nbDateDue = '', - $scRenewal = 'N', - $itmProp = '', - $fee = 'N', - $noBlock = 'N', - $cancel = 'N' - ) { - - $this->newMessage('11'); - $this->addFixedOption($scRenewal, 1); - $this->addFixedOption($noBlock, 1); - $this->addFixedOption($this->datestamp(), 18); - if ($nbDateDue != '') { - /* override default date due */ - $this->addFixedOption($this->datestamp($nbDateDue), 18); - } else { - /* send a blank date due to allow ACS to use default date due computed for item */ - $this->addFixedOption('', 18); - } - $this->addVarOption('AO', $this->institutionId); - $this->addVarOption('AA', $this->patron); - $this->addVarOption('AB', $item); - $this->addVarOption('AC', $this->terminalPassword); - $this->addVarOption('CH', $itmProp, true); - $this->addVarOption('AD', $this->patronpwd, true); - $this->addVarOption('BO', $fee, true); /* Y or N */ - $this->addVarOption('BI', $cancel, true); /* Y or N */ - - return $this->returnMessage(); - } - - /** - * This message is used by the SC to request to check in an item, and also to cancel a Checkout request that did not - * successfully complete. The ACS must respond to this command with a Checkin Response message. - * @param string $item item identifier - * @param string $itmReturnDate unix timestamp of return date - * @param string $itmLocation item location - * @param string $itmProp item properties - * @param string $noBlock no block, either Y or N - * @param string $cancel Y or N - used to cancel an incomplete checkout - * @return string - * - * @see SIP2Client::parseCheckinResponse() - */ - public function msgCheckin($item, $itmReturnDate, $itmLocation = '', $itmProp = '', $noBlock = 'N', $cancel = '') - { - if ($itmLocation == '') { - /* If no location is specified, assume the default location of the SC, behaviour suggested by spec*/ - $itmLocation = $this->location; - } - - $this->newMessage('09'); - $this->addFixedOption($noBlock, 1); - $this->addFixedOption($this->datestamp(), 18); - $this->addFixedOption($this->datestamp($itmReturnDate), 18); - $this->addVarOption('AP', $itmLocation); - $this->addVarOption('AO', $this->institutionId); - $this->addVarOption('AB', $item); - $this->addVarOption('AC', $this->terminalPassword); - $this->addVarOption('CH', $itmProp, true); - $this->addVarOption('BI', $cancel, true); /* Y or N */ - - return $this->returnMessage(); - } - - /** - * This message requests that the patron card be blocked by the ACS. This is, for example, sent when the patron is - * detected tampering with the SC or when a patron forgets to take their card. The ACS should invalidate the - * patron’s card and respond with a Patron Status Response message. The ACS could also notify the library staff - * that the card has been blocked. - * - * @param string $message blocked card message - * @param string $retained Y/N indicating whether card was retained - * @return string - * - * @see SIP2Client::parsePatronStatusResponse() - */ - public function msgBlockPatron($message, $retained = 'N') - { - $this->newMessage('01'); - $this->addFixedOption($retained, 1); /* Y if card has been retained */ - $this->addFixedOption($this->datestamp(), 18); - $this->addVarOption('AO', $this->institutionId); - $this->addVarOption('AL', $message); - $this->addVarOption('AA', $this->patronId); - $this->addVarOption('AC', $this->terminalPassword); - - return $this->returnMessage(); - } - - /** - * The SC status message sends SC status to the ACS. It requires an ACS Status Response message reply from the ACS. - * This message will be the first message sent by the SC to the ACS once a connection has been established - * (exception: the Login Message may be sent first to login to an ACS server program). The ACS will respond with a - * message that establishes some of the rules to be followed by the SC and establishes some parameters needed for - * further communication. - * - * @param int $status 0=OK, 1=out of paper, 2=shutting down - * @param int $width print width - * @param int $version version number X.YY - * @return bool|string - * - * @see SIP2Client::parseACSStatusResponse() - */ - public function msgSCStatus($status = 0, $width = 80, $version = 2) - { - $version = min(2, $version); - - if ($status < 0 || $status > 2) { - //@codeCoverageIgnoreStart - $this->logger->error("SIP2: Invalid status passed to msgSCStatus"); - return false; - //@codeCoverageIgnoreEnd - } - $this->newMessage('99'); - $this->addFixedOption($status, 1); - $this->addFixedOption($width, 3); - $this->addFixedOption(sprintf("%03.2f", $version), 4); - return $this->returnMessage(); - } - - /** - * This message requests the ACS to re-transmit its last message. It is sent by the SC to the ACS when the checksum - * in a received message does not match the value calculated by the SC. The ACS should respond by re-transmitting - * its last message, This message should never include a “sequence number” field, even when error detection is - * enabled, but would include a “checksum” field since checksums are in use. - * - * @return string - */ - public function msgRequestACSResend() - { - $this->newMessage('97'); - return $this->returnMessage(false); - } - - /** - * This message can be used to login to an ACS server program. The ACS should respond with the Login Response - * message. Whether to use this message or to use some other mechanism to login to the ACS is configurable on the - * SC. When this message is used, it will be the first message sent to the ACS. - * - * @param string $sipLogin username - * @param string $sipPassword password - * @return string - * - * @see SIP2Client::parseLoginResponse() - */ - public function msgLogin($sipLogin, $sipPassword) - { - $this->newMessage('93'); - $this->addFixedOption($this->uidAlgorithm, 1); - $this->addFixedOption($this->passwordAlgorithm, 1); - $this->addVarOption('CN', $sipLogin); - $this->addVarOption('CO', $sipPassword); - $this->addVarOption('CP', $this->location, true); - return $this->returnMessage(); - } - - /** - * This message is a superset of the Patron Status Request message. It should be used to request patron information. - * The ACS should respond with the Patron Information Response message. - * - * @param string $type one of none,hold,overdue,charged,fine,recall or unavail - * @param string $start item - * @param string $end item - * @return string - * - * @see SIP2Client::parsePatronInfoResponse() - */ - public function msgPatronInformation($type, $start = '1', $end = '5') - { - /* - * According to the specification: - * Only one category of items should be requested at a time, i.e. it would take 6 of these messages, - * each with a different position set to Y, to get all the detailed information about a patron's items. - */ - $summary = []; - $summary['none'] = ' '; - $summary['hold'] = 'Y '; - $summary['overdue'] = ' Y '; - $summary['charged'] = ' Y '; - $summary['fine'] = ' Y '; - $summary['recall'] = ' Y '; - $summary['unavail'] = ' Y'; - - /* Request patron information */ - $this->newMessage('63'); - $this->addFixedOption($this->language, 3); - $this->addFixedOption($this->datestamp(), 18); - $this->addFixedOption(sprintf("%-10s", $summary[$type]), 10); - $this->addVarOption('AO', $this->institutionId); - $this->addVarOption('AA', $this->patron); - $this->addVarOption('AC', $this->terminalPassword, true); - $this->addVarOption('AD', $this->patronpwd, true); - /* old function version used padded 5 digits, not sure why */ - $this->addVarOption('BP', $start, true); - /* old function version used padded 5 digits, not sure why */ - $this->addVarOption('BQ', $end, true); - return $this->returnMessage(); - } - - /** - * This message will be sent when a patron has completed all of their transactions. The ACS may, upon receipt of - * this command, close any open files or deallocate data structures pertaining to that patron. The ACS should - * respond with an End Session Response message. - * @return string - * - * @see SIP2Client::parseEndSessionResponse() - */ - public function msgEndPatronSession() - { - $this->newMessage('35'); - $this->addFixedOption($this->datestamp(), 18); - $this->addVarOption('AO', $this->institutionId); - $this->addVarOption('AA', $this->patron); - $this->addVarOption('AC', $this->terminalPassword, true); - $this->addVarOption('AD', $this->patronpwd, true); - return $this->returnMessage(); - } - - /** - * This message can be used to notify the ACS that a fee has been collected from the patron. The ACS should record - * this information in their database and respond with a Fee Paid Response message. - * - * @param string $feeType fee type - * 01 other/unknown - * 02 administrative - * 03 damage - * 04 overdue - * 05 processing - * 06 rental - * 07 replacement - * 08 computer access charge - * 09 hold fee - * @param string $pmtType payment type - * 00 cash - * 01 visa - * 02 credit card - * @param string $pmtAmount payment amount - * @param string $curType currency 3-letter code following ISO Standard 4217:1995 - * @param string $feeId Identifies a specific fee, possibly in combination with fee type. - * @param string $transId transaction identifier - * - * @return bool|string - * - * @see SIP2Client::parseFeePaidResponse() - */ - public function msgFeePaid($feeType, $pmtType, $pmtAmount, $curType = 'USD', $feeId = '', $transId = '') - { - if (!is_numeric($feeType) || $feeType > 99 || $feeType < 1) { - $this->logger->error("SIP2: (msgFeePaid) Invalid fee type: {$feeType}"); - return false; - } - - if (!is_numeric($pmtType) || $pmtType > 99 || $pmtType < 0) { - $this->logger->error("SIP2: (msgFeePaid) Invalid payment type: {$pmtType}"); - return false; - } - - $this->newMessage('37'); - $this->addFixedOption($this->datestamp(), 18); - $this->addFixedOption(sprintf('%02d', $feeType), 2); - $this->addFixedOption(sprintf('%02d', $pmtType), 2); - $this->addFixedOption($curType, 3); - - // due to currency format localization, it is up to the programmer - // to properly format their payment amount - $this->addVarOption('BV', $pmtAmount); - $this->addVarOption('AO', $this->institutionId); - $this->addVarOption('AA', $this->patron); - $this->addVarOption('AC', $this->terminalPassword, true); - $this->addVarOption('AD', $this->patronpwd, true); - $this->addVarOption('CG', $feeId, true); - $this->addVarOption('BK', $transId, true); - - return $this->returnMessage(); - } /** - * This message may be used to request item information. The ACS should respond with the Item Information Response - * message. - * - * @param string $item item identifier - * @return string - * - * @see SIP2Client::parseItemInfoResponse() + * @param SIP2Request $request + * @return SIP2Response + * @throws RuntimeException if server fails to produce a valid response */ - public function msgItemInformation($item) + public function sendRequest(SIP2Request $request) : SIP2Response { - $this->newMessage('17'); - $this->addFixedOption($this->datestamp(), 18); - $this->addVarOption('AO', $this->institutionId); - $this->addVarOption('AB', $item); - $this->addVarOption('AC', $this->terminalPassword, true); - return $this->returnMessage(); + $raw = $this->getRawResponse($request); + return SIP2Response::parse($raw); } - /** - * This message can be used to send item information to the ACS, without having to do a Checkout or Checkin - * operation. The item properties could be stored on the ACS’s database. The ACS should respond with an Item - * Status Update Response message. - * - * @param string $item item identifier - * @param string $itmProp item properties - * @return string - * - * @see SIP2Client::parseItemStatusResponse() - */ - public function msgItemStatus($item, $itmProp = '') - { - $this->newMessage('19'); - $this->addFixedOption($this->datestamp(), 18); - $this->addVarOption('AO', $this->institutionId); - $this->addVarOption('AB', $item); - $this->addVarOption('AC', $this->terminalPassword, true); - $this->addVarOption('CH', $itmProp); - return $this->returnMessage(); - } - - /** - * This message can be used by the SC to re-enable canceled patrons. It should only be used for system testing and - * validation. The ACS should respond with a Patron Enable Response message. - * - * @return string - * - * @see SIP2Client::parsePatronEnableResponse() - */ - public function msgPatronEnable() + private function getRawResponse(SIP2Request $request, $depth = 0) { - /* Patron Enable function (25) - untested */ - /* This message can be used by the SC to re-enable cancelled patrons. - It should only be used for system testing and validation. */ - $this->newMessage('25'); - $this->addFixedOption($this->datestamp(), 18); - $this->addVarOption('AO', $this->institutionId); - $this->addVarOption('AA', $this->patron); - $this->addVarOption('AC', $this->terminalPassword, true); - $this->addVarOption('AD', $this->patronpwd, true); - return $this->returnMessage(); - } - - /** - * This message is used to create, modify, or delete a hold. The ACS should respond with a Hold Response message. - * Either or both of the “item identifier” and “title identifier” fields must be present for the message to - * be useful. - * - * @param string $mode one of '+', '-' or '*' to denote add, delete or change - * @param string $expDate unix timestamp expiration date - * @param string $holdtype optional single digit, one of following values: - * 1 other - * 2 any copy of title - * 3 specific copy - * 4 any copy at a single branch or location - * @param string $item item identifier - * @param string $title item title - * @param string $feeAcknowledged Y/N to indicate if fee has been acknowledged - * @param string $pickupLocation pickup location - * @return bool|string - * - * @see SIP2Client::parseHoldResponse() - */ - public function msgHold( - $mode, - $expDate = '', - $holdtype = '', - $item = '', - $title = '', - $feeAcknowledged = 'N', - $pickupLocation = '' - ) { - - if (strpos('-+*', $mode) === false) { - $this->logger->error("SIP2: Invalid hold mode: {$mode}"); - return false; - } - - if ($holdtype != '' && ($holdtype < 1 || $holdtype > 9)) { - $this->logger->error("SIP2: Invalid hold type code: {$holdtype}"); - return false; - } - - $this->newMessage('15'); - $this->addFixedOption($mode, 1); - $this->addFixedOption($this->datestamp(), 18); - if ($expDate != '') { - $this->addVarOption('BW', $this->datestamp($expDate), true); - } - $this->addVarOption('BS', $pickupLocation, true); - $this->addVarOption('BY', $holdtype, true); - $this->addVarOption('AO', $this->institutionId); - $this->addVarOption('AA', $this->patron); - $this->addVarOption('AD', $this->patronpwd, true); - $this->addVarOption('AB', $item, true); - $this->addVarOption('AJ', $title, true); - $this->addVarOption('AC', $this->terminalPassword, true); - $this->addVarOption('BO', $feeAcknowledged, true); - - return $this->returnMessage(); - } - - /** - * This message is used to renew an item. The ACS should respond with a Renew Response message. Either or both of - * the “item identifier” and “title identifier” fields must be present for the message to be useful. - * - * @param string $item item identifier - * @param string $title item title - * @param string $nbDateDue unix timestamp of no block due date - * @param string $itemProperties item properties - * @param string $feeAcknowledged Y/N if fee acknowledged - * @param string $noBlock Y/N if no blocking permitted - see specification - * @param string $thirdParty Y/N if third party renewals allowed - * @return string - * - * @see SIP2Client::parseRenewResponse() - */ - public function msgRenew( - $item = '', - $title = '', - $nbDateDue = '', - $itemProperties = '', - $feeAcknowledged = 'N', - $noBlock = 'N', - $thirdParty = 'N' - ) { - - $this->newMessage('29'); - $this->addFixedOption($thirdParty, 1); - $this->addFixedOption($noBlock, 1); - $this->addFixedOption($this->datestamp(), 18); - if ($nbDateDue != '') { - /* override default date due */ - $this->addFixedOption($this->datestamp($nbDateDue), 18); - } else { - /* send a blank date due to allow ACS to use default date due computed for item */ - $this->addFixedOption('', 18); - } - $this->addVarOption('AO', $this->institutionId); - $this->addVarOption('AA', $this->patron); - $this->addVarOption('AD', $this->patronpwd, true); - $this->addVarOption('AB', $item, true); - $this->addVarOption('AJ', $title, true); - $this->addVarOption('AC', $this->terminalPassword, true); - $this->addVarOption('CH', $itemProperties, true); - $this->addVarOption('BO', $feeAcknowledged, true); /* Y or N */ - - return $this->returnMessage(); - } - - /** - * This message is used to renew all items that the patron has checked out. The ACS should respond with a Renew All - * Response message. - * - * @param string $feeAcknowledged - * @return string - * - * @see SIP2Client::parseRenewAllResponse() - */ - public function msgRenewAll($feeAcknowledged = 'N') - { - $this->newMessage('65'); - $this->addVarOption('AO', $this->institutionId); - $this->addVarOption('AA', $this->patron); - $this->addVarOption('AD', $this->patronpwd, true); - $this->addVarOption('AC', $this->terminalPassword, true); - $this->addVarOption('BO', $feeAcknowledged, true); /* Y or N */ - - return $this->returnMessage(); - } - - /** - * Parse response from a Patron Status request - * @param string $response ACS response - * @return array with 'fixed' and 'variable' keys - * - * @see SIP2Client::msgPatronStatusRequest() - */ - public function parsePatronStatusResponse($response) - { - $result = []; - $result['fixed'] = - array( - 'PatronStatus' => substr($response, 2, 14), - 'Language' => substr($response, 16, 3), - 'TransactionDate' => substr($response, 19, 18), - ); - - $result['variable'] = $this->parseVariableData($response, 37); - return $result; - } - - /** - * Parse response from a Checkout request - * @param string $response ACS response - * @return array with 'fixed' and 'variable' keys - * - * @see SIP2Client::msgCheckout() - */ - public function parseCheckoutResponse($response) - { - $result = []; - $result['fixed'] = - array( - 'Ok' => substr($response, 2, 1), - 'RenewalOk' => substr($response, 3, 1), - 'Magnetic' => substr($response, 4, 1), - 'Desensitize' => substr($response, 5, 1), - 'TransactionDate' => substr($response, 6, 18), - ); - - $result['variable'] = $this->parseVariableData($response, 24); - return $result; - } - - /** - * Parse response from a Checkin request - * @param string $response ACS response - * @return array with 'fixed' and 'variable' keys - * - * @see SIP2Client::msgCheckin() - */ - public function parseCheckinResponse($response) - { - $result = []; - $result['fixed'] = - array( - 'Ok' => substr($response, 2, 1), - 'Resensitize' => substr($response, 3, 1), - 'Magnetic' => substr($response, 4, 1), - 'Alert' => substr($response, 5, 1), - 'TransactionDate' => substr($response, 6, 18), - ); - - $result['variable'] = $this->parseVariableData($response, 24); - return $result; - } - - /** - * Parse response from a SC Status request - * @param string $response ACS response - * @return array with 'fixed' and 'variable' keys - * - * @see SIP2Client::msgSCStatus() - */ - public function parseACSStatusResponse($response) - { - $result = []; - $result['fixed'] = - [ - 'Online' => substr($response, 2, 1), - // is Checkin by the SC allowed ? - 'Checkin' => substr($response, 3, 1), - // is Checkout by the SC allowed ? - 'Checkout' => substr($response, 4, 1), - // renewal allowed? */ - 'Renewal' => substr($response, 5, 1), - //is patron status updating by the SC allowed ? (status update ok) - 'PatronUpdate' => substr($response, 6, 1), - 'Offline' => substr($response, 7, 1), - 'Timeout' => substr($response, 8, 3), - 'Retries' => substr($response, 11, 3), - 'TransactionDate' => substr($response, 14, 18), - 'Protocol' => substr($response, 32, 4), - ]; - - $result['variable'] = $this->parseVariableData($response, 36); - return $result; - } - - /** - * Parse response from a Login request - * @param string $response ACS response - * @return array with 'fixed' and 'variable' keys - * - * @see SIP2Client::msgSCStatus() - */ - public function parseLoginResponse($response) - { - $result = []; - $result['fixed'] = - [ - 'Ok' => substr($response, 2, 1), - ]; - $result['variable'] = array(); - return $result; - } - - /** - * Parse response from a Patron Information request - * @param string $response ACS response - * @return array with 'fixed' and 'variable' keys - * - * @see SIP2Client::msgPatronInformation() - */ - public function parsePatronInfoResponse($response) - { - $result = []; - $result['fixed'] = - [ - 'PatronStatus' => substr($response, 2, 14), - 'Language' => substr($response, 16, 3), - 'TransactionDate' => substr($response, 19, 18), - 'HoldCount' => intval(substr($response, 37, 4)), - 'OverdueCount' => intval(substr($response, 41, 4)), - 'ChargedCount' => intval(substr($response, 45, 4)), - 'FineCount' => intval(substr($response, 49, 4)), - 'RecallCount' => intval(substr($response, 53, 4)), - 'UnavailableCount' => intval(substr($response, 57, 4)) - ]; - - $result['variable'] = $this->parseVariableData($response, 61); - return $result; - } - - /** - * Parse response from a End Session request - * @param string $response ACS response - * @return array with 'fixed' and 'variable' keys - * - * @see SIP2Client::msgPatronInformation() - */ - public function parseEndSessionResponse($response) - { - /* Response example: 36Y20080228 145537AOWOHLERS|AAX00000000|AY9AZF474 */ - $result = []; - $result['fixed'] = - [ - 'EndSession' => substr($response, 2, 1), - 'TransactionDate' => substr($response, 3, 18), - ]; - - - $result['variable'] = $this->parseVariableData($response, 21); - - return $result; - } - - /** - * Parse response from a Fee Paid request - * @param string $response ACS response - * @return array with 'fixed' and 'variable' keys - * - * @see SIP2Client::msgFeePaid() - */ - public function parseFeePaidResponse($response) - { - $result = []; - $result['fixed'] = - [ - 'PaymentAccepted' => substr($response, 2, 1), - 'TransactionDate' => substr($response, 3, 18), - ]; - - $result['variable'] = $this->parseVariableData($response, 21); - return $result; - } - - /** - * Parse response from a Item Information request - * @param string $response ACS response - * @return array with 'fixed' and 'variable' keys - * - * @see SIP2Client::msgItemInformation() - */ - public function parseItemInfoResponse($response) - { - $result = []; - $result['fixed'] = - [ - 'CirculationStatus' => intval(substr($response, 2, 2)), - 'SecurityMarker' => intval(substr($response, 4, 2)), - 'FeeType' => intval(substr($response, 6, 2)), - 'TransactionDate' => substr($response, 8, 18), - ]; - - $result['variable'] = $this->parseVariableData($response, 26); - - return $result; - } - - /** - * Parse response from a Item Status request - * @param string $response ACS response - * @return array with 'fixed' and 'variable' keys - * - * @see SIP2Client::msgItemStatus() - */ - public function parseItemStatusResponse($response) - { - $result = []; - $result['fixed'] = - [ - 'PropertiesOk' => substr($response, 2, 1), - 'TransactionDate' => substr($response, 3, 18), - ]; - - $result['variable'] = $this->parseVariableData($response, 21); - return $result; - } - - /** - * Parse response from a Patron Enable request - * @param string $response ACS response - * @return array with 'fixed' and 'variable' keys - * - * @see SIP2Client::msgPatronEnable() - */ - public function parsePatronEnableResponse($response) - { - $result = []; - $result['fixed'] = - [ - 'PatronStatus' => substr($response, 2, 14), - 'Language' => substr($response, 16, 3), - 'TransactionDate' => substr($response, 19, 18), - ]; - - $result['variable'] = $this->parseVariableData($response, 37); - return $result; - } - - /** - * Parse response from a Hold request - * @param string $response ACS response - * @return array with 'fixed' and 'variable' keys - * - * @see SIP2Client::msgHold() - */ - public function parseHoldResponse($response) - { - $result = []; - $result['fixed'] = - [ - 'Ok' => substr($response, 2, 1), - 'available' => substr($response, 3, 1), - 'TransactionDate' => substr($response, 4, 18) - ]; - - //expiration date is optional an indicated by BW - $variableOffset = 22; - if (substr($response, 22, 2) === 'BW') { - $result['fixed']['ExpirationDate'] = substr($response, 24, 18); - $variableOffset = 42; - } - - $result['variable'] = $this->parseVariableData($response, $variableOffset); - - return $result; - } - - - /** - * Parse response from a Renew request - * @param string $response ACS response - * @return array with 'fixed' and 'variable' keys - * - * @see SIP2Client::msgRenew() - */ - public function parseRenewResponse($response) - { - /* Response Example: - 300NUU20080228 222232AOWOHLERS|AAX00000241|ABM02400028262| - AJFolksongs of Britain and Ireland|AH5/23/2008,23:59|CH| - AFOverride required to exceed renewal limit.|AY1AZCDA5 - */ - $result = []; - $result['fixed'] = - [ - 'Ok' => substr($response, 2, 1), - 'RenewalOk' => substr($response, 3, 1), - 'Magnetic' => substr($response, 4, 1), - 'Desensitize' => substr($response, 5, 1), - 'TransactionDate' => substr($response, 6, 18), - ]; - - - $result['variable'] = $this->parseVariableData($response, 24); - - return $result; - } - - /** - * Parse response from a Renew All request - * @param string $response ACS response - * @return array with 'fixed' and 'variable' keys - * - * @see SIP2Client::msgRenewAll() - */ - public function parseRenewAllResponse($response) - { - $result = []; - $result['fixed'] = - [ - 'Ok' => substr($response, 2, 1), - 'Renewed' => substr($response, 3, 4), - 'Unrenewed' => substr($response, 7, 4), - 'TransactionDate' => substr($response, 11, 18), - ]; - - - $result['variable'] = $this->parseVariableData($response, 29); - - return $result; - } - - /** - * Send a request to ACS and obtain raw response - * - * @param string $message generated by one of the msg*() methods - * @return bool|string false in event of failure, otherwise a string containing ACS response - */ - public function getMessage($message) - { - /* sends the current message, and gets the response */ $result = ''; $terminator = ''; - $this->logger->debug('SIP2: Sending SIP2 request...'); + $message = $request->getMessageString(); + + $this->logger->debug('SIP2: Sending SIP2 request '.trim($message)); $this->socket->write($message); $this->logger->debug('SIP2: Request Sent, Reading response'); @@ -1028,43 +172,45 @@ public function getMessage($message) $this->logger->info("SIP2: result={$result}"); - /* test message for CRC validity */ - if ($this->checkCRC($result)) { - /* reset the retry counter on successful send */ - $this->retry = 0; + // test message for CRC validity + if (SIP2Response::checkCRC($result)) { $this->logger->debug("SIP2: Message from ACS passed CRC check"); } else { - /* CRC check failed, request a resend */ - $this->retry++; - if ($this->retry < $this->maxretry) { - /* try again */ - $this->logger->warning("SIP2: Message failed CRC check, retrying ({$this->retry})"); - - $result = $this->getMessage($message); + //CRC check failed, we resend the request + if ($depth < $this->maxretry) { + $depth++; + $this->logger->warning("SIP2: Message failed CRC check, retry {$depth})"); + $result = $this->getRawResponse($request, $depth); } else { - /* give up */ - $this->logger->error("SIP2: Failed to get valid CRC after {$this->maxretry} retries."); - return false; + $errMsg="SIP2: Failed to get valid CRC after {$this->maxretry} retries."; + $this->logger->critical($errMsg); + throw new RuntimeException($errMsg); } } + return $result; } /** * Connect to ACS via SIP2 - * @return bool returns true if connection is established + * + * The $bind parameter can be useful where a machine which has multiple outbound connections and its important + * to control which one is used (normally because the remote SIP2 service is firewalled to particular IPs + * + * @param string $address ip:port of remote SIP2 service + * @param string|null $bind local ip to bind socket to + * @throws RuntimeException if connection cannot be established */ - public function connect() + public function connect($address, $bind = null) { - /* Socket Communications */ - $this->logger->debug("SIP2: --- BEGIN SIP communication ---"); - $address = $this->hostname . ':' . $this->port; + $this->logger->debug("SIP2Client: Attempting connection to $address"); $this->socket = $this->getSocketFactory()->createFromString($address, $scheme); try { - if (!empty($this->bindTo)) { - $this->socket->bind($this->bindTo); + if (!empty($bind)) { + $this->logger->debug("SIP2Client: binding socket to $bind"); + $this->socket->bind($bind); } $this->socket->connect($address); @@ -1072,11 +218,10 @@ public function connect() $this->socket->close(); $this->socket = null; $this->logger->error("SIP2Client: Failed to connect: " . $e->getMessage()); - return false; + throw new RuntimeException("Connection failure", 0, $e); } - $this->logger->debug("SIP2: --- SOCKET READY ---"); - return true; + $this->logger->debug("SIP2Client: connected"); } /** @@ -1087,135 +232,4 @@ public function disconnect() $this->socket->close(); $this->socket = null; } - - /* Core local utility functions */ - private function datestamp($timestamp = '') - { - /* generate a SIP2 compatible datestamp */ - /* From the spec: - * YYYYMMDDZZZZHHMMSS. - * All dates and times are expressed according to the ANSI standard X3.30 for date and X3.43 for time. - * The ZZZZ field should contain blanks (code $20) to represent local time. To represent universal time, - * a Z character(code $5A) should be put in the last (right hand) position of the ZZZZ field. - * To represent other time zones the appropriate character should be used; a Q character (code $51) - * should be put in the last (right hand) position of the ZZZZ field to represent Atlantic Standard Time. - * When possible local time is the preferred format. - */ - if ($timestamp != '') { - /* Generate a proper date time from the date provided */ - return date('Ymd His', $timestamp); - } else { - /* Current Date/Time */ - return date('Ymd His'); - } - } - - private function parseVariableData($response, $start) - { - - $result = array(); - $result['Raw'] = explode("|", substr($response, $start, -7)); - foreach ($result['Raw'] as $item) { - $field = substr($item, 0, 2); - $value = substr($item, 2); - /* SD returns some odd values on occasion, Unable to locate the purpose in spec, so I strip from - * the parsed array. Orig values will remain in ['raw'] element - */ - $clean = trim($value, "\x00..\x1F"); - if (trim($clean) <> '') { - $result[$field][] = $clean; - } - } - $result['AZ'][] = trim(substr($response, -5)); - - return ($result); - } - - private function crc($buf) - { - /* Calculate CRC */ - $sum = 0; - - $len = strlen($buf); - for ($n = 0; $n < $len; $n++) { - $sum = $sum + ord(substr($buf, $n, 1)); - } - - $crc = ($sum & 0xFFFF) * -1; - - /* 2008.03.15 - Fixed a bug that allowed the checksum to be larger then 4 digits */ - return substr(sprintf("%4X", $crc), -4, 4); - } - - private function getSeqNumber() - { - /* Get a sequence number for the AY field */ - /* valid numbers range 0-9 */ - $this->seq++; - if ($this->seq > 9) { - $this->seq = 0; - } - return ($this->seq); - } - - private function checkCRC($message) - { - /* test the received message's CRC by generating our own CRC from the message */ - $test = preg_split('/(.{4})$/', trim($message), 2, PREG_SPLIT_DELIM_CAPTURE); - - if ($this->crc($test[0]) == $test[1]) { - return true; - } else { - //echo "Expected SRC was ".$this->crc($test[0])." but found ".$test[1]."\n"; - return false; - } - } - - private function newMessage($code) - { - /* resets the msgBuild variable to the value of $code, and clears the flag for fixed messages */ - $this->noFixed = false; - $this->msgBuild = $code; - } - - private function addFixedOption($value, $len) - { - /* adds a fixed length option to the msgBuild IF no variable options have been added. */ - if ($this->noFixed) { - //@codeCoverageIgnoreStart - throw new \LogicException('Cannot add fixed options after variable options'); - //@codeCoverageIgnoreEnd - } - - $this->msgBuild .= sprintf("%{$len}s", substr($value, 0, $len)); - return true; - } - - private function addVarOption($field, $value, $optional = false) - { - /* adds a variable length option to the message, and also prevents adding additional fixed fields */ - if ($optional == true && $value == '') { - /* skipped */ - $this->logger->debug("SIP2: Skipping optional field {$field}"); - } else { - $this->noFixed = true; /* no more fixed for this message */ - $this->msgBuild .= $field . substr($value, 0, 255) . $this->fldTerminator; - } - return true; - } - - private function returnMessage($withSeq = true, $withCrc = true) - { - /* Finalizes the message and returns it. Message will remain in msgBuild until newMessage is called */ - if ($withSeq) { - $this->msgBuild .= 'AY' . $this->getSeqNumber(); - } - if ($withCrc) { - $this->msgBuild .= 'AZ'; - $this->msgBuild .= $this->crc($this->msgBuild); - } - $this->msgBuild .= $this->msgTerminator; - - return $this->msgBuild; - } } diff --git a/src/SIP2Message.php b/src/SIP2Message.php new file mode 100644 index 0000000..fe71ba8 --- /dev/null +++ b/src/SIP2Message.php @@ -0,0 +1,158 @@ +var[$name]); + } + + public function getVariable($varName) + { + $this->ensureVariableExists($varName); + return $this->var[$varName]['value'] ?? + $this->var[$varName]['default'] ?? + $this->handleMissing($varName); + } + + public function getAll() + { + $result=[]; + foreach ($this->var as $name => $data) { + $result[$name] = $this->getVariable($name); + } + return $result; + } + + public function setVariable($varName, $value) + { + $this->ensureVariableExists($varName); + + //check type... + $type = $this->var[$varName]['type'] ?? 'string'; + switch ($type) { + case 'timestamp': + $value = $this->datestamp($value); + break; + case 'array': + $value = is_array($value) ? $value : [$value]; + break; + } + + return $this->var[$varName]['value'] = $value; + } + + /** + * If $varName is defined as an array, this will append given value. Otherwise value is set as normal + * @param $varName + * @param $value + */ + public function addVariable($varName, $value) + { + $this->ensureVariableExists($varName); + $type = $this->var[$varName]['type'] ?? 'string'; + if (($type === 'array') && isset($this->var[$varName]['value'])) { + $this->var[$varName]['value'][] = $value; + } else { + $this->setVariable($varName, $value); + } + } + + + /** + * Get current timestamp, which can be override with setTimestamp for testing + * @return int + */ + public function getTimestamp() + { + return $this->timestamp ?? time(); + } + + /** + * Sets current timestamp, which is useful for creating predictable tests + * @param $timestamp + */ + public function setTimestamp($timestamp) + { + $this->timestamp = $timestamp; + } + + protected function datestamp($timestamp = '') + { + /* generate a SIP2 compatible datestamp */ + /* From the spec: + * YYYYMMDDZZZZHHMMSS. + * All dates and times are expressed according to the ANSI standard X3.30 for date and X3.43 for time. + * The ZZZZ field should contain blanks (code $20) to represent local time. To represent universal time, + * a Z character(code $5A) should be put in the last (right hand) position of the ZZZZ field. + * To represent other time zones the appropriate character should be used; a Q character (code $51) + * should be put in the last (right hand) position of the ZZZZ field to represent Atlantic Standard Time. + * When possible local time is the preferred format. + */ + if ($timestamp != '') { + /* Generate a proper date time from the date provided */ + return date('Ymd His', $timestamp); + } else { + /* Current Date/Time */ + return date('Ymd His', $this->getTimestamp()); + } + } + + protected function ensureVariableExists($name) + { + if (!isset($this->var[$name])) { + throw new LogicException(get_class($this) . ' has no ' . $name . ' member'); + } + } + + protected function handleMissing($varName) + { + throw new LogicException(get_class($this) . '::set' . $varName . ' must be called'); + } + + public function __call($name, $arguments) + { + if (!preg_match('/^(get|set)(.+)$/', $name, $match)) { + throw new LogicException(get_class($this) . ' has no ' . $name . ' method'); + } + $varName = $match[2]; + + //get? + if ($match[1] === 'get') { + return $this->getVariable($varName); + } + //set + $this->setVariable($varName, $arguments[0]); + return $this; + } +} diff --git a/tests/ACSResendTest.php b/tests/ACSResendTest.php deleted file mode 100644 index bc14405..0000000 --- a/tests/ACSResendTest.php +++ /dev/null @@ -1,24 +0,0 @@ -makeResponse("96")]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - $client->connect(); - - $msg = $client->msgRequestACSResend(); - $this->assertEquals("97AZFEF5", trim($msg)); - } -} diff --git a/tests/AbstractSIP2ClientTest.php b/tests/AbstractSIP2ClientTest.php index 9a00e0f..b992560 100644 --- a/tests/AbstractSIP2ClientTest.php +++ b/tests/AbstractSIP2ClientTest.php @@ -2,6 +2,7 @@ namespace lordelph\SIP2; +use lordelph\SIP2\Request\SIP2Request; use Prophecy\Argument; /** @@ -12,6 +13,11 @@ */ abstract class AbstractSIP2ClientTest extends \PHPUnit\Framework\TestCase { + public function setUp() + { + SIP2Request::resetSequence(); + } + /** * Make a valid response by adding sequence number and CRC * @param $str diff --git a/tests/BindingTest.php b/tests/BindingTest.php deleted file mode 100644 index 9a5babe..0000000 --- a/tests/BindingTest.php +++ /dev/null @@ -1,44 +0,0 @@ -bindTo = '1.2.3.4'; - $client->setSocketFactory($this->createBindingTestMockSIP2Server()); - - $ok = $client->connect(); - $this->assertTrue($ok); - } - - /** - * This provides a socket factory which will verify the bind method is called - * @return \Socket\Raw\Factory - */ - protected function createBindingTestMockSIP2Server() - { - $socket = $this->prophesize(\Socket\Raw\Socket::class); - $socket->connect(Argument::type('string'))->willReturn(true); - - //we verify bind gets called... - $socket->bind(Argument::type('string'))->shouldBeCalled()->willReturn(true); - - //our factory will always fail to connect... - $factory = $this->prophesize(\Socket\Raw\Factory::class); - $factory->createFromString( - Argument::type('string'), - Argument::any() - )->willReturn($socket->reveal()); - - return $factory->reveal(); - } -} diff --git a/tests/BlockPatronTest.php b/tests/BlockPatronTest.php deleted file mode 100644 index a16b051..0000000 --- a/tests/BlockPatronTest.php +++ /dev/null @@ -1,41 +0,0 @@ -makeResponse("24". - "xxxYYYYxxxYYYY". - "ENG". - "20180711 185645". - "AO1234|". - "AApatron|". - "AEJoe|". - "BLY|". - "CQY|". - "BHGBP|". - "BV1.23|". - "AFmessage|". - "AGprint|") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgBlockPatron('Blocked', 'Y'); - $response = $client->getMessage($msg); - - //no need to fully test this response as other tests do it - $info = $client->parsePatronStatusResponse($response); - $this->assertFixedMetadata('xxxYYYYxxxYYYY', $info, 'PatronStatus'); - $this->assertFixedMetadata('ENG', $info, 'Language'); - $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); - } -} diff --git a/tests/CRCFailureTest.php b/tests/CRCFailureTest.php deleted file mode 100644 index bee3e23..0000000 --- a/tests/CRCFailureTest.php +++ /dev/null @@ -1,54 +0,0 @@ -makeResponse("941") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgLogin('username', 'password'); - $response = $client->getMessage($msg); - - $info = $client->parseLoginResponse($response); - $this->assertFixedMetadata('1', $info, 'Ok'); - } - - public function testCRCFailureAbort() - { - //our mock socket will return these responses in sequence after each write() to the socket - //here we simulate a continued failure to provide a valid response, leading us to abort after - //3 retries - $responses = [ - "940AY0AZ1234\r", - "940AY0AZ1234\r", - "940AY0AZ1234\r", - "940AY0AZ1234\r", - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgLogin('username', 'password'); - $response = $client->getMessage($msg); - $this->assertFalse($response); - } -} diff --git a/tests/CRCTest.php b/tests/CRCTest.php deleted file mode 100644 index 80f5f8c..0000000 --- a/tests/CRCTest.php +++ /dev/null @@ -1,39 +0,0 @@ -assertEquals('EB80', $this->invokeMethod($client, 'crc', [$in])); - - $in = '09N20160419 12171320160419 121713APReading Room 1|AO830|AB830$28170815|AC|AY2AZ'; - $this->assertEquals('EB7C', $this->invokeMethod($client, 'crc', [$in])); - } - - /** - * Call protected/private method of a class. - * - * @param object &$object Instantiated object that we will run method on. - * @param string $methodName Method name to call - * @param array $parameters Array of parameters to pass into method. - * - * @return mixed Method return. - * @throws \ReflectionException - */ - private function invokeMethod(&$object, $methodName, array $parameters = []) - { - $reflection = new \ReflectionClass(get_class($object)); - $method = $reflection->getMethod($methodName); - $method->setAccessible(true); - - return $method->invokeArgs($object, $parameters); - } -} diff --git a/tests/CheckinTest.php b/tests/CheckinTest.php deleted file mode 100644 index b9c6920..0000000 --- a/tests/CheckinTest.php +++ /dev/null @@ -1,97 +0,0 @@ -makeResponse("12" . - "1" . - "Y" . - "U" . - "N" . - "20180711 185645" . - "AO1234|" . - "ABitem|" . - "AQloc|" . - "AJtitle|" . - "CLsort|" . - "AApatron|" . - "CKmda|" . - "CHprop|" . - "AFmessage|" . - "AGprint|") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgCheckin( - 'mybook', - strtotime('2018-07-11 11:22:33'), - 'loc', - 'prop', - 'Y', - 'N' - ); - $response = $client->getMessage($msg); - - $info = $client->parseCheckinResponse($response); - - $this->assertFixedMetadata('1', $info, 'Ok'); - $this->assertFixedMetadata('Y', $info, 'Resensitize'); - $this->assertFixedMetadata('U', $info, 'Magnetic'); - $this->assertFixedMetadata('N', $info, 'Alert'); - $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); - - $this->assertVariableMetadata('1234', $info, 'AO'); - $this->assertVariableMetadata('item', $info, 'AB'); - $this->assertVariableMetadata('loc', $info, 'AQ'); - $this->assertVariableMetadata('title', $info, 'AJ'); - $this->assertVariableMetadata('sort', $info, 'CL'); - $this->assertVariableMetadata('patron', $info, 'AA'); - $this->assertVariableMetadata('mda', $info, 'CK'); - $this->assertVariableMetadata('prop', $info, 'CH'); - $this->assertVariableMetadata('message', $info, 'AF'); - $this->assertVariableMetadata('print', $info, 'AG'); - } - - public function testExampleOLIBCheckin() - { - //here we mock an example response from the OCLC OLIB SIP server - //http://www.oclc.org/support/help/olib/900/Content/System/Supported%20SIP2%20Messages.htm#11 - //Note that the example gives the CRC as E777 but we calculate it as E6C0 - $responses = [ - $this->makeResponse("101YUN20110217 075306". - "AOMAIN|AB111111|AQ|AJThe 7 pillars of wisdom|AAJSMITH|CK001|") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgCheckin('mybook', '', '', 'prop', 'Y', 'N'); - $response = $client->getMessage($msg); - - $info = $client->parseCheckinResponse($response); - - $this->assertFixedMetadata('1', $info, 'Ok'); - $this->assertFixedMetadata('Y', $info, 'Resensitize'); - $this->assertFixedMetadata('U', $info, 'Magnetic'); - $this->assertFixedMetadata('N', $info, 'Alert'); - $this->assertFixedMetadata('20110217 075306', $info, 'TransactionDate'); - - $this->assertVariableMetadata('MAIN', $info, 'AO'); - $this->assertVariableMetadata('111111', $info, 'AB'); - $this->assertVariableMetadata('The 7 pillars of wisdom', $info, 'AJ'); - $this->assertVariableMetadata('JSMITH', $info, 'AA'); - $this->assertVariableMetadata('001', $info, 'CK'); - } -} diff --git a/tests/CheckoutTest.php b/tests/CheckoutTest.php deleted file mode 100644 index cb7ad74..0000000 --- a/tests/CheckoutTest.php +++ /dev/null @@ -1,107 +0,0 @@ -makeResponse("12" . - "1" . - "Y" . - "N" . - "Y" . - "20180711 185645" . - "AO1234|" . - "AApatron|" . - "ABitem|" . - "AJtitle|" . - "AH20180711 185645|" . - "BT01|" . - "CIN|" . - "BHGBP|" . - "BV1.23|" . - "CKmda|" . - "CHprop|" . - "BKxyz|" . - "AFmessage|" . - "AGprint|") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgCheckout( - 'mybook', - strtotime('2018-07-11 11:22:33'), - 'N', - 'prop', - 'Y', - 'N', - 'N' - ); - $response = $client->getMessage($msg); - - $info = $client->parseCheckoutResponse($response); - - $this->assertFixedMetadata('1', $info, 'Ok'); - $this->assertFixedMetadata('Y', $info, 'RenewalOk'); - $this->assertFixedMetadata('N', $info, 'Magnetic'); - $this->assertFixedMetadata('Y', $info, 'Desensitize'); - $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); - - $this->assertVariableMetadata('1234', $info, 'AO'); - $this->assertVariableMetadata('patron', $info, 'AA'); - $this->assertVariableMetadata('item', $info, 'AB'); - $this->assertVariableMetadata('title', $info, 'AJ'); - $this->assertVariableMetadata('20180711 185645', $info, 'AH'); - $this->assertVariableMetadata('01', $info, 'BT'); - $this->assertVariableMetadata('N', $info, 'CI'); - $this->assertVariableMetadata('GBP', $info, 'BH'); - $this->assertVariableMetadata('1.23', $info, 'BV'); - $this->assertVariableMetadata('mda', $info, 'CK'); - $this->assertVariableMetadata('prop', $info, 'CH'); - $this->assertVariableMetadata('xyz', $info, 'BK'); - $this->assertVariableMetadata('message', $info, 'AF'); - $this->assertVariableMetadata('print', $info, 'AG'); - } - - public function testExampleOLIBCheckout() - { - //here we mock an example response from the OCLC OLIB SIP server - //http://www.oclc.org/support/help/olib/900/Content/System/Supported%20SIP2%20Messages.htm#11 - //Note that the example gives the CRC as DC91 but we calculate it as DD61 - $responses = [ - $this->makeResponse("121NUY20101014 121215AOMAIN|AH20101104 120000|AAJSMITH|AB111111|" . - "AJMarmalade and jam making for dummies|BV1.30|AF|") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgCheckout('mybook', '', 'N', 'prop', 'Y', 'N', 'N'); - $response = $client->getMessage($msg); - - $info = $client->parseCheckoutResponse($response); - - $this->assertFixedMetadata('1', $info, 'Ok'); - $this->assertFixedMetadata('N', $info, 'RenewalOk'); - $this->assertFixedMetadata('U', $info, 'Magnetic'); - $this->assertFixedMetadata('Y', $info, 'Desensitize'); - $this->assertFixedMetadata('20101014 121215', $info, 'TransactionDate'); - - $this->assertVariableMetadata('MAIN', $info, 'AO'); - $this->assertVariableMetadata('20101104 120000', $info, 'AH'); - $this->assertVariableMetadata('JSMITH', $info, 'AA'); - $this->assertVariableMetadata('111111', $info, 'AB'); - $this->assertVariableMetadata('Marmalade and jam making for dummies', $info, 'AJ'); - $this->assertVariableMetadata('1.30', $info, 'BV'); - } -} diff --git a/tests/ConnectionFailureTest.php b/tests/ConnectionFailureTest.php deleted file mode 100644 index d57b45a..0000000 --- a/tests/ConnectionFailureTest.php +++ /dev/null @@ -1,44 +0,0 @@ -setSocketFactory($this->createUnconnectableMockSIP2Server()); - - $ok = $client->connect(); - $this->assertFalse($ok); - } - - /** - * This provides a socket factory which will always fail to connect - * @return \Socket\Raw\Factory - */ - protected function createUnconnectableMockSIP2Server() - { - $socket = $this->prophesize(\Socket\Raw\Socket::class); - $socket->connect(Argument::type('string'))->will(function () { - throw new \Exception('Test connection failure'); - }); - - $socket->close()->willReturn(true); - - //our factory will always fail to connect... - $factory = $this->prophesize(\Socket\Raw\Factory::class); - $factory->createFromString( - Argument::type('string'), - Argument::any() - )->willReturn($socket->reveal()); - - return $factory->reveal(); - } -} diff --git a/tests/EndPatronSessionTest.php b/tests/EndPatronSessionTest.php deleted file mode 100644 index 2ffb398..0000000 --- a/tests/EndPatronSessionTest.php +++ /dev/null @@ -1,41 +0,0 @@ -makeResponse("36". - "Y". - "20180711 185645". - "AO1234|". - "AApatron|". - "AEJoe|". - "AFmessage|". - "AGprint|") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgEndPatronSession(); - $response = $client->getMessage($msg); - - $info = $client->parseEndSessionResponse($response); - - $this->assertFixedMetadata('Y', $info, 'EndSession'); - $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); - - $this->assertVariableMetadata('1234', $info, 'AO'); - $this->assertVariableMetadata('patron', $info, 'AA'); - $this->assertVariableMetadata('Joe', $info, 'AE'); - $this->assertVariableMetadata('message', $info, 'AF'); - $this->assertVariableMetadata('print', $info, 'AG'); - } -} diff --git a/tests/FeePaidTest.php b/tests/FeePaidTest.php deleted file mode 100644 index fceca54..0000000 --- a/tests/FeePaidTest.php +++ /dev/null @@ -1,53 +0,0 @@ -makeResponse("36". - "Y". - "20180711 185645". - "AO1234|". - "AApatron|". - "BK5555|". - "AFmessage|". - "AGprint|") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgFeePaid(4, 0, '1.30', 'GBP', 'xxx', 'yyy'); - $response = $client->getMessage($msg); - - $info = $client->parseFeePaidResponse($response); - - $this->assertFixedMetadata('Y', $info, 'PaymentAccepted'); - $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); - - $this->assertVariableMetadata('1234', $info, 'AO'); - $this->assertVariableMetadata('patron', $info, 'AA'); - $this->assertVariableMetadata('5555', $info, 'BK'); - $this->assertVariableMetadata('message', $info, 'AF'); - $this->assertVariableMetadata('print', $info, 'AG'); - } - - public function testBadFeeType() - { - $client = new SIP2Client; - $this->assertFalse($client->msgFeePaid(100, 0, '1.30')); - } - - public function testBadPaymentType() - { - $client = new SIP2Client; - $this->assertFalse($client->msgFeePaid(2, 100, '1.30')); - } -} diff --git a/tests/HoldTest.php b/tests/HoldTest.php deleted file mode 100644 index 2482230..0000000 --- a/tests/HoldTest.php +++ /dev/null @@ -1,52 +0,0 @@ -makeResponse("161Y20180711 185645BW20180711 185645" . - "BR1|BSLibrary|AO123|AApatron|ABitem|AJtitle|AFthankyou|AGprint|") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgHold('+', strtotime('2018-07-11 11:22:33'), 1, 'Item', 'Title', 'N', 'Loc'); - $response = $client->getMessage($msg); - - $info = $client->parseHoldResponse($response); - - $this->assertFixedMetadata('1', $info, 'Ok'); - $this->assertFixedMetadata('Y', $info, 'available'); - $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); - $this->assertFixedMetadata('20180711 185645', $info, 'ExpirationDate'); - - $this->assertVariableMetadata('1', $info, 'BR'); - $this->assertVariableMetadata('Library', $info, 'BS'); - $this->assertVariableMetadata('123', $info, 'AO'); - $this->assertVariableMetadata('patron', $info, 'AA'); - $this->assertVariableMetadata('item', $info, 'AB'); - $this->assertVariableMetadata('title', $info, 'AJ'); - $this->assertVariableMetadata('thankyou', $info, 'AF'); - $this->assertVariableMetadata('print', $info, 'AG'); - } - - public function testBadMode() - { - $client = new SIP2Client; - $this->assertFalse($client->msgHold('X')); - } - - public function testBadHoldType() - { - $client = new SIP2Client; - $this->assertFalse($client->msgHold('+', '', 999)); - } -} diff --git a/tests/ItemInformationTest.php b/tests/ItemInformationTest.php deleted file mode 100644 index 38da626..0000000 --- a/tests/ItemInformationTest.php +++ /dev/null @@ -1,75 +0,0 @@ -makeResponse("18" . - "01" . - "02" . - "03" . - "20180711 185645" . - "CF3|" . - "AH20180711 185645|" . - "CJ20180711 185645|" . - "CM20180711 185645|" . - "AB1565921879|" . - "AJPerl 5 desktop reference|" . - "BGBR1|" . - "BHGBP|" . - "BV1.23|" . - "CK001|" . - "AQBR2|" . - "APBR3|" . - "CHprop|" . - "AFmessage|" . - "AGprint|") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgItemInformation('item'); - $response = $client->getMessage($msg); - - $info = $client->parseItemInfoResponse($response); - - $this->assertFixedMetadata('01', $info, 'CirculationStatus'); - $this->assertFixedMetadata('02', $info, 'SecurityMarker'); - $this->assertFixedMetadata('03', $info, 'FeeType'); - $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); - - $this->assertVariableMetadata('3', $info, 'CF'); - $this->assertVariableMetadata('20180711 185645', $info, 'AH'); - $this->assertVariableMetadata('20180711 185645', $info, 'CJ'); - $this->assertVariableMetadata('20180711 185645', $info, 'CM'); - $this->assertVariableMetadata('1565921879', $info, 'AB'); - $this->assertVariableMetadata('Perl 5 desktop reference', $info, 'AJ'); - $this->assertVariableMetadata('BR1', $info, 'BG'); - $this->assertVariableMetadata('GBP', $info, 'BH'); - $this->assertVariableMetadata('1.23', $info, 'BV'); - $this->assertVariableMetadata('001', $info, 'CK'); - $this->assertVariableMetadata('BR2', $info, 'AQ'); - $this->assertVariableMetadata('BR3', $info, 'AP'); - $this->assertVariableMetadata('prop', $info, 'CH'); - $this->assertVariableMetadata('message', $info, 'AF'); - $this->assertVariableMetadata('print', $info, 'AG'); - } -} diff --git a/tests/ItemStatusTest.php b/tests/ItemStatusTest.php deleted file mode 100644 index b40b29c..0000000 --- a/tests/ItemStatusTest.php +++ /dev/null @@ -1,41 +0,0 @@ -makeResponse("20" . - "1" . - "20180711 185645" . - "AB1565921879|" . - "AJPerl 5 desktop reference|" . - "CHprop|" . - "AFmessage|" . - "AGprint|") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgItemStatus('item', 'prop'); - $response = $client->getMessage($msg); - - $info = $client->parseItemStatusResponse($response); - - $this->assertFixedMetadata('1', $info, 'PropertiesOk'); - $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); - - $this->assertVariableMetadata('1565921879', $info, 'AB'); - $this->assertVariableMetadata('Perl 5 desktop reference', $info, 'AJ'); - $this->assertVariableMetadata('prop', $info, 'CH'); - $this->assertVariableMetadata('message', $info, 'AF'); - $this->assertVariableMetadata('print', $info, 'AG'); - } -} diff --git a/tests/LoginTest.php b/tests/LoginTest.php deleted file mode 100644 index a223fb2..0000000 --- a/tests/LoginTest.php +++ /dev/null @@ -1,28 +0,0 @@ -makeResponse("941") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgLogin('username', 'password'); - $response = $client->getMessage($msg); - - $info = $client->parseLoginResponse($response); - $this->assertFixedMetadata('1', $info, 'Ok'); - - $client->disconnect(); - } -} diff --git a/tests/PatronEnableTest.php b/tests/PatronEnableTest.php deleted file mode 100644 index c860ee1..0000000 --- a/tests/PatronEnableTest.php +++ /dev/null @@ -1,47 +0,0 @@ -makeResponse("26" . - "XXXXyyyXXXXyyy" . - "ENG". - "20180711 185645" . - "AOinstitution|" . - "AApatron|" . - "AEJoe Tester|" . - "BLY|". - "CQY|". - "AFmessage|" . - "AGprint|") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgPatronEnable(); - $response = $client->getMessage($msg); - - $info = $client->parsePatronEnableResponse($response); - - $this->assertFixedMetadata('XXXXyyyXXXXyyy', $info, 'PatronStatus'); - $this->assertFixedMetadata('ENG', $info, 'Language'); - $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); - - $this->assertVariableMetadata('institution', $info, 'AO'); - $this->assertVariableMetadata('patron', $info, 'AA'); - $this->assertVariableMetadata('Joe Tester', $info, 'AE'); - $this->assertVariableMetadata('Y', $info, 'BL'); - $this->assertVariableMetadata('Y', $info, 'CQ'); - $this->assertVariableMetadata('message', $info, 'AF'); - $this->assertVariableMetadata('print', $info, 'AG'); - } -} diff --git a/tests/PatronInfoTest.php b/tests/PatronInfoTest.php deleted file mode 100644 index ab5bfe3..0000000 --- a/tests/PatronInfoTest.php +++ /dev/null @@ -1,58 +0,0 @@ -makeResponse("64 00020180711 185645000000000010000000000009" . - "AOExample City Library|AA1381380|AEMr Joe Tester|BZ9999|CA9999|CB9999|BLY|CQY|BV0.00|" . - "BEjoe.tester@example.com|") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $ok = $client->connect(); - $this->assertTrue($ok); - - $msg = $client->msgPatronInformation('none'); - $this->assertNotEmpty($msg); - - $response = $client->getMessage($msg); - $this->assertNotEmpty($response); - - $info = $client->parsePatronInfoResponse($response); - $this->assertArrayHasKey('fixed', $info); - $this->assertArrayHasKey('variable', $info); - $this->assertArrayHasKey('Raw', $info['variable']); - - //check the fixed data - $this->assertFixedMetadata(' ', $info, 'PatronStatus'); - $this->assertFixedMetadata('000', $info, 'Language'); - $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); - $this->assertFixedMetadata(0, $info, 'HoldCount'); - $this->assertFixedMetadata(10, $info, 'ChargedCount'); - $this->assertFixedMetadata(0, $info, 'FineCount'); - $this->assertFixedMetadata(0, $info, 'RecallCount'); - $this->assertFixedMetadata(9, $info, 'UnavailableCount'); - - //check variable data - $this->assertVariableMetadata('Example City Library', $info, 'AO'); - $this->assertVariableMetadata('1381380', $info, 'AA'); - $this->assertVariableMetadata('Mr Joe Tester', $info, 'AE'); - $this->assertVariableMetadata('9999', $info, 'BZ'); - $this->assertVariableMetadata('9999', $info, 'CA'); - $this->assertVariableMetadata('9999', $info, 'CB'); - $this->assertVariableMetadata('Y', $info, 'BL'); - $this->assertVariableMetadata('Y', $info, 'CQ'); - $this->assertVariableMetadata('0.00', $info, 'BV'); - $this->assertVariableMetadata('joe.tester@example.com', $info, 'BE'); - $this->assertVariableMetadata('0', $info, 'AY'); - $this->assertVariableMetadata("CF82", $info, 'AZ'); - } -} diff --git a/tests/PatronStatusTest.php b/tests/PatronStatusTest.php deleted file mode 100644 index 998081a..0000000 --- a/tests/PatronStatusTest.php +++ /dev/null @@ -1,51 +0,0 @@ -makeResponse("24". - "xxxYYYYxxxYYYY". - "ENG". - "20180711 185645". - "AO1234|". - "AApatron|". - "AEJoe|". - "BLY|". - "CQY|". - "BHGBP|". - "BV1.23|". - "AFmessage|". - "AGprint|") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgPatronStatusRequest(); - $response = $client->getMessage($msg); - - $info = $client->parsePatronStatusResponse($response); - - $this->assertFixedMetadata('xxxYYYYxxxYYYY', $info, 'PatronStatus'); - $this->assertFixedMetadata('ENG', $info, 'Language'); - $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); - - $this->assertVariableMetadata('1234', $info, 'AO'); - $this->assertVariableMetadata('patron', $info, 'AA'); - $this->assertVariableMetadata('Joe', $info, 'AE'); - $this->assertVariableMetadata('Y', $info, 'BL'); - $this->assertVariableMetadata('Y', $info, 'CQ'); - $this->assertVariableMetadata('GBP', $info, 'BH'); - $this->assertVariableMetadata('1.23', $info, 'BV'); - $this->assertVariableMetadata('message', $info, 'AF'); - $this->assertVariableMetadata('print', $info, 'AG'); - } -} diff --git a/tests/RenewAllTest.php b/tests/RenewAllTest.php deleted file mode 100644 index c4429f9..0000000 --- a/tests/RenewAllTest.php +++ /dev/null @@ -1,50 +0,0 @@ -makeResponse("66" . - "1" . - "0002". - "0003". - "20180711 185645" . - "AOinstitution|" . - "BMbook 1|". - "BMbook 2|". - "BNbook 3|". - "BNbook 4|". - "BNbook 5|". - "AFmessage|" . - "AGprint|") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgRenewAll('Y'); - $response = $client->getMessage($msg); - - $info = $client->parseRenewAllResponse($response); - - $this->assertFixedMetadata('1', $info, 'Ok'); - $this->assertFixedMetadata('0002', $info, 'Renewed'); - $this->assertFixedMetadata('0003', $info, 'Unrenewed'); - $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); - - $this->assertVariableMetadata('institution', $info, 'AO'); - - $this->assertVariableMetadata(['book 1', 'book 2'], $info, 'BM'); - $this->assertVariableMetadata(['book 3', 'book 4', 'book 5'], $info, 'BN'); - - $this->assertVariableMetadata('message', $info, 'AF'); - $this->assertVariableMetadata('print', $info, 'AG'); - } -} diff --git a/tests/RenewTest.php b/tests/RenewTest.php deleted file mode 100644 index 403948c..0000000 --- a/tests/RenewTest.php +++ /dev/null @@ -1,77 +0,0 @@ -makeResponse("30" . - "1" . - "Y". - "U". - "N". - "20180711 185645" . - "AOinstitution|" . - "AApatron|" . - "AB1565921879|" . - "AJPerl 5 desktop reference|" . - "AH20180711 185645|" . - "BT01|". - "CIY|". - "BHGBP|". - "BV1.23|". - "CK001|". - "CHprop|". - "BK1234|". - "AFmessage|" . - "AGprint|") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgRenew( - '1565921879', - 'Perl 5 desktop reference', - strtotime('2018-07-11 11:22:33'), - 'prop', - 'N', - 'N', - 'N' - ); - $response = $client->getMessage($msg); - - $info = $client->parseRenewResponse($response); - - $this->assertFixedMetadata('1', $info, 'Ok'); - $this->assertFixedMetadata('Y', $info, 'RenewalOk'); - $this->assertFixedMetadata('U', $info, 'Magnetic'); - $this->assertFixedMetadata('N', $info, 'Desensitize'); - $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); - - $this->assertVariableMetadata('institution', $info, 'AO'); - $this->assertVariableMetadata('patron', $info, 'AA'); - $this->assertVariableMetadata('1565921879', $info, 'AB'); - $this->assertVariableMetadata('Perl 5 desktop reference', $info, 'AJ'); - $this->assertVariableMetadata('20180711 185645', $info, 'AH'); - $this->assertVariableMetadata('01', $info, 'BT'); - $this->assertVariableMetadata('Y', $info, 'CI'); - $this->assertVariableMetadata('GBP', $info, 'BH'); - $this->assertVariableMetadata('1.23', $info, 'BV'); - $this->assertVariableMetadata('001', $info, 'CK'); - $this->assertVariableMetadata('prop', $info, 'CH'); - $this->assertVariableMetadata('1234', $info, 'BK'); - $this->assertVariableMetadata('message', $info, 'AF'); - $this->assertVariableMetadata('print', $info, 'AG'); - - //for coverage, build message with empty date - $msg = $client->msgRenew('123', 'Test Book', ''); - $this->assertNotEmpty($msg); - } -} diff --git a/tests/Request/BlockPatronRequestTest.php b/tests/Request/BlockPatronRequestTest.php new file mode 100644 index 0000000..d830fd4 --- /dev/null +++ b/tests/Request/BlockPatronRequestTest.php @@ -0,0 +1,22 @@ +setTimestamp(strtotime('2018-07-23 09:46:11')); + + $req->setInstitutionId('Banjo'); + $req->setPatronIdentifier('paul'); + $req->setMessage('You are blocked'); + + $msg = $req->getMessageString(); + + $this->assertEquals("01N20180723 094611AOBanjo|ALYou are blocked|AApaul|AC|AY0AZED68\r\n", $msg); + } +} diff --git a/tests/Request/CheckInRequestTest.php b/tests/Request/CheckInRequestTest.php new file mode 100644 index 0000000..218bd9d --- /dev/null +++ b/tests/Request/CheckInRequestTest.php @@ -0,0 +1,26 @@ +setTimestamp(strtotime('2018-07-23 09:46:11')); + + $req->setItemIdentifier('1234'); + $req->setInstitutionId('Banjo'); + $req->setItemLocation('paul'); + $req->setItemReturnDate(strtotime('2018-08-01 10:00:00')); + + $msg = $req->getMessageString(); + + $this->assertEquals( + "09N20180723 09461120180801 100000APpaul|AOBanjo|AB1234|AC|BIN|AY0AZED90\r\n", + $msg + ); + } +} diff --git a/tests/Request/CheckOutRequestTest.php b/tests/Request/CheckOutRequestTest.php new file mode 100644 index 0000000..1e67772 --- /dev/null +++ b/tests/Request/CheckOutRequestTest.php @@ -0,0 +1,27 @@ +setTimestamp(strtotime('2018-07-23 09:46:11')); + + $req->setItemIdentifier('1234'); + $req->setInstitutionId('Banjo'); + $req->setPatronIdentifier('paul'); + $req->setPatronPassword('foo'); + + $msg = $req->getMessageString(); + + $this->assertEquals( + "11NN20180723 094611 ". + "AOBanjo|AApaul|AB1234|AC|ADfoo|BON|BIN|AY0AZEAAD\r\n", + $msg + ); + } +} diff --git a/tests/Request/EndPatronSessionRequestTest.php b/tests/Request/EndPatronSessionRequestTest.php new file mode 100644 index 0000000..10d47c9 --- /dev/null +++ b/tests/Request/EndPatronSessionRequestTest.php @@ -0,0 +1,21 @@ +setTimestamp(strtotime('2018-07-23 09:46:11')); + + $req->setInstitutionId('Banjo'); + $req->setPatronIdentifier('paul'); + + $msg = $req->getMessageString(); + + $this->assertEquals("3520180723 094611AOBanjo|AApaul|AY0AZF541\r\n", $msg); + } +} diff --git a/tests/Request/FeePaidRequestTest.php b/tests/Request/FeePaidRequestTest.php new file mode 100644 index 0000000..f6543f4 --- /dev/null +++ b/tests/Request/FeePaidRequestTest.php @@ -0,0 +1,26 @@ +setTimestamp(strtotime('2018-07-23 09:46:11')); + + $req->setFeeType('01'); + $req->setPaymentType('02'); + $req->setPaymentAmount('2.34'); + + $req->setInstitutionId('Banjo'); + $req->setPatronIdentifier('paul'); + $req->setPatronPassword('foo'); + + $msg = $req->getMessageString(); + + $this->assertEquals("3720180723 0946110102USDBV2.34|AOBanjo|AApaul|AC|ADfoo|AY0AZEE70\r\n", $msg); + } +} diff --git a/tests/Request/HoldRequestTest.php b/tests/Request/HoldRequestTest.php new file mode 100644 index 0000000..52fb897 --- /dev/null +++ b/tests/Request/HoldRequestTest.php @@ -0,0 +1,24 @@ +setTimestamp(strtotime('2018-07-23 09:46:11')); + + $req->setHoldMode(HoldRequest::MODE_ADD); + $req->setItemIdentifier('1234'); + $req->setInstitutionId('Banjo'); + $req->setPatronIdentifier('paul'); + $req->setPatronPassword('foo'); + + $msg = $req->getMessageString(); + + $this->assertEquals("15+20180723 094611AOBanjo|AApaul|ADfoo|AB1234|BON|AY0AZEFAF\r\n", $msg); + } +} diff --git a/tests/Request/ItemInformationRequestTest.php b/tests/Request/ItemInformationRequestTest.php new file mode 100644 index 0000000..f56802c --- /dev/null +++ b/tests/Request/ItemInformationRequestTest.php @@ -0,0 +1,22 @@ +setTimestamp(strtotime('2018-07-23 09:46:11')); + + $req->setItemIdentifier('1234'); + $req->setInstitutionId('Banjo'); + $req->setTerminalPassword('foo'); + + $msg = $req->getMessageString(); + + $this->assertEquals("1720180723 094611AOBanjo|AB1234|ACfoo|AY0AZF3E4\r\n", $msg); + } +} diff --git a/tests/Request/ItemStatusUpdateRequestTest.php b/tests/Request/ItemStatusUpdateRequestTest.php new file mode 100644 index 0000000..c9da03b --- /dev/null +++ b/tests/Request/ItemStatusUpdateRequestTest.php @@ -0,0 +1,23 @@ +setTimestamp(strtotime('2018-07-23 09:46:11')); + + $req->setItemIdentifier('1234'); + $req->setInstitutionId('Banjo'); + $req->setTerminalPassword('foo'); + $req->setItemProperties('xyz'); + + $msg = $req->getMessageString(); + + $this->assertEquals("1920180723 094611AOBanjo|AB1234|ACfoo|CHxyz|AY0AZF170\r\n", $msg); + } +} diff --git a/tests/Request/LoginRequestTest.php b/tests/Request/LoginRequestTest.php new file mode 100644 index 0000000..7920539 --- /dev/null +++ b/tests/Request/LoginRequestTest.php @@ -0,0 +1,21 @@ +setTimestamp(strtotime('2018-07-23 09:46:11')); + + $req->setSIPLogin('alice'); + $req->setSIPPassword('c4rr0ll'); + + $msg = $req->getMessageString(); + + $this->assertEquals("9300CNalice|COc4rr0ll|AY0AZF733\r\n", $msg); + } +} diff --git a/tests/Request/PatronEnableRequestTest.php b/tests/Request/PatronEnableRequestTest.php new file mode 100644 index 0000000..023fcbd --- /dev/null +++ b/tests/Request/PatronEnableRequestTest.php @@ -0,0 +1,23 @@ +setTimestamp(strtotime('2018-07-23 09:46:11')); + + $req->setInstitutionId('Banjo'); + $req->setTerminalPassword('bar'); + $req->setPatronIdentifier('paul'); + $req->setPatronPassword('foo'); + + $msg = $req->getMessageString(); + + $this->assertEquals("2520180723 094611AOBanjo|AApaul|ACbar|ADfoo|AY0AZF0C8\r\n", $msg); + } +} diff --git a/tests/Request/PatronInformationRequestTest.php b/tests/Request/PatronInformationRequestTest.php new file mode 100644 index 0000000..9d6aeff --- /dev/null +++ b/tests/Request/PatronInformationRequestTest.php @@ -0,0 +1,27 @@ +setTimestamp(strtotime('2018-07-23 09:46:11')); + + $req->setInstitutionId('Banjo'); + $req->setType('overdue'); + $req->setInstitutionId('Banjo'); + $req->setPatronIdentifier('paul'); + $req->setPatronPassword('foo'); + + $msg = $req->getMessageString(); + + $this->assertEquals( + "6300120180723 094611 Y AOBanjo|AApaul|AC|ADfoo|BP1|BQ5|AY0AZED6E\r\n", + $msg + ); + } +} diff --git a/tests/Request/PatronStatusRequestTest.php b/tests/Request/PatronStatusRequestTest.php new file mode 100644 index 0000000..81dbb22 --- /dev/null +++ b/tests/Request/PatronStatusRequestTest.php @@ -0,0 +1,22 @@ +setTimestamp(strtotime('2018-07-23 09:46:11')); + + $req->setInstitutionId('Banjo'); + $req->setPatronIdentifier('paul'); + $req->setPatronPassword('foo'); + + $msg = $req->getMessageString(); + + $this->assertEquals("2300120180723 094611AOBanjo|AApaul|AC|ADfoo|AY0AZF16E\r\n", $msg); + } +} diff --git a/tests/Request/RenewAllRequestTest.php b/tests/Request/RenewAllRequestTest.php new file mode 100644 index 0000000..edf5c80 --- /dev/null +++ b/tests/Request/RenewAllRequestTest.php @@ -0,0 +1,22 @@ +setTimestamp(strtotime('2018-07-23 09:46:11')); + + $req->setInstitutionId('Banjo'); + $req->setPatronIdentifier('paul'); + $req->setPatronPassword('foo'); + + $msg = $req->getMessageString(); + + $this->assertEquals("65AOBanjo|AApaul|ADfoo|BON|AY0AZF4EA\r\n", $msg); + } +} diff --git a/tests/Request/RenewRequestTest.php b/tests/Request/RenewRequestTest.php new file mode 100644 index 0000000..95c49e6 --- /dev/null +++ b/tests/Request/RenewRequestTest.php @@ -0,0 +1,26 @@ +setTimestamp(strtotime('2018-07-23 09:46:11')); + + $req->setItemIdentifier('1234'); + $req->setInstitutionId('Banjo'); + $req->setPatronIdentifier('paul'); + $req->setPatronPassword('foo'); + + $msg = $req->getMessageString(); + + $this->assertEquals( + "29NN20180723 094611 AOBanjo|AApaul|ADfoo|AB1234|BON|AY0AZECF9\r\n", + $msg + ); + } +} diff --git a/tests/Request/RequestACSResendRequestTest.php b/tests/Request/RequestACSResendRequestTest.php new file mode 100644 index 0000000..2646b65 --- /dev/null +++ b/tests/Request/RequestACSResendRequestTest.php @@ -0,0 +1,15 @@ +getMessageString(); + $this->assertEquals("97AY0AZFE2B\r\n", $msg); + } +} diff --git a/tests/Request/SCStatusRequestTest.php b/tests/Request/SCStatusRequestTest.php new file mode 100644 index 0000000..1c33900 --- /dev/null +++ b/tests/Request/SCStatusRequestTest.php @@ -0,0 +1,15 @@ +getMessageString(); + $this->assertEquals("990 802.00AY0AZFCB1\r\n", $msg); + } +} diff --git a/tests/Request/SIP2RequestTest.php b/tests/Request/SIP2RequestTest.php new file mode 100644 index 0000000..472178f --- /dev/null +++ b/tests/Request/SIP2RequestTest.php @@ -0,0 +1,52 @@ +setSIPLogin('user'); + $login->setSIPPassword('pass'); + + $msg = $login->getMessageString(); + //9300CNuu|COpp|AY1AZF9E9 + + $lastSep=strrpos($msg, '|'); + $ay=substr($msg, $lastSep+1, 2); + $seq=substr($msg, $lastSep+3, 1); + + $this->assertEquals('AY', $ay); + $this->assertEquals($s % 10, intval($seq)); + } + } + + + /** + * Test that getting a variable before setting one with no default throws exception + * @expectedException LogicException + */ + public function testMissingSet() + { + $login = new LoginRequest(); + $login->getVariable('SIPLogin'); + } + + /** + * Test that getting a variable before setting one with no default throws exception + * @expectedException LogicException + */ + public function testBadMethodCall() + { + $login = new LoginRequest(); + $login->garbageCall(); + } +} diff --git a/tests/Response/ACSStatusResponseTest.php b/tests/Response/ACSStatusResponseTest.php new file mode 100644 index 0000000..6a935a5 --- /dev/null +++ b/tests/Response/ACSStatusResponseTest.php @@ -0,0 +1,41 @@ +makeResponse("98". + "Y". + "N". + "Y". + "N". + "Y". + "N". + "123". + "456". + "20180711 185645". + "2.00". + "AOinstitution|"); + + /** @var ACSStatusResponse $response */ + $response = SIP2Response::parse($raw); + $this->assertInstanceOf(ACSStatusResponse::class, $response); + + $this->assertEquals('Y', $response->getOnline()); + $this->assertEquals('N', $response->getCheckin()); + $this->assertEquals('Y', $response->getCheckout()); + $this->assertEquals('N', $response->getRenewal()); + $this->assertEquals('Y', $response->getPatronUpdate()); + $this->assertEquals('N', $response->getOffline()); + $this->assertEquals('123', $response->getTimeout()); + $this->assertEquals('456', $response->getRetries()); + $this->assertEquals('20180711 185645', $response->getTransactionDate()); + $this->assertEquals('2.00', $response->getProtocol()); + $this->assertEquals('institution', $response->getInstitutionId()); + } +} diff --git a/tests/Response/CheckInResponseTest.php b/tests/Response/CheckInResponseTest.php new file mode 100644 index 0000000..d101110 --- /dev/null +++ b/tests/Response/CheckInResponseTest.php @@ -0,0 +1,48 @@ +makeResponse("10" . + "1" . + "Y" . + "U" . + "N" . + "20180711 185645" . + "AO1234|" . + "ABitem|" . + "AQloc|" . + "AJtitle|" . + "CLsort|" . + "AApatron|" . + "CKmda|" . + "CHprop|" . + "AFmessage|" . + "AGprint|"); + + /** @var CheckInResponse $response */ + $response = SIP2Response::parse($raw); + $this->assertInstanceOf(CheckInResponse::class, $response); + + $this->assertEquals('1', $response->getOk()); + $this->assertEquals('Y', $response->getResensitize()); + $this->assertEquals('U', $response->getMagnetic()); + $this->assertEquals('N', $response->getAlert()); + $this->assertEquals('20180711 185645', $response->getTransactionDate()); + + $this->assertEquals('1234', $response->getInstitutionId()); + $this->assertEquals('patron', $response->getPatronIdentifier()); + $this->assertEquals('sort', $response->getSortBin()); + $this->assertEquals('item', $response->getItemIdentifier()); + $this->assertEquals('title', $response->getTitleIdentifier()); + $this->assertEquals('loc', $response->getPermanentLocation()); + $this->assertEquals('mda', $response->getMediaType()); + $this->assertEquals('prop', $response->getItemProperties()); + } +} diff --git a/tests/Response/CheckOutResponseTest.php b/tests/Response/CheckOutResponseTest.php new file mode 100644 index 0000000..9af7ec0 --- /dev/null +++ b/tests/Response/CheckOutResponseTest.php @@ -0,0 +1,56 @@ +makeResponse("12" . + "1" . + "Y" . + "N" . + "Y" . + "20180711 185645" . + "AO1234|" . + "AApatron|" . + "ABitem|" . + "AJtitle|" . + "AH20180711 185645|" . + "BT01|" . + "CIN|" . + "BHGBP|" . + "BV1.23|" . + "CKmda|" . + "CHprop|" . + "BKxyz|" . + "AFmessage|" . + "AGprint|"); + + /** @var CheckOutResponse $response */ + $response = SIP2Response::parse($raw); + $this->assertInstanceOf(CheckOutResponse::class, $response); + + $this->assertEquals('1', $response->getOk()); + $this->assertEquals('Y', $response->getRenewalOk()); + $this->assertEquals('N', $response->getMagnetic()); + $this->assertEquals('Y', $response->getDesensitize()); + $this->assertEquals('20180711 185645', $response->getTransactionDate()); + + $this->assertEquals('1234', $response->getInstitutionId()); + $this->assertEquals('patron', $response->getPatronIdentifier()); + $this->assertEquals('item', $response->getItemIdentifier()); + $this->assertEquals('title', $response->getTitleIdentifier()); + $this->assertEquals('20180711 185645', $response->getDueDate()); + $this->assertEquals('01', $response->getFeeType()); + $this->assertEquals('N', $response->getSecurityInhibit()); + $this->assertEquals('GBP', $response->getCurrencyType()); + $this->assertEquals('1.23', $response->getFeeAmount()); + $this->assertEquals('mda', $response->getMediaType()); + $this->assertEquals('prop', $response->getItemProperties()); + $this->assertEquals('xyz', $response->getTransactionId()); + } +} diff --git a/tests/Response/EndSessionResponseTest.php b/tests/Response/EndSessionResponseTest.php new file mode 100644 index 0000000..11a4c8e --- /dev/null +++ b/tests/Response/EndSessionResponseTest.php @@ -0,0 +1,37 @@ +makeResponse("36". + "Y". + "20180711 185645". + "AO1234|". + "AApatron|". + "AEJoe|". + "AFmessage|". + "AGprint|"); + + /** @var EndSessionResponse $response */ + $response = SIP2Response::parse($raw); + $this->assertInstanceOf(EndSessionResponse::class, $response); + + $this->assertEquals('Y', $response->getEndSession()); + $this->assertEquals('20180711 185645', $response->getTransactionDate()); + $this->assertEquals('1234', $response->getInstitutionId()); + $this->assertEquals('patron', $response->getPatronIdentifier()); + $this->assertContains('message', $response->getScreenMessage()); + $this->assertContains('print', $response->getPrintLine()); + + //we've thown an AE Personal name in the test response. We wouldn't expect this, but we + //tolerate extra data... + $this->assertTrue($response->hasVariable('PersonalName')); + $this->assertEquals('Joe', $response->getVariable('PersonalName')); + } +} diff --git a/tests/Response/FeePaidResponseTest.php b/tests/Response/FeePaidResponseTest.php new file mode 100644 index 0000000..dfa3230 --- /dev/null +++ b/tests/Response/FeePaidResponseTest.php @@ -0,0 +1,33 @@ +makeResponse("38". + "Y". + "20180711 185645". + "AO1234|". + "AApatron|". + "BK5555|". + "AFmessage|". + "AGprint|"); + + /** @var FeePaidResponse $response */ + $response = SIP2Response::parse($raw); + $this->assertInstanceOf(FeePaidResponse::class, $response); + + $this->assertEquals('Y', $response->getPaymentAccepted()); + $this->assertEquals('20180711 185645', $response->getTransactionDate()); + $this->assertEquals('1234', $response->getInstitutionId()); + $this->assertEquals('patron', $response->getPatronIdentifier()); + $this->assertEquals('5555', $response->getTransactionId()); + $this->assertContains('message', $response->getScreenMessage()); + $this->assertContains('print', $response->getPrintLine()); + } +} diff --git a/tests/Response/HoldResponseTest.php b/tests/Response/HoldResponseTest.php new file mode 100644 index 0000000..6b34adb --- /dev/null +++ b/tests/Response/HoldResponseTest.php @@ -0,0 +1,33 @@ +makeResponse("161Y20180711 185645BW20180711 185645|" . + "BR1|BSLibrary|AO123|AApatron|ABitem|AJtitle|AFthankyou|AGprint|"); + + /** @var HoldResponse $response */ + $response = SIP2Response::parse($raw); + $this->assertInstanceOf(HoldResponse::class, $response); + + $this->assertEquals('1', $response->getOk()); + $this->assertEquals('Y', $response->getAvailable()); + $this->assertEquals('20180711 185645', $response->getTransactionDate()); + $this->assertEquals('20180711 185645', $response->getExpirationDate()); + + $this->assertEquals('1', $response->getQueuePosition()); + $this->assertEquals('Library', $response->getPickupLocation()); + $this->assertEquals('123', $response->getInstitutionId()); + $this->assertEquals('patron', $response->getPatronIdentifier()); + $this->assertEquals('item', $response->getItemIdentifier()); + $this->assertEquals('title', $response->getTitleIdentifier()); + $this->assertContains('thankyou', $response->getScreenMessage()); + $this->assertContains('print', $response->getPrintLine()); + } +} diff --git a/tests/Response/ItemInformationResponseTest.php b/tests/Response/ItemInformationResponseTest.php new file mode 100644 index 0000000..08fd776 --- /dev/null +++ b/tests/Response/ItemInformationResponseTest.php @@ -0,0 +1,57 @@ +makeResponse("18" . + "01" . + "02" . + "03" . + "20180711 185645" . + "CF3|" . + "AH20180711 185645|" . + "CJ20180711 185646|" . + "CM20180711 185647|" . + "AB1565921879|" . + "AJPerl 5 desktop reference|" . + "BGBR1|" . + "BHGBP|" . + "BV1.23|" . + "CK001|" . + "AQBR2|" . + "APBR3|" . + "CHprop|" . + "AFmessage|" . + "AGprint|"); + + /** @var ItemInformationResponse $response */ + $response = SIP2Response::parse($raw); + $this->assertInstanceOf(ItemInformationResponse::class, $response); + + $this->assertEquals('01', $response->getCirculationStatus()); + $this->assertEquals('02', $response->getSecurityMarker()); + $this->assertEquals('03', $response->getFeeType()); + $this->assertEquals('20180711 185645', $response->getTransactionDate()); + $this->assertEquals('3', $response->getHoldQueueLength()); + $this->assertEquals('20180711 185645', $response->getDueDate()); + $this->assertEquals('20180711 185646', $response->getRecallDate()); + $this->assertEquals('20180711 185647', $response->getHoldPickupDate()); + $this->assertEquals('1565921879', $response->getItemIdentifier()); + $this->assertEquals('Perl 5 desktop reference', $response->getTitleIdentifier()); + $this->assertEquals('BR1', $response->getOwner()); + $this->assertEquals('GBP', $response->getCurrencyType()); + $this->assertEquals('1.23', $response->getFeeAmount()); + $this->assertEquals('001', $response->getMediaType()); + $this->assertEquals('BR2', $response->getPermanentLocation()); + $this->assertEquals('BR3', $response->getCurrentLocation()); + $this->assertEquals('prop', $response->getItemProperties()); + $this->assertContains('message', $response->getScreenMessage()); + $this->assertContains('print', $response->getPrintLine()); + } +} diff --git a/tests/Response/ItemStatusUpdateResponseTest.php b/tests/Response/ItemStatusUpdateResponseTest.php new file mode 100644 index 0000000..0c45533 --- /dev/null +++ b/tests/Response/ItemStatusUpdateResponseTest.php @@ -0,0 +1,33 @@ +makeResponse("20" . + "1" . + "20180711 185645" . + "AB1565921879|" . + "AJPerl 5 desktop reference|" . + "CHprop|" . + "AFmessage|" . + "AGprint|"); + + /** @var ItemStatusUpdateResponse $response */ + $response = SIP2Response::parse($raw); + $this->assertInstanceOf(ItemStatusUpdateResponse::class, $response); + + $this->assertEquals('1', $response->getPropertiesOk()); + $this->assertEquals('20180711 185645', $response->getTransactionDate()); + $this->assertEquals('1565921879', $response->getItemIdentifier()); + $this->assertEquals('Perl 5 desktop reference', $response->getTitleIdentifier()); + $this->assertEquals('prop', $response->getItemProperties()); + $this->assertContains('message', $response->getScreenMessage()); + $this->assertContains('print', $response->getPrintLine()); + } +} diff --git a/tests/Response/LoginResponseTest.php b/tests/Response/LoginResponseTest.php new file mode 100644 index 0000000..87b86b3 --- /dev/null +++ b/tests/Response/LoginResponseTest.php @@ -0,0 +1,19 @@ +makeResponse("941"); + + /** @var LoginResponse $response */ + $response = SIP2Response::parse($raw); + $this->assertInstanceOf(LoginResponse::class, $response); + $this->assertEquals('1', $response->getOk()); + } +} diff --git a/tests/Response/PatronEnableResponseTest.php b/tests/Response/PatronEnableResponseTest.php new file mode 100644 index 0000000..07078bf --- /dev/null +++ b/tests/Response/PatronEnableResponseTest.php @@ -0,0 +1,39 @@ +makeResponse("26" . + "XXXXyyyXXXXyyy" . + "ENG". + "20180711 185645" . + "AO1234|" . + "AApatron|" . + "AEJoe Tester|" . + "BLY|". + "CQY|". + "AFmessage|" . + "AGprint|"); + + /** @var PatronEnableResponse $response */ + $response = SIP2Response::parse($raw); + $this->assertInstanceOf(PatronEnableResponse::class, $response); + $this->assertEquals('XXXXyyyXXXXyyy', $response->getPatronStatus()); + $this->assertEquals('ENG', $response->getLanguage()); + $this->assertEquals('20180711 185645', $response->getTransactionDate()); + + $this->assertEquals('1234', $response->getInstitutionId()); + $this->assertEquals('patron', $response->getPatronIdentifier()); + $this->assertEquals('Joe Tester', $response->getPersonalName()); + + $this->assertEquals('Y', $response->getValidPatron()); + $this->assertEquals('Y', $response->getValidPatronPassword()); + $this->assertEquals('0', $response->getSequenceNumber()); + } +} diff --git a/tests/Response/PatronInformationResponseTest.php b/tests/Response/PatronInformationResponseTest.php new file mode 100644 index 0000000..159a3a2 --- /dev/null +++ b/tests/Response/PatronInformationResponseTest.php @@ -0,0 +1,44 @@ +makeResponse("64 00020180711 185645000000000010000000000009" . + "AOExample City Library|AA1381380|AEMr Joe Tester|BZ9999|CA8888|CB7777|BLY|CQY|BV0.00|" . + "AS123|AS456|". + "BEjoe.tester@example.com|"); + + /** @var PatronInformationResponse $response */ + $response = SIP2Response::parse($raw); + $this->assertInstanceOf(PatronInformationResponse::class, $response); + $this->assertEquals(' ', $response->getPatronStatus()); + $this->assertEquals('000', $response->getLanguage()); + $this->assertEquals('20180711 185645', $response->getTransactionDate()); + $this->assertEquals(0, $response->getHoldCount()); + $this->assertEquals(10, $response->getChargedCount()); + $this->assertEquals(0, $response->getFineCount()); + $this->assertEquals(0, $response->getRecallCount()); + $this->assertEquals(9, $response->getUnavailableCount()); + + $this->assertEquals('Example City Library', $response->getInstitutionId()); + $this->assertEquals('1381380', $response->getPatronIdentifier()); + $this->assertEquals('Mr Joe Tester', $response->getPersonalName()); + $this->assertEquals('9999', $response->getHoldItemsLimit()); + $this->assertEquals('8888', $response->getOverdueItemsLimit()); + $this->assertEquals('7777', $response->getChargedItemsLimit()); + $this->assertEquals('Y', $response->getValidPatron()); + $this->assertEquals('Y', $response->getValidPatronPassword()); + $this->assertEquals('0.00', $response->getFeeAmount()); + $this->assertEquals('joe.tester@example.com', $response->getEmailAddress()); + $this->assertEquals('0', $response->getSequenceNumber()); + + $this->assertContains('123', $response->getHoldItems()); + $this->assertContains('456', $response->getHoldItems()); + } +} diff --git a/tests/Response/PatronStatusResponseTest.php b/tests/Response/PatronStatusResponseTest.php new file mode 100644 index 0000000..1b1f836 --- /dev/null +++ b/tests/Response/PatronStatusResponseTest.php @@ -0,0 +1,43 @@ +makeResponse("24". + "xxxYYYYxxxYYYY". + "ENG". + "20180711 185645". + "AO1234|". + "AApatron|". + "AEJoe|". + "BLY|". + "CQY|". + "BHGBP|". + "BV1.23|". + "AFmessage|". + "AGprint|"); + + /** @var PatronStatusResponse $response */ + $response = SIP2Response::parse($raw); + $this->assertInstanceOf(PatronStatusResponse::class, $response); + $this->assertEquals('xxxYYYYxxxYYYY', $response->getPatronStatus()); + $this->assertEquals('ENG', $response->getLanguage()); + $this->assertEquals('20180711 185645', $response->getTransactionDate()); + + $this->assertEquals('1234', $response->getInstitutionId()); + $this->assertEquals('patron', $response->getPatronIdentifier()); + $this->assertEquals('Joe', $response->getPersonalName()); + + $this->assertEquals('Y', $response->getValidPatron()); + $this->assertEquals('Y', $response->getValidPatronPassword()); + $this->assertEquals('1.23', $response->getFeeAmount()); + $this->assertEquals('GBP', $response->getCurrencyType()); + $this->assertEquals('0', $response->getSequenceNumber()); + } +} diff --git a/tests/Response/RenewAllResponseTest.php b/tests/Response/RenewAllResponseTest.php new file mode 100644 index 0000000..def92e6 --- /dev/null +++ b/tests/Response/RenewAllResponseTest.php @@ -0,0 +1,48 @@ +makeResponse("66" . + "1" . + "0002". + "0003". + "20180711 185645" . + "AOinstitution|" . + "BMbook 1|". + "BMbook 2|". + "BNbook 3|". + "BNbook 4|". + "BNbook 5|". + "AFmessage|" . + "AGprint|"); + + /** @var RenewAllResponse $response */ + $response = SIP2Response::parse($raw); + $this->assertInstanceOf(RenewAllResponse::class, $response); + + $this->assertEquals('1', $response->getOk()); + $this->assertEquals('0002', $response->getRenewed()); + $this->assertEquals('0003', $response->getUnrenewed()); + $this->assertEquals('20180711 185645', $response->getTransactionDate()); + + $this->assertEquals('institution', $response->getInstitutionId()); + + + $this->assertCount(2, $response->getRenewedItems()); + $this->assertContains('book 1', $response->getRenewedItems()); + $this->assertContains('book 2', $response->getRenewedItems()); + + $this->assertCount(3, $response->getUnrenewedItems()); + $this->assertContains('book 3', $response->getUnrenewedItems()); + $this->assertContains('book 4', $response->getUnrenewedItems()); + $this->assertContains('book 5', $response->getUnrenewedItems()); + } +} diff --git a/tests/Response/RenewResponseTest.php b/tests/Response/RenewResponseTest.php new file mode 100644 index 0000000..f84dabf --- /dev/null +++ b/tests/Response/RenewResponseTest.php @@ -0,0 +1,56 @@ +makeResponse("30" . + "1" . + "Y" . + "N" . + "Y" . + "20180711 185645" . + "AO1234|" . + "AApatron|" . + "ABitem|" . + "AJtitle|" . + "AH20180711 185645|" . + "BT01|" . + "CIN|" . + "BHGBP|" . + "BV1.23|" . + "CKmda|" . + "CHprop|" . + "BKxyz|" . + "AFmessage|" . + "AGprint|"); + + /** @var RenewResponse $response */ + $response = SIP2Response::parse($raw); + $this->assertInstanceOf(RenewResponse::class, $response); + + $this->assertEquals('1', $response->getOk()); + $this->assertEquals('Y', $response->getRenewalOk()); + $this->assertEquals('N', $response->getMagnetic()); + $this->assertEquals('Y', $response->getDesensitize()); + $this->assertEquals('20180711 185645', $response->getTransactionDate()); + + $this->assertEquals('1234', $response->getInstitutionId()); + $this->assertEquals('patron', $response->getPatronIdentifier()); + $this->assertEquals('item', $response->getItemIdentifier()); + $this->assertEquals('title', $response->getTitleIdentifier()); + $this->assertEquals('20180711 185645', $response->getDueDate()); + $this->assertEquals('01', $response->getFeeType()); + $this->assertEquals('N', $response->getSecurityInhibit()); + $this->assertEquals('GBP', $response->getCurrencyType()); + $this->assertEquals('1.23', $response->getFeeAmount()); + $this->assertEquals('mda', $response->getMediaType()); + $this->assertEquals('prop', $response->getItemProperties()); + $this->assertEquals('xyz', $response->getTransactionId()); + } +} diff --git a/tests/Response/SIP2ResponseTest.php b/tests/Response/SIP2ResponseTest.php new file mode 100644 index 0000000..9a4b64c --- /dev/null +++ b/tests/Response/SIP2ResponseTest.php @@ -0,0 +1,87 @@ +makeResponse("771"); + SIP2Response::parse($raw); + } + + /** + * Test that a response with new codes in it is handled + * + */ + public function testUnknownVariableCodes() + { + $raw = $this->makeResponse("36". + "Y". + "20180711 185645". + "AJ1234|". + "ZZtop|"); + + /** @var EndSessionResponse $response */ + $response = SIP2Response::parse($raw); + $this->assertInstanceOf(EndSessionResponse::class, $response); + $this->assertEquals('Y', $response->getEndSession()); + + $this->assertTrue($response->hasVariable('TitleIdentifier')); + $this->assertTrue($response->hasVariable('ZZ')); + } + + public function testGetAll() + { + $raw = $this->makeResponse("36". + "Y". + "20180711 185645". + "AOlibrary|". + "AGline1|". + "AGline2|"); + + /** @var EndSessionResponse $response */ + $response = SIP2Response::parse($raw); + $this->assertInstanceOf(EndSessionResponse::class, $response); + + $data = $response->getAll(); + $this->assertCount(7, $data); + $this->assertArrayHasKey('EndSession', $data); + $this->assertEquals('Y', $data['EndSession']); + + $this->assertArrayHasKey('PrintLine', $data); + $this->assertCount(2, $data['PrintLine']); + $this->assertEquals('line1', $data['PrintLine'][0]); + } + + /** + * Test that attempting to get unexpected data on a response will throw exception + * + * @expectedException LogicException + */ + public function testGetInvalidVar() + { + $raw = $this->makeResponse("36". + "Y". + "20180711 185645". + "AOlibrary|". + "AGline1|". + "AGline2|"); + + /** @var EndSessionResponse $response */ + $response = SIP2Response::parse($raw); + + $response->getVariable('TitleIdentifier'); + } +} diff --git a/tests/SCStatusTest.php b/tests/SCStatusTest.php deleted file mode 100644 index 793e3be..0000000 --- a/tests/SCStatusTest.php +++ /dev/null @@ -1,49 +0,0 @@ -makeResponse("98". - "Y". - "N". - "Y". - "N". - "Y". - "N". - "123". - "456". - "20180711 185645". - "2.00". - "AOinstitution|") - ]; - - $client = new SIP2Client; - $client->setSocketFactory($this->createMockSIP2Server($responses)); - - $client->connect(); - - $msg = $client->msgSCStatus(0, 80, 2); - $response = $client->getMessage($msg); - - $info = $client->parseACSStatusResponse($response); - - $this->assertFixedMetadata('Y', $info, 'Online'); - $this->assertFixedMetadata('N', $info, 'Checkin'); - $this->assertFixedMetadata('Y', $info, 'Checkout'); - $this->assertFixedMetadata('N', $info, 'Renewal'); - $this->assertFixedMetadata('Y', $info, 'PatronUpdate'); - $this->assertFixedMetadata('N', $info, 'Offline'); - $this->assertFixedMetadata('123', $info, 'Timeout'); - $this->assertFixedMetadata('456', $info, 'Retries'); - $this->assertFixedMetadata('20180711 185645', $info, 'TransactionDate'); - $this->assertFixedMetadata('2.00', $info, 'Protocol'); - - $this->assertVariableMetadata('institution', $info, 'AO'); - } -} diff --git a/tests/SIP2ClientTest.php b/tests/SIP2ClientTest.php new file mode 100644 index 0000000..31a75c5 --- /dev/null +++ b/tests/SIP2ClientTest.php @@ -0,0 +1,171 @@ +makeResponse("941") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect('10.0.0.0'); + + $loginRequest = new LoginRequest(); + $loginRequest->setSIPLogin('username'); + $loginRequest->setSIPPassword('password'); + + /** @var LoginResponse $response */ + $response = $client->sendRequest($loginRequest); + $this->assertInstanceOf(LoginResponse::class, $response); + + $this->assertEquals('1', $response->getOk()); + + $client->disconnect(); + } + + /** + * Test that client will retry a request with bad CRC + */ + public function testCRCFailureRetry() + { + //our mock socket will return these responses in sequence after each write() to the socket + //here we simulate a login response with a bad CRC, followed by a good one + $responses = [ + "940AY0AZ1234\r", + $this->makeResponse("941") + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect('10.0.0.0'); + + $loginRequest = new LoginRequest(); + $loginRequest->setSIPLogin('username'); + $loginRequest->setSIPPassword('password'); + + /** @var LoginResponse $response */ + $response = $client->sendRequest($loginRequest); + $this->assertInstanceOf(LoginResponse::class, $response); + $this->assertEquals('1', $response->getOk()); + } + + /** + * Test that repeated failure of a SIP2 server to provide a valid CRC produces an exception + * + * @expectedException RuntimeException + */ + public function testCRCFailureAbort() + { + //our mock socket will return these responses in sequence after each write() to the socket + //here we simulate a continued failure to provide a valid response, leading us to abort after + //3 retries + $responses = [ + "940AY0AZ1234\r", + "940AY0AZ1234\r", + "940AY0AZ1234\r", + "940AY0AZ1234\r", + ]; + + $client = new SIP2Client; + $client->setSocketFactory($this->createMockSIP2Server($responses)); + + $client->connect('10.0.0.0'); + + $loginRequest = new LoginRequest(); + $loginRequest->setSIPLogin('username'); + $loginRequest->setSIPPassword('password'); + + /** @var LoginResponse $response */ + $client->sendRequest($loginRequest); //exception should be thrown + } + + /** + * THis just verifies that Socket::bind is called if we've asked for a specific binding + */ + public function testBinding() + { + $client = new SIP2Client; + $client->setSocketFactory($this->createBindingTestMockSIP2Server()); + + $client->connect('10.0.0.0', '192.168.1.1'); + + //we don't really need an assertion, as its enough to reach here without exception + //and the mock includes a prediction for a call on bind... + } + + /** + * Test that failure to connect throws exception + * + * @expectedException RuntimeException + */ + public function testConnectionFailure() + { + $client = new SIP2Client; + $client->setSocketFactory($this->createUnconnectableMockSIP2Server()); + $client->connect('10.0.0.0'); + } + + /** + * This provides a socket factory which will always fail to connect + * @return \Socket\Raw\Factory + */ + protected function createUnconnectableMockSIP2Server() + { + $socket = $this->prophesize(\Socket\Raw\Socket::class); + $socket->connect(Argument::type('string'))->will(function () { + throw new \Exception('Test connection failure'); + }); + + $socket->close()->willReturn(true); + + //our factory will always fail to connect... + $factory = $this->prophesize(\Socket\Raw\Factory::class); + $factory->createFromString( + Argument::type('string'), + Argument::any() + )->willReturn($socket->reveal()); + + return $factory->reveal(); + } + + /** + * This provides a socket factory which will verify the bind method is called + * @return \Socket\Raw\Factory + */ + private function createBindingTestMockSIP2Server() + { + $socket = $this->prophesize(\Socket\Raw\Socket::class); + $socket->connect(Argument::type('string'))->willReturn(true); + + //we verify bind gets called... + $socket->bind(Argument::type('string'))->shouldBeCalled()->willReturn(true); + + //our factory will always fail to connect... + $factory = $this->prophesize(\Socket\Raw\Factory::class); + $factory->createFromString( + Argument::type('string'), + Argument::any() + )->willReturn($socket->reveal()); + + return $factory->reveal(); + } +} diff --git a/tests/SequencingTest.php b/tests/SequencingTest.php deleted file mode 100644 index b46c3b5..0000000 --- a/tests/SequencingTest.php +++ /dev/null @@ -1,28 +0,0 @@ -setSocketFactory($this->createMockSIP2Server([])); - - for ($s=0; $s<=11; $s++) { - $msg = $client->msgLogin('uu', 'pp'); - //9300CNuu|COpp|AY1AZF9E9 - - $lastSep=strrpos($msg, '|'); - $ay=substr($msg, $lastSep+1, 2); - $seq=substr($msg, $lastSep+3, 1); - - $this->assertEquals('AY', $ay); - $this->assertEquals($s % 10, intval($seq)); - } - } -} From f7bdbbb462362dfafff5a50926a5281260e0001c Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sun, 29 Jul 2018 11:42:18 +0100 Subject: [PATCH 22/35] Improve and clean up phpdoc comments --- src/Request/BlockPatronRequest.php | 4 ++++ src/Request/CheckInRequest.php | 4 ++++ src/Request/CheckOutRequest.php | 4 ++++ src/Request/EndPatronSessionRequest.php | 4 ++++ src/Request/FeePaidRequest.php | 4 ++++ src/Request/HoldRequest.php | 4 ++++ src/Request/ItemInformationRequest.php | 4 ++++ src/Request/ItemStatusUpdateRequest.php | 4 ++++ src/Request/LoginRequest.php | 4 ++++ src/Request/PatronEnableRequest.php | 4 ++++ src/Request/PatronInformationRequest.php | 4 ++++ src/Request/PatronStatusRequest.php | 4 ++++ src/Request/RenewAllRequest.php | 4 ++++ src/Request/RenewRequest.php | 4 ++++ src/Request/RequestACSResendRequest.php | 4 ++++ src/Request/SCStatusRequest.php | 4 ++++ src/Request/SIP2Request.php | 8 ++++---- src/Response/ACSStatusResponse.php | 4 ++++ src/Response/CheckInResponse.php | 4 ++++ src/Response/CheckOutResponse.php | 4 ++++ src/Response/EndSessionResponse.php | 4 ++++ src/Response/FeePaidResponse.php | 4 ++++ src/Response/HoldResponse.php | 4 ++++ src/Response/ItemInformationResponse.php | 4 ++++ src/Response/ItemStatusUpdateResponse.php | 4 ++++ src/Response/LoginResponse.php | 4 ++++ src/Response/PatronEnableResponse.php | 4 ++++ src/Response/PatronInformationResponse.php | 4 ++++ src/Response/PatronStatusResponse.php | 4 ++++ src/Response/RenewAllResponse.php | 4 ++++ src/Response/RenewResponse.php | 4 ++++ src/Response/SIP2Response.php | 4 ++++ 32 files changed, 128 insertions(+), 4 deletions(-) diff --git a/src/Request/BlockPatronRequest.php b/src/Request/BlockPatronRequest.php index 10c797b..21c8ae2 100644 --- a/src/Request/BlockPatronRequest.php +++ b/src/Request/BlockPatronRequest.php @@ -13,6 +13,10 @@ * @method setMessage(string $message) * @method setPatronIdentifier(string $patron) * @method setTerminalPassword(string $terminalPassword) + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class BlockPatronRequest extends SIP2Request { diff --git a/src/Request/CheckInRequest.php b/src/Request/CheckInRequest.php index 4e53595..867cb42 100644 --- a/src/Request/CheckInRequest.php +++ b/src/Request/CheckInRequest.php @@ -17,6 +17,10 @@ * @method setTerminalPassword(string $terminalPassword) * @method setItemProperties(string $itemProperties) * @method setCancel(string $yn) + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class CheckInRequest extends SIP2Request { diff --git a/src/Request/CheckOutRequest.php b/src/Request/CheckOutRequest.php index 1a483b8..39e32b0 100644 --- a/src/Request/CheckOutRequest.php +++ b/src/Request/CheckOutRequest.php @@ -20,6 +20,10 @@ * @method setPatronPassword(string $patronPassword) * @method setFeeAcknowledged(string $yn) * @method setCancel(string $yn) + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class CheckOutRequest extends SIP2Request { diff --git a/src/Request/EndPatronSessionRequest.php b/src/Request/EndPatronSessionRequest.php index 0752fd0..0d3941d 100644 --- a/src/Request/EndPatronSessionRequest.php +++ b/src/Request/EndPatronSessionRequest.php @@ -11,6 +11,10 @@ * @method setPatronIdentifier(string $patron) * @method setTerminalPassword(string $terminalPassword) * @method setPatronPassword(string $patronPassword) + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class EndPatronSessionRequest extends SIP2Request { diff --git a/src/Request/FeePaidRequest.php b/src/Request/FeePaidRequest.php index 19b3bcd..c2acd71 100644 --- a/src/Request/FeePaidRequest.php +++ b/src/Request/FeePaidRequest.php @@ -33,6 +33,10 @@ * @method setPatronPassword(string $patronPassword) * @method setFeeIdentifier(string $feeId) * @method setTransactionIdentifier(string $transactionId) + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class FeePaidRequest extends SIP2Request { diff --git a/src/Request/HoldRequest.php b/src/Request/HoldRequest.php index 58b5ab1..d71e66c 100644 --- a/src/Request/HoldRequest.php +++ b/src/Request/HoldRequest.php @@ -29,6 +29,10 @@ * @method setItemTitle(string $itemTitle) * @method setTerminalPassword(string $terminalPassword) * @method setFeeAcknowledged(string $yn) + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class HoldRequest extends SIP2Request { diff --git a/src/Request/ItemInformationRequest.php b/src/Request/ItemInformationRequest.php index ff6236b..dc352b8 100644 --- a/src/Request/ItemInformationRequest.php +++ b/src/Request/ItemInformationRequest.php @@ -9,6 +9,10 @@ * @method setInstitutionId(string $institutionId) * @method setItemIdentifier(string $itemIdentifier) * @method setTerminalPassword(string $terminalPassword) + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class ItemInformationRequest extends SIP2Request { diff --git a/src/Request/ItemStatusUpdateRequest.php b/src/Request/ItemStatusUpdateRequest.php index 98e98d8..392ec77 100644 --- a/src/Request/ItemStatusUpdateRequest.php +++ b/src/Request/ItemStatusUpdateRequest.php @@ -11,6 +11,10 @@ * @method setItemIdentifier(string $itemIdentifier) * @method setTerminalPassword(string $terminalPassword) * @method setItemProperties(string $itemProperties) + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class ItemStatusUpdateRequest extends SIP2Request { diff --git a/src/Request/LoginRequest.php b/src/Request/LoginRequest.php index 63e9194..2a2e1ac 100644 --- a/src/Request/LoginRequest.php +++ b/src/Request/LoginRequest.php @@ -14,6 +14,10 @@ * @method setSIPLogin(string $username) * @method setSIPPassword(string $password) * @method setLocation(string $location) + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class LoginRequest extends SIP2Request { diff --git a/src/Request/PatronEnableRequest.php b/src/Request/PatronEnableRequest.php index ed00e6c..c7469e5 100644 --- a/src/Request/PatronEnableRequest.php +++ b/src/Request/PatronEnableRequest.php @@ -10,6 +10,10 @@ * @method setPatronIdentifier(string $patron) * @method setTerminalPassword(string $terminalPassword) * @method setPatronPassword(string $patronPassword) + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class PatronEnableRequest extends SIP2Request { diff --git a/src/Request/PatronInformationRequest.php b/src/Request/PatronInformationRequest.php index 017b1e6..aee595c 100644 --- a/src/Request/PatronInformationRequest.php +++ b/src/Request/PatronInformationRequest.php @@ -16,6 +16,10 @@ * @method setPatronPassword(string $patronPassword) * @method setStart(string $start) * @method setEnd(string $end) + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class PatronInformationRequest extends SIP2Request { diff --git a/src/Request/PatronStatusRequest.php b/src/Request/PatronStatusRequest.php index 8a6c403..4a8fdca 100644 --- a/src/Request/PatronStatusRequest.php +++ b/src/Request/PatronStatusRequest.php @@ -11,6 +11,10 @@ * @method setPatronIdentifier(string $patron) * @method setTerminalPassword(string $terminalPassword) * @method setPatronPassword(string $patronPassword) + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class PatronStatusRequest extends SIP2Request { diff --git a/src/Request/RenewAllRequest.php b/src/Request/RenewAllRequest.php index 9bad9fa..30683a6 100644 --- a/src/Request/RenewAllRequest.php +++ b/src/Request/RenewAllRequest.php @@ -11,6 +11,10 @@ * @method setPatronPassword(string $patronPassword) * @method setTerminalPassword(string $terminalPassword) * @method setFeeAcknowledged(string $yn) + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class RenewAllRequest extends SIP2Request { diff --git a/src/Request/RenewRequest.php b/src/Request/RenewRequest.php index bc37827..5d0e130 100644 --- a/src/Request/RenewRequest.php +++ b/src/Request/RenewRequest.php @@ -18,6 +18,10 @@ * @method setTerminalPassword(string $terminalPassword) * @method setItemProperties(string $itemProperties) * @method setFeeAcknowledged(string $yn) + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class RenewRequest extends SIP2Request { diff --git a/src/Request/RequestACSResendRequest.php b/src/Request/RequestACSResendRequest.php index 09487d4..bc3a499 100644 --- a/src/Request/RequestACSResendRequest.php +++ b/src/Request/RequestACSResendRequest.php @@ -7,6 +7,10 @@ * checksum in a received message does not match the value calculated by the SC. The ACS should respond by * re-transmitting its last message, This message should never include a “sequence number” field, even when error * detection is enabled, but would include a “checksum” field since checksums are in use. + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class RequestACSResendRequest extends SIP2Request { diff --git a/src/Request/SCStatusRequest.php b/src/Request/SCStatusRequest.php index fe58ff4..54bbf2c 100644 --- a/src/Request/SCStatusRequest.php +++ b/src/Request/SCStatusRequest.php @@ -14,6 +14,10 @@ * @method setMessage(string $message) * @method setPatronIdentifier(string $patron) * @method setTerminalPassword(string $terminalPassword) + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class SCStatusRequest extends SIP2Request { diff --git a/src/Request/SIP2Request.php b/src/Request/SIP2Request.php index 82ee04b..4567b75 100644 --- a/src/Request/SIP2Request.php +++ b/src/Request/SIP2Request.php @@ -5,11 +5,11 @@ use lordelph\SIP2\SIP2Message; /** - * Class SIP2Request provides a way to declare the variables a message needs and provide some - * magic get/set methods which can then be documented with annotations to provide hints as to what can - * be done with the request. + * Class SIP2Request extends SIP2Message with methods for building SIP2 message strings * - * @package lordelph\SIP2 + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ abstract class SIP2Request extends SIP2Message { diff --git a/src/Response/ACSStatusResponse.php b/src/Response/ACSStatusResponse.php index 5a733e3..30b8933 100644 --- a/src/Response/ACSStatusResponse.php +++ b/src/Response/ACSStatusResponse.php @@ -23,6 +23,10 @@ * @method getScreenMessage() * @method getPrintLine() * @method getSequenceNumber() + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class ACSStatusResponse extends SIP2Response { diff --git a/src/Response/CheckInResponse.php b/src/Response/CheckInResponse.php index 261e5c9..112a75a 100644 --- a/src/Response/CheckInResponse.php +++ b/src/Response/CheckInResponse.php @@ -21,6 +21,10 @@ * @method getScreenMessage() * @method getPrintLine() * @method getSequenceNumber() + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class CheckInResponse extends SIP2Response { diff --git a/src/Response/CheckOutResponse.php b/src/Response/CheckOutResponse.php index 9939c66..8a0fd02 100644 --- a/src/Response/CheckOutResponse.php +++ b/src/Response/CheckOutResponse.php @@ -25,6 +25,10 @@ * @method getScreenMessage() * @method getPrintLine() * @method getSequenceNumber() + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class CheckOutResponse extends SIP2Response { diff --git a/src/Response/EndSessionResponse.php b/src/Response/EndSessionResponse.php index 2b7ca79..09a55bf 100644 --- a/src/Response/EndSessionResponse.php +++ b/src/Response/EndSessionResponse.php @@ -12,6 +12,10 @@ * @method getScreenMessage() * @method getPrintLine() * @method getSequenceNumber() + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class EndSessionResponse extends SIP2Response { diff --git a/src/Response/FeePaidResponse.php b/src/Response/FeePaidResponse.php index aa079ae..169bfd9 100644 --- a/src/Response/FeePaidResponse.php +++ b/src/Response/FeePaidResponse.php @@ -13,6 +13,10 @@ * @method getScreenMessage() * @method getPrintLine() * @method getSequenceNumber() + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class FeePaidResponse extends SIP2Response { diff --git a/src/Response/HoldResponse.php b/src/Response/HoldResponse.php index dbdc5e4..ae312f8 100644 --- a/src/Response/HoldResponse.php +++ b/src/Response/HoldResponse.php @@ -18,6 +18,10 @@ * @method getScreenMessage() * @method getPrintLine() * @method getSequenceNumber() + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class HoldResponse extends SIP2Response { diff --git a/src/Response/ItemInformationResponse.php b/src/Response/ItemInformationResponse.php index 75a9276..c5bd793 100644 --- a/src/Response/ItemInformationResponse.php +++ b/src/Response/ItemInformationResponse.php @@ -25,6 +25,10 @@ * @method getScreenMessage() * @method getPrintLine() * @method getSequenceNumber() + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class ItemInformationResponse extends SIP2Response { diff --git a/src/Response/ItemStatusUpdateResponse.php b/src/Response/ItemStatusUpdateResponse.php index ea68fa4..b757e05 100644 --- a/src/Response/ItemStatusUpdateResponse.php +++ b/src/Response/ItemStatusUpdateResponse.php @@ -13,6 +13,10 @@ * @method getScreenMessage() * @method getPrintLine() * @method getSequenceNumber() + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class ItemStatusUpdateResponse extends SIP2Response { diff --git a/src/Response/LoginResponse.php b/src/Response/LoginResponse.php index 01b1478..b84aa0b 100644 --- a/src/Response/LoginResponse.php +++ b/src/Response/LoginResponse.php @@ -6,6 +6,10 @@ * Class LoginResponse provides the response from a LoginRequest * * @method getOk() + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class LoginResponse extends SIP2Response { diff --git a/src/Response/PatronEnableResponse.php b/src/Response/PatronEnableResponse.php index 6726708..07eeda6 100644 --- a/src/Response/PatronEnableResponse.php +++ b/src/Response/PatronEnableResponse.php @@ -16,6 +16,10 @@ * @method getScreenMessage() * @method getPrintLine() * @method getSequenceNumber() + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class PatronEnableResponse extends SIP2Response { diff --git a/src/Response/PatronInformationResponse.php b/src/Response/PatronInformationResponse.php index 06d2091..373560e 100644 --- a/src/Response/PatronInformationResponse.php +++ b/src/Response/PatronInformationResponse.php @@ -36,6 +36,10 @@ * @method getScreenMessage() * @method getPrintLine() * @method getSequenceNumber() + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class PatronInformationResponse extends SIP2Response { diff --git a/src/Response/PatronStatusResponse.php b/src/Response/PatronStatusResponse.php index 52240f8..c3c49ce 100644 --- a/src/Response/PatronStatusResponse.php +++ b/src/Response/PatronStatusResponse.php @@ -18,6 +18,10 @@ * @method getScreenMessage() * @method getPrintLine() * @method getSequenceNumber() + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class PatronStatusResponse extends SIP2Response { diff --git a/src/Response/RenewAllResponse.php b/src/Response/RenewAllResponse.php index 1867ca2..73aa4c7 100644 --- a/src/Response/RenewAllResponse.php +++ b/src/Response/RenewAllResponse.php @@ -15,6 +15,10 @@ * @method getScreenMessage() * @method getPrintLine() * @method getSequenceNumber() + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class RenewAllResponse extends SIP2Response { diff --git a/src/Response/RenewResponse.php b/src/Response/RenewResponse.php index 5d4647c..4d77a9a 100644 --- a/src/Response/RenewResponse.php +++ b/src/Response/RenewResponse.php @@ -25,6 +25,10 @@ * @method getScreenMessage() * @method getPrintLine() * @method getSequenceNumber() + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ class RenewResponse extends SIP2Response { diff --git a/src/Response/SIP2Response.php b/src/Response/SIP2Response.php index f1b3b34..1369077 100644 --- a/src/Response/SIP2Response.php +++ b/src/Response/SIP2Response.php @@ -11,6 +11,10 @@ * * Derived classes declare the variable data they expect to receive, and provide a parser for the 'fixed' * fields + * + * @licence https://opensource.org/licenses/MIT + * @copyright John Wohlers + * @copyright Paul Dixon */ abstract class SIP2Response extends SIP2Message { From 4fa21886fc00b4bc7fa173001e321e67ee3649b4 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sun, 29 Jul 2018 11:43:14 +0100 Subject: [PATCH 23/35] Improve migration documentation --- CHANGELOG.md | 7 +++-- MIGRATION.md | 57 ++++++++++++++++++++++++++++++++++++ README.md | 81 +++++++++++++++++++++++++++++++--------------------- 3 files changed, 109 insertions(+), 36 deletions(-) create mode 100644 MIGRATION.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 27c3de2..6656b4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - MIT License adopted - prior releases were GPL - PSR-2 formatting/naming conventions, including change of classname from sip2 to SIP2Client +- PSR-3 logger support +- Classes for each request and response type - Support for binding to particular interface - Full unit tests @@ -16,11 +18,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Nothing ### Fixed -- Ensure parseHoldResponse copes with optional elements -- Ensure getMessage properly handles retries +- Ensure client properly handles retries in event of CRC failure ### Removed -- debug flag, replaced with PSR-3 logger support +- public methods and variables all removed - see [MIGRATION](MIGRATION.md) ### Security - Nothing diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..67ecb67 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,57 @@ +# Migration from v1.0 + +While this is derived from [cap60552/php-sip2](https://github.com/cap60552/php-sip2) +many changes were made to make the code more easily testable and less complex. As a result, +this isn't a drop-in replacement. Here's the main differences for a typical scenaio: + +## Before +```php +// create object +$mysip = new sip2; + +// Set host name +$mysip->hostname = 'server.example.com'; +$mysip->port = 6002; + +// Identify a patron +$mysip->patron = '101010101'; +$mysip->patronpwd = '010101'; + +// connect to SIP server +$result = $mysip->connect(); + +// Get Charged Items Raw response +$in = $mysip->msgPatronInformation('charged'); + +// parse the raw response into an array +$result = $mysip->parsePatronInfoResponse( $mysip->get_message($in) ); + +// extra data from result +$status = $result['fixed']['PatronStatus']; +$name = $result['variable']['AE']; +``` + +## After +```php +use lordelph\SIP2\SIP2Client; +use lordelph\SIP2\Request\PatronInformationRequest; + +// instantiate client, set any defaults used for all requests +$mysip = new SIP2Client; +$mysip->setDefault('PatronIdentifier', '101010101'); +$mysip->setDefault('PatronPassword', '010101'); + +// connect to SIP server +$mysip->connect("server.example.com:6002"); + +// Get Charged Items Raw response +$request=new PatronInformationRequest(); +$request->setType('charged'); + +$response = $mysip->sendRequest($request); + +// extra data from result +$status = $response->getPatronStatus(); +$name = $response->getPersonalName(); + +``` \ No newline at end of file diff --git a/README.md b/README.md index 72f806c..f1c9d5a 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,13 @@ PHP client library to facilitate communication with Integrated Library System (I This is derived from [cap60552/php-sip2](https://github.com/cap60552/php-sip2) by John Wohlers, with following improvements: +* MIT license (with consent of original author who used GPL) +* Separate classes for each SIP2 request and response type +* Ability to bind to specific interface when making requests * Full unit tests * PSR-2 formatting conventions * PSR-3 logging * PSR-4 auto-loading -* Ability to bind to specific interface when making requests -* MIT license (with consent of original author who used GPL) ## Install @@ -28,53 +29,67 @@ Via Composer $ composer require lordelph/php-sip2 ``` -## Migration from v1.0 - -If you want to switch to using this class from [cap60552/php-sip2](https://github.com/cap60552/php-sip2), -you need to change instantations of `sip2` to `SIP2Client` and ensure you include the class with -`use lordelph\SIP2\SIP2Client` +## Example +Here's a typical example of use ```php -#before -$mysip = new sip2; - -#after use lordelph\SIP2\SIP2Client; +use lordelph\SIP2\Request\PatronInformationRequest; +// instantiate client, set any defaults used for all requests, +// typically you might set the PatronIdentifier and PatronPassword +// so that you don't have to set this for every request $mysip = new SIP2Client; -``` +$mysip->setDefault('PatronIdentifier', '101010101'); +$mysip->setDefault('PatronPassword', '010101'); -Also, the `get_message` method is now `getMessage` +// connect to SIP server +$mysip->connect("server.example.com:6002"); -## Usage +// to make a request, instantiate relevant request class +// and configure as appropriate +$request=new PatronInformationRequest(); +$request->setType('charged'); -``` php -use lordelph\SIP2\SIP2Client; +// send the request, obtaining, in this case a +// PatronInformationResponse object +$response = $mysip->sendRequest($request); +// now we can obtain information from the result object +$status = $response->getPatronStatus(); +$name = $response->getPersonalName(); -// create object -$mysip = new SIP2Client; +``` -// Set host name -$mysip->hostname = 'server.example.com'; -$mysip->port = 6002; +## SIP2 requests and responses -// Identify a patron -$mysip->patron = '101010101'; -$mysip->patronpwd = '010101'; +All requests defined in SIP2 are available - note that not all SIP2 +services will support every request. -// connect to SIP server -$result = $mysip->connect(); -// build a request for patron information -$request = $mysip->msgPatronInformation('charged'); +| Request | Response | +| ------------- | ------------- | +| PatronStatusRequest | PatronStatusResponse | +| CheckOutRequest | CheckOutResponse | +| CheckInRequest | CheckInResponse | +| BlockPatronRequest | PatronStatusResponse | +| SCStatusRequest | ASCStatusResponse | +| RequestACSResendRequest | _previous response_ | +| LoginRequest | LoginResponse | +| PatronInformationRequest | PatronInformationResponse | +| EndPatronSessionRequest | EndSessionResponse | +| FeePaidRequest | FeePaidResponse | +| ItemInformationRequest | ItemInformationResponse | +| ItemStatusUpdateRequest | ItemStatusUpdateResponse | +| PatronEnableRequest | PatronEnableResponse | +| HoldRequest | HoldResponse | +| RenewRequest | RenewResponse | +| RenewAllRequest | RenewAllResponse | -// send that request and obtain a raw response -$response = $mysip->getMessage($request) -// parse the raw response into an array -$result = $mysip->parsePatronInfoResponse($response); -``` +## Migration from v1.0 + +See [MIGRATION](MIGRATION.md) for details. ## Binding to a specific local outbound address From d176f7f041e8b99cf3006a9a0371fb5810f3c4b3 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sun, 29 Jul 2018 11:43:40 +0100 Subject: [PATCH 24/35] Improve phpdoc comments --- src/Exception/LogicException.php | 5 ++++- src/Exception/RuntimeException.php | 5 ++++- src/Exception/SIP2ClientException.php | 6 ++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Exception/LogicException.php b/src/Exception/LogicException.php index 5224619..562d66b 100644 --- a/src/Exception/LogicException.php +++ b/src/Exception/LogicException.php @@ -3,7 +3,10 @@ namespace lordelph\SIP2\Exception; /** - * Class LogicException represents an integration problem - the code is being used incorrectly + * LogicException represents an integration problem - the code is being used incorrectly + * + * @licence https://opensource.org/licenses/MIT + * @copyright Paul Dixon */ class LogicException extends \LogicException implements SIP2ClientException { diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php index ce80895..fa19a86 100644 --- a/src/Exception/RuntimeException.php +++ b/src/Exception/RuntimeException.php @@ -3,8 +3,11 @@ namespace lordelph\SIP2\Exception; /** - * Class RuntimeException is fired for conditions which arise only at runtime, e.g. external services being down + * RuntimeException is fired for conditions which arise only at runtime, e.g. external services being down * bad CRCs from remote services + * + * @licence https://opensource.org/licenses/MIT + * @copyright Paul Dixon */ class RuntimeException extends \RuntimeException implements SIP2ClientException { diff --git a/src/Exception/SIP2ClientException.php b/src/Exception/SIP2ClientException.php index fc673d4..d499f59 100644 --- a/src/Exception/SIP2ClientException.php +++ b/src/Exception/SIP2ClientException.php @@ -3,9 +3,11 @@ namespace lordelph\SIP2\Exception; /** - * This is just a 'marker interface' that all exceptions thrown by SIP2Client will + * SIP2ClientException is just a 'marker interface' that all exceptions thrown by SIP2Client will * implement, allowing integrators to catch all exceptions - * @package LibLynx\Connect\Exception + * + * @licence https://opensource.org/licenses/MIT + * @copyright Paul Dixon */ interface SIP2ClientException { From 7c8c7d6b1c3f92187cd2e183e09e512d79f08781 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sun, 29 Jul 2018 11:44:12 +0100 Subject: [PATCH 25/35] Improve phpdoc comments --- src/SIP2Message.php | 73 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/src/SIP2Message.php b/src/SIP2Message.php index fe71ba8..b72c817 100644 --- a/src/SIP2Message.php +++ b/src/SIP2Message.php @@ -4,6 +4,15 @@ use lordelph\SIP2\Exception\LogicException; +/** + * SIP2Message Class + * + * This class provides support for getting/setting request and response variables with some magic methods + * as well as SIP2 CRC calculation + * + * @licence https://opensource.org/licenses/MIT + * @copyright Paul Dixon + */ abstract class SIP2Message { /** @@ -16,9 +25,13 @@ abstract class SIP2Message protected $timestamp = null; - protected static function crc($buf) + /** + * Calculate SIP2 CRC value + * @param string $buf + * @return string + */ + protected static function crc(string $buf) : string { - /* Calculate CRC */ $sum = 0; $len = strlen($buf); @@ -32,12 +45,42 @@ protected static function crc($buf) return substr(sprintf("%4X", $crc), -4, 4); } - public function hasVariable($name) + /** + * Check if class supports given variable + * @param string $name + * @return bool + */ + public function hasVariable(string $name) : bool { return isset($this->var[$name]); } - public function getVariable($varName) + /** + * Set default value for a variable - this can be overridden by setVariable + * + * This method will allow you to attempt to set a default for a variable which the derived class + * does not support, in which case it is silently ignored. + * + * @param string $name + * @param string|array $value + */ + public function setDefault(string $name, $value) + { + if ($this->hasVariable($name)) { + $this->var[$name]['default'] = $value; + } + } + + /** + * Get value of specific variable. + * + * There is also a magic method which instead of getVariable('PatronStatus') would instead allow + * getPatronStatus() to be called + * + * @param string $varName + * @return string|array + */ + public function getVariable(string $varName) { $this->ensureVariableExists($varName); return $this->var[$varName]['value'] ?? @@ -45,6 +88,12 @@ public function getVariable($varName) $this->handleMissing($varName); } + /** + * Get name/values of all variables + * + * This can be useful for building JSON-based results of SIP2 responses + * @return array + */ public function getAll() { $result=[]; @@ -54,6 +103,14 @@ public function getAll() return $result; } + /** + * Set variable + * + * Variables which are defined as timestamps are converted to SIP2 date format automatically + * + * @param $varName + * @param string|array $value + */ public function setVariable($varName, $value) { $this->ensureVariableExists($varName); @@ -69,15 +126,15 @@ public function setVariable($varName, $value) break; } - return $this->var[$varName]['value'] = $value; + $this->var[$varName]['value'] = $value; } /** * If $varName is defined as an array, this will append given value. Otherwise value is set as normal - * @param $varName - * @param $value + * @param string $varName + * @param string $value */ - public function addVariable($varName, $value) + public function addVariable(string $varName, $value) { $this->ensureVariableExists($varName); $type = $this->var[$varName]['type'] ?? 'string'; From 13405ad088511bd547212c57cb8ce170ed03dc5c Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sun, 29 Jul 2018 11:44:46 +0100 Subject: [PATCH 26/35] Add setDefault and remove no-longer-required member variables --- src/SIP2Client.php | 69 ++++++++++++---------------------------------- 1 file changed, 17 insertions(+), 52 deletions(-) diff --git a/src/SIP2Client.php b/src/SIP2Client.php index 055190d..13968bc 100644 --- a/src/SIP2Client.php +++ b/src/SIP2Client.php @@ -8,11 +8,9 @@ * This class provides a method of communicating with an Integrated * Library System using 3M's SIP2 standard. * - * @package - * @author John Wohlers * @licence https://opensource.org/licenses/MIT * @copyright John Wohlers - * @version 2.0.0 + * @copyright Paul Dixon */ use lordelph\SIP2\Exception\RuntimeException; @@ -35,62 +33,22 @@ class SIP2Client implements LoggerAwareInterface { use LoggerAwareTrait; - //----------------------------------------------------- - // connection configuration - //----------------------------------------------------- - - /** @var int maximum number of resends in the event of CRC failure */ - public $maxretry = 3; - - //----------------------------------------------------- - // patron credentials - //----------------------------------------------------- - - /** @var string patron identifier / barcode */ - public $patron = ''; - - /** @var string patron password / pin */ - public $patronpwd = ''; - //----------------------------------------------------- // request options //----------------------------------------------------- - /** @var string language code - 001 is English */ - public $language = '001'; - /** - * @var string terminator for requests. This should be just \r (0x0d) according to docs, but some vendors - * require \r\n + * @var array name=>value request defaults used for every request */ - public $msgTerminator = "\r\n"; - - /** @var string variable length field terminator */ - public $fldTerminator = '|'; - - /** @var int encryption algorithm for user id using during login 0=unencrypted */ - public $uidAlgorithm = 0; - - /** @var int encryption algorithm for user password using during login (no docs for this) */ - public $passwordAlgorithm = 0; - - /** @var string Default location used in some request messages */ - public $location = ''; - - /** @var string Institution ID */ - public $institutionId = 'WohlersSIP'; - - /** @var string Patron identifier */ - public $patronId = ''; - - /** @var string Terminal password */ - public $terminalPassword = ''; - + private $default=[]; //----------------------------------------------------- - // internal socket handling + // connection handling //----------------------------------------------------- + /** @var int maximum number of resends in the event of CRC failure */ + public $maxretry = 3; + /** @var Socket */ private $socket; @@ -101,14 +59,17 @@ class SIP2Client implements LoggerAwareInterface * Constructor allows you to provide a PSR-3 logger, but you can also use the setLogger method * later on. * - * You can also specific the IP address you want to bind to, which is useful if you have multiple local - * IPs, but you want the remote SIP2 service to see a specific IP address - * * @param LoggerInterface|null $logger */ public function __construct(LoggerInterface $logger = null) { $this->logger = $logger ?? new NullLogger(); + $this->setDefault('InstitutionId', 'WohlersSIP'); + } + + public function setDefault($name, $value) + { + $this->default[$name] = $value; } /** @@ -142,6 +103,10 @@ private function getSocketFactory() */ public function sendRequest(SIP2Request $request) : SIP2Response { + foreach ($this->default as $name => $value) { + $request->setDefault($name, $value); + } + $raw = $this->getRawResponse($request); return SIP2Response::parse($raw); } From 977034b2d078fdb7cd936ee363ed0d66187f5f72 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sun, 29 Jul 2018 12:13:48 +0100 Subject: [PATCH 27/35] Add casts to deal with scrutinizer warning --- src/Request/FeePaidRequest.php | 4 ++-- src/Request/SCStatusRequest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Request/FeePaidRequest.php b/src/Request/FeePaidRequest.php index c2acd71..cc012e1 100644 --- a/src/Request/FeePaidRequest.php +++ b/src/Request/FeePaidRequest.php @@ -57,8 +57,8 @@ public function getMessageString($withSeq = true, $withCrc = true): string { $this->newMessage('37'); $this->addFixedOption($this->datestamp(), 18); - $this->addFixedOption(sprintf('%02d', $this->getVariable('FeeType')), 2); - $this->addFixedOption(sprintf('%02d', $this->getVariable('PaymentType')), 2); + $this->addFixedOption(sprintf('%02d', (string)$this->getVariable('FeeType')), 2); + $this->addFixedOption(sprintf('%02d', (string)$this->getVariable('PaymentType')), 2); $this->addFixedOption($this->getVariable('CurrencyType'), 3); // due to currency format localization, it is up to the programmer diff --git a/src/Request/SCStatusRequest.php b/src/Request/SCStatusRequest.php index 54bbf2c..13684a3 100644 --- a/src/Request/SCStatusRequest.php +++ b/src/Request/SCStatusRequest.php @@ -32,7 +32,7 @@ public function getMessageString($withSeq = true, $withCrc = true): string $this->newMessage('99'); $this->addFixedOption($this->getVariable('Status'), 1); $this->addFixedOption($this->getVariable('Width'), 3); - $this->addFixedOption(sprintf("%03.2f", $this->getVariable('Version')), 4); + $this->addFixedOption(sprintf("%03.2f", (float)$this->getVariable('Version')), 4); return $this->returnMessage($withSeq, $withCrc); } From 2dc8ab14f02196ca3bce2b76d0f04973570f1ab7 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sun, 29 Jul 2018 12:14:09 +0100 Subject: [PATCH 28/35] Add test for setDefault --- tests/Request/SIP2RequestTest.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/Request/SIP2RequestTest.php b/tests/Request/SIP2RequestTest.php index 472178f..5ffa039 100644 --- a/tests/Request/SIP2RequestTest.php +++ b/tests/Request/SIP2RequestTest.php @@ -29,6 +29,15 @@ public function testSequencing() } } + /** + * Test that getting a variable after setting a default works... + */ + public function testDefault() + { + $login = new LoginRequest(); + $login->setDefault('SIPLogin', 'foo'); + $this->assertEquals('foo', $login->getVariable('SIPLogin')); + } /** * Test that getting a variable before setting one with no default throws exception From 80bc4536b32cfd8688f9754e125c6381ea37aa00 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sun, 29 Jul 2018 12:53:51 +0100 Subject: [PATCH 29/35] Corrected phpdoc comment --- src/Request/SCStatusRequest.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Request/SCStatusRequest.php b/src/Request/SCStatusRequest.php index 13684a3..2c7e909 100644 --- a/src/Request/SCStatusRequest.php +++ b/src/Request/SCStatusRequest.php @@ -3,17 +3,15 @@ namespace lordelph\SIP2\Request; /** - * BlockPatronRequest message sends SC status to the ACS. It requires an ACS Status Response message reply from the + * SCStatusRequest message sends SC status to the ACS. It requires an ACS Status Response message reply from the * ACS. This message will be the first message sent by the SC to the ACS once a connection has been established * (exception: the Login Message may be sent first to login to an ACS server program). The ACS will respond with a * message that establishes some of the rules to be followed by the SC and establishes some parameters needed for * further communication. * - * @method setCardRetained(string $yn) - * @method setInstitutionId(string $institutionId) - * @method setMessage(string $message) - * @method setPatronIdentifier(string $patron) - * @method setTerminalPassword(string $terminalPassword) + * @method setStatus(string $status) + * @method setWidth(string $width) + * @method setVersion(string $version) * * @licence https://opensource.org/licenses/MIT * @copyright John Wohlers From 12d357f2c0d337e89299c09d852a5a5e6d859fe4 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sun, 29 Jul 2018 12:54:10 +0100 Subject: [PATCH 30/35] Add constants for request variables --- src/Request/SIP2Request.php | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/Request/SIP2Request.php b/src/Request/SIP2Request.php index 4567b75..f1fe0d8 100644 --- a/src/Request/SIP2Request.php +++ b/src/Request/SIP2Request.php @@ -13,6 +13,39 @@ */ abstract class SIP2Request extends SIP2Message { + // can use these constants for getVariable/setVariable/setDefault etc + + const CANCEL = 'Cancel'; + const END = 'End'; + const EXPIRY_DATE = 'ExpiryDate'; + const FEE_ACKNOWLEDGED = 'FeeAcknowledged'; + const FEE_IDENTIFIER = 'FeeIdentifier'; + const HOLD_TYPE = 'HoldType'; + const INSTITUTION_ID = 'InstitutionID'; + const ITEM_IDENTIFIER = 'ItemIdentifier'; + const ITEM_LOCATION = 'ItemLocation'; + const ITEM_PROPERTIES = 'ItemProperties'; + const ITEM_TITLE = 'ItemTitle'; + const LOCATION = 'Location'; + const MESSAGE = 'Message'; + const NB_DATEDUE = 'NBDateDue'; + const NO_BLOCK = 'NoBlock'; + const PASSWORD_ALGORITHM = 'PasswordAlgorithm'; + const PATRON_IDENTIFIER = 'PatronIdentifier'; + const PATRON_PASSWORD = 'PatronPassword'; + const PAYMENT_AMOUNT = 'PaymentAmount'; + const PICKUP_LOCATION = 'PickupLocation'; + const SIP_LOGIN = 'SIPLogin'; + const SIP_PASSWORD = 'SIPPassword'; + const START = 'Start'; + const STATUS = 'Status'; + const TERMINAL_PASSWORD = 'TerminalPassword'; + const THIRD_PARTY = 'ThirdParty'; + const TRANSACTION_IDENTIFIER = 'TransactionIdentifier'; + const USERID_ALGORITHM = 'UserIdAlgorithm'; + const VERSION = 'Version'; + const WIDTH = 'Width'; + /** @var string request is built up here */ private $msgBuild = ''; @@ -43,7 +76,7 @@ public static function resetSequence() /** * Derived class must implement this to build its SIP2 request */ - abstract public function getMessageString($withSeq = true, $withCrc = true) : string; + abstract public function getMessageString($withSeq = true, $withCrc = true): string; /** @@ -83,7 +116,7 @@ protected function addVarOption($field, $value, $optional = false) return true; } - protected function returnMessage($withSeq = true, $withCrc = true) : string + protected function returnMessage($withSeq = true, $withCrc = true): string { /* Finalizes the message and returns it. Message will remain in msgBuild until newMessage is called */ if ($withSeq) { From 20c7779cdc313193ca7a8e455b7fafb4ea32a593 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sun, 29 Jul 2018 12:58:27 +0100 Subject: [PATCH 31/35] Update credits and keywords --- composer.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 118bc43..0667a8d 100644 --- a/composer.json +++ b/composer.json @@ -3,12 +3,20 @@ "type": "library", "description": "Communicate with Integrated Library System (ILS) servers via 3M's SIP2", "keywords": [ - "lordelph", + "SIP2", + "SIP2Client", + "ILS", "php-sip2" ], "homepage": "https://github.com/lordelph/php-sip2", "license": "MIT", "authors": [ + { + "name": "John Wohlers", + "email": "john@wohlershome.net", + "homepage": "https://github.com/cap60552", + "role": "Developer" + }, { "name": "Paul Dixon", "email": "paul@elphin.com", From 88f27bc9ceec70823a58b8b6782d61320275473f Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sun, 29 Jul 2018 13:01:56 +0100 Subject: [PATCH 32/35] Add date of v2 release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6656b4c..b76448d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to `php-sip2` will be documented in this file. Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. -## 2.0.0 - 2018-07-xx +## 2.0.0 - 2018-07-29 ### Added - MIT License adopted - prior releases were GPL From 2967495df38c8fc92fccd126263c557b72f001d6 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Wed, 1 Aug 2018 07:32:24 +0100 Subject: [PATCH 33/35] Allow client to specify timeout for connection --- src/SIP2Client.php | 6 +++--- tests/AbstractSIP2ClientTest.php | 2 +- tests/SIP2ClientTest.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/SIP2Client.php b/src/SIP2Client.php index 13968bc..21590dc 100644 --- a/src/SIP2Client.php +++ b/src/SIP2Client.php @@ -164,13 +164,13 @@ private function getRawResponse(SIP2Request $request, $depth = 0) * * @param string $address ip:port of remote SIP2 service * @param string|null $bind local ip to bind socket to - * @throws RuntimeException if connection cannot be established + * @param int $timeout number of seconds to allow for connection to succeed */ - public function connect($address, $bind = null) + public function connect($address, $bind = null, $timeout = 15) { $this->logger->debug("SIP2Client: Attempting connection to $address"); - $this->socket = $this->getSocketFactory()->createFromString($address, $scheme); + $this->socket = $this->getSocketFactory()->createClient($address, $timeout); try { if (!empty($bind)) { diff --git a/tests/AbstractSIP2ClientTest.php b/tests/AbstractSIP2ClientTest.php index b992560..7cd4ecb 100644 --- a/tests/AbstractSIP2ClientTest.php +++ b/tests/AbstractSIP2ClientTest.php @@ -98,7 +98,7 @@ protected function createMockSIP2Server(array $responses) //our factory just returns our mock $factory = $this->prophesize(\Socket\Raw\Factory::class); - $factory->createFromString( + $factory->createClient( Argument::type('string'), Argument::any() )->willReturn($socket->reveal()); diff --git a/tests/SIP2ClientTest.php b/tests/SIP2ClientTest.php index 31a75c5..3abfbd7 100644 --- a/tests/SIP2ClientTest.php +++ b/tests/SIP2ClientTest.php @@ -139,7 +139,7 @@ protected function createUnconnectableMockSIP2Server() //our factory will always fail to connect... $factory = $this->prophesize(\Socket\Raw\Factory::class); - $factory->createFromString( + $factory->createClient( Argument::type('string'), Argument::any() )->willReturn($socket->reveal()); @@ -161,7 +161,7 @@ private function createBindingTestMockSIP2Server() //our factory will always fail to connect... $factory = $this->prophesize(\Socket\Raw\Factory::class); - $factory->createFromString( + $factory->createClient( Argument::type('string'), Argument::any() )->willReturn($socket->reveal()); From 883cd8fef953e7fa6d9562661d4df1fc9c7ac63f Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Wed, 1 Aug 2018 07:35:49 +0100 Subject: [PATCH 34/35] Add note about 2.0.1 --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b76448d..5a39789 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to `php-sip2` will be documented in this file. Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. +## 2.0.1 - 2018-08-01 + +### Added +- SIP2Client::connect now accepts a timeout parameter, default 15 seconds + + ## 2.0.0 - 2018-07-29 ### Added @@ -21,7 +27,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Ensure client properly handles retries in event of CRC failure ### Removed -- public methods and variables all removed - see [MIGRATION](MIGRATION.md) +- original v1 classname changed +- original public methods and variables all removed - see [MIGRATION](MIGRATION.md) ### Security - Nothing From 51625342f3f4ced36945991bf08d9c9640d517d1 Mon Sep 17 00:00:00 2001 From: Paul Dixon Date: Sat, 11 Aug 2018 18:59:24 +0100 Subject: [PATCH 35/35] Change namespace to cap60552 --- CONTRIBUTING.md | 2 +- MIGRATION.md | 4 +-- README.md | 32 +++++++++---------- composer.json | 10 +++--- src/Exception/LogicException.php | 2 +- src/Exception/RuntimeException.php | 2 +- src/Exception/SIP2ClientException.php | 2 +- src/Request/BlockPatronRequest.php | 2 +- src/Request/CheckInRequest.php | 2 +- src/Request/CheckOutRequest.php | 2 +- src/Request/EndPatronSessionRequest.php | 2 +- src/Request/FeePaidRequest.php | 2 +- src/Request/HoldRequest.php | 2 +- src/Request/ItemInformationRequest.php | 2 +- src/Request/ItemStatusUpdateRequest.php | 2 +- src/Request/LoginRequest.php | 2 +- src/Request/PatronEnableRequest.php | 2 +- src/Request/PatronInformationRequest.php | 2 +- src/Request/PatronStatusRequest.php | 2 +- src/Request/RenewAllRequest.php | 2 +- src/Request/RenewRequest.php | 2 +- src/Request/RequestACSResendRequest.php | 2 +- src/Request/SCStatusRequest.php | 2 +- src/Request/SIP2Request.php | 4 +-- src/Response/ACSStatusResponse.php | 2 +- src/Response/CheckInResponse.php | 2 +- src/Response/CheckOutResponse.php | 2 +- src/Response/EndSessionResponse.php | 2 +- src/Response/FeePaidResponse.php | 2 +- src/Response/HoldResponse.php | 2 +- src/Response/ItemInformationResponse.php | 2 +- src/Response/ItemStatusUpdateResponse.php | 2 +- src/Response/LoginResponse.php | 2 +- src/Response/PatronEnableResponse.php | 2 +- src/Response/PatronInformationResponse.php | 2 +- src/Response/PatronStatusResponse.php | 2 +- src/Response/RenewAllResponse.php | 2 +- src/Response/RenewResponse.php | 2 +- src/Response/SIP2Response.php | 8 ++--- src/SIP2Client.php | 8 ++--- src/SIP2Message.php | 4 +-- tests/AbstractSIP2ClientTest.php | 6 ++-- tests/Request/BlockPatronRequestTest.php | 4 +-- tests/Request/CheckInRequestTest.php | 4 +-- tests/Request/CheckOutRequestTest.php | 4 +-- tests/Request/EndPatronSessionRequestTest.php | 4 +-- tests/Request/FeePaidRequestTest.php | 4 +-- tests/Request/HoldRequestTest.php | 4 +-- tests/Request/ItemInformationRequestTest.php | 4 +-- tests/Request/ItemStatusUpdateRequestTest.php | 4 +-- tests/Request/LoginRequestTest.php | 4 +-- tests/Request/PatronEnableRequestTest.php | 4 +-- .../Request/PatronInformationRequestTest.php | 4 +-- tests/Request/PatronStatusRequestTest.php | 4 +-- tests/Request/RenewAllRequestTest.php | 4 +-- tests/Request/RenewRequestTest.php | 4 +-- tests/Request/RequestACSResendRequestTest.php | 4 +-- tests/Request/SCStatusRequestTest.php | 4 +-- tests/Request/SIP2RequestTest.php | 6 ++-- tests/Response/ACSStatusResponseTest.php | 6 ++-- tests/Response/CheckInResponseTest.php | 6 ++-- tests/Response/CheckOutResponseTest.php | 6 ++-- tests/Response/EndSessionResponseTest.php | 6 ++-- tests/Response/FeePaidResponseTest.php | 6 ++-- tests/Response/HoldResponseTest.php | 6 ++-- .../Response/ItemInformationResponseTest.php | 6 ++-- .../Response/ItemStatusUpdateResponseTest.php | 6 ++-- tests/Response/LoginResponseTest.php | 6 ++-- tests/Response/PatronEnableResponseTest.php | 6 ++-- .../PatronInformationResponseTest.php | 6 ++-- tests/Response/PatronStatusResponseTest.php | 6 ++-- tests/Response/RenewAllResponseTest.php | 8 ++--- tests/Response/RenewResponseTest.php | 6 ++-- tests/Response/SIP2ResponseTest.php | 12 +++---- tests/SIP2ClientTest.php | 8 ++--- 75 files changed, 159 insertions(+), 161 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d0f82b7..a5a547c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Contributions are **welcome** and will be fully **credited**. -We accept contributions via Pull Requests on [Github](https://github.com/lordelph/php-sip2). +We accept contributions via Pull Requests on [Github](https://github.com/cap60552/php-sip2). ## Pull Requests diff --git a/MIGRATION.md b/MIGRATION.md index 67ecb67..2041cc0 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -33,8 +33,8 @@ $name = $result['variable']['AE']; ## After ```php -use lordelph\SIP2\SIP2Client; -use lordelph\SIP2\Request\PatronInformationRequest; +use cap60552\SIP2\SIP2Client; +use cap60552\SIP2\Request\PatronInformationRequest; // instantiate client, set any defaults used for all requests $mysip = new SIP2Client; diff --git a/README.md b/README.md index f1c9d5a..a259146 100644 --- a/README.md +++ b/README.md @@ -26,15 +26,15 @@ with following improvements: Via Composer ``` bash -$ composer require lordelph/php-sip2 +$ composer require cap60552/php-sip2 ``` ## Example Here's a typical example of use ```php -use lordelph\SIP2\SIP2Client; -use lordelph\SIP2\Request\PatronInformationRequest; +use cap60552\SIP2\SIP2Client; +use cap60552\SIP2\Request\PatronInformationRequest; // instantiate client, set any defaults used for all requests, // typically you might set the PatronIdentifier and PatronPassword @@ -101,7 +101,7 @@ To do this, specify the IP with `bindTo` public member variable *before* calling ``` php -use lordelph\SIP2\SIP2Client; +use cap60552\SIP2\SIP2Client; // create object @@ -151,18 +151,18 @@ The MIT License (MIT). Please see [License File](LICENSE.md) for more informatio Note that prior to v2.0.0, the GPL licence was used. The original author, John Wohlers, kindly agreed to allow the MIT license terms. -[ico-version]: https://img.shields.io/packagist/v/lordelph/php-sip2.svg?style=flat-square +[ico-version]: https://img.shields.io/packagist/v/cap60552/php-sip2.svg?style=flat-square [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square -[ico-travis]: https://img.shields.io/travis/lordelph/php-sip2/master.svg?style=flat-square -[ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/lordelph/php-sip2.svg?style=flat-square -[ico-code-quality]: https://img.shields.io/scrutinizer/g/lordelph/php-sip2.svg?style=flat-square -[ico-downloads]: https://img.shields.io/packagist/dt/lordelph/php-sip2.svg?style=flat-square - -[link-packagist]: https://packagist.org/packages/lordelph/php-sip2 -[link-travis]: https://travis-ci.org/lordelph/php-sip2 -[link-scrutinizer]: https://scrutinizer-ci.com/g/lordelph/php-sip2/code-structure -[link-code-quality]: https://scrutinizer-ci.com/g/lordelph/php-sip2 -[link-downloads]: https://packagist.org/packages/lordelph/php-sip2 +[ico-travis]: https://img.shields.io/travis/cap60552/php-sip2/master.svg?style=flat-square +[ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/cap60552/php-sip2.svg?style=flat-square +[ico-code-quality]: https://img.shields.io/scrutinizer/g/cap60552/php-sip2.svg?style=flat-square +[ico-downloads]: https://img.shields.io/packagist/dt/cap60552/php-sip2.svg?style=flat-square + +[link-packagist]: https://packagist.org/packages/cap60552/php-sip2 +[link-travis]: https://travis-ci.org/cap60552/php-sip2 +[link-scrutinizer]: https://scrutinizer-ci.com/g/cap60552/php-sip2/code-structure +[link-code-quality]: https://scrutinizer-ci.com/g/cap60552/php-sip2 +[link-downloads]: https://packagist.org/packages/cap60552/php-sip2 [link-author1]: https://github.com/cap60552 -[link-author2]: https://github.com/lordelph +[link-author2]: https://github.com/cap60552 [link-contributors]: ../../contributors diff --git a/composer.json b/composer.json index 0667a8d..1990297 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "lordelph/php-sip2", + "name": "cap60552/php-sip2", "type": "library", "description": "Communicate with Integrated Library System (ILS) servers via 3M's SIP2", "keywords": [ @@ -8,7 +8,7 @@ "ILS", "php-sip2" ], - "homepage": "https://github.com/lordelph/php-sip2", + "homepage": "https://github.com/cap60552/php-sip2", "license": "MIT", "authors": [ { @@ -20,7 +20,7 @@ { "name": "Paul Dixon", "email": "paul@elphin.com", - "homepage": "https://github.com/lordelph", + "homepage": "https://github.com/cap60552", "role": "Developer" } ], @@ -38,12 +38,12 @@ }, "autoload": { "psr-4": { - "lordelph\\SIP2\\": "src" + "cap60552\\SIP2\\": "src" } }, "autoload-dev": { "psr-4": { - "lordelph\\SIP2\\": "tests" + "cap60552\\SIP2\\": "tests" } }, "scripts": { diff --git a/src/Exception/LogicException.php b/src/Exception/LogicException.php index 562d66b..73f6906 100644 --- a/src/Exception/LogicException.php +++ b/src/Exception/LogicException.php @@ -1,6 +1,6 @@ */ -use lordelph\SIP2\Exception\RuntimeException; -use lordelph\SIP2\Request\SIP2Request; -use lordelph\SIP2\Response\SIP2Response; +use cap60552\SIP2\Exception\RuntimeException; +use cap60552\SIP2\Request\SIP2Request; +use cap60552\SIP2\Response\SIP2Response; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerInterface; diff --git a/src/SIP2Message.php b/src/SIP2Message.php index b72c817..574eb7a 100644 --- a/src/SIP2Message.php +++ b/src/SIP2Message.php @@ -1,8 +1,8 @@