PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.18
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.18
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 1.9.14 1.9.13 All 162 releases
woocommerce-pos / includes / Services / Cloud_Print_Poll_Request.php

Cloud_Print_Poll_Request.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.18, at includes/Services/Cloud_Print_Poll_Request.php

169 lines 4.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Star CloudPRNT poll request body.
4 *
5 * The printer POSTs a small JSON document on every poll. Until now the plugin
6 * discarded it, which cost us two things the protocol hands over for free: the
7 * printer telling us it is mid-job (so we must not offer another), and the
8 * printer's answers to capability questions we asked in an earlier poll
9 * response (`clientAction`). This value object reads both, defensively — the
10 * body comes off the public poll route, so every field is untrusted and any of
11 * them can be absent, mistyped, or hostile.
12 *
13 * @package WCPOS\WooCommercePOS\Services
14 */
15
16 namespace WCPOS\WooCommercePOS\Services;
17
18 /**
19 * Cloud_Print_Poll_Request class.
20 */
21 class Cloud_Print_Poll_Request {
22 /**
23 * Longest accepted answer string, to bound what a printer can make us store.
24 */
25 private const MAX_ANSWER_LENGTH = 512;
26
27 /**
28 * Whether the printer reports a job still printing.
29 *
30 * @var bool
31 */
32 private $printing_in_progress = false;
33
34 /**
35 * The printer's reported status code (e.g. '200 OK').
36 *
37 * @var string
38 */
39 private $status_code = '';
40
41 /**
42 * Answers to `clientAction` requests, keyed by request name.
43 *
44 * @var array<string, string>
45 */
46 private $answers = array();
47
48 /**
49 * Parse a poll body.
50 *
51 * WordPress only decodes JSON into `get_json_params()` when the request
52 * carries a JSON content type; Star firmware is not guaranteed to send one,
53 * so the raw body is decoded here as a fallback.
54 *
55 * `$json_params` is deliberately untyped. A body of `"status"` is valid JSON,
56 * so WordPress hands back a string, and a signature of `?array` would turn
57 * that into a TypeError — a 500 on a route any registered printer can reach.
58 *
59 * @param string $raw_body The raw request body.
60 * @param mixed $json_params Already-decoded JSON params, when available.
61 *
62 * @return self
63 */
64 public static function from_body( string $raw_body, $json_params = null ): self {
65 $data = \is_array( $json_params ) ? $json_params : array();
66 if ( array() === $data ) {
67 $decoded = json_decode( $raw_body, true );
68 $data = \is_array( $decoded ) ? $decoded : array();
69 }
70
71 $request = new self();
72
73 // Firmware sends this as a JSON boolean, but the string forms "true"/"false"
74 // have been seen in the wild; FILTER_VALIDATE_BOOLEAN reads both and treats
75 // anything it cannot parse as false — the safe answer, since a false
76 // negative only costs us one poll cycle.
77 $request->printing_in_progress = filter_var( $data['printingInProgress'] ?? false, FILTER_VALIDATE_BOOLEAN );
78
79 if ( isset( $data['statusCode'] ) && \is_scalar( $data['statusCode'] ) ) {
80 $request->status_code = self::clean( (string) $data['statusCode'] );
81 }
82
83 $request->answers = self::parse_client_action( $data['clientAction'] ?? null );
84
85 return $request;
86 }
87
88 /**
89 * Whether the printer says it is still printing the previous job.
90 *
91 * @return bool
92 */
93 public function printing_in_progress(): bool {
94 return $this->printing_in_progress;
95 }
96
97 /**
98 * The printer's reported status code, or '' when it sent none.
99 *
100 * @return string
101 */
102 public function status_code(): string {
103 return $this->status_code;
104 }
105
106 /**
107 * Answers to `clientAction` requests, keyed by request name.
108 *
109 * @return array<string, string>
110 */
111 public function answers(): array {
112 return $this->answers;
113 }
114
115 /**
116 * Read the `clientAction` array into request => result pairs.
117 *
118 * The printer echoes the request name alongside its answer. Firmware has
119 * been observed using both `result` and `response` for the answer key, so
120 * both are accepted.
121 *
122 * @param mixed $client_action The raw `clientAction` value.
123 *
124 * @return array<string, string>
125 */
126 private static function parse_client_action( $client_action ): array {
127 if ( ! \is_array( $client_action ) ) {
128 return array();
129 }
130
131 $answers = array();
132 foreach ( $client_action as $entry ) {
133 if ( ! \is_array( $entry ) || ! isset( $entry['request'] ) || ! \is_scalar( $entry['request'] ) ) {
134 continue;
135 }
136
137 $name = self::clean( (string) $entry['request'] );
138 if ( '' === $name ) {
139 continue;
140 }
141
142 $value = $entry['result'] ?? ( $entry['response'] ?? null );
143 if ( \is_array( $value ) ) {
144 $value = implode( ',', array_filter( $value, 'is_scalar' ) );
145 }
146 if ( ! \is_scalar( $value ) ) {
147 continue;
148 }
149
150 $answers[ $name ] = self::clean( (string) $value );
151 }
152
153 return $answers;
154 }
155
156 /**
157 * Strip control characters and cap the length of an untrusted field.
158 *
159 * @param string $value The raw value.
160 *
161 * @return string
162 */
163 private static function clean( string $value ): string {
164 $value = (string) preg_replace( '/[\x00-\x1F\x7F]/', '', $value );
165
166 return trim( substr( $value, 0, self::MAX_ANSWER_LENGTH ) );
167 }
168 }
169