Мова

Зв'язатися

Плагін для WooCommerce · з 2018 · aka WOOBE

BEAR

Табличний масовий редактор товарів WooCommerce з MCP-сервером, який дає змогу AI-агентам безпечно редагувати магазин.

Перший реліз
Активні встановлення
30 000+
Рейтинг
4.7 / 5 (227)Читати відгуки
  • PHP
  • WordPress
  • WooCommerce CRUD (WC_Product)
  • WP_Query
  • $wpdb / MySQL
  • WP REST API
  • JSON-RPC 2.0
  • Model Context Protocol
  • jQuery
  • DataTables

Про мене

BEAR показує каталог WooCommerce як одну редаговану таблицю й змінює тисячі товарів за раз: ціни, залишки, SKU, таксономії, атрибути, варіації та власні meta-поля. Кожна зміна проходить одним шляхом запису поля, який для властивостей товару використовує об'єкти товарів WooCommerce, зберігається разом із попереднім значенням і може бути відкочена на вкладці «Історія».

Складна частина не в таблиці. Складно зробити масовий запис коректним на реальному магазині: значення зберігаються в п'яти різних місцях, у змінних товарів ціни й залишки лежать на варіаціях, WooCommerce має побічні ефекти (наприклад, статус наявності виводиться з кількості на складі), а на спільному хостингу PHP-запит обривається через 30 секунд.

