( (int) ( $a['priority'] ?? 0 ) ); } ); return $rules; } /** Sauvegarde / met à jour une règle. */ public function save_rule( array $rule ): array { $rules = $this->get_rules(); $valid_types = class_exists( 'Aiforya_Schema_Markup_Detector' ) ? Aiforya_Schema_Markup_Detector::supported_schema_types() : array(); $schema_type = (string) ( $rule['schema_type'] ?? 'Article' ); if ( ! in_array( $schema_type, $valid_types, true ) ) $schema_type = 'Article'; $clean = array( 'id' => isset( $rule['id'] ) && (int) $rule['id'] > 0 ? (int) $rule['id'] : ( time() + wp_rand( 100, 999 ) ), 'name' => sanitize_text_field( (string) ( $rule['name'] ?? __( 'Unnamed rule', 'aiforya-schema-markup' ) ) ), 'enabled' => ! empty( $rule['enabled'] ) ? 1 : 0, 'priority' => max( 0, min( 999, (int) ( $rule['priority'] ?? 10 ) ) ), 'operator' => in_array( ( $rule['operator'] ?? 'AND' ), array( 'AND', 'OR' ), true ) ? $rule['operator'] : 'AND', 'schema_type' => $schema_type, 'conditions' => $this->sanitize_conditions( is_array( $rule['conditions'] ?? null ) ? $rule['conditions'] : array() ), ); // Update si l'id existe déjà, sinon append. $found = false; foreach ( $rules as $i => $r ) { if ( (int) ( $r['id'] ?? 0 ) === (int) $clean['id'] ) { $rules[ $i ] = $clean; $found = true; break; } } if ( ! $found ) $rules[] = $clean; update_option( 'aiforya_sm_rules', array_values( $rules ), false ); return $clean; } private function sanitize_conditions( array $conditions ): array { $out = array(); $allowed_types = array( 'post_type', 'category', 'tag', 'author', 'url_regex', 'title_contains', 'meta_field' ); foreach ( $conditions as $c ) { if ( ! is_array( $c ) ) continue; $type = (string) ( $c['type'] ?? '' ); if ( ! in_array( $type, $allowed_types, true ) ) continue; $op = (string) ( $c['op'] ?? 'is' ); $value = isset( $c['value'] ) ? sanitize_text_field( (string) $c['value'] ) : ''; $field = isset( $c['field'] ) ? sanitize_text_field( (string) $c['field'] ) : ''; $out[] = array( 'type' => $type, 'op' => $op, 'value' => $value, 'field' => $field ); } return $out; } public function delete_rule( int $rule_id ): bool { $rules = $this->get_rules(); $filtered = array_values( array_filter( $rules, function ( $r ) use ( $rule_id ) { return (int) ( $r['id'] ?? 0 ) !== $rule_id; } ) ); update_option( 'aiforya_sm_rules', $filtered, false ); return true; } /** * Évalue les règles pour un post — retourne le schema_type si une règle match, sinon null. */ public function resolve_for_post( int $post_id ): ?string { $post = get_post( $post_id ); if ( ! $post ) return null; foreach ( $this->get_rules() as $rule ) { if ( empty( $rule['enabled'] ) ) continue; if ( $this->rule_matches_post( $rule, $post ) ) { return (string) $rule['schema_type']; } } return null; } private function rule_matches_post( array $rule, $post ): bool { $conditions = is_array( $rule['conditions'] ?? null ) ? $rule['conditions'] : array(); if ( empty( $conditions ) ) return false; $operator = $rule['operator'] ?? 'AND'; foreach ( $conditions as $c ) { $match = $this->evaluate_condition( $c, $post ); if ( $operator === 'AND' && ! $match ) return false; if ( $operator === 'OR' && $match ) return true; } return $operator === 'AND'; // AND : toutes ont matché } private function evaluate_condition( array $c, $post ): bool { $type = (string) ( $c['type'] ?? '' ); $op = (string) ( $c['op'] ?? '' ); $val = (string) ( $c['value'] ?? '' ); $field = (string) ( $c['field'] ?? '' ); switch ( $type ) { case 'post_type': if ( $op === 'is' ) return $post->post_type === $val; if ( $op === 'is_not' ) return $post->post_type !== $val; if ( $op === 'in' ) { $list = array_map( 'trim', explode( ',', $val ) ); return in_array( $post->post_type, $list, true ); } return false; case 'category': $cats = wp_get_post_categories( $post->ID, array( 'fields' => 'slugs' ) ); $cats = is_array( $cats ) ? $cats : array(); if ( $op === 'is' ) return in_array( $val, $cats, true ); if ( $op === 'is_not' ) return ! in_array( $val, $cats, true ); return false; case 'tag': $tags = wp_get_post_tags( $post->ID, array( 'fields' => 'slugs' ) ); $tags = is_array( $tags ) ? $tags : array(); if ( $op === 'has' ) return in_array( $val, $tags, true ); if ( $op === 'has_not' ) return ! in_array( $val, $tags, true ); return false; case 'author': if ( $op === 'is' ) return ( (int) $post->post_author ) === ( (int) $val ); return false; case 'url_regex': $url = get_permalink( $post->ID ); $pattern = '#' . str_replace( '#', '\\#', $val ) . '#i'; $r = @preg_match( $pattern, (string) $url ); return $r === 1; case 'title_contains': $title = wp_strip_all_tags( $post->post_title ); if ( $op === 'contains' ) return stripos( $title, $val ) !== false; if ( $op === 'starts_with' ) return stripos( $title, $val ) === 0; if ( $op === 'ends_with' ) return $val !== '' && substr_compare( $title, $val, -strlen( $val ), strlen( $val ), true ) === 0; return false; case 'meta_field': // Lit le meta directement — supporte ACF / Metabox / Pods (qui stockent tous en postmeta natif). $key = $field !== '' ? $field : $val; $meta = get_post_meta( $post->ID, $key, true ); if ( $op === 'exists' ) return $meta !== '' && $meta !== null && $meta !== false && $meta !== array(); if ( $op === 'not_empty' ) return ! empty( $meta ); if ( $op === 'equals' ) return (string) $meta === $val; if ( $op === 'contains' && is_string( $meta ) ) return stripos( $meta, $val ) !== false; return false; } return false; } /** * Helper : expose les meta_keys distincts du site pour l'autocomplete UI. */ public function discover_meta_keys( int $limit = 100 ): array { global $wpdb; // Les deux motifs LIKE sont des littéraux voulus (exclure les meta_keys privées, puis // les nôtres) : le % y est le joker qu'on veut, pas une valeur à échapper. Aucune // entrée utilisateur n'entre dans ces motifs — seul $limit est variable, et il passe // par prepare(). $rows = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT meta_key FROM {$wpdb->postmeta} WHERE meta_key NOT LIKE '\\_%' AND meta_key NOT LIKE 'aiforya_sm_%' ORDER BY meta_key ASC LIMIT %d", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery -- motifs LIKE littéraux, sans donnée utilisateur. $limit ) ); return is_array( $rows ) ? $rows : array(); } }