PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.19
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.19
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 1.9.14 All 163 releases
← All changes | includes/API/V1/Print_Jobs_Controller.php +37 -559 1.10.81.10.19 View file →
@@ -7,16 +7,14 @@
7 7
8 8 namespace WCPOS\WooCommercePOS\API\V1;
9 9
10 10 use WCPOS\WooCommercePOS\Logger;
11 -use WCPOS\WooCommercePOS\Services\Cloud_Print_Diagnostic;
12 -use WCPOS\WooCommercePOS\Services\Cloud_Print_Media_Types;
13 -use WCPOS\WooCommercePOS\Services\Cloud_Print_Poll_Request;
11 +use WCPOS\WooCommercePOS\Interfaces\Push_Provider_Adapter_Interface;
12 +use WCPOS\WooCommercePOS\Services\Print_Job_Lifecycle;
14 13 use WCPOS\WooCommercePOS\Services\Cloud_Print_Relay_Service;
15 14 use WCPOS\WooCommercePOS\Services\Cloud_Print_Registry;
16 15 use WCPOS\WooCommercePOS\Services\Cloud_Print_Trigger_Service;
17 16 use WCPOS\WooCommercePOS\Services\PrintNode_Client;
18 -use WCPOS\WooCommercePOS\Services\Print_Format_Resolver;
19 17 use WCPOS\WooCommercePOS\Services\Print_Job_Service;
20 18 use WCPOS\WooCommercePOS\Services\Provider;
21 19 use WCPOS\WooCommercePOS\Services\Star_Online_Client;
22 20 use WP_Error;
@@ -31,19 +29,13 @@
31 29 * Print_Jobs_Controller class.
32 30 */
33 31 class Print_Jobs_Controller extends WP_REST_Controller {
34 32 /**
35 - * Milliseconds an Epson Server Direct Print printer waits for the local
36 - * print device to become printable before it gives up and reports
37 - * EX_TIMEOUT.
33 + * Legacy Epson timeout.
38 34 *
39 - * Epson allows 5000–300000. The previous 10000 (near the floor) was
40 - * unforgiving for printers that briefly go not-ready — a paper change, a
41 - * sleep/wake, or a momentary network blip — declaring the job failed
42 - * before the printer could recover. 60000 gives those transient states
43 - * room to clear without letting a genuinely offline printer hang for long.
35 + * @deprecated Use Epson_Sdp_Adapter::EPSON_SDP_PRINT_TIMEOUT_MS.
44 36 */
45 - const EPSON_SDP_PRINT_TIMEOUT_MS = 60000;
37 + const EPSON_SDP_PRINT_TIMEOUT_MS = \WCPOS\WooCommercePOS\Services\Providers\Epson_Sdp_Adapter::EPSON_SDP_PRINT_TIMEOUT_MS;
46 38
47 39 /**
48 40 * Endpoint namespace.
49 41 *
@@ -772,16 +764,15 @@
772 764 );
773 765 }
774 766 $printer = $this->registry->get_printer( (string) $source['printer_id'] );
775 767 if ( null !== $printer ) {
776 - // Refresh both halves of the pairing together. A legacy job can
777 - // carry a media type from before the provider declared its own,
768 + // Provider::format() refreshes both halves together. A legacy job
769 + // can carry a media type from before the provider declared its own,
778 770 // but content_type and pn_kind must keep agreeing: reprinting a
779 - // raw (escpos) PrintNode job through the printer-only resolver
771 + // raw (escpos) PrintNode job through printer_content_type()
780 772 // relabels it application/pdf in the queue view, even though
781 773 // submit still sends raw bytes off the stored pn_kind.
782 - $resolver = new Print_Format_Resolver();
783 - $fmt = null === $template ? array( 'kind' => '' ) : $resolver->resolve( $printer, $template );
774 + $fmt = null === $template ? array( 'kind' => '' ) : Provider::format( $printer, $template );
784 775 if ( '' === (string) $fmt['kind'] ) {
785 776 // No loadable template, or one this printer can no longer
786 777 // render. Refresh from the provider's declared type only
787 778 // when no stored kind can contradict it; otherwise the
@@ -786,9 +777,9 @@
786 777 // render. Refresh from the provider's declared type only
787 778 // when no stored kind can contradict it; otherwise the
788 779 // source pairing is the best answer left.
789 780 if ( '' === $pn_kind ) {
790 - $content_type = $resolver->content_type_for_printer( $printer );
781 + $content_type = Provider::printer_content_type( $printer );
791 782 }
792 783 } else {
793 784 $content_type = $fmt['content_type'];
794 785 $pn_kind = Provider::stores_job_kind( Provider::normalize( (string) ( $printer['provider'] ?? '' ) ) )
@@ -853,206 +844,12 @@
853 844 *
854 845 * @return \WP_REST_Response|WP_Error
855 846 */
856 847 public function cloudprnt( $request ) {
857 - $printer_id = sanitize_text_field( (string) $request->get_param( 'printer_id' ) );
858 - $this->registry->record_seen( $printer_id );
859 - $this->jobs->release_stale_claims( $printer_id );
860 -
861 - if ( 'POST' === $request->get_method() ) {
862 - return $this->cloudprnt_poll( $request, $printer_id );
863 - }
864 -
865 - $job = $this->get_cloud_job_for_request( $request, $printer_id );
866 - if ( is_wp_error( $job ) ) {
867 - return $job;
868 - }
869 -
870 - if ( 'DELETE' === $request->get_method() ) {
871 - $code = sanitize_text_field( (string) $request->get_param( 'code' ) );
872 - $status = '' === $code || '000' === $code || 1 === preg_match( '/^2\d{2,3}(?:\s|$)/', $code ) ? Print_Job_Service::STATUS_PRINTED : Print_Job_Service::STATUS_FAILED;
873 - $this->jobs->record_printer_result( (int) $job['id'], Print_Job_Service::STATUS_PRINTED === $status );
874 -
875 - if ( Print_Job_Service::STATUS_FAILED === $status ) {
876 - $this->log_printer_failure( $request, $printer_id, $code, (int) $job['id'] );
877 - }
878 -
879 - return rest_ensure_response( array( 'ok' => true ) );
880 - }
881 -
882 - return $this->cloudprnt_fetch( $request, $printer_id, $job );
848 + return $this->handle_provider_poll( $request, 'star-cloudprnt' );
883 849 }
884 850
885 851 /**
886 - * Answer a CloudPRNT poll: offer a job, and ask what the printer can decode.
887 - *
888 - * The poll body is the printer's half of the conversation. `printingInProgress`
889 - * says a job is still on the paper, and the spec is explicit that the server
890 - * must not offer another one until it clears — doing so risks the printer
891 - * dropping the second job. `clientAction` carries the printer's answers to
892 - * questions asked in an earlier poll response, which is the only way the
893 - * protocol exposes what formats the hardware can decode.
894 - *
895 - * @param WP_REST_Request $request Request.
896 - * @param string $printer_id Printer ID.
897 - *
898 - * @return \WP_REST_Response
899 - */
900 - private function cloudprnt_poll( WP_REST_Request $request, string $printer_id ) {
901 - $poll = Cloud_Print_Poll_Request::from_body( (string) $request->get_body(), $request->get_json_params() );
902 - $this->registry->record_capabilities( $printer_id, $poll->answers(), $poll->status_code() );
903 -
904 - $response = array( 'jobReady' => false );
905 -
906 - if ( ! $poll->printing_in_progress() && ! $this->jobs->find_active_claim( $printer_id ) ) {
907 - $job = $this->jobs->next_pending( $printer_id );
908 - if ( null !== $job ) {
909 - // The offer is a list: the printer picks its preferred decodable
910 - // entry and names it in the fetch's `?type`. Nothing decodable in
911 - // the list means no GET at all, just a 510 confirmation — so the
912 - // list is filtered by what this printer said it can decode.
913 - // `mediaType` (singular) is kept for older firmware.
914 - $media_types = $this->media_types_for_job( $job, $printer_id );
915 - $response = array(
916 - 'jobReady' => true,
917 - 'jobToken' => (string) $job['id'],
918 - 'mediaType' => $media_types[0],
919 - 'mediaTypes' => array_values( $media_types ),
920 - );
921 - }
922 - }
923 -
924 - if ( $this->registry->should_request_capabilities( $printer_id ) ) {
925 - $response['clientAction'] = array(
926 - array( 'request' => 'ClientType' ),
927 - array( 'request' => 'Encodings' ),
928 - );
929 - $this->registry->record_capability_request( $printer_id );
930 - }
931 -
932 - return rest_ensure_response( $response );
933 - }
934 -
935 - /**
936 - * Serve a CloudPRNT job in the media type the printer asked for.
937 - *
938 - * @param WP_REST_Request $request Request.
939 - * @param string $printer_id Printer ID.
940 - * @param array $job Job array.
941 - *
942 - * @return \WP_REST_Response|WP_Error
943 - */
944 - private function cloudprnt_fetch( WP_REST_Request $request, string $printer_id, array $job ) {
945 - // The fetch GET names the printer's chosen media type. A type the server
946 - // cannot produce is answered with 415 (per the CloudPRNT spec) and the job
947 - // is left unclaimed. What is servable is deliberately wider than what the
948 - // poll advertised: the printer naming a type is a stronger signal than our
949 - // cached capability answer, so a capability update landing between the two
950 - // requests must not reject a format we had just offered. Firmware that
951 - // omits the parameter gets our best offer for this printer instead. The
952 - // logged value is length-capped: printers poll every few seconds, so a
953 - // wedged loop must not flood the log with unbounded input.
954 - $servable = ( new Cloud_Print_Media_Types() )->servable_for_job( $job, $this->registry->get_printer( $printer_id ) );
955 - $requested = sanitize_text_field( (string) $request->get_param( 'type' ) );
956 - $chosen = '' === $requested
957 - ? $this->media_types_for_job( $job, $printer_id )[0]
958 - : Cloud_Print_Media_Types::match( $requested, $servable );
959 -
960 - if ( '' === $chosen ) {
961 - Logger::warning(
962 - sprintf(
963 - '%s: printer "%s" requested media type "%s" for print job %d, which the server can only serve as %s.',
964 - $request->get_route(),
965 - $printer_id,
966 - substr( $requested, 0, 100 ),
967 - (int) $job['id'],
968 - implode( ', ', $servable )
969 - )
970 - );
971 -
972 - return new WP_Error(
973 - 'wcpos_print_job_incompatible_media_type',
974 - __( 'The print job is not available in the requested media type.', 'woocommerce-pos' ),
975 - array( 'status' => 415 )
976 - );
977 - }
978 -
979 - if ( ! $this->jobs->try_claim( (int) $job['id'] ) ) {
980 - return rest_ensure_response( array( 'jobReady' => false ) );
981 - }
982 -
983 - $render = $this->jobs->render_job( $job, $chosen );
984 - if ( '' === $render['body'] ) {
985 - Logger::error(
986 - sprintf(
987 - '%s: print job %d rendered an empty payload for printer "%s".',
988 - $request->get_route(),
989 - (int) $job['id'],
990 - $printer_id
991 - )
992 - );
993 - }
994 -
995 - return $this->serve_raw( $render['body'], $chosen, self::control_headers( $chosen, $render ) );
996 - }
997 -
998 - /**
999 - * The media types a CloudPRNT job can be served in, best first.
1000 - *
1001 - * @param array $job Job array.
1002 - * @param string $printer_id Printer ID.
1003 - *
1004 - * @return array<int, string>
1005 - */
1006 - private function media_types_for_job( array $job, string $printer_id ): array {
1007 - $capabilities = $this->registry->get_capabilities( $printer_id );
1008 -
1009 - return ( new Cloud_Print_Media_Types() )->for_job(
1010 - $job,
1011 - $this->registry->get_printer( $printer_id ),
1012 - $capabilities['encodings']
1013 - );
1014 - }
1015 -
1016 - /**
1017 - * Peripheral-control headers for a job served in a command-free format.
1018 - *
1019 - * `text/plain` and images carry no cut or drawer commands, so CloudPRNT reads
1020 - * them off the fetch response instead. Command formats express both in-band
1021 - * and must not also be told to cut, or the receipt cuts twice.
1022 - *
1023 - * Both headers are always sent, `none` included. Omitting them leaves the
1024 - * decision to the printer's own defaults, which cut plain-text jobs — so a
1025 - * template that deliberately does not cut would cut anyway, and would behave
1026 - * differently in text than in StarPRNT. Saying `none` out loud keeps the two
1027 - * formats rendering the same receipt.
1028 - *
1029 - * @param string $media_type The media type being served.
1030 - * @param array $render Render result from Print_Job_Service::render_job().
1031 - *
1032 - * @return array<string, string>
1033 - */
1034 - private static function control_headers( string $media_type, array $render ): array {
1035 - if ( ! Cloud_Print_Media_Types::is_header_controlled( $media_type ) ) {
1036 - return array();
1037 - }
1038 -
1039 - $headers = array(
1040 - 'X-Star-Cut' => null === $render['cut'] ? 'none' : (string) $render['cut'],
1041 - 'X-Star-CashDrawer' => null === $render['drawer'] ? 'none' : (string) $render['drawer'],
1042 - );
1043 -
1044 - // The raster is already two-colour, so the printer's Floyd-Steinberg
1045 - // default would dither an image that has nothing left to dither —
1046 - // softening crisp black-on-white text into stipple.
1047 - if ( Cloud_Print_Media_Types::PNG === Cloud_Print_Media_Types::normalize( $media_type ) ) {
1048 - $headers['X-Star-ImageDitherPattern'] = 'none';
1049 - }
1050 -
1051 - return $headers;
1052 - }
1053 -
1054 - /**
1055 852 * Permission check for printer-token routes.
1056 853 *
1057 854 * @param WP_REST_Request $request Request.
1058 855 *
@@ -1080,110 +877,40 @@
1080 877
1081 878 return true;
1082 879 }
1083 880
1084 - /**
1085 - * Resolve and authorize a CloudPRNT job token.
1086 - *
1087 - * @param WP_REST_Request $request Request.
1088 - * @param string $printer_id Printer ID.
1089 - *
1090 - * @return array|WP_Error
1091 - */
1092 - private function get_cloud_job_for_request( WP_REST_Request $request, string $printer_id ) {
1093 - $job_id = (int) $request->get_param( 'token' );
1094 - $job = $this->jobs->get( $job_id );
1095 - if ( null === $job || $printer_id !== $job['printer_id'] ) {
1096 - Logger::warning(
1097 - sprintf(
1098 - '%s: print job "%d" was not found for printer "%s".',
1099 - $request->get_route(),
1100 - $job_id,
1101 - $printer_id
1102 - )
1103 - );
1104 881
1105 - return new WP_Error(
1106 - 'wcpos_print_job_not_found',
1107 - __( 'Print job not found.', 'woocommerce-pos' ),
1108 - array( 'status' => 404 )
1109 - );
1110 - }
1111 882
1112 - return $job;
1113 - }
1114 883
1115 884 /**
1116 - * Log a printer-reported failure without request credentials or payloads.
885 + * Parse the vendor request and serve the lifecycle's plain-data response.
1117 886 *
1118 887 * @param WP_REST_Request $request Request.
1119 - * @param string $printer_id Printer ID.
1120 - * @param string $code Failure code.
1121 - * @param int $job_id Print job ID.
1122 - * @param int|null $status Epson ePOS response status bitmask, when the result carried one.
888 + * @param string $provider_key Protocol selected by the route.
889 + * @return WP_REST_Response|WP_Error
1123 890 */
1124 - private function log_printer_failure( WP_REST_Request $request, string $printer_id, string $code, int $job_id, ?int $status = null ): void {
1125 - // One detail string serves both the stored reason and the log line, and it
1126 - // keeps the raw hex even when no bit decodes, so an unmapped or
1127 - // model-specific status can still be read back from a screenshot.
1128 - $flags = null === $status ? '' : implode( ', ', self::describe_epson_status( $status ) );
1129 - $detail = null === $status ? '' : sprintf( ' (0x%08X%s)', $status, '' === $flags ? '' : ': ' . $flags );
1130 - $reason = $code . $detail;
1131 -
1132 - // Persist it too, not just log it. Push providers already record their
1133 - // submission error against the job; polling printers report theirs here,
1134 - // and without this the queue can only ever say "Failed" while the reason
1135 - // (EX_TIMEOUT, a Star status code) is buried in the log. The decoded
1136 - // Epson status bits are what turn "EX_TIMEOUT" into "cover open".
1137 - if ( '' !== $reason ) {
1138 - update_post_meta( $job_id, Print_Job_Service::META_ERROR, sanitize_text_field( $reason ) );
891 + private function handle_provider_poll( WP_REST_Request $request, string $provider_key ) {
892 + $printer_id = sanitize_text_field( (string) $request->get_param( 'printer_id' ) );
893 + $printer = $this->registry->get_printer( $printer_id );
894 + if ( null === $printer ) {
895 + return new WP_Error( 'wcpos_print_job_invalid_token', __( 'Invalid printer token.', 'woocommerce-pos' ), array( 'status' => 401 ) );
1139 896 }
1140 -
1141 - Logger::error(
1142 - sprintf(
1143 - '%s: printer "%s" reported failure code "%s"%s for print job %d.',
1144 - $request->get_route(),
1145 - $printer_id,
1146 - $code,
1147 - $detail,
1148 - $job_id
897 + $poll = Provider::poll_adapter( $provider_key )->parse(
898 + array(
899 + 'params' => $request->get_params(),
900 + 'body' => (string) $request->get_body(),
901 + 'json' => 'POST' === $request->get_method() ? $request->get_json_params() : null,
902 + 'method' => $request->get_method(),
903 + 'route' => $request->get_route(),
1149 904 )
1150 905 );
1151 - }
1152 -
1153 - /**
1154 - * Decode an Epson ePOS-Print response status bitmask.
1155 - *
1156 - * @param int $status Decimal ASB status bitmask.
1157 - *
1158 - * @return array<int, string>
1159 - */
1160 - private static function describe_epson_status( int $status ): array {
1161 - // Epson ePOS-Print XML User's Manual, response `status` table (ASB bits).
1162 - // Fault bits only: informational ones (print complete, drawer pin, feed
1163 - // button, panel switch, buzzer) say nothing about why a print failed.
1164 - $labels = array(
1165 - 0x00000001 => __( 'no response from printer', 'woocommerce-pos' ),
1166 - 0x00000008 => __( 'offline', 'woocommerce-pos' ),
1167 - 0x00000020 => __( 'cover open', 'woocommerce-pos' ),
1168 - 0x00000100 => __( 'waiting for online recovery', 'woocommerce-pos' ),
1169 - 0x00000400 => __( 'mechanical error', 'woocommerce-pos' ),
1170 - 0x00000800 => __( 'autocutter error', 'woocommerce-pos' ),
1171 - 0x00002000 => __( 'unrecoverable error', 'woocommerce-pos' ),
1172 - 0x00004000 => __( 'auto-recoverable error', 'woocommerce-pos' ),
1173 - 0x00020000 => __( 'paper near end', 'woocommerce-pos' ),
1174 - 0x00080000 => __( 'paper end', 'woocommerce-pos' ),
1175 - 0x80000000 => __( 'spooler stopped', 'woocommerce-pos' ),
1176 - );
1177 -
1178 - $descriptions = array();
1179 - foreach ( $labels as $bit => $label ) {
1180 - if ( 0 !== ( $status & $bit ) ) {
1181 - $descriptions[] = $label;
1182 - }
906 + $result = ( new Print_Job_Lifecycle( $this->jobs, $this->registry ) )->handle_poll( $provider_key, $printer, $poll );
907 + if ( \is_string( $result['body'] ) ) {
908 + $response = $this->serve_raw( $result['body'], $result['headers']['Content-Type'], $result['headers'] );
909 + $response->set_status( $result['status'] );
910 + return $response;
1183 911 }
1184 -
1185 - return $descriptions;
912 + return new WP_REST_Response( $result['body'], $result['status'], $result['headers'] );
1186 913 }
1187 914
1188 915 /**
1189 916 * Serve raw bytes from a REST callback.
@@ -1206,155 +933,9 @@
1206 933 *
1207 934 * @return \WP_REST_Response
1208 935 */
1209 936 public function epson_sdp( $request ) {
1210 - $printer_id = sanitize_text_field( (string) $request->get_param( 'printer_id' ) );
1211 - $this->registry->record_seen( $printer_id );
1212 - $raw_body = (string) $request->get_body();
1213 - $soap = 'text/xml; charset=utf-8';
1214 - $ack = '<response success="true" code="" status=""/>';
1215 -
1216 - $this->jobs->release_stale_claims( $printer_id );
1217 -
1218 - // Server Direct Print multiplexes three different request types onto the
1219 - // one configured URL, as URL-encoded form data distinguished by
1220 - // ConnectionType (User's Manual Rev.K, ch.3 and the Test_print.php
1221 - // reference implementation):
1222 - //
1223 - // GetRequest — poll for a job
1224 - // SetResponse — printing result; the XML rides in the ResponseFile field
1225 - // SetStatus — status notification; the XML rides in the Status field
1226 - //
1227 - // Answering a status notification with print data hands the job to a
1228 - // request that discards it, so the printer never prints and the job stays
1229 - // claimed. Dispatching on ConnectionType is what keeps the job on the
1230 - // GetRequest that is actually asking for one.
1231 - $connection_type = (string) $request->get_param( 'ConnectionType' );
1232 -
1233 - // Result XML is a form field, so in the raw body it is percent-encoded
1234 - // (`<response` arrives as `%3Cresponse`) and a raw-body substring test can
1235 - // never match it. The raw-body branch is kept only for a caller that posts
1236 - // the bare XML, which the printer never does.
1237 - $result_xml = (string) $request->get_param( 'ResponseFile' );
1238 - if ( '' === $result_xml && false !== strpos( $raw_body, '<response' ) ) {
1239 - $result_xml = $raw_body;
1240 - }
1241 -
1242 - // A print result is recognised by its ePOS <response> element, whichever
1243 - // way it arrived — the bare-XML caller sends no ConnectionType, so the
1244 - // type is deliberately not consulted here. The printer also posts an
1245 - // empty <PrintResponseInfo/> after every idle poll; reading that as a
1246 - // result marked the in-flight job failed ("unknown"). It is acked below
1247 - // and never dispatched on.
1248 - if ( false !== strpos( $result_xml, '<response' ) ) {
1249 - $claim = $this->jobs->find_active_claim( $printer_id );
1250 - if ( null === $claim ) {
1251 - // A result that arrives after the claim timed out belongs to the job
1252 - // that was failed as unconfirmed — record it there instead of
1253 - // dropping it, so a printer that merely reported late shows the truth.
1254 - $claim = $this->jobs->find_unconfirmed( $printer_id );
1255 - } else {
1256 - // A result carries no job token — printjobid is SDP 2.00 only — so it
1257 - // can only be attributed to the active claim. A printer holding an
1258 - // unsent result retries that POST before polling for more work, so a
1259 - // result should not arrive while an earlier job is still awaiting one.
1260 - // Log it if it ever does: the attribution below would be the wrong job.
1261 - $unconfirmed = $this->jobs->find_unconfirmed( $printer_id );
1262 - if ( null !== $unconfirmed ) {
1263 - Logger::warning(
1264 - sprintf(
1265 - 'Printer "%s": result recorded against claimed job %d while unconfirmed job %d is still awaiting one.',
1266 - $printer_id,
1267 - (int) $claim['id'],
1268 - (int) $unconfirmed['id']
1269 - )
1270 - );
1271 - }
1272 - }
1273 - if ( null !== $claim ) {
1274 - $ok = false !== strpos( $result_xml, 'success="true"' );
1275 - $this->jobs->record_printer_result( (int) $claim['id'], $ok );
1276 -
1277 - if ( ! $ok ) {
1278 - $code = 'unknown';
1279 - if ( 1 === preg_match( '/\bcode="([^"]*)"/', $result_xml, $matches ) ) {
1280 - $code = sanitize_text_field( $matches[1] );
1281 - }
1282 -
1283 - $status = null;
1284 - if ( 1 === preg_match( '/\bstatus="(\d+)"/', $result_xml, $matches ) ) {
1285 - // The ASB status is unsigned 32-bit; on a 32-bit PHP build a value
1286 - // with bit 31 set does not fit and (int) would saturate to
1287 - // 0x7FFFFFFF, decoding every label. Such a value is left undecoded
1288 - // rather than misdecoded.
1289 - $status = (float) $matches[1] <= PHP_INT_MAX ? (int) $matches[1] : null;
1290 - }
1291 -
1292 - $this->log_printer_failure( $request, $printer_id, $code, (int) $claim['id'], $status );
1293 - }
1294 - }
1295 -
1296 - return $this->serve_raw( $ack, $soap );
1297 - }
1298 -
1299 - // A status notification or a result post — typed, or recognisable only by
1300 - // its ResponseFile — is not asking for work. Only the GetRequest lane below
1301 - // (and a legacy untyped poll) may be handed a job.
1302 - if ( 'SetStatus' === $connection_type || 'SetResponse' === $connection_type || '' !== $result_xml ) {
1303 - return $this->serve_raw( $ack, $soap );
1304 - }
1305 -
1306 - if ( null !== $this->jobs->find_active_claim( $printer_id ) ) {
1307 - return $this->serve_raw( $ack, $soap );
1308 - }
1309 -
1310 - $job = $this->jobs->next_pending( $printer_id );
1311 - if ( null === $job ) {
1312 - return $this->serve_raw( $ack, $soap );
1313 - }
1314 -
1315 - if ( ! $this->jobs->try_claim( (int) $job['id'] ) ) {
1316 - return $this->serve_raw( $ack, $soap );
1317 - }
1318 - $epos = $this->jobs->render_payload( $job );
1319 -
1320 - // An empty render means the job produced nothing printable — most often
1321 - // a template whose engine this provider cannot render (Server Direct
1322 - // Print only speaks the thermal pipeline's ePOS-Print XML). Dispatching
1323 - // it anyway sends <PrintData></PrintData>: the printer parses that
1324 - // happily, prints nothing, and posts back success="true", so the job is
1325 - // recorded as Printed. Fail the job here instead, so the queue shows the
1326 - // truth and the log names the printer.
1327 - if ( '' === $epos ) {
1328 - Logger::error(
1329 - sprintf(
1330 - '%s: print job %d rendered an empty payload for printer "%s"; nothing was sent to the printer.',
1331 - $request->get_route(),
1332 - (int) $job['id'],
1333 - $printer_id
1334 - )
1335 - );
1336 - update_post_meta( (int) $job['id'], Print_Job_Service::META_ERROR, 'empty_rendered_payload' );
1337 - $this->jobs->set_status( (int) $job['id'], Print_Job_Service::STATUS_FAILED );
1338 -
1339 - return $this->serve_raw( $ack, $soap );
1340 - }
1341 -
1342 - // Server Direct Print expects the print data wrapped in
1343 - // PrintRequestInfo > ePOSPrint > PrintData — NOT the SOAP envelope used
1344 - // by the direct ePOS-Print web service, which is a different protocol.
1345 - // A printer that receives an unrecognised wrapper discards it silently:
1346 - // it neither prints nor posts a result, so the job sits claimed forever.
1347 - // Version 1.00 is the only version every SDP printer family supports
1348 - // (Server Direct Print User's Manual Rev.K, "Response (Print request)");
1349 - // 2.00+ adds printjobid but is limited to TM-i/TM-DT/TM-T88VI.
1350 - $envelope = '<?xml version="1.0" encoding="utf-8"?>';
1351 - $envelope .= '<PrintRequestInfo Version="1.00"><ePOSPrint>';
1352 - $envelope .= '<Parameter><devid>local_printer</devid><timeout>' . self::EPSON_SDP_PRINT_TIMEOUT_MS . '</timeout></Parameter>';
1353 - $envelope .= '<PrintData>' . $epos . '</PrintData>';
1354 - $envelope .= '</ePOSPrint></PrintRequestInfo>';
1355 -
1356 - return $this->serve_raw( $envelope, $soap );
937 + return $this->handle_provider_poll( $request, 'epson-sdp' );
1357 938 }
1358 939
1359 940 /**
1360 941 * Enqueue a print job (raw payload or order-based).
@@ -1515,26 +1096,14 @@
1515 1096 // Legacy printer rows saved before the provider field existed must test
1516 1097 // as the default provider, not fall through to the no-diagnostic error.
1517 1098 $provider = Provider::normalize( \is_string( $printer['provider'] ?? null ) ? $printer['provider'] : null );
1518 1099
1519 - if ( 'printnode' === $provider ) {
1520 - return $this->test_print_printnode( $printer );
1100 + $adapter = Provider::adapter( $provider );
1101 + if ( $adapter instanceof Push_Provider_Adapter_Interface ) {
1102 + return $adapter->test_print( $printer );
1521 1103 }
1104 + $diag = $adapter->diagnostic( (string) $printer['name'] );
1522 1105
1523 - if ( 'star-online' === $provider ) {
1524 - return $this->test_print_star_online( $printer_id, $printer );
1525 - }
1526 -
1527 - try {
1528 - $diag = ( new Cloud_Print_Diagnostic() )->build( $provider, (string) $printer['name'] );
1529 - } catch ( \RuntimeException $e ) {
1530 - return new WP_Error(
1531 - 'wcpos_print_job_no_diagnostic',
1532 - __( 'Test print is not available for this printer yet.', 'woocommerce-pos' ),
1533 - array( 'status' => 400 )
1534 - );
1535 - }
1536 -
1537 1106 $id = $this->jobs->create(
1538 1107 array(
1539 1108 'printer_id' => $printer_id,
1540 1109 'content_type' => $diag['content_type'],
@@ -1554,100 +1123,9 @@
1554 1123
1555 1124 return $response;
1556 1125 }
1557 1126
1558 - /**
1559 - * Queue a Star Markup test receipt and submit it through the push pipeline.
1560 - *
1561 - * @param string $printer_id Registered printer id.
1562 - * @param array $printer Registered star-online printer.
1563 - *
1564 - * @return \WP_REST_Response|WP_Error
1565 - */
1566 - private function test_print_star_online( string $printer_id, array $printer ) {
1567 - $markup = ( new Cloud_Print_Diagnostic() )->star_markup( (string) $printer['name'] );
1568 1127
1569 - $id = $this->jobs->create(
1570 - array(
1571 - 'printer_id' => $printer_id,
1572 - 'content_type' => 'text/vnd.star.markup',
1573 - 'payload' => base64_encode( $markup ),
1574 - )
1575 - );
1576 - if ( $id <= 0 ) {
1577 - return new WP_Error(
1578 - 'wcpos_print_job_create_failed',
1579 - __( 'Print job could not be created.', 'woocommerce-pos' ),
1580 - array( 'status' => 500 )
1581 - );
1582 - }
1583 -
1584 - wp_schedule_single_event( time(), Cloud_Print_Trigger_Service::CRON_SUBMIT, array( $id ) );
1585 - ( new \WCPOS\WooCommercePOS\Services\Cloud_Print_Submit_Service() )->submit( $id );
1586 -
1587 - $response = rest_ensure_response( $this->jobs->get( $id ) );
1588 - $response->set_status( 201 );
1589 -
1590 - return $response;
1591 - }
1592 -
1593 - /**
1594 - * Submit a diagnostic PDF to a PrintNode printer.
1595 - *
1596 - * @param array $printer Registered PrintNode printer.
1597 - *
1598 - * @return \WP_REST_Response|WP_Error
1599 - */
1600 - private function test_print_printnode( array $printer ) {
1601 - $api_key = (string) ( $printer['printnode_api_key'] ?? '' );
1602 - $pn_printer_id = (int) ( $printer['printnode_printer_id'] ?? 0 );
1603 - if ( '' === $api_key || 0 === $pn_printer_id ) {
1604 - return new WP_Error(
1605 - 'wcpos_print_job_printnode_unconfigured',
1606 - __( 'This PrintNode printer is missing its API key or printer id.', 'woocommerce-pos' ),
1607 - array( 'status' => 400 )
1608 - );
1609 - }
1610 -
1611 - try {
1612 - $pdf = ( new Cloud_Print_Diagnostic() )->build_pdf( (string) $printer['name'] );
1613 - } catch ( \Throwable $e ) {
1614 - // Defense in depth: a Dompdf/font-cache/temp-dir failure must not
1615 - // surface as an uncaught 500. Mirror the render_payload() guard.
1616 - Logger::log( 'Cloud print: PrintNode diagnostic PDF render failed: ' . $e->getMessage() );
1617 -
1618 - return new WP_Error(
1619 - 'wcpos_print_job_diagnostic_failed',
1620 - __( 'Could not generate the test print.', 'woocommerce-pos' ),
1621 - array( 'status' => 500 )
1622 - );
1623 - }
1624 -
1625 - $result = ( new PrintNode_Client( $api_key ) )->submit_job(
1626 - $pn_printer_id,
1627 - 'WCPOS Test Print',
1628 - 'pdf_base64',
1629 - base64_encode( $pdf )
1630 - );
1631 -
1632 - if ( is_wp_error( $result ) ) {
1633 - return new WP_Error(
1634 - 'wcpos_print_job_printnode_failed',
1635 - $result->get_error_message(),
1636 - array( 'status' => 502 )
1637 - );
1638 - }
1639 -
1640 - return new WP_REST_Response(
1641 - array(
1642 - 'submitted' => true,
1643 - 'external_provider' => 'printnode',
1644 - 'external_job_id' => (string) $result['id'],
1645 - 'external_state' => 'submitted',
1646 - ),
1647 - 201
1648 - );
1649 - }
1650 1128
1651 1129 /**
1652 1130 * Extract sanitized cash-drawer options from a REST request.
1653 1131 *