added additional plugins
This commit is contained in:
parent
c85895d306
commit
00e60ec1b7
132 changed files with 27514 additions and 0 deletions
33
native/wordpress/maple-code-blocks/includes/basic-block.php
Normal file
33
native/wordpress/maple-code-blocks/includes/basic-block.php
Normal 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
|
||||
|
|
@ -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'));
|
||||
|
|
@ -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();
|
||||
|
|
@ -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' => '<script',
|
||||
'/<\/script/i' => '</script',
|
||||
'/javascript:/i' => 'javascript:',
|
||||
'/on\w+\s*=/i' => 'on_event=',
|
||||
'/<iframe/i' => '<iframe',
|
||||
'/<object/i' => '<object',
|
||||
'/<embed/i' => '<embed',
|
||||
'/<applet/i' => '<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;
|
||||
}
|
||||
}
|
||||
749
native/wordpress/maple-code-blocks/includes/class-github-api.php
Normal file
749
native/wordpress/maple-code-blocks/includes/class-github-api.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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'));
|
||||
|
|
@ -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');
|
||||
}
|
||||
));
|
||||
});
|
||||
322
native/wordpress/maple-code-blocks/includes/class-security.php
Normal file
322
native/wordpress/maple-code-blocks/includes/class-security.php
Normal 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();
|
||||
190
native/wordpress/maple-code-blocks/includes/class-shortcode.php
Normal file
190
native/wordpress/maple-code-blocks/includes/class-shortcode.php
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
2
native/wordpress/maple-code-blocks/includes/index.php
Normal file
2
native/wordpress/maple-code-blocks/includes/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
// Silence is golden.
|
||||
398
native/wordpress/maple-code-blocks/includes/security-audit.php
Normal file
398
native/wordpress/maple-code-blocks/includes/security-audit.php
Normal 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>';
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue