cap_key => bool */ private static array $cap_cache = []; /** @var array "uid:plate_id" => tier */ private static array $tier_cache = []; /** @var AKSRT_Forum */ private $forum; public function __construct($forum) { $this->forum = $forum; } // ── Capability matrix ───────────────────────────────────────── /** * Capability definitions matrix. * * Keys: * 'min_tier' — minimum TIER_* constant required * 'context' — 'topic'|'plate'|'global'|'any' * 'self' — true = only applies to own content * 'logged' — shorthand: any logged-in user * 'settings_key' — optional forum settings permissions key */ private function cap_defs(): array { return [ // ── Topic viewing ───────────────────────────────────────────── 'topic_view_pending' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], 'topic_view_hidden' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], 'topic_view_deleted' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'], // ── Topic editing ───────────────────────────────────────────── 'topic_edit' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'], 'topic_edit_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], 'topic_delete' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'], 'topic_delete_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], // ── Topic governance ────────────────────────────────────────── 'topic_sticky_plate' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], 'topic_sticky_super' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate'], 'topic_sticky_global' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'], 'topic_featured' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], 'topic_lock' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'], 'topic_lock_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], 'topic_highlight' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], 'topic_score_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], 'topic_move' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], 'topic_moderate' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], // ── Topic interaction (logged users) ────────────────────────── 'topic_vote' => ['min_tier' => self::TIER_NONE, 'logged' => true], 'topic_favorite' => ['min_tier' => self::TIER_NONE, 'logged' => true], 'topic_score' => ['min_tier' => self::TIER_NONE, 'logged' => true], 'topic_report' => ['min_tier' => self::TIER_NONE, 'logged' => true], // ── Reply governance ────────────────────────────────────────── 'reply_adopt' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'], 'reply_adopt_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], 'reply_delete_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], 'reply_edit_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], 'reply_top' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], // ── Reply interaction ───────────────────────────────────────── 'reply_like' => ['min_tier' => self::TIER_NONE, 'logged' => true], 'reply_score' => ['min_tier' => self::TIER_NONE, 'logged' => true], // ── Plate management ────────────────────────────────────────── 'plate_follow' => ['min_tier' => self::TIER_NONE, 'logged' => true], 'plate_create' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'], 'plate_manage' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate'], 'plate_delete' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'], // ── Moderator system ────────────────────────────────────────── 'moderator_manage' => ['min_tier' => self::TIER_CAT_MOD, 'context' => 'plate'], 'moderator_apply' => ['min_tier' => self::TIER_NONE, 'logged' => true], 'moderator_review' => ['min_tier' => self::TIER_CAT_MOD, 'context' => 'plate'], // ── User governance ─────────────────────────────────────────── 'user_ban' => ['min_tier' => self::TIER_CAT_MOD, 'context' => 'global'], 'user_report' => ['min_tier' => self::TIER_NONE, 'logged' => true], // ── Compose capabilities ────────────────────────────────────── 'compose_post' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => ''], 'compose_post_no_review' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], 'compose_upload_image' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_img'], 'compose_upload_video' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_video'], 'compose_upload_file' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_file'], 'compose_iframe_video' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_iframe_video'], 'compose_set_type' => ['min_tier' => self::TIER_NONE, 'logged' => true], 'compose_set_visibility' => ['min_tier' => self::TIER_NONE, 'logged' => true], 'compose_set_cover' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_img'], 'compose_set_vote' => ['min_tier' => self::TIER_NONE, 'logged' => true], 'compose_set_paid' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate', 'settings_key' => 'paid_content'], 'compose_set_hide' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate', 'settings_key' => 'paid_content'], ]; } // ── Tier resolution ─────────────────────────────────────────── /** * Return the user's highest tier for a given plate. * * Result is cached per request on "$user_id:$plate_id". */ public function get_user_plate_tier(int $user_id, int $plate_id): int { if ($user_id <= 0) { return self::TIER_NONE; } $cache_key = "$user_id:$plate_id"; if (isset(self::$tier_cache[$cache_key])) { return self::$tier_cache[$cache_key]; } $tier = $this->compute_user_plate_tier($user_id, $plate_id); self::$tier_cache[$cache_key] = $tier; return $tier; } private function compute_user_plate_tier(int $user_id, int $plate_id): int { // Tier 4: WP admin or bbs_admin_users list if ($this->forum->is_forum_admin($user_id)) { return self::TIER_FORUM_ADMIN; } // Tier 3: category moderator (plate_cat term meta 'moderator') $cats = get_the_terms($plate_id, 'aksrt_plate_cat'); if ($cats && !is_wp_error($cats)) { foreach ($cats as $cat) { if ($this->forum->is_cat_moderator($user_id, (int) $cat->term_id)) { return self::TIER_CAT_MOD; } } } // Tier 2: plate_author (plate CPT post_author) if ($this->forum->is_plate_cpt_active()) { $post = get_post($plate_id); if ($post && (int) $post->post_author === $user_id) { return self::TIER_PLATE_AUTHOR; } // Tier 1: in plate _aksrt_moderators list $mods = get_post_meta($plate_id, '_aksrt_moderators', true); if (is_array($mods) && in_array($user_id, $mods, true)) { return self::TIER_MODERATOR; } return self::TIER_NONE; } // Fallback: taxonomy board $roles = get_term_meta($plate_id, '_aksrt_moderator_roles', true); $roles = is_array($roles) ? $roles : []; $role = $roles[$user_id] ?? ''; if ($role === 'super_moderator' || $role === 'plate_author') { return self::TIER_PLATE_AUTHOR; } $mods = get_term_meta($plate_id, '_aksrt_moderators', true); if (is_array($mods) && in_array($user_id, $mods, true)) { return self::TIER_MODERATOR; } return self::TIER_NONE; } // ── Capability check ────────────────────────────────────────── /** * Unified capability resolver. * * @param int $user_id User to check (0 = guest). * @param string $cap Capability id from cap_defs(). * @param int $context_id topic_id for 'topic' context, plate_id for 'plate' context. * @return bool */ public function forum_user_can(int $user_id, string $cap, int $context_id = 0): bool { $cache_key = "$user_id:$cap:$context_id"; if (array_key_exists($cache_key, self::$cap_cache)) { return self::$cap_cache[$cache_key]; } $result = $this->compute_forum_cap($user_id, $cap, $context_id); self::$cap_cache[$cache_key] = $result; return $result; } private function compute_forum_cap(int $user_id, string $cap, int $context_id): bool { $defs = $this->cap_defs(); if (!isset($defs[$cap])) { return false; } $def = $defs[$cap]; $logged_only = !empty($def['logged']) && ($def['min_tier'] ?? self::TIER_NONE) === self::TIER_NONE; $self_only = !empty($def['self']); $context = $def['context'] ?? 'any'; $min_tier = (int) ($def['min_tier'] ?? self::TIER_NONE); $settings_key = $def['settings_key'] ?? ''; // Guest short-circuit if ($user_id <= 0) { return false; } // Forum-wide admin bypasses everything if ($this->forum->is_forum_admin($user_id)) { // Honour settings-gated compose caps even for admins when the feature is globally off if ($settings_key !== '') { $perms = $this->forum->get_config()['permissions'] ?? []; if (isset($perms[$settings_key]) && $perms[$settings_key] === false) { return false; } } return true; } // 'logged' / any-user caps: just need to be logged in if ($logged_only || ($min_tier === self::TIER_NONE && $context === 'any')) { if ($settings_key !== '') { $perms = $this->forum->get_config()['permissions'] ?? []; if (array_key_exists($settings_key, $perms) && empty($perms[$settings_key])) { return false; } } return true; } // 'self' caps: caller passes topic_id as context; check authorship if ($self_only) { if ($context_id <= 0) { return false; } $post = get_post($context_id); return $post && (int) $post->post_author === $user_id; } // Global caps (no specific plate/topic context) if ($context === 'global') { if ($min_tier <= self::TIER_CAT_MOD) { return $this->is_cat_moderator_anywhere($user_id); } return false; } // Plate-context caps if ($context === 'plate') { $plate_id = $this->resolve_plate_id($context_id, $cap); if ($plate_id <= 0) { return false; } $tier = $this->get_user_plate_tier($user_id, $plate_id); // Check optional settings gate if ($settings_key !== '') { $perms = $this->forum->get_config()['permissions'] ?? []; if (array_key_exists($settings_key, $perms) && empty($perms[$settings_key])) { return $tier >= self::TIER_PLATE_AUTHOR; } } return $tier >= $min_tier; } return false; } // ── Helper methods ──────────────────────────────────────────── /** * Resolve a plate_id from either a plate_id or topic_id, depending on the cap context. */ private function resolve_plate_id(int $context_id, string $cap): int { if ($context_id <= 0) { return 0; } $post = get_post($context_id); if (!$post) { return 0; } if ($post->post_type === 'aksrt_forum_plate') { return $context_id; } if ($post->post_type === 'aksrt_forum_topic') { return $this->forum->get_topic_plate_id($context_id); } return 0; } /** * Check if the user is a cat_moderator on ANY plate category (for global caps). */ private function is_cat_moderator_anywhere(int $user_id): bool { if ($user_id <= 0) { return false; } if ($this->forum->is_forum_admin($user_id)) { return true; } $cats = get_terms(['taxonomy' => 'aksrt_plate_cat', 'hide_empty' => false, 'fields' => 'ids']); if (is_wp_error($cats) || empty($cats)) { return false; } foreach ((array) $cats as $cat_id) { if ($this->forum->is_cat_moderator($user_id, (int) $cat_id)) { return true; } } return false; } // ── Compose capabilities ────────────────────────────────────── /** * Get per-feature capabilities for the compose/edit form. * * @return array */ public function get_compose_capabilities(int $user_id): array { $keys = [ 'can_post', 'can_set_type', 'can_set_visibility', 'can_set_cover', 'can_set_cover_image', 'can_set_cover_slide', 'can_set_cover_video', 'can_set_vote', 'can_set_paid', 'can_set_hide', 'can_upload_image', 'can_upload_video', 'can_upload_file', 'can_iframe_video', ]; if ($user_id <= 0) { return array_fill_keys($keys, false); } return [ 'can_post' => $this->forum_user_can($user_id, 'compose_post'), 'can_set_type' => $this->forum_user_can($user_id, 'compose_set_type'), 'can_set_visibility' => $this->forum_user_can($user_id, 'compose_set_visibility'), 'can_set_cover' => $this->forum_user_can($user_id, 'compose_set_cover'), 'can_set_cover_image' => $this->forum_user_can($user_id, 'compose_set_cover'), 'can_set_cover_slide' => $this->forum_user_can($user_id, 'compose_set_cover'), 'can_set_cover_video' => $this->forum_user_can($user_id, 'compose_upload_video'), 'can_set_vote' => $this->forum_user_can($user_id, 'compose_set_vote'), 'can_set_paid' => $this->forum_user_can($user_id, 'compose_set_paid'), 'can_set_hide' => $this->forum_user_can($user_id, 'compose_set_hide'), 'can_upload_image' => $this->forum_user_can($user_id, 'compose_upload_image'), 'can_upload_video' => $this->forum_user_can($user_id, 'compose_upload_video'), 'can_upload_file' => $this->forum_user_can($user_id, 'compose_upload_file'), 'can_iframe_video' => $this->forum_user_can($user_id, 'compose_iframe_video'), ]; } // ── Cache invalidation ──────────────────────────────────────── public function flush_cap_cache(int $user_id = 0): void { if ($user_id > 0) { foreach (array_keys(self::$cap_cache) as $k) { if (strncmp($k, "$user_id:", strlen("$user_id:")) === 0) { unset(self::$cap_cache[$k]); } } foreach (array_keys(self::$tier_cache) as $k) { if (strncmp($k, "$user_id:", strlen("$user_id:")) === 0) { unset(self::$tier_cache[$k]); } } } else { self::$cap_cache = []; self::$tier_cache = []; } } public function cap_cache_on_login(string $user_login, \WP_User $user): void { $this->flush_cap_cache((int) $user->ID); } /** Flush cache when plate moderator meta changes. */ public function cap_cache_on_plate_meta(int $meta_id, int $object_id, string $meta_key): void { if (in_array($meta_key, ['_aksrt_moderators', '_aksrt_moderator_roles'], true)) { self::$cap_cache = []; self::$tier_cache = []; } } /** Flush cache when plate_cat moderator term meta changes. */ public function cap_cache_on_cat_meta(int $meta_id, int $object_id, string $meta_key): void { if ($meta_key === 'moderator') { self::$cap_cache = []; self::$tier_cache = []; } } /** Flush cache when a plate CPT post is saved (plate_author may change). */ public function cap_cache_on_plate_save(int $post_id): void { foreach (array_keys(self::$tier_cache) as $k) { if (substr($k, -strlen(":$post_id")) === ":$post_id") { unset(self::$tier_cache[$k]); } } self::$cap_cache = []; } }