<? phpukraine СПІВБЕСІДИ
Пошук по платформі
WORDPRESS · MIDDLE

Як додати власний REST-ендпоінт у WordPress з правами й валідацією?

register_rest_route на хуку rest_api_init: namespace/версія, methods, callback, обовʼязковий permission_callback з current_user_can і args зі sanitize_callback та validate_callback; помилки повертаються як WP_Error зі status.

Куди вішати register_rest_route і чому виклик у плагіні «просто так» не працює?
Ендпоінт віддає 401 з браузера, хоча користувач залогінений — у чому річ?
Ми написали type: integer в args, але приходить рядок і код падає. Чому WordPress не перевірив?
Чим permission_callback відрізняється від перевірки прав усередині callback?
REST API register_rest_route permission_callback валідація nonce

REST API в ядрі з WP 4.7, і власний маршрут реєструється однією функцією — register_rest_route($namespace, $route, $args). Ключове обмеження: викликати її можна лише на хуку rest_api_init, бо саме там ядро створює WP_REST_Server і збирає таблицю маршрутів. Виклик у файлі плагіна або на init мовчки нічого не дасть — маршрут просто не зʼявиться у /wp-json/. Namespace має вигляд vendor/v1: версія живе в namespace, а не в шляху, щоб згодом можна було випустити vendor/v2 поруч зі старим. Динамічні сегменти описуються іменованою групою регулярного виразу, /leads/(?P<id>\d+), і потрапляють у $request['id']. Якщо на сайті plain-перміалінки, /wp-json/ не працює й потрібен запасний вигляд ?rest_route=/crm/v1/leads — про це варто памʼятати, коли ендпоінт «не існує» лише на одному стенді.

Права перевіряє permission_callback, і з WP 5.5 це обовʼязковий аргумент: без нього ядро пише _doing_it_wrong, але маршрут усе одно реєструється і лишається публічним. Тому забутий permission_callback — це не помилка розробки, а відкритий ендпоінт у продакшені; для навмисно публічного маршруту пишуть явне 'permission_callback' => '__return_true', щоб намір було видно з коду. Усередині перевіряють здатність через current_user_can з конкретною capability, а для дії над конкретним записом — мета-здатність з id: current_user_can('edit_post', (int) $request['id']), бо вона проходить через map_meta_cap і враховує авторство та статус. is_user_logged_in() як перевірка прав означає, що будь-який передплатник дістає доступ до адмінської дії. Повертати з колбеку можна true, false або WP_Error; зручний хелпер rest_authorization_required_code() дає 401 для гостя й 403 для залогіненого.

Валідація описується в масиві args. Тут є пастка, на якій валяться на співбесіді: у рукописному args ключі type, enum, format, minimum самі по собі нічого не перевіряють — вони лише потрапляють у схему, яку віддає запит OPTIONS. Реальну перевірку робить validate_callback, тому в кожен параметр підставляють 'validate_callback' => 'rest_validate_request_arg', і лише тоді enum чи minimum починають відхиляти запит з кодом rest_invalid_param і статусом 400. Контролери, успадковані від WP_REST_Controller, отримують це безкоштовно: rest_get_endpoint_args_for_schema() будує args з get_item_schema() і сам додає rest_validate_request_arg та rest_sanitize_request_arg. Порядок теж важливий: ядро спершу валідує всі параметри (has_valid_params), потім санітизує (sanitize_params), тож у валідатор приходить сире значення. Санітизація без валідації небезпечна тим, що absint('abc') тихо перетворить сміття на 0 і запит виконається не з тими даними.

Автентифікація — окремий шар від авторизації. Запит із браузера на тому ж домені йде під cookie, але cookie в REST довіряють лише разом із nonce дії wp_rest: його передають у заголовку X-WP-Nonce або параметром _wpnonce, інакше ядро повертає rest_cookie_invalid_nonce, і current_user_can у вашому колбеку бачить гостя. Класичний антипатерн — «полікувати» цей 401 заміною перевірки на __return_true. Для зовнішніх клієнтів cookie не підходять взагалі: з WP 5.6 у ядрі є Application Passwords, які працюють як Basic Auth поверх HTTPS і не дають доступу до адмінки.

Відповідь формують поверненням масиву, WP_REST_Response або WP_Error — жодних echo і wp_die(), які ламають JSON і віддають 200 замість потрібного коду. WP_Error з ['status' => 4xx] ядро саме серіалізує у {code, message, data}. Для колекцій варто повторити ядрову поведінку: заголовки X-WP-Total і X-WP-TotalPages, параметри page і per_page з minimum/maximum, інакше per_page=100000 стане найдешевшим способом покласти базу. І межа розумності: власний маршрут виправданий там, де ресурс не мапиться на наявний, — агрегації, дії, інтеграції. Якщо треба лише додати поле до поста, дешевше register_rest_field() або register_post_meta() з 'show_in_rest' => true, ніж дублювати половину WP_REST_Posts_Controller.

