12 ) { return true; } foreach ( $value as $key => $child ) { if ( in_array( $key, array( '__proto__', 'prototype', 'constructor' ), true ) || sflo_style_unsafe_keys( $child, $depth + 1 ) ) { return true; } } return false; } function sflo_style_error( $code, $message, $status = 400 ) { return new WP_Error( $code, $message, array( 'status' => $status ) ); } /** Bound encoded bytes, including after normalization; this is not a style-count quota. */ function sflo_styles_json_fits( $value ) { $json = wp_json_encode( $value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_LINE_TERMINATORS ); return is_string( $json ) && strlen( $json ) <= 65536; } /** Return only the four canonical fields, or a validation error. No HTML/CSS is stored. */ function sflo_style_record_normalize( $record ) { if ( ! is_array( $record ) || array_is_list( $record ) || sflo_style_unsafe_keys( $record ) ) { return sflo_style_error( 'sflo_invalid_style', __( 'Le style fourni est invalide.', 'ondavilo' ) ); } if ( array_diff( array_keys( $record ), array( 'id', 'name', 'module', 'settings' ) ) ) { return sflo_style_error( 'sflo_invalid_style', __( 'Le style contient des champs non reconnus.', 'ondavilo' ) ); } $id = $record['id'] ?? null; $name = $record['name'] ?? null; $module = $record['module'] ?? null; $settings = $record['settings'] ?? null; if ( ! is_string( $id ) || ! preg_match( '/\A[a-z0-9-]{8,64}\z/D', $id ) ) { return sflo_style_error( 'sflo_invalid_id', __( 'L’identifiant du style est invalide.', 'ondavilo' ) ); } if ( ! is_string( $name ) || 1 !== preg_match( '//u', $name ) ) { return sflo_style_error( 'sflo_invalid_name', __( 'Donnez un nom de 1 à 60 caractères au style.', 'ondavilo' ) ); } $name = trim( $name ); $length = preg_match_all( '/./us', $name ); if ( $length < 1 || $length > 60 || preg_match( '/[\x00-\x1f\x7f-\x9f<>]/u', $name ) ) { return sflo_style_error( 'sflo_invalid_name', __( 'Le nom doit contenir de 1 à 60 caractères, sans balise ni caractère de contrôle.', 'ondavilo' ) ); } if ( ! in_array( $module, array( 'divider', 'backgrounds' ), true ) || ! is_array( $settings ) || array_is_list( $settings ) ) { return sflo_style_error( 'sflo_invalid_module', __( 'Le type ou les réglages du style sont invalides.', 'ondavilo' ) ); } $families = $module === 'divider' ? array( 'triangles', 'steps', 'diagonal', 'curves', 'waves' ) : array( 'gradient', 'dots', 'grid', 'stripes' ); if ( ! isset( $settings['family'] ) || ! in_array( $settings['family'], $families, true ) ) { return sflo_style_error( 'sflo_invalid_family', __( 'Cette famille de styles n’est pas reconnue.', 'ondavilo' ) ); } foreach ( $module === 'divider' ? array( 'upper', 'lower' ) : array( 'background', 'accent' ) as $key ) { if ( ! isset( $settings[$key] ) || ! is_string( $settings[$key] ) || ! preg_match( '/\A#[0-9a-f]{3}(?:[0-9a-f]{3})?\z/iD', $settings[$key] ) ) { return sflo_style_error( 'sflo_invalid_color', __( 'Les deux couleurs du style doivent être des couleurs hexadécimales valides.', 'ondavilo' ) ); } } // Keep the validated label literal (including spaces and percent text). JSON and textContent handle output. return array( 'id' => $id, 'name' => $name, 'module' => $module, 'settings' => $module === 'divider' ? sflo_normalize( $settings ) : sflo_background_normalize( $settings ), ); } /** Validate the whole collection before a single metadata mutation is attempted. */ function sflo_styles_payload( $records ) { if ( ! is_array( $records ) || ! array_is_list( $records ) ) { return sflo_style_error( 'sflo_invalid_collection', __( 'La bibliothèque doit être une liste de styles.', 'ondavilo' ) ); } if ( ! sflo_styles_json_fits( $records ) ) { return sflo_style_error( 'sflo_payload_too_large', __( 'La bibliothèque dépasse la taille maximale de 64 Ko.', 'ondavilo' ), 413 ); } $items = array(); $ids = array(); foreach ( $records as $record ) { $item = sflo_style_record_normalize( $record ); if ( is_wp_error( $item ) ) { return $item; } if ( isset( $ids[$item['id']] ) ) { return sflo_style_error( 'sflo_duplicate_id', __( 'Deux styles portent le même identifiant.', 'ondavilo' ) ); } $ids[$item['id']] = true; $items[] = $item; } if ( ! sflo_styles_json_fits( $items ) ) { return sflo_style_error( 'sflo_payload_too_large', __( 'La bibliothèque dépasse la taille maximale de 64 Ko.', 'ondavilo' ), 413 ); } return $items; } /** Internally retain the previous value for compare-and-swap updates. */ function sflo_styles_read_state( $user_id ) { $rows = get_user_meta( $user_id, 'sflo_saved_styles', false ); // Metadata uniqueness is not a database constraint. Preserve all copies if initial writes raced. if ( count( $rows ) > 1 ) { return sflo_style_error( 'sflo_conflict', __( 'Plusieurs copies de la bibliothèque sont enregistrées. Aucun style n’a été remplacé. Demandez à l’administrateur de vérifier ces copies.', 'ondavilo' ), 409 ); } $raw = $rows[0] ?? null; if ( $raw === '' || $raw === null ) { return array( 'revision' => 0, 'items' => array(), '_raw' => null ); } if ( ! is_array( $raw ) || ! isset( $raw['revision'], $raw['items'] ) || ! is_int( $raw['revision'] ) || $raw['revision'] < 0 || $raw['revision'] > 2147483646 ) { return sflo_style_error( 'sflo_invalid_store', __( 'La bibliothèque enregistrée ne peut pas être lue. Elle n’a pas été remplacée.', 'ondavilo' ) ); } $items = sflo_styles_payload( $raw['items'] ); if ( is_wp_error( $items ) ) { return $items; } if ( ! sflo_styles_json_fits( array( 'revision' => $raw['revision'], 'items' => $items ) ) ) { return sflo_style_error( 'sflo_payload_too_large', __( 'La bibliothèque enregistrée est trop volumineuse. Aucun style n’a été modifié.', 'ondavilo' ), 413 ); } return array( 'revision' => $raw['revision'], 'items' => $items, '_raw' => $raw ); } function sflo_styles_send_error( $error ) { $data = $error->get_error_data(); wp_send_json_error( array( 'code' => $error->get_error_code(), 'message' => $error->get_error_message() ), isset( $data['status'] ) ? $data['status'] : 400 ); } function sflo_styles_authorize() { if ( ! current_user_can( 'edit_posts' ) || get_current_user_id() < 1 ) { wp_send_json_error( array( 'code' => 'sflo_forbidden', 'message' => __( 'Vous ne pouvez pas gérer cette bibliothèque.', 'ondavilo' ) ), 403 ); return false; } if ( false === check_ajax_referer( 'sflo_styles', 'nonce', false ) ) { wp_send_json_error( array( 'code' => 'sflo_invalid_nonce', 'message' => __( 'La session a expiré. Rechargez l’atelier avant de réessayer.', 'ondavilo' ) ), 403 ); return false; } return true; } function sflo_styles_load() { if ( ! sflo_styles_authorize() ) { return; } $state = sflo_styles_read_state( get_current_user_id() ); if ( is_wp_error( $state ) ) { sflo_styles_send_error( $state ); return; } wp_send_json_success( array( 'revision' => $state['revision'], 'items' => $state['items'] ) ); } add_action( 'wp_ajax_sflo_styles_load', 'sflo_styles_load' ); function sflo_styles_save() { if ( ! sflo_styles_authorize() ) { return; } // Authorization above verifies the nonce and edit_posts before reading input. // JSON must remain literal: bound raw bytes, then wp_unslash, decode and validate every retained field below. // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verified by sflo_styles_authorize(); JSON is unslashed and fully validated below before any write. $json = $_POST['records'] ?? null; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verified by sflo_styles_authorize(); strict decimal-only regex and integer bounds below reject escaped or non-scalar revisions. $revision = $_POST['revision'] ?? null; if ( ! is_string( $json ) || ! is_string( $revision ) || ! preg_match( '/\A(?:0|[1-9][0-9]{0,9})\z/D', $revision ) || (float) $revision > 2147483645 ) { sflo_styles_send_error( sflo_style_error( 'sflo_invalid_request', __( 'La requête d’enregistrement est invalide.', 'ondavilo' ) ) ); return; } // WordPress slashes request strings. Bound both forms before decoding local settings. if ( strlen( $json ) > 131072 ) { sflo_styles_send_error( sflo_style_error( 'sflo_payload_too_large', __( 'Le fichier de réglages est trop volumineux.', 'ondavilo' ), 413 ) ); return; } $json = wp_unslash( $json ); if ( strlen( $json ) > 65536 ) { sflo_styles_send_error( sflo_style_error( 'sflo_payload_too_large', __( 'Le fichier de réglages est trop volumineux.', 'ondavilo' ), 413 ) ); return; } // Preserve the JSON distinction between [] and {}, lost by associative decoding. if ( substr( ltrim( $json ), 0, 1 ) !== '[' ) { sflo_styles_send_error( sflo_style_error( 'sflo_invalid_json', __( 'Le fichier ne contient pas une liste de styles valide.', 'ondavilo' ) ) ); return; } try { $records = json_decode( $json, true, 16, JSON_THROW_ON_ERROR ); } catch ( JsonException $error ) { sflo_styles_send_error( sflo_style_error( 'sflo_invalid_json', __( 'Le fichier de réglages JSON est invalide.', 'ondavilo' ) ) ); return; } $items = sflo_styles_payload( $records ); if ( is_wp_error( $items ) ) { sflo_styles_send_error( $items ); return; } $user_id = get_current_user_id(); $state = sflo_styles_read_state( $user_id ); if ( is_wp_error( $state ) ) { sflo_styles_send_error( $state ); return; } if ( (int) $revision !== $state['revision'] ) { sflo_styles_send_error( sflo_style_error( 'sflo_conflict', __( 'La bibliothèque a changé dans une autre fenêtre. Rechargez-la avant d’enregistrer.', 'ondavilo' ), 409 ) ); return; } $next = array( 'revision' => $state['revision'] + 1, 'items' => $items ); if ( ! sflo_styles_json_fits( $next ) ) { sflo_styles_send_error( sflo_style_error( 'sflo_payload_too_large', __( 'La bibliothèque dépasse la taille maximale de 64 Ko.', 'ondavilo' ), 413 ) ); return; } // WordPress unslashes metadata values internally. Preserve user-entered backslashes. $saved = $state['_raw'] === null ? add_user_meta( $user_id, 'sflo_saved_styles', wp_slash( $next ), true ) : update_user_meta( $user_id, 'sflo_saved_styles', wp_slash( $next ), $state['_raw'] ); if ( false === $saved ) { sflo_styles_send_error( sflo_style_error( 'sflo_conflict', __( 'La bibliothèque n’a pas été remplacée. Rechargez-la avant de réessayer.', 'ondavilo' ), 409 ) ); return; } wp_send_json_success( $next ); } add_action( 'wp_ajax_sflo_styles_save', 'sflo_styles_save' );