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.