417 lines
20 KiB
PHP
417 lines
20 KiB
PHP
<?php
|
|
/**
|
|
* Forum permission/capability service.
|
|
*
|
|
* Extracted from AKSRT_Forum to keep the capability matrix,
|
|
* tier resolution, and cache invalidation in a focused unit.
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
class AKSRT_Forum_Permission_Service {
|
|
|
|
// ── Tier constants (mirrored from AKSRT_Forum) ───────────────
|
|
const TIER_NONE = 0;
|
|
const TIER_MODERATOR = 1;
|
|
const TIER_PLATE_AUTHOR = 2;
|
|
const TIER_CAT_MOD = 3;
|
|
const TIER_FORUM_ADMIN = 4;
|
|
|
|
/** @var array<string,bool> cap_key => bool */
|
|
private static array $cap_cache = [];
|
|
/** @var array<string,int> "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<string,bool>
|
|
*/
|
|
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 = [];
|
|
}
|
|
}
|