monorepo/native/wordpress/maple-code-blocks/includes/class-code-renderer.php

222 lines
7.2 KiB
PHP

<?php
/**
* Code Renderer Class
* Safely renders code content with proper escaping and syntax highlighting
*/
// Prevent direct access
if (!defined('WPINC')) {
die('Direct access not permitted.');
}
if (!defined('ABSPATH')) {
exit;
}
class MCB_Code_Renderer {
/**
* Render code content safely
*/
public function render_code($content, $filename) {
// Validate content before processing
$validation = $this->validate_content($content);
if (is_wp_error($validation)) {
return '<div class="gcv-error">' . esc_html($validation->get_error_message()) . '</div>';
}
// First, ensure the content is treated as plain text
// Multiple layers of safety to prevent any code execution
// 1. Convert to UTF-8 if needed
if (!mb_check_encoding($content, 'UTF-8')) {
$content = mb_convert_encoding($content, 'UTF-8', mb_detect_encoding($content));
}
// 2. Remove any null bytes
$content = str_replace("\0", '', $content);
// 3. HTML encode everything - this is crucial for safety
$safe_content = htmlspecialchars($content, ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE, 'UTF-8', false);
// 4. Additional escaping for JavaScript context
$safe_content = $this->escape_for_javascript($safe_content);
// 5. Get language for syntax highlighting
$language = $this->detect_language($filename);
// 6. Prepare the code block with line numbers
$lines = explode("\n", $safe_content);
$formatted_code = $this->format_with_line_numbers($lines);
// 7. Wrap in proper HTML structure
$output = '<div class="gcv-code-container" data-language="' . esc_attr($language) . '">';
$output .= '<div class="gcv-code-header">';
$output .= '<span class="gcv-filename">' . esc_html(basename($filename)) . '</span>';
$output .= '<button class="gcv-copy-btn" data-content="' . esc_attr($safe_content) . '">Copy</button>';
$output .= '</div>';
$output .= '<div class="gcv-code-wrapper">';
$output .= '<pre class="line-numbers"><code class="language-' . esc_attr($language) . '">';
$output .= $formatted_code;
$output .= '</code></pre>';
$output .= '</div>';
$output .= '</div>';
return $output;
}
/**
* Format code with line numbers
*/
private function format_with_line_numbers($lines) {
$output = '';
$line_count = count($lines);
$digit_count = strlen((string)$line_count);
foreach ($lines as $index => $line) {
$line_num = $index + 1;
$padded_num = str_pad($line_num, $digit_count, ' ', STR_PAD_LEFT);
$output .= '<span class="line-number" data-line="' . $line_num . '">' . $padded_num . '</span>';
$output .= '<span class="line-content">' . $line . '</span>' . "\n";
}
return rtrim($output);
}
/**
* Additional escaping for JavaScript context
*/
private function escape_for_javascript($content) {
// Escape any remaining potentially dangerous patterns
$patterns = array(
'/<script/i' => '&lt;script',
'/<\/script/i' => '&lt;/script',
'/javascript:/i' => 'javascript&colon;',
'/on\w+\s*=/i' => 'on_event=',
'/<iframe/i' => '&lt;iframe',
'/<object/i' => '&lt;object',
'/<embed/i' => '&lt;embed',
'/<applet/i' => '&lt;applet'
);
foreach ($patterns as $pattern => $replacement) {
$content = preg_replace($pattern, $replacement, $content);
}
return $content;
}
/**
* Detect programming language from filename
*/
private function detect_language($filename) {
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$name_lower = strtolower($filename);
// Map extensions to Prism.js language identifiers
$language_map = array(
'php' => 'php',
'js' => 'javascript',
'jsx' => 'jsx',
'ts' => 'typescript',
'tsx' => 'tsx',
'py' => 'python',
'rb' => 'ruby',
'java' => 'java',
'c' => 'c',
'cpp' => 'cpp',
'cc' => 'cpp',
'cxx' => 'cpp',
'h' => 'c',
'hpp' => 'cpp',
'cs' => 'csharp',
'swift' => 'swift',
'kt' => 'kotlin',
'go' => 'go',
'rs' => 'rust',
'scala' => 'scala',
'r' => 'r',
'sql' => 'sql',
'sh' => 'bash',
'bash' => 'bash',
'yml' => 'yaml',
'yaml' => 'yaml',
'json' => 'json',
'xml' => 'xml',
'html' => 'html',
'htm' => 'html',
'css' => 'css',
'scss' => 'scss',
'sass' => 'sass',
'less' => 'less',
'md' => 'markdown',
'markdown' => 'markdown',
'txt' => 'plain',
'ini' => 'ini',
'conf' => 'ini',
'cfg' => 'ini'
);
// Special file names
if ($name_lower === 'dockerfile') {
return 'docker';
}
if ($name_lower === 'makefile' || $name_lower === 'gnumakefile') {
return 'makefile';
}
if ($name_lower === '.gitignore') {
return 'git';
}
if ($name_lower === '.htaccess') {
return 'apacheconf';
}
if ($name_lower === '.env') {
return 'bash';
}
return isset($language_map[$extension]) ? $language_map[$extension] : 'plain';
}
/**
* Sanitize and validate file content before rendering
*/
public function validate_content($content) {
// Check for binary content
if ($this->is_binary($content)) {
return new WP_Error('binary_file', 'Binary files cannot be displayed');
}
// Check file size (limit to 1MB for performance)
if (strlen($content) > 1048576) {
return new WP_Error('file_too_large', 'File is too large to display (max 1MB)');
}
return true;
}
/**
* Check if content appears to be binary
*/
private function is_binary($content) {
// Check for null bytes or excessive non-printable characters
$null_count = substr_count($content, "\0");
if ($null_count > 0) {
return true;
}
// Sample first 8192 bytes
$sample = substr($content, 0, 8192);
$non_printable = 0;
for ($i = 0; $i < strlen($sample); $i++) {
$char = ord($sample[$i]);
// Allow common whitespace and printable ASCII
if ($char < 32 && $char !== 9 && $char !== 10 && $char !== 13) {
$non_printable++;
}
}
// If more than 30% non-printable, consider it binary
return ($non_printable / strlen($sample)) > 0.3;
}
}