* * @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 '
'; $renderer->render( $custom_template, $order, $receipt_data ); echo '
'; } /** * Get template engine type from metadata. * * @param array $template Template metadata. * * @return string */ private function get_template_engine( array $template ): string { $engine = isset( $template['engine'] ) ? sanitize_text_field( $template['engine'] ) : 'legacy-php'; return in_array( $engine, array( 'logicless', 'thermal', 'legacy-php' ), true ) ? $engine : 'legacy-php'; } /** * Shutdown handler to catch fatal errors during template rendering. * * @return void */ public static function handle_shutdown(): void { if ( ! self::$rendering ) { return; } $error = error_get_last(); // Check if there was a fatal error. if ( $error && \in_array( $error['type'], array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR ), true ) ) { // Clean any partial output. if ( ob_get_level() ) { ob_end_clean(); } // Display a user-friendly error page. self::display_error_page( $error ); } } /** * Display a user-friendly error page. * * @param array $error Error details from error_get_last(). * * @return void */ private static function display_error_page( array $error ): void { $error_type = self::get_error_type_name( $error['type'] ); // Only show detailed error info to administrators. $show_details = current_user_can( 'manage_options' ) || ( \defined( 'WP_DEBUG' ) && WP_DEBUG ); ?> > <?php /* translators: Short WCPOS UI label; keep concise. */ esc_html_e( 'Receipt Error', 'woocommerce-pos' ); ?>

:



'Fatal Error', E_PARSE => 'Parse Error', E_CORE_ERROR => 'Core Error', E_COMPILE_ERROR => 'Compile Error', ); return $types[ $type ] ?? 'Error'; } /** * Get the template path. * * @param string $file_name The template file name. * * @return null|mixed */ private function get_template_path( string $file_name ) { /* * Filters the path to the receipt template file. * * @param {string} $path Full server path to the template file. * * @returns {string} $path Full server path to the template file. * * @since 1.0.0 * * @hook woocommerce_pos_print_receipt_path */ return apply_filters( 'woocommerce_pos_print_receipt_path', woocommerce_pos_locate_template( $file_name ) ); } /** * Get receipt data payload for the selected mode. * * @param \WC_Abstract_Order $order Order object. * @param string $mode Receipt mode. * * @return array */ private function get_receipt_data( \WC_Abstract_Order $order, string $mode ): array { $mode = 'fiscal' === $mode ? 'live' : $mode; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $store_id = 'preview' === $mode && isset( $_GET['store_id'] ) ? (int) $_GET['store_id'] : 0; $pos_store = $store_id > 0 ? wcpos_get_store( $store_id ) : null; if ( $store_id > 0 && ! \is_object( $pos_store ) ) { $pos_store = null; } return ( new Receipt_Data_Builder() )->build( $order, $mode, $pos_store ); } /** * Get the active custom receipt template. * * @return null|array Custom template data or null if not found. */ private function get_custom_template(): ?array { /** * Filters the active receipt template. * * @param null|array $template Active template data or null. * * @returns array|null Active template data or null. * * @since 1.8.0 * * @hook woocommerce_pos_active_receipt_template */ $template = apply_filters( 'woocommerce_pos_active_receipt_template', null ); if ( $template ) { return $template; } // Check for preview template parameter (used in admin preview). // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['wcpos_preview_template'] ) && current_user_can( 'manage_woocommerce_pos' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $preview_id = sanitize_text_field( wp_unslash( $_GET['wcpos_preview_template'] ) ); if ( is_numeric( $preview_id ) ) { // Database template. return TemplatesManager::get_template( (int) $preview_id ); } // Virtual template (theme/plugin-pro/plugin-core). $template = TemplatesManager::get_virtual_template( $preview_id, 'receipt' ); if ( $template ) { return $template; } // Gallery template (e.g. "standard-receipt"). return TemplatesManager::get_gallery_template_by_key( $preview_id ); } // Check for template selection parameter (used by POS app to switch templates). // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['template'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $template_id = sanitize_text_field( wp_unslash( $_GET['template'] ) ); if ( is_numeric( $template_id ) ) { $post_id = (int) $template_id; $template = 'publish' === get_post_status( $post_id ) ? TemplatesManager::get_template( $post_id ) : null; } else { $template = TemplatesManager::get_virtual_template( $template_id, 'receipt' ); if ( ! $template ) { $template = TemplatesManager::get_gallery_template_by_key( $template_id ); } } // Only allow published receipt templates. if ( $template && 'receipt' === ( $template['type'] ?? '' ) ) { return $template; } } // Get active receipt template (can be virtual or from database). return TemplatesManager::get_active_template( 'receipt' ); } }