Роль у WordPress не містить коду. Це рядок у wp_options під ключем wp_user_roles, де лежить масив виду ['editor' => ['name' => 'Editor', 'capabilities' => ['edit_posts' => true, ...]]]. Те, що роль призначена конкретній людині, зберігається окремо, в usermeta wp_capabilities, і теж масивом: ключ - слаг ролі або окрема capability, значення - true або false. Під час завантаження користувача WP_User::get_role_caps() зводить роль і персональні права в один плаский масив allcaps, і далі всі перевірки працюють уже з ним.
Звідси класична пастка. Слаг ролі лежить у тому самому масиві caps, тому current_user_can('editor') повертає true для редактора, хоч можливості з такою назвою не існує. Код «працює», поки роль призначена напряму, і тихо ламається, коли права видає інший плагін через фільтр user_has_cap або коли в користувача дві ролі. Перевіряти треба дію: current_user_can('manage_options'), current_user_can('edit_courses'). Назва ролі - організаційна зручність замовника, і список ролей на живому сайті перекроюють як завгодно.
Primitive capabilities (edit_posts, publish_posts, upload_files) лежать у ролях і перевіряються як є. Meta (edit_post, delete_post, read_post) у ролях відсутні: map_meta_cap() розкладає їх на primitive, дивлячись на конкретний обʼєкт - хто автор, чи запис опублікований, чи він приватний. Тому current_user_can('edit_post', $postId) і current_user_can('edit_posts') відповідають на різні питання, а meta cap без ID обʼєкта мапиться на do_not_allow. Для власного post type цю машинерію увімкніть явно: 'map_meta_cap' => true поряд з 'capability_type', інакше мапінг для нестандартного типу не відбудеться.
Власна роль додається функцією add_role, окрема можливість до наявної ролі - через get_role('editor')->add_cap(). Обидві пишуть в options, тому їх місце в register_activation_hook, а не в коді, що виконується на кожен запит. Другий нюанс: add_role повертає null, якщо роль із таким слагом уже є, і не оновлює нічого. Щоб дописувати capabilities у вже встановлений плагін, тримайте в options номер версії схеми прав і доганяйте різницю через add_cap/remove_cap на plugins_loaded: активаційний хук після звичайного оновлення плагіна не спрацьовує.
Прибирання йде в uninstall.php, не в деактивацію: вимкнути плагін на годину людина може будь-коли, і знищений при цьому розподіл прав на сайті нікого не порадує. Порядок дій тут важливий. remove_role видаляє роль із options, але слаг залишається в usermeta кожного, кому її призначили, і такий користувач входить в адмінку взагалі без можливостей. Спершу переводимо людей на безпечну роль через set_role, далі видаляємо роль і прибираємо ті capabilities, які плагін додавав у вбудовані ролі. Результат перевіряйте під реальною роллю, а не під адміністратором: адміністратор на одиночному сайті проходить майже будь-яку перевірку й приховує дірки, які під редактором видно одразу.
// Ролі пишемо в базу один раз - при активації, не на кожен запит
register_activation_hook(__FILE__, function (): void {
add_role('course_manager', 'Менеджер курсів', [
'read' => true,
'edit_courses' => true, // свої курси
'edit_published_courses' => true,
'publish_courses' => true,
'upload_files' => true,
]); // поверне null, якщо роль уже існує - нічого не перезапише
get_role('editor')?->add_cap('edit_others_courses'); // доповнюємо вбудовану роль
});
// Без map_meta_cap => true meta caps для власного типу не мапляться
register_post_type('course', [
'public' => true,
'capability_type' => ['course', 'courses'],
'map_meta_cap' => true,
]);
// Правило на конкретний запис: закритий курс не редагує ніхто, крім адміна
add_filter('map_meta_cap', function (array $caps, string $cap, int $userId, array $args): array {
if ($cap !== 'edit_post' || ! isset($args[0])) {
return $caps;
}
$post = get_post($args[0]);
if ($post?->post_type === 'course'
&& get_post_meta($post->ID, '_course_locked', true)
&& ! user_can($userId, 'manage_options')) {
return ['do_not_allow'];
}
return $caps;
}, 10, 4);
// Перевірка там, де відбувається дія, а не лише там, де малюється кнопка
if (! current_user_can('edit_post', $courseId)) { // meta cap - разом з ID
wp_die('Недостатньо прав', 403);
}
Скажіть, що право на дію перевіряється в тому ж місці, де дія відбувається, а не там, де малюється кнопка. І згадайте версійність схеми прав: номер версії в options плюс міграція на `plugins_loaded` дають змогу дописувати capabilities у вже встановлений плагін, бо `add_role` існуючу роль не чіпає.