| @@ -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 | * |