added additional plugins

This commit is contained in:
Rodolfo Martinez 2025-12-12 19:05:48 -05:00
parent c85895d306
commit 00e60ec1b7
132 changed files with 27514 additions and 0 deletions

View file

@ -0,0 +1,33 @@
<?php
/**
* Ultra Simple Block Registration - PHP Only
* This ensures the block is registered even if JavaScript fails
*/
add_action('init', function() {
if (!function_exists('register_block_type')) {
return;
}
// Register a basic block with PHP only
register_block_type('maple-code-blocks/basic', array(
'title' => 'Maple Code Block (Basic)',
'description' => 'Display code from repositories',
'category' => 'widgets',
'icon' => 'editor-code',
'keywords' => array('maple', 'code', 'github'),
'attributes' => array(
'repository' => array(
'type' => 'string',
'default' => ''
)
),
'render_callback' => function($attributes) {
$repo = isset($attributes['repository']) ? $attributes['repository'] : '';
if (empty($repo)) {
return '<p>Enter a repository in block settings</p>';
}
return do_shortcode('[maple_code_block repo="' . esc_attr($repo) . '"]');
}
));
}, 100); // Very late priority to ensure everything is loaded

View file

@ -0,0 +1,351 @@
<?php
/**
* Gutenberg Block Registration
* Registers the GitHub Code Viewer block for the block editor
*/
// Prevent direct access
if (!defined('WPINC')) {
die('Direct access not permitted.');
}
if (!defined('ABSPATH')) {
exit;
}
class MCB_Block_Editor {
/**
* Initialize block editor support
*/
public static function init() {
// Check if Gutenberg is available
if (!function_exists('register_block_type')) {
return;
}
add_action('init', array(__CLASS__, 'register_block'));
add_action('enqueue_block_editor_assets', array(__CLASS__, 'enqueue_block_editor_assets'));
add_action('enqueue_block_assets', array(__CLASS__, 'enqueue_block_assets'));
add_filter('block_categories_all', array(__CLASS__, 'add_block_category'), 10, 2);
}
/**
* Register the block
*/
public static function register_block() {
// Debug: Log that we're attempting to register
error_log('MCB: Attempting to register block');
// Register block editor script
wp_register_script(
'mcb-block-editor',
MCB_PLUGIN_URL . 'assets/js/block-editor.js',
array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-data'),
MCB_PLUGIN_VERSION,
true
);
// Register block editor styles
wp_register_style(
'mcb-block-editor-style',
MCB_PLUGIN_URL . 'assets/css/block-editor.css',
array('wp-edit-blocks'),
MCB_PLUGIN_VERSION
);
// Register the block with simplified attributes first
$result = register_block_type('maple-code-blocks/code-block', array(
'editor_script' => 'mcb-block-editor',
'editor_style' => 'mcb-block-editor-style',
'render_callback' => array(__CLASS__, 'render_block'),
'attributes' => array(
'repository' => array(
'type' => 'string',
'default' => ''
),
'theme' => array(
'type' => 'string',
'default' => 'dark'
),
'height' => array(
'type' => 'string',
'default' => '600px'
),
'showLineNumbers' => array(
'type' => 'boolean',
'default' => true
),
'initialFile' => array(
'type' => 'string',
'default' => ''
),
'title' => array(
'type' => 'string',
'default' => ''
),
'align' => array(
'type' => 'string',
'default' => 'none'
),
'className' => array(
'type' => 'string',
'default' => ''
)
),
'supports' => array(
'align' => array('wide', 'full'),
'className' => true,
'customClassName' => true,
'html' => false,
'anchor' => true
),
'example' => array(
'attributes' => array(
'repository' => 'facebook/react',
'theme' => 'dark',
'height' => '400px',
'title' => 'React Source Code Example'
)
)
));
// Add block variations
wp_register_script(
'mcb-block-variations',
MCB_PLUGIN_URL . 'assets/js/block-variations.js',
array('wp-blocks', 'wp-dom-ready'),
MCB_PLUGIN_VERSION,
true
);
wp_enqueue_script('mcb-block-variations');
}
/**
* Render the block on the frontend
*/
public static function render_block($attributes, $content) {
// Validate required attributes
if (empty($attributes['repository'])) {
return '<div class="mcb-error">Please specify a repository (e.g., owner/repository)</div>';
}
// Sanitize attributes
$repository = sanitize_text_field($attributes['repository']);
// Validate repository format
if (!MCB_Security::validate_repo_format($repository)) {
return '<div class="mcb-error">Invalid repository format. Use: owner/repository</div>';
}
// Build shortcode attributes
$shortcode_atts = array(
'repo' => $repository,
'theme' => sanitize_text_field($attributes['theme'] ?? 'dark'),
'height' => sanitize_text_field($attributes['height'] ?? '600px'),
'show_line_numbers' => $attributes['showLineNumbers'] ? 'true' : 'false',
'initial_file' => sanitize_text_field($attributes['initialFile'] ?? ''),
'title' => sanitize_text_field($attributes['title'] ?? '')
);
// Add alignment class if needed
$wrapper_class = 'mcb-block-wrapper';
if (!empty($attributes['align'])) {
$wrapper_class .= ' align' . esc_attr($attributes['align']);
}
if (!empty($attributes['className'])) {
$wrapper_class .= ' ' . esc_attr($attributes['className']);
}
// Generate shortcode
$shortcode = '[maple_code_block';
foreach ($shortcode_atts as $key => $value) {
if (!empty($value)) {
$shortcode .= ' ' . $key . '="' . esc_attr($value) . '"';
}
}
$shortcode .= ']';
// Render with wrapper
return sprintf(
'<div class="%s">%s</div>',
esc_attr($wrapper_class),
do_shortcode($shortcode)
);
}
/**
* Enqueue block editor assets
*/
public static function enqueue_block_editor_assets() {
// Debug log
error_log('MCB: Enqueueing block editor assets');
// Make sure our script is registered
if (!wp_script_is('mcb-block-editor', 'registered')) {
error_log('MCB: Script not registered, registering now');
wp_register_script(
'mcb-block-editor',
MCB_PLUGIN_URL . 'assets/js/block-editor.js',
array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-data'),
MCB_PLUGIN_VERSION,
true
);
}
// Localize script with data for the editor
wp_localize_script('mcb-block-editor', 'mcbBlockData', array(
'pluginUrl' => MCB_PLUGIN_URL,
'themes' => array(
array('label' => 'Dark', 'value' => 'dark'),
array('label' => 'Light', 'value' => 'light'),
array('label' => 'Monokai', 'value' => 'monokai'),
array('label' => 'Solarized', 'value' => 'solarized')
),
'defaultHeight' => '600px',
'popularRepos' => array(
'facebook/react',
'vuejs/vue',
'angular/angular',
'microsoft/vscode',
'torvalds/linux',
'tensorflow/tensorflow',
'kubernetes/kubernetes',
'nodejs/node',
'rust-lang/rust',
'golang/go'
),
'nonce' => wp_create_nonce('mcb_block_nonce')
));
}
/**
* Enqueue frontend block assets
*/
public static function enqueue_block_assets() {
// Only enqueue on frontend
if (!is_admin()) {
// These will be enqueued by the shortcode handler
// We just need to ensure the block wrapper styles are loaded
wp_enqueue_style(
'mcb-block-style',
MCB_PLUGIN_URL . 'assets/css/block-style.css',
array(),
MCB_PLUGIN_VERSION
);
}
}
/**
* Add custom block category
*/
public static function add_block_category($categories, $post) {
return array_merge(
array(
array(
'slug' => 'maple-code-blocks',
'title' => __('Maple Code Blocks', 'maple-code-blocks'),
'icon' => 'editor-code'
)
),
$categories
);
}
/**
* Register REST API endpoint for repository validation
*/
public static function register_rest_routes() {
register_rest_route('maple-code-blocks/v1', '/validate-repo', array(
'methods' => 'POST',
'callback' => array(__CLASS__, 'validate_repository'),
'permission_callback' => function() {
return current_user_can('edit_posts');
},
'args' => array(
'repository' => array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field'
)
)
));
register_rest_route('maple-code-blocks/v1', '/get-files', array(
'methods' => 'POST',
'callback' => array(__CLASS__, 'get_repository_files'),
'permission_callback' => function() {
return current_user_can('edit_posts');
},
'args' => array(
'repository' => array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field'
)
)
));
}
/**
* Validate repository via REST API
*/
public static function validate_repository($request) {
$repository = $request->get_param('repository');
// Validate format
if (!MCB_Security::validate_repo_format($repository)) {
return new WP_Error('invalid_format', 'Invalid repository format. Use: owner/repository');
}
// Quick check with GitHub API
$github_api = new MCB_GitHub_API();
$result = $github_api->validate_repository_exists($repository);
if (is_wp_error($result)) {
return $result;
}
return array(
'valid' => true,
'repository' => $repository
);
}
/**
* Get repository files for block editor preview
*/
public static function get_repository_files($request) {
$repository = $request->get_param('repository');
// Validate format
if (!MCB_Security::validate_repo_format($repository)) {
return new WP_Error('invalid_format', 'Invalid repository format');
}
$github_api = new MCB_GitHub_API();
$files = $github_api->get_repository_files($repository);
if (is_wp_error($files)) {
return $files;
}
// Return only file names for the editor
$file_list = array_map(function($file) {
return array(
'path' => $file['path'],
'name' => $file['name']
);
}, array_slice($files, 0, 10)); // Limit to 10 files for preview
return array(
'files' => $file_list,
'total' => count($files)
);
}
}
// Initialize block editor support
add_action('init', array('MCB_Block_Editor', 'init'));
add_action('rest_api_init', array('MCB_Block_Editor', 'register_rest_routes'));

View file

