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