<?php
|
namespace JVBase\managers;
|
|
use JVBase\utility\Features;
|
use WP_User;
|
use WP_Role;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
class RoleManager
|
{
|
private array $roles;
|
private array $content;
|
|
public function __construct()
|
{
|
$this->roles = array_keys(JVB_USER);
|
$this->content = array_map(function($content) {
|
return strtolower($content['plural']??$content['singular'].'s');
|
},JVB_CONTENT);
|
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);
|
if (array_key_exists($temp, JVB_USER)) {
|
$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);
|
|
// Check in JVB_CONTENT array
|
if (array_key_exists($type, JVB_CONTENT)) {
|
return true;
|
}
|
|
// Check in JVB_TAXONOMY for content taxonomies
|
if (array_key_exists($type, JVB_TAXONOMY)) {
|
$tax_config = JVB_TAXONOMY[$type];
|
if ($tax_config['is_content'] ?? false) {
|
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);
|
$config = JVB_USER[$role]??false;
|
if (!$config) {
|
return false;
|
}
|
foreach ($config 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;
|
}
|
$check = JVB_USER[$role]['can_create'] ??false;
|
if (!$check) {
|
return false;
|
}
|
$out = [];
|
foreach ($check as $types) {
|
foreach ($types as $type => $content) {
|
$out[$type] = $content;
|
}
|
}
|
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, array $config): void
|
{
|
$role_name = BASE . $slug;
|
$display_name = $config['label'] ?? ucfirst($slug);
|
|
// Build capabilities for this role
|
$capabilities = $this->buildRoleCapabilities($slug, $config);
|
|
// 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 ($config['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, array $config): array
|
{
|
//Everyone can see the things
|
$capabilities = [
|
'read' => true,
|
];
|
|
// Dashboard access
|
if ($this->config['has_dashboard'] ?? false) {
|
$capabilities['access_dashboard'] = true;
|
}
|
|
if (Features::forSite()->has('favourites') && $config['can_favourite'] ?? true) {
|
$capabilities['can_favourite'] = true;
|
}
|
|
// Content creation capabilities
|
if (!empty($config['can_create'])) {
|
foreach ($config['can_create'] as $content_type) {
|
$this->addContentCapabilities($capabilities, $content_type, $config);
|
}
|
}
|
|
// Management capabilities
|
if (!empty($this->config['manage_others'])) {
|
foreach ($this->config['manage_others'] 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, array $config): void
|
{
|
if (is_array($content_type)) {
|
// Handle array format for type-specific permissions
|
foreach ($content_type as $sub_type => $types) {
|
foreach ($types as $type) {
|
if (!$this->isValidContentType($type)) {
|
error_log("Warning: Role references non-existent content type '{$type}'");
|
continue;
|
}
|
$this->addSingleContentCapabilities($capabilities, $type, $config);
|
}
|
}
|
} else {
|
if (!$this->isValidContentType($content_type)) {
|
error_log("Warning: Role references non-existent content type '{$content_type}'");
|
return;
|
}
|
$this->addSingleContentCapabilities($capabilities, $content_type, $config);
|
}
|
}
|
|
/**
|
* Add capabilities for a single content type
|
*/
|
private function addSingleContentCapabilities(array &$capabilities, string $type, array $config): void
|
{
|
$caps = $this->getCapabilities($type);
|
foreach ($caps as $cap) {
|
$capabilities[$cap] = true;
|
}
|
|
if (array_key_exists('approve_new', $config)) {
|
$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}",
|
"edit_{$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);
|
$config = Features::getConfig($content);
|
$capsMap = $config['capability_type']??[];
|
if (empty($capsMap)){
|
$capsMap = [
|
$content,
|
str_replace('-', '_',sanitize_title(strtolower(JVB_CONTENT[$content]['plural']??JVB_TAXONOMY[$content]['plural'])))
|
];
|
return $capsMap[1];
|
}
|
return str_replace('-', '_', sanitize_title(strtolower($content . 's')));
|
}
|
|
public function activate(): void
|
{
|
foreach (JVB_USER as $slug => $config) {
|
$this->registerRole($slug, $config);
|
}
|
}
|
|
/******************************************************************
|
* 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);
|
|
// Verify this is an ownable content taxonomy
|
if (!Features::forTaxonomy($taxonomy)->has('is_content') ||
|
!Features::forTaxonomy($taxonomy)->has('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);
|
|
// Verify this is an ownable content taxonomy
|
if (!Features::forTaxonomy($taxonomy)->has('is_content') ||
|
!Features::forTaxonomy($taxonomy)->has('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 = [];
|
foreach (JVB_TAXONOMY as $taxonomy => $config) {
|
if (Features::forTaxonomy($taxonomy)->has('is_content') &&
|
Features::forTaxonomy($taxonomy)->has('is_ownable')) {
|
$ownable[] = $taxonomy;
|
}
|
}
|
}
|
|
return $ownable;
|
}
|
|
/**
|
* Get all invitable taxonomies
|
*
|
* @return array Array of taxonomy slugs
|
*/
|
public function getInvitableTaxonomies(): array
|
{
|
static $invitable = null;
|
|
if ($invitable === null) {
|
$invitable = [];
|
foreach (JVB_TAXONOMY as $taxonomy => $config) {
|
if (Features::forTaxonomy($taxonomy)->has('invitable')) {
|
$invitable[] = $taxonomy;
|
}
|
}
|
}
|
|
return $invitable;
|
}
|
}
|