// Маршрути реєструються ЛИШЕ на rest_api_init: раніше WP_REST_Server ще не існує
add_action('rest_api_init', function (): void {
    register_rest_route('crm/v1', '/leads', [
        'methods'  => WP_REST_Server::CREATABLE, // POST
        'callback' => 'crm_create_lead',
        // Обовʼязковий з WP 5.5; для публічного ендпоінта пишуть явне '__return_true'
        'permission_callback' => static function (WP_REST_Request $request): bool|WP_Error {
            if (! current_user_can('edit_others_posts')) {
                return new WP_Error(
                    'crm_forbidden',
                    'Недостатньо прав для створення ліда',
                    ['status' => rest_authorization_required_code()] // 401 гостю, 403 залогіненому
                );
            }

            return true;
        },
        'args' => [
            'email' => [
                'required'          => true,
                'type'              => 'string',
                'format'            => 'email',
                // Без validate_callback ключі type і format лишаються лише документацією схеми
                'validate_callback' => 'rest_validate_request_arg',
                'sanitize_callback' => 'sanitize_email',
            ],
            'source' => [
                'type'              => 'string',
                'enum'              => ['form', 'phone', 'import'],
                'default'           => 'form',
                'validate_callback' => 'rest_validate_request_arg',
            ],
        ],
    ]);
});

function crm_create_lead(WP_REST_Request $request): WP_REST_Response|WP_Error
{
    $postId = wp_insert_post([
        'post_type'   => 'crm_lead',
        'post_status' => 'private',
        'post_title'  => $request['email'],       // вже санітизоване
        'meta_input'  => ['source' => $request['source']],
    ], true); // true — повертати WP_Error замість 0

    if (is_wp_error($postId)) {
        $postId->add_data(['status' => 500]);

        return $postId; // ядро саме перетворить WP_Error на JSON з потрібним кодом
    }

    return new WP_REST_Response(['id' => $postId], 201);
}
Що маршрути реєструються лише на хуку rest_api_init, і namespace має вигляд vendor/v1 — версія в namespace, а не в шляху.
Що permission_callback обовʼязковий з WP 5.5: без нього ядро кидає _doing_it_wrong, а ендпоінт лишається публічним; для справді публічного треба явне '__return_true'.
Що права перевіряють через current_user_can з конкретною здатністю, а для конкретного обʼєкта — з його id: current_user_can('edit_post', $id), а не is_user_logged_in().
Що в args працюють sanitize_callback і validate_callback, а сам по собі ключ type у рукописному масиві args нічого не перевіряє — потрібен явний rest_validate_request_arg.
Що помилку повертають як WP_Error з ['status' => 4xx], а не echo/wp_die, і що rest_authorization_required_code() дає 401 для гостя й 403 для залогіненого.
Що cookie-автентифікація в REST вимагає nonce wp_rest у заголовку X-WP-Nonce, а для зовнішніх клієнтів є Application Passwords (WP 5.6+).
Викликати register_rest_route одразу при завантаженні плагіна, а не на rest_api_init — маршрут не зареєструється, бо WP_REST_Server ще не створений.
Ставити permission_callback => '__return_true' на ендпоінт, що пише в базу, «щоб не заважало», і отримати відкритий запис для анонімів.
Перевіряти is_user_logged_in() замість здатності: будь-який передплатник отримує доступ до адмінських дій.
Описати 'type' => 'integer' в args без validate_callback і вважати, що ядро перевірить тип; насправді валідація запускається лише за наявності validate_callback.
Повертати з callback echo json_encode(...) або wp_die() — відповідь ламає JSON і виходить 200 замість коректного статусу.
Забувати X-WP-Nonce у fetch з фронтенду й лікувати 401/403 тим, що ставлять '__return_true'.
Тестувати ендпоінт лише на /wp-json/ і дивуватись 404 на сайті з plain-перміалінками, де працює ?rest_route=.
ПОРАДА

Скажіть, що permission_callback — це не «додаткова опція», а обовʼязковий аргумент з WP 5.5, і що ключ type в args — документація для схеми, а не валідація: перевірку вмикає rest_validate_request_arg. Додайте, що для складніших ресурсів варто успадкувати WP_REST_Controller, бо там args генеруються з get_item_schema через rest_get_endpoint_args_for_schema і валідатори підставляються автоматично.

оновлено 5 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

З WP 5.5 permission_callback обовʼязковий, але його відсутність не блокує реєстрацію: ядро лише кидає _doing_it_wrong, а ендпоінт лишається відкритим для всіх. Саме тому забутий permission_callback — це діра, а не помилка на етапі розробки.