@ -0,0 +1,273 @@
<?php
/**
* Block Patterns Registration
* Registers reusable block patterns for the GitHub Code Viewer
*/
// Prevent direct access
if (!defined('WPINC')) {
die('Direct access not permitted.');
}
if (!defined('ABSPATH')) {
exit;
}
class MCB_Block_Patterns {
/**
* Initialize block patterns
*/
public static function init() {
add_action('init', array(__CLASS__, 'register_pattern_category'));
add_action('init', array(__CLASS__, 'register_patterns'));
}
/**
* Register pattern category
*/
public static function register_pattern_category() {
if (function_exists('register_block_pattern_category')) {
register_block_pattern_category(
'maple-code-blocks',
array(
'label' => __('GitHub Code Viewer', 'maple-code-blocks')
)
);
}
}
/**
* Register block patterns
*/
public static function register_patterns() {
if (!function_exists('register_block_pattern')) {
return;
}
// Code with Explanation Pattern
register_block_pattern(
'maple-code-blocks/code-with-explanation',
array(
'title' => __('Maple: Code with Explanation', 'maple-code-blocks'),
'description' => __('Display code with explanatory text', 'maple-code-blocks'),
'categories' => array('maple-code-blocks', 'text'),
'keywords' => array('code', 'github', 'explanation', 'tutorial'),
'content' => '<!-- wp:group {"backgroundColor":"white","style":{"spacing":{"padding":{"top":"30px","right":"30px","bottom":"30px","left":"30px"}}}} -->
<div class="wp-block-group has-white-background-color has-background" style="padding-top:30px;padding-right:30px;padding-bottom:30px;padding-left:30px">
<!-- wp:heading {"level":3} -->
<h3>' . __('Understanding the Code', 'maple-code-blocks') . '</h3>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>' . __('This example demonstrates a key concept in modern development:', 'maple-code-blocks') . '</p>
<!-- /wp:paragraph -->
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"dark","height":"400px","showLineNumbers":true} /-->
<!-- wp:list -->
<ul>
<li>' . __('Line 1-5: Initial setup and configuration', 'maple-code-blocks') . '</li>
<li>' . __('Line 6-15: Core functionality implementation', 'maple-code-blocks') . '</li>
<li>' . __('Line 16-20: Error handling and cleanup', 'maple-code-blocks') . '</li>
</ul>
<!-- /wp:list -->
</div>
<!-- /wp:group -->'
)
);
// Side-by-side Code Comparison
register_block_pattern(
'maple-code-blocks/side-by-side',
array(
'title' => __('Maple: Side-by-Side Comparison', 'maple-code-blocks'),
'description' => __('Compare two code implementations', 'maple-code-blocks'),
'categories' => array('maple-code-blocks', 'columns'),
'keywords' => array('compare', 'code', 'github', 'columns'),
'content' => '<!-- wp:columns {"align":"wide"} -->
<div class="wp-block-columns alignwide">
<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:heading {"level":4,"style":{"color":{"text":"#0073aa"}}} -->
<h4 class="has-text-color" style="color:#0073aa">' . __('Method A', 'maple-code-blocks') . '</h4>
<!-- /wp:heading -->
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"350px","showLineNumbers":true} /-->
</div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:heading {"level":4,"style":{"color":{"text":"#00a32a"}}} -->
<h4 class="has-text-color" style="color:#00a32a">' . __('Method B', 'maple-code-blocks') . '</h4>
<!-- /wp:heading -->
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"350px","showLineNumbers":true} /-->
</div>
<!-- /wp:column -->
</div>
<!-- /wp:columns -->'
)
);
// Code Gallery Pattern
register_block_pattern(
'maple-code-blocks/code-gallery',
array(
'title' => __('Maple: Code Gallery', 'maple-code-blocks'),
'description' => __('Showcase multiple code examples', 'maple-code-blocks'),
'categories' => array('maple-code-blocks', 'gallery'),
'keywords' => array('gallery', 'showcase', 'multiple', 'code'),
'content' => '<!-- wp:heading {"textAlign":"center"} -->
<h2 class="has-text-align-center">' . __('Code Examples', 'maple-code-blocks') . '</h2>
<!-- /wp:heading -->
<!-- wp:spacer {"height":"30px"} -->
<div style="height:30px" aria-hidden="true" class="wp-block-spacer"></div>
<!-- /wp:spacer -->
<!-- wp:columns -->
<div class="wp-block-columns">
<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:heading {"level":5,"textAlign":"center"} -->
<h5 class="has-text-align-center">' . __('Example 1', 'maple-code-blocks') . '</h5>
<!-- /wp:heading -->
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"monokai","height":"250px","showLineNumbers":false} /-->
</div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:heading {"level":5,"textAlign":"center"} -->
<h5 class="has-text-align-center">' . __('Example 2', 'maple-code-blocks') . '</h5>
<!-- /wp:heading -->
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"monokai","height":"250px","showLineNumbers":false} /-->
</div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:heading {"level":5,"textAlign":"center"} -->
<h5 class="has-text-align-center">' . __('Example 3', 'maple-code-blocks') . '</h5>
<!-- /wp:heading -->
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"monokai","height":"250px","showLineNumbers":false} /-->
</div>
<!-- /wp:column -->
</div>
<!-- /wp:columns -->'
)
);
// Featured Code Pattern
register_block_pattern(
'maple-code-blocks/featured-code',
array(
'title' => __('Maple: Featured Code', 'maple-code-blocks'),
'description' => __('Highlight important code with context', 'maple-code-blocks'),
'categories' => array('maple-code-blocks', 'featured'),
'keywords' => array('featured', 'highlight', 'code', 'important'),
'content' => '<!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"top":"40px","right":"40px","bottom":"40px","left":"40px"}},"border":{"radius":"8px"}},"backgroundColor":"light-gray"} -->
<div class="wp-block-group alignwide has-light-gray-background-color has-background" style="border-radius:8px;padding-top:40px;padding-right:40px;padding-bottom:40px;padding-left:40px">
<!-- wp:columns -->
<div class="wp-block-columns">
<!-- wp:column {"width":"40%"} -->
<div class="wp-block-column" style="flex-basis:40%">
<!-- wp:heading {"level":3} -->
<h3>' . __('Featured Implementation', 'maple-code-blocks') . '</h3>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>' . __('This code demonstrates best practices for this common pattern.', 'maple-code-blocks') . '</p>
<!-- /wp:paragraph -->
<!-- wp:buttons -->
<div class="wp-block-buttons">
<!-- wp:button -->
<div class="wp-block-button"><a class="wp-block-button__link">' . __('View on GitHub', 'maple-code-blocks') . '</a></div>
<!-- /wp:button -->
</div>
<!-- /wp:buttons -->
</div>
<!-- /wp:column -->
<!-- wp:column {"width":"60%"} -->
<div class="wp-block-column" style="flex-basis:60%">
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"dark","height":"350px","showLineNumbers":true} /-->
</div>
<!-- /wp:column -->
</div>
<!-- /wp:columns -->
</div>
<!-- /wp:group -->'
)
);
// Tutorial Step Pattern
register_block_pattern(
'maple-code-blocks/tutorial-steps',
array(
'title' => __('Maple: Tutorial Steps', 'maple-code-blocks'),
'description' => __('Step-by-step code tutorial', 'maple-code-blocks'),
'categories' => array('maple-code-blocks', 'text'),
'keywords' => array('tutorial', 'steps', 'education', 'learn'),
'content' => '<!-- wp:group -->
<div class="wp-block-group">
<!-- wp:heading -->
<h2>' . __('Building Your First Application', 'maple-code-blocks') . '</h2>
<!-- /wp:heading -->
<!-- wp:group {"style":{"spacing":{"margin":{"top":"30px","bottom":"30px"}}}} -->
<div class="wp-block-group" style="margin-top:30px;margin-bottom:30px">
<!-- wp:heading {"level":3,"style":{"typography":{"fontSize":"20px"}}} -->
<h3 style="font-size:20px">📝 ' . __('Step 1: Setup', 'maple-code-blocks') . '</h3>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>' . __('First, we need to set up our development environment:', 'maple-code-blocks') . '</p>
<!-- /wp:paragraph -->
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"200px","showLineNumbers":true} /-->
</div>
<!-- /wp:group -->
<!-- wp:group {"style":{"spacing":{"margin":{"top":"30px","bottom":"30px"}}}} -->
<div class="wp-block-group" style="margin-top:30px;margin-bottom:30px">
<!-- wp:heading {"level":3,"style":{"typography":{"fontSize":"20px"}}} -->
<h3 style="font-size:20px">🔧 ' . __('Step 2: Configuration', 'maple-code-blocks') . '</h3>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>' . __('Next, configure the application settings:', 'maple-code-blocks') . '</p>
<!-- /wp:paragraph -->
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"250px","showLineNumbers":true} /-->
</div>
<!-- /wp:group -->
<!-- wp:group {"style":{"spacing":{"margin":{"top":"30px","bottom":"30px"}}}} -->
<div class="wp-block-group" style="margin-top:30px;margin-bottom:30px">
<!-- wp:heading {"level":3,"style":{"typography":{"fontSize":"20px"}}} -->
<h3 style="font-size:20px">🚀 ' . __('Step 3: Deploy', 'maple-code-blocks') . '</h3>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>' . __('Finally, deploy your application:', 'maple-code-blocks') . '</p>
<!-- /wp:paragraph -->
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"200px","showLineNumbers":true} /-->
</div>
<!-- /wp:group -->
</div>
<!-- /wp:group -->'
)
);
}
}
// Initialize patterns
MCB_Block_Patterns::init();

View file

@ -0,0 +1,222 @@
<?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;
}
}

View file

