From 94de71140be2d0c80bf6a2e03cb9381b37736ed5 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 06 Feb 2026 17:03:02 +0000
Subject: [PATCH] =Some minor CRUD.js and UploadManager.js tweaks
---
inc/rest/routes/ContentTermsRoutes.php | 551 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 550 insertions(+), 1 deletions(-)
diff --git a/inc/rest/routes/ContentTermsRoutes.php b/inc/rest/routes/ContentTermsRoutes.php
index 695f192..aa863e0 100644
--- a/inc/rest/routes/ContentTermsRoutes.php
+++ b/inc/rest/routes/ContentTermsRoutes.php
@@ -1,17 +1,566 @@
<?php
namespace JVBase\rest\routes;
+use JVBase\managers\queue\executors\ContentTermExecutor;
+use JVBase\managers\queue\TypeConfig;
use JVBase\rest\Rest;
+use JVBase\rest\Route;
+use JVBase\rest\PermissionHandler;
+use JVBase\utility\Features;
+use JVBase\managers\CustomTable;
+use WP_REST_Request;
+use WP_REST_Response;
+use Exception;
if (!defined('ABSPATH')) {
exit;
}
+/**
+ * Generic routes for managing content taxonomies (shops, studios, etc.)
+ *
+ * Responsibilities:
+ * - Update term settings/metadata
+ * - Manage members (add/remove via history table)
+ * - Manage ownership/management permissions
+ * - Process membership requests (if verify_entry enabled)
+ *
+ * Note: Invitations handled by InvitationRoutes
+ * Note: Approvals handled by ApprovalRoutes
+ */
class ContentTermsRoutes extends Rest
{
+ protected string $taxonomy;
+ protected array $config;
+ protected ?CustomTable $historyTable = null;
+ protected ?CustomTable $requestsTable = null;
+
+ public function __construct(string $taxonomy = '')
+ {
+ $this->taxonomy = jvbNoBase($taxonomy);
+
+ if ($taxonomy && isset(JVB_TAXONOMY[$this->taxonomy])) {
+ $this->config = JVB_TAXONOMY[$this->taxonomy];
+ $this->cacheName = $this->taxonomy;
+ parent::__construct();
+ $this->setupTables();
+ $this->setupCacheConnections();
+ }
+
+ add_action('init', [$this, 'registerContentTermsExecutors'], 5);
+ }
+
+ public function registerContentTermsExecutors():void
+ {
+ $registry = JVB()->queue()->registry();
+ $executor = new ContentTermExecutor();
+ $taxonomies = Features::getTypesWithFeature('is_content', 'taxonomy');
+
+ foreach($taxonomies as $taxonomy) {
+ $registry->register("{$taxonomy}_update", new TypeConfig(
+ executor: $executor,
+ ));
+
+ if (Features::forTaxonomy($taxonomy)->has('track_changes')) {
+ $registry->register("{$taxonomy}_member_add", new TypeConfig(
+ executor: $executor
+ ));
+
+ $registry->register("{$taxonomy}_member_remove", new TypeConfig(
+ executor: $executor
+ ));
+ }
+
+ }
+ }
+
+ protected function setupTables(): void
+ {
+ $content = $this->config['for_content'] ?? [];
+
+ if (Features::forTaxonomy($this->taxonomy)->has('track_changes') && !empty($content)) {
+ foreach ($content as $contentType) {
+ $tableName = "history_{$contentType}_{$this->taxonomy}";
+ $this->historyTable = CustomTable::for($tableName);
+ break; // Only need one table
+ }
+ }
+
+ if (Features::forTaxonomy($this->taxonomy)->has('verify_entry') && !empty($content)) {
+ foreach ($content as $contentType) {
+ $tableName = "{$contentType}_{$this->taxonomy}_requests";
+ $this->requestsTable = CustomTable::for($tableName);
+ break;
+ }
+ }
+ }
+
+ protected function setupCacheConnections(): void
+ {
+ $this->cache
+ ->connect('taxonomy')
+ ->connect('user');
+ }
public function registerRoutes(): void
{
- // TODO: Implement registerRoutes() method.
+ if (!Features::forTaxonomy($this->taxonomy)->has('is_content')) {
+ return;
+ }
+
+ $base = $this->taxonomy;
+
+ // Update term settings
+ Route::for("{$base}/:term_id/settings")
+ ->post([$this, 'updateSettings'])
+ ->args([
+ 'user' => 'int|required',
+ 'term_id' => 'int|required'
+ ])
+ ->auth(PermissionHandler::custom([$this, 'checkTermPermission']))
+ ->rateLimit(10);
+
+ // Member management (if track_changes enabled)
+ if (Features::forTaxonomy($this->taxonomy)->has('track_changes')) {
+ Route::for("{$base}/:term_id/members")
+ ->get([$this, 'getMembers'])
+ ->args([
+ 'term_id' => 'int|required',
+ 'status' => 'string|enum:active,inactive,all|default:active',
+ 'page' => 'int|default:1|min:1'
+ ])
+ ->auth('logged_in')
+ ->rateLimit(30)
+
+ ->post([$this, 'manageMember'])
+ ->args([
+ 'user' => 'int|required',
+ 'term_id' => 'int|required',
+ 'target_user' => 'int|required',
+ 'action' => 'string|enum:add,remove|required'
+ ])
+ ->auth(PermissionHandler::custom([$this, 'checkTermPermission']))
+ ->rateLimit(5);
+ }
+
+ // Membership requests (if verify_entry enabled)
+ if (Features::forTaxonomy($this->taxonomy)->has('verify_entry')) {
+ Route::for("{$base}/:term_id/requests")
+ ->get([$this, 'getRequests'])
+ ->args([
+ 'user' => 'int|required',
+ 'term_id' => 'int|required',
+ 'status' => 'string|enum:requested,accepted,rejected,all|default:requested',
+ 'page' => 'int|default:1|min:1'
+ ])
+ ->auth(PermissionHandler::custom([$this, 'checkTermPermission']))
+ ->rateLimit(20);
+
+ Route::for("{$base}/request")
+ ->post([$this, 'handleRequest'])
+ ->args([
+ 'user' => 'int|required',
+ 'term_id' => 'int|required',
+ 'action' => 'string|enum:create,accept,reject|required',
+ 'request_id' => 'int',
+ 'notes' => 'string'
+ ])
+ ->auth('verified')
+ ->rateLimit(5);
+ }
+
+ // Ownership/management (if is_ownable enabled)
+ if (Features::forTaxonomy($this->taxonomy)->has('is_ownable')) {
+ Route::for("{$base}/:term_id/permissions")
+ ->post([$this, 'updatePermissions'])
+ ->args([
+ 'user' => 'int|required',
+ 'term_id' => 'int|required',
+ 'target_user' => 'int|required',
+ 'role' => 'string|enum:owner,manager|required',
+ 'grant' => 'bool|required'
+ ])
+ ->auth(PermissionHandler::custom([$this, 'checkOwnerPermission']))
+ ->rateLimit(5);
+ }
+ }
+
+ /**
+ * Permission Callbacks
+ */
+ public function checkTermPermission(WP_REST_Request $request): bool
+ {
+ $userID = $request->get_param('user') ?? get_current_user_id();
+ $termID = (int)$request->get_param('term_id');
+
+ if (!$this->checkUser($userID) || !term_exists($termID, jvbCheckBase($this->taxonomy))) {
+ return false;
+ }
+
+ return user_can($userID, 'manage_options') ||
+ JVB()->roles()->isManager($userID, $termID);
+ }
+
+ public function checkOwnerPermission(WP_REST_Request $request): bool
+ {
+ $userID = $request->get_param('user') ?? get_current_user_id();
+ $termID = (int)$request->get_param('term_id');
+
+ if (!$this->checkUser($userID) || !term_exists($termID, jvbCheckBase($this->taxonomy))) {
+ return false;
+ }
+
+ return user_can($userID, 'manage_options') ||
+ JVB()->roles()->isOwner($userID, $termID);
+ }
+
+ /**
+ * Update term settings
+ */
+ public function updateSettings(WP_REST_Request $request): WP_REST_Response
+ {
+ $termID = (int)$request->get_param('term_id');
+ $userID = $request->get_param('user');
+
+ $data = $request->get_params();
+ unset($data['user'], $data['term_id']);
+
+ // Queue the update
+ $op = JVB()->queue()->add(
+ "{$this->taxonomy}_update",
+ $userID,
+ array_merge(['term_id' => $termID], $data),
+ [
+ 'priority' => 'high',
+ 'notification' => true
+ ]
+ );
+
+ return $this->queued($op['operation_id']);
+ }
+
+ /**
+ * Get term members
+ */
+ public function getMembers(WP_REST_Request $request): WP_REST_Response
+ {
+ $termID = (int)$request->get_param('term_id');
+ $status = $request->get_param('status');
+ $page = (int)$request->get_param('page');
+
+ $cacheKey = $this->cache->generateKey(compact('termID', 'status', 'page'));
+
+ return $this->cache->remember($cacheKey, function() use ($termID, $status, $page) {
+ $perPage = 20;
+ $offset = ($page - 1) * $perPage;
+
+ $query = $this->historyTable->where(['term_id' => $termID]);
+
+ if ($status === 'active') {
+ $query = $query->where(['end_date' => null]);
+ } elseif ($status === 'inactive') {
+ $query = $query->whereNotNull('end_date');
+ }
+
+ $total = $query->countResults();
+
+ $members = $query
+ ->orderBy('is_primary', 'DESC')
+ ->orderBy('created_at', 'ASC')
+ ->limit($perPage, $offset)
+ ->getResults(ARRAY_A);
+
+ // Enrich with user data
+ $members = array_map(function($member) {
+ $user = get_userdata($member['user_id']);
+ $member['display_name'] = $user ? $user->display_name : 'Unknown';
+ $member['user_email'] = $user ? $user->user_email : '';
+ return $member;
+ }, $members);
+
+ return $this->success([
+ 'members' => $members,
+ 'total' => $total,
+ 'pages' => ceil($total / $perPage),
+ 'page' => $page,
+ 'per_page' => $perPage
+ ]);
+ });
+ }
+
+ /**
+ * Manage member (add/remove)
+ */
+ public function manageMember(WP_REST_Request $request): WP_REST_Response
+ {
+ $action = $request->get_param('action');
+ $userID = $request->get_param('user');
+ $termID = (int)$request->get_param('term_id');
+ $targetUserID = (int)$request->get_param('target_user');
+
+ if (!$this->checkUser($targetUserID)) {
+ return $this->error('Invalid target user');
+ }
+
+ // Queue the operation
+ $op = JVB()->queue()->add(
+ "{$this->taxonomy}_member_{$action}",
+ $userID,
+ [
+ 'term_id' => $termID,
+ 'target_user' => $targetUserID
+ ],
+ ['priority' => 'high']
+ );
+
+ return $this->queued($op['operation_id']);
+ }
+
+ /**
+ * Get membership requests
+ */
+ public function getRequests(WP_REST_Request $request): WP_REST_Response
+ {
+ $termID = (int)$request->get_param('term_id');
+ $status = $request->get_param('status');
+ $page = (int)$request->get_param('page');
+
+ $cacheKey = $this->cache->generateKey(compact('termID', 'status', 'page'));
+
+ return $this->cache->remember($cacheKey, function() use ($termID, $status, $page) {
+ $perPage = 20;
+ $offset = ($page - 1) * $perPage;
+
+ $query = $this->requestsTable->where(['term_id' => $termID]);
+
+ if ($status !== 'all') {
+ $query = $query->where(['status' => $status]);
+ }
+
+ $total = $query->countResults();
+
+ $requests = $query
+ ->orderBy('created_date', 'DESC')
+ ->limit($perPage, $offset)
+ ->getResults(ARRAY_A);
+
+ return $this->success([
+ 'requests' => $requests,
+ 'total' => $total,
+ 'pages' => ceil($total / $perPage),
+ 'page' => $page,
+ 'per_page' => $perPage
+ ]);
+ });
+ }
+
+ /**
+ * Handle membership request (create/accept/reject)
+ */
+ public function handleRequest(WP_REST_Request $request): WP_REST_Response
+ {
+ $action = $request->get_param('action');
+ $userID = $request->get_param('user');
+ $termID = (int)$request->get_param('term_id');
+
+ return match($action) {
+ 'create' => $this->createRequest($userID, $termID),
+ 'accept', 'reject' => $this->processRequest($request),
+ default => $this->error('Invalid action')
+ };
+ }
+
+ protected function createRequest(int $userID, int $termID): WP_REST_Response
+ {
+ if (!term_exists($termID, jvbCheckBase($this->taxonomy))) {
+ return $this->error('Invalid ' . $this->taxonomy);
+ }
+
+ // Check if request already exists
+ $existing = $this->requestsTable
+ ->where([
+ 'user_id' => $userID,
+ 'term_id' => $termID
+ ])
+ ->first();
+
+ if ($existing) {
+ return $this->error('Request already exists');
+ }
+
+ $contentID = get_user_meta($userID, BASE . 'link', true);
+ if (!$contentID) {
+ return $this->error('User profile not found');
+ }
+
+ try {
+ $requestID = $this->requestsTable->create([
+ 'user_id' => $userID,
+ 'content_id' => $contentID,
+ 'term_id' => $termID,
+ 'status' => 'requested',
+ 'created_date' => current_time('mysql')
+ ]);
+
+ if ($requestID) {
+ $this->notifyTermManagers($termID, $userID, 'membership_request');
+ $this->cache->flush();
+
+ return $this->success([
+ 'message' => 'Request submitted successfully',
+ 'request_id' => $requestID
+ ]);
+ }
+
+ return $this->error('Failed to create request');
+ } catch (Exception $e) {
+ $this->logError('createRequest', [
+ 'error' => $e->getMessage(),
+ 'user_id' => $userID,
+ 'term_id' => $termID
+ ]);
+
+ return $this->error('An error occurred');
+ }
+ }
+
+ protected function processRequest(WP_REST_Request $request): WP_REST_Response
+ {
+ $action = $request->get_param('action');
+ $requestID = (int)$request->get_param('request_id');
+ $notes = $request->get_param('notes') ?? '';
+
+ if (!$requestID) {
+ return $this->error('Request ID required');
+ }
+
+ $requestData = $this->requestsTable
+ ->where(['id' => $requestID])
+ ->first(ARRAY_A);
+
+ if (!$requestData) {
+ return $this->error('Request not found');
+ }
+
+ $status = $action === 'accept' ? 'accepted' : 'rejected';
+
+ try {
+ $this->requestsTable->transaction(function($table) use ($requestID, $status, $notes, $requestData) {
+ // Update request
+ $table->where(['id' => $requestID])->updateResults([
+ 'status' => $status,
+ 'notes' => $notes,
+ 'updated_date' => current_time('mysql')
+ ]);
+
+ // If accepted, add member via action
+ if ($status === 'accepted') {
+ do_action(
+ BASE . 'add_user_to_term',
+ $requestData['user_id'],
+ $requestData['term_id'],
+ $this->taxonomy,
+ 'member'
+ );
+ }
+ });
+
+ // Notify user
+ JVB()->notification()->addNotification(
+ $requestData['user_id'],
+ 'request_' . $status,
+ [
+ 'term_id' => $requestData['term_id'],
+ 'taxonomy' => $this->taxonomy,
+ 'notes' => $notes
+ ]
+ );
+
+ $this->cache->flush();
+
+ return $this->success(['message' => 'Request ' . $status]);
+
+ } catch (Exception $e) {
+ $this->logError('processRequest', [
+ 'error' => $e->getMessage(),
+ 'request_id' => $requestID,
+ 'action' => $action
+ ]);
+
+ return $this->error('Failed to process request');
+ }
+ }
+
+ /**
+ * Update ownership/management permissions
+ */
+ public function updatePermissions(WP_REST_Request $request): WP_REST_Response
+ {
+ $termID = (int)$request->get_param('term_id');
+ $targetUserID = (int)$request->get_param('target_user');
+ $role = $request->get_param('role');
+ $grant = (bool)$request->get_param('grant');
+
+ if (!$this->checkUser($targetUserID)) {
+ return $this->error('Invalid target user');
+ }
+
+ $roleManager = JVB()->roles();
+
+ try {
+ $result = match($role) {
+ 'owner' => $grant
+ ? $roleManager->grantOwnership($targetUserID, $termID, $this->taxonomy)
+ : $roleManager->revokeOwnership($targetUserID, $termID, $this->taxonomy),
+ 'manager' => $grant
+ ? $roleManager->grantManagement($targetUserID, $termID, $this->taxonomy)
+ : $roleManager->revokeManagement($targetUserID, $termID, $this->taxonomy),
+ default => false
+ };
+
+ if ($result) {
+ $this->cache->flush();
+
+ return $this->success([
+ 'message' => ucfirst($role) . ' permissions ' . ($grant ? 'granted' : 'revoked')
+ ]);
+ }
+
+ return $this->error('Failed to update permissions');
+
+ } catch (Exception $e) {
+ $this->logError('updatePermissions', [
+ 'error' => $e->getMessage(),
+ 'term_id' => $termID,
+ 'target_user' => $targetUserID,
+ 'role' => $role
+ ]);
+
+ return $this->error('An error occurred');
+ }
+ }
+
+ /**
+ * Notify term managers
+ */
+ protected function notifyTermManagers(int $termID, int $actingUserID, string $notificationType): void
+ {
+ $roleManager = JVB()->roles();
+ $managers = $roleManager->getManagedTerms($actingUserID, $this->taxonomy);
+
+ $term = get_term($termID, jvbCheckBase($this->taxonomy));
+
+ foreach ($managers as $managerID) {
+ JVB()->notification()->addNotification(
+ $managerID,
+ $notificationType,
+ [
+ 'user_id' => $actingUserID,
+ 'term_id' => $termID,
+ 'term_name' => $term->name ?? 'Unknown',
+ 'taxonomy' => $this->taxonomy
+ ]
+ );
+ }
}
}
--
Gitblit v1.10.0