plugin_template_dir = GENIUS_COMMERCE_PLUGIN_PATH . 'templates/'; } /** * Render a template with given arguments. * * Template lookup order: * 1. Child theme: /genius-commerce/templates/{template} * 2. Parent theme: /genius-commerce/templates/{template} * 3. Admin custom template (stored in options) * 4. Plugin default: /templates/{template} * * @param string $template_name Template file name (e.g., 'discount-price.php'). * @param array $args Variables to extract into template scope. * @return string Rendered template HTML. */ public function render( string $template_name, array $args = array() ): string { // Theme templates have highest priority - render PHP file directly. if ( $this->is_theme_override( $template_name ) ) { $theme_path = $this->get_theme_template_path( $template_name ); return $this->render_file( $theme_path, $args ); } // Check for admin custom template (stored as option). $custom_template = $this->get_custom_template( $template_name ); if ( null !== $custom_template ) { return $this->render_string( $custom_template, $args ); } // Fall back to plugin default template. $template_path = $this->plugin_template_dir . $template_name; if ( ! file_exists( $template_path ) ) { Genius_Commerce::log( sprintf( 'Template not found: %s', $template_name ), 'error' ); return ''; } // Render PHP template file. return $this->render_file( $template_path, $args ); } /** * Locate a template file. * * @param string $template_name Template file name. * @return string|null Full path to template or null if not found. */ public function locate_template( string $template_name ): ?string { // Check theme directories. $theme_template = locate_template( array( $this->theme_template_dir . '/' . $template_name, ) ); if ( $theme_template ) { return $theme_template; } // Check plugin directory. $plugin_template = $this->plugin_template_dir . $template_name; if ( file_exists( $plugin_template ) ) { return $plugin_template; } return null; } /** * Render a PHP template file. * * @param string $template_path Full path to template file. * @param array $args Variables to extract into template scope. * @return string Rendered HTML. */ private function render_file( string $template_path, array $args ): string { // Validate the resolved path is within allowed directories to prevent file inclusion attacks. $real_path = realpath( $template_path ); $allowed_dirs = array( realpath( $this->plugin_template_dir ), realpath( get_stylesheet_directory() ), realpath( get_template_directory() ), ); $is_allowed = false; foreach ( array_filter( $allowed_dirs ) as $dir ) { if ( 0 === strpos( $real_path, $dir ) ) { $is_allowed = true; break; } } if ( ! $real_path || ! $is_allowed ) { Genius_Commerce::log( sprintf( 'Template path not allowed: %s', $template_path ), 'error' ); return ''; } // Extract variables into scope. // phpcs:ignore WordPress.PHP.DontExtract.extract_extract extract( $args, EXTR_SKIP ); ob_start(); include $real_path; return ob_get_clean(); } /** * Render a template string with placeholder replacement. * * Supports mustache-style placeholders: {{variable_name}} * * @param string $template Template string with placeholders. * @param array $args Variables to replace placeholders with. * @return string Rendered HTML. */ private function render_string( string $template, array $args ): string { $output = $template; foreach ( $args as $key => $value ) { // Skip non-scalar values. if ( ! is_scalar( $value ) ) { continue; } $placeholder = '{{' . $key . '}}'; $output = str_replace( $placeholder, esc_html( (string) $value ), $output ); } // Allow HTML in certain placeholders (prices contain markup). $html_placeholders = array( 'original_price', 'discount_price', 'discount_amount', 'original_html', ); foreach ( $html_placeholders as $key ) { if ( isset( $args[ $key ] ) ) { $placeholder = '{{' . $key . '_html}}'; $output = str_replace( $placeholder, wp_kses_post( (string) $args[ $key ] ), $output ); } } return $output; } /** * Get the default template content for admin editor. * * @param string $template_name Template file name. * @return string Template content or empty string. */ public function get_default_template_content( string $template_name ): string { $template_path = $this->plugin_template_dir . $template_name; if ( ! file_exists( $template_path ) ) { return ''; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents return file_get_contents( $template_path ); } /** * Save custom template content. * * @param string $template_name Template file name. * @param string $content Custom template content. * @return bool True on success. */ public function save_custom_template( string $template_name, string $content ): bool { $option_key = 'genius_commerce_template_' . sanitize_key( str_replace( '.php', '', $template_name ) ); // Empty content resets to default. if ( empty( trim( $content ) ) ) { delete_option( $option_key ); return true; } // Sanitize content but allow safe HTML. $allowed_html = array( 'div' => array( 'class' => array(), 'id' => array(), 'style' => array(), ), 'span' => array( 'class' => array(), 'id' => array(), 'style' => array(), ), 'del' => array( 'class' => array(), ), 'ins' => array( 'class' => array(), ), 'strong' => array(), 'em' => array(), 'small' => array( 'class' => array(), ), 'br' => array(), 'p' => array( 'class' => array(), ), ); $sanitized = wp_kses( $content, $allowed_html ); return update_option( $option_key, $sanitized ); } /** * Get custom template content if exists. * * @param string $template_name Template file name. * @return string|null Custom content or null if using default. */ public function get_custom_template( string $template_name ): ?string { $option_key = 'genius_commerce_template_' . sanitize_key( str_replace( '.php', '', $template_name ) ); $custom = get_option( $option_key, '' ); return ! empty( $custom ) ? $custom : null; } /** * Check if a template is overridden by the theme. * * @param string $template_name Template file name. * @return bool True if theme override exists. */ public function is_theme_override( string $template_name ): bool { $theme_template = locate_template( array( $this->theme_template_dir . '/' . $template_name, ) ); return ! empty( $theme_template ); } /** * Get the full path to a theme template override. * * @param string $template_name Template file name. * @return string|null Full path to theme template or null if not found. */ public function get_theme_template_path( string $template_name ): ?string { $theme_template = locate_template( array( $this->theme_template_dir . '/' . $template_name, ) ); return ! empty( $theme_template ) ? $theme_template : null; } /** * Get the source of the active template. * * @param string $template_name Template file name. * @return string Source: 'theme', 'admin', or 'plugin'. */ public function get_template_source( string $template_name ): string { if ( $this->is_theme_override( $template_name ) ) { return 'theme'; } $custom = $this->get_custom_template( $template_name ); if ( null !== $custom ) { return 'admin'; } return 'plugin'; } /** * Get the active template path for debugging/testing. * * @param string $template_name Template file name. * @return string|null Path or database key for the active template. */ public function get_active_template_path( string $template_name ): ?string { if ( $this->is_theme_override( $template_name ) ) { return $this->get_theme_template_path( $template_name ); } if ( null !== $this->get_custom_template( $template_name ) ) { return 'database:genius_commerce_template_' . sanitize_key( str_replace( '.php', '', $template_name ) ); } $plugin_path = $this->plugin_template_dir . $template_name; return file_exists( $plugin_path ) ? $plugin_path : null; } /** * Get the theme template directory path. * * @return string Theme template directory (relative to theme root). */ public function get_theme_template_dir(): string { return $this->theme_template_dir; } }