Langue

Me contacter

Extension WooCommerce · depuis 2018 · aka WOOBE

BEAR

Un éditeur en masse de produits WooCommerce de type tableur, avec un serveur MCP qui permet à des agents IA de modifier la boutique en toute sécurité.

Première version
Installations actives
30 000+
Note
4.7 / 5 (227)Lire les avis
  • PHP
  • WordPress
  • WooCommerce CRUD (WC_Product)
  • WP_Query
  • $wpdb / MySQL
  • WP REST API
  • JSON-RPC 2.0
  • Model Context Protocol
  • jQuery
  • DataTables

À propos

BEAR présente le catalogue WooCommerce sous la forme d'un tableau modifiable et change des milliers de produits en une seule fois : prix, stock, SKU, taxonomies, attributs, variations et méta personnalisées. Chaque modification passe par un unique chemin d'écriture de champ, qui utilise les objets produit de WooCommerce pour les propriétés du produit, est enregistrée avec sa valeur précédente et peut être annulée depuis l'onglet Historique.

La difficulté n'est pas le tableau. Elle consiste à rendre une écriture en masse correcte sur une vraie boutique : des valeurs stockées à cinq endroits différents, des produits variables dont les prix et le stock se trouvent sur leurs variations, des effets de bord de WooCommerce tels que le statut de stock déduit de la quantité en stock, et un hébergement mutualisé qui interrompt une requête PHP au bout de 30 secondes.

Depuis 2026, le plugin fait aussi tourner un serveur MCP. Un agent IA peut trouver des produits, prévisualiser une modification, l'appliquer par portions et l'annuler, avec les mêmes moteurs de filtre, de modification en masse et d'historique que ceux de l'écran, derrière des clés, un jeton de session confirmé (facultatif pour la clé de toute la boutique, toujours actif pour les clés personnelles) et une carte des permissions par secteur.

Les versions gratuite et premium partagent une seule base de code, commutée par un seul indicateur ; la version gratuite limite la taille de page, les champs méta, la profondeur de l'historique et le débit d'écriture MCP.

BEAR · éditeur en masse

ce qui a été résolu

EN LIGNEdepuis 2018

Grille de produits et modèle de champs

shipped · 2018

Ce que cela apporteLe personnel peut voir et modifier n'importe quel détail d'un produit ou d'une variation dans un seul écran de type tableur, même sur des catalogues trop volumineux pour être ouverts d'un coup.

Problème
Un seul écran doit afficher et modifier n'importe quelle valeur d'un catalogue de n'importe quelle taille : champs d'article, propriétés WooCommerce, taxonomies, attributs, téléchargements et méta arbitraires, pour les produits et leurs variations.
Solution

Un registre de champs déclaratif pilote chaque colonne et chaque écriture, et le tableau est paginé côté serveur, une page WP_Query par requête AJAX.

Stack
  • WP_Query
  • DataTables en mode serveur
  • Setters de WC_Product
  • wp_options
Tâches · 6
Les grands catalogues ne peuvent pas être chargés d'un coup dans le navigateurPagination côté serveur : chaque page du tableau est une WP_Query avec décalage, tri et filtres

woobe_get_products() reçoit start, length et order de DataTables et les transmet à WOOBE_PRODUCTS::gets(), qui construit la WP_Query. Le tri sur une colonne méta passe à meta_value_num pour les types number, timestamp et unix, et à meta_value dans les autres cas.

La taille de page est plafonnée à 100 lignes dans le modèle de réglages, avec le commentaire que des pages plus grandes provoquaient des erreurs HTTP 500 sur les serveurs peu puissants ; la version gratuite est fixée à 10. Lorsque l'option des variations est activée, les variations de chaque produit variable sont interrogées et insérées en lignes juste après leur parent.

Chaque type de colonne est stocké différemment dans WooCommerceUne entrée de registre par colonne : type de stockage, type de valeur, éditeur, filtre de nettoyage, droits

woobe_get_fields() décrit chaque colonne : field_type (field, meta, prop, taxonomy, attribute, downloads, gallery, upsells et d'autres), type de valeur, vue d'édition, fonction de nettoyage, types de produits auxquels elle ne s'applique pas, et visibilité pour les gestionnaires de boutique. Les attributs globaux et toutes les taxonomies de produits sont ajoutés à l'exécution à partir de wc_get_attribute_taxonomies() et get_object_taxonomies().

update_page_field() aiguille selon field_type : update_post_meta pour les méta, les setters de WC_Product puis save() pour les propriétés, wp_set_post_terms pour les taxonomies, des listes WC_Product_Attribute reconstruites pour les attributs. Les cellules de la grille, les retours en arrière de l'historique et la plupart des écritures en masse aboutissent dans cette fonction. Les modifications en masse de taxonomies et d'attributs écrivent directement les termes et déclenchent le même hook d'historique avant d'écrire.

Les clés méta personnalisées doivent être distinguées des clés internes de WooCommerceUne sous-classe de WC_Product_Data_Store_CPT expose sa liste protégée de clés internes

woobe_meta_get_keys() lit toutes les clés méta d'un produit échantillon et en soustrait les clés internes de WooCommerce. Cette liste est une propriété protégée du data store des produits ; WOOBE_PDS_CPT étend donc le store uniquement pour la renvoyer, en ajoutant _button_text et _product_url.

Les clés méta sont débarrassées de leurs espaces mais ni passées en minuscules ni nettoyées, car les clés réelles écrites par d'autres plugins, comme _woocs_sale_price_USD, sont sensibles à la casse.

Un champ méta typé doit fonctionner dans les colonnes, les filtres et la modification en masseUne définition de méta est répercutée par des hooks dans le registre, le panneau de filtres et le panneau de modification en masse

woobe_extend_fields() transforme chaque définition de méta enregistrée en colonne ; un type number reçoit la fonction de nettoyage floatval et un tri numérique. La même définition alimente woobe_filter_numbers (plage de/à), woobe_filter_text (LIKE, =, !=, NOT LIKE, vide, non vide), les filtres à interrupteur et woobe_bulk_number (définir, augmenter ou diminuer d'une valeur ou d'un pourcentage).

Une colonne à interrupteur peut stocker d'autres valeurs que 1/0 (par exemple yes/no) grâce à une règle de remplacement par clé lue par over_switcher_swicher_to_val(). La version gratuite conserve les 2 premiers champs méta.

Une méta de type tableau sérialisé n'a pas de cellule plate à modifierL'éditeur en fenêtre envoie clés et valeurs sous forme de tableaux parallèles, reconstruits en tableaux PHP imbriqués

process_jsoned_meta_data() reconstruit une valeur méta à partir de paires keys/values pour les tableaux et keys2/values2 pour les objets, décode les valeurs JSON imbriquées et se rabat sur la valeur brute pour les chaînes et nombres simples. L'historique stocke ces valeurs en JSON avec JSON_HEX_QUOT et JSON_HEX_TAG pour qu'elles survivent à l'aller-retour.

L'état de l'éditeur propre à chaque utilisateur doit survivre aux vidages du cache d'objetsUne ligne d'option non chargée automatiquement par utilisateur, avec un TTL par clé, au lieu de transients

Les filtres et les configurations de modification en masse sont conservés d'une requête à l'autre par WOOBE_STORAGE. Avec Redis ou Memcached, un transient ne vit que dans le cache d'objets et disparaît lors d'un vidage ; le stockage par défaut est donc une ligne d'option woobe_memory_<user id> : chaque clé porte sa propre expiration, les clés expirées sont supprimées à l'écriture, l'autoload est désactivé et la ligne est supprimée avec l'utilisateur. Les modes session, transient et cookie restent disponibles derrière le filtre woobe_storage_type.

Filtrage

shipped · 2018

Ce que cela apporteLe personnel peut choisir exactement les produits dont il a besoin, y compris selon le prix ou le stock d'une variation, et la même sélection sert à la modification et à l'export.

Problème
Les filtres sur les prix, le stock, les dimensions, le texte et les taxonomies doivent sélectionner exactement les bons produits, y compris les produits variables dont les valeurs se trouvent sur les variations, et le même ensemble doit être réutilisé par la pagination, la modification en masse et l'export.
Solution

Le filtre est stocké sur le serveur sous une clé et compilé à chaque requête en clauses tax_query, meta_query et posts_where par un seul hook WP_Query.

Stack
  • WP_Query
  • tax_query
  • meta_query
  • posts_where
  • $wpdb->prepare
  • MySQL REGEXP
Tâches · 5
Les requêtes de pagination, de modification en masse et d'export doivent voir le même ensemble filtréFiltre enregistré sous une clé aléatoire à chaque exécution ; un seul hook WP_Query le reconstruit à chaque requête

woobe_filter_products() stocke le formulaire de filtre analysé sous woobe_filter_<key>, où la clé est une chaîne aléatoire générée à chaque filtrage ; la modification en masse par filtre fonctionne ainsi dans plusieurs onglets du navigateur à la fois. Chaque appel de WOOBE_PRODUCTS::gets() fait passer ses arguments par woobe_apply_query_filter_data(), qui lit le filtre enregistré et renvoie tax_query, meta_query, post__in et post_status, et attache des hooks posts_where pour le reste.

La pagination de la grille, le comptage pour la modification en masse, le comptage pour l'export et les outils de variations appellent tous gets() ; ils ne peuvent donc pas diverger sur les produits que désigne un filtre.

Le prix et le stock se trouvent sur les variations, mais la grille liste les parentsRequête par plage sur les produits et les variations, puis correspondances de variations rattachées à leurs parents

regular_price_where(), sale_price_where(), stock_quantity_where() et dimensions_where() interrogent postmeta pour product et product_variation, puis lisent post_parent des variations trouvées et restreignent la requête principale aux parents plus les variations avec ID IN (...).

Quand rien ne correspond, la clause devient ID = -1 : une correspondance vide ne renvoie aucun produit au lieu de tout le catalogue, ce qui compte à un clic d'une modification en masse.

Les nombres stockés dans des chaînes méta se comparent mal dans MySQLCAST en DECIMAL avec une échelle explicite ; chaînes vides exclues de toute plage

Les plages de nombres personnalisées utilisent le type de meta_query DECIMAL(30,20) : un DECIMAL nu a une échelle de 0, si bien qu'une recherche de 100 à 150 trouverait aussi 99.xx. Le SQL des prix convertit en DECIMAL(30,10).

Un poids vide est stocké sous la forme '' et MySQL lit '' comme 0 ; les plages de prix et de dimensions exigent donc meta_value <> '' et une case vide ne compte jamais pour 0. Une borne absente d'une plage est ouverte (remplacée par -999999999 ou 999999999), et un « de » égal au « à » signifie exactement cette valeur.

Les utilisateurs ont besoin de AND, OR et NOT dans un seul champ de recherche de textePetite syntaxe de recherche (^ OR, ^~ AND, ^! NOT) compilée en clauses MySQL REGEXP

posts_txt_where() découpe le texte sur ^ ; un préfixe ~ relie la partie par AND, un préfixe ! la transforme en NOT REGEXP dans un groupe AND distinct. Les comportements exact, begin, end, not et empty correspondent à = et à un REGEXP ancré ; les crochets et accolades de regex sont échappés. Cela s'applique au titre, au contenu, à l'extrait et au slug.

Le filtre SKU accepte une liste séparée par des virgules et construit des conditions LIKE préparées avec esc_like, OR pour les modes de correspondance et AND pour « not ».

Faire correspondre tous les termes d'un ensemble demande une clause tax_query par termeAND découpé en une clause par terme ; les opérateurs inconnus se rabattent sur IN

Pour l'opérateur AND, chaque terme devient sa propre clause tax_query sous la relation AND, car une clause unique avec l'opérateur AND peut ne renvoyer aucun produit. Les opérateurs sont restreints à une liste blanche : IN, NOT IN, AND, EXISTS et NOT EXISTS ; WordPress abandonne une clause dont l'opérateur est inconnu et ferait correspondre tous les produits.

La visibilité dans le catalogue est traduite en combinaisons des termes product_visibility exclude-from-catalog et exclude-from-search, et un filtre de statut de stock limite aussi product_type aux types qui ont un statut de stock.

Moteur de modification en masse

shipped · 2018

Ce que cela apporteLes modifications de prix, de stock et de variations sur des milliers de produits se terminent de façon fiable sur un hébergement ordinaire et suivent les propres règles de prix et de stock de la boutique.

Problème
Une modification en masse peut toucher des milliers de produits sur un hébergement aux limites de temps PHP courtes, et chaque écriture doit respecter les règles de WooCommerce pour les prix, le stock et les variations.
Solution

Les ID cibles sont résolus une seule fois, puis écrits par petits lots AJAX via le même chemin d'écriture champ par champ que celui de la grille, qui porte les règles de WooCommerce.

Stack
  • Traitement AJAX par lots
  • WC_Product
  • WC_Product_Variation
  • WC_Product_Attribute
Tâches · 6
Une modification en masse de milliers de produits se heurte aux limites de temps PHPID récupérés une fois, puis envoyés par lots de N par requête AJAX ; N est un réglage

woobe_bulk_products_count() stocke la configuration de la modification en masse sous un bulk_key aléatoire de 16 caractères et renvoie chaque ID filtré (fields=ids, sans pagination). __woobe_bulk_products() dans bulk.js découpe cette liste, envoie un lot après l'autre, met à jour la barre de progression et appelle woobe_bulk_finish à la fin ; l'utilisateur peut interrompre la requête en cours.

La taille de lot est number_posts_processed, de 10 à 100 par requête, 30 par défaut, de sorte qu'un serveur peu puissant peut sacrifier de la vitesse pour de la stabilité.

Les produits modifiés sortent du filtre pendant l'exécutionListe d'ID figée : chaque lot porte des ID issus de l'instantané, et non d'une requête relancée

Paginer la requête filtrée entre les lots ferait sauter des produits : une modification de prix fait sortir un produit d'un filtre de prix et tous les décalages suivants bougent. La liste d'ID prise au départ est conservée par le client et chaque requête de lot envoie ses ID explicitement, sans la clé de filtre.

Après l'exécution, les lignes cochées qui n'apparaissent plus dans le tableau redessiné sont retirées de la sélection, de sorte que l'action suivante ne cible pas des produits que l'utilisateur ne voit plus.

Les comportements numériques doivent se combiner avec des formules et l'arrondi des prixChaîne de traitement par produit : comportement, formule avec une autre méta, plage aléatoire, arrondi

_process_number_data() applique set, augmenter ou diminuer d'une valeur ou d'un pourcentage, ainsi que des règles entre champs, par exemple prix promotionnel = prix normal moins un pourcentage. Vient ensuite une formule facultative avec un autre champ méta (+, -, *, /, la division par zéro étant neutralisée), puis un montant aléatoire facultatif dans une plage avec un nombre de décimales défini.

update_page_field() arrondit selon les décimales de prix de la boutique : à l'unité, au 0.05 supérieur, au 0.1 supérieur, ou à une terminaison fixe en centimes comme .99. Les champs de texte acceptent append, prepend, replace (sensible à la casse ou non) et des variables telles que {TITLE}, {SKU}, {meta:key} et {attribute:pa_size}.

Le prix promotionnel doit rester inférieur au prix normal et les dates doivent prendre effetPromo >= normal ignorée, promo <= 0 la supprime ; _price recalculé quand les dates changent

Dans une exécution en masse, un prix promotionnel calculé qui atteint le prix normal est ignoré, et une valeur de zéro ou moins supprime la promotion. Une écriture isolée d'un prix promotionnel égal ou supérieur au prix normal est ramenée au prix normal moins la plus petite unité de prix.

Quand les dates de promotion sont écrites, _price est immédiatement fixé au prix qui s'applique maintenant (promotion pas encore commencée, en cours, terminée) et les transients du produit sont vidés, au lieu d'attendre la tâche planifiée de promotion de WooCommerce.

Les écritures de stock et de prix ne doivent pas abîmer les produits variables ni le stock partagéParents variables et variations au stock parent laissés intacts ; stock conservé lors des bascules

WooCommerce lit les prix d'un produit variable sur ses variations ; un prix écrit sur le parent est donc ignoré et une écriture de suppression retire tout prix résiduel du parent. Un stock de 0 ou moins active normalement la gestion du stock ; ce n'est pas fait sur un parent variable qui ne suit pas le stock, où cela mettrait en rupture toutes les variations sans stock propre, ni sur une variation qui vend à partir du stock de son parent, qu'on détacherait ainsi.

WooCommerce efface la quantité, les commandes en rupture et le seuil de stock bas quand la gestion du stock est désactivée. keep_stock_on_switch() les enregistre dans sa propre méta et les restaure quand la gestion est réactivée.

Les variations doivent être créées et modifiées par combinaison d'attributsProduit cartésien des termes choisis, sans doublons ; modifications associées par combinaison avec joker

generate_combinations() construit le produit cartésien des termes d'attributs sélectionnés. Son application rattache les attributs comme attributs de variation, saute les combinaisons que le produit possède déjà, copie les prix du parent, donne à chaque nouvelle variation le SKU parent-SKU-<id> pour éviter les doublons, et retrie les variations via le data store.

La modification, la suppression et l'export d'un sous-ensemble de variations comparent les attributs de chaque variation avec les combinaisons choisies : clés et valeurs triées, entrées '-1' traitées comme n'importe quelle valeur, et les deux côtés décodés par URL, car les slugs d'attributs non latins sont stockés encodés.

Historique et annulation

dans le produit

Ce que cela apporteUne mauvaise modification, manuelle, en masse ou faite par un assistant IA, peut être annulée au lieu d'être corrigée produit par produit.

Problème
Chaque modification, manuelle, en masse ou venant d'un agent IA, doit pouvoir être annulée, y compris les effets de bord que WooCommerce applique de lui-même, et l'annulation d'une grosse exécution se heurte aux mêmes limites de temps que l'exécution.
Solution

Les valeurs précédentes sont enregistrées champ par champ dans deux tables personnalisées par un hook placé sur le chemin d'écriture unique, puis rétablies par portions via le même chemin.

Stack
  • Tables MySQL personnalisées
  • $wpdb
  • Actions WordPress
Tâches · 6
Enregistrer les valeurs d'avant pour n'importe quel type de champ sans code propre à chaque fonctionnalitéUn hook avant chaque écriture de champ ; valeur précédente stockée par champ dans une table personnalisée

update_page_field() déclenche woobe_before_update_page_field et WOOBE_HISTORY::write() lit la valeur actuelle et l'encode selon son type : ID de termes, attributs, galerie et relations en JSON, téléchargements en nom/fichier/hash, statut de stock en 1/0, dates de promotion en dates ISO avec leur décalage de fuseau horaire, de sorte qu'un retour en arrière restaure le calendrier exact.

Les lignes contiennent field_key, product_id, prev_val, mod_date, bulk_key, user_id et via_mcp, indexés par produit, clé de masse et utilisateur. Une seconde table conserve un en-tête par exécution en masse : état (completed ou terminated), début, fin, nombre de produits et clés des champs.

WooCommerce change le statut de stock comme effet de bord des modifications de stockEffets de bord écrits comme lignes supplémentaires d'une même modification et rétablis ensemble, dans l'ordre

Activer la gestion du stock, ou un stock de 0 sur un produit non suivi, fait déduire le statut de stock par WooCommerce ; désactiver la gestion supprime aussi la quantité. update_page_field() enregistre stock_status, et stock_quantity si nécessaire, comme lignes supplémentaires juste après le champ modifié.

stock_status_pair() et stock_row_at() retrouvent ces lignes par des ID adjacents (+1, +2) et exigent le même produit, le même auteur, le même canal (à la main ou via un agent) et la même clé de masse, ainsi qu'un horaire d'écriture proche. revert() restaure ensuite la quantité avant le statut, car WooCommerce déduit le statut de la quantité tant que le stock est géré. Un stock précédent vide signifie « non suivi » ; untrack_stock() désactive donc la gestion au lieu d'écrire 0.

L'annulation d'une exécution en masse de milliers de lignes dépasse aussi le temps impartiAnnulation par portions selon bulk_key ; chaque ligne rétablie est supprimée ; en-tête retiré à la fin

Dans l'onglet Historique, woobe_history_revert_bulk_portion() rétablit 10 lignes par requête AJAX avec une barre de progression, et la ligne d'en-tête est supprimée quand le compteur atteint le total. Chaque annulation supprime sa ligne, si bien que la portion suivante prend simplement ce qui reste.

L'outil d'annulation MCP suit un chemin distinct : revert_bulk_portion() rétablit jusqu'à 200 lignes par appel et indique les lignes réellement supprimées, car les deux lignes d'une modification de stock sont rétablies ensemble et une portion peut prendre une ligne de plus que sa limite.

revert() retire d'abord tous les écouteurs de woobe_before_update_page_field, afin que les propres écritures de l'annulation ne soient pas enregistrées comme un nouvel historique.

Plusieurs utilisateurs et agents IA partagent un même historiqueUne seule règle scope() : les administrateurs voient toutes les lignes, les autres les leurs ; appliquée côté serveur

Chaque ligne porte user_id et via_mcp. La clé MCP de toute la boutique écrit en tant qu'utilisateur -777, qui ne peut jamais entrer en collision avec un vrai utilisateur ni avec 0 (non connecté). scope() renvoie une condition SQL unique utilisée par chaque liste, annulation, suppression et vidage, de sorte qu'aucune requête ne peut s'écarter de la règle.

Les gestionnaires vérifient solo_in_scope() ou bulk_in_scope() et répondent 403 à une requête construite à la main pour une ligne que l'utilisateur ne peut pas voir. Les noms d'auteurs de la liste sont lus en une seule requête.

Les installations existantes avaient besoin d'une nouvelle colonne sans casser les anciennes lignesOption de schéma versionnée, ALTER protégé par une vérification de colonne, rattrapage des lignes d'agent

CREATE TABLE IF NOT EXISTS ne modifie jamais une table qui existe déjà. maybe_upgrade_schema() s'exécute une fois par version de schéma (actuellement 2), vérifie SHOW COLUMNS avant chaque ALTER pour qu'une requête concurrente n'échoue pas dessus, réessaie au chargement suivant si ALTER échoue, et marque les anciennes lignes de l'utilisateur -777 comme écrites via un agent.

La version gratuite garde un historique court sans toucher aux autres utilisateursÉlagage par auteur à l'arrêt : 2 dernières modifications isolées et 2 en masse, lignes de stock conservées

Dans la version gratuite, prune_author() conserve les deux modifications isolées les plus récentes et les deux exécutions en masse les plus récentes d'un auteur. Les lignes qui appartiennent à une modification de stock conservée sont conservées avec elle grâce à stock_change_rows(). L'élagage s'exécute à l'arrêt après une écriture, et l'ouverture de l'onglet ne supprime jamais les lignes d'un autre utilisateur.

Export

dans le produit

Ce que cela apporteLes listes de produits filtrées s'exportent vers des fichiers que la boutique peut réimporter, construits par parties sur des serveurs peu puissants et jamais laissés ouverts sur le web.

Problème
Un export de catalogue filtré avec variations doit s'exécuter par parties sur des serveurs peu puissants, produire des fichiers que WooCommerce peut réimporter, et ne pas laisser les données produits ouvertes sur le web.
Solution

L'export s'exécute comme une suite de requêtes AJAX qui complètent un même fichier dont le nom est dérivé pour chaque export, conservé dans un dossier fermé et servi par un gestionnaire autorisé.

Stack
  • fputcsv
  • DOMDocument (WXR)
  • hash_hmac
  • .htaccess
  • WP REST API
Tâches · 4
Un gros export ne peut pas être construit en une seule requêteEn-tête écrit d'abord, puis lignes ajoutées par lot AJAX au même fichier

woobe_export_products_count() efface les fichiers précédents, écrit l'en-tête CSV ou le squelette XML et renvoie les ID filtrés. woobe_export_products() ajoute un lot par requête avec fopen(a+) et fputcsv, car WP_Filesystem ne sait pas écrire ligne par ligne en flux ; pour le XML, il charge le document WXR et ajoute des nœuds item.

Les variations sont ajoutées après leur parent, éventuellement sans celles en rupture de stock, et peuvent être limitées à certaines combinaisons d'attributs ou en exclure.

Le CSV doit se charger dans l'importateur de produits natif de WooCommerceAttributs sur quatre colonnes chacun, téléchargements en paires nom/URL, toute valeur aplatie

get_csv_titles() écrit « Attribute N value(s) », « name », « visible » et « global » par attribut, ainsi que « Download N name » et « URL » par emplacement de fichier, soit les colonnes que lit l'importateur WooCommerce ; les colonnes méta sont nommées « Meta: key ».

flatten_export_value() réduit les tableaux et objets (termes, attributs, objets de données WC) à des scalaires joints par '|', de sorte qu'un type de colonne que le plugin n'a jamais vu, y compris tiers, n'écrit pas le mot « Array ».

Les fichiers d'export contiennent des prix et des URL de fichiers et ne doivent pas être publicsDossier fermé par .htaccess, nom de fichier avec suffixe HMAC, téléchargement via un gestionnaire contrôlé

Le suffixe du nom de fichier doit correspondre à un horodatage à la minute et reçoit une queue de 12 caractères issue de hash_hmac('sha256', postfix, wp_salt('auth')) : identique pour toutes les requêtes d'un même export, si bien que tous les lots aboutissent dans un seul fichier, mais impossible à deviner de l'extérieur. Cette queue compte sur nginx, qui ignore la règle de refus de .htaccess.

woobe_export_download() vérifie le nonce et la capacité, n'accepte que les noms que l'extension génère, vide tous les tampons de sortie et diffuse avec readfile() sans Content-Length. Les fichiers de plus de 8 heures sont supprimés, la vérification ayant lieu au plus une fois par heure.

Un agent IA a besoin d'un lien de téléchargement sans exposer sa cléRoute REST identifiée par un jeton de 32 caractères hexadécimaux, valable pour un seul fichier et expirant au bout de 15 minutes

Un clic dans le navigateur ne peut pas envoyer d'en-tête Authorization, et la clé MCP est tenue hors des URL. L'outil d'export stocke le nom du fichier sous un jeton aléatoire dans un transient d'une durée de vie de 900 secondes, et /woobe/v1/mcp/export/<token> ne sert que ce fichier.

Serveur MCP pour agents IA

shipped · 2026

Ce que cela apporteUn assistant IA peut modifier la boutique pour son propriétaire, mais uniquement sur une sélection confirmée, avec un aperçu préalable et chaque modification réversible.

Problème
Un agent IA travaille par une API réseau, sans écran, peut mal lire un filtre ou un décompte, et peut émettre une écriture sur tout le catalogue ; il doit pourtant rester sûr, réversible et compatible avec les limites de temps de PHP.
Solution

Les outils réutilisent les propres moteurs de filtre, de modification en masse et d'historique de l'éditeur ; les écritures n'acceptent que des sélections figées, confirmées par leur nombre, peuvent être vérifiées d'abord avec un outil d'aperçu à blanc, et s'exécutent par portions reprenables.

Stack
  • WP REST API
  • JSON-RPC 2.0
  • Model Context Protocol
  • wp_options
Tâches · 6
Un filtre mal lu ne doit pas étendre une écriture à tout le catalogueLes écritures n'acceptent qu'un selection_id figé plus un confirm_count égal à sa taille

woobe_find_products exécute le filtre une fois et stocke les ID dans une option non chargée automatiquement (jusqu'à 200,000 ID, valable 24 heures). La réponse contient selection_id, count, un échantillon et whole_catalogue, mesuré par rapport à une requête non filtrée, de sorte qu'un filtre qui ne restreint rien est signalé comme tel.

woobe_apply_bulk n'accepte aucun filtre. Il refuse tant que confirm_count n'est pas égal à la taille de la sélection : un agent qui n'a pas lu le nombre ne peut donc pas le deviner. Les produits créés après la recherche ne font pas partie de la sélection.

Le filtre d'un agent doit être refusé, et non ignoré, quand le moteur ne peut pas l'appliquerUn validateur strict refuse toute clé ou forme que le moteur n'appliquerait pas, et dit pourquoi

Le moteur de filtre lit les champs du propre formulaire de l'éditeur et ignore toute autre clé. Un agent peut envoyer une clé mal orthographiée ou une date sous la forme {from, to}, et une condition ignorée élargirait la sélection. WOOBE_MCP_FILTER vérifie chaque clé par rapport à la liste de champs que lit le moteur, refuse en nommant la clé et la forme acceptée, et transmet le reste sous la forme qu'envoie l'écran.

Le moteur attache des hooks posts_where qui restent actifs jusqu'à la fin de la requête, ce qui correspond à un chargement de page sur l'écran mais peut représenter plusieurs recherches dans un même lot JSON-RPC. release_filter_hooks() les retire après chaque requête MCP, de sorte qu'une recherche n'hérite jamais des conditions d'une autre. Les types des arguments sont vérifiés par rapport à l'inputSchema de chaque outil ; la chaîne 'maybe' ne vaut donc pas confirmation.

L'aperçu doit montrer ce que l'écriture produira réellementL'essai à blanc reproduit l'arithmétique de la modification en masse et enchaîne les opérations d'une exécution dans l'ordre

simulate() copie littéralement _process_number_data(), et run_chain() transmet chaque résultat à l'opération suivante ; un prix promotionnel défini en même temps qu'un nouveau prix normal est donc vérifié par rapport au nouveau. L'arithmétique ne s'applique qu'aux champs numériques sur lesquels le moteur calcule ; tout autre champ est présenté comme un simple remplacement.

L'aperçu renvoie trois décomptes (sélection, concernés après extension des parents en variations, inscriptibles) et des avertissements : pourcentage d'un champ vide ignoré, parents variables sans prix propre, stock non suivi.

Un appel REST a la même limite de temps qu'un lot AJAXTronçons de 25 via do_bulk, 20 s par appel, reprise par bulk_key et next_offset

La boucle d'application découpe la file en tronçons de 25 et appelle WOOBE_BULK::do_bulk(), le moteur qu'utilise l'écran, jusqu'à ce que 20 secondes se soient écoulées. Elle renvoie next_offset et bulk_key ; une reprise écrit dans le même en-tête d'historique, de sorte qu'une seule annulation défait tout le travail. Les caches sont vidés une fois après les écritures, et non à chaque tronçon.

Dans la version gratuite, le tronçon se réduit à l'allocation d'écriture restante : un seau à jetons de 100 produits qui se remplit en continu sur 30 minutes, avec des jetons fractionnaires et un plafond strict, de sorte que le temps d'inactivité ne peut pas financer une grosse exécution.

Les variations ne portent aucun des termes ni des méta sur lesquels le filtre s'appuieEnfants ajoutés après la requête : tous, ou seulement ceux qui correspondent à chaque attribut nommé

include_variations 'all' ajoute chaque enfant des parents trouvés ; 'matching' conserve les enfants dont les valeurs d'attributs correspondent à tous les termes pa_ du filtre (« jaune et petit » est une seule variation, pas l'union), une valeur vide comptant pour « n'importe laquelle ». Les catégories et les étiquettes ne peuvent pas restreindre les enfants.

Les clés d'attributs sont construites avec sanitize_title(), comme le fait WooCommerce, car les noms d'attributs non latins sont stockés encodés en URL et sanitize_key() les supprimerait. En mode matching, les parents variables sont retirés de la sélection pour qu'un variations_only ultérieur ne les redéveloppe pas en toutes leurs variations.

Dans un appel REST, le plugin et la boutique se comportent autrement que dans l'administrationDémarrage propre sur rest_api_init, filtres de prix des devises retirés, termes parents ajoutés

WOOBE::init() s'exécute sur init, avant que REST_REQUEST soit défini ; lors d'un appel REST, il ne voit donc ni l'administration ni REST et ne ferait rien. bootstrap.php s'accroche lui-même à rest_api_init et démarre le plugin ; is_should_init() n'autorise REST que pour les routes du plugin, pas pour tout le trafic REST du site.

Les sélecteurs de devises filtrent les getters de prix, de sorte que l'agent lisait des prix convertis ; strip_price_filters() retire ces filtres pour la seule requête MCP. Un produit placé dans une sous-catégorie est aussi placé dans ses ancêtres par un écouteur de set_object_terms, car personne ne coche les cases des parents dans un chat.

Contrôle d'accès et sécurité

shipped · 2026

Ce que cela apporteSeules les bonnes personnes peuvent modifier les produits ; avec le second facteur facultatif activé, une clé d'accès IA qui a fuité ne suffit pas à ouvrir la boutique, et l'accès peut être limité par domaine.

Problème
Un éditeur en masse détient un accès en écriture à chaque produit. Chaque gestionnaire AJAX, l'historique et le point d'accès MCP doivent vérifier qui agit, et une clé MCP conservée dans les réglages d'un assistant peut fuiter.
Solution

Une garde unique pour tous les gestionnaires AJAX avec des contrôles de propriété par produit ; pour MCP, des clés transmises uniquement par en-tête et comparées en temps constant, un jeton de session confirmé (facultatif pour la clé de toute la boutique, toujours actif pour les clés personnelles), des clés personnelles et une carte des permissions par secteur.

Stack
  • Nonces WordPress
  • current_user_can / capacités méta
  • hash_equals
  • random_int
  • WP REST permission_callback
Tâches · 5
Des dizaines de gestionnaires AJAX avaient chacun leurs propres contrôles partielsGarde unique : nonce, capacité, puis edit_post par produit, via le parent pour les variations

Après un rapport WPScan, l'ensemble des gestionnaires AJAX a été audité, et pas seulement ceux signalés, et chaque gestionnaire a été déplacé vers WOOBE_HELPER::check_ajax_access(). Elle vérifie le nonce, puis la capacité, puis current_user_can('edit_post') pour chaque ID de produit.

Les variations sont vérifiées par rapport au produit parent : WooCommerce fait correspondre edit_post sur une variation au simple edit_products, qui ignore la propriété, et les variations ont un post_author à 0. get_request_array() rejette un scalaire là où un tableau est attendu au lieu de le convertir. Les rôles non administrateurs sont en outre limités champ par champ.

Une clé d'API statique fuit par les URL, les journaux et le temps de réponseClé uniquement dans les en-têtes, hash_equals, chaque requête contrôlée, aucune route tant que MCP est désactivé

Quand MCP est désactivé, register_routes() retourne avant d'enregistrer quoi que ce soit, de sorte que le point d'accès n'existe pas. permission() exige la clé à chaque requête, y compris la prise de contact MCP, de sorte qu'un appelant non authentifié n'apprend rien sur le point d'accès. La clé est lue dans Authorization: Bearer ou X-WOOBE-KEY, avec le repli Apache REDIRECT_HTTP_AUTHORIZATION, et comparée avec hash_equals.

Les refus utilisent 403 et non 401 : dans MCP, un 401 déclenche la découverte OAuth et le client signale le serveur comme défaillant. Les liens de téléchargement et de dépôt utilisent leurs propres jetons à usage unique, car un clic dans le navigateur ne peut pas envoyer d'en-tête.

Une clé qui a fuité ne devrait pas, à elle seule, donner accès à la boutiqueSecond facteur : un jeton de session confirmé dans WordPress, stocké haché, expiration après 1 h d'inactivité

L'assistant demande un jeton, composé de quatre groupes de huit lettres et chiffres tirés par random_int, qui est confirmé par une personne connectée à WordPress : un administrateur pour la clé de toute la boutique, le propriétaire de la clé dans ses propres réglages pour une clé personnelle. Pour la clé de toute la boutique, ce second facteur est un réglage ; pour les clés personnelles, il est toujours actif. Seul son hachage sha256 est stocké, avec les heures de confirmation et de dernier usage. check_connection() compare avec hash_equals et coupe la connexion après 3600 secondes sans appel ; l'heure de dernier usage est écrite au plus une fois par minute.

Les connexions sont propres à chaque propriétaire : un jeton confirmé pour une clé personnelle est refusé avec toute autre clé.

Les clés par utilisateur ne doivent pas révéler quels utilisateurs ont accèsClé stockée toujours lue et comparée, avec un leurre de même longueur quand il n'y en a pas

Une clé personnelle a la forme woobe_<40 hex>-<user id>. personal_key_owner() lit et compare toujours la clé stockée avec hash_equals, en utilisant un leurre de même longueur quand l'utilisateur n'en a pas, et ne consulte la liste des autorisations et la capacité qu'après une correspondance complète, de sorte que le temps de réponse n'indique pas à un inconnu qui a accès. Les ID avec des zéros initiaux prennent le chemin du leurre.

La requête s'exécute ensuite sous l'identité de cet utilisateur via wp_set_current_user(), de sorte que son rôle et la visibilité des champs s'appliquent exactement comme à l'écran, et la connexion à deux facteurs est toujours active pour les clés personnelles.

Les propriétaires ont besoin d'un accès en lecture seule ou nul par domaine de la boutiqueCarte des secteurs full/read/none par utilisateur, restreinte par les capacités WP ; outils fermés masqués

Chaque outil déclare un secteur et annotations.readOnlyHint. Le propriétaire renvoie une carte depuis le filtre woobe_mcp_permissions ; '*' définit la valeur par défaut, ce qui en fait une liste d'autorisations. Les entrées invalides sont journalisées et ignorées, et un '*' invalide se rabat sur full, comme en l'absence de callback. Pour une clé personnelle, chaque secteur est de nouveau restreint par la capacité que WooCommerce demande sur cet écran (edit_products, edit_shop_orders, view_woocommerce_reports et d'autres).

Les outils que l'identité ne peut pas appeler ne sont pas listés du tout. Une écriture qui atterrit dans un autre secteur, comme une annulation qui réécrit des produits, demande require_access() pour ce secteur avant d'écrire. Le filtre woobe_mcp_tool_allowed ne peut que restreindre davantage. Aucune clé MCP, pour toute la boutique ou personnelle, ne peut écrire post_author ni ID ; ils se modifient dans wp-admin, par une personne.

Chiffres

wordpress.org
  • Installations actives 30 000+ compteur de wordpress.org.
  • Avis 227 Moyenne de 4.7 sur 5.
  • Années de maintenance 8 Depuis 7 mai 2018, dernière mise à jour 13 octobre 2026.