<?php
|
namespace JVBase\rest\routes;
|
|
use JVBase\rest\Rest;
|
use JVBase\managers\CustomTable;
|
use JVBase\rest\Route;
|
use WP_REST_Request;
|
use WP_REST_Response;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Invitations Route Manager
|
*
|
* Handles user invitations:
|
* - Global invitations (user to user, based on JVB_MEMBERSHIP['can_invite'])
|
* - Term invitations (to ownable content taxonomies with 'invitable' flag)
|
*/
|
class Invitations extends Rest
|
{
|
protected array $inviteConfig;
|
protected CustomTable $table;
|
|
public function __construct()
|
{
|
$this->cacheName = 'invitations';
|
parent::__construct();
|
|
// Get invitation configuration
|
$this->inviteConfig = JVB()->invitations()->getInviteConfig();
|
$this->table = CustomTable::for('invitations');
|
|
// Cache connections
|
$this->cache
|
->connect('user')
|
->connect('taxonomy');
|
}
|
|
public function registerRoutes(): void
|
{
|
Route::for('invitations')
|
->get([$this, 'getInvitations'])
|
->args([
|
'user' => 'int|required',
|
'to_term' => 'int',
|
'taxonomy' => 'string',
|
'status' => 'string|enum:all,pending,accepted,rejected,expired,revoked|default:all',
|
'page' => 'int|default:1|min:1'
|
])
|
->auth('user')
|
->rateLimit(20)
|
|
->post([$this, 'createInvitationRequest'])
|
->args([
|
'user' => 'int|required',
|
'action' => 'string|enum:create,revoke,refresh|default:create',
|
'invites' => 'array',
|
'invitation_id' => 'int'
|
])
|
->auth('verified')
|
->rateLimit(10, 300);
|
}
|
|
/**
|
* Get invitations for a user or term
|
*/
|
public function getInvitations(WP_REST_Request $request): WP_REST_Response
|
{
|
$userID = $request->get_param('user');
|
$termID = $request->get_param('to_term');
|
$taxonomy = $request->get_param('taxonomy');
|
|
// Validate user
|
if (get_current_user_id() !== $userID) {
|
return $this->unauthorized('Invalid user');
|
}
|
|
$args = [
|
'user' => $userID,
|
'to_term' => $termID,
|
'taxonomy' => $taxonomy ? jvbNoBase($taxonomy) : null,
|
'status' => $request->get_param('status'),
|
'page' => $request->get_param('page')
|
];
|
|
// Check cache
|
$key = $this->cache->generateKey($args);
|
if ($cached = $this->cache->get($key)) {
|
return $this->success($cached);
|
}
|
|
// Get appropriate invitations
|
$result = ($args['to_term'] && $args['taxonomy'])
|
? $this->getTermInvitations($args)
|
: $this->getUserInvitations($args);
|
|
// Cache result
|
$this->cache->set($key, $result);
|
|
return $this->success($result);
|
}
|
|
/**
|
* Get invitations for a specific term
|
*/
|
protected function getTermInvitations(array $args): array
|
{
|
// Check permission
|
if (!JVB()->roles()->isManager($args['user'], $args['to_term'])) {
|
return $this->forbidden('You cannot view invitations for this ' . $args['taxonomy'])->get_data();
|
}
|
|
$perPage = 20;
|
$offset = ($args['page'] - 1) * $perPage;
|
|
// Build query
|
$where = ['to_' . $args['taxonomy'] => $args['to_term']];
|
if ($args['status'] !== 'all') {
|
$where['status'] = $args['status'];
|
}
|
|
// Fluent CustomTable usage
|
$total = $this->table
|
->where($where)
|
->countResults();
|
|
$invitations = $this->table
|
->where($where)
|
->orderBy('created_at')
|
->limit($perPage, $offset)
|
->getResults();
|
|
return [
|
'invitations' => array_map([$this, 'formatInvitation'], $invitations),
|
'total' => $total,
|
'pages' => ceil($total / $perPage),
|
'page' => $args['page'],
|
'per_page' => $perPage
|
];
|
}
|
|
/**
|
* Get invitations sent by a user
|
*/
|
protected function getUserInvitations(array $args): array
|
{
|
$perPage = 20;
|
$offset = ($args['page'] - 1) * $perPage;
|
|
// Use raw query for JSON search
|
$query = "SELECT * FROM {$this->table->getFullTableName()}
|
WHERE JSON_SEARCH(inviters, 'one', %s, NULL, '$[*].user_id') IS NOT NULL";
|
$params = [$args['user']];
|
|
if ($args['status'] !== 'all') {
|
$query .= " AND status = %s";
|
$params[] = $args['status'];
|
}
|
|
$query .= " ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
$params = array_merge($params, [$perPage, $offset]);
|
|
$invitations = $this->table->queryResults($query, $params);
|
|
// Get count
|
$countQuery = str_replace('SELECT *', 'SELECT COUNT(*)',
|
substr($query, 0, strrpos($query, 'ORDER BY')));
|
$countParams = array_slice($params, 0, -2);
|
$total = (int) $this->table->queryVar($countQuery, $countParams);
|
|
return [
|
'invitations' => array_map([$this, 'formatInvitation'], $invitations),
|
'total' => $total,
|
'pages' => ceil($total / $perPage),
|
'page' => $args['page'],
|
'per_page' => $perPage
|
];
|
}
|
|
/**
|
* Create invitation request - queues invitations for processing
|
*/
|
public function createInvitationRequest(WP_REST_Request $request): WP_REST_Response
|
{
|
$userID = $request->get_param('user');
|
$action = $request->get_param('action');
|
|
// Validate user
|
if (get_current_user_id() !== $userID) {
|
return $this->unauthorized('Invalid user');
|
}
|
|
// Handle actions
|
return match($action) {
|
'revoke' => $this->revokeInvite($userID, $request->get_params()),
|
'refresh' => $this->resendInvite($userID, $request->get_params()),
|
default => $this->queueInvitations($userID, $request->get_param('invites'))
|
};
|
}
|
|
protected function queueInvitations(int $userID, array $invites): WP_REST_Response
|
{
|
if (empty($invites)) {
|
return $this->error('No invitations provided');
|
}
|
|
// Validate invitations
|
$validated = $this->validateInvitations($userID, $invites);
|
|
if (empty($validated)) {
|
return $this->error('No valid invitations to send');
|
}
|
|
// Queue using fluent interface
|
$op = JVB()->queue()->add(
|
'invitation_create',
|
$userID,
|
['invitations' => $validated],
|
[
|
'priority' => 'high',
|
'chunk_key' => 'invitations',
|
'chunk_size' => 20
|
]
|
);
|
return $this->queued($op['operation_id']);
|
}
|
|
/**
|
* Validate and sanitize invitation data
|
*/
|
protected function validateInvitations(int $userID, array $invites): array
|
{
|
return JVB()->invitations()->validateInvitations($userID, $invites);
|
}
|
|
|
|
|
/**
|
* Revoke an invitation
|
*/
|
protected function revokeInvite(int $userID, array $data): WP_REST_Response
|
{
|
$invitationID = (int) ($data['invitation_id'] ?? 0);
|
|
if (!$invitationID) {
|
return $this->error('Invitation ID required');
|
}
|
|
$op = JVB()->queue()->add(
|
'invitation_revoke',
|
$userID,
|
['invitation_id' => $invitationID],
|
['priority' => 'high']
|
);
|
|
return $this->queued($op['operation_id']);
|
}
|
|
/**
|
* Resend an invitation
|
*/
|
protected function resendInvite(int $userID, array $data): WP_REST_Response
|
{
|
$invitationID = (int) ($data['invitation_id'] ?? 0);
|
|
if (!$invitationID) {
|
return $this->error('Invitation ID required');
|
}
|
|
$op = JVB()->queue()->add(
|
'invitation_resend',
|
$userID,
|
['invitation_id' => $invitationID],
|
['priority' => 'high']
|
);
|
|
if (is_wp_error($op)) {
|
return $this->error($op->get_error_message());
|
}
|
return $this->queued($op['operation_id']);
|
}
|
|
|
|
/**
|
* Format invitation for API response
|
*/
|
protected function formatInvitation(object $invitation): array
|
{
|
$inviters = json_decode($invitation->inviters ?? '[]', true) ?: [];
|
|
$formatted = [
|
'id' => (int) $invitation->id,
|
'name' => $invitation->name,
|
'email' => $invitation->email,
|
'invited_role' => $invitation->invited_role,
|
'status' => $invitation->status,
|
'expires_at' => $invitation->expires_at,
|
'accepted_at' => $invitation->accepted_at ?? null,
|
'created_at' => $invitation->created_at,
|
'inviters' => array_map(fn($inviter) => [
|
'id' => (int) $inviter['user_id'],
|
'name' => jvbGetUsername($inviter['user_id']),
|
'invited_at' => $inviter['invited_at']
|
], $inviters)
|
];
|
|
// Add term information if present
|
foreach (JVB()->roles()->getInvitableTaxonomies() as $taxonomy) {
|
$column = 'to_' . $taxonomy;
|
if (isset($invitation->$column) && $invitation->$column) {
|
$termID = (int) $invitation->$column;
|
$term = get_term($termID, BASE . $taxonomy);
|
|
if ($term && !is_wp_error($term)) {
|
$formatted['term'] = [
|
'id' => $termID,
|
'name' => $term->name,
|
'taxonomy' => $taxonomy
|
];
|
break; // Only show first term
|
}
|
}
|
}
|
|
return $formatted;
|
}
|
|
}
|