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..e2ef079 --- /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: 1 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..50f9347 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,30 @@ +dist: trusty +language: php + +php: + - 7.0 + - 7.1 + - 7.2 + +# 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 + +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" == '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..5a39789 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,39 @@ +# 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.1 - 2018-08-01 + +### Added +- SIP2Client::connect now accepts a timeout parameter, default 15 seconds + + +## 2.0.0 - 2018-07-29 + +### 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 + +### Deprecated +- Nothing + +### Fixed +- Ensure client properly handles retries in event of CRC failure + +### Removed +- original v1 classname changed +- original public methods and variables all removed - see [MIGRATION](MIGRATION.md) + +### 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..a5a547c --- /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/cap60552/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/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..2041cc0 --- /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 cap60552\SIP2\SIP2Client; +use cap60552\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/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..a259146 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,168 @@ -# 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. + +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 + +## Install + +Via Composer + +``` bash +$ composer require cap60552/php-sip2 +``` + +## Example + +Here's a typical example of use +```php +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 +// so that you don't have to set this for every request +$mysip = new SIP2Client; +$mysip->setDefault('PatronIdentifier', '101010101'); +$mysip->setDefault('PatronPassword', '010101'); + +// connect to SIP server +$mysip->connect("server.example.com:6002"); + +// to make a request, instantiate relevant request class +// and configure as appropriate +$request=new PatronInformationRequest(); +$request->setType('charged'); + +// 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(); -To install this package, run this command: -```sh -composer require cap60552/php-sip2 ``` -## 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 +## SIP2 requests and responses + +All requests defined in SIP2 are available - note that not all SIP2 +services will support every request. + + +| 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 | + + +## Migration from v1.0 + +See [MIGRATION](MIGRATION.md) for details. + +## 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 cap60552\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. + +## 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/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/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/cap60552 +[link-contributors]: ../../contributors diff --git a/composer.json b/composer.json index 00f77be..1990297 100644 --- a/composer.json +++ b/composer.json @@ -1,23 +1,62 @@ { "name": "cap60552/php-sip2", - "description": "PHP class library to facilitate communication with Integrated Library System (ILS) servers via 3M's SIP2.", - "type": "library", + "type": "library", + "description": "Communicate with Integrated Library System (ILS) servers via 3M's SIP2", + "keywords": [ + "SIP2", + "SIP2Client", + "ILS", + "php-sip2" + ], + "homepage": "https://github.com/cap60552/php-sip2", + "license": "MIT", "authors": [ { "name": "John Wohlers", "email": "john@wohlershome.net", - "role": "Maintainer" + "homepage": "https://github.com/cap60552", + "role": "Developer" + }, + { + "name": "Paul Dixon", + "email": "paul@elphin.com", + "homepage": "https://github.com/cap60552", + "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": "~7.0", + "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": { - "classmap": ["/"] + "psr-4": { + "cap60552\\SIP2\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "cap60552\\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..4f294dd --- /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/Exception/LogicException.php b/src/Exception/LogicException.php new file mode 100644 index 0000000..73f6906 --- /dev/null +++ b/src/Exception/LogicException.php @@ -0,0 +1,13 @@ + + */ +class LogicException extends \LogicException implements SIP2ClientException +{ +} diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php new file mode 100644 index 0000000..1019a4e --- /dev/null +++ b/src/Exception/RuntimeException.php @@ -0,0 +1,14 @@ + + */ +class RuntimeException extends \RuntimeException implements SIP2ClientException +{ +} diff --git a/src/Exception/SIP2ClientException.php b/src/Exception/SIP2ClientException.php new file mode 100644 index 0000000..09a43bf --- /dev/null +++ b/src/Exception/SIP2ClientException.php @@ -0,0 +1,14 @@ + + */ +interface SIP2ClientException +{ +} diff --git a/src/Request/BlockPatronRequest.php b/src/Request/BlockPatronRequest.php new file mode 100644 index 0000000..7276eaa --- /dev/null +++ b/src/Request/BlockPatronRequest.php @@ -0,0 +1,43 @@ + + * @copyright Paul Dixon + */ +class BlockPatronRequest extends SIP2Request +{ + protected $var = [ + 'CardRetained' => ['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..159b4fc --- /dev/null +++ b/src/Request/CheckInRequest.php @@ -0,0 +1,53 @@ + + * @copyright Paul Dixon + */ +class CheckInRequest extends SIP2Request +{ + protected $var = [ + 'NoBlock' => ['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..50c0429 --- /dev/null +++ b/src/Request/CheckOutRequest.php @@ -0,0 +1,63 @@ + + * @copyright Paul Dixon + */ +class CheckOutRequest extends SIP2Request +{ + protected $var = [ + 'SCRenewal' => ['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..e4b2af0 --- /dev/null +++ b/src/Request/EndPatronSessionRequest.php @@ -0,0 +1,39 @@ + + * @copyright Paul Dixon + */ +class EndPatronSessionRequest extends SIP2Request +{ + protected $var = [ + 'InstitutionId' => [], + '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..1dbb858 --- /dev/null +++ b/src/Request/FeePaidRequest.php @@ -0,0 +1,76 @@ + + * @copyright Paul Dixon + */ +class FeePaidRequest extends SIP2Request +{ + protected $var = [ + 'FeeType' => ['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', (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 + // 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..27e4474 --- /dev/null +++ b/src/Request/HoldRequest.php @@ -0,0 +1,81 @@ + + * @copyright Paul Dixon + */ +class HoldRequest extends SIP2Request +{ + const MODE_ADD = '+'; + const MODE_DELETE = '-'; + const MODE_CHANGE = '*'; + + const HOLD_OTHER = 1; + const HOLD_ANY_COPY = 2; + const HOLD_SPECIFIC_COPY = 3; + const HOLD_ANY_COPY_AT_LOCATION = 4; + + protected $var = [ + 'HoldMode' => ['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..5c02800 --- /dev/null +++ b/src/Request/ItemInformationRequest.php @@ -0,0 +1,35 @@ + + * @copyright Paul Dixon + */ +class ItemInformationRequest extends SIP2Request +{ + protected $var = [ + 'InstitutionId' => [], + '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..9d24703 --- /dev/null +++ b/src/Request/ItemStatusUpdateRequest.php @@ -0,0 +1,39 @@ + + * @copyright Paul Dixon + */ +class ItemStatusUpdateRequest extends SIP2Request +{ + protected $var = [ + 'InstitutionId' => [], + '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..cabbe4e --- /dev/null +++ b/src/Request/LoginRequest.php @@ -0,0 +1,43 @@ + + * @copyright Paul Dixon + */ +class LoginRequest extends SIP2Request +{ + protected $var = [ + 'UserIdAlgorithm' => ['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..a9383e8 --- /dev/null +++ b/src/Request/PatronEnableRequest.php @@ -0,0 +1,38 @@ + + * @copyright Paul Dixon + */ +class PatronEnableRequest extends SIP2Request +{ + protected $var = [ + 'InstitutionId' => [], + '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..4af02f9 --- /dev/null +++ b/src/Request/PatronInformationRequest.php @@ -0,0 +1,70 @@ + + * @copyright Paul Dixon + */ +class PatronInformationRequest extends SIP2Request +{ + protected $var = [ + 'Language' => ['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..d353f27 --- /dev/null +++ b/src/Request/PatronStatusRequest.php @@ -0,0 +1,41 @@ + + * @copyright Paul Dixon + */ +class PatronStatusRequest extends SIP2Request +{ + protected $var = [ + 'Language' => ['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..8e60e11 --- /dev/null +++ b/src/Request/RenewAllRequest.php @@ -0,0 +1,40 @@ + + * @copyright Paul Dixon + */ +class RenewAllRequest extends SIP2Request +{ + protected $var = [ + 'InstitutionId' => [], + '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..ced40ff --- /dev/null +++ b/src/Request/RenewRequest.php @@ -0,0 +1,61 @@ + + * @copyright Paul Dixon + */ +class RenewRequest extends SIP2Request +{ + protected $var = [ + 'ThirdParty' => ['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..17df2dc --- /dev/null +++ b/src/Request/RequestACSResendRequest.php @@ -0,0 +1,22 @@ + + * @copyright Paul Dixon + */ +class RequestACSResendRequest extends SIP2Request +{ + public function getMessageString($withSeq = true, $withCrc = true): string + { + $this->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..a4c653a --- /dev/null +++ b/src/Request/SCStatusRequest.php @@ -0,0 +1,37 @@ + + * @copyright Paul Dixon + */ +class SCStatusRequest extends SIP2Request +{ + protected $var = [ + 'Status' => ['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", (float)$this->getVariable('Version')), 4); + + return $this->returnMessage($withSeq, $withCrc); + } +} diff --git a/src/Request/SIP2Request.php b/src/Request/SIP2Request.php new file mode 100644 index 0000000..7e1e109 --- /dev/null +++ b/src/Request/SIP2Request.php @@ -0,0 +1,146 @@ + + * @copyright Paul Dixon + */ +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 = ''; + + /** @var bool tracks when a variable field is used to prevent further fixed fields */ + private $noFixed = false; + + /** + * @var string terminator for requests. This should be just \r (0x0d) according to docs, but some vendors + * require \r\n + */ + private $msgTerminator = "\r\n"; + + /** @var string variable length field terminator */ + private $fldTerminator = '|'; + + + private static $seq = -1; + + /** + * This class automatically increments a static sequence number, but for testing its useful to have this + * start at 0. This method allows it to be reset + */ + public static function resetSequence() + { + self::$seq = -1; + } + + /** + * Derived class must implement this to build its SIP2 request + */ + abstract public function getMessageString($withSeq = true, $withCrc = true): string; + + + /** + * Start building a new message + * @param string $code + */ + protected 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; + } + + 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): string + { + /* 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 .= self::crc($this->msgBuild); + } + $this->msgBuild .= $this->msgTerminator; + + return $this->msgBuild; + } + + /* Core local utility functions */ + + 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; + } +} diff --git a/src/Response/ACSStatusResponse.php b/src/Response/ACSStatusResponse.php new file mode 100644 index 0000000..1ca3771 --- /dev/null +++ b/src/Response/ACSStatusResponse.php @@ -0,0 +1,72 @@ + + * @copyright Paul Dixon + */ +class ACSStatusResponse extends SIP2Response +{ + protected $var = [ + 'Online' => [], + '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..845f86b --- /dev/null +++ b/src/Response/CheckInResponse.php @@ -0,0 +1,65 @@ + + * @copyright Paul Dixon + */ +class CheckInResponse extends SIP2Response +{ + //fixed part of response contains these + protected $var = [ + 'Ok' => [], + '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..40d5849 --- /dev/null +++ b/src/Response/CheckOutResponse.php @@ -0,0 +1,73 @@ + + * @copyright Paul Dixon + */ +class CheckOutResponse extends SIP2Response +{ + //fixed part of response contains these + protected $var = [ + 'Ok' => [], + '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..b4ed4a8 --- /dev/null +++ b/src/Response/EndSessionResponse.php @@ -0,0 +1,43 @@ + + * @copyright Paul Dixon + */ +class EndSessionResponse extends SIP2Response +{ + //fixed part of response contains these + protected $var = [ + 'EndSession' => [], + '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..b5409dc --- /dev/null +++ b/src/Response/FeePaidResponse.php @@ -0,0 +1,45 @@ + + * @copyright Paul Dixon + */ +class FeePaidResponse extends SIP2Response +{ + //fixed part of response contains these + protected $var = [ + 'PaymentAccepted' => [], + '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..8544331 --- /dev/null +++ b/src/Response/HoldResponse.php @@ -0,0 +1,74 @@ + + * @copyright Paul Dixon + */ +class HoldResponse extends SIP2Response +{ + //fixed part of response contains these + protected $var = [ + 'Ok' => [], + '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..afc54b1 --- /dev/null +++ b/src/Response/ItemInformationResponse.php @@ -0,0 +1,72 @@ + + * @copyright Paul Dixon + */ +class ItemInformationResponse extends SIP2Response +{ + //fixed part of response contains these + protected $var = [ + 'CirculationStatus' => [], + '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..0e09329 --- /dev/null +++ b/src/Response/ItemStatusUpdateResponse.php @@ -0,0 +1,45 @@ + + * @copyright Paul Dixon + */ +class ItemStatusUpdateResponse extends SIP2Response +{ + //fixed part of response contains these + protected $var = [ + 'PropertiesOk' => [], + '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..9d9e997 --- /dev/null +++ b/src/Response/LoginResponse.php @@ -0,0 +1,24 @@ + + * @copyright Paul Dixon + */ +class LoginResponse extends SIP2Response +{ + protected $var = [ + 'Ok' => ['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..6a3aa59 --- /dev/null +++ b/src/Response/PatronEnableResponse.php @@ -0,0 +1,53 @@ + + * @copyright Paul Dixon + */ +class PatronEnableResponse extends SIP2Response +{ + //fixed part of response contains these + protected $var = [ + 'PatronStatus' => [], + '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..2174de5 --- /dev/null +++ b/src/Response/PatronInformationResponse.php @@ -0,0 +1,100 @@ + + * @copyright Paul Dixon + */ +class PatronInformationResponse extends SIP2Response +{ + //fixed part of response contains these + protected $var = [ + 'PatronStatus' => [], + '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..26e568b --- /dev/null +++ b/src/Response/PatronStatusResponse.php @@ -0,0 +1,57 @@ + + * @copyright Paul Dixon + */ +class PatronStatusResponse extends SIP2Response +{ + //fixed part of response contains these + protected $var = [ + 'PatronStatus' => [], + '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..bcbce07 --- /dev/null +++ b/src/Response/RenewAllResponse.php @@ -0,0 +1,51 @@ + + * @copyright Paul Dixon + */ +class RenewAllResponse extends SIP2Response +{ + //fixed part of response contains these + protected $var = [ + 'Ok' => [], + '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..e517e52 --- /dev/null +++ b/src/Response/RenewResponse.php @@ -0,0 +1,73 @@ + + * @copyright Paul Dixon + */ +class RenewResponse extends SIP2Response +{ + //fixed part of response contains these + protected $var = [ + 'Ok' => [], + '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..bc39e05 --- /dev/null +++ b/src/Response/SIP2Response.php @@ -0,0 +1,206 @@ + + * @copyright Paul Dixon + */ +abstract class SIP2Response extends SIP2Message +{ + const AA_PATRON_IDENTIFIER = 'AA'; + const AB_ITEM_IDENTIFIER = 'AB'; + const AE_PERSONAL_NAME = 'AE'; + const AF_SCREEN_MESSAGE = 'AF'; + const AG_PRINT_LINE = 'AG'; + const AH_DUE_DATE = 'AH'; + const AJ_TITLE_IDENTIFIER = 'AJ'; + const AM_LIBRARY_NAME='AM'; + const AN_TERMINAL_LOCATION='AN'; + const AO_INSTITUTION_ID = 'AO'; + const AP_CURRENT_LOCATION = 'AP'; + const AQ_PERMANENT_LOCATION='AQ'; + const AS_HOLD_ITEMS = 'AS'; + const AT_OVERDUE_ITEMS = 'AT'; + const AU_CHARGED_ITEMS = 'AU'; + const AV_FINE_ITEMS = 'AV'; + const AY_SEQUENCE_NUMBER = 'AY'; + const BD_HOME_ADDRESS = 'BD'; + const BE_EMAIL_ADDRESS = 'BE'; + const BF_HOME_PHONE_NUMBER = 'BF'; + const BG_OWNER = 'BG'; + const BH_CURRENCY_TYPE = 'BH'; + const BK_TRANSACTION_ID= 'BK'; + const BL_VALID_PATRON = 'BL'; + const BM_RENEWED_ITEMS = 'BM'; + const BN_UNRENEWED_ITEMS = 'BN'; + const BR_QUEUE_POSITION = 'BR'; + const BS_PICKUP_LOCATION = 'BS'; + const BT_FEE_TYPE = 'BT'; + const BU_RECALL_ITEMS = 'BU'; + const BV_FEE_AMOUNT = 'BV'; + const BW_EXPIRATION_DATE = 'BW'; + const BX_SUPPORTED_MESSAGES='BX'; + const BZ_HOLD_ITEMS_LIMIT = 'BZ'; + const CA_OVERDUE_ITEMS_LIMIT = 'CA'; + const CB_CHARGED_ITEMS_LIMIT = 'CB'; + const CC_FEE_LIMIT = 'CC'; + const CD_UNAVAILABLE_HOLD_ITEMS = 'CD'; + const CF_HOLD_QUEUE_LENGTH = 'CF'; + const CH_ITEM_PROPERTIES= 'CH'; + const CI_SECURITY_INHIBIT = 'CI'; + const CJ_RECALL_DATE = 'CJ'; + const CK_MEDIA_TYPE= 'CK'; + const CL_SORT_BIN='CL'; + const CM_HOLD_PICKUP_DATE = 'CM'; + const CQ_VALID_PATRON_PASSWORD = 'CQ'; + + /** @var array maps SIP2 numeric response codes onto response classes */ + private static $mapResponseToClass = [ + '10' => 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 new file mode 100644 index 0000000..7e6a4de --- /dev/null +++ b/src/SIP2Client.php @@ -0,0 +1,200 @@ + + * @copyright Paul Dixon + */ + +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; +use Psr\Log\NullLogger; +use Socket\Raw\Factory; +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 +{ + use LoggerAwareTrait; + + //----------------------------------------------------- + // request options + //----------------------------------------------------- + + /** + * @var array name=>value request defaults used for every request + */ + private $default=[]; + + //----------------------------------------------------- + // connection handling + //----------------------------------------------------- + + /** @var int maximum number of resends in the event of CRC failure */ + public $maxretry = 3; + + /** @var Socket */ + private $socket; + + /** @var Factory injectable factory for creating socket connections */ + 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(); + $this->setDefault('InstitutionId', 'WohlersSIP'); + } + + public function setDefault($name, $value) + { + $this->default[$name] = $value; + } + + /** + * 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 one if necessary + * @return Factory + */ + private function getSocketFactory() + { + if (is_null($this->socketFactory)) { + $this->socketFactory = new Factory(); //@codeCoverageIgnore + } + return $this->socketFactory; + } + + + /** + * @param SIP2Request $request + * @return SIP2Response + * @throws RuntimeException if server fails to produce a valid response + */ + public function sendRequest(SIP2Request $request) : SIP2Response + { + foreach ($this->default as $name => $value) { + $request->setDefault($name, $value); + } + + $raw = $this->getRawResponse($request); + return SIP2Response::parse($raw); + } + + private function getRawResponse(SIP2Request $request, $depth = 0) + { + $result = ''; + $terminator = ''; + + $message = $request->getMessageString(); + + $this->logger->debug('SIP2: Sending SIP2 request '.trim($message)); + $this->socket->write($message); + + $this->logger->debug('SIP2: Request Sent, Reading response'); + + while ($terminator != "\x0D") { + //@codeCoverageIgnoreStart + try { + $terminator = $this->socket->recv(1, 0); + } catch (\Exception $e) { + break; + } + //@codeCoverageIgnoreEnd + + $result = $result . $terminator; + } + + $this->logger->info("SIP2: result={$result}"); + + // test message for CRC validity + if (SIP2Response::checkCRC($result)) { + $this->logger->debug("SIP2: Message from ACS passed CRC check"); + } else { + //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 { + $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 + * + * 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 + * @param int $timeout number of seconds to allow for connection to succeed + */ + public function connect($address, $bind = null, $timeout = 15) + { + $this->logger->debug("SIP2Client: Attempting connection to $address"); + + $this->socket = $this->getSocketFactory()->createClient($address, $timeout); + + try { + if (!empty($bind)) { + $this->logger->debug("SIP2Client: binding socket to $bind"); + $this->socket->bind($bind); + } + + $this->socket->connect($address); + } catch (\Exception $e) { + $this->socket->close(); + $this->socket = null; + $this->logger->error("SIP2Client: Failed to connect: " . $e->getMessage()); + throw new RuntimeException("Connection failure", 0, $e); + } + + $this->logger->debug("SIP2Client: connected"); + } + + /** + * Disconnect from ACS + */ + public function disconnect() + { + $this->socket->close(); + $this->socket = null; + } +} diff --git a/src/SIP2Message.php b/src/SIP2Message.php new file mode 100644 index 0000000..574eb7a --- /dev/null +++ b/src/SIP2Message.php @@ -0,0 +1,215 @@ + + */ +abstract class SIP2Message +{ + /** + * @var array provides a list of the variables this message can use. Array key is the variable name in + * StudlyCaps, value is an array which can contain type, default values + */ + protected $var=[]; + + /** @var integer|null current timestamp, useful for testing */ + protected $timestamp = null; + + + /** + * Calculate SIP2 CRC value + * @param string $buf + * @return string + */ + protected static function crc(string $buf) : string + { + $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); + } + + /** + * Check if class supports given variable + * @param string $name + * @return bool + */ + public function hasVariable(string $name) : bool + { + return isset($this->var[$name]); + } + + /** + * 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'] ?? + $this->var[$varName]['default'] ?? + $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=[]; + foreach ($this->var as $name => $data) { + $result[$name] = $this->getVariable($name); + } + 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); + + //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; + } + + $this->var[$varName]['value'] = $value; + } + + /** + * If $varName is defined as an array, this will append given value. Otherwise value is set as normal + * @param string $varName + * @param string $value + */ + public function addVariable(string $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/AbstractSIP2ClientTest.php b/tests/AbstractSIP2ClientTest.php new file mode 100644 index 0000000..4b08369 --- /dev/null +++ b/tests/AbstractSIP2ClientTest.php @@ -0,0 +1,138 @@ +crc($str); + //add terminator + $str .= "\x0D"; + return $str; + } + + /** + * 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); + } + + /** + * 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 + */ + protected 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 our 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++]; + }); + + $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'), + Argument::any() + )->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 + */ + protected 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 + */ + protected 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]); + } + } +} diff --git a/tests/Request/BlockPatronRequestTest.php b/tests/Request/BlockPatronRequestTest.php new file mode 100644 index 0000000..9b250e8 --- /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..52115f8 --- /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..f83f323 --- /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..3f3bb73 --- /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..77cf42d --- /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..7ea4727 --- /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..4631c8c --- /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..59cdb05 --- /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..1ce2f7e --- /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..e729e31 --- /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..6ee0195 --- /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..3f99e4e --- /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..b68525f --- /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..d43845f --- /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..4512e72 --- /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..4f9f662 --- /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..fb28401 --- /dev/null +++ b/tests/Request/SIP2RequestTest.php @@ -0,0 +1,61 @@ +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 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 + * @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..46996e4 --- /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..7f2eebe --- /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..284079e --- /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..b346f64 --- /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..ef04033 --- /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..dc0236f --- /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..cae4306 --- /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..4ec0900 --- /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..1465f50 --- /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..e9ae7fe --- /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..7e18a8f --- /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..752ce35 --- /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..9b989ca --- /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..fac6320 --- /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..575f79b --- /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/SIP2ClientTest.php b/tests/SIP2ClientTest.php new file mode 100644 index 0000000..f076a2f --- /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->createClient( + 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->createClient( + Argument::type('string'), + Argument::any() + )->willReturn($socket->reveal()); + + return $factory->reveal(); + } +}