@ -0,0 +1,749 @@
<?php
/**
* Git Platform API Handler Class
* Handles API interactions for GitHub, GitLab, Bitbucket, and Codeberg
*/
// Prevent direct access
if (!defined('WPINC')) {
die('Direct access not permitted.');
}
if (!defined('ABSPATH')) {
exit;
}
class MCB_GitHub_API {
private $cache_duration = 3600; // 1 hour cache
// Platform API configurations
private $platforms = array(
'github' => array(
'name' => 'GitHub',
'api_base' => 'https://api.github.com',
'repo_pattern' => '/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/',
'web_base' => 'https://github.com'
),
'gitlab' => array(
'name' => 'GitLab',
'api_base' => 'https://gitlab.com/api/v4',
'repo_pattern' => '/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/',
'web_base' => 'https://gitlab.com'
),
'bitbucket' => array(
'name' => 'Bitbucket',
'api_base' => 'https://api.bitbucket.org/2.0',
'repo_pattern' => '/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/',
'web_base' => 'https://bitbucket.org'
),
'codeberg' => array(
'name' => 'Codeberg',
'api_base' => 'https://codeberg.org/api/v1',
'repo_pattern' => '/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/',
'web_base' => 'https://codeberg.org'
)
);
/**
* Detect platform from repository string
*/
private function detect_platform($repo_string) {
// Check for platform prefix (e.g., "gitlab:owner/repo")
if (strpos($repo_string, ':') !== false) {
list($platform, $repo) = explode(':', $repo_string, 2);
if (isset($this->platforms[$platform])) {
return array('platform' => $platform, 'repo' => $repo);
}
}
// Check for full URLs
if (strpos($repo_string, 'https://') === 0 || strpos($repo_string, 'http://') === 0) {
foreach ($this->platforms as $key => $config) {
if (strpos($repo_string, $config['web_base']) !== false) {
// Extract owner/repo from URL
$parsed = parse_url($repo_string);
$path = trim($parsed['path'], '/');
$parts = explode('/', $path);
if (count($parts) >= 2) {
return array(
'platform' => $key,
'repo' => $parts[0] . '/' . $parts[1]
);
}
}
}
}
// Default to GitHub for backward compatibility
return array('platform' => 'github', 'repo' => $repo_string);
}
/**
* Get repository files list
*/
public function get_repository_files($repo_string, $path = '') {
$platform_info = $this->detect_platform($repo_string);
$platform = $platform_info['platform'];
$repo = $platform_info['repo'];
// Validate repo format
if (!preg_match($this->platforms[$platform]['repo_pattern'], $repo)) {
return new WP_Error('invalid_repo', 'Invalid repository format. Use: owner/repository');
}
// Check cache first
$cache_key = 'mcb_repo_' . $platform . '_' . md5($repo . '_' . $path);
$cached_data = get_transient($cache_key);
if ($cached_data !== false) {
return $cached_data;
}
// Get files based on platform
switch ($platform) {
case 'github':
$files = $this->get_github_files($repo, $path);
break;
case 'gitlab':
$files = $this->get_gitlab_files($repo, $path);
break;
case 'bitbucket':
$files = $this->get_bitbucket_files($repo, $path);
break;
case 'codeberg':
$files = $this->get_codeberg_files($repo, $path);
break;
default:
return new WP_Error('unsupported_platform', 'Unsupported platform');
}
if (!is_wp_error($files)) {
// Cache the results
set_transient($cache_key, $files, $this->cache_duration);
}
return $files;
}
/**
* Get file content from repository
*/
public function get_file_content($repo_string, $file_path) {
$platform_info = $this->detect_platform($repo_string);
$platform = $platform_info['platform'];
$repo = $platform_info['repo'];
// Validate inputs
if (!preg_match($this->platforms[$platform]['repo_pattern'], $repo)) {
return new WP_Error('invalid_repo', 'Invalid repository format');
}
// Sanitize and validate file path
$file_path = trim($file_path, '/');
// Block path traversal attempts
if (strpos($file_path, '..') !== false ||
strpos($file_path, '//') !== false ||
strpos($file_path, '\\') !== false ||
preg_match('/[<>"|*?]/', $file_path)) {
return new WP_Error('invalid_path', 'Invalid file path');
}
// Check cache
$cache_key = 'mcb_file_' . $platform . '_' . md5($repo . $file_path);
$cached_content = get_transient($cache_key);
if ($cached_content !== false) {
return $cached_content;
}
// Get content based on platform
switch ($platform) {
case 'github':
$content = $this->get_github_file_content($repo, $file_path);
break;
case 'gitlab':
$content = $this->get_gitlab_file_content($repo, $file_path);
break;
case 'bitbucket':
$content = $this->get_bitbucket_file_content($repo, $file_path);
break;
case 'codeberg':
$content = $this->get_codeberg_file_content($repo, $file_path);
break;
default:
return new WP_Error('unsupported_platform', 'Unsupported platform');
}
if (!is_wp_error($content)) {
// Cache the content
set_transient($cache_key, $content, $this->cache_duration);
}
return $content;
}
/**
* GitHub-specific file listing
*/
private function get_github_files($repo, $path = '') {
$url = $this->platforms['github']['api_base'] . '/repos/' . $repo . '/contents';
if (!empty($path)) {
$url .= '/' . ltrim($path, '/');
}
$response = $this->make_api_request($url, 'github');
if (is_wp_error($response)) {
return $response;
}
return $this->parse_github_contents($response, $repo, $path);
}
/**
* GitLab-specific file listing
*/
private function get_gitlab_files($repo, $path = '') {
// GitLab uses project ID or URL-encoded path
$project_id = urlencode($repo);
$url = $this->platforms['gitlab']['api_base'] . '/projects/' . $project_id . '/repository/tree?recursive=true&per_page=100';
$response = $this->make_api_request($url, 'gitlab');
if (is_wp_error($response)) {
return $response;
}
return $this->parse_gitlab_contents($response);
}
/**
* Bitbucket-specific file listing
*/
private function get_bitbucket_files($repo, $path = '') {
$url = $this->platforms['bitbucket']['api_base'] . '/repositories/' . $repo . '/src';
$response = $this->make_api_request($url, 'bitbucket');
if (is_wp_error($response)) {
return $response;
}
return $this->parse_bitbucket_contents($response, $repo);
}
/**
* Codeberg-specific file listing (uses Gitea API)
*/
private function get_codeberg_files($repo, $path = '') {
$url = $this->platforms['codeberg']['api_base'] . '/repos/' . $repo . '/contents';
if (!empty($path)) {
$url .= '/' . ltrim($path, '/');
}
$response = $this->make_api_request($url, 'codeberg');
if (is_wp_error($response)) {
return $response;
}
// Codeberg uses Gitea, similar to GitHub API
return $this->parse_github_contents($response, $repo, $path);
}
/**
* GitHub file content retrieval
*/
private function get_github_file_content($repo, $file_path) {
$url = $this->platforms['github']['api_base'] . '/repos/' . $repo . '/contents/' . $file_path;
$response = $this->make_api_request($url, 'github');
if (is_wp_error($response)) {
return $response;
}
$data = json_decode($response, true);
if (!isset($data['content'])) {
return new WP_Error('no_content', 'File content not found');
}
return base64_decode($data['content']);
}
/**
* GitLab file content retrieval
*/
private function get_gitlab_file_content($repo, $file_path) {
$project_id = urlencode($repo);
$file_path_encoded = urlencode($file_path);
$url = $this->platforms['gitlab']['api_base'] . '/projects/' . $project_id . '/repository/files/' . $file_path_encoded . '/raw?ref=main';
// Try main branch first, then master
$response = $this->make_api_request($url, 'gitlab');
if (is_wp_error($response)) {
// Try master branch
$url = $this->platforms['gitlab']['api_base'] . '/projects/' . $project_id . '/repository/files/' . $file_path_encoded . '/raw?ref=master';
$response = $this->make_api_request($url, 'gitlab');
}
return $response;
}
/**
* Bitbucket file content retrieval
*/
private function get_bitbucket_file_content($repo, $file_path) {
// Get default branch first
$repo_url = $this->platforms['bitbucket']['api_base'] . '/repositories/' . $repo;
$repo_response = $this->make_api_request($repo_url, 'bitbucket');
if (is_wp_error($repo_response)) {
return $repo_response;
}
$repo_data = json_decode($repo_response, true);
$branch = isset($repo_data['mainbranch']['name']) ? $repo_data['mainbranch']['name'] : 'master';
$url = $this->platforms['bitbucket']['api_base'] . '/repositories/' . $repo . '/src/' . $branch . '/' . $file_path;
return $this->make_api_request($url, 'bitbucket');
}
/**
* Codeberg file content retrieval
*/
private function get_codeberg_file_content($repo, $file_path) {
// Codeberg uses Gitea API, similar to GitHub
$url = $this->platforms['codeberg']['api_base'] . '/repos/' . $repo . '/contents/' . $file_path;
$response = $this->make_api_request($url, 'codeberg');
if (is_wp_error($response)) {
return $response;
}
$data = json_decode($response, true);
if (!isset($data['content'])) {
return new WP_Error('no_content', 'File content not found');
}
return base64_decode($data['content']);
}
/**
* Make HTTP request to Git platform API
*/
private function make_api_request($url, $platform) {
// SSRF Protection - validate URL
$parsed_url = parse_url($url);
// Only allow HTTPS protocol
if ($parsed_url['scheme'] !== 'https') {
return new WP_Error('invalid_protocol', 'Only HTTPS is allowed');
}
// Only allow known platform hosts
$allowed_hosts = array(
'api.github.com',
'gitlab.com',
'api.bitbucket.org',
'codeberg.org'
);
if (!in_array($parsed_url['host'], $allowed_hosts)) {
return new WP_Error('invalid_host', 'Invalid API host');
}
// Add request throttling to prevent rate limit issues
static $last_request_time = 0;
$min_interval = 0.1; // Minimum 100ms between requests
$current_time = microtime(true);
$time_since_last = $current_time - $last_request_time;
if ($time_since_last < $min_interval) {
usleep(($min_interval - $time_since_last) * 1000000);
}
$args = array(
'timeout' => 10, // Reduced timeout to prevent hanging
'redirection' => 3, // Limit redirects
'headers' => $this->get_platform_headers($platform)
);
// Add authentication if available
$token = $this->get_platform_token($platform);
if (!empty($token)) {
$args['headers']['Authorization'] = $this->get_auth_header($platform, $token);
}
$response = wp_remote_get($url, $args);
if (is_wp_error($response)) {
return $response;
}
$status_code = wp_remote_retrieve_response_code($response);
if ($status_code !== 200) {
return new WP_Error('api_error', 'API returned status: ' . $status_code);
}
$last_request_time = microtime(true);
return wp_remote_retrieve_body($response);
}
/**
* Get platform-specific headers
*/
private function get_platform_headers($platform) {
switch ($platform) {
case 'github':
return array(
'Accept' => 'application/vnd.github.v3+json',
'User-Agent' => 'WordPress/GitHub-Code-Viewer'
);
case 'gitlab':
return array(
'Accept' => 'application/json',
'User-Agent' => 'WordPress/GitHub-Code-Viewer'
);
case 'bitbucket':
return array(
'Accept' => 'application/json',
'User-Agent' => 'WordPress/GitHub-Code-Viewer'
);
case 'codeberg':
return array(
'Accept' => 'application/json',
'User-Agent' => 'WordPress/GitHub-Code-Viewer'
);
default:
return array(
'User-Agent' => 'WordPress/GitHub-Code-Viewer'
);
}
}
/**
* Get platform token if configured
*/
private function get_platform_token($platform) {
// Use secure token manager if available
if (class_exists('MCB_Token_Manager')) {
return MCB_Token_Manager::get_token($platform);
}
// Fallback to direct option (for backwards compatibility)
return get_option('mcb_' . $platform . '_token', '');
}
/**
* Get authorization header format for platform
*/
private function get_auth_header($platform, $token) {
switch ($platform) {
case 'github':
return 'token ' . $token;
case 'gitlab':
return 'Bearer ' . $token;
case 'bitbucket':
return 'Bearer ' . $token;
case 'codeberg':
return 'token ' . $token;
default:
return 'Bearer ' . $token;
}
}
/**
* Parse GitHub/Codeberg repository contents
*/
private function parse_github_contents($response, $repo, $current_path = '') {
$items = json_decode($response, true);
$files = array();
if (!is_array($items)) {
return $files;
}
// Add parent directory navigation if not at root
if (!empty($current_path)) {
$parent_path = dirname($current_path);
if ($parent_path === '.') {
$parent_path = '';
}
$files[] = array(
'name' => '..',
'path' => $parent_path,
'size' => 0,
'type' => 'parent',
'is_folder' => true,
'url' => ''
);
}
$code_extensions = $this->get_code_extensions();
// First add all directories
foreach ($items as $item) {
if ($item['type'] === 'dir') {
$files[] = array(
'name' => $item['name'],
'path' => $item['path'],
'size' => 0,
'type' => 'folder',
'is_folder' => true,
'url' => $item['html_url']
);
}
}
// Then add all files
foreach ($items as $item) {
if ($item['type'] === 'file') {
$extension = strtolower(pathinfo($item['name'], PATHINFO_EXTENSION));
$name_lower = strtolower($item['name']);
// Include all files, not just code files, for better browsing
$files[] = array(
'name' => $item['name'],
'path' => $item['path'],
'size' => $item['size'],
'type' => $this->get_file_type($item['name']),
'is_folder' => false,
'url' => $item['html_url']
);
}
}
return $files;
}
/**
* Parse GitLab repository contents
*/
private function parse_gitlab_contents($response) {
$items = json_decode($response, true);
$files = array();
if (!is_array($items)) {
return $files;
}
$code_extensions = $this->get_code_extensions();
foreach ($items as $item) {
if ($item['type'] === 'blob') { // GitLab uses 'blob' for files
$extension = strtolower(pathinfo($item['name'], PATHINFO_EXTENSION));
$name_lower = strtolower($item['name']);
if (in_array($extension, $code_extensions) || $this->is_code_file($name_lower)) {
$files[] = array(
'name' => $item['name'],
'path' => $item['path'],
'size' => 0, // GitLab doesn't provide size in tree API
'type' => $this->get_file_type($item['name']),
'url' => '' // Will need to construct if needed
);
}
}
}
return $files;
}
/**
* Parse Bitbucket repository contents
*/
private function parse_bitbucket_contents($response, $repo) {
$data = json_decode($response, true);
$files = array();
if (!isset($data['values']) || !is_array($data['values'])) {
return $files;
}
$code_extensions = $this->get_code_extensions();
foreach ($data['values'] as $item) {
if ($item['type'] === 'commit_file') {
$extension = strtolower(pathinfo($item['path'], PATHINFO_EXTENSION));
$name_lower = strtolower(basename($item['path']));
if (in_array($extension, $code_extensions) || $this->is_code_file($name_lower)) {
$files[] = array(
'name' => basename($item['path']),
'path' => $item['path'],
'size' => isset($item['size']) ? $item['size'] : 0,
'type' => $this->get_file_type(basename($item['path'])),
'url' => 'https://bitbucket.org/' . $repo . '/src/master/' . $item['path']
);
}
}
}
return $files;
}
/**
* Get list of code file extensions
*/
private function get_code_extensions() {
return array(
'php', 'js', 'jsx', 'ts', 'tsx', 'py', 'rb', 'java', 'c', 'cpp', 'cc', 'cxx',
'h', 'hpp', 'cs', 'swift', 'kt', 'go', 'rs', 'scala', 'r', 'sql', 'sh', 'bash',
'yml', 'yaml', 'json', 'xml', 'html', 'css', 'scss', 'sass', 'less',
'md', 'markdown', 'txt', 'ini', 'conf', 'dockerfile', 'makefile',
'vue', 'svelte', 'elm', 'clj', 'ex', 'exs', 'erl', 'hrl', 'lua', 'pl', 'pm'
);
}
/**
* Check if filename indicates a code file
*/
private function is_code_file($filename) {
$code_files = array(
'dockerfile', 'makefile', '.gitignore', '.env', '.htaccess',
'gemfile', 'rakefile', 'gulpfile', 'gruntfile', 'webpack.config.js',
'package.json', 'composer.json', 'cargo.toml', 'go.mod'
);
return in_array($filename, $code_files);
}
/**
* Get file type based on extension
*/
private function get_file_type($filename) {
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$name_lower = strtolower($filename);
$type_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',
'css' => 'css',
'scss' => 'scss',
'sass' => 'sass',
'less' => 'less',
'md' => 'markdown',
'markdown' => 'markdown'
);
// Check special file names
if ($name_lower === 'dockerfile') {
return 'docker';
}
if ($name_lower === 'makefile') {
return 'makefile';
}
return isset($type_map[$extension]) ? $type_map[$extension] : 'plain';
}
/**
* Validate if repository exists
*/
public function validate_repository_exists($repo_string) {
$platform_info = $this->detect_platform($repo_string);
$platform = $platform_info['platform'];
$repo = $platform_info['repo'];
// Validate format first
if (!preg_match($this->platforms[$platform]['repo_pattern'], $repo)) {
return new WP_Error('invalid_repo', 'Invalid repository format');
}
// Check cache first
$cache_key = 'mcb_repo_valid_' . $platform . '_' . md5($repo);
$cached_result = get_transient($cache_key);
if ($cached_result !== false) {
return $cached_result;
}
// Check with platform API
$exists = false;
switch ($platform) {
case 'github':
$url = $this->platforms['github']['api_base'] . '/repos/' . $repo;
break;
case 'gitlab':
$project_id = urlencode($repo);
$url = $this->platforms['gitlab']['api_base'] . '/projects/' . $project_id;
break;
case 'bitbucket':
$url = $this->platforms['bitbucket']['api_base'] . '/repositories/' . $repo;
break;
case 'codeberg':
$url = $this->platforms['codeberg']['api_base'] . '/repos/' . $repo;
break;
default:
return new WP_Error('unsupported_platform', 'Unsupported platform');
}
$response = $this->make_api_request($url, $platform);
if (!is_wp_error($response)) {
$data = json_decode($response, true);
if (isset($data['id']) || isset($data['uuid']) || isset($data['name'])) {
$exists = true;
}
}
if ($exists) {
set_transient($cache_key, true, 3600);
return true;
}
return new WP_Error('repo_not_found', 'Repository not found or is private');
}
/**
* Get platform display name
*/
public function get_platform_name($repo_string) {
$platform_info = $this->detect_platform($repo_string);
return $this->platforms[$platform_info['platform']]['name'];
}
/**
* Get repository web URL
*/
public function get_repository_url($repo_string) {
$platform_info = $this->detect_platform($repo_string);
$platform = $platform_info['platform'];
$repo = $platform_info['repo'];
return $this->platforms[$platform]['web_base'] . '/' . $repo;
}
}

