PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.20
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.20
1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 All 164 releases
woocommerce-pos / includes / Services / Providers / Epson_Sdp_Adapter.php

Epson_Sdp_Adapter.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.20, at includes/Services/Providers/Epson_Sdp_Adapter.php

263 lines 8.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Epson Server Direct Print provider adapter.
4 *
5 * @package WCPOS\WooCommercePOS\Services\Providers
6 */
7
8 namespace WCPOS\WooCommercePOS\Services\Providers;
9
10 use WCPOS\WooCommercePOS\Interfaces\Poll_Provider_Adapter_Interface;
11
12 /**
13 * Epson_Sdp_Adapter class.
14 */
15 class Epson_Sdp_Adapter implements Poll_Provider_Adapter_Interface {
16 /**
17 * Milliseconds an Epson Server Direct Print printer waits for the local
18 * print device to become printable before it gives up and reports
19 * EX_TIMEOUT.
20 *
21 * Epson allows 5000–300000. The previous 10000 (near the floor) was
22 * unforgiving for printers that briefly go not-ready — a paper change, a
23 * sleep/wake, or a momentary network blip — declaring the job failed
24 * before the printer could recover. 60000 gives those transient states
25 * room to clear without letting a genuinely offline printer hang for long.
26 */
27 const EPSON_SDP_PRINT_TIMEOUT_MS = 60000;
28
29 /**
30 * Return the ePOS-Print XML content type.
31 *
32 * @return string
33 */
34 public function content_type(): string {
35 return 'application/xml';
36 }
37
38 /**
39 * Resolve ePOS-Print XML format data.
40 *
41 * @param array $printer Printer configuration.
42 * @param array $template Template configuration.
43 *
44 * @return array{kind:string, content_type:string}
45 */
46 public function format( array $printer, array $template ): array {
47 if ( 'thermal' !== (string) ( $template['engine'] ?? '' ) ) {
48 return array(
49 'kind' => '',
50 'content_type' => '',
51 );
52 }
53
54 return array(
55 'kind' => 'epos-xml',
56 'content_type' => $this->content_type(),
57 );
58 }
59
60 /**
61 * Build an ePOS-Print XML diagnostic.
62 *
63 * @param string $printer_name Printer display name.
64 *
65 * @return array{content_type:string, payload:string}
66 */
67 public function diagnostic( string $printer_name ): array {
68 $name = (string) preg_replace( '/[\x00-\x1F\x7F]/', '', $printer_name );
69 $text = "WCPOS - Cloud Print Test\nPrinter: " . $name . "\n";
70 $text .= 'Date: ' . gmdate( 'Y-m-d H:i' ) . "\nIf you can read this, printing works!\n";
71 // Three blank lines before the cut, the same bottom margin the receipt
72 // templates use, so the last line is not flush against the blade.
73 $xml = '<epos-print xmlns="http://www.epson-pos.com/schemas/2011/03/epos-print">'
74 . '<text>' . esc_html( $text ) . '</text><feed line="3"/><cut type="feed"/></epos-print>';
75
76 return array(
77 'content_type' => $this->content_type(),
78 'payload' => base64_encode( $xml ),
79 );
80 }
81
82 /**
83 * Resolve polling status.
84 *
85 * @param array $printer Printer configuration.
86 * @param array $context Runtime status context.
87 *
88 * @return string
89 */
90 public function status( array $printer, array $context ): string {
91 $relay = $context['relay_status'] ?? null;
92 if ( \is_array( $relay ) && 'blocked' === ( $relay['origin_status'] ?? '' ) ) {
93 return 'blocked';
94 }
95 if ( \is_array( $relay ) && null !== ( $relay['last_seen_seconds_ago'] ?? null ) && (int) $relay['last_seen_seconds_ago'] <= (int) $context['seen_ttl'] ) {
96 return 'connected';
97 }
98
99 $seen = (int) ( $context['seen'] ?? 0 );
100 if ( 0 === $seen ) {
101 return 'waiting';
102 }
103
104 return ( (int) $context['now'] - $seen ) <= (int) $context['seen_ttl'] ? 'connected' : 'offline';
105 }
106
107 /**
108 * Parse the vendor request.
109 *
110 * @param array $request Request data.
111 */
112 public function parse( array $request ): array {
113 $params = $request['params'];
114 // Server Direct Print multiplexes three different request types onto the
115 // one configured URL, as URL-encoded form data distinguished by
116 // ConnectionType (User's Manual Rev.K, ch.3 and the Test_print.php
117 // reference implementation):
118 //
119 // GetRequest — poll for a job
120 // SetResponse — printing result; the XML rides in the ResponseFile field
121 // SetStatus — status notification; the XML rides in the Status field
122 //
123 // Answering a status notification with print data hands the job to a
124 // request that discards it, so the printer never prints and the job stays
125 // claimed. Dispatching on ConnectionType is what keeps the job on the
126 // GetRequest that is actually asking for one.
127 $connection_type = (string) ( $params['ConnectionType'] ?? '' );
128 $result_xml = (string) ( $params['ResponseFile'] ?? '' );
129 if ( '' === $result_xml && false !== strpos( $request['body'], '<response' ) ) {
130 $result_xml = $request['body'];
131 }
132 // Idle PrintResponseInfo and SetStatus posts must never consume a job.
133 $phase = false !== strpos( $result_xml, '<response' ) ? 'result' : ( 'SetStatus' === $connection_type || 'SetResponse' === $connection_type || '' !== $result_xml ? 'advertise' : 'fetch' );
134 return array(
135 'phase' => $phase,
136 'route' => $request['route'],
137 'result_xml' => $result_xml,
138 'busy' => 'advertise' === $phase,
139 );
140 }
141
142 /**
143 * Build an offer or acknowledgement.
144 *
145 * @param array $printer Printer data.
146 * @param array $poll Parsed request.
147 * @param array|null $next_job Available job.
148 */
149 public function advertise( array $printer, array $poll, ?array $next_job ): array {
150 return array(
151 'status' => 200,
152 'headers' => array( 'Content-Type' => 'text/xml; charset=utf-8' ),
153 'body' => '<response success="true" code="" status=""/>',
154 );
155 }
156
157 /**
158 * Choose a format before claiming.
159 *
160 * @param array $printer Printer data.
161 * @param array $poll Parsed request.
162 * @param array $job Candidate job.
163 */
164 public function negotiate( array $printer, array $poll, array $job ): array {
165 // SDP uses the provider default render, not an HTTP Accept media type.
166 return array( 'media_type' => '' );
167 }
168
169 /**
170 * Build the print-data response, including empty renders.
171 *
172 * @param array $job Claimed job.
173 * @param array $render Rendered bytes.
174 * @param array $poll Parsed request.
175 */
176 public function deliver( array $job, array $render, array $poll ): array {
177 if ( '' === $render['body'] ) {
178 return $this->advertise( array(), $poll, null );
179 }
180 // SDP 1.00 is supported by every printer family; this is not a SOAP envelope.
181 $envelope = '<?xml version="1.0" encoding="utf-8"?>';
182 $envelope .= '<PrintRequestInfo Version="1.00"><ePOSPrint>';
183 $envelope .= '<Parameter><devid>local_printer</devid><timeout>' . self::EPSON_SDP_PRINT_TIMEOUT_MS . '</timeout></Parameter>';
184 $envelope .= '<PrintData>' . $render['body'] . '</PrintData>';
185 $envelope .= '</ePOSPrint></PrintRequestInfo>';
186 return array(
187 'status' => 200,
188 'headers' => array( 'Content-Type' => 'text/xml; charset=utf-8' ),
189 'body' => $envelope,
190 );
191 }
192
193 /**
194 * Identify the result target.
195 *
196 * @param array $poll Parsed request.
197 */
198 public function match_result( array $poll ): array {
199 // SDP 1.00 carries no job token: match the active or latest unconfirmed claim.
200 return array( 'job_id' => null );
201 }
202
203 /**
204 * Decode the printer result.
205 *
206 * @param array $poll Parsed request.
207 */
208 public function interpret_result( array $poll ): array {
209 $result_xml = $poll['result_xml'];
210 $code = 'unknown';
211 if ( 1 === preg_match( '/\bcode="([^"]*)"/', $result_xml, $matches ) ) {
212 $code = sanitize_text_field( $matches[1] );
213 }
214 $status = null;
215 if ( 1 === preg_match( '/\bstatus="(\d+)"/', $result_xml, $matches ) ) {
216 // Do not misdecode unsigned bit 31 on a 32-bit PHP build.
217 $status = (float) $matches[1] <= PHP_INT_MAX ? (int) $matches[1] : null;
218 }
219 $flags = null === $status ? '' : implode( ', ', self::describe_epson_status( $status ) );
220 $detail = null === $status ? '' : sprintf( ' (0x%08X%s)', $status, '' === $flags ? '' : ': ' . $flags );
221 return array(
222 'ok' => false !== strpos( $result_xml, 'success="true"' ),
223 'code' => $code,
224 'detail' => $detail,
225 );
226 }
227
228 /**
229 * Decode an Epson ePOS-Print response status bitmask.
230 *
231 * @param int $status Decimal ASB status bitmask.
232 *
233 * @return array<int, string>
234 */
235 private static function describe_epson_status( int $status ): array {
236 // Epson ePOS-Print XML User's Manual, response `status` table (ASB bits).
237 // Fault bits only: informational ones (print complete, drawer pin, feed
238 // button, panel switch, buzzer) say nothing about why a print failed.
239 $labels = array(
240 0x00000001 => __( 'no response from printer', 'woocommerce-pos' ),
241 0x00000008 => __( 'offline', 'woocommerce-pos' ),
242 0x00000020 => __( 'cover open', 'woocommerce-pos' ),
243 0x00000100 => __( 'waiting for online recovery', 'woocommerce-pos' ),
244 0x00000400 => __( 'mechanical error', 'woocommerce-pos' ),
245 0x00000800 => __( 'autocutter error', 'woocommerce-pos' ),
246 0x00002000 => __( 'unrecoverable error', 'woocommerce-pos' ),
247 0x00004000 => __( 'auto-recoverable error', 'woocommerce-pos' ),
248 0x00020000 => __( 'paper near end', 'woocommerce-pos' ),
249 0x00080000 => __( 'paper end', 'woocommerce-pos' ),
250 0x80000000 => __( 'spooler stopped', 'woocommerce-pos' ),
251 );
252
253 $descriptions = array();
254 foreach ( $labels as $bit => $label ) {
255 if ( 0 !== ( $status & $bit ) ) {
256 $descriptions[] = $label;
257 }
258 }
259
260 return $descriptions;
261 }
262 }
263