$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( '
', $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( '', '