View file

@ -0,0 +1,324 @@
<?php
/**
* Token Manager Class
* Handles secure token storage with encryption
*/
// Prevent direct access
if (!defined('WPINC')) {
die('Direct access not permitted.');
}
if (!defined('ABSPATH')) {
exit;
}
class MCB_Token_Manager {
/**
* Check if encryption is available
*/
public static function encryption_available() {
return function_exists('openssl_encrypt') && function_exists('openssl_decrypt');
}
/**
* Encrypt token for secure storage
*/
public static function encrypt_token($token) {
if (empty($token)) {
return '';
}
// Use WordPress salts for encryption
if (self::encryption_available()) {
$key = substr(hash('sha256', wp_salt('auth')), 0, 32);
$iv = substr(hash('sha256', wp_salt('secure')), 0, 16);
$encrypted = openssl_encrypt($token, 'AES-256-CBC', $key, 0, $iv);
if ($encrypted !== false) {
return base64_encode($encrypted);
}
}
// Fallback to base64 if encryption not available
return base64_encode($token);
}
/**
* Decrypt token for use
*/
public static function decrypt_token($encrypted_token) {
if (empty($encrypted_token)) {
return '';
}
// Use WordPress salts for decryption
if (self::encryption_available()) {
$key = substr(hash('sha256', wp_salt('auth')), 0, 32);
$iv = substr(hash('sha256', wp_salt('secure')), 0, 16);
$decoded = base64_decode($encrypted_token);
if ($decoded !== false) {
$decrypted = openssl_decrypt($decoded, 'AES-256-CBC', $key, 0, $iv);
if ($decrypted !== false) {
return $decrypted;
}
}
}
// Fallback to base64 if encryption not available
return base64_decode($encrypted_token);
}
/**
* Store encrypted token
*/
public static function store_token($platform, $token) {
if (empty($token)) {
delete_option('mcb_' . $platform . '_token_encrypted');
return true;
}
$encrypted = self::encrypt_token($token);
return update_option('mcb_' . $platform . '_token_encrypted', $encrypted);
}
/**
* Retrieve and decrypt token
*/
public static function get_token($platform) {
$encrypted = get_option('mcb_' . $platform . '_token_encrypted', '');
if (empty($encrypted)) {
// Check for legacy unencrypted token
$legacy = get_option('mcb_' . $platform . '_token', '');
if (!empty($legacy)) {
// Migrate to encrypted storage
self::store_token($platform, $legacy);
delete_option('mcb_' . $platform . '_token');
return $legacy;
}
return '';
}
return self::decrypt_token($encrypted);
}
/**
* Remove all tokens (for privacy/GDPR)
*/
public static function remove_all_tokens() {
$platforms = array('github', 'gitlab', 'bitbucket', 'codeberg');
foreach ($platforms as $platform) {
delete_option('mcb_' . $platform . '_token');
delete_option('mcb_' . $platform . '_token_encrypted');
}
}
}
/**
* Privacy Manager Class
* Handles GDPR compliance and privacy features
*/
class MCB_Privacy_Manager {
/**
* Initialize privacy features
*/
public static function init() {
// WordPress privacy policy content
add_action('admin_init', array(__CLASS__, 'privacy_policy_content'));
// Data exporter for GDPR
add_filter('wp_privacy_personal_data_exporters', array(__CLASS__, 'register_data_exporter'));
// Data eraser for GDPR
add_filter('wp_privacy_personal_data_erasers', array(__CLASS__, 'register_data_eraser'));
}
/**
* Check if privacy mode is enabled
*/
public static function is_privacy_mode() {
return get_option('mcb_privacy_mode', false);
}
/**
* Add suggested privacy policy content
*/
public static function privacy_policy_content() {
if (!function_exists('wp_add_privacy_policy_content')) {
return;
}
$content = '
<h3>Git Code Viewer Plugin</h3>
<p>When you visit pages that display code repositories using the Git Code Viewer plugin:</p>
<ul>
<li>We temporarily store an anonymized version of your IP address (for 60 seconds) to prevent abuse through rate limiting.</li>
<li>If security events occur, we may log your browser user agent (for 7 days) for security monitoring.</li>
<li>We do not use cookies or tracking technologies.</li>
<li>We only access publicly available repository data from GitHub, GitLab, Bitbucket, and Codeberg.</li>
<li>No personal information is shared with third parties.</li>
<li>All data is automatically deleted after the retention period.</li>
</ul>
<p>If Privacy Mode is enabled, no IP addresses or user agents are collected.</p>
';
wp_add_privacy_policy_content(
'Git Code Viewer',
wp_kses_post($content)
);
}
/**
* Register data exporter for GDPR requests
*/
public static function register_data_exporter($exporters) {
$exporters['git-code-viewer'] = array(
'exporter_friendly_name' => __('Git Code Viewer Plugin'),
'callback' => array(__CLASS__, 'data_exporter')
);
return $exporters;
}
/**
* Export user data for GDPR requests
*/
public static function data_exporter($email_address, $page = 1) {
$export_items = array();
// Get security logs that might contain this user's data
$logs = get_transient('mcb_security_logs');
if (is_array($logs)) {
$user_data = array();
// Note: We don't store email addresses, so we can't directly match
// This is actually good for privacy!
$user_data[] = array(
'name' => 'Security Logs Notice',
'value' => 'The Git Code Viewer plugin does not store email addresses. Any security logs contain only anonymized IP addresses and user agents.'
);
$export_items[] = array(
'group_id' => 'git-code-viewer',
'group_label' => 'Git Code Viewer Data',
'item_id' => 'gcv-notice',
'data' => $user_data
);
}
return array(
'data' => $export_items,
'done' => true
);
}
/**
* Register data eraser for GDPR requests
*/
public static function register_data_eraser($erasers) {
$erasers['git-code-viewer'] = array(
'eraser_friendly_name' => __('Git Code Viewer Plugin'),
'callback' => array(__CLASS__, 'data_eraser')
);
return $erasers;
}
/**
* Erase user data for GDPR requests
*/
public static function data_eraser($email_address, $page = 1) {
// Since we don't store email addresses or persistent user data,
// we can only clear all transient data if requested
if ($page === 1) {
// Clear all security logs
delete_transient('mcb_security_logs');
// Clear all rate limit transients (they expire in 60 seconds anyway)
global $wpdb;
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mcb_rate_%'");
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mcb_rate_%'");
}
return array(
'items_removed' => true,
'items_retained' => false,
'messages' => array('All temporary Git Code Viewer data has been removed.'),
'done' => true
);
}
/**
* Get anonymized IP for privacy compliance
*/
public static function get_anonymized_ip($ip) {
if (self::is_privacy_mode()) {
return 'privacy-mode';
}
// Anonymize IP by removing last octet for IPv4 or last 80 bits for IPv6
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$parts = explode('.', $ip);
$parts[3] = '0';
return implode('.', $parts);
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
// Zero out last 80 bits
return substr($ip, 0, strrpos($ip, ':')) . ':0:0:0:0:0';
}
return 'unknown';
}
/**
* Clean up old data automatically
*/
public static function cleanup_old_data() {
// This is called on a daily schedule
global $wpdb;
// Remove expired rate limit transients
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mcb_%' AND option_value < UNIX_TIMESTAMP()");
// Remove orphaned transients (fixed MySQL compatibility issue)
// First get all valid transient names
$valid_transients = $wpdb->get_col(
"SELECT CONCAT('_transient_', SUBSTRING(option_name, 20))
FROM {$wpdb->options}
WHERE option_name LIKE '_transient_timeout_mcb_%'"
);
if (!empty($valid_transients)) {
// Build a safe IN clause
$placeholders = array_fill(0, count($valid_transients), '%s');
$in_clause = implode(',', $placeholders);
// Delete orphaned transients
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE '_transient_mcb_%'
AND option_name NOT IN ($in_clause)",
$valid_transients
)
);
} else {
// No valid transients with timeouts, remove all MCB transients
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mcb_%'");
}
// Also clean up expired timeout options
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mcb_%' AND option_value < UNIX_TIMESTAMP()");
}
}
// Initialize privacy features
MCB_Privacy_Manager::init();
// Schedule cleanup (weekly is sufficient for transient cleanup)
if (!wp_next_scheduled('mcb_privacy_cleanup')) {
wp_schedule_event(time(), 'weekly', 'mcb_privacy_cleanup');
}
add_action('mcb_privacy_cleanup', array('MCB_Privacy_Manager', 'cleanup_old_data'));

