roles = array_keys(array_map(function ($instance) { return $instance->slug; }, Registrar::getRegistered('user'))); $this->content = array_map(function($content) { $registrar = Registrar::getInstance($content); return strtolower(str_replace(' ', '_', $registrar->getPlural()??$registrar->getSingular().'s')); },array_merge( Registrar::getRegistered('post'), Registrar::getFeatured('is_content', 'term') )); add_action('set_user_role', [$this, 'updateRoles'], 10, 3); } public function updateRoles(int $userID, string $role, array $oldRoles):void { if (doing_action('set_user_role') > 1) { return; } $temp = jvbNoBase($role); $registrar = Registrar::getInstance($temp); if ($registrar) { $user = get_userdata($userID); if (!$user) { return; } $this->reset($user); $this->setUserAs($user, $temp); } } /** * @param WP_User $user * * @return void */ public function reset(WP_User $user):void { foreach ($this->content as $content => $plural) { $content = jvbCheckBase($content); $this->grantContent($user, $content, false); $this->grantOthersContent($user, $content, false); } } private function isValidContentType(string $type): bool { $type = jvbNoBase($type); $registrar = Registrar::getInstance($type); if ($registrar && $registrar->getType() === 'post') { return true; } if ($registrar && $registrar->getType() === 'term' && $registrar->hasFeature('is_content')) { return true; } return false; } /** * Ensures the role can manage the content before changing capabilities * @param int|WP_User $user * @param string $content * @return bool */ public function checkRole(int|WP_User $user, string $content):bool { if(user_can($user, 'manage_options')) { return true; } if (is_int($user)) { $user = get_userdata($user); } $roles = array_keys($user->roles); foreach ($roles as $role) { $role = jvbNoBase($role); $registrar = Registrar::getInstance($role); if (!$registrar) { return false; } foreach ($registrar->getCreatable() as $type) { if (is_array($type) && in_array($content, $type)) { return true; } elseif ($content === $type) { return true; } } } return false; } /** * Grants content management capabilities to a user * @param WP_User $user * @param string $content * @param bool $add * @return void */ public function grantContent(WP_User $user, string $content, bool $add = true):void { if (!$this->isValidContentType($content) || !$this->checkRole($user, $content)) { return; } $capabilities = $this->getCapabilities($content); foreach ($capabilities as $cap) { if ($add) { $user->add_cap($cap); } else { $user->remove_cap($cap); } } } /** * Grants management of other users' content to a user * @param WP_User $user * @param string $content * @param bool $add * @return void */ public function grantOthersContent(WP_USER $user, string $content, bool $add = true) { if (!$this->isValidContentType($content) || !$this->checkRole($user, $content)) { return; } $capabilities = $this->getOthersCapabilities($content); foreach ($capabilities as $cap) { if ($add) { $user->add_cap($cap); } else { $user->remove_cap($cap); } } } /** * @param WP_User $user * @param string $type * * @return void */ public function setUserAs(WP_User $user, string $type):void { $role = jvbNoBase(array_keys($user->roles)[0]); $config = $this->getTypesConfig($role); if (!$config || !array_key_exists($type, $config)) { return; } foreach ($config as $role => $content) { if ($type !== $role) { foreach ($content as $c) { $this->grantContent($user, $c, false); } } else { foreach ($content as $c) { $this->grantContent($user, $c); } } } } protected function getTypesConfig(string $role):bool|array { $check = get_transient(BASE.'role_config_'.$role); if ($check) { return $check; } $registrar = Registrar::getInstance($role); if (!$registrar) { return false; } $out = []; foreach ($registrar->getCreatable() as $types) { if (is_array($types)) { $out = array_merge($out, $types); } else { $out = [$types]; } $out = array_unique($out); } set_transient(BASE.'role_config_'.$role, $out, MONTH_IN_SECONDS); return $out; } /** * @param WP_User $user * @param string $type * * @return int|false */ public function addUserLink(WP_User $user, string $type) { if (!in_array($type, $this->roles)) { return false; } $link = get_user_meta($user->ID, BASE.'link', true); if ($link === '') { $type = BASE.$type; $name = $user->display_name; $status = ($user->has_cap('skip_moderation')) ? 'publish' : 'draft'; $link = wp_insert_post([ 'post_type' => $type, 'post_status' => $status, 'post_title' => $name, 'post_author' => $user->ID, ]); if ($link) { update_user_meta($user->ID, BASE.'link', $link); update_post_meta($link, BASE.'link', $user->ID); } } return $link; } public function registerRole(string $slug): void { $role_name = BASE . $slug; $display_name = $config['label'] ?? ucfirst($slug); $registrar = Registrar::getInstance($slug); if (!$registrar){ return; } // Build capabilities for this role $capabilities = $this->buildRoleCapabilities($slug, $registrar); // Remove role first to ensure clean slate remove_role($role_name); // Add the role with capabilities add_role($role_name, $display_name, $capabilities); // Add management capabilities to administrator if ($registrar->hasFeature('has_dashboard') ?? false) { $admin_role = get_role('administrator'); if ($admin_role) { $admin_role->add_cap("manage_{$role_name}s", true); $admin_role->add_cap("edit_{$role_name}_settings", true); } } } private function buildRoleCapabilities(string $slug, Registrar $registrar): array { //Everyone can see the things $capabilities = [ 'read' => true, ]; // Dashboard access if ($registrar->hasFeature('has_dashboard') ?? false) { $capabilities['access_dashboard'] = true; } if (Features::forSite()->has('favourites') && $registrar->hasFeature('can_favourite') ?? true) { $capabilities['can_favourite'] = true; } // Content creation capabilities if (!empty($registrar->getCreatable())) { $content = []; foreach ($registrar->getCreatable() as $content_type) { if (is_array($content_type)) { $content = array_merge($content, $content_type); }else { $content[] = $content_type; } } foreach ($content as $c) { $this->addContentCapabilities($capabilities, $c, $registrar); } } // Management capabilities if (!empty($registrar->getManageOthers())) { foreach ($registrar->getManageOthers() as $type) { // Skip if content type doesn't exist if (!$this->isValidContentType($type)) { error_log("Warning: User role '{$slug}' references non-existent content type '{$type}'"); continue; } $addCaps = $this->getOthersCapabilities($type); foreach ($addCaps as $cap) { $capabilities[$cap] = true; } } } return apply_filters(BASE . 'role_capabilities', $capabilities, $slug); } /** * Add content capabilities to capability array */ private function addContentCapabilities(array &$capabilities, $content_type, Registrar $registrar): void { if (!$this->isValidContentType($content_type)) { error_log("Warning: Role references non-existent content type '{$content_type}'"); return; } $this->addSingleContentCapabilities($capabilities, $content_type, $registrar); } /** * Add capabilities for a single content type */ private function addSingleContentCapabilities(array &$capabilities, string $type, Registrar $registrar): void { $caps = $this->getCapabilities($type); foreach ($caps as $cap) { $capabilities[$cap] = true; } if ($registrar->hasFeature('approve_new')) { $plural = $this->getContentPlural($type); // Publish capability depends on approval setting $capabilities["publish_{$plural}"] = !($config['approve_new'] ?? false); } } public function grantRoleCapabilities(string $role_name, string $content_slug, bool $grant = true): void { $role = get_role($role_name); if (!$role) { return; } $capabilities = $this->getCapabilities(jvbNoBase($content_slug)); foreach ($capabilities as $capability) { if ($grant) { $role->add_cap($capability); } else { $role->remove_cap($capability); } } } public function grantRoleOthersCapabilities(string $role_name, string $content_slug, bool $grant = true): void { $role = get_role($role_name); if (!$role) { return; } $capabilities = $this->getOthersCapabilities(jvbNoBase($content_slug)); foreach ($capabilities as $capability) { if ($grant) { $role->add_cap($capability); } else { $role->remove_cap($capability); } } } /** * @param string $content * @return array|string[] * Note: must match what is created in PostTypeRegistrar.php::register */ protected function getCapabilities(string $content):array { $content = jvbNoBase($content); if (!$this->isValidContentType($content)) { return []; } $plural = $this->getContentPlural($content); return [ "edit_{$content}", "read_{$content}", "delete_{$content}", "edit_{$plural}", "edit_others_{$plural}", "publish_{$plural}", "read_private_{$plural}", ]; } protected function getOthersCapabilities(string $content):array { $content = jvbNoBase($content); if (!$this->isValidContentType($content)) { return []; } $plural = $this->getContentPlural($content); return [ "edit_others_{$plural}", "delete_others_{$plural}", "read_private_{$plural}", "edit_private_{$plural}", "delete_private_{$plural}", ]; } public static function getPlural(string $content): string { $self = new self; return $self->getContentPlural($content); } public function getContentPlural(string $content): string { $content = jvbNoBase($content); $registrar = Registrar::getInstance($content); if ($registrar && $registrar->getPlural()) { return str_replace(' ', '_', $registrar->getPlural()); } return str_replace(' ', '_', $content.'s'); } public function activate(): void { foreach (Registrar::getRegistered('user') as $role) { $this->registerRole($role); } } /****************************************************************** * OWNABLE and MANAGABLE terms (ie: tattoo shops) ******************************************************************/ /** * Grant ownership of a content taxonomy term * Owners have full control over the term and its members * * @param int $userID User ID * @param int $termID Term ID * @param string $taxonomy Taxonomy slug (without BASE) * @return bool Success */ public function grantOwnership(int $userID, int $termID, string $taxonomy): bool { if (!get_userdata($userID) || !term_exists($termID)){ return false; } $taxonomy = jvbNoBase($taxonomy); $registrar = Registrar::getInstance($taxonomy); // Verify this is an ownable content taxonomy if (!$registrar || !$registrar->hasFeature('is_content') || !$registrar->hasFeature('is_ownable')) { return false; } $user = get_userdata($userID); if (!$user) { return false; } // Grant both ownership and management $user->add_cap(BASE . 'can_own_' . $termID); $user->add_cap(BASE . 'can_manage_' . $termID); do_action(BASE . 'granted_ownership', $userID, $termID, $taxonomy); return true; } /** * Revoke ownership of a content taxonomy term * * @param int $userID User ID * @param int $termID Term ID * @param string $taxonomy Taxonomy slug (without BASE) * @return bool Success */ public function revokeOwnership(int $userID, int $termID, string $taxonomy): bool { if (!get_userdata($userID) || !term_exists($termID)){ return false; } $taxonomy = jvbNoBase($taxonomy); $user = get_userdata($userID); if (!$user) { return false; } $user->remove_cap(BASE . 'can_own_' . $termID); do_action(BASE . 'revoked_ownership', $userID, $termID, $taxonomy); return true; } /** * Grant management capabilities for a content taxonomy term * Managers can approve members and edit content but don't own the term * * @param int $userID User ID * @param int $termID Term ID * @param string $taxonomy Taxonomy slug (without BASE) * @return bool Success */ public function grantManagement(int $userID, int $termID, string $taxonomy): bool { if (!get_userdata($userID) || !term_exists($termID)){ return false; } $taxonomy = jvbNoBase($taxonomy); $registrar = Registrar::getInstance($taxonomy); // Verify this is an ownable content taxonomy if (!$registrar || !$registrar->hasFeature('is_content') || !$registrar->hasFeature('is_ownable')) { return false; } $user = get_userdata($userID); if (!$user) { return false; } $user->add_cap(BASE . 'can_manage_' . $termID); do_action(BASE . 'granted_management', $userID, $termID, $taxonomy); return true; } /** * Revoke management capabilities for a content taxonomy term * * @param int $userID User ID * @param int $termID Term ID * @param string $taxonomy Taxonomy slug (without BASE) * @return bool Success */ public function revokeManagement(int $userID, int $termID, string $taxonomy): bool { if (!get_userdata($userID) || !term_exists($termID)){ return false; } $taxonomy = jvbNoBase($taxonomy); $user = get_userdata($userID); if (!$user) { return false; } $user->remove_cap(BASE . 'can_manage_' . $termID); do_action(BASE . 'revoked_management', $userID, $termID, $taxonomy); return true; } /** * Check if user owns a term * * @param int $userID User ID * @param int $termID Term ID * @return bool */ public function isOwner(int $userID, int $termID): bool { return user_can($userID, BASE . 'can_own_' . $termID); } /** * Check if user can manage a term (owner or manager) * * @param int $userID User ID * @param int $termID Term ID * @return bool */ public function isManager(int $userID, int $termID): bool { return user_can($userID, BASE . 'can_manage_' . $termID) || user_can($userID, BASE . 'can_own_' . $termID); } /** * Get all terms a user owns * * @param int $userID User ID * @param string|null $taxonomy Optional: filter by taxonomy * @return array Array of term IDs */ public function getOwnedTerms(int $userID, ?string $taxonomy = null): array { $user = get_userdata($userID); if (!$user) { return []; } $owned = []; foreach ($user->allcaps as $cap => $value) { if ($value && strpos($cap, BASE . 'can_own_') === 0) { $termID = (int) str_replace(BASE . 'can_own_', '', $cap); if ($termID) { $owned[] = $termID; } } } // Filter by taxonomy if specified if ($taxonomy && !empty($owned)) { $taxonomy = jvbCheckBase($taxonomy); $filtered = []; foreach ($owned as $termID) { $term = get_term($termID); if ($term && !is_wp_error($term) && $term->taxonomy === $taxonomy) { $filtered[] = $termID; } } return $filtered; } return $owned; } /** * Get all terms a user can manage (owns or manages) * * @param int $userID User ID * @param string|null $taxonomy Optional: filter by taxonomy * @return array Array of term IDs */ public function getManagedTerms(int $userID, ?string $taxonomy = null): array { $user = get_userdata($userID); if (!$user) { return []; } $managed = []; foreach ($user->allcaps as $cap => $value) { if ($value && (strpos($cap, BASE . 'can_manage_') === 0 || strpos($cap, BASE . 'can_own_') === 0)) { $termID = (int) str_replace([BASE . 'can_manage_', BASE . 'can_own_'], '', $cap); if ($termID && !in_array($termID, $managed)) { $managed[] = $termID; } } } // Filter by taxonomy if specified if ($taxonomy && !empty($managed)) { $taxonomy = jvbCheckBase($taxonomy); $filtered = []; foreach ($managed as $termID) { $term = get_term($termID); if ($term && !is_wp_error($term) && $term->taxonomy === $taxonomy) { $filtered[] = $termID; } } return $filtered; } return $managed; } /** * Get all ownable taxonomies * * @return array Array of taxonomy slugs */ public function getOwnableTaxonomies(): array { static $ownable = null; if ($ownable === null) { $ownable = array_map(function ($instance) { return $instance->slug; }, Registrar::getFeatured('is_ownable', 'term')); } return $ownable; } /** * Get all invitable taxonomies * * @return array Array of taxonomy slugs */ public function getInvitableTaxonomies(): array { static $invitable = null; if ($invitable === null) { $invitable = array_map(function ($instance) { return $instance->slug; }, Registrar::getFeatured('invitable', 'term')); } return $invitable; } public function getPermission(string $action, string $content, ?int $ID = null):?string { $plural = $this->getContentPlural($content); switch ($action) { case 'edit': if ($ID) { return "edit_{$content}"; } return "edit_{$plural}"; } return null; } }