"text/plain" if ( false !== strpos( $mime, ';' ) ) { $mime = trim( strstr( $mime, ';', true ) ); } $name = strtolower( (string) $name ); // Filename must end in .json (required in all cases). if ( ! preg_match( '/\.json$/', $name ) ) { return false; } // Exact MIME allowlist. Empty MIME is allowed only when extension is .json // (some environments cannot detect MIME for temp uploads). $allowed = array( '', 'application/json', 'text/json', 'text/plain', 'application/octet-stream', ); return in_array( $mime, $allowed, true ); } public function __construct() { add_action( 'admin_post_qwsc_export_settings', array( $this, 'export' ) ); add_action( 'admin_post_qwsc_import_settings', array( $this, 'import' ) ); } /** * Build export payload (testable; no headers/exit). * * @return array */ public function build_export_data() { $settings = get_option( 'qwsc_settings', array() ); if ( ! is_array( $settings ) ) { $settings = array(); } foreach ( \QWSpeedCache\Settings\Schema::secret_keys() as $k ) { unset( $settings[ $k ] ); } foreach ( array_keys( $settings ) as $k ) { if ( ! in_array( $k, \QWSpeedCache\Settings\Schema::exportable_keys(), true ) ) { unset( $settings[ $k ] ); } } return array( 'plugin' => 'qw-optimize', 'schema' => 3, 'version' => defined( 'QWSC_VERSION' ) ? QWSC_VERSION : '', 'settings' => $settings, 'global_unload' => get_option( 'qwsc_global_unload', array() ), 'pagetype_unload' => get_option( 'qwsc_pagetype_unload', array() ), 'advanced_unload' => get_option( 'qwsc_advanced_unload', array() ), 'critical_css' => get_option( 'qwsc_critical_css_global', '' ), 'exported' => gmdate( 'c' ), ); } /** * Apply imported payload to options (testable; no redirect/exit). * * @param array $data Decoded import JSON. * @return true|string True on success, error code string on failure. */ public function apply_import_data( array $data ) { if ( ! isset( $data['settings'] ) || ! is_array( $data['settings'] ) ) { return 'invalid'; } if ( empty( $data['plugin'] ) || ! in_array( $data['plugin'], array( 'qw-optimize', 'qw-speed-cache' ), true ) ) { return 'plugin'; } $schema = isset( $data['schema'] ) ? (int) $data['schema'] : 0; if ( $schema < 1 || $schema > 3 ) { return 'schema'; } foreach ( \QWSpeedCache\Settings\Schema::secret_keys() as $k ) { unset( $data['settings'][ $k ] ); } $current = get_option( 'qwsc_settings', array() ); if ( ! is_array( $current ) ) { $current = array(); } $premium = array_fill_keys( \QWSpeedCache\Free\FeatureCatalog::keys(), true ); $provided = array_fill_keys( \QWSpeedCache\Free\Extensions::available_pro_keys(), true ); foreach ( \QWSpeedCache\Settings\Schema::all() as $key => $definition ) { if ( empty( $definition['exportable'] ) || ! empty( $definition['secret'] ) ) { continue; } if ( isset( $premium[ $key ] ) && ! isset( $provided[ $key ] ) ) { continue; // Import only settings provided by the installed add-on. } if ( ! array_key_exists( $key, $data['settings'] ) ) { continue; } $value = \QWSpeedCache\Settings\Schema::sanitize_value( $key, $data['settings'][ $key ] ); if ( null !== $value ) { $current[ $key ] = $value; } } update_option( 'qwsc_settings', $current ); if ( isset( $data['global_unload'] ) && is_array( $data['global_unload'] ) ) { update_option( 'qwsc_global_unload', $this->sanitize_unload( $data['global_unload'] ) ); } if ( isset( $data['pagetype_unload'] ) && is_array( $data['pagetype_unload'] ) ) { $allowed = self::allowed_page_types(); $pt = array(); foreach ( $data['pagetype_unload'] as $type => $rules ) { $type = sanitize_key( $type ); if ( ! in_array( $type, $allowed, true ) ) { continue; } $pt[ $type ] = $this->sanitize_unload( is_array( $rules ) ? $rules : array() ); } update_option( 'qwsc_pagetype_unload', $pt ); } if ( isset( $data['advanced_unload'] ) && is_array( $data['advanced_unload'] ) && class_exists( '\QWSpeedCache\Assets\AdvancedRules' ) ) { $clean = array(); foreach ( $data['advanced_unload'] as $rule ) { if ( ! is_array( $rule ) ) { continue; } $sane = \QWSpeedCache\Assets\AdvancedRules::sanitize_rule( $rule ); if ( $sane ) { $clean[] = $sane; } } update_option( 'qwsc_advanced_unload', $clean, false ); } if ( isset( $data['critical_css'] ) && is_string( $data['critical_css'] ) ) { $css = wp_strip_all_tags( $data['critical_css'] ); if ( strlen( $css ) > self::MAX_CRITICAL_CSS ) { $css = substr( $css, 0, self::MAX_CRITICAL_CSS ); } update_option( 'qwsc_critical_css_global', $css ); } return true; } public function export() { check_admin_referer( 'qwsc_export' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'qw-optimize' ), '', array( 'response' => 403 ) ); } $data = $this->build_export_data(); header( 'Content-Type: application/json; charset=utf-8' ); header( 'Content-Disposition: attachment; filename=qw-optimize-settings-' . gmdate( 'Y-m-d' ) . '.json' ); echo wp_json_encode( $data, JSON_PRETTY_PRINT ); exit; } public function import() { check_admin_referer( 'qwsc_import' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'qw-optimize' ), '', array( 'response' => 403 ) ); } if ( empty( $_FILES['qwsc_import_file'] ) || ! is_array( $_FILES['qwsc_import_file'] ) || ! isset( $_FILES['qwsc_import_file']['error'] ) || ! is_int( $_FILES['qwsc_import_file']['error'] ) || UPLOAD_ERR_OK !== $_FILES['qwsc_import_file']['error'] ) { $this->fail( 'upload' ); } $file = $_FILES['qwsc_import_file']; if ( ! isset( $file['tmp_name'], $file['size'], $file['name'] ) || ! is_string( $file['tmp_name'] ) || ! is_string( $file['name'] ) || ! is_numeric( $file['size'] ) || (int) $file['size'] <= 0 || (int) $file['size'] > 512 * 1024 ) { $this->fail( 'size' ); } if ( ! is_uploaded_file( $file['tmp_name'] ) || ! is_readable( $file['tmp_name'] ) ) { $this->fail( 'upload' ); } if ( false === filesize( $file['tmp_name'] ) || filesize( $file['tmp_name'] ) > 512 * 1024 ) { $this->fail( 'size' ); } $mime = ''; if ( function_exists( 'mime_content_type' ) ) { $mime = strtolower( (string) @mime_content_type( $file['tmp_name'] ) ); } $name = strtolower( (string) $file['name'] ); if ( ! self::is_allowed_import_mime( $mime, $name ) ) { $this->fail( 'mime' ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading a verified local PHP upload temporary file (512 KiB limit); not a URL. $raw = file_get_contents( $file['tmp_name'] ); if ( false === $raw || '' === $raw ) { $this->fail( 'empty' ); } $data = json_decode( $raw, true ); if ( ! is_array( $data ) ) { $this->fail( 'invalid' ); } $result = $this->apply_import_data( $data ); if ( true !== $result ) { $this->fail( is_string( $result ) ? $result : 'invalid' ); } \QWSpeedCache\Plugin::clear_all_cache(); wp_safe_redirect( add_query_arg( array( 'page' => 'qw-speed-cache-tools', 'imported' => '1' ), admin_url( 'admin.php' ) ) ); exit; } /** * Sanitize unload rules with handle count and length limits. * * @param array $arr Raw rules. * @return array{styles: string[], scripts: string[]} */ public function sanitize_unload( $arr ) { $out = array( 'styles' => array(), 'scripts' => array() ); foreach ( array( 'styles', 'scripts' ) as $type ) { if ( empty( $arr[ $type ] ) || ! is_array( $arr[ $type ] ) ) { continue; } $handles = array(); foreach ( $arr[ $type ] as $handle ) { if ( ! is_string( $handle ) ) { continue; } $handle = sanitize_text_field( $handle ); if ( '' === $handle ) { continue; } if ( strlen( $handle ) > self::MAX_HANDLE_LENGTH ) { $handle = substr( $handle, 0, self::MAX_HANDLE_LENGTH ); } $handles[] = $handle; if ( count( $handles ) >= self::MAX_UNLOAD_HANDLES ) { break; } } $out[ $type ] = array_values( array_unique( $handles ) ); } return $out; } private function fail( $code ) { wp_safe_redirect( add_query_arg( array( 'page' => 'qw-speed-cache-tools', 'error' => $code ), admin_url( 'admin.php' ) ) ); exit; } /** * Dry-run summary for admin (N-24). * * @param array $data Decoded JSON. * @return array{ok:bool,error?:string,settings_keys?:int,has_unload?:bool,has_critical?:bool,schema?:int} */ public static function preview_import( array $data ) { if ( empty( $data['plugin'] ) || ! in_array( $data['plugin'], array( 'qw-optimize', 'qw-speed-cache' ), true ) ) { return array( 'ok' => false, 'error' => 'plugin' ); } $schema = isset( $data['schema'] ) ? (int) $data['schema'] : 0; if ( $schema < 1 || $schema > 3 ) { return array( 'ok' => false, 'error' => 'schema' ); } $keys = isset( $data['settings'] ) && is_array( $data['settings'] ) ? count( $data['settings'] ) : 0; return array( 'ok' => true, 'schema' => $schema, 'settings_keys' => $keys, 'has_unload' => ! empty( $data['global_unload'] ) || ! empty( $data['pagetype_unload'] ) || ! empty( $data['advanced_unload'] ), 'has_critical' => ! empty( $data['critical_css'] ), 'version' => isset( $data['version'] ) ? (string) $data['version'] : '', ); } }