View file

@ -0,0 +1,385 @@
<?php
/**
* Critical Security Fixes and Plugin Compatibility
* Implementation of audit recommendations
*/
// Prevent direct access
if (!defined('WPINC')) {
die('Direct access not permitted.');
}
if (!defined('ABSPATH')) {
exit;
}
class MCB_Security_Fixes {
/**
* Initialize security fixes and compatibility patches
*/
public static function init() {
// Fix 1: SSRF DNS Rebinding Protection
add_filter('mcb_validate_api_url', array(__CLASS__, 'validate_api_url_dns'), 10, 2);
// Fix 2: Wordfence Compatibility
add_action('init', array(__CLASS__, 'wordfence_compatibility'));
// Fix 3: Error Suppression in Production
add_action('init', array(__CLASS__, 'suppress_errors_production'));
// Fix 4: Token Autoloading Fix
add_filter('mcb_update_option', array(__CLASS__, 'disable_autoload'), 10, 3);
// Fix 5: User Agent Hashing
add_filter('mcb_log_user_agent', array(__CLASS__, 'hash_user_agent'));
// Fix 6: Plugin Conflict Detection
add_action('admin_notices', array(__CLASS__, 'compatibility_notices'));
// Fix 7: Performance Optimization
add_action('init', array(__CLASS__, 'optimize_loading'));
}
/**
* CRITICAL FIX: SSRF DNS Rebinding Protection
* Prevents accessing internal network resources
*/
public static function validate_api_url_dns($valid, $url) {
$parsed = parse_url($url);
if (!$parsed || !isset($parsed['host'])) {
return false;
}
$host = $parsed['host'];
// Whitelist of allowed API hosts
$allowed_hosts = array(
'api.github.com',
'gitlab.com',
'api.bitbucket.org',
'codeberg.org'
);
if (!in_array($host, $allowed_hosts, true)) {
return false;
}
// Get IP address of the host
$ip = gethostbyname($host);
// If resolution failed, block
if ($ip === $host) {
error_log('GCV Security: DNS resolution failed for ' . $host);
return false;
}
// Check for private and reserved IP ranges
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
error_log('GCV Security: Blocked private/reserved IP ' . $ip . ' for host ' . $host);
return false;
}
// Additional check for IPv6
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
// Check for IPv6 private ranges
$private_ranges = array(
'fc00::/7', // Unique local addresses
'fe80::/10', // Link-local addresses
'::1/128', // Loopback
'::/128', // Unspecified
'::ffff:0:0/96' // IPv4-mapped addresses
);
foreach ($private_ranges as $range) {
if (self::ip_in_range($ip, $range)) {
error_log('GCV Security: Blocked private IPv6 ' . $ip);
return false;
}
}
}
return true;
}
/**
* Wordfence Compatibility Layer
*/
public static function wordfence_compatibility() {
if (!defined('WORDFENCE_VERSION')) {
return;
}
// Add API URLs to Wordfence whitelist
if (class_exists('wfConfig')) {
$whitelisted = wfConfig::get('whitelisted');
$whitelist_urls = array(
'api.github.com',
'gitlab.com',
'api.bitbucket.org',
'codeberg.org'
);
foreach ($whitelist_urls as $url) {
if (strpos($whitelisted, $url) === false) {
$whitelisted .= "\n" . $url;
}
}
wfConfig::set('whitelisted', $whitelisted);
}
// Disable plugin rate limiting if Wordfence is active
if (has_filter('mcb_enable_rate_limiting')) {
add_filter('mcb_enable_rate_limiting', '__return_false');
}
// Add Wordfence compatibility headers
add_action('mcb_before_api_request', function() {
if (function_exists('wfUtils::doNotCache')) {
wfUtils::doNotCache();
}
});
}
/**
* Suppress errors in production environments
*/
public static function suppress_errors_production() {
// Only in production (non-debug mode)
if (defined('WP_DEBUG') && WP_DEBUG) {
return;
}
// Suppress PHP errors in AJAX handlers
if (wp_doing_ajax()) {
@error_reporting(0);
@ini_set('display_errors', '0');
@ini_set('display_startup_errors', '0');
// Set custom error handler
set_error_handler(function($errno, $errstr, $errfile, $errline) {
// Log to error log but don't display
if (WP_DEBUG_LOG) {
error_log(sprintf(
'GCV Error: %s in %s on line %d',
$errstr,
$errfile,
$errline
));
}
return true; // Prevent default error handler
});
}
}
/**
* Fix token autoloading issue
*/
public static function disable_autoload($value, $option, $autoload) {
// Disable autoload for token options
if (strpos($option, 'mcb_') === 0 && strpos($option, '_token') !== false) {
return 'no';
}
return $autoload;
}
/**
* Hash user agents for privacy
*/
public static function hash_user_agent($user_agent) {
if (empty($user_agent)) {
return 'unknown';
}
// Use a consistent salt for hashing
$salt = wp_salt('auth');
// Create a hash that's consistent but not reversible
return substr(hash('sha256', $salt . $user_agent), 0, 16);
}
/**
* Show compatibility notices
*/
public static function compatibility_notices() {
$notices = array();
// Wordfence compatibility notice
if (defined('WORDFENCE_VERSION')) {
$notices[] = array(
'type' => 'warning',
'message' => __('Git Code Viewer: Wordfence detected. Please ensure the following URLs are whitelisted in Wordfence settings: api.github.com, gitlab.com, api.bitbucket.org, codeberg.org', 'maple-code-blocks'),
'dismissible' => true
);
}
// WooCommerce compatibility (informational)
if (class_exists('WooCommerce')) {
// No issues, just log for debugging
error_log('GCV: WooCommerce detected - compatibility mode active');
}
// LearnDash compatibility (informational)
if (defined('LEARNDASH_VERSION')) {
// No issues, just log for debugging
error_log('GCV: LearnDash detected - compatibility mode active');
}
// Memory limit warning
$memory_limit = ini_get('memory_limit');
if ($memory_limit && self::convert_to_bytes($memory_limit) < 67108864) { // 64MB
$notices[] = array(
'type' => 'warning',
'message' => __('Git Code Viewer: Low memory limit detected. Consider increasing memory_limit to at least 64MB for optimal performance.', 'maple-code-blocks'),
'dismissible' => true
);
}
// Display notices
foreach ($notices as $notice) {
$dismissible = $notice['dismissible'] ? 'is-dismissible' : '';
printf(
'<div class="notice notice-%s %s"><p>%s</p></div>',
esc_attr($notice['type']),
esc_attr($dismissible),
esc_html($notice['message'])
);
}
}
/**
* Optimize plugin loading
*/
public static function optimize_loading() {
// Hook into admin_init for screen-specific optimizations
if (is_admin()) {
add_action('admin_init', function() {
// Check screen only after it's available
add_action('current_screen', function($screen) {
if ($screen && $screen->id !== 'settings_page_maple-code-blocks') {
remove_action('admin_enqueue_scripts', array('Maple_Code_Blocks', 'enqueue_scripts'));
}
});
});
}
// Optimize transient cleanup
if (!wp_next_scheduled('mcb_cleanup_transients')) {
wp_schedule_event(time(), 'daily', 'mcb_cleanup_transients');
}
add_action('mcb_cleanup_transients', function() {
global $wpdb;
// Use WordPress function instead of direct query
if (function_exists('delete_expired_transients')) {
delete_expired_transients();
} else {
// Fallback for older WordPress versions
$wpdb->query(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE '_transient_timeout_mcb_%'
AND option_value < UNIX_TIMESTAMP()"
);
}
});
}
/**
* Helper: Check if IP is in range
*/
private static function ip_in_range($ip, $range) {
if (strpos($range, '/') === false) {
$range .= '/32';
}
list($subnet, $bits) = explode('/', $range);
// IPv4
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) &&
filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$ip = ip2long($ip);
$subnet = ip2long($subnet);
$mask = -1 << (32 - $bits);
$subnet &= $mask;
return ($ip & $mask) == $subnet;
}
// IPv6
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) &&
filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$ip_bin = inet_pton($ip);
$subnet_bin = inet_pton($subnet);
$bytes = $bits / 8;
$remainder = $bits % 8;
if ($bytes > 0) {
if (substr($ip_bin, 0, $bytes) !== substr($subnet_bin, 0, $bytes)) {
return false;
}
}
if ($remainder > 0 && $bytes < 16) {
$mask = 0xFF << (8 - $remainder);
$ip_byte = ord($ip_bin[$bytes]);
$subnet_byte = ord($subnet_bin[$bytes]);
if (($ip_byte & $mask) !== ($subnet_byte & $mask)) {
return false;
}
}
return true;
}
return false;
}
/**
* Helper: Convert memory string to bytes
*/
private static function convert_to_bytes($val) {
$val = trim($val);
$last = strtolower($val[strlen($val)-1]);
$val = (int)$val;
switch($last) {
case 'g':
$val *= 1024;
case 'm':
$val *= 1024;
case 'k':
$val *= 1024;
}
return $val;
}
}
// Initialize security fixes
add_action('plugins_loaded', array('MCB_Security_Fixes', 'init'), 5);
add_action('rest_api_init', function() {
register_rest_route('maple-code-blocks/v1', '/health', array(
'methods' => 'GET',
'callback' => function() {
$health = array(
'status' => 'healthy',
'version' => '2.0.0',
'php_version' => PHP_VERSION,
'wordpress_version' => get_bloginfo('version'),
'memory_usage' => memory_get_usage(true),
'memory_limit' => ini_get('memory_limit'),
'wordfence_active' => defined('WORDFENCE_VERSION'),
'woocommerce_active' => class_exists('WooCommerce'),
'learndash_active' => defined('LEARNDASH_VERSION'),
'cache_size' => count(get_transient('mcb_security_logs') ?: array()),
'rate_limit_active' => !defined('WORDFENCE_VERSION')
);
return new WP_REST_Response($health, 200);
},
'permission_callback' => function() {
return current_user_can('manage_options');
}
));
});

