> */ public static function get_theme_registry(): array { return [ 'atom-one' => [ 'label' => 'Atom One', 'dark_css' => 'atom-one-dark', 'light_css' => 'atom-one-light', 'dark_bg' => '#282c34', 'dark_toolbar' => '#21252b', 'light_bg' => '#fafafa', 'light_toolbar'=> '#e8eaed', ], 'github' => [ 'label' => 'GitHub', 'dark_css' => 'github-dark', 'light_css' => 'github', 'dark_bg' => '#24292e', 'dark_toolbar' => '#1f2428', 'light_bg' => '#fff', 'light_toolbar'=> '#f6f8fa', ], 'monokai' => [ 'label' => 'Monokai', 'dark_css' => 'monokai', 'light_css' => 'atom-one-light', 'dark_bg' => '#272822', 'dark_toolbar' => '#1e1f1c', 'light_bg' => '#fafafa', 'light_toolbar'=> '#e8eaed', ], 'nord' => [ 'label' => 'Nord', 'dark_css' => 'nord', 'light_css' => 'atom-one-light', 'dark_bg' => '#2e3440', 'dark_toolbar' => '#272c36', 'light_bg' => '#fafafa', 'light_toolbar'=> '#e8eaed', ], 'dracula' => [ 'label' => 'Dracula', 'dark_css' => 'dracula', 'light_css' => 'atom-one-light', 'dark_bg' => '#282a36', 'dark_toolbar' => '#21222c', 'light_bg' => '#fafafa', 'light_toolbar'=> '#e8eaed', ], 'tokyo-night' => [ 'label' => 'Tokyo Night', 'dark_css' => 'tokyo-night-dark', 'light_css' => 'tokyo-night-light', 'dark_bg' => '#1a1b26', 'dark_toolbar' => '#16161e', 'light_bg' => '#d5d6db', 'light_toolbar'=> '#c8c9ce', ], 'vs2015' => [ 'label' => 'VS 2015 / VS Code', 'dark_css' => 'vs2015', 'light_css' => 'vs', 'dark_bg' => '#1e1e1e', 'dark_toolbar' => '#181818', 'light_bg' => '#fff', 'light_toolbar'=> '#f3f3f3', ], 'stackoverflow' => [ 'label' => 'Stack Overflow', 'dark_css' => 'stackoverflow-dark', 'light_css' => 'stackoverflow-light', 'dark_bg' => '#1c1b1b', 'dark_toolbar' => '#151414', 'light_bg' => '#f6f6f6', 'light_toolbar'=> '#e8e8e8', ], 'night-owl' => [ 'label' => 'Night Owl', 'dark_css' => 'night-owl', 'light_css' => 'atom-one-light', 'dark_bg' => '#011627', 'dark_toolbar' => '#001122', 'light_bg' => '#fafafa', 'light_toolbar'=> '#e8eaed', ], 'gruvbox' => [ 'label' => 'Gruvbox', 'dark_css' => 'base16/gruvbox-dark-hard', 'light_css' => 'base16/gruvbox-light-hard', 'dark_bg' => '#1d2021', 'dark_toolbar' => '#171819', 'light_bg' => '#f9f5d7', 'light_toolbar'=> '#ece8c8', ], 'solarized' => [ 'label' => 'Solarized', 'dark_css' => 'base16/solarized-dark', 'light_css' => 'base16/solarized-light', 'dark_bg' => '#002b36', 'dark_toolbar' => '#002530', 'light_bg' => '#fdf6e3', 'light_toolbar'=> '#eee8d5', ], 'panda' => [ 'label' => 'Panda', 'dark_css' => 'panda-syntax-dark', 'light_css' => 'panda-syntax-light', 'dark_bg' => '#292a2b', 'dark_toolbar' => '#222324', 'light_bg' => '#e6e6e6', 'light_toolbar'=> '#d9d9d9', ], 'tomorrow' => [ 'label' => 'Tomorrow Night', 'dark_css' => 'tomorrow-night-bright', 'light_css' => 'atom-one-light', 'dark_bg' => '#000', 'dark_toolbar' => '#0a0a0a', 'light_bg' => '#fafafa', 'light_toolbar'=> '#e8eaed', ], 'shades-of-purple' => [ 'label' => 'Shades of Purple', 'dark_css' => 'shades-of-purple', 'light_css' => 'atom-one-light', 'dark_bg' => '#2d2b55', 'dark_toolbar' => '#252347', 'light_bg' => '#fafafa', 'light_toolbar'=> '#e8eaed', ], ]; } private static $instance_count = 0; private static $assets_enqueued = false; // Performance monitor — static storage. /** @var array HTTP calls captured during this request. */ private static $perf_http_calls = []; /** @var float|null Microtime when last HTTP request started. */ private static $perf_http_timer = null; /** @var array PHP errors captured during this request. */ private static $perf_php_errors = []; /** @var array|null Active plugin prefix → slug map cache. */ private static $perf_plugin_map = null; /** @var callable|null Previous PHP error handler to chain into. */ private static $perf_prev_error_handler = null; /** @var string Template filename captured via template_include filter. */ private static $perf_template = ''; /** @var array Hook fire stats: [ hook => ['count'=>int,'total_ms'=>float,'max_ms'=>float] ] */ private static $perf_hooks = []; /** @var float|null Timestamp of last hook fire (ms). */ private static $perf_hook_last_ms = null; /** @var string|null Name of last hook fired. */ private static $perf_hook_last_name = null; /** @var array Transient stats: [ key => [ gets, hits, sets, deletes ] ] */ private static $perf_transients = []; /** @var array Template hierarchy candidates captured via *_template_hierarchy filters. */ private static $perf_template_hierarchy = []; /** @var array Request lifecycle milestones: [ ['label'=>string, 'ms'=>float] ] */ private static $perf_milestones = []; /** @var array|null Pending email log entry for the in-flight wp_mail() call. */ private static $smtp_log_pending = null; /** * Registers all plugin hooks. * * @since 1.0.0 * @return void */ public static function init() { self::maybe_migrate_prefix(); self::maybe_migrate_smtp_prefix(); self::maybe_migrate_usermeta_prefix(); add_filter( 'xmlrpc_enabled', '__return_false' ); // One-click security hardening — option-driven filters applied at every boot if ( get_option( 'csdt_devtools_disable_app_passwords', '0' ) === '1' ) { if ( get_option( 'csdt_test_accounts_enabled', '0' ) === '1' ) { // Test-account mode: block per-user (not site-wide) so test accounts still authenticate add_filter( 'wp_is_application_passwords_available_for_user', [ __CLASS__, 'filter_app_pw_for_user' ], 10, 2 ); } else { add_filter( 'wp_is_application_passwords_available', '__return_false' ); } } // Test-account cleanup cron + single-use hook (always when feature is enabled) if ( get_option( 'csdt_test_accounts_enabled', '0' ) === '1' ) { add_action( 'application_password_did_authenticate', [ __CLASS__, 'test_account_after_auth' ], 10, 2 ); } if ( get_option( 'csdt_devtools_hide_wp_version', '0' ) === '1' ) { remove_action( 'wp_head', 'wp_generator' ); add_filter( 'the_generator', '__return_empty_string' ); // Strip ?ver= query strings from enqueued scripts/styles to prevent version fingerprinting add_filter( 'style_loader_src', [ __CLASS__, 'strip_asset_ver' ], 9999 ); add_filter( 'script_loader_src', [ __CLASS__, 'strip_asset_ver' ], 9999 ); } add_action( 'init', [ __CLASS__, 'load_textdomain' ] ); add_action( 'init', [ __CLASS__, 'register_block' ] ); add_action( 'init', [ __CLASS__, 'register_shortcode' ] ); add_action( 'enqueue_block_editor_assets', [ __CLASS__, 'enqueue_convert_script' ] ); add_action( 'admin_menu', [ __CLASS__, 'add_tools_page' ] ); add_action( 'wp_dashboard_setup', [ __CLASS__, 'register_dashboard_widget' ] ); add_action( 'admin_init', [ __CLASS__, 'register_settings' ] ); add_action( 'admin_init', [ __CLASS__, 'redirect_legacy_slug' ] ); add_action( 'init', [ __CLASS__, 'redirect_legacy_help_url' ], 1 ); add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue_admin_assets' ] ); // Migration AJAX add_action( 'wp_ajax_csdt_devtools_migrate_scan', [ __CLASS__, 'ajax_scan' ] ); add_action( 'wp_ajax_csdt_devtools_migrate_preview', [ __CLASS__, 'ajax_preview' ] ); add_action( 'wp_ajax_csdt_devtools_migrate_single', [ __CLASS__, 'ajax_migrate_single' ] ); add_action( 'wp_ajax_csdt_devtools_migrate_all', [ __CLASS__, 'ajax_migrate_all' ] ); // SQL AJAX add_action( 'wp_ajax_csdt_devtools_sql_run', [ __CLASS__, 'ajax_sql_run' ] ); // Settings AJAX add_action( 'wp_ajax_csdt_devtools_save_theme_setting', [ __CLASS__, 'ajax_save_theme_setting' ] ); // Login security AJAX add_action( 'wp_ajax_csdt_devtools_login_save', [ __CLASS__, 'ajax_login_save' ] ); add_action( 'wp_ajax_csdt_devtools_bf_log_fetch', [ __CLASS__, 'ajax_bf_log_fetch' ] ); add_action( 'wp_ajax_csdt_devtools_totp_setup_start', [ __CLASS__, 'ajax_totp_setup_start' ] ); add_action( 'wp_ajax_csdt_devtools_totp_setup_verify', [ __CLASS__, 'ajax_totp_setup_verify' ] ); add_action( 'wp_ajax_csdt_devtools_2fa_disable', [ __CLASS__, 'ajax_2fa_disable' ] ); add_action( 'wp_ajax_csdt_devtools_email_2fa_enable', [ __CLASS__, 'ajax_email_2fa_enable' ] ); add_action( 'admin_init', [ __CLASS__, 'email_2fa_confirm_check' ] ); add_action( 'after_password_reset', [ __CLASS__, 'on_password_reset' ], 10, 1 ); add_action( 'profile_update', [ __CLASS__, 'on_profile_update' ], 10, 2 ); CSDT_DevTools_Passkey::register_hooks(); // Thumbnails / Social Preview AJAX add_action( 'wp_ajax_csdt_devtools_social_check_url', [ __CLASS__, 'ajax_social_check_url' ] ); add_action( 'wp_ajax_csdt_devtools_social_scan_posts', [ __CLASS__, 'ajax_social_scan_posts' ] ); add_action( 'wp_ajax_csdt_devtools_social_scan_media', [ __CLASS__, 'ajax_social_scan_media' ] ); add_action( 'wp_ajax_csdt_devtools_social_fix_image', [ __CLASS__, 'ajax_social_fix_image' ] ); add_action( 'wp_ajax_csdt_devtools_social_generate_formats', [ __CLASS__, 'ajax_social_generate_formats' ] ); add_action( 'wp_ajax_csdt_devtools_social_platform_save', [ __CLASS__, 'ajax_social_platform_save' ] ); add_action( 'wp_ajax_csdt_devtools_social_fix_all_batch', [ __CLASS__, 'ajax_social_fix_all_batch' ] ); add_action( 'wp_ajax_csdt_devtools_social_diagnose_formats', [ __CLASS__, 'ajax_social_diagnose_formats' ] ); add_action( 'save_post_post', [ __CLASS__, 'on_post_saved' ], 100, 3 ); add_action( 'admin_notices', [ __CLASS__, 'social_format_admin_notice' ] ); // Serve platform-specific og:image based on crawler User-Agent. add_action( 'wp_head', [ __CLASS__, 'output_crawler_og_image' ], 1 ); add_action( 'wp_ajax_csdt_devtools_social_cf_test', [ __CLASS__, 'ajax_social_cf_test' ] ); add_action( 'wp_ajax_csdt_devtools_cf_purge', [ __CLASS__, 'ajax_cf_purge' ] ); add_action( 'wp_ajax_csdt_devtools_cf_save', [ __CLASS__, 'ajax_cf_save' ] ); // SMTP AJAX add_action( 'wp_ajax_csdt_devtools_smtp_save', [ __CLASS__, 'ajax_smtp_save' ] ); add_action( 'wp_ajax_csdt_devtools_smtp_test', [ __CLASS__, 'ajax_smtp_test' ] ); add_action( 'wp_ajax_csdt_devtools_smtp_log_clear', [ __CLASS__, 'ajax_smtp_log_clear' ] ); add_action( 'wp_ajax_csdt_devtools_smtp_log_fetch', [ __CLASS__, 'ajax_smtp_log_fetch' ] ); add_action( 'wp_ajax_csdt_devtools_vuln_scan', [ __CLASS__, 'ajax_vuln_scan' ] ); add_action( 'wp_ajax_csdt_devtools_deep_scan', [ __CLASS__, 'ajax_deep_scan' ] ); add_action( 'wp_ajax_csdt_devtools_scan_status', [ __CLASS__, 'ajax_scan_status' ] ); add_action( 'wp_ajax_csdt_devtools_cancel_scan', [ __CLASS__, 'ajax_cancel_scan' ] ); add_action( 'wp_ajax_csdt_devtools_vuln_save_key', [ __CLASS__, 'ajax_vuln_save_key' ] ); add_action( 'wp_ajax_csdt_devtools_security_test_key', [ __CLASS__, 'ajax_security_test_key' ] ); add_action( 'wp_ajax_csdt_devtools_server_logs_status', [ __CLASS__, 'ajax_server_logs_status' ] ); add_action( 'wp_ajax_csdt_devtools_server_logs_fetch', [ __CLASS__, 'ajax_server_logs_fetch' ] ); add_action( 'wp_ajax_csdt_devtools_logs_setup_php', [ __CLASS__, 'ajax_logs_setup_php' ] ); add_action( 'wp_ajax_csdt_devtools_logs_custom_save', [ __CLASS__, 'ajax_logs_custom_save' ] ); add_action( 'wp_ajax_csdt_devtools_scan_history', [ __CLASS__, 'ajax_scan_history' ] ); add_action( 'wp_ajax_csdt_devtools_save_schedule', [ __CLASS__, 'ajax_save_schedule' ] ); add_action( 'wp_ajax_csdt_devtools_quick_fix', [ __CLASS__, 'ajax_apply_quick_fix' ] ); add_action( 'wp_ajax_csdt_devtools_csp_save', [ __CLASS__, 'ajax_csp_save' ] ); add_action( 'wp_ajax_csdt_devtools_csp_rollback', [ __CLASS__, 'ajax_csp_rollback' ] ); add_action( 'send_headers', [ __CLASS__, 'output_security_headers' ] ); add_action( 'wp_ajax_csdt_test_account_create', [ __CLASS__, 'ajax_create_test_account' ] ); add_action( 'wp_ajax_csdt_test_account_revoke', [ __CLASS__, 'ajax_revoke_test_account' ] ); add_action( 'wp_ajax_csdt_test_account_settings_save', [ __CLASS__, 'ajax_save_test_account_settings' ] ); add_action( 'csdt_cleanup_test_accounts', [ __CLASS__, 'cleanup_expired_test_accounts' ] ); add_action( 'csdt_scheduled_scan', [ __CLASS__, 'run_scheduled_scan' ] ); add_filter( 'cron_schedules', [ __CLASS__, 'add_cron_schedules' ] ); add_action( 'csdt_devtools_run_vuln_scan', [ __CLASS__, 'cron_vuln_scan' ] ); add_action( 'csdt_devtools_run_deep_scan', [ __CLASS__, 'cron_deep_scan' ] ); // Email log — always active so every wp_mail() call is tracked site-wide, // regardless of whether our SMTP is enabled. add_filter( 'wp_mail', [ __CLASS__, 'smtp_log_capture' ] ); add_action( 'wp_mail_failed', [ __CLASS__, 'smtp_log_on_failure' ] ); // Priority 5 so it runs before phpmailer_configure (priority 10) and sets action_function first. add_action( 'phpmailer_init', [ __CLASS__, 'smtp_log_set_callback' ], 5 ); // SMTP — configure phpmailer and override from address only when fully configured. // Guard: if host is empty we skip configuration entirely so other plugins' emails // continue to work via PHP mail() rather than silently failing. if ( get_option( 'csdt_devtools_smtp_enabled', '0' ) === '1' && '' !== trim( (string) get_option( 'csdt_devtools_smtp_host', '' ) ) ) { add_action( 'phpmailer_init', [ __CLASS__, 'phpmailer_configure' ] ); if ( get_option( 'csdt_devtools_smtp_from_email', '' ) ) { add_filter( 'wp_mail_from', [ __CLASS__, 'smtp_from_email' ] ); } if ( get_option( 'csdt_devtools_smtp_from_name', '' ) ) { add_filter( 'wp_mail_from_name', [ __CLASS__, 'smtp_from_name' ] ); } } // Login security — URL intercept / 2FA flow (early, priority 1 on init). add_action( 'init', [ __CLASS__, 'login_serve_custom_slug' ], 1 ); add_action( 'login_init', [ __CLASS__, 'login_redirect_authenticated' ], 0 ); add_action( 'login_init', [ __CLASS__, 'login_block_direct_access' ], 1 ); add_filter( 'auth_cookie_expiration', [ __CLASS__, 'login_session_expiration' ], 10, 3 ); add_action( 'login_init', [ __CLASS__, 'login_2fa_handle' ] ); add_filter( 'authenticate', [ __CLASS__, 'login_2fa_intercept' ], 100, 3 ); add_filter( 'login_url', [ __CLASS__, 'login_custom_url' ], 10, 3 ); add_filter( 'logout_url', [ __CLASS__, 'login_custom_logout_url' ], 10, 2 ); add_filter( 'lostpassword_url', [ __CLASS__, 'login_custom_lostpassword_url' ], 10, 2 ); add_filter( 'network_site_url', [ __CLASS__, 'login_custom_network_url' ], 10, 3 ); add_filter( 'site_url', [ __CLASS__, 'login_custom_site_url' ], 10, 4 ); // Brute-force protection — check before authentication (priority 1, before password check). add_filter( 'authenticate', [ __CLASS__, 'login_brute_force_check' ], 1, 3 ); // Force persistent cookie when a custom session duration is configured. // Must be login_init (fires before the POST is processed) not login_form_login // (which is a display hook that never fires on a successful login POST). add_action( 'login_init', [ __CLASS__, 'login_force_remember' ], 5 ); // Security monitor — always track failed logins regardless of monitor toggle. add_action( 'wp_login_failed', [ __CLASS__, 'perf_track_failed_login' ] ); // Custom 404 page + hiscore leaderboard. add_action( 'template_redirect', [ __CLASS__, 'maybe_custom_404' ], 1 ); add_action( 'rest_api_init', [ __CLASS__, 'register_hiscore_routes' ] ); add_action( 'wp_ajax_csdt_devtools_save_404_settings', [ __CLASS__, 'ajax_save_404_settings' ] ); // Performance monitor — EXPLAIN endpoint. add_action( 'wp_ajax_csdt_devtools_perf_explain', [ __CLASS__, 'ajax_perf_explain' ] ); add_action( 'wp_ajax_csdt_devtools_perf_debug_toggle', [ __CLASS__, 'ajax_perf_debug_toggle' ] ); // Performance monitor — only register data-collection hooks when the monitor is enabled. // This prevents SAVEQUERIES-scale memory accumulation on every request when disabled. if ( get_option( 'csdt_devtools_perf_monitor_enabled', '1' ) !== '0' ) { add_filter( 'pre_http_request', [ __CLASS__, 'perf_http_before' ], 10, 3 ); add_action( 'http_api_debug', [ __CLASS__, 'perf_http_after' ], 10, 5 ); // If the user enabled debug logging via the panel, activate PHP error logging // using ini_set — this works regardless of WP_DEBUG in wp-config.php and // survives Docker container rebuilds because the setting lives in the DB. if ( get_option( 'csdt_devtools_perf_debug_logging', false ) ) { // phpcs:ignore WordPress.PHP.IniSet.Risky @ini_set( 'log_errors', '1' ); // phpcs:ignore WordPress.PHP.IniSet.Risky @ini_set( 'error_log', WP_CONTENT_DIR . '/debug.log' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting error_reporting( E_ALL ); } // Register error handler late (priority 9999 on plugins_loaded) so we sit // on top of any handler registered by other plugins (e.g. Query Monitor). // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler add_action( 'plugins_loaded', function () { self::$perf_prev_error_handler = set_error_handler( [ __CLASS__, 'perf_error_handler' ], E_WARNING | E_NOTICE | E_DEPRECATED | E_USER_WARNING | E_USER_NOTICE | E_USER_DEPRECATED ); }, 9999 ); // Performance monitor — panel rendering (admin pages). add_action( 'admin_enqueue_scripts', [ __CLASS__, 'perf_enqueue' ] ); // Inject JSON data at priority 15 — before wp_print_footer_scripts (priority 20) so // cs-perf-monitor.js reads window.csdtDevtoolsPerfData when its IIFE runs. add_action( 'admin_footer', [ __CLASS__, 'perf_inject_data' ], 15 ); add_action( 'admin_footer', [ __CLASS__, 'perf_output_panel' ], 9999 ); // Performance monitor — panel rendering (frontend, admin users only). add_action( 'wp_enqueue_scripts', [ __CLASS__, 'perf_frontend_enqueue' ] ); add_action( 'wp_footer', [ __CLASS__, 'perf_inject_data' ], 15 ); add_action( 'wp_footer', [ __CLASS__, 'perf_output_panel' ], 9999 ); // Capture the active template filename for the page-context strip. add_filter( 'template_include', [ __CLASS__, 'perf_capture_template' ], 9999 ); // Hook timing tracker — fires on every action/filter. add_action( 'all', [ __CLASS__, 'perf_hook_tracker' ] ); // Transient + template hierarchy observer (single all-hook for both). add_action( 'all', [ __CLASS__, 'perf_misc_tracker' ] ); add_action( 'setted_transient', [ __CLASS__, 'perf_transient_set' ] ); add_action( 'setted_site_transient', [ __CLASS__, 'perf_transient_set' ] ); add_action( 'deleted_transient', [ __CLASS__, 'perf_transient_delete' ] ); add_action( 'deleted_site_transient', [ __CLASS__, 'perf_transient_delete' ] ); // Scripts & styles — collect at footer time (after everything is enqueued). add_action( 'admin_footer', [ __CLASS__, 'perf_capture_assets' ], 1 ); add_action( 'wp_footer', [ __CLASS__, 'perf_capture_assets' ], 1 ); // Request lifecycle milestones for the waterfall timeline. // Registered at PHP_INT_MAX so we capture the time after all other // callbacks on that hook have finished running. foreach ( [ 'plugins_loaded' => 'Plugins loaded', 'init' => 'WP init', 'admin_init' => 'Admin init', 'wp_loaded' => 'WP loaded', 'wp' => 'Query setup', 'template_redirect' => 'Template', ] as $_ms_hook => $_ms_label ) { add_action( $_ms_hook, static function () use ( $_ms_label ) { self::perf_record_milestone( $_ms_label ); }, PHP_INT_MAX ); } } } /* ================================================================== 0. TEXT DOMAIN ================================================================== */ /** * Loads the plugin text domain for translations. * * @since 1.0.0 * @return void */ public static function load_textdomain(): void { load_plugin_textdomain( 'cloudscale-devtools', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); } /* ================================================================== 1. BLOCK REGISTRATION ================================================================== */ /** * Registers the block type and all its scripts and stylesheets. * * @since 1.0.0 * @return void */ public static function register_block() { $cdn = self::HLJS_CDN . self::HLJS_VERSION; wp_register_script( 'hljs-core', $cdn . '/highlight.min.js', [], self::HLJS_VERSION, true ); // Register both theme stylesheets from the selected pair $pair_slug = get_option( 'csdt_devtools_code_theme_pair', 'atom-one' ); $registry = self::get_theme_registry(); $pair = isset( $registry[ $pair_slug ] ) ? $registry[ $pair_slug ] : $registry['atom-one']; wp_register_style( 'hljs-theme-dark', $cdn . '/styles/' . $pair['dark_css'] . '.min.css', [], self::HLJS_VERSION ); wp_register_style( 'hljs-theme-light', $cdn . '/styles/' . $pair['light_css'] . '.min.css', [], self::HLJS_VERSION ); wp_register_style( 'csdt-code-block-frontend', plugins_url( 'assets/cs-code-block.css', __FILE__ ), [ 'hljs-theme-dark', 'hljs-theme-light' ], self::VERSION ); wp_register_script( 'csdt-code-block-frontend', plugins_url( 'assets/cs-code-block.js', __FILE__ ), [ 'hljs-core' ], self::VERSION, true ); wp_register_style( 'csdt-code-block-editor', plugins_url( 'assets/cs-code-block-editor.css', __FILE__ ), [], self::VERSION ); wp_register_script( 'cloudscale-code-block-editor-script', plugins_url( 'blocks/code/editor.js', __FILE__ ), [ 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n', 'wp-data', 'wp-hooks' ], self::VERSION, true ); register_block_type( __DIR__ . '/blocks/code', [ 'render_callback' => [ __CLASS__, 'render_block' ], 'editor_script' => 'cloudscale-code-block-editor-script', ] ); } /* ================================================================== 1b. CONVERT SCRIPT ================================================================== */ /** * Enqueues the block editor auto-convert script and attaches the toast inline style. * * @since 1.5.0 * @return void */ public static function enqueue_convert_script() { wp_enqueue_script( 'csdt-code-block-convert', plugins_url( 'assets/cs-convert.js', __FILE__ ), [ 'wp-blocks', 'wp-data' ], self::VERSION, true ); wp_add_inline_style( 'csdt-code-block-editor', self::get_convert_toast_css() ); } /** * Returns the CSS string for the block editor convert-all toast notification. * * @since 1.7.17 * @return string */ private static function get_convert_toast_css(): string { return '#cs-convert-all-toast{' . 'position:fixed;bottom:24px;right:24px;z-index:999999;' . 'background:linear-gradient(135deg,#1e3a5f 0%,#0d9488 100%);' . 'color:#fff;padding:16px 20px;border-radius:10px;' . 'box-shadow:0 8px 32px rgba(0,0,0,0.3);' . 'display:flex;align-items:center;gap:16px;' . 'font-size:14px;font-weight:500;' . 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;' . 'animation:cs-toast-in 0.3s ease-out;' . '}' . '#cs-convert-all-toast button{' . 'background:#fff;color:#1e3a5f;font-weight:700;border-radius:6px;' . 'padding:10px 24px;font-size:14px;border:none;white-space:nowrap;' . 'cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,0.15);font-family:inherit;' . '}' . '#cs-convert-all-toast button:hover{background:#f0fdf4;}' . '@keyframes cs-toast-in{' . 'from{opacity:0;transform:translateY(20px);}' . 'to{opacity:1;transform:translateY(0);}' . '}'; } /* ================================================================== 2. RENDER (shared by block + shortcode) ================================================================== */ /** * Renders a code block on the frontend. * * @since 1.0.0 * @param array $attributes Block attributes. * @param string $block_content Existing block content (unused). * @return string HTML output. */ public static function render_block( $attributes, $block_content = '' ) { self::maybe_enqueue_frontend(); self::$instance_count++; $id = 'cs-code-' . self::$instance_count; $code = isset( $attributes['content'] ) ? $attributes['content'] : ''; $lang = isset( $attributes['language'] ) ? $attributes['language'] : ''; $title = isset( $attributes['title'] ) ? $attributes['title'] : ''; $theme = isset( $attributes['theme'] ) ? $attributes['theme'] : ''; return self::build_html( $id, $code, $lang, $title, $theme ); } /** * Builds the full HTML markup for a code block. * * @since 1.0.0 * @param string $id Unique HTML element ID. * @param string $code Code content to display. * @param string $lang Language identifier for highlight.js, or empty for auto-detect. * @param string $title Optional filename or title label. * @param string $theme Per-block colour-theme override slug, or empty for site default. * @return string HTML markup. */ private static function build_html( $id, $code, $lang, $title, $theme ) { $lang_class = $lang ? 'language-' . esc_attr( $lang ) : ''; $cloudscale_link = '⚡ Powered by CloudScale'; $title_html = ''; if ( $title ) { $title_html = '
|
$#i', '', trim( $content ) ); $content = str_replace( [ 'wp:code and wp:preformatted blocks that can be upgraded to CloudScale Code Blocks with full syntax highlighting.' ],
[ 'name' => 'Preview', 'rec' => 'Recommended', 'html' => 'Shows a side-by-side before/after diff for each post before committing any changes, so you can review exactly what will be converted.' ],
[ 'name' => 'Migrate', 'rec' => 'Optional', 'html' => 'Converts detected legacy blocks to CloudScale format. Each post is saved with the converted markup.
' . esc_html__( 'Scan Posts', 'cloudscale-devtools' ) . '' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- format string is hardcoded, only %s is user-visible and escaped above ?>
· ⚠
'Read-only', 'rec' => 'Informational', 'html' => 'Only SELECT, SHOW, DESCRIBE, and EXPLAIN queries are permitted. Write operations (INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE) are blocked to prevent accidental data loss.' ],
[ 'name' => 'Table Prefix', 'rec' => 'Informational', 'html' => 'Your WordPress table prefix is shown in the header. Use it in your queries, e.g. SELECT * FROM wp_posts LIMIT 10 or SELECT * FROM wp_options WHERE option_name = \'siteurl\'.' ],
[ 'name' => 'Quick Queries', 'rec' => 'Recommended', 'html' => 'Use the preset queries below for common diagnostics without needing to write SQL from scratch. Press Enter or Ctrl+Enter to run a query, Shift+Enter to insert a newline.' ],
] ); ?>
postmeta rows, expired transients, and bloated wp_options autoloaded rows that may be slowing down your database.' ],
[ 'name' => 'Security Checks', 'rec' => 'Optional', 'html' => 'Looks for http:// (non-HTTPS) URLs or stale IP addresses in wp_options and post GUIDs — common indicators of old content or unfinished HTTP→HTTPS migrations.' ],
] ); ?>
🏥
📈
🧹
🔍
⏰
/wp-login.php return a 404, stopping bots and credential-stuffing scripts from even finding the login form.' ],
[ 'name' => 'Custom Login Path', 'rec' => 'Recommended', 'html' => 'The URL slug that serves your login page, e.g. /my-secret-login. Use letters, numbers, and hyphens only.3 — tighter security, but risks locking out users who mistype their password5 — default, good balance10 — more forgiving for sites with non-technical userscsdt_devtools_lockout_{username} from the database.' ],
[ 'name' => 'Lockout period', 'rec' => 'Recommended', 'html' => 'Default is 5 minutes. The lock lifts automatically — no admin action needed.administrator-role users from accessing the dashboard until they have set up 2FA. Strongly recommended on any multi-user site.' ],
[ 'name' => 'Grace Logins', 'rec' => 'Advanced', 'html' => 'Allows a user to log in up to N times before 2FA is enforced. The counter is per-user and never resets automatically. Default is 0 (2FA required from the first login).1. Tools like Playwright cannot complete a real 2FA challenge — one grace login lets a test account authenticate for setup steps without disabling 2FA site-wide.' ],
] ); ?>
iPhone 16 or MacBook Touch ID), then follow your device\'s biometric prompt.]*class="[^"]*wp-block-code[^"]*"[^>]*>\s*'
. ']*)>(.*?)\s*'
. '\s*'
. '#s';
}
/**
* Returns the regex pattern that matches legacy wp:preformatted blocks.
*
* @since 1.5.0
* @return string PCRE pattern string.
*/
private static function get_preformatted_pattern() {
return '#\s*'
. ']*class="[^"]*wp-block-preformatted[^"]*"[^>]*>(.*?)\s*' . '#s'; } /** * Converts a matched legacy wp:code block into a CloudScale block comment. * * @since 1.5.0 * @param array $matches preg_replace_callback match array. * @return string New block comment markup. */ private static function convert_code_block( $matches ) { $block_json = $matches[2] ?? ''; $code_attrs = $matches[3] ?? ''; $code_content = $matches[4] ?? ''; $code = html_entity_decode( $code_content, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $code = rtrim( $code, "\n" ); $lang = ''; if ( ! empty( $block_json ) ) { $json = json_decode( $block_json, true ); if ( isset( $json['language'] ) ) { $lang = $json['language']; } } if ( empty( $lang ) && preg_match( '/lang=["\']([^"\']+)["\']/', $code_attrs, $lm ) ) { $lang = $lm[1]; } if ( empty( $lang ) && preg_match( '/class=["\'][^"\']*language-([a-zA-Z0-9+#._-]+)/', $code_attrs, $lm ) ) { $lang = $lm[1]; } return self::build_migrate_block( $code, $lang ); } /** * Converts a matched legacy wp:preformatted block into a CloudScale block comment. * * @since 1.5.0 * @param array $matches preg_replace_callback match array. * @return string New block comment markup. */ private static function convert_preformatted_block( $matches ) { $code_content = $matches[2] ?? ''; $code = str_ireplace( [ '
You\'re receiving this because you have an account on this site.
If you didn\'t request this, you can safely ignore it.