roles as $slug => $role) { if ($slug !== 'administrator' && !empty($role['capabilities'][self::EDITOR_CAPABILITY])) { $roles[] = (string) $slug; } } sort($roles); return $roles; } /** * Filter `user_has_cap`: grant our capabilities. Never calls current_user_can() (that would run this filter * again); only looks at the capabilities and roles WordPress hands over. * * @param array $allcaps All capabilities of the user (name => bool). * @param array $caps The primitive capabilities that are asked for. * @param array $args The arguments of the has_cap() call. * @param \WP_User $user The user. * * @return array */ public static function grant($allcaps, $caps, $args, $user) { if (!is_array($allcaps) || !is_array($caps)) { return $allcaps; } $ours = array_filter($caps, function ($cap) { return is_string($cap) && self::isOurs($cap); }); if ($ours === []) { return $allcaps; } $roles = is_object($user) && isset($user->roles) && is_array($user->roles) ? $user->roles : []; $rules = self::rules(); foreach ($ours as $cap) { if (self::allows($cap, $allcaps, $roles, $rules)) { $allcaps[$cap] = true; } } return $allcaps; } /** * Whether a user with these capabilities and roles may have one of our capabilities. * * @param string $cap `aop_page_…` or `aop_menu_…`. * @param array $allcaps The user's capabilities (name => bool). * @param array $roles The user's role slugs. * @param array $rules See rulesFrom(). */ public static function allows(string $cap, array $allcaps, array $roles, array $rules): bool { // 1. Administrators can open every page. 2. A role plugin gave this capability to the role or the user. if (!empty($allcaps[self::ADMINISTRATOR_CAPABILITY]) || !empty($allcaps[$cap])) { return true; } if (strpos($cap, self::MENU_PREFIX) === 0) { foreach ($rules['menus'][$cap] ?? [] as $pageCap) { if (self::allows($pageCap, $allcaps, $roles, $rules)) { return true; } } return false; } if (!isset($rules['pages'][$cap])) { return false; } $page = $rules['pages'][$cap]; // 3. The roles that are ticked for the page. if (is_array($page['roles'])) { return array_intersect($roles, $page['roles']) !== []; } // 4. A page saved before 1.1: the stored capability, as it worked in 1.0. return $page['capability'] !== '' && !empty($allcaps[$page['capability']]); } /** * Rules from stored page values. * * @param array $pageValues Decoded page values. * * @return array{pages: array, menus: array} */ public static function rulesFrom(array $pageValues): array { $rules = ['pages' => [], 'menus' => []]; foreach ($pageValues as $pageValue) { if (!is_array($pageValue) || !isset($pageValue['pages']) || !is_array($pageValue['pages'])) { continue; } $pageCaps = []; foreach ($pageValue['pages'] as $page) { if (!is_array($page) || empty($page['menu_slug']) || !is_string($page['menu_slug'])) { continue; } $cap = self::PAGE_PREFIX . self::slug($page['menu_slug']); $pageCaps[] = $cap; $rules['pages'][$cap] = [ 'roles' => isset($page['roles']) && is_array($page['roles']) ? array_values(array_filter($page['roles'], 'is_string')) : null, 'capability' => isset($page['capability']) && is_string($page['capability']) ? $page['capability'] : '', 'title' => isset($page['menu_title']) && is_string($page['menu_title']) ? $page['menu_title'] : $page['menu_slug'], ]; } if (!isset($pageValue['menu']['menu_slug']) || !is_string($pageValue['menu']['menu_slug'])) { continue; } // The menu takes the slug of its first page the user can open (Data::getAllFromPageValueColumn()), // so the menu is known under the slug of every page as well. $slugs = array_merge([$pageValue['menu']['menu_slug']], array_column(array_filter($pageValue['pages'], 'is_array'), 'menu_slug')); foreach (array_unique(array_filter($slugs, 'is_string')) as $slug) { $rules['menus'][self::MENU_PREFIX . self::slug($slug)] = $pageCaps; } } return $rules; } /** * The page capabilities with a label (the menu title), for role plugins. * * @return array capability => label */ public static function pageCapabilities(): array { return array_map(function ($page) { return $page['title']; }, self::rules()['pages']); } /** * Members: a group "Admin Options Pages" on the screen where roles are edited. */ public static function registerMembersGroup(): void { if (!function_exists('members_register_cap_group') || self::pageCapabilities() === []) { return; } // Called by name: the function only exists while Members is active. call_user_func('members_register_cap_group', Plugin::NAME, [ 'label' => Plugin::TITLE, 'icon' => 'dashicons-admin-generic', 'priority' => 30, ]); } /** * Members: one capability per page, labelled with the menu title of the page. */ public static function registerMembersCaps(): void { if (!function_exists('members_register_cap')) { return; } foreach (self::pageCapabilities() as $cap => $label) { call_user_func('members_register_cap', $cap, [ 'label' => $label, 'group' => Plugin::NAME, ]); } } /** * Filter `members_get_capabilities` (Members, and User Role Editor reads it too): add the page capabilities. * * @param array $caps * * @return array */ public static function addCapabilityNames($caps) { return array_values(array_unique(array_merge((array) $caps, array_keys(self::pageCapabilities())))); } /** * User Role Editor: a group "Admin Options Pages" under the custom capabilities. * * @param array $groups * * @return array */ public static function addUreGroup($groups) { $groups = (array) $groups; $groups[Plugin::_NAME] = ['caption' => Plugin::TITLE, 'parent' => 'custom', 'level' => 2]; return $groups; } /** * User Role Editor: put the page capabilities in our group. * * @param array $groups * @param string $cap * * @return array */ public static function ureGroupsOf($groups, $cap) { $groups = (array) $groups; if (is_string($cap) && strpos($cap, self::PAGE_PREFIX) === 0) { $groups[] = 'custom'; $groups[] = Plugin::_NAME; } return $groups; } /** * The rules of the stored pages. Read from the table directly, not through Data: Data asks current_user_can() * for every page, which would run the filter again. */ private static function rules(): array { if (self::$rules === null) { $pageValues = []; $rows = PageRepository::tableExists() ? PageRepository::pageValueRows() : []; foreach ($rows as $row) { if (isset($row->page_value) && is_string($row->page_value)) { $pageValues[] = PageRepository::decode($row->page_value); } } self::$rules = self::rulesFrom($pageValues); } return self::$rules; } private static function isOurs(string $cap): bool { return strpos($cap, self::PAGE_PREFIX) === 0 || strpos($cap, self::MENU_PREFIX) === 0; } /** * A slug as a role plugin stores it in a capability name: Members runs names through sanitize_key(). */ private static function slug(string $slug): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($slug)); } }