$v ) { $data[ $k ] = self::recursive_str_replace( $from, $to, $v ); } return $data; } if ( ! is_string( $data ) ) { return $data; } // allowed_classes=false: prevents PHP object-injection / POP-chain // execution if any DB cell contains attacker-supplied serialized data. // Objects become __PHP_Incomplete_Class; arrays and scalars deserialise // normally so URL rewriting still works. $unserialized = @unserialize( $data, array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize,WordPress.PHP.NoSilencedErrors.Discouraged -- @ suppresses E_NOTICE on malformed data; allowed_classes=false prevents object injection if ( $unserialized !== false && $data !== 'b:0;' ) { $replaced = self::recursive_str_replace( $from, $to, $unserialized ); return serialize( $replaced ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize } return str_replace( $from, $to, $data ); } /* ================================================================== 6a. Settings AJAX save ================================================================== */ /** * AJAX handler: saves the colour theme and default mode settings. * * @since 1.6.0 * @return void Sends JSON response and exits. */ public static function ajax_save_theme_setting(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Forbidden', 403 ); } if ( ! check_ajax_referer( 'csdt_devtools_code_settings_inline', 'nonce', false ) ) { wp_send_json_error( 'Bad nonce', 403 ); } $theme = isset( $_POST['theme'] ) ? sanitize_text_field( wp_unslash( $_POST['theme'] ) ) : 'dark'; if ( ! in_array( $theme, array( 'dark', 'light' ), true ) ) { $theme = 'dark'; } update_option( 'csdt_devtools_code_default_theme', $theme ); $valid_pairs = array_keys( CSDT_DevTools::get_theme_registry() ); $pair = isset( $_POST['theme_pair'] ) ? sanitize_text_field( wp_unslash( $_POST['theme_pair'] ) ) : 'atom-one'; if ( ! in_array( $pair, $valid_pairs, true ) ) { $pair = 'atom-one'; } update_option( 'csdt_devtools_code_theme_pair', $pair ); // Only touch the perf-monitor flag when the caller explicitly sent it. // The Re-apply panel saves theme settings without that checkbox in scope // and must not silently turn perf monitoring off. if ( array_key_exists( 'csdt_devtools_perf_monitor_enabled', $_POST ) ) { $perf_enabled = '1' === sanitize_key( wp_unslash( $_POST['csdt_devtools_perf_monitor_enabled'] ) ) ? '1' : '0'; update_option( 'csdt_devtools_perf_monitor_enabled', $perf_enabled ); } else { $perf_enabled = (string) CSDT_Options::get( 'csdt_devtools_perf_monitor_enabled' ); } if ( array_key_exists( 'powered_by', $_POST ) ) { $powered_by = '1' === sanitize_text_field( wp_unslash( $_POST['powered_by'] ) ) ? '1' : '0'; update_option( 'csdt_show_powered_by', $powered_by ); } if ( array_key_exists( 'emoji_off', $_POST ) ) { $emoji_off = '1' === sanitize_text_field( wp_unslash( $_POST['emoji_off'] ) ) ? '1' : '0'; update_option( 'csdt_emoji_polyfill_off', $emoji_off ); } if ( array_key_exists( 'pdf_export', $_POST ) ) { $pdf_export = '1' === sanitize_text_field( wp_unslash( $_POST['pdf_export'] ) ) ? '1' : '0'; update_option( CSDT_PDF_Export::OPTION, $pdf_export ); } wp_send_json_success( array( 'theme' => $theme, 'theme_pair' => $pair, 'perf_enabled' => $perf_enabled, ) ); } public static function ajax_save_perf_monitor(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Forbidden', 403 ); } if ( ! check_ajax_referer( CSDT_DevTools::PERF_NONCE, 'nonce', false ) ) { wp_send_json_error( 'Bad nonce', 403 ); } $enabled = isset( $_POST['enabled'] ) && '1' === sanitize_key( wp_unslash( $_POST['enabled'] ) ) ? '1' : '0'; update_option( 'csdt_devtools_perf_monitor_enabled', $enabled ); wp_send_json_success( array( 'perf_enabled' => $enabled ) ); } // ── Object cache toggle ─────────────────────────────────────────────────── public static function get_object_cache_status(): array { $drop_in = CSDT_Paths::content_dir() . '/object-cache.php'; // core loads drop-ins only from directly inside the content directory if ( ! file_exists( $drop_in ) ) { return array( 'type' => 'none', 'label' => 'Not installed', ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading drop-in header at known local path $head = (string) file_get_contents( $drop_in, false, null, 0, 1024 ); if ( false !== strpos( $head, 'CSDT_APCU_AVAILABLE' ) ) { return array( 'type' => 'ours', 'label' => 'APCu (older CloudScale drop-in)', ); } if ( false !== stripos( $head, 'redis' ) ) { return array( 'type' => 'redis', 'label' => 'Redis', ); } if ( false !== stripos( $head, 'memcached' ) ) { return array( 'type' => 'memcached', 'label' => 'Memcached', ); } return array( 'type' => 'other', 'label' => 'Third-party drop-in', ); } public static function ajax_object_cache_toggle(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Forbidden', 403 ); } if ( ! check_ajax_referer( 'csdt_object_cache_nonce', 'nonce', false ) ) { wp_send_json_error( 'Bad nonce', 403 ); } // Remove is the only action: the plugin no longer ships a drop-in to install (1.10.882). $action = isset( $_POST['action_type'] ) ? sanitize_key( wp_unslash( $_POST['action_type'] ) ) : ''; $drop_in = CSDT_Paths::content_dir() . '/object-cache.php'; // core loads drop-ins only from directly inside the content directory if ( 'uninstall' === $action ) { if ( ! file_exists( $drop_in ) ) { wp_send_json_success( self::get_object_cache_status() ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading drop-in header at a known local path; wp_remote_get() is for URLs, not local files $head = (string) file_get_contents( $drop_in, false, null, 0, 1024 ); if ( false === strpos( $head, 'CSDT_APCU_AVAILABLE' ) ) { wp_send_json_error( 'Not a CloudScale object cache, remove it manually if needed.' ); } wp_delete_file( $drop_in ); if ( file_exists( $drop_in ) ) { wp_send_json_error( 'Could not remove object-cache.php, check directory permissions.' ); } wp_send_json_success( self::get_object_cache_status() ); } wp_send_json_error( 'Unknown action.' ); } /* ================================================================== 7. MIGRATION TOOL ================================================================== */ /* ================================================================== 7a. Migration: Block conversion logic ================================================================== */ /** * Returns the regex pattern that matches legacy wp:code blocks. * * @since 1.5.0 * @return string PCRE pattern string. */ private static function get_code_pattern() { return '#\s*' . ']*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( array( '
', '
', '
' ), "\n", $code_content ); $code = wp_strip_all_tags( $code ); $code = html_entity_decode( $code, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $code = rtrim( $code, "\n" ); return self::build_migrate_block( $code, '' ); } /** * Opening and closing comment of a Code Block Pro block (the kevinbatdorf/code-block-pro plugin). * * @since 1.10.1012 */ const CBP_OPEN = '', $offset ); if ( false === $head_end ) { break; } $attrs = trim( substr( $content, $offset, $head_end - $offset ) ); $offset = $head_end + 3; // Written as one self-closing comment: there is no markup and no closing comment. if ( '/' === substr( $attrs, -1 ) ) { $found[] = array( 'start' => $start, 'length' => $offset - $start, 'attrs' => trim( substr( $attrs, 0, -1 ) ), 'inner' => '', ); continue; } $close = strpos( $content, self::CBP_CLOSE, $offset ); if ( false === $close ) { continue; } $nested = strpos( $content, self::CBP_OPEN, $offset ); if ( false !== $nested && $nested < $close ) { continue; } $close_end = strpos( $content, '-->', $close ); if ( false === $close_end ) { continue; } $found[] = array( 'start' => $start, 'length' => $close_end + 3 - $start, 'attrs' => $attrs, 'inner' => substr( $content, $offset, $close - $offset ), ); $offset = $close_end + 3; } return $found; } /** * Reads the attributes of a Code Block Pro block. * * @since 1.10.1012 * @param string $attrs_json The JSON from the opening comment. * @return array Empty when there is none or it does not parse. */ private static function cbp_attrs( string $attrs_json ): array { if ( '' === $attrs_json ) { return array(); } $attrs = json_decode( $attrs_json, true ); return is_array( $attrs ) ? $attrs : array(); } /** * The code held by a Code Block Pro block, exactly as the author typed it. * * The `code` attribute is the author's text and is used whenever it is there. A block * without one is read from the copy button, then from the coloured markup. A block that * yields nothing by any of the three answers null and is not converted. * * @since 1.10.1012 * @param array $attrs Block attributes. * @param string $inner The markup between the two comments. * @return string|null */ private static function cbp_code( array $attrs, string $inner ): ?string { $code = null; if ( isset( $attrs['code'] ) && is_string( $attrs['code'] ) && '' !== trim( $attrs['code'] ) ) { $code = $attrs['code']; } else { $from = stripos( $inner, '', $from ); $to = false === $from ? false : stripos( $inner, '', $from ); if ( false !== $from && false !== $to ) { $code = html_entity_decode( substr( $inner, $from + 1, $to - $from - 1 ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); } else { $from = stripos( $inner, '', $from ); $to = false === $from ? false : strripos( $inner, '' ); if ( false !== $from && false !== $to && $to > $from ) { $code = html_entity_decode( wp_strip_all_tags( substr( $inner, $from + 1, $to - $from - 1 ) ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); } } } if ( null === $code ) { return null; } $code = rtrim( str_replace( "\r\n", "\n", $code ), "\n" ); return '' === trim( $code ) ? null : $code; } /** * The language to carry over from a Code Block Pro block. * * @since 1.10.1012 * @param mixed $lang The block's `language` attribute. * @return string A language this block highlights, or '' to let it detect one. */ private static function cbp_language( $lang ): string { if ( ! is_string( $lang ) ) { return ''; } $lang = strtolower( trim( $lang ) ); $lang = self::CBP_LANGUAGE_ALIASES[ $lang ] ?? $lang; return in_array( $lang, self::HIGHLIGHT_LANGUAGES, true ) ? $lang : ''; } /** * Converts one Code Block Pro block found by find_cbp_blocks(). * * @since 1.10.1012 * @param array{start:int, length:int, attrs:string, inner:string} $block The block. * @return string|null New block comment markup, or null when no code could be read. */ private static function convert_cbp_block( array $block ): ?string { $attrs = self::cbp_attrs( $block['attrs'] ); $code = self::cbp_code( $attrs, $block['inner'] ); if ( null === $code ) { return null; } // The text Code Block Pro printed in the block's header bar, when the author set one. $title = isset( $attrs['headerString'] ) && is_string( $attrs['headerString'] ) ? trim( wp_strip_all_tags( $attrs['headerString'] ) ) : ''; return self::build_migrate_block( $code, self::cbp_language( $attrs['language'] ?? '' ), $title ); } /** * Replaces every readable Code Block Pro block in post content. * * @since 1.10.1012 * @param string $content Post content. * @return string */ private static function convert_cbp_content( string $content ): string { $out = ''; $from = 0; foreach ( self::find_cbp_blocks( $content ) as $block ) { $converted = self::convert_cbp_block( $block ); if ( null === $converted ) { continue; } $out .= substr( $content, $from, $block['start'] - $from ) . $converted; $from = $block['start'] + $block['length']; } return $out . substr( $content, $from ); } /** * Builds a CloudScale block comment from code content and an optional language slug. * * @since 1.5.0 * @param string $code Code content. * @param string $lang Language identifier, or empty string for auto-detect. * @param string $title Title shown above the code, or empty string for none. * @return string Block comment markup. */ private static function build_migrate_block( $code, $lang, $title = '' ) { $attrs = array( 'content' => $code ); if ( ! empty( $lang ) ) { $attrs['language'] = $lang; } if ( '' !== $title ) { $attrs['title'] = $title; } // serialize_block_attributes(), not wp_json_encode(): core escapes // `--`, `<`, `>`, `&` and `"` to \u-sequences precisely because this // JSON lives inside an HTML comment. Raw `-->` in code would terminate // the comment (breaking the block into raw text the editor then // mangles into shortcode/paragraph fragments), and raw `<`/`>`/`--` // get eaten by wp_kses on any save without unfiltered_html (e.g. cron). $attrs_json = serialize_block_attributes( $attrs ); return ''; } /** * LIKE patterns for the five legacy block markers the migrator converts. * * @since 1.10.878 * @return string[] */ private static function migrate_block_like_patterns(): array { global $wpdb; return array_map( static function ( string $marker ) use ( $wpdb ): string { return '%' . $wpdb->esc_like( $marker ) . '%'; }, array( '', '