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 / Star_Cloudprnt_Adapter.php

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

297 lines 10.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Star CloudPRNT 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 use WCPOS\WooCommercePOS\Logger;
12 use WCPOS\WooCommercePOS\Services\Cloud_Print_Media_Types;
13 use WCPOS\WooCommercePOS\Services\Cloud_Print_Poll_Request;
14
15 /**
16 * Star_Cloudprnt_Adapter class.
17 */
18 class Star_Cloudprnt_Adapter implements Poll_Provider_Adapter_Interface {
19 /**
20 * Return the StarPRNT content type.
21 *
22 * @return string
23 */
24 public function content_type(): string {
25 return 'application/vnd.star.starprnt';
26 }
27
28 /**
29 * Resolve StarPRNT format data.
30 *
31 * @param array $printer Printer configuration.
32 * @param array $template Template configuration.
33 *
34 * @return array{kind:string, content_type:string}
35 */
36 public function format( array $printer, array $template ): array {
37 if ( 'thermal' !== (string) ( $template['engine'] ?? '' ) ) {
38 return array(
39 'kind' => '',
40 'content_type' => '',
41 );
42 }
43
44 return array(
45 'kind' => 'starprnt',
46 'content_type' => $this->content_type(),
47 );
48 }
49
50 /**
51 * Build a native StarPRNT diagnostic.
52 *
53 * @param string $printer_name Printer display name.
54 *
55 * @return array{content_type:string, payload:string}
56 */
57 public function diagnostic( string $printer_name ): array {
58 $name = (string) preg_replace( '/[\x00-\x1F\x7F]/', '', $printer_name );
59 $date = gmdate( 'Y-m-d H:i' );
60 $bytes = "\x1B\x1D\x29\x55\x02\x00\x30\x01";
61 $bytes .= "\x1B\x1D\x29\x55\x02\x00\x40\x00";
62 $bytes .= "\x1B\x1D\x61\x01WCPOS\nCloud Print Test\n\x1B\x1D\x61\x00";
63 $bytes .= 'Printer: ' . $name . "\nDate: " . $date . "\nIf you can read this, printing works!\n\n\n";
64 $bytes .= "\x1B\x64\x03";
65
66 return array(
67 'content_type' => $this->content_type(),
68 'payload' => base64_encode( $bytes ),
69 );
70 }
71
72 /**
73 * Resolve polling status.
74 *
75 * @param array $printer Printer configuration.
76 * @param array $context Runtime status context.
77 *
78 * @return string
79 */
80 public function status( array $printer, array $context ): string {
81 $relay = $context['relay_status'] ?? null;
82 if ( \is_array( $relay ) && 'blocked' === ( $relay['origin_status'] ?? '' ) ) {
83 return 'blocked';
84 }
85 if ( \is_array( $relay ) && null !== ( $relay['last_seen_seconds_ago'] ?? null ) && (int) $relay['last_seen_seconds_ago'] <= (int) $context['seen_ttl'] ) {
86 return 'connected';
87 }
88
89 $seen = (int) ( $context['seen'] ?? 0 );
90 if ( 0 === $seen ) {
91 return 'waiting';
92 }
93
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;
295 }
296 }
297