З 2026 року плагін також запускає MCP-сервер. AI-агент може знаходити товари, переглядати зміну наперед, застосовувати її порціями та відкочувати — через ті самі рушії фільтрів, масового редагування та історії, що й екран, — за ключами, підтвердженим токеном сесії (для ключа всього магазину необов'язковим, для персональних ключів завжди увімкненим) і картою дозволів за секторами.

Безкоштовна та преміум-збірки — це одна кодова база, що перемикається одним прапорцем; безкоштовна збірка обмежує розмір сторінки, кількість meta-полів, глибину історії та частоту записів через MCP.

BEAR · масовий редактор

що розв'язано

ПРАЦЮЄз 2018

Таблиця товарів і модель полів

shipped · 2018

Що це даєСпівробітники бачать і змінюють будь-яку деталь товару чи варіації на одному екрані, схожому на таблицю, навіть у каталогах, надто великих, щоб відкрити їх цілком.

Проблема
Один екран має показувати й редагувати будь-яке значення каталогу будь-якого розміру: поля запису, властивості WooCommerce, таксономії, атрибути, завантаження та довільні meta-поля — для товарів і їхніх варіацій.
Рішення

Декларативний реєстр полів керує кожною колонкою та кожним записом, а таблиця розбивається на сторінки на сервері: одна сторінка WP_Query на один AJAX-запит.

Стек
  • WP_Query
  • Серверний режим DataTables
  • Сетери WC_Product
  • wp_options
Задачі · 6
Великі каталоги не можна завантажити в браузер за один разСерверна пагінація: кожна сторінка таблиці — це WP_Query зі зміщенням, сортуванням і фільтрами

woobe_get_products() приймає start, length і order від DataTables та передає їх у WOOBE_PRODUCTS::gets(), який будує WP_Query. Сортування за meta-колонкою переходить на meta_value_num для типів number, timestamp і unix, в інших випадках — на meta_value.

Розмір сторінки в моделі налаштувань обмежено 100 рядками з коментарем, що більші сторінки спричиняли HTTP 500 на слабких серверах; у безкоштовній збірці він зафіксований на 10. Коли перемикач варіацій увімкнено, варіації кожного змінного товару запитуються й вставляються рядками одразу після батьківського товару.

Кожен тип колонки зберігається в WooCommerce по-різномуОдин запис реєстру на колонку: тип сховища, тип значення, редактор, санітайзер, права

woobe_get_fields() описує кожну колонку: field_type (field, meta, prop, taxonomy, attribute, downloads, gallery, upsells та інші), тип значення, вигляд редагування, санітайзер, типи товарів, до яких вона не застосовується, і видимість для менеджерів магазину. Глобальні атрибути та всі таксономії товарів додаються під час виконання з wc_get_attribute_taxonomies() і get_object_taxonomies().

update_page_field() розподіляє за field_type: update_post_meta для meta, сетери WC_Product плюс save() для props, wp_set_post_terms для таксономій, перебудовані списки WC_Product_Attribute для атрибутів. Комірки таблиці, відкати з Історії та більшість масових записів завершуються цією функцією. Масове редагування таксономій і атрибутів пише терміни напряму та перед записом викликає той самий хук Історії.

Власні meta-ключі треба відрізняти від внутрішніх ключів WooCommerceПідклас WC_Product_Data_Store_CPT відкриває свій захищений список внутрішніх ключів

woobe_meta_get_keys() читає всі meta-ключі зразкового товару й віднімає внутрішні ключі WooCommerce. Цей список — захищена властивість сховища даних товару, тому WOOBE_PDS_CPT розширює сховище лише для того, щоб його повернути, додаючи _button_text і _product_url.

Meta-ключі обрізаються по пробілах, але не приводяться до нижнього регістру й не санітизуються, бо реальні ключі, які пишуть інші плагіни, наприклад _woocs_sale_price_USD, чутливі до регістру.

Типізоване meta-поле має працювати в колонках, фільтрах і масовому редагуванніОдне визначення meta через хуки потрапляє в реєстр, панель фільтрів і панель масового редагування

woobe_extend_fields() перетворює кожне збережене визначення meta на колонку; числовий тип отримує санітайзер floatval і числове сортування. Те саме визначення живить woobe_filter_numbers (діапазон від/до), woobe_filter_text (LIKE, =, !=, NOT LIKE, порожнє, непорожнє), фільтри-перемикачі та woobe_bulk_number (встановити, збільшити або зменшити на значення чи на відсоток).

Колонка-перемикач може зберігати значення, відмінні від 1/0 (наприклад, yes/no), через правило перевизначення для кожного ключа, яке читає over_switcher_swicher_to_val(). Безкоштовна збірка залишає перші 2 meta-поля.

У серіалізованого meta-масиву немає плоскої комірки для редагуванняРедактор у спливному вікні надсилає ключі й значення паралельними масивами, які перебудовуються у вкладені PHP-масиви

process_jsoned_meta_data() перебудовує meta-значення з пар keys/values для масивів і keys2/values2 для об'єктів, декодуючи вкладені JSON-значення, а для звичайних рядків і чисел повертається до сирого значення. Історія зберігає такі значення як JSON із JSON_HEX_QUOT і JSON_HEX_TAG, щоб вони пережили круговий шлях без втрат.

Стан редактора для кожного користувача має пережити скидання об'єктного кешуОдин рядок опції без autoload на користувача з TTL для кожного ключа замість transients

Фільтри та конфігурації масового редагування зберігаються між запитами через WOOBE_STORAGE. За Redis або Memcached transient живе лише в об'єктному кеші й губиться при його скиданні, тому сховище за замовчуванням — рядок опції woobe_memory_<user id>: кожен ключ має власний термін дії, прострочені ключі видаляються під час запису, autoload вимкнено, а рядок видаляється разом із користувачем. Режими session, transient і cookie лишаються за фільтром woobe_storage_type.

Фільтрація

shipped · 2018

Що це даєСпівробітники вибирають саме ті товари, які потрібні, зокрема за ціною чи залишком варіації, а та сама вибірка використовується для редагування й експорту.

Проблема
Фільтри за цінами, залишками, габаритами, текстом і таксономіями мають вибирати саме потрібні товари, зокрема змінні, чиї значення лежать на варіаціях, а ту саму вибірку мають використовувати пагінація, масове редагування й експорт.
Рішення

Фільтр зберігається на сервері під ключем і при кожному запиті компілюється в частини tax_query, meta_query і posts_where через один хук WP_Query.

Стек
  • WP_Query
  • tax_query
  • meta_query
  • posts_where
  • $wpdb->prepare
  • MySQL REGEXP
Задачі · 5
Запити пагінації, масового редагування й експорту мають бачити одну й ту саму відфільтровану вибіркуФільтр зберігається під випадковим ключем на кожен запуск; один хук WP_Query перебудовує його для кожного запиту

woobe_filter_products() зберігає розібрану форму фільтра під woobe_filter_<key>, де ключ — випадковий рядок, що генерується при кожній фільтрації, тому масове редагування за фільтром працює одразу в кількох вкладках браузера. Кожен виклик WOOBE_PRODUCTS::gets() пропускає свої аргументи через woobe_apply_query_filter_data(), яка читає збережений фільтр і повертає tax_query, meta_query, post__in та post_status, а для решти підключає хуки posts_where.

Пагінація таблиці, лічильник масового редагування, лічильник експорту та інструменти варіацій викликають gets(), тому не можуть розійтися в тому, що означає фільтр.

Ціна й залишок лежать на варіаціях, а таблиця показує батьківські товариЗапит діапазону по товарах і варіаціях, потім знайдені варіації зіставляються з батьками

regular_price_where(), sale_price_where(), stock_quantity_where() і dimensions_where() запитують postmeta і для product, і для product_variation, потім читають post_parent знайдених варіацій і обмежують основний запит батьками плюс варіаціями через ID IN (...).

Коли нічого не знайдено, умова стає ID = -1, тож порожній збіг повертає нуль товарів, а не весь каталог, що важливо за один клік до масового редагування.

Числа, збережені як рядки meta, неправильно порівнюються в MySQLCAST до DECIMAL із явною точністю; порожні рядки виключаються з кожного діапазону

Власні числові діапазони використовують meta_query типу DECIMAL(30,20): голий DECIMAL має scale 0, тому пошук від 100 до 150 знайшов би й 99.xx. SQL цін приводить до DECIMAL(30,10).

Порожня вага зберігається як '', а MySQL читає '' як 0, тому діапазони цін і габаритів вимагають meta_value <> '', і порожнє поле ніколи не вважається нулем. Відсутній кінець діапазону відкритий (підставляється -999999999 або 999999999), а «від» дорівнює «до» означає саме це значення.

Користувачам потрібні AND, OR і NOT в одному полі текстового пошукуНевеликий синтаксис пошуку (^ OR, ^~ AND, ^! NOT), що компілюється в умови MySQL REGEXP

posts_txt_where() розбиває текст за ^; префікс ~ з'єднує частину через AND, префікс ! перетворює її на NOT REGEXP в окремій групі AND. Режими exact, begin, end, not і empty відповідають = та REGEXP із прив'язкою; дужки regex екрануються. Застосовується до заголовка, вмісту, уривка та slug.

Фільтр SKU приймає список через кому й будує підготовлені умови LIKE з esc_like: OR для режимів збігу й AND для «not».

Збіг з усіма термінами одночасно потребує однієї частини tax_query на кожен термінAND розбивається на одну частину на термін; невідомі оператори замінюються на IN

Для оператора AND кожен термін стає окремою частиною tax_query під relation AND, бо одна частина з оператором AND може не повернути жодного товару. Оператори обмежено білим списком: IN, NOT IN, AND, EXISTS і NOT EXISTS; WordPress відкидає частину з невідомим оператором і збігся б з усіма товарами.

Видимість у каталозі перетворюється на комбінації термінів product_visibility exclude-from-catalog і exclude-from-search, а фільтр статусу наявності ще й обмежує product_type типами, які мають статус наявності.

Рушій масового редагування

shipped · 2018

Що це даєЗміни цін, залишків і варіацій у тисячах товарів надійно завершуються на звичайному хостингу та дотримуються власних правил магазину щодо цін і залишків.

Проблема
Масова зміна може зачепити тисячі товарів на хостингу з короткими лімітами часу PHP, і кожен запис має дотримуватися правил WooCommerce щодо цін, залишків і варіацій.
Рішення

ID цілей визначаються один раз, потім записуються малими AJAX-пакетами через той самий шлях запису одного поля, що й таблиця, який несе правила WooCommerce.

Стек
  • AJAX-пакети
  • WC_Product
  • WC_Product_Variation
  • WC_Product_Attribute
Задачі · 6
Масове редагування тисяч товарів упирається в ліміти часу PHPID отримуються один раз, потім надсилаються пакетами по N на AJAX-запит; N — налаштування

woobe_bulk_products_count() зберігає конфігурацію масового редагування під випадковим 16-символьним bulk_key і повертає всі відфільтровані ID (fields=ids, без пагінації). __woobe_bulk_products() у bulk.js нарізає цей список і надсилає пакет за пакетом, оновлює індикатор прогресу й наприкінці викликає woobe_bulk_finish; користувач може перервати поточний запит.

Розмір пакета — number_posts_processed, від 10 до 100 на запит, за замовчуванням 30, тож слабкий сервер може проміняти швидкість на стабільність.

Відредаговані товари випадають з фільтра, поки триває запускЗаморожений список ID: кожен пакет несе ID зі знімка, а не повторно виконаного запиту

Пагінація відфільтрованого запиту між пакетами пропускала б товари: зміна ціни виводить товар з фільтра за ціною, і всі наступні зміщення зсуваються. Список ID, знятий на початку, зберігається на клієнті, а кожен запит пакета явно надсилає свої ID без ключа фільтра.

Після запуску позначені рядки, яких більше немає в перемальованій таблиці, прибираються з виділення, тож наступна дія не торкнеться товарів, яких користувач уже не бачить.

Числові поведінки мають поєднуватися з формулами й округленням цінКонвеєр на товар: поведінка, формула з іншим meta, випадковий діапазон, округлення

_process_number_data() застосовує встановлення, збільшення або зменшення на значення чи відсоток і міжпольові правила, наприклад ціна зі знижкою = звичайна мінус відсоток. Потім необов'язкова формула з іншим meta-полем (+, -, *, /, з захистом від ділення на нуль), потім необов'язкова випадкова величина в діапазоні із заданою кількістю десяткових знаків.

update_page_field() округлює в десяткових знаках цін магазину: до цілих одиниць, угору до 0.05, до 0.1 або до фіксованого закінчення копійок, як .99. Текстові поля отримують append, prepend, replace (з урахуванням регістру чи без) і плейсхолдери на кшталт {TITLE}, {SKU}, {meta:key} та {attribute:pa_size}.

Ціна зі знижкою має лишатися нижчою за звичайну, а дати мають набирати чинностіЗнижка >= звичайної пропускається, знижка <= 0 її прибирає; _price перераховується при зміні дат

У масовому запуску обчислена ціна зі знижкою, що досягає звичайної ціни, пропускається, а значення нуль або менше видаляє знижку. Одиночний запис ціни зі знижкою, рівної звичайній або вищої, обмежується звичайною мінус найменша цінова одиниця.

Коли записуються дати знижки, _price одразу встановлюється на ціну, що діє зараз (знижка ще не почалася, триває, закінчилась), а transients товару очищаються, замість того щоб чекати на запланове завдання знижок WooCommerce.

Записи залишків і цін не повинні пошкоджувати змінні товари чи спільний залишокЗмінні батьки й варіації із залишком батька не чіпаються; залишок зберігається при перемиканнях

WooCommerce читає ціни змінного товару з його варіацій, тому ціна, записана батьку, пропускається, а очищувальний запис видаляє будь-яку залишкову ціну батька. Залишок 0 або менше зазвичай вмикає облік залишків; це не робиться на змінному батьку, який не веде облік (інакше всі варіації без власного залишку стали б відсутніми на складі), і не на варіації, що продається із залишку батька (її було б від'єднано).

WooCommerce очищає кількість, дозамовлення та поріг низького залишку, коли облік залишків вимикається. keep_stock_on_switch() зберігає їх у власному meta і відновлює, коли облік вмикається знову.

Варіації треба створювати й редагувати за комбінацією атрибутівДекартів добуток вибраних термінів без дублікатів; редагування зіставляється за комбінацією з підстановкою

generate_combinations() будує декартів добуток вибраних термінів атрибутів. Застосування прикріплює атрибути як атрибути варіацій, пропускає комбінації, які товар уже має, копіює ціни батька, дає кожній новій варіації SKU parent-SKU-<id>, щоб уникнути дублікатів, і пересортовує варіації через сховище даних.

Редагування, видалення та експорт підмножини варіацій порівнює атрибути кожної варіації з вибраними комбінаціями: ключі та значення відсортовані, записи '-1' вважаються будь-яким значенням, а обидві сторони URL-декодуються, бо нелатинські slug атрибутів зберігаються закодованими.

Історія та відкат

у продукті

Що це даєПомилкову зміну — вручну, масово чи від AI-асистента — можна скасувати, а не виправляти товар за товаром.

Проблема
Кожну зміну — вручну, масово чи від AI-агента — потрібно мати можливість скасувати, зокрема побічні ефекти, які WooCommerce застосовує сам, а відкат великого запуску впирається в ті самі ліміти часу, що й сам запуск.
Рішення

Попередні значення записуються по полях у двох власних таблицях хуком на єдиному шляху запису й відкочуються порціями тим самим шляхом.

Стек
  • Власні таблиці MySQL
  • $wpdb
  • Actions WordPress
Задачі · 6
Записувати попередні значення для будь-якого типу поля без коду під кожну функціюОдин хук перед кожним записом поля; попереднє значення зберігається по полях у власній таблиці

update_page_field() викликає woobe_before_update_page_field, і WOOBE_HISTORY::write() читає поточне значення та кодує його за типом: ID термінів, атрибути, галерея й зв'язки як JSON, завантаження як name/file/hash, статус наявності як 1/0, дати знижки як ISO-дати зі зсувом часового поясу, тож відкат відновлює точний розклад.

Рядки містять field_key, product_id, prev_val, mod_date, bulk_key, user_id і via_mcp та індексуються за товаром, bulk key і користувачем. Друга таблиця зберігає один заголовок на кожен масовий запуск: стан (completed або terminated), початок, завершення, кількість товарів і ключі полів.

WooCommerce змінює статус наявності як побічний ефект редагування залишкуПобічні ефекти записуються додатковими рядками однієї зміни й відкочуються разом, по порядку

Перемикання обліку залишків або залишок 0 на товарі без обліку змушує WooCommerce вивести статус наявності; вимкнення обліку також скидає кількість. update_page_field() записує stock_status і, за потреби, stock_quantity додатковими рядками одразу після відредагованого поля.

stock_status_pair() і stock_row_at() знаходять ці рядки за сусідніми ID (+1, +2) і вимагають того самого товару, автора, каналу (вручну чи через агента) та bulk key, а також близького часу запису. Потім revert() відновлює кількість раніше за статус, бо WooCommerce виводить статус з кількості, поки залишок ведеться. Порожній попередній залишок означає «не відстежується», тому untrack_stock() вимикає облік замість запису 0.

Відкат масового запуску на тисячі рядків теж упирається в тайм-аутВідкат порціями за bulk_key; кожен відкочений рядок видаляється; заголовок видаляється наприкінці

На вкладці «Історія» woobe_history_revert_bulk_portion() відкочує по 10 рядків за AJAX-запит з індикатором прогресу, а рядок заголовка видаляється, коли лічильник досягає загальної кількості. Кожен відкат видаляє свій рядок, тож наступна порція просто бере те, що залишилось.

Інструмент відкату MCP — окремий шлях: revert_bulk_portion() відкочує до 200 рядків за виклик і повідомляє фактично видалені рядки, бо два рядки зміни залишку відкочуються разом і порція може взяти на один рядок більше за свій ліміт.

revert() спершу знімає всі слухачі woobe_before_update_page_field, щоб власні записи відкату не фіксувалися як нова історія.

Кілька користувачів і AI-агентів ділять одну історіюОдне правило scope(): адміністратори бачать усі рядки, інші — свої; примусово на сервері

Кожен рядок несе user_id і via_mcp. Ключ MCP для всього магазину пише від користувача -777, який ніколи не збігається ні з реальним користувачем, ні з 0 (не авторизований). scope() повертає одну SQL-умову, що використовується в кожному списку, відкаті, видаленні й очищенні, тож жоден запит не може відійти від правила.

Обробники перевіряють solo_in_scope() або bulk_in_scope() і відповідають 403 на запит, зібраний вручну для рядка, якого користувач не бачить. Імена авторів для списку читаються одним запитом.

Наявним установкам потрібна була нова колонка без пошкодження старих рядківОпція з версією схеми, ALTER із перевіркою колонки, заповнення рядків агента заднім числом

CREATE TABLE IF NOT EXISTS ніколи не змінює таблицю, що вже існує. maybe_upgrade_schema() запускається раз на версію схеми (зараз 2), перед кожним ALTER перевіряє SHOW COLUMNS, щоб паралельний запит не завершився помилкою, повторює спробу при наступному завантаженні, якщо ALTER не вдався, і позначає старі рядки користувача -777 як записані через агента.

Безкоштовна збірка зберігає коротку історію, не чіпаючи інших користувачівОчищення по авторах під час shutdown: останні 2 одиночні й 2 масові зміни, рядки залишків зберігаються

У безкоштовній збірці prune_author() зберігає дві найновіші одиночні зміни та два найновіші масові запуски одного автора. Рядки, що належать до збереженої зміни залишку, зберігаються разом із нею через stock_change_rows(). Очищення виконується під час shutdown після запису, а відкриття вкладки ніколи не видаляє рядки іншого користувача.

Експорт

у продукті

Що це даєВідфільтровані списки товарів експортуються у файли, які магазин може імпортувати назад, будуються частинами на слабких серверах і ніколи не лишаються відкритими в мережі.

Проблема
Експорт відфільтрованого каталогу з варіаціями має виконуватися частинами на слабких серверах, давати файли, які WooCommerce може імпортувати назад, і не залишати дані товарів відкритими в мережі.
Рішення

Експорт виконується як послідовність AJAX-запитів, що дописують в один файл, ім'я якого виводиться окремо для кожного експорту, який лежить у закритій теці й віддається через авторизований обробник.

Стек
  • fputcsv
  • DOMDocument (WXR)
  • hash_hmac
  • .htaccess
  • WP REST API
Задачі · 4
Великий експорт не можна побудувати за один запитСпершу записується заголовок, потім рядки дописуються пакетами AJAX в той самий файл

woobe_export_products_count() очищає попередні файли, записує заголовок CSV або каркас XML і повертає відфільтровані ID. woobe_export_products() дописує один пакет за запит через fopen(a+) і fputcsv, бо WP_Filesystem не вміє потокового запису по рядках; для XML він завантажує документ WXR і дописує вузли item.

Варіації додаються після свого батька, за бажанням без відсутніх на складі, і можуть обмежуватися комбінаціями атрибутів або виключатися за ними.

CSV має завантажуватися у власний імпортер товарів WooCommerceАтрибути як чотири колонки кожен, завантаження як пари name/URL, будь-яке значення розгортається в рядок

get_csv_titles() пише 'Attribute N value(s)', 'name', 'visible' і 'global' для кожного атрибута та 'Download N name' і 'URL' для кожного слота файлу — колонки, які читає імпортер WooCommerce; meta-колонки називаються 'Meta: key'.

flatten_export_value() зводить масиви й об'єкти (терміни, атрибути, об'єкти даних WC) до скалярів, з'єднаних через '|', тож тип колонки, якого плагін ніколи не бачив, зокрема сторонній, не пише слово 'Array'.

Файли експорту містять ціни та URL файлів і не повинні бути публічнимиТека закрита через .htaccess, ім'я файлу з HMAC-хвостом, завантаження через перевірений обробник

Постфікс імені файлу має відповідати хвилинній мітці часу й отримує 12-символьний хвіст із hash_hmac('sha256', postfix, wp_salt('auth')): однаковий для всіх запитів одного експорту, тож усі пакети потрапляють в один файл, але його неможливо вгадати ззовні. Хвіст важливий на nginx, який ігнорує правило deny з .htaccess.

woobe_export_download() перевіряє nonce і capability, приймає лише імена, які генерує розширення, очищає всі буфери виводу й віддає потоком через readfile() без Content-Length. Файли старші за 8 годин видаляються, перевірка — не частіше ніж раз на годину.

AI-агенту потрібне посилання для завантаження без розкриття його ключаREST-маршрут за токеном одного файлу з 32 hex-символів, який спливає через 15 хвилин

Клік у браузері не може надіслати заголовок Authorization, а ключ MCP не потрапляє в URL. Інструмент експорту зберігає ім'я файлу під випадковим токеном у transient із часом життя 900 секунд, а /woobe/v1/mcp/export/<token> віддає лише цей файл.

MCP-сервер для AI-агентів

shipped · 2026

Що це даєAI-асистент може редагувати магазин за власника, але лише на підтвердженій вибірці, спершу з переглядом наперед, і кожна зміна оборотна.

Проблема
AI-агент працює через мережевий API без екрана, може неправильно прочитати фільтр чи кількість і може видати запис на весь каталог; і все ж він має бути безпечним, оборотним і вкладатися в ліміти часу PHP.
Рішення

Інструменти використовують власні рушії фільтрів, масового редагування та історії редактора; записи приймають лише заморожені вибірки, підтверджені їхньою кількістю, можуть бути попередньо перевірені інструментом пробного запуску й виконуються порціями, які можна продовжити.

Стек
  • WP REST API
  • JSON-RPC 2.0
  • Model Context Protocol
  • wp_options
Задачі · 6
Неправильно прочитаний фільтр не повинен розширити запис на весь каталогЗаписи приймають лише заморожений selection_id і confirm_count, що дорівнює його розміру

woobe_find_products виконує фільтр один раз і зберігає ID в опції без autoload (до 200,000 ID, діє 24 години). Відповідь містить selection_id, count, зразок і whole_catalogue, виміряне порівнянням із нефільтрованим запитом, тож фільтр, що нічого не звужує, повідомляється саме так.

woobe_apply_bulk не приймає жодного фільтра. Він відмовляє, якщо confirm_count не дорівнює розміру вибірки, тож агент, який не прочитав число, не може його вгадати. Товари, створені після пошуку, до вибірки не входять.

Фільтр агента має бути відхилений, а не проігнорований, якщо рушій не може його застосуватиСуворий валідатор відхиляє будь-який ключ чи форму, яку рушій не застосував би, і пояснює чому

Рушій фільтра читає поля власної форми редактора й ігнорує будь-які інші ключі. Агент може надіслати ключ з помилкою або дату як {from, to}, а проігнорована умова розширила б вибірку. WOOBE_MCP_FILTER перевіряє кожен ключ за списком полів, які читає рушій, відхиляє запит із зазначенням ключа та допустимої форми, а решту передає у вигляді, в якому її надсилає екран.

Рушій підключає хуки posts_where, які лишаються до кінця запиту: на екрані це одне завантаження сторінки, але в пакеті JSON-RPC може бути кілька пошуків. release_filter_hooks() знімає їх після кожного запиту MCP, тож один пошук ніколи не успадковує умови іншого. Типи аргументів перевіряються за inputSchema кожного інструмента, тому рядок 'maybe' не є підтвердженням.

Перегляд наперед має показувати, що запис справді дастьПробний запуск дзеркалить арифметику масового редагування та послідовно ланцюжить операції запуску

simulate() буквально копіює _process_number_data(), а run_chain() передає кожен результат наступній операції, тож ціна зі знижкою, задана разом з новою звичайною ціною, перевіряється щодо нової. Арифметика застосовується лише до числових полів, на яких рахує рушій; будь-яке інше поле показується як проста заміна.

Перегляд повертає три лічильники (вибірка, зачеплені після розгортання батьків у варіації, доступні для запису) і попередження: відсоток від порожнього поля пропущено, змінні батьки без власної ціни, залишок не відстежується.

REST-виклик має той самий ліміт часу, що й AJAX-пакетЧастини по 25 через do_bulk, 20 с на виклик, продовження за bulk_key і next_offset

Цикл застосування нарізає чергу на частини по 25 і викликає WOOBE_BULK::do_bulk(), рушій, який використовує екран, доки не мине 20 секунд. Він повертає next_offset і bulk_key; продовження пише в той самий заголовок Історії, тож один відкат скасовує все завдання. Кеші очищаються один раз після записів, а не на кожну частину.

У безкоштовній збірці частина зменшується до залишку дозволу на запис: token bucket зі 100 товарів, що безперервно поповнюється протягом 30 хвилин, з дробовими токенами та жорсткою стелею, тож простій не може оплатити великий запуск.

Варіації не несуть жодного з термінів чи meta, за якими збігається фільтрДочірні додаються після запиту: всі або лише ті, що збігаються з кожним названим атрибутом

include_variations 'all' додає всіх дочірніх знайдених батьків; 'matching' залишає дочірніх, чиї значення атрибутів збігаються з усіма термінами pa_ фільтра («жовтий і малий» — це одна варіація, а не об'єднання), причому порожнє значення означає «будь-яке». Категорії та мітки не можуть звузити дочірні.

Ключі атрибутів будуються через sanitize_title(), як це робить WooCommerce, бо нелатинські назви атрибутів зберігаються URL-закодованими, а sanitize_key() їх би зрізав. У режимі matching змінні батьки прибираються з вибірки, щоб подальший variations_only не розгорнув їх назад у всі варіації.

Усередині REST-виклику плагін і магазин поводяться інакше, ніж в адмінціВласний запуск на rest_api_init, фільтри цін валют знімаються, батьківські терміни доповнюються

WOOBE::init() виконується на init, до визначення REST_REQUEST, тож під час REST-виклику не бачить ні адмінки, ні REST і нічого б не робив. bootstrap.php сам підключається до rest_api_init і піднімає плагін; is_should_init() дозволяє REST лише для власних маршрутів плагіна, а не для всього REST-трафіку сайту.

Перемикачі валют фільтрують гетери цін, тож агент читав конвертовані ціни; strip_price_filters() знімає ці фільтри лише на час запиту MCP. Товар, поміщений у підкатегорію, також поміщається в її предків через слухач set_object_terms, бо в чаті ніхто не ставить позначки на батьківських категоріях.

Контроль доступу та безпека

shipped · 2026

Що це даєЗмінювати товари можуть лише потрібні люди, з увімкненим необов'язковим другим фактором; один лише витеклий ключ доступу AI не відкриває магазин, а доступ можна обмежити по ділянках.

Проблема
Масовий редактор має право запису до кожного товару. Кожен AJAX-обробник, історія та кінцева точка MCP мають перевіряти, хто діє, а ключ MCP, що зберігається в налаштуваннях асистента, може витекти.
Рішення

Один захист для всіх AJAX-обробників із перевіркою володіння по кожному товару; для MCP — ключі лише в заголовках, що порівнюються за сталий час, підтверджений токен сесії (для ключа всього магазину необов'язковий, для персональних ключів завжди увімкнений), персональні ключі та карта дозволів за секторами.

Стек
  • Nonces WordPress
  • current_user_can / meta capabilities
  • hash_equals
  • random_int
  • WP REST permission_callback
Задачі · 5
Десятки AJAX-обробників мали кожен свої неповні перевіркиЄдиний захист: nonce, capability, потім edit_post на кожен товар, для варіацій — через батька

Після звіту WPScan було проаудитовано весь набір AJAX-обробників, а не лише згадані, і кожен обробник перенесено на WOOBE_HELPER::check_ajax_access(). Він перевіряє nonce, потім capability, потім current_user_can('edit_post') для кожного ID товару.

Варіації перевіряються щодо батьківського товару: WooCommerce зіставляє edit_post на варіації зі звичайним edit_products, який ігнорує володіння, а варіації мають post_author 0. get_request_array() відхиляє скаляр там, де очікується масив, замість приведення типу. Ролі не-адмінів додатково обмежуються по полях.

Статичний API-ключ витікає через URL, логи та часові характеристикиКлюч лише в заголовках, hash_equals, кожен запит перевіряється, без маршруту, поки MCP вимкнено

Коли MCP вимкнено, register_routes() повертається до реєстрації будь-чого, тож кінцевої точки не існує. permission() вимагає ключ у кожному запиті, включно з рукостисканням MCP, тому неавторизований викликач нічого не дізнається про кінцеву точку. Ключ читається з Authorization: Bearer або X-WOOBE-KEY, із запасним варіантом Apache REDIRECT_HTTP_AUTHORIZATION, і порівнюється через hash_equals.

Відмови використовують 403, а не 401: у MCP код 401 запускає виявлення OAuth, і клієнт повідомляє, що сервер зламаний. Посилання для завантаження й вивантаження використовують власні одноцільові токени, бо клік у браузері не може надіслати заголовок.

Сам по собі витеклий ключ не повинен давати доступ до магазинуДругий фактор: токен сесії, підтверджений у WordPress, зберігається хешованим, спливає після 1 год простою

Асистент просить токен, що складається з чотирьох груп по вісім літер і цифр з random_int, і його підтверджує людина, яка увійшла у WordPress: адміністратор для ключа всього магазину, власник ключа у своїх налаштуваннях для персонального ключа. Для ключа всього магазину цей другий фактор — налаштування; для персональних ключів він завжди увімкнений. Зберігається лише його хеш sha256 з часом підтвердження й останнього використання. check_connection() порівнює через hash_equals і розриває з'єднання після 3600 секунд без викликів; час останнього використання записується не частіше ніж раз на хвилину.

З'єднання прив'язані до власника: токен, підтверджений для одного персонального ключа, відхиляється з будь-яким іншим ключем.

Ключі окремих користувачів не повинні розкривати, хто має доступЗбережений ключ завжди читається й порівнюється, з фіктивним ключем тієї самої довжини, якщо справжнього немає

Персональний ключ має вигляд woobe_<40 hex>-<user id>. personal_key_owner() завжди читає й порівнює збережений ключ через hash_equals, використовуючи фіктивний ключ тієї самої довжини, якщо в користувача його немає, і питає список дозволів та capability лише після повного збігу, тож час відповіді не підказує стороннім, хто має доступ. ID з ведучими нулями йдуть фіктивним шляхом.

Потім запит виконується від імені цього користувача через wp_set_current_user(), тож його роль і видимість полів діють точно так, як на екрані, а двофакторне з'єднання для персональних ключів завжди увімкнене.

Власникам потрібен доступ лише для читання або його відсутність по ділянках магазинуКарта секторів full/read/none для кожного користувача, звужена правами WP; закриті інструменти приховані

Кожен інструмент оголошує сектор і annotations.readOnlyHint. Власник повертає карту з фільтра woobe_mcp_permissions; '*' задає значення за замовчуванням, тож це працює як білий список. Некоректні записи логуються та ігноруються, а некоректний '*' повертається до full, так само як і відсутність callback. Для персонального ключа кожен сектор додатково звужується capability, яку WooCommerce запитує на відповідному екрані (edit_products, edit_shop_orders, view_woocommerce_reports та інші).

Інструменти, які ідентичність не може викликати, взагалі не показуються. Запис, що потрапляє в інший сектор, наприклад відкат, який переписує товари, перед записом запитує require_access() для цього сектора. Фільтр woobe_mcp_tool_allowed може лише звужувати далі. Жоден ключ MCP, ні для всього магазину, ні персональний, не може записувати post_author чи ID; їх редагує людина в wp-admin.

Цифри

wordpress.org
  • Активні встановлення 30 000+ лічильник wordpress.org.
  • Відгуки 227 Середня оцінка: 4.7 з 5.
  • Років підтримки 8 З 7 травня 2018 р., останнє оновлення 13 жовтня 2026 р..