cache_enabled = $cache_enabled; $this->setup_template_directories(); $this->setup_global_vars(); } /** * Render a template with data. * * @since 1.0.0 * @param string $template_name Template name (without .php extension). * @param array $data Data to pass to template. * @param bool $return Whether to return output instead of echoing. * @return string|void Template output if $return is true. */ public function render( string $template_name, array $data = array(), bool $return = false ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.returnFound -- Kept for backward compatibility; not used as named argument. $template_path = $this->find_template( $template_name ); if ( ! $template_path ) { $error_msg = sprintf( 'Template not found: %s', esc_html( $template_name ) ); if ( $return ) { return $this->render_error( $error_msg ); } echo wp_kses_post( $this->render_error( $error_msg ) ); return; } $output = $this->render_template( $template_path, $data ); if ( $return ) { return $output; } echo wp_kses_post( $output ); } /** * Render template with caching. * * @since 1.0.0 * @param string $template_path Full path to template file. * @param array $data Template data. * @return string Rendered output. */ private function render_template( string $template_path, array $data ): string { $cache_key = $this->get_cache_key( $template_path, $data ); // Check cache first. if ( $this->cache_enabled && isset( self::$template_cache[ $cache_key ] ) ) { return self::$template_cache[ $cache_key ]; } // Start output buffering. ob_start(); try { // Make data available to template. $template_data = $this->prepare_template_data( $data ); // phpcs:ignore WordPress.PHP.DontExtract -- Świadomie udostępniamy przygotowane zmienne w zasięgu szablonu; EXTR_SKIP zapobiega nadpisaniu. extract( $template_data, EXTR_SKIP ); // Include the template. // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- Ładowanie plików szablonów po weryfikacji ścieżki. include $template_path; $output = ob_get_clean(); // Cache the output. if ( $this->cache_enabled ) { self::$template_cache[ $cache_key ] = $output; } return $output; } catch ( \Throwable $e ) { ob_end_clean(); $error_msg = sprintf( 'Error rendering template %s: %s', basename( $template_path ), $e->getMessage() ); return $this->render_error( $error_msg ); } } /** * Find template file in registered directories. * * @since 1.0.0 * @param string $template_name Template name. * @return string|false Template path or false if not found. */ private function find_template( string $template_name ) { // Add .php extension if not present. if ( ! str_ends_with( $template_name, '.php' ) ) { $template_name .= '.php'; } // Search in registered directories. foreach ( $this->template_directories as $directory ) { $template_path = $directory . '/' . $template_name; if ( file_exists( $template_path ) && is_readable( $template_path ) ) { return $template_path; } } return false; } /** * Prepare template data with escaping and global variables. * * @since 1.0.0 * @param array $data Raw template data. * @return array Prepared template data. */ private function prepare_template_data( array $data ): array { // Merge with global variables. $template_data = array_merge( $this->global_vars, $data ); // Add helper functions. $template_data['esc'] = array( $this, 'escape_data' ); $template_data['render_partial'] = array( $this, 'render_partial' ); $template_data['include_template'] = array( $this, 'include_template' ); return $template_data; } /** * Escape data for safe output. * * @since 1.0.0 * @param mixed $data Data to escape. * @param string $context Escaping context (html, attr, url, js, css). * @return mixed Escaped data. */ public function escape_data( $data, string $context = 'html' ) { if ( is_array( $data ) ) { return array_map( function ( $item ) use ( $context ) { return $this->escape_data( $item, $context ); }, $data ); } if ( ! is_string( $data ) ) { return $data; } switch ( $context ) { case 'attr': return esc_attr( $data ); case 'url': return esc_url( $data ); case 'js': return esc_js( $data ); case 'css': return esc_attr( $data ); // WordPress doesn't have esc_css. case 'textarea': return esc_textarea( $data ); case 'html': default: return esc_html( $data ); } } /** * Render a partial template. * * @since 1.0.0 * @param string $partial_name Partial template name. * @param array $data Data for partial. * @return string Rendered partial. */ public function render_partial( string $partial_name, array $data = array() ): string { return $this->render( $partial_name, $data, true ); } /** * Include another template within current template. * * @since 1.0.0 * @param string $template_name Template to include. * @param array $data Additional data for included template. * @return void */ public function include_template( string $template_name, array $data = array() ): void { echo wp_kses_post( $this->render( $template_name, $data, true ) ); } /** * Add template directory. * * @since 1.0.0 * @param string $directory Directory path. * @param int $priority Priority (lower = higher priority). * @return void */ public function add_template_directory( string $directory, int $priority = 10 ): void { if ( ! is_dir( $directory ) ) { return; } $this->template_directories[ $priority ] = rtrim( $directory, '/' ); ksort( $this->template_directories ); } /** * Set global template variable. * * @since 1.0.0 * @param string $key Variable name. * @param mixed $value Variable value. * @return void */ public function set_global_var( string $key, $value ): void { $this->global_vars[ $key ] = $value; } /** * Set multiple global variables. * * @since 1.0.0 * @param array $vars Variables array. * @return void */ public function set_global_vars( array $vars ): void { $this->global_vars = array_merge( $this->global_vars, $vars ); } /** * Clear template cache. * * @since 1.0.0 * @param string|null $template_name Specific template to clear, or null for all. * @return void */ public function clear_cache( ?string $template_name = null ): void { if ( null === $template_name ) { self::$template_cache = array(); } else { $pattern = $this->get_cache_key_pattern( $template_name ); foreach ( array_keys( self::$template_cache ) as $key ) { if ( str_contains( $key, $pattern ) ) { unset( self::$template_cache[ $key ] ); } } } } /** * Enable or disable caching. * * @since 1.0.0 * @param bool $enabled Whether to enable caching. * @return void */ public function set_cache_enabled( bool $enabled ): void { $this->cache_enabled = $enabled; if ( ! $enabled ) { $this->clear_cache(); } } /** * Check if template exists. * * @since 1.0.0 * @param string $template_name Template name. * @return bool True if template exists. */ public function template_exists( string $template_name ): bool { return false !== $this->find_template( $template_name ); } /** * Get cache statistics. * * @since 1.0.0 * @return array Cache statistics. */ public function get_cache_stats(): array { return array( 'enabled' => $this->cache_enabled, 'cached_templates' => count( self::$template_cache ), 'memory_usage' => $this->calculate_cache_memory_usage(), ); } /** * Setup default template directories. * * @since 1.0.0 * @return void */ private function setup_template_directories(): void { // Plugin templates directory. $plugin_templates = ACCESSIBILITY_AUDIO_TTS_PLUGIN_DIR . 'admin/templates/settings'; if ( is_dir( $plugin_templates ) ) { $this->add_template_directory( $plugin_templates, 10 ); } // Theme override directory. $theme_templates = get_template_directory() . '/accessibility-audio-tts/admin'; if ( is_dir( $theme_templates ) ) { $this->add_template_directory( $theme_templates, 5 ); } // Child theme override directory. if ( is_child_theme() ) { $child_theme_templates = get_stylesheet_directory() . '/accessibility-audio-tts/admin'; if ( is_dir( $child_theme_templates ) ) { $this->add_template_directory( $child_theme_templates, 1 ); } } } /** * Setup global template variables. * * @since 1.0.0 * @return void */ private function setup_global_vars(): void { $this->global_vars = array( 'plugin_url' => ACCESSIBILITY_AUDIO_TTS_PLUGIN_URL, 'plugin_path' => ACCESSIBILITY_AUDIO_TTS_PLUGIN_DIR, 'plugin_version' => ACCESSIBILITY_AUDIO_TTS_VERSION, 'admin_url' => admin_url(), 'current_user' => wp_get_current_user(), 'is_admin' => is_admin(), 'nonce_field' => wp_nonce_field( 'accessibility_audio_tts_admin', '_wpnonce', true, false ), ); } /** * Generate cache key for template and data. * * @since 1.0.0 * @param string $template_path Template path. * @param array $data Template data. * @return string Cache key. */ private function get_cache_key( string $template_path, array $data ): string { $template_name = basename( $template_path ); $data_json = wp_json_encode( $data ); if ( false === $data_json ) { $data_json = ''; } $data_hash = md5( $data_json ); $file_time = filemtime( $template_path ); return sprintf( '%s_%s_%s', $template_name, $data_hash, $file_time ); } /** * Get cache key pattern for template. * * @since 1.0.0 * @param string $template_name Template name. * @return string Cache key pattern. */ private function get_cache_key_pattern( string $template_name ): string { if ( ! str_ends_with( $template_name, '.php' ) ) { $template_name .= '.php'; } return $template_name . '_'; } /** * Calculate cache memory usage. * * @since 1.0.0 * @return int Memory usage in bytes. */ private function calculate_cache_memory_usage(): int { $total_size = 0; foreach ( self::$template_cache as $content ) { $total_size += strlen( $content ); } return $total_size; } /** * Render error message. * * @since 1.0.0 * @param string $message Error message. * @return string Error HTML. */ private function render_error( string $message ): string { if ( defined( 'WP_DEBUG' ) && true === WP_DEBUG ) { return sprintf( '

Template Error: %s

', esc_html( $message ) ); } return ''; } /** * Render JSON data safely. * * @since 1.0.0 * @param mixed $data Data to encode as JSON. * @param int $flags JSON encoding flags. * @return string JSON string. */ public function render_json( $data, int $flags = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ): string { return wp_json_encode( $data, $flags ); } /** * Render inline CSS safely. * * @since 1.0.0 * @param string $css CSS content. * @return string Safe CSS. */ public function render_inline_css( string $css ): string { // Basic CSS sanitization - remove potentially dangerous content. $css = preg_replace( '/javascript:/i', '', $css ); $css = preg_replace( '/expression\s*\(/i', '', $css ); $css = preg_replace( '/import\s+/i', '', $css ); return $css; } /** * Render inline JavaScript safely. * * @since 1.0.0 * @param string $js JavaScript content. * @return string Safe JavaScript. */ public function render_inline_js( string $js ): string { return esc_js( $js ); } }