Files
wordpress-theme-aksrt/inc/classes/forum/class-aksrt-forum-permission-service.php
T

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 = [];
}
}