lordelph /
php-sip2
| 1 | <?php |
||
| 2 | |||
| 3 | namespace lordelph\SIP2; |
||
| 4 | |||
| 5 | /** |
||
| 6 | * SIP2Client Class |
||
| 7 | * |
||
| 8 | * This class provides a method of communicating with an Integrated |
||
| 9 | * Library System using 3M's SIP2 standard. |
||
| 10 | * |
||
| 11 | * @licence https://opensource.org/licenses/MIT |
||
| 12 | * @copyright John Wohlers <[email protected]> |
||
| 13 | * @copyright Paul Dixon <[email protected]> |
||
| 14 | */ |
||
| 15 | |||
| 16 | use lordelph\SIP2\Exception\RuntimeException; |
||
| 17 | use lordelph\SIP2\Request\SIP2Request; |
||
| 18 | use lordelph\SIP2\Response\SIP2Response; |
||
| 19 | use Psr\Log\LoggerAwareInterface; |
||
| 20 | use Psr\Log\LoggerAwareTrait; |
||
| 21 | use Psr\Log\LoggerInterface; |
||
| 22 | use Psr\Log\NullLogger; |
||
| 23 | use Socket\Raw\Factory; |
||
| 24 | use Socket\Raw\Socket; |
||
| 25 | |||
| 26 | /** |
||
| 27 | * SIP2Client provides a simple client for SIP2 library services |
||
| 28 | * |
||
| 29 | * In the specification, and the comments below, 'SC' (or Self Check) denotes the client, and ACS (or Automated |
||
| 30 | * Circulation System) denotes the server. |
||
| 31 | */ |
||
| 32 | class SIP2Client implements LoggerAwareInterface |
||
| 33 | { |
||
| 34 | use LoggerAwareTrait; |
||
| 35 | |||
| 36 | //----------------------------------------------------- |
||
| 37 | // request options |
||
| 38 | //----------------------------------------------------- |
||
| 39 | |||
| 40 | /** |
||
| 41 | * @var array name=>value request defaults used for every request |
||
| 42 | */ |
||
| 43 | private $default=[]; |
||
| 44 | |||
| 45 | //----------------------------------------------------- |
||
| 46 | // connection handling |
||
| 47 | //----------------------------------------------------- |
||
| 48 | |||
| 49 | /** @var int maximum number of resends in the event of CRC failure */ |
||
| 50 | public $maxretry = 3; |
||
| 51 | |||
| 52 | /** @var Socket */ |
||
| 53 | private $socket; |
||
| 54 | |||
| 55 | /** @var Factory injectable factory for creating socket connections */ |
||
| 56 | private $socketFactory; |
||
| 57 | |||
| 58 | /** @var bool CRC checks can be disabled by setting this to false */ |
||
| 59 | private static $enableCRCChecks = true; |
||
| 60 | |||
| 61 | /** |
||
| 62 | * Constructor allows you to provide a PSR-3 logger, but you can also use the setLogger method |
||
| 63 | * later on. |
||
| 64 | 5 | * |
|
| 65 | * @param LoggerInterface|null $logger |
||
| 66 | 5 | */ |
|
| 67 | 5 | public function __construct(LoggerInterface $logger = null) |
|
| 68 | 5 | { |
|
| 69 | $this->logger = $logger ?? new NullLogger(); |
||
| 70 | 5 | $this->setDefault('InstitutionId', 'WohlersSIP'); |
|
| 71 | } |
||
| 72 | 5 | ||
| 73 | 5 | public function setDefault($name, $value) |
|
| 74 | { |
||
| 75 | $this->default[$name] = $value; |
||
| 76 | } |
||
| 77 | |||
| 78 | /** |
||
| 79 | * Can be used to disable CRC checks |
||
| 80 | * |
||
| 81 | 5 | * Some SIP2 services have been observed producing invalid CRCs when extended/UTF-8 chars are involved. As |
|
| 82 | * the CRC was originally part of the protocol for noisy serial lines, it serves little purpose in error |
||
| 83 | 5 | * checking when connecting via TCP/IP, but disable at your own risk! |
|
| 84 | 5 | * |
|
| 85 | * @param bool $enable |
||
| 86 | */ |
||
| 87 | public static function enableCRCCheck($enable) |
||
| 88 | { |
||
| 89 | self::$enableCRCChecks = $enable; |
||
| 90 | 5 | } |
|
| 91 | |||
| 92 | 5 | public static function isCRCCheckEnabled() |
|
| 93 | { |
||
| 94 | return self::$enableCRCChecks; |
||
| 95 | 5 | } |
|
| 96 | |||
| 97 | |||
| 98 | /** |
||
| 99 | * Allows an alternative socket factory to be injected. The allows us to |
||
| 100 | * mock socket connections for testing |
||
| 101 | * |
||
| 102 | * @param Factory $factory |
||
| 103 | */ |
||
| 104 | 3 | public function setSocketFactory(Factory $factory) |
|
| 105 | { |
||
| 106 | 3 | $this->socketFactory = $factory; |
|
| 107 | 3 | } |
|
| 108 | |||
| 109 | /** |
||
| 110 | 3 | * Get the current socket factory, creating a default one if necessary |
|
| 111 | 2 | * @return Factory |
|
| 112 | */ |
||
| 113 | private function getSocketFactory() |
||
| 114 | 3 | { |
|
| 115 | if (is_null($this->socketFactory)) { |
||
| 116 | 3 | $this->socketFactory = new Factory(); //@codeCoverageIgnore |
|
| 117 | 3 | } |
|
| 118 | return $this->socketFactory; |
||
| 119 | 3 | } |
|
| 120 | |||
| 121 | 3 | ||
| 122 | 3 | /** |
|
| 123 | * @param SIP2Request $request |
||
| 124 | 3 | * @return SIP2Response |
|
| 125 | * @throws RuntimeException if server fails to produce a valid response |
||
| 126 | 3 | */ |
|
| 127 | public function sendRequest(SIP2Request $request) : SIP2Response |
||
| 128 | { |
||
| 129 | foreach ($this->default as $name => $value) { |
||
| 130 | $request->setDefault($name, $value); |
||
| 131 | } |
||
| 132 | |||
| 133 | $raw = $this->getRawResponse($request); |
||
| 134 | return SIP2Response::parse($raw); |
||
| 135 | 3 | } |
|
| 136 | |||
| 137 | private function getRawResponse(SIP2Request $request, $depth = 0) |
||
| 138 | { |
||
| 139 | 3 | $result = ''; |
|
| 140 | $terminator = ''; |
||
| 141 | 3 | ||
| 142 | $message = $request->getMessageString(); |
||
| 143 | |||
| 144 | 3 | $this->logger->debug('SIP2: Sending SIP2 request '.trim($message)); |
|
|
0 ignored issues
–
show
|
|||
| 145 | 2 | $this->socket->write($message); |
|
| 146 | |||
| 147 | $this->logger->debug('SIP2: Request Sent, Reading response'); |
||
| 148 | 2 | ||
| 149 | 2 | while ($terminator != "\x0D") { |
|
| 150 | 2 | //@codeCoverageIgnoreStart |
|
| 151 | 2 | try { |
|
| 152 | $terminator = $this->socket->recv(1, 0); |
||
| 153 | 1 | } catch (\Exception $e) { |
|
| 154 | 1 | break; |
|
| 155 | 1 | } |
|
| 156 | //@codeCoverageIgnoreEnd |
||
| 157 | |||
| 158 | $result = $result . $terminator; |
||
| 159 | 2 | } |
|
| 160 | //if the remote end uses CRLF as a terminator, we might have a stray LF in our buffer - by trimming |
||
| 161 | //the responses, we ensure those don't upset us |
||
| 162 | $result=trim($result); |
||
| 163 | |||
| 164 | $this->logger->info("SIP2: result={$result}"); |
||
| 165 | |||
| 166 | // test message for CRC validity |
||
| 167 | if (SIP2Response::checkCRC($result)) { |
||
| 168 | $this->logger->debug("SIP2: Message from ACS passed CRC check"); |
||
| 169 | } else { |
||
| 170 | //CRC check failed, we resend the request |
||
| 171 | if ($depth < $this->maxretry) { |
||
| 172 | 5 | $depth++; |
|
| 173 | $this->logger->warning("SIP2: Message failed CRC check, retry {$depth})"); |
||
| 174 | 5 | $result = $this->getRawResponse($request, $depth); |
|
| 175 | } else { |
||
| 176 | $errMsg="SIP2: Failed to get valid CRC after {$this->maxretry} retries."; |
||
| 177 | 5 | $this->logger->critical($errMsg); |
|
| 178 | 1 | throw new RuntimeException($errMsg); |
|
| 179 | 1 | } |
|
| 180 | 1 | } |
|
| 181 | 1 | ||
| 182 | return $result; |
||
| 183 | } |
||
| 184 | 4 | ||
| 185 | 4 | /** |
|
| 186 | * Connect to ACS via SIP2 |
||
| 187 | * |
||
| 188 | * The $bind parameter can be useful where a machine which has multiple outbound connections and its important |
||
| 189 | * to control which one is used (normally because the remote SIP2 service is firewalled to particular IPs |
||
| 190 | * |
||
| 191 | * @param string $address ip:port of remote SIP2 service |
||
| 192 | * @param string|null $bind local ip to bind socket to |
||
| 193 | * @param int $timeout number of seconds to allow for connection to succeed |
||
| 194 | */ |
||
| 195 | 5 | public function connect($address, $bind = null, $timeout = 15) |
|
| 196 | { |
||
| 197 | 5 | $this->logger->debug("SIP2Client: Attempting connection to $address"); |
|
| 198 | |||
| 199 | 5 | try { |
|
| 200 | $this->socket = $this->createClient($address, $bind, $timeout); |
||
| 201 | } catch (\Exception $e) { |
||
| 202 | 5 | $this->socket = null; |
|
| 203 | 1 | $this->logger->error("SIP2Client: Failed to connect: " . $e->getMessage()); |
|
| 204 | 1 | throw new RuntimeException("Connection failure", 0, $e); |
|
| 205 | } |
||
| 206 | |||
| 207 | 5 | $this->logger->debug("SIP2Client: connected"); |
|
| 208 | } |
||
| 209 | |||
| 210 | /** |
||
| 211 | 5 | * A 'missing' factory method which allows us to establish a socket bound to a particular interface |
|
| 212 | 4 | * @param $address |
|
| 213 | * @param $bind |
||
| 214 | 1 | * @param $timeout |
|
| 215 | 1 | * @return Socket |
|
| 216 | 1 | * @throws \Exception |
|
| 217 | */ |
||
| 218 | private function createClient($address, $bind, $timeout) : Socket |
||
| 219 | 4 | { |
|
| 220 | $factory = $this->getSocketFactory(); |
||
| 221 | |||
| 222 | $socket = $factory->createFromString($address, $scheme); |
||
| 223 | |||
| 224 | try { |
||
| 225 | if (!empty($bind)) { |
||
| 226 | 1 | $this->logger->debug("SIP2Client: binding socket to $bind"); |
|
| 227 | $socket->bind($bind); |
||
| 228 | 1 | } |
|
| 229 | 1 | ||
| 230 | 1 | if ($timeout === null) { |
|
| 231 | $socket->connect($address); |
||
| 232 | } else { |
||
| 233 | // connectTimeout enables non-blocking mode, so turn blocking on again |
||
| 234 | $socket->connectTimeout($address, $timeout); |
||
| 235 | $socket->setBlocking(true); |
||
| 236 | } |
||
| 237 | } catch (\Exception $e) { |
||
| 238 | $socket->close(); |
||
| 239 | throw $e; |
||
| 240 | } |
||
| 241 | |||
| 242 | return $socket; |
||
| 243 | } |
||
| 244 | |||
| 245 | |||
| 246 | /** |
||
| 247 | * Disconnect from ACS |
||
| 248 | */ |
||
| 249 | public function disconnect() |
||
| 250 | { |
||
| 251 | $this->socket->close(); |
||
| 252 | $this->socket = null; |
||
| 253 | } |
||
| 254 | } |
||
| 255 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.