View file

@ -0,0 +1,322 @@
<?php
/**
* Security Handler Class
* Implements OWASP security best practices
*/
// Prevent direct access
if (!defined('WPINC')) {
die('Direct access not permitted.');
}
if (!defined('ABSPATH')) {
exit;
}
class MCB_Security {
/**
* Initialize security features
*/
public static function init() {
// Add security headers
add_action('send_headers', array(__CLASS__, 'add_security_headers'));
// Add Content Security Policy
add_action('wp_head', array(__CLASS__, 'add_csp_meta'));
// Note: Global input sanitization removed - sanitize data at point of use instead
// This prevents conflicts with other plugins (e.g., WPForms)
}
/**
* Add security headers
*/
public static function add_security_headers() {
// Only add headers on pages with our plugin
if (!self::is_plugin_active_on_page()) {
return;
}
// X-Content-Type-Options
header('X-Content-Type-Options: nosniff');
// X-Frame-Options
header('X-Frame-Options: SAMEORIGIN');
// X-XSS-Protection (legacy but still useful)
header('X-XSS-Protection: 1; mode=block');
// Referrer Policy
header('Referrer-Policy: strict-origin-when-cross-origin');
}
/**
* Add Content Security Policy meta tag
*/
public static function add_csp_meta() {
if (!self::is_plugin_active_on_page()) {
return;
}
// Strict CSP for code viewer areas
$csp = "default-src 'self'; " .
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " . // Needed for Prism.js
"style-src 'self' 'unsafe-inline'; " . // Needed for inline styles
"img-src 'self' data: https:; " .
"connect-src 'self'; " .
"font-src 'self' data:; " .
"object-src 'none'; " .
"base-uri 'self'; " .
"form-action 'self'; " .
"frame-ancestors 'self';";
echo '<meta http-equiv="Content-Security-Policy" content="' . esc_attr($csp) . '">' . "\n";
}
/**
* Check if plugin is active on current page
*/
private static function is_plugin_active_on_page() {
global $post;
if (!is_a($post, 'WP_Post')) {
return false;
}
return has_shortcode($post->post_content, 'maple_code_block');
}
/**
* Sanitize global input arrays
*/
public static function sanitize_global_input() {
// Sanitize $_GET
if (!empty($_GET)) {
$_GET = self::sanitize_array($_GET);
}
// Sanitize $_POST
if (!empty($_POST)) {
$_POST = self::sanitize_array($_POST);
}
// Sanitize $_REQUEST
if (!empty($_REQUEST)) {
$_REQUEST = self::sanitize_array($_REQUEST);
}
// Sanitize $_COOKIE
if (!empty($_COOKIE)) {
$_COOKIE = self::sanitize_array($_COOKIE);
}
}
/**
* Recursively sanitize array
*/
private static function sanitize_array($array) {
foreach ($array as $key => $value) {
if (is_array($value)) {
$array[$key] = self::sanitize_array($value);
} else {
// Remove null bytes
$value = str_replace(chr(0), '', $value);
// Strip tags and encode special chars
$array[$key] = htmlspecialchars(strip_tags($value), ENT_QUOTES, 'UTF-8');
}
}
return $array;
}
/**
* Validate repository format
* Supports: owner/repo, platform:owner/repo, or full URLs
*/
public static function validate_repo_format($repo) {
// Remove any whitespace
$repo = trim($repo);
// If it's a full URL, validate it
if (strpos($repo, 'https://') === 0 || strpos($repo, 'http://') === 0) {
// Check if it's from a supported platform
$supported_domains = array(
'github.com',
'gitlab.com',
'bitbucket.org',
'codeberg.org'
);
$parsed = parse_url($repo);
if (!$parsed || !isset($parsed['host'])) {
return false;
}
$domain_valid = false;
foreach ($supported_domains as $domain) {
if (strpos($parsed['host'], $domain) !== false) {
$domain_valid = true;
break;
}
}
if (!$domain_valid) {
return false;
}
// Extract path and validate format
if (!isset($parsed['path'])) {
return false;
}
$path = trim($parsed['path'], '/');
$parts = explode('/', $path);
// Need at least owner/repo
if (count($parts) < 2) {
return false;
}
// Validate owner and repo names
$owner = $parts[0];
$repo_name = $parts[1];
if (!preg_match('/^[a-zA-Z0-9\-_]+$/', $owner) ||
!preg_match('/^[a-zA-Z0-9\-_\.]+$/', $repo_name)) {
return false;
}
return true;
}
// Check for platform prefix (e.g., gitlab:owner/repo)
if (strpos($repo, ':') !== false) {
list($platform, $repo_path) = explode(':', $repo, 2);
// Validate platform
$valid_platforms = array('github', 'gitlab', 'bitbucket', 'codeberg');
if (!in_array($platform, $valid_platforms)) {
return false;
}
// Validate repo path
if (!preg_match('/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/', $repo_path)) {
return false;
}
return true;
}
// Standard format: owner/repo
if (!preg_match('/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/', $repo)) {
return false;
}
// Check length limits
$parts = explode('/', $repo);
if (count($parts) !== 2) {
return false;
}
// Owner: 1-39 characters (GitHub limit)
if (strlen($parts[0]) < 1 || strlen($parts[0]) > 39) {
return false;
}
// Repo name: 1-100 characters (GitHub limit)
if (strlen($parts[1]) < 1 || strlen($parts[1]) > 100) {
return false;
}
return true;
}
/**
* Validate file path
*/
public static function validate_file_path($path) {
// Remove leading/trailing slashes
$path = trim($path, '/');
// Check for path traversal attempts
$dangerous_patterns = array(
'..',
'//',
'\\',
'.git',
'.env',
'wp-config',
'.htaccess',
'.htpasswd'
);
foreach ($dangerous_patterns as $pattern) {
if (stripos($path, $pattern) !== false) {
return false;
}
}
// Only allow safe characters
if (!preg_match('/^[a-zA-Z0-9\-_\.\/]+$/', $path)) {
return false;
}
// Check path depth (max 10 levels)
if (substr_count($path, '/') > 10) {
return false;
}
return true;
}
/**
* Generate secure random token
*/
public static function generate_token($length = 32) {
if (function_exists('random_bytes')) {
return bin2hex(random_bytes($length));
} elseif (function_exists('openssl_random_pseudo_bytes')) {
return bin2hex(openssl_random_pseudo_bytes($length));
} else {
// Fallback to less secure method
return wp_generate_password($length * 2, false, false);
}
}
/**
* Log security events
*/
public static function log_security_event($event_type, $details = array()) {
$log_entry = array(
'timestamp' => current_time('mysql'),
'event_type' => $event_type,
'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
'details' => $details
);
// Store in WordPress transient for review (expires in 7 days)
$logs = get_transient('mcb_security_logs');
if (!is_array($logs)) {
$logs = array();
}
// Keep only last 100 entries
if (count($logs) >= 100) {
array_shift($logs);
}
$logs[] = $log_entry;
set_transient('mcb_security_logs', $logs, 7 * DAY_IN_SECONDS);
// For critical events, also log to error log
if (in_array($event_type, array('invalid_nonce', 'rate_limit_exceeded', 'path_traversal_attempt'))) {
error_log('GCV Security Event: ' . json_encode($log_entry));
}
}
}
// Initialize security features
MCB_Security::init();

