* * @see http://wcpos.com * @package WCPOS\WooCommercePOS */ namespace WCPOS\WooCommercePOS\Templates; use Exception; use WCPOS\WooCommercePOS\Logger; use WCPOS\WooCommercePOS\Services\Receipt_Data_Builder; use WCPOS\WooCommercePOS\Services\Receipt_Renderer_Factory; use WCPOS\WooCommercePOS\Services\Template_Pdf_Service; use WCPOS\WooCommercePOS\Templates as TemplatesManager; /** * Receipt class. */ class Receipt { /** * The order ID. * * @var int */ private $order_id; /** * Flag to track if we're rendering a template. * * @var bool */ private static $rendering = false; /** * Constructor. * * @param int $order_id The order ID. */ public function __construct( int $order_id ) { $this->order_id = $order_id; add_filter( 'show_admin_bar', '__return_false' ); add_action( 'woocommerce_pos_receipt_head', array( $this, 'receipt_head' ) ); } /** * Adds a script to the head of the WordPress template when the * 'woocommerce_pos_receipt_head' action is triggered. The script listens for * a 'message' event with a specific action ('wcpos-print-receipt') and, upon * receiving such an event, triggers the browser's print functionality. * * Usage: Call `do_action( 'woocommerce_pos_receipt_head' );` at the desired * location in your template file to include the script. */ public function receipt_head(): void { ?> order_id ); // Validate order key for security. Missing orders share the permission // message so unauthenticated requests cannot enumerate order IDs. $order_key = isset( $_GET['key'] ) ? sanitize_text_field( wp_unslash( $_GET['key'] ) ) : ''; if ( ! $order || empty( $order_key ) || ! hash_equals( $order->get_order_key(), $order_key ) ) { wp_die( esc_html__( 'You do not have permission to view this receipt.', 'woocommerce-pos' ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $format = isset( $_GET['format'] ) ? sanitize_text_field( wp_unslash( $_GET['format'] ) ) : ''; if ( 'pdf' === $format ) { $this->render_pdf( $order ); } /* * Fires before rendering the receipt template. * * @param int $order_id Order ID. * @param WC_Abstract_Order $order Order object. * * @since 1.8.0 * * @hook woocommerce_pos_before_template_render */ do_action( 'woocommerce_pos_before_template_render', $this->order_id, $order ); /** * Check for custom template first. */ $custom_template = $this->get_custom_template(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $is_preview = isset( $_GET['wcpos_preview_template'] ) && current_user_can( 'manage_woocommerce_pos' ); $receipt_data = $this->get_receipt_data( $order, $is_preview ? 'preview' : 'live' ); // Start output buffering and register shutdown handler for fatal errors. self::$rendering = true; register_shutdown_function( array( __CLASS__, 'handle_shutdown' ) ); ob_start(); if ( $custom_template ) { $this->render_custom_template( $custom_template, $order, $receipt_data ); } else { /** * Put WC_Order into the global scope so that the template can access it. */ $path = $this->get_template_path( 'receipt.php' ); include $path; } // If we got here, template rendered successfully. self::$rendering = false; ob_end_flush(); /* * Fires after rendering the receipt template. * * @param int $order_id Order ID. * @param WC_Abstract_Order $order Order object. * * @since 1.8.0 * * @hook woocommerce_pos_after_template_render */ do_action( 'woocommerce_pos_after_template_render', $this->order_id, $order ); exit; } catch ( Exception $e ) { self::$rendering = false; if ( ob_get_level() ) { ob_end_clean(); } wc_print_notice( $e->getMessage(), 'error' ); } } /** * Render and serve a custom receipt template as a PDF download. * * @param \WC_Abstract_Order $order Order object. * * @return void */ private function render_pdf( \WC_Abstract_Order $order ): void { /* * Filters the receipt template used for storefront PDF downloads. * * Receives the same template array resolved for the HTML receipt surface * (including the woocommerce_pos_active_receipt_template filter and the * ?template= query param), so both surfaces stay in sync by default. * * @param null|array $template Resolved template data or null. * @param WC_Abstract_Order $order Order object. * * @returns null|array Template data or null. * * @since 1.9.11 * * @hook woocommerce_pos_storefront_receipt_template */ $template = apply_filters( 'woocommerce_pos_storefront_receipt_template', $this->get_custom_template(), $order ); if ( ! \is_array( $template ) || empty( $template ) ) { wp_die( esc_html__( 'No receipt template is configured.', 'woocommerce-pos' ), '', array( 'response' => 404 ) ); } try { $pdf = ( new Template_Pdf_Service() )->render( $template, $order ); } catch ( \Throwable $e ) { Logger::log( sprintf( 'Storefront receipt PDF render failed for order %d: %s', $order->get_id(), $e->getMessage() ) ); wp_die( esc_html__( 'Could not generate the receipt PDF.', 'woocommerce-pos' ), '', array( 'response' => 500 ) ); } if ( '' === $pdf ) { Logger::log( sprintf( 'Storefront receipt PDF render failed for order %d: renderer returned no data.', $order->get_id() ) ); wp_die( esc_html__( 'Could not generate the receipt PDF.', 'woocommerce-pos' ), '', array( 'response' => 500 ) ); } // Discard any open output buffers (e.g. zlib compression) so the // Content-Length header matches the bytes actually sent. while ( ob_get_level() ) { if ( ! ob_end_clean() ) { wp_die( esc_html__( 'Could not generate the receipt PDF.', 'woocommerce-pos' ), '', array( 'response' => 500 ) ); } } $order_number = sanitize_file_name( (string) $order->get_order_number() ); header( 'Content-Type: application/pdf' ); header( 'Content-Disposition: attachment; filename="receipt-' . $order_number . '.pdf"' ); // A remaining (non-removable) output buffer may transform the body on // flush, e.g. ob_gzhandler, making the raw PDF byte count wrong. Only // declare Content-Length when the output stream is unbuffered; browsers // fall back to reading until the response ends. if ( 0 === ob_get_level() ) { header( 'Content-Length: ' . \strlen( $pdf ) ); } header( 'Cache-Control: no-store' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $pdf; exit; } /** * Render a custom template for the browser print surface. * * This route serves the HTML the POS prints via window.print(); PDFs render * through Template_Pdf_Service and never pass here. Logicless output is * wrapped with a print-only grayscale filter so physical printouts stay * black-and-white while on-screen and PDF output keep their colour. Browsers * that ignore the filter still print acceptably because template colours are * print-safe (dark accent ink, near-white fills). Legacy PHP templates emit a * full HTML document and own their print styling, so they render untouched. * * @param array $custom_template Template metadata/content. * @param \WC_Abstract_Order|null $order Order object. * @param array $receipt_data Canonical receipt payload. */ private function render_custom_template( array $custom_template, ?\WC_Abstract_Order $order, array $receipt_data ): void { $template_engine = $this->get_template_engine( $custom_template ); $renderer = ( new Receipt_Renderer_Factory() )->create( $template_engine ); if ( 'logicless' !== $template_engine ) { $renderer->render( $custom_template, $order, $receipt_data ); return; } echo ''; echo '