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.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 +116 -446 1.10.21.10.18 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 *
@@ -172,8 +164,20 @@
172 164 );
173 165
174 166 register_rest_route(
175 167 $this->namespace,
168 + '/' . $this->rest_base . '/queue/delete',
169 + array(
170 + array(
171 + 'methods' => WP_REST_Server::CREATABLE,
172 + 'callback' => array( $this, 'delete_queue' ),
173 + 'permission_callback' => array( $this, 'manage_permissions_check' ),
174 + ),
175 + )
176 + );
177 +
178 + register_rest_route(
179 + $this->namespace,
176 180 '/' . $this->rest_base . '/test',
177 181 array(
178 182 'methods' => WP_REST_Server::CREATABLE,
179 183 'callback' => array( $this, 'test_print' ),
@@ -482,8 +486,29 @@
482 486 array( 'status' => 404 )
483 487 );
484 488 }
485 489
490 + // Without force this route cancels: it takes a waiting job out of the
491 + // running but leaves the row as history. With force the row goes for
492 + // good, which is the only way to clear a terminal job before its
493 + // retention window expires.
494 + if ( rest_sanitize_boolean( $request->get_param( 'force' ) ) ) {
495 + if ( ! $this->jobs->delete( $id ) ) {
496 + return new WP_Error(
497 + 'wcpos_print_job_not_deleted',
498 + __( 'The print job could not be deleted.', 'woocommerce-pos' ),
499 + array( 'status' => 500 )
500 + );
501 + }
502 +
503 + return rest_ensure_response(
504 + array(
505 + 'deleted' => true,
506 + 'previous' => $job,
507 + )
508 + );
509 + }
510 +
486 511 if ( ! $this->jobs->cancel_if_waiting( $id ) ) {
487 512 return new WP_Error(
488 513 'wcpos_print_job_not_cancellable',
489 514 __( 'Only pending or claimed print jobs can be cancelled.', 'woocommerce-pos' ),
@@ -538,8 +563,11 @@
538 563 $filters,
539 564 array(
540 565 'limit' => $per_page,
541 566 'page' => $page,
567 + // Newest first: the job an admin opens the queue to check
568 + // on is the one that just fired, not the oldest survivor.
569 + 'order' => 'DESC',
542 570 )
543 571 )
544 572 )
545 573 );
@@ -642,8 +670,49 @@
642 670 return rest_ensure_response( array( 'cancelled' => $cancelled ) );
643 671 }
644 672
645 673 /**
674 + * Permanently delete queue rows.
675 + *
676 + * Unlike cancel_queue(), this removes history: any status may be deleted,
677 + * because the point is clearing a queue the admin no longer wants to look
678 + * at rather than stopping work. Waiting jobs are cancelled on the way out
679 + * so nothing is left half-claimed.
680 + *
681 + * @param WP_REST_Request $request Request.
682 + *
683 + * @return \WP_REST_Response|WP_Error
684 + */
685 + public function delete_queue( $request ) {
686 + $ids = $request->get_param( 'ids' );
687 + if ( ! \is_array( $ids ) || empty( $ids ) ) {
688 + return new WP_Error(
689 + 'wcpos_print_job_no_ids',
690 + __( 'No print jobs were selected.', 'woocommerce-pos' ),
691 + array( 'status' => 400 )
692 + );
693 + }
694 + foreach ( $ids as $id ) {
695 + if ( ( ! \is_int( $id ) && ! \is_string( $id ) ) || ! ctype_digit( (string) $id ) || (int) $id < 1 ) {
696 + return new WP_Error(
697 + 'wcpos_print_job_invalid_ids',
698 + __( 'One or more selected print jobs are invalid.', 'woocommerce-pos' ),
699 + array( 'status' => 400 )
700 + );
701 + }
702 + }
703 +
704 + $deleted = 0;
705 + foreach ( array_map( 'intval', $ids ) as $id ) {
706 + if ( $id > 0 && $this->jobs->delete( $id ) ) {
707 + ++$deleted;
708 + }
709 + }
710 +
711 + return rest_ensure_response( array( 'deleted' => $deleted ) );
712 + }
713 +
714 + /**
646 715 * Reprint a print job by copying it to a new pending job.
647 716 *
648 717 * @param WP_REST_Request $request Request.
649 718 *
@@ -695,16 +764,15 @@
695 764 );
696 765 }
697 766 $printer = $this->registry->get_printer( (string) $source['printer_id'] );
698 767 if ( null !== $printer ) {
699 - // Refresh both halves of the pairing together. A legacy job can
700 - // 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,
701 770 // but content_type and pn_kind must keep agreeing: reprinting a
702 - // raw (escpos) PrintNode job through the printer-only resolver
771 + // raw (escpos) PrintNode job through printer_content_type()
703 772 // relabels it application/pdf in the queue view, even though
704 773 // submit still sends raw bytes off the stored pn_kind.
705 - $resolver = new Print_Format_Resolver();
706 - $fmt = null === $template ? array( 'kind' => '' ) : $resolver->resolve( $printer, $template );
774 + $fmt = null === $template ? array( 'kind' => '' ) : Provider::format( $printer, $template );
707 775 if ( '' === (string) $fmt['kind'] ) {
708 776 // No loadable template, or one this printer can no longer
709 777 // render. Refresh from the provider's declared type only
710 778 // when no stored kind can contradict it; otherwise the
@@ -709,9 +777,9 @@
709 777 // render. Refresh from the provider's declared type only
710 778 // when no stored kind can contradict it; otherwise the
711 779 // source pairing is the best answer left.
712 780 if ( '' === $pn_kind ) {
713 - $content_type = $resolver->content_type_for_printer( $printer );
781 + $content_type = Provider::printer_content_type( $printer );
714 782 }
715 783 } else {
716 784 $content_type = $fmt['content_type'];
717 785 $pn_kind = Provider::stores_job_kind( Provider::normalize( (string) ( $printer['provider'] ?? '' ) ) )
@@ -776,206 +844,12 @@
776 844 *
777 845 * @return \WP_REST_Response|WP_Error
778 846 */
779 847 public function cloudprnt( $request ) {
780 - $printer_id = sanitize_text_field( (string) $request->get_param( 'printer_id' ) );
781 - $this->registry->record_seen( $printer_id );
782 - $this->jobs->release_stale_claims( $printer_id );
783 -
784 - if ( 'POST' === $request->get_method() ) {
785 - return $this->cloudprnt_poll( $request, $printer_id );
786 - }
787 -
788 - $job = $this->get_cloud_job_for_request( $request, $printer_id );
789 - if ( is_wp_error( $job ) ) {
790 - return $job;
791 - }
792 -
793 - if ( 'DELETE' === $request->get_method() ) {
794 - $code = sanitize_text_field( (string) $request->get_param( 'code' ) );
795 - $status = '' === $code || '000' === $code || 1 === preg_match( '/^2\d{2,3}(?:\s|$)/', $code ) ? Print_Job_Service::STATUS_PRINTED : Print_Job_Service::STATUS_FAILED;
796 - $this->jobs->set_status( (int) $job['id'], $status );
797 -
798 - if ( Print_Job_Service::STATUS_FAILED === $status ) {
799 - $this->log_printer_failure( $request, $printer_id, $code, (int) $job['id'] );
800 - }
801 -
802 - return rest_ensure_response( array( 'ok' => true ) );
803 - }
804 -
805 - return $this->cloudprnt_fetch( $request, $printer_id, $job );
848 + return $this->handle_provider_poll( $request, 'star-cloudprnt' );
806 849 }
807 850
808 851 /**
809 - * Answer a CloudPRNT poll: offer a job, and ask what the printer can decode.
810 - *
811 - * The poll body is the printer's half of the conversation. `printingInProgress`
812 - * says a job is still on the paper, and the spec is explicit that the server
813 - * must not offer another one until it clears — doing so risks the printer
814 - * dropping the second job. `clientAction` carries the printer's answers to
815 - * questions asked in an earlier poll response, which is the only way the
816 - * protocol exposes what formats the hardware can decode.
817 - *
818 - * @param WP_REST_Request $request Request.
819 - * @param string $printer_id Printer ID.
820 - *
821 - * @return \WP_REST_Response
822 - */
823 - private function cloudprnt_poll( WP_REST_Request $request, string $printer_id ) {
824 - $poll = Cloud_Print_Poll_Request::from_body( (string) $request->get_body(), $request->get_json_params() );
825 - $this->registry->record_capabilities( $printer_id, $poll->answers(), $poll->status_code() );
826 -
827 - $response = array( 'jobReady' => false );
828 -
829 - if ( ! $poll->printing_in_progress() && ! $this->jobs->find_active_claim( $printer_id ) ) {
830 - $job = $this->jobs->next_pending( $printer_id );
831 - if ( null !== $job ) {
832 - // The offer is a list: the printer picks its preferred decodable
833 - // entry and names it in the fetch's `?type`. Nothing decodable in
834 - // the list means no GET at all, just a 510 confirmation — so the
835 - // list is filtered by what this printer said it can decode.
836 - // `mediaType` (singular) is kept for older firmware.
837 - $media_types = $this->media_types_for_job( $job, $printer_id );
838 - $response = array(
839 - 'jobReady' => true,
840 - 'jobToken' => (string) $job['id'],
841 - 'mediaType' => $media_types[0],
842 - 'mediaTypes' => array_values( $media_types ),
843 - );
844 - }
845 - }
846 -
847 - if ( $this->registry->should_request_capabilities( $printer_id ) ) {
848 - $response['clientAction'] = array(
849 - array( 'request' => 'ClientType' ),
850 - array( 'request' => 'Encodings' ),
851 - );
852 - $this->registry->record_capability_request( $printer_id );
853 - }
854 -
855 - return rest_ensure_response( $response );
856 - }
857 -
858 - /**
859 - * Serve a CloudPRNT job in the media type the printer asked for.
860 - *
861 - * @param WP_REST_Request $request Request.
862 - * @param string $printer_id Printer ID.
863 - * @param array $job Job array.
864 - *
865 - * @return \WP_REST_Response|WP_Error
866 - */
867 - private function cloudprnt_fetch( WP_REST_Request $request, string $printer_id, array $job ) {
868 - // The fetch GET names the printer's chosen media type. A type the server
869 - // cannot produce is answered with 415 (per the CloudPRNT spec) and the job
870 - // is left unclaimed. What is servable is deliberately wider than what the
871 - // poll advertised: the printer naming a type is a stronger signal than our
872 - // cached capability answer, so a capability update landing between the two
873 - // requests must not reject a format we had just offered. Firmware that
874 - // omits the parameter gets our best offer for this printer instead. The
875 - // logged value is length-capped: printers poll every few seconds, so a
876 - // wedged loop must not flood the log with unbounded input.
877 - $servable = ( new Cloud_Print_Media_Types() )->servable_for_job( $job, $this->registry->get_printer( $printer_id ) );
878 - $requested = sanitize_text_field( (string) $request->get_param( 'type' ) );
879 - $chosen = '' === $requested
880 - ? $this->media_types_for_job( $job, $printer_id )[0]
881 - : Cloud_Print_Media_Types::match( $requested, $servable );
882 -
883 - if ( '' === $chosen ) {
884 - Logger::warning(
885 - sprintf(
886 - '%s: printer "%s" requested media type "%s" for print job %d, which the server can only serve as %s.',
887 - $request->get_route(),
888 - $printer_id,
889 - substr( $requested, 0, 100 ),
890 - (int) $job['id'],
891 - implode( ', ', $servable )
892 - )
893 - );
894 -
895 - return new WP_Error(
896 - 'wcpos_print_job_incompatible_media_type',
897 - __( 'The print job is not available in the requested media type.', 'woocommerce-pos' ),
898 - array( 'status' => 415 )
899 - );
900 - }
901 -
902 - if ( ! $this->jobs->try_claim( (int) $job['id'] ) ) {
903 - return rest_ensure_response( array( 'jobReady' => false ) );
904 - }
905 -
906 - $render = $this->jobs->render_job( $job, $chosen );
907 - if ( '' === $render['body'] ) {
908 - Logger::error(
909 - sprintf(
910 - '%s: print job %d rendered an empty payload for printer "%s".',
911 - $request->get_route(),
912 - (int) $job['id'],
913 - $printer_id
914 - )
915 - );
916 - }
917 -
918 - return $this->serve_raw( $render['body'], $chosen, self::control_headers( $chosen, $render ) );
919 - }
920 -
921 - /**
922 - * The media types a CloudPRNT job can be served in, best first.
923 - *
924 - * @param array $job Job array.
925 - * @param string $printer_id Printer ID.
926 - *
927 - * @return array<int, string>
928 - */
929 - private function media_types_for_job( array $job, string $printer_id ): array {
930 - $capabilities = $this->registry->get_capabilities( $printer_id );
931 -
932 - return ( new Cloud_Print_Media_Types() )->for_job(
933 - $job,
934 - $this->registry->get_printer( $printer_id ),
935 - $capabilities['encodings']
936 - );
937 - }
938 -
939 - /**
940 - * Peripheral-control headers for a job served in a command-free format.
941 - *
942 - * `text/plain` and images carry no cut or drawer commands, so CloudPRNT reads
943 - * them off the fetch response instead. Command formats express both in-band
944 - * and must not also be told to cut, or the receipt cuts twice.
945 - *
946 - * Both headers are always sent, `none` included. Omitting them leaves the
947 - * decision to the printer's own defaults, which cut plain-text jobs — so a
948 - * template that deliberately does not cut would cut anyway, and would behave
949 - * differently in text than in StarPRNT. Saying `none` out loud keeps the two
950 - * formats rendering the same receipt.
951 - *
952 - * @param string $media_type The media type being served.
953 - * @param array $render Render result from Print_Job_Service::render_job().
954 - *
955 - * @return array<string, string>
956 - */
957 - private static function control_headers( string $media_type, array $render ): array {
958 - if ( ! Cloud_Print_Media_Types::is_header_controlled( $media_type ) ) {
959 - return array();
960 - }
961 -
962 - $headers = array(
963 - 'X-Star-Cut' => null === $render['cut'] ? 'none' : (string) $render['cut'],
964 - 'X-Star-CashDrawer' => null === $render['drawer'] ? 'none' : (string) $render['drawer'],
965 - );
966 -
967 - // The raster is already two-colour, so the printer's Floyd-Steinberg
968 - // default would dither an image that has nothing left to dither —
969 - // softening crisp black-on-white text into stipple.
970 - if ( Cloud_Print_Media_Types::PNG === Cloud_Print_Media_Types::normalize( $media_type ) ) {
971 - $headers['X-Star-ImageDitherPattern'] = 'none';
972 - }
973 -
974 - return $headers;
975 - }
976 -
977 - /**
978 852 * Permission check for printer-token routes.
979 853 *
980 854 * @param WP_REST_Request $request Request.
981 855 *
@@ -1003,57 +877,40 @@
1003 877
1004 878 return true;
1005 879 }
1006 880
1007 - /**
1008 - * Resolve and authorize a CloudPRNT job token.
1009 - *
1010 - * @param WP_REST_Request $request Request.
1011 - * @param string $printer_id Printer ID.
1012 - *
1013 - * @return array|WP_Error
1014 - */
1015 - private function get_cloud_job_for_request( WP_REST_Request $request, string $printer_id ) {
1016 - $job_id = (int) $request->get_param( 'token' );
1017 - $job = $this->jobs->get( $job_id );
1018 - if ( null === $job || $printer_id !== $job['printer_id'] ) {
1019 - Logger::warning(
1020 - sprintf(
1021 - '%s: print job "%d" was not found for printer "%s".',
1022 - $request->get_route(),
1023 - $job_id,
1024 - $printer_id
1025 - )
1026 - );
1027 881
1028 - return new WP_Error(
1029 - 'wcpos_print_job_not_found',
1030 - __( 'Print job not found.', 'woocommerce-pos' ),
1031 - array( 'status' => 404 )
1032 - );
1033 - }
1034 882
1035 - return $job;
1036 - }
1037 883
1038 884 /**
1039 - * Log a printer-reported failure without request credentials or payloads.
885 + * Parse the vendor request and serve the lifecycle's plain-data response.
1040 886 *
1041 887 * @param WP_REST_Request $request Request.
1042 - * @param string $printer_id Printer ID.
1043 - * @param string $code Failure code.
1044 - * @param int $job_id Print job ID.
888 + * @param string $provider_key Protocol selected by the route.
889 + * @return WP_REST_Response|WP_Error
1045 890 */
1046 - private function log_printer_failure( WP_REST_Request $request, string $printer_id, string $code, int $job_id ): void {
1047 - Logger::error(
1048 - sprintf(
1049 - '%s: printer "%s" reported failure code "%s" for print job %d.',
1050 - $request->get_route(),
1051 - $printer_id,
1052 - $code,
1053 - $job_id
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 ) );
896 + }
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(),
1054 904 )
1055 905 );
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;
911 + }
912 + return new WP_REST_Response( $result['body'], $result['status'], $result['headers'] );
1056 913 }
1057 914
1058 915 /**
1059 916 * Serve raw bytes from a REST callback.
@@ -1076,93 +933,9 @@
1076 933 *
1077 934 * @return \WP_REST_Response
1078 935 */
1079 936 public function epson_sdp( $request ) {
1080 - $printer_id = sanitize_text_field( (string) $request->get_param( 'printer_id' ) );
1081 - $this->registry->record_seen( $printer_id );
1082 - $raw_body = (string) $request->get_body();
1083 - $soap = 'text/xml; charset=utf-8';
1084 - $ack = '<response success="true" code="" status=""/>';
1085 -
1086 - $this->jobs->release_stale_claims( $printer_id );
1087 -
1088 - // Server Direct Print multiplexes three different request types onto the
1089 - // one configured URL, as URL-encoded form data distinguished by
1090 - // ConnectionType (User's Manual Rev.K, ch.3 and the Test_print.php
1091 - // reference implementation):
1092 - //
1093 - // GetRequest — poll for a job
1094 - // SetResponse — printing result; the XML rides in the ResponseFile field
1095 - // SetStatus — status notification; the XML rides in the Status field
1096 - //
1097 - // Answering a status notification with print data hands the job to a
1098 - // request that discards it, so the printer never prints and the job stays
1099 - // claimed. Dispatching on ConnectionType is what keeps the job on the
1100 - // GetRequest that is actually asking for one.
1101 - $connection_type = (string) $request->get_param( 'ConnectionType' );
1102 -
1103 - // Result XML is a form field, so in the raw body it is percent-encoded
1104 - // (`success="true"` arrives as `success%3D%22true%22`) and a raw-body
1105 - // substring test can never match it. The raw-body branch is kept only for
1106 - // a caller that posts the bare XML, which the printer never does.
1107 - $result_xml = (string) $request->get_param( 'ResponseFile' );
1108 - if ( '' === $result_xml && false !== strpos( $raw_body, 'success=' ) ) {
1109 - $result_xml = $raw_body;
1110 - }
1111 -
1112 - if ( 'SetResponse' === $connection_type || '' !== $result_xml ) {
1113 - $claim = $this->jobs->find_active_claim( $printer_id );
1114 - if ( null !== $claim ) {
1115 - $ok = false !== strpos( $result_xml, 'success="true"' );
1116 - $this->jobs->set_status( (int) $claim['id'], $ok ? Print_Job_Service::STATUS_PRINTED : Print_Job_Service::STATUS_FAILED );
1117 -
1118 - if ( ! $ok ) {
1119 - $code = 'unknown';
1120 - if ( 1 === preg_match( '/\bcode="([^"]*)"/', $result_xml, $matches ) ) {
1121 - $code = sanitize_text_field( $matches[1] );
1122 - }
1123 -
1124 - $this->log_printer_failure( $request, $printer_id, $code, (int) $claim['id'] );
1125 - }
1126 - }
1127 -
1128 - return $this->serve_raw( $ack, $soap );
1129 - }
1130 -
1131 - // A status notification is not asking for work.
1132 - if ( 'SetStatus' === $connection_type ) {
1133 - return $this->serve_raw( $ack, $soap );
1134 - }
1135 -
1136 - if ( null !== $this->jobs->find_active_claim( $printer_id ) ) {
1137 - return $this->serve_raw( $ack, $soap );
1138 - }
1139 -
1140 - $job = $this->jobs->next_pending( $printer_id );
1141 - if ( null === $job ) {
1142 - return $this->serve_raw( $ack, $soap );
1143 - }
1144 -
1145 - if ( ! $this->jobs->try_claim( (int) $job['id'] ) ) {
1146 - return $this->serve_raw( $ack, $soap );
1147 - }
1148 - $epos = $this->jobs->render_payload( $job );
1149 -
1150 - // Server Direct Print expects the print data wrapped in
1151 - // PrintRequestInfo > ePOSPrint > PrintData — NOT the SOAP envelope used
1152 - // by the direct ePOS-Print web service, which is a different protocol.
1153 - // A printer that receives an unrecognised wrapper discards it silently:
1154 - // it neither prints nor posts a result, so the job sits claimed forever.
1155 - // Version 1.00 is the only version every SDP printer family supports
1156 - // (Server Direct Print User's Manual Rev.K, "Response (Print request)");
1157 - // 2.00+ adds printjobid but is limited to TM-i/TM-DT/TM-T88VI.
1158 - $envelope = '<?xml version="1.0" encoding="utf-8"?>';
1159 - $envelope .= '<PrintRequestInfo Version="1.00"><ePOSPrint>';
1160 - $envelope .= '<Parameter><devid>local_printer</devid><timeout>' . self::EPSON_SDP_PRINT_TIMEOUT_MS . '</timeout></Parameter>';
1161 - $envelope .= '<PrintData>' . $epos . '</PrintData>';
1162 - $envelope .= '</ePOSPrint></PrintRequestInfo>';
1163 -
1164 - return $this->serve_raw( $envelope, $soap );
937 + return $this->handle_provider_poll( $request, 'epson-sdp' );
1165 938 }
1166 939
1167 940 /**
1168 941 * Enqueue a print job (raw payload or order-based).
@@ -1323,26 +1096,14 @@
1323 1096 // Legacy printer rows saved before the provider field existed must test
1324 1097 // as the default provider, not fall through to the no-diagnostic error.
1325 1098 $provider = Provider::normalize( \is_string( $printer['provider'] ?? null ) ? $printer['provider'] : null );
1326 1099
1327 - if ( 'printnode' === $provider ) {
1328 - 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 );
1329 1103 }
1104 + $diag = $adapter->diagnostic( (string) $printer['name'] );
1330 1105
1331 - if ( 'star-online' === $provider ) {
1332 - return $this->test_print_star_online( $printer_id, $printer );
1333 - }
1334 -
1335 - try {
1336 - $diag = ( new Cloud_Print_Diagnostic() )->build( $provider, (string) $printer['name'] );
1337 - } catch ( \RuntimeException $e ) {
1338 - return new WP_Error(
1339 - 'wcpos_print_job_no_diagnostic',
1340 - __( 'Test print is not available for this printer yet.', 'woocommerce-pos' ),
1341 - array( 'status' => 400 )
1342 - );
1343 - }
1344 -
1345 1106 $id = $this->jobs->create(
1346 1107 array(
1347 1108 'printer_id' => $printer_id,
1348 1109 'content_type' => $diag['content_type'],
@@ -1362,100 +1123,9 @@
1362 1123
1363 1124 return $response;
1364 1125 }
1365 1126
1366 - /**
1367 - * Queue a Star Markup test receipt and submit it through the push pipeline.
1368 - *
1369 - * @param string $printer_id Registered printer id.
1370 - * @param array $printer Registered star-online printer.
1371 - *
1372 - * @return \WP_REST_Response|WP_Error
1373 - */
1374 - private function test_print_star_online( string $printer_id, array $printer ) {
1375 - $markup = ( new Cloud_Print_Diagnostic() )->star_markup( (string) $printer['name'] );
1376 1127
1377 - $id = $this->jobs->create(
1378 - array(
1379 - 'printer_id' => $printer_id,
1380 - 'content_type' => 'text/vnd.star.markup',
1381 - 'payload' => base64_encode( $markup ),
1382 - )
1383 - );
1384 - if ( $id <= 0 ) {
1385 - return new WP_Error(
1386 - 'wcpos_print_job_create_failed',
1387 - __( 'Print job could not be created.', 'woocommerce-pos' ),
1388 - array( 'status' => 500 )
1389 - );
1390 - }
1391 -
1392 - wp_schedule_single_event( time(), Cloud_Print_Trigger_Service::CRON_SUBMIT, array( $id ) );
1393 - ( new \WCPOS\WooCommercePOS\Services\Cloud_Print_Submit_Service() )->submit( $id );
1394 -
1395 - $response = rest_ensure_response( $this->jobs->get( $id ) );
1396 - $response->set_status( 201 );
1397 -
1398 - return $response;
1399 - }
1400 -
1401 - /**
1402 - * Submit a diagnostic PDF to a PrintNode printer.
1403 - *
1404 - * @param array $printer Registered PrintNode printer.
1405 - *
1406 - * @return \WP_REST_Response|WP_Error
1407 - */
1408 - private function test_print_printnode( array $printer ) {
1409 - $api_key = (string) ( $printer['printnode_api_key'] ?? '' );
1410 - $pn_printer_id = (int) ( $printer['printnode_printer_id'] ?? 0 );
1411 - if ( '' === $api_key || 0 === $pn_printer_id ) {
1412 - return new WP_Error(
1413 - 'wcpos_print_job_printnode_unconfigured',
1414 - __( 'This PrintNode printer is missing its API key or printer id.', 'woocommerce-pos' ),
1415 - array( 'status' => 400 )
1416 - );
1417 - }
1418 -
1419 - try {
1420 - $pdf = ( new Cloud_Print_Diagnostic() )->build_pdf( (string) $printer['name'] );
1421 - } catch ( \Throwable $e ) {
1422 - // Defense in depth: a Dompdf/font-cache/temp-dir failure must not
1423 - // surface as an uncaught 500. Mirror the render_payload() guard.
1424 - Logger::log( 'Cloud print: PrintNode diagnostic PDF render failed: ' . $e->getMessage() );
1425 -
1426 - return new WP_Error(
1427 - 'wcpos_print_job_diagnostic_failed',
1428 - __( 'Could not generate the test print.', 'woocommerce-pos' ),
1429 - array( 'status' => 500 )
1430 - );
1431 - }
1432 -
1433 - $result = ( new PrintNode_Client( $api_key ) )->submit_job(
1434 - $pn_printer_id,
1435 - 'WCPOS Test Print',
1436 - 'pdf_base64',
1437 - base64_encode( $pdf )
1438 - );
1439 -
1440 - if ( is_wp_error( $result ) ) {
1441 - return new WP_Error(
1442 - 'wcpos_print_job_printnode_failed',
1443 - $result->get_error_message(),
1444 - array( 'status' => 502 )
1445 - );
1446 - }
1447 -
1448 - return new WP_REST_Response(
1449 - array(
1450 - 'submitted' => true,
1451 - 'external_provider' => 'printnode',
1452 - 'external_job_id' => (string) $result['id'],
1453 - 'external_state' => 'submitted',
1454 - ),
1455 - 201
1456 - );
1457 - }
1458 1128
1459 1129 /**
1460 1130 * Extract sanitized cash-drawer options from a REST request.
1461 1131 *