View file

@ -0,0 +1,190 @@
<?php
/**
* Shortcode Handler Class
* Manages the [github_code_viewer] shortcode
*/
// Prevent direct access
if (!defined('WPINC')) {
die('Direct access not permitted.');
}
if (!defined('ABSPATH')) {
exit;
}
class MCB_Shortcode {
/**
* Initialize shortcode
*/
public static function init() {
add_shortcode('maple_code_block', array(__CLASS__, 'render_shortcode'));
}
/**
* Render the shortcode
*/
public static function render_shortcode($atts) {
// Parse shortcode attributes
$atts = shortcode_atts(array(
'repo' => '',
'theme' => 'dark',
'height' => '600px',
'show_line_numbers' => 'true',
'initial_file' => '',
'title' => ''
), $atts, 'github_code_viewer');
// Validate repository
if (empty($atts['repo'])) {
return '<div class="mcb-error">Error: No repository specified. Use repo="owner/repository"</div>';
}
// Sanitize attributes
$repo = sanitize_text_field($atts['repo']);
$theme = in_array($atts['theme'], array('dark', 'light', 'monokai', 'solarized')) ? $atts['theme'] : 'dark';
// Validate height - only allow specific units
$height = sanitize_text_field($atts['height']);
if (!preg_match('/^\d+(px|%|em|rem|vh)$/', $height)) {
$height = '600px'; // Default to safe value if invalid
}
$show_line_numbers = filter_var($atts['show_line_numbers'], FILTER_VALIDATE_BOOLEAN);
$initial_file = sanitize_text_field($atts['initial_file']);
$title = sanitize_text_field($atts['title']);
// Detect platform from repo string
$platform = 'github'; // default
$display_repo = $repo;
if (strpos($repo, ':') !== false) {
list($platform, $display_repo) = explode(':', $repo, 2);
} elseif (strpos($repo, 'https://') === 0 || strpos($repo, 'http://') === 0) {
// Parse URL to detect platform
if (strpos($repo, 'gitlab.com') !== false) {
$platform = 'gitlab';
} elseif (strpos($repo, 'bitbucket.org') !== false) {
$platform = 'bitbucket';
} elseif (strpos($repo, 'codeberg.org') !== false) {
$platform = 'codeberg';
}
// Extract repo name from URL
$parsed = parse_url($repo);
if ($parsed && isset($parsed['path'])) {
$display_repo = trim($parsed['path'], '/');
}
}
// Generate unique ID for this instance
$viewer_id = 'mcb-' . uniqid();
// Build the viewer HTML
ob_start();
?>
<div id="<?php echo esc_attr($viewer_id); ?>" class="maple-code-blocks mcb-theme-<?php echo esc_attr($theme); ?>"
data-repo="<?php echo esc_attr($repo); ?>"
data-theme="<?php echo esc_attr($theme); ?>"
data-show-line-numbers="<?php echo $show_line_numbers ? 'true' : 'false'; ?>"
data-initial-file="<?php echo esc_attr($initial_file); ?>"
style="height: <?php echo esc_attr($height); ?>;">
<div class="mcb-header">
<?php if (!empty($title)) : ?>
<h3 class="mcb-title"><?php echo esc_html($title); ?></h3>
<?php endif; ?>
<div class="mcb-repo-info">
<?php
// Display platform-specific icon
switch($platform) {
case 'gitlab':
?>
<svg class="mcb-platform-icon" viewBox="0 0 24 24" width="20" height="20">
<path fill="#FC6D26" d="M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51L23 13.45a.84.84 0 0 1-.35.94z"/>
</svg>
<?php
break;
case 'bitbucket':
?>
<svg class="mcb-platform-icon" viewBox="0 0 24 24" width="20" height="20">
<path fill="#0052CC" d="M3.28 2.42a1 1 0 00-.97.8l-2.3 14.3a1.35 1.35 0 00.78 1.51l9.84 4.13c.36.15.77.15 1.13 0l9.86-4.13a1.35 1.35 0 00.78-1.5l-2.3-14.31a1 1 0 00-.97-.8H3.28zm8.3 12.66h-3.8l-1-6.4h5.56l.75 6.4z"/>
</svg>
<?php
break;
case 'codeberg':
?>
<svg class="mcb-platform-icon" viewBox="0 0 24 24" width="20" height="20">
<path fill="#2185D0" d="M12 2C6.48 2 2 6.48 2 12c0 4.42 2.87 8.17 6.84 9.49.5.09.68-.22.68-.48 0-.24-.01-1.02-.01-1.85-2.78.6-3.37-1.18-3.37-1.18-.45-1.15-1.11-1.46-1.11-1.46-.91-.62.07-.61.07-.61 1 .07 1.53 1.03 1.53 1.03.89 1.53 2.34 1.09 2.91.83.09-.64.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.93 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.64 0 0 .84-.27 2.75 1.03A9.58 9.58 0 0112 6.8c.85.004 1.71.115 2.51.34 1.91-1.3 2.75-1.03 2.75-1.03.55 1.37.2 2.39.1 2.64.64.7 1.03 1.59 1.03 2.68 0 3.83-2.34 4.68-4.56 4.92.36.31.68.92.68 1.85 0 1.34-.01 2.42-.01 2.75 0 .27.18.58.69.48A10.01 10.01 0 0022 12c0-5.52-4.48-10-10-10z"/>
</svg>
<?php
break;
case 'github':
default:
?>
<svg class="mcb-platform-icon" viewBox="0 0 24 24" width="20" height="20">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
<?php
break;
}
?>
<span class="mcb-repo-name"><?php echo esc_html($display_repo); ?></span>
</div>
<div class="mcb-controls">
<button class="mcb-home-btn" title="Go to root">
<svg viewBox="0 0 24 24" width="18" height="18">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
</svg>
</button>
<button class="mcb-refresh-btn" title="Refresh files">
<svg viewBox="0 0 24 24" width="18" height="18">
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
</svg>
</button>
<button class="mcb-fullscreen-btn" title="Toggle fullscreen">
<svg viewBox="0 0 24 24" width="18" height="18">
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
</svg>
</button>
</div>
</div>
<div class="mcb-content">
<div class="mcb-sidebar">
<div class="mcb-search-box">
<input type="text" class="mcb-search-input" placeholder="Search files...">
</div>
<div class="mcb-file-list">
<div class="mcb-loading">
<div class="mcb-spinner"></div>
<span>Loading repository files...</span>
</div>
</div>
</div>
<div class="mcb-editor">
<div class="mcb-tabs">
<!-- Tabs will be added dynamically -->
</div>
<div class="mcb-code-area">
<div class="mcb-welcome">
<svg viewBox="0 0 24 24" width="64" height="64">
<path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/>
</svg>
<h4>GitHub Code Viewer</h4>
<p>Select a file from the sidebar to view its content</p>
</div>
</div>
</div>
</div>
<div class="mcb-status-bar">
<span class="mcb-status-text">Ready</span>
<span class="mcb-file-info"></span>
</div>
</div>
<?php
return ob_get_clean();
}
}

View file

@ -0,0 +1,82 @@
<?php
/**
* Simplified Gutenberg Block Registration for Maple Code Blocks
*/
class MCB_Simple_Block {
public static function init() {
// Register block on init
add_action('init', array(__CLASS__, 'register_block'));
// Make sure script is enqueued in editor
add_action('enqueue_block_editor_assets', array(__CLASS__, 'enqueue_editor_assets'));
}
public static function register_block() {
// Only register if Gutenberg is available
if (!function_exists('register_block_type')) {
return;
}
// Register the block
register_block_type('maple-code-blocks/code-block', array(
'render_callback' => array(__CLASS__, 'render_block'),
'attributes' => array(
'repository' => array(
'type' => 'string',
'default' => ''
),
'theme' => array(
'type' => 'string',
'default' => 'dark'
),
'height' => array(
'type' => 'string',
'default' => '600px'
)
)
));
}
public static function enqueue_editor_assets() {
// Enqueue the block editor script
wp_enqueue_script(
'maple-code-blocks-editor',
MCB_PLUGIN_URL . 'assets/js/simple-block.js',
array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n'),
MCB_PLUGIN_VERSION,
false // Load in header, not footer
);
// Add inline script to ensure registration happens
wp_add_inline_script('maple-code-blocks-editor', '
console.log("Maple Code Blocks: Script loaded");
if (typeof wp !== "undefined" && wp.blocks) {
console.log("Maple Code Blocks: wp.blocks is available");
} else {
console.error("Maple Code Blocks: wp.blocks not available");
}
', 'after');
}
public static function render_block($attributes) {
$repository = isset($attributes['repository']) ? $attributes['repository'] : '';
$theme = isset($attributes['theme']) ? $attributes['theme'] : 'dark';
$height = isset($attributes['height']) ? $attributes['height'] : '600px';
if (empty($repository)) {
return '<div class="notice notice-warning">Please enter a repository (e.g., facebook/react)</div>';
}
// Use the shortcode for rendering
return do_shortcode(sprintf(
'[maple_code_block repo="%s" theme="%s" height="%s"]',
esc_attr($repository),
esc_attr($theme),
esc_attr($height)
));
}
}
MCB_Simple_Block::init();

View file

@ -0,0 +1,2 @@
<?php
// Silence is golden.

View file

@ -0,0 +1,398 @@
<?php
/**
* Security Audit Script for GitHub Code Viewer Plugin
* Run this to verify all security measures are in place
*/
// Prevent direct access
if (!defined('WPINC')) {
die('Direct access not permitted.');
}
if (!defined('ABSPATH')) {
exit;
}
class MCB_Security_Audit {
private $issues = array();
private $passed = array();
/**
* Run complete security audit
*/
public function run_audit() {
$this->check_file_permissions();
$this->check_index_files();
$this->check_htaccess_files();
$this->check_php_files_protection();
$this->check_input_validation();
$this->check_output_escaping();
$this->check_nonce_usage();
$this->check_capability_checks();
$this->check_ssl_usage();
$this->check_rate_limiting();
return $this->generate_report();
}
/**
* Check file permissions
*/
private function check_file_permissions() {
$plugin_dir = plugin_dir_path(__FILE__);
// Check directory permissions (should be 755 or stricter)
if (is_readable($plugin_dir)) {
$perms = fileperms($plugin_dir);
$octal = substr(sprintf('%o', $perms), -3);
if ($octal > '755') {
$this->issues[] = 'Directory permissions too permissive: ' . $octal;
} else {
$this->passed[] = 'Directory permissions OK: ' . $octal;
}
}
// Check file permissions (should be 644 or stricter)
$files = glob($plugin_dir . '*.php');
foreach ($files as $file) {
$perms = fileperms($file);
$octal = substr(sprintf('%o', $perms), -3);
if ($octal > '644') {
$this->issues[] = 'File permissions too permissive for ' . basename($file) . ': ' . $octal;
}
}
if (empty($this->issues)) {
$this->passed[] = 'All file permissions are secure';
}
}
/**
* Check for index.php files in all directories
*/
private function check_index_files() {
$plugin_dir = plugin_dir_path(__FILE__);
$directories = array(
$plugin_dir,
$plugin_dir . 'admin/',
$plugin_dir . 'includes/',
$plugin_dir . 'assets/',
$plugin_dir . 'assets/css/',
$plugin_dir . 'assets/js/'
);
foreach ($directories as $dir) {
if (is_dir($dir) && !file_exists($dir . 'index.php')) {
$this->issues[] = 'Missing index.php in ' . str_replace($plugin_dir, '', $dir);
} else {
$this->passed[] = 'index.php present in ' . str_replace($plugin_dir, '', $dir);
}
}
}
/**
* Check for .htaccess files
*/
private function check_htaccess_files() {
$plugin_dir = plugin_dir_path(__FILE__);
$required_htaccess = array(
$plugin_dir => 'root',
$plugin_dir . 'admin/' => 'admin',
$plugin_dir . 'includes/' => 'includes',
$plugin_dir . 'assets/' => 'assets'
);
foreach ($required_htaccess as $dir => $name) {
if (file_exists($dir . '.htaccess')) {
$this->passed[] = '.htaccess present in ' . $name . ' directory';
} else {
$this->issues[] = 'Missing .htaccess in ' . $name . ' directory';
}
}
}
/**
* Check PHP files have direct access protection
*/
private function check_php_files_protection() {
$plugin_dir = plugin_dir_path(__FILE__);
$php_files = $this->get_all_php_files($plugin_dir);
foreach ($php_files as $file) {
$content = file_get_contents($file);
// Skip index.php files
if (basename($file) === 'index.php') {
continue;
}
// Check for ABSPATH or WPINC checks
if (!strpos($content, 'ABSPATH') && !strpos($content, 'WPINC')) {
$this->issues[] = 'No direct access protection in ' . str_replace($plugin_dir, '', $file);
} else {
$this->passed[] = 'Direct access protected: ' . basename($file);
}
}
}
/**
* Check input validation
*/
private function check_input_validation() {
$checks = array(
'sanitize_text_field' => 'Text sanitization',
'wp_verify_nonce' => 'Nonce verification',
'esc_attr' => 'Attribute escaping',
'esc_html' => 'HTML escaping',
'esc_js' => 'JavaScript escaping',
'absint' => 'Integer validation',
'filter_var' => 'Input filtering'
);
$plugin_dir = plugin_dir_path(__FILE__);
$php_files = $this->get_all_php_files($plugin_dir);
foreach ($checks as $function => $description) {
$found = false;
foreach ($php_files as $file) {
$content = file_get_contents($file);
if (strpos($content, $function) !== false) {
$found = true;
break;
}
}
if ($found) {
$this->passed[] = $description . ' implemented (' . $function . ')';
} else {
$this->issues[] = $description . ' not found (' . $function . ')';
}
}
}
/**
* Check output escaping
*/
private function check_output_escaping() {
$escaping_functions = array(
'esc_html',
'esc_attr',
'esc_url',
'esc_js',
'htmlspecialchars'
);
$plugin_dir = plugin_dir_path(__FILE__);
$php_files = $this->get_all_php_files($plugin_dir);
$escaping_found = false;
foreach ($php_files as $file) {
$content = file_get_contents($file);
foreach ($escaping_functions as $func) {
if (strpos($content, $func) !== false) {
$escaping_found = true;
break 2;
}
}
}
if ($escaping_found) {
$this->passed[] = 'Output escaping implemented';
} else {
$this->issues[] = 'No output escaping functions found';
}
}
/**
* Check nonce usage
*/
private function check_nonce_usage() {
$nonce_functions = array(
'wp_create_nonce',
'wp_verify_nonce',
'wp_nonce_field',
'check_admin_referer'
);
$plugin_dir = plugin_dir_path(__FILE__);
$php_files = $this->get_all_php_files($plugin_dir);
$nonce_found = 0;
foreach ($php_files as $file) {
$content = file_get_contents($file);
foreach ($nonce_functions as $func) {
if (strpos($content, $func) !== false) {
$nonce_found++;
break;
}
}
}
if ($nonce_found >= 2) { // Should have both create and verify
$this->passed[] = 'Nonce protection implemented';
} else {
$this->issues[] = 'Insufficient nonce protection';
}
}
/**
* Check capability checks
*/
private function check_capability_checks() {
$capability_functions = array(
'current_user_can',
'user_can',
'is_admin'
);
$plugin_dir = plugin_dir_path(__FILE__);
$php_files = $this->get_all_php_files($plugin_dir);
$capability_found = false;
foreach ($php_files as $file) {
$content = file_get_contents($file);
foreach ($capability_functions as $func) {
if (strpos($content, $func) !== false) {
$capability_found = true;
break 2;
}
}
}
if ($capability_found) {
$this->passed[] = 'Capability checks implemented';
} else {
$this->issues[] = 'No capability checks found';
}
}
/**
* Check SSL usage
*/
private function check_ssl_usage() {
$plugin_dir = plugin_dir_path(__FILE__);
$php_files = $this->get_all_php_files($plugin_dir);
$https_enforced = false;
foreach ($php_files as $file) {
$content = file_get_contents($file);
if (strpos($content, 'https://api.github.com') !== false) {
$https_enforced = true;
break;
}
}
if ($https_enforced) {
$this->passed[] = 'HTTPS enforced for API calls';
} else {
$this->issues[] = 'HTTPS not enforced for API calls';
}
}
/**
* Check rate limiting
*/
private function check_rate_limiting() {
$plugin_dir = plugin_dir_path(__FILE__);
$php_files = $this->get_all_php_files($plugin_dir);
$rate_limiting = false;
foreach ($php_files as $file) {
$content = file_get_contents($file);
if (strpos($content, 'rate_limit') !== false || strpos($content, 'throttl') !== false) {
$rate_limiting = true;
break;
}
}
if ($rate_limiting) {
$this->passed[] = 'Rate limiting implemented';
} else {
$this->issues[] = 'No rate limiting found';
}
}
/**
* Get all PHP files recursively
*/
private function get_all_php_files($dir) {
$files = array();
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir)
);
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
$files[] = $file->getPathname();
}
}
return $files;
}
/**
* Generate audit report
*/
private function generate_report() {
$report = array(
'timestamp' => current_time('mysql'),
'passed_count' => count($this->passed),
'issues_count' => count($this->issues),
'passed' => $this->passed,
'issues' => $this->issues,
'score' => $this->calculate_score(),
'status' => empty($this->issues) ? 'SECURE' : 'NEEDS ATTENTION'
);
return $report;
}
/**
* Calculate security score
*/
private function calculate_score() {
$total = count($this->passed) + count($this->issues);
if ($total === 0) {
return 0;
}
return round((count($this->passed) / $total) * 100);
}
}
// Run audit if requested
if (isset($_GET['mcb_security_audit']) && current_user_can('manage_options')) {
$audit = new MCB_Security_Audit();
$report = $audit->run_audit();
echo '<div class="wrap">';
echo '<h1>GitHub Code Viewer Security Audit</h1>';
echo '<div class="notice notice-' . (empty($report['issues']) ? 'success' : 'warning') . '">';
echo '<p><strong>Security Score: ' . $report['score'] . '%</strong></p>';
echo '<p>Status: ' . $report['status'] . '</p>';
echo '</div>';
if (!empty($report['passed'])) {
echo '<h2>✅ Passed Checks (' . $report['passed_count'] . ')</h2>';
echo '<ul>';
foreach ($report['passed'] as $pass) {
echo '<li style="color: green;">✓ ' . esc_html($pass) . '</li>';
}
echo '</ul>';
}
if (!empty($report['issues'])) {
echo '<h2>⚠️ Issues Found (' . $report['issues_count'] . ')</h2>';
echo '<ul>';
foreach ($report['issues'] as $issue) {
echo '<li style="color: red;">✗ ' . esc_html($issue) . '</li>';
}
echo '</ul>';
}
echo '<p><em>Audit completed at ' . $report['timestamp'] . '</em></p>';
echo '</div>';
}