| 1 |
<?php |
| 2 |
/** |
| 3 |
* Thermal HTML receipt renderer. |
| 4 |
* |
| 5 |
* Renders a thermal (receipt-printer) template to HTML for the browser print |
| 6 |
* surface. Thermal template content is stored raw — it is exempt from wp_kses |
| 7 |
* because it is XML markup for printers, not HTML — so it must NEVER be executed |
| 8 |
* as PHP. This renderer runs the content through the thermal pipeline |
| 9 |
* (Mustache render -> XML AST -> HTML), which escapes receipt data and discards |
| 10 |
* anything that is not recognised thermal markup (PHP processing instructions |
| 11 |
* included). The include-based Legacy_Php_Renderer must never receive thermal |
| 12 |
* content. |
| 13 |
* |
| 14 |
* @package WCPOS\WooCommercePOS\Templates\Renderers |
| 15 |
*/ |
| 16 |
|
| 17 |
namespace WCPOS\WooCommercePOS\Templates\Renderers; |
| 18 |
|
| 19 |
use WCPOS\WooCommercePOS\Interfaces\Receipt_Renderer_Interface; |
| 20 |
use WCPOS\WooCommercePOS\Logger; |
| 21 |
use WCPOS\WooCommercePOS\Templates\Thermal\Html_Thermal_Emitter; |
| 22 |
use WCPOS\WooCommercePOS\Templates\Thermal\Thermal_Renderer; |
| 23 |
use WC_Abstract_Order; |
| 24 |
|
| 25 |
/** |
| 26 |
* Thermal_Html_Renderer class. |
| 27 |
*/ |
| 28 |
class Thermal_Html_Renderer implements Receipt_Renderer_Interface { |
| 29 |
/** |
| 30 |
* Render a thermal template to HTML. |
| 31 |
* |
| 32 |
* @param array $template Template metadata/content. |
| 33 |
* @param WC_Abstract_Order|null $order Order object, or null for sample-data preview. |
| 34 |
* @param array $receipt_data Canonical receipt payload (unused; the thermal pipeline rebuilds its own data). |
| 35 |
*/ |
| 36 |
public function render( array $template, ?WC_Abstract_Order $order, array $receipt_data ): void { |
| 37 |
// The thermal pipeline builds its receipt data from a concrete order. |
| 38 |
if ( ! $order instanceof WC_Abstract_Order ) { |
| 39 |
echo '<!-- Thermal receipt preview requires an order -->'; |
| 40 |
return; |
| 41 |
} |
| 42 |
|
| 43 |
$paper_width_px = $this->paper_width_px( $template ); |
| 44 |
|
| 45 |
try { |
| 46 |
$ast = ( new Thermal_Renderer() )->build_ast( $template, $order ); |
| 47 |
$html = ( new Html_Thermal_Emitter() )->emit( |
| 48 |
$ast, |
| 49 |
array( 'paper_width_px' => $paper_width_px ) |
| 50 |
); |
| 51 |
} catch ( \Throwable $e ) { |
| 52 |
// Malformed thermal markup (e.g. a raw PHP payload with no <receipt> |
| 53 |
// root) throws during parsing. Fail closed with a harmless comment, |
| 54 |
// but log the cause so a genuinely broken template is diagnosable. |
| 55 |
Logger::log( |
| 56 |
sprintf( |
| 57 |
'Thermal receipt render failed for template %s: %s', |
| 58 |
isset( $template['id'] ) ? (string) $template['id'] : 'unknown', |
| 59 |
$e->getMessage() |
| 60 |
) |
| 61 |
); |
| 62 |
echo '<!-- Thermal receipt could not be rendered -->'; |
| 63 |
return; |
| 64 |
} |
| 65 |
|
| 66 |
// The emitter leaves its output width-agnostic (the PDF path constrains it |
| 67 |
// via the physical page size). On the browser-print surface the page can be |
| 68 |
// wider than the roll, so constrain to the resolved paper width here or the |
| 69 |
// row tables would stretch across the whole page. |
| 70 |
$open = '<div style="width:' . esc_attr( $this->format_px( $paper_width_px ) ) |
| 71 |
. 'px;max-width:100%;margin:0 auto;">'; |
| 72 |
|
| 73 |
// $html is built by the thermal emitter, which escapes receipt data; $open |
| 74 |
// is a fixed wrapper with an escaped numeric width. |
| 75 |
echo $open . $html . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 76 |
} |
| 77 |
|
| 78 |
/** |
| 79 |
* Format a pixel value for inline CSS without a trailing decimal point. |
| 80 |
* |
| 81 |
* @param float $px Pixel value. |
| 82 |
* |
| 83 |
* @return string |
| 84 |
*/ |
| 85 |
private function format_px( float $px ): string { |
| 86 |
return rtrim( rtrim( number_format( $px, 2, '.', '' ), '0' ), '.' ); |
| 87 |
} |
| 88 |
|
| 89 |
/** |
| 90 |
* Resolve the template's paper width in CSS pixels (96dpi). |
| 91 |
* |
| 92 |
* Template metadata declares the physical roll ('58mm' / '80mm'); fall back |
| 93 |
* to 80mm when absent or out of range so the emitter can scale the character |
| 94 |
* grid to fit the page. |
| 95 |
* |
| 96 |
* @param array $template Template metadata/content. |
| 97 |
* |
| 98 |
* @return float Paper width in CSS px. |
| 99 |
*/ |
| 100 |
private function paper_width_px( array $template ): float { |
| 101 |
$raw = isset( $template['paper_width'] ) ? (string) $template['paper_width'] : ''; |
| 102 |
$mm = (float) $raw; // Leading-number cast: '58mm' -> 58.0. |
| 103 |
|
| 104 |
if ( $mm < 25.0 || $mm > 250.0 ) { |
| 105 |
$mm = 80.0; |
| 106 |
} |
| 107 |
|
| 108 |
return round( $mm * 96 / 25.4, 2 ); |
| 109 |
} |
| 110 |
} |
| 111 |
|