PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.21
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.21
1.10.25 1.10.24 1.10.23 1.10.22 1.10.21 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 All 169 releases
← All changes | includes/Services/Providers/Star_Cloudprnt_Adapter.php +208 -5 1.10.13 → 1.10.21 View file →
@@ -6,14 +6,17 @@
6 6 */
7 7
8 8 namespace WCPOS\WooCommercePOS\Services\Providers;
9 9
10 -use WCPOS\WooCommercePOS\Interfaces\Provider_Adapter_Interface;
10 +use WCPOS\WooCommercePOS\Interfaces\Poll_Provider_Adapter_Interface;
11 +use WCPOS\WooCommercePOS\Logger;
12 +use WCPOS\WooCommercePOS\Services\Cloud_Print_Media_Types;
13 +use WCPOS\WooCommercePOS\Services\Cloud_Print_Poll_Request;
11 14
12 15 /**
13 16 * Star_Cloudprnt_Adapter class.
14 17 */
15 -class Star_Cloudprnt_Adapter implements Provider_Adapter_Interface {
18 +class Star_Cloudprnt_Adapter implements Poll_Provider_Adapter_Interface {
16 19 /**
17 20 * Return the StarPRNT content type.
18 21 *
19 22 * @return string
@@ -32,15 +35,15 @@
32 35 */
33 36 public function format( array $printer, array $template ): array {
34 37 if ( 'thermal' !== (string) ( $template['engine'] ?? '' ) ) {
35 38 return array(
36 - 'kind' => '',
39 + 'kind' => '',
37 40 'content_type' => '',
38 41 );
39 42 }
40 43
41 44 return array(
42 - 'kind' => 'starprnt',
45 + 'kind' => 'starprnt',
43 46 'content_type' => $this->content_type(),
44 47 );
45 48 }
46 49
@@ -61,9 +64,9 @@
61 64 $bytes .= "\x1B\x64\x03";
62 65
63 66 return array(
64 67 'content_type' => $this->content_type(),
65 - 'payload' => base64_encode( $bytes ),
68 + 'payload' => base64_encode( $bytes ),
66 69 );
67 70 }
68 71
69 72 /**
@@ -88,6 +91,206 @@
88 91 return 'waiting';
89 92 }
90 93
91 94 return ( (int) $context['now'] - $seen ) <= (int) $context['seen_ttl'] ? 'connected' : 'offline';
95 + }
96 +
97 + /**
98 + * Parse the vendor request.
99 + *
100 + * @param array $request Request data.
101 + */
102 + public function parse( array $request ): array {
103 + // The poll body is the printer's half of the conversation. `printingInProgress`
104 + // says a job is still on the paper, and the spec is explicit that the server
105 + // must not offer another one until it clears — doing so risks the printer
106 + // dropping the second job. `clientAction` carries the printer's answers to
107 + // questions asked in an earlier poll response, which is the only way the
108 + // protocol exposes what formats the hardware can decode.
109 + $params = $request['params'];
110 + $phase = 'POST' === $request['method'] ? 'advertise' : ( 'DELETE' === $request['method'] ? 'result' : 'fetch' );
111 + $poll = array(
112 + 'phase' => $phase,
113 + 'route' => $request['route'],
114 + 'token' => (int) ( $params['token'] ?? 0 ),
115 + 'type' => sanitize_text_field( (string) ( $params['type'] ?? '' ) ),
116 + 'code' => sanitize_text_field( (string) ( $params['code'] ?? '' ) ),
117 + );
118 + $poll['needs_capabilities'] = 'advertise' === $phase || ( 'fetch' === $phase && '' === $poll['type'] );
119 + if ( 'advertise' === $phase ) {
120 + $body = Cloud_Print_Poll_Request::from_body( $request['body'], $request['json'] );
121 + $poll['busy'] = $body->printing_in_progress();
122 + $poll['answers'] = $body->answers();
123 + $poll['status_code'] = $body->status_code();
124 + }
125 + return $poll;
126 + }
127 +
128 + /**
129 + * Build an offer or acknowledgement.
130 + *
131 + * @param array $printer Printer data.
132 + * @param array $poll Parsed request.
133 + * @param array|null $next_job Available job.
134 + */
135 + public function advertise( array $printer, array $poll, ?array $next_job ): array {
136 + if ( 'not_found' === ( $poll['intent'] ?? '' ) ) {
137 + return array(
138 + 'status' => 404,
139 + 'headers' => array(),
140 + 'body' => array(
141 + 'code' => 'wcpos_print_job_not_found',
142 + 'message' => __( 'Print job not found.', 'woocommerce-pos' ),
143 + 'data' => array( 'status' => 404 ),
144 + ),
145 + );
146 + }
147 + $response = array( 'jobReady' => false );
148 + if ( 'result' === $poll['phase'] ) {
149 + $response = array( 'ok' => true );
150 + } elseif ( null !== $next_job ) {
151 + $media_types = $this->media_types_for_job( $next_job, $printer );
152 + $response = array(
153 + 'jobReady' => true,
154 + 'jobToken' => (string) $next_job['id'],
155 + 'mediaType' => $media_types[0],
156 + 'mediaTypes' => array_values( $media_types ),
157 + );
158 + }
159 + if ( ! empty( $poll['request_capabilities'] ) ) {
160 + $response['clientAction'] = array(
161 + array( 'request' => 'ClientType' ),
162 + array( 'request' => 'Encodings' ),
163 + );
164 + }
165 + return array(
166 + 'status' => 200,
167 + 'headers' => array(),
168 + 'body' => $response,
169 + );
170 + }
171 +
172 + /**
173 + * Choose a format before claiming.
174 + *
175 + * @param array $printer Printer data.
176 + * @param array $poll Parsed request.
177 + * @param array $job Candidate job.
178 + */
179 + public function negotiate( array $printer, array $poll, array $job ): array {
180 + // The fetch GET names the printer's chosen media type. A type the server
181 + // cannot produce is answered with 415 (per the CloudPRNT spec) and the job
182 + // is left unclaimed. What is servable is deliberately wider than what the
183 + // poll advertised: the printer naming a type is a stronger signal than our
184 + // cached capability answer, so a capability update landing between the two
185 + // requests must not reject a format we had just offered. Firmware that
186 + // omits the parameter gets our best offer for this printer instead. The
187 + // logged value is length-capped: printers poll every few seconds, so a
188 + // wedged loop must not flood the log with unbounded input.
189 + $servable = ( new Cloud_Print_Media_Types() )->servable_for_job( $job, $printer );
190 + $requested = $poll['type'];
191 + $chosen = '' === $requested ? $this->media_types_for_job( $job, $printer )[0] : Cloud_Print_Media_Types::match( $requested, $servable );
192 + if ( '' === $chosen ) {
193 + Logger::warning( sprintf( '%s: printer "%s" requested media type "%s" for print job %d, which the server can only serve as %s.', $poll['route'], $printer['id'], substr( $requested, 0, 100 ), (int) $job['id'], implode( ', ', $servable ) ) );
194 + return array(
195 + 'response' => array(
196 + 'status' => 415,
197 + 'headers' => array(),
198 + 'body' => array(
199 + 'code' => 'wcpos_print_job_incompatible_media_type',
200 + 'message' => __( 'The print job is not available in the requested media type.', 'woocommerce-pos' ),
201 + 'data' => array( 'status' => 415 ),
202 + ),
203 + ),
204 + );
205 + }
206 + return array( 'media_type' => $chosen );
207 + }
208 +
209 + /**
210 + * Build the print-data response, including empty renders.
211 + *
212 + * @param array $job Claimed job.
213 + * @param array $render Rendered bytes.
214 + * @param array $poll Parsed request.
215 + */
216 + public function deliver( array $job, array $render, array $poll ): array {
217 + $chosen = $poll['media_type'];
218 + return array(
219 + 'status' => 200,
220 + 'headers' => array_merge( array( 'Content-Type' => $chosen ), self::control_headers( $chosen, $render ) ),
221 + 'body' => $render['body'],
222 + );
223 + }
224 +
225 + /**
226 + * Identify the result target.
227 + *
228 + * @param array $poll Parsed request.
229 + */
230 + public function match_result( array $poll ): array {
231 + return array( 'job_id' => $poll['token'] );
232 + }
233 +
234 + /**
235 + * Decode the printer result.
236 + *
237 + * @param array $poll Parsed request.
238 + */
239 + public function interpret_result( array $poll ): array {
240 + $code = $poll['code'];
241 + return array(
242 + 'ok' => '' === $code || '000' === $code || 1 === preg_match( '/^2\d{2,3}(?:\s|$)/', $code ),
243 + 'code' => $code,
244 + 'detail' => '',
245 + );
246 + }
247 +
248 + /**
249 + * List the formats this printer can decode.
250 + *
251 + * @param array $job Candidate job.
252 + * @param array $printer Printer and cached encodings.
253 + * @return array Ordered media types.
254 + */
255 + private function media_types_for_job( array $job, array $printer ): array {
256 + return ( new Cloud_Print_Media_Types() )->for_job( $job, $printer, $printer['encodings'] );
257 + }
258 +
259 + /**
260 + * Peripheral-control headers for a job served in a command-free format.
261 + *
262 + * `text/plain` and images carry no cut or drawer commands, so CloudPRNT reads
263 + * them off the fetch response instead. Command formats express both in-band
264 + * and must not also be told to cut, or the receipt cuts twice.
265 + *
266 + * Both headers are always sent, `none` included. Omitting them leaves the
267 + * decision to the printer's own defaults, which cut plain-text jobs — so a
268 + * template that deliberately does not cut would cut anyway, and would behave
269 + * differently in text than in StarPRNT. Saying `none` out loud keeps the two
270 + * formats rendering the same receipt.
271 + *
272 + * @param string $media_type The media type being served.
273 + * @param array $render Render result from Print_Job_Service::render_job().
274 + *
275 + * @return array<string, string>
276 + */
277 + private static function control_headers( string $media_type, array $render ): array {
278 + if ( ! Cloud_Print_Media_Types::is_header_controlled( $media_type ) ) {
279 + return array();
280 + }
281 +
282 + $headers = array(
283 + 'X-Star-Cut' => null === $render['cut'] ? 'none' : (string) $render['cut'],
284 + 'X-Star-CashDrawer' => null === $render['drawer'] ? 'none' : (string) $render['drawer'],
285 + );
286 +
287 + // The raster is already two-colour, so the printer's Floyd-Steinberg
288 + // default would dither an image that has nothing left to dither —
289 + // softening crisp black-on-white text into stipple.
290 + if ( Cloud_Print_Media_Types::PNG === Cloud_Print_Media_Types::normalize( $media_type ) ) {
291 + $headers['X-Star-ImageDitherPattern'] = 'none';
292 + }
293 +
294 + return $headers;
92 295 }
93 296 }