*/ private $answers = array(); /** * Parse a poll body. * * WordPress only decodes JSON into `get_json_params()` when the request * carries a JSON content type; Star firmware is not guaranteed to send one, * so the raw body is decoded here as a fallback. * * `$json_params` is deliberately untyped. A body of `"status"` is valid JSON, * so WordPress hands back a string, and a signature of `?array` would turn * that into a TypeError — a 500 on a route any registered printer can reach. * * @param string $raw_body The raw request body. * @param mixed $json_params Already-decoded JSON params, when available. * * @return self */ public static function from_body( string $raw_body, $json_params = null ): self { $data = \is_array( $json_params ) ? $json_params : array(); if ( array() === $data ) { $decoded = json_decode( $raw_body, true ); $data = \is_array( $decoded ) ? $decoded : array(); } $request = new self(); // Firmware sends this as a JSON boolean, but the string forms "true"/"false" // have been seen in the wild; FILTER_VALIDATE_BOOLEAN reads both and treats // anything it cannot parse as false — the safe answer, since a false // negative only costs us one poll cycle. $request->printing_in_progress = filter_var( $data['printingInProgress'] ?? false, FILTER_VALIDATE_BOOLEAN ); if ( isset( $data['statusCode'] ) && \is_scalar( $data['statusCode'] ) ) { $request->status_code = self::clean( (string) $data['statusCode'] ); } $request->answers = self::parse_client_action( $data['clientAction'] ?? null ); return $request; } /** * Whether the printer says it is still printing the previous job. * * @return bool */ public function printing_in_progress(): bool { return $this->printing_in_progress; } /** * The printer's reported status code, or '' when it sent none. * * @return string */ public function status_code(): string { return $this->status_code; } /** * Answers to `clientAction` requests, keyed by request name. * * @return array */ public function answers(): array { return $this->answers; } /** * Read the `clientAction` array into request => result pairs. * * The printer echoes the request name alongside its answer. Firmware has * been observed using both `result` and `response` for the answer key, so * both are accepted. * * @param mixed $client_action The raw `clientAction` value. * * @return array */ private static function parse_client_action( $client_action ): array { if ( ! \is_array( $client_action ) ) { return array(); } $answers = array(); foreach ( $client_action as $entry ) { if ( ! \is_array( $entry ) || ! isset( $entry['request'] ) || ! \is_scalar( $entry['request'] ) ) { continue; } $name = self::clean( (string) $entry['request'] ); if ( '' === $name ) { continue; } $value = $entry['result'] ?? ( $entry['response'] ?? null ); if ( \is_array( $value ) ) { $value = implode( ',', array_filter( $value, 'is_scalar' ) ); } if ( ! \is_scalar( $value ) ) { continue; } $answers[ $name ] = self::clean( (string) $value ); } return $answers; } /** * Strip control characters and cap the length of an untrusted field. * * @param string $value The raw value. * * @return string */ private static function clean( string $value ): string { $value = (string) preg_replace( '/[\x00-\x1F\x7F]/', '', $value ); return trim( substr( $value, 0, self::MAX_ANSWER_LENGTH ) ); } }