| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\JVB; |
| | | use JVBase\rest\RestRouteManager; |
| | | use Exception; |
| | | use JVBase\utility\Features; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\rest\Route; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use WP_Error; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | exit; |
| | | } |
| | | // TODO: Get this to work with the constants setup |
| | | /*** |
| | | * WORKFLOW: |
| | | * 1) Verified user (userA) invites user |
| | | * a) USER EXISTS -> notify user they're already up |
| | | * b) USER DOESN'T EXIST: |
| | | * i) check if user exists in invitation table |
| | | * ii) if they exist, add userA to inviters |
| | | * - if status is expired, resent email invite and set status to 'pending' |
| | | * iii) if they don't exist, add to table |
| | | * iv) once invited user registers: |
| | | * - set status to 'accepted', add new_user_id |
| | | * - set user as verified |
| | | * - if user was invited to a specific shop, pass user along to that shop |
| | | |
| | | /** |
| | | * 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 RestRouteManager |
| | | class Invitations extends Rest |
| | | { |
| | | protected string $tableName; |
| | | protected array $inviteTypes; |
| | | protected $wpdb; |
| | | protected string $prefix; |
| | | protected array $tableNames; |
| | | protected int $expiryDays = 14; // Invitations expire after 14 days |
| | | protected array $inviteConfig; |
| | | protected CustomTable $table; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache_name = 'invitations'; |
| | | parent::__construct(); |
| | | global $wpdb; |
| | | $this->inviteTypes = jvbInviteTableTypes(); |
| | | $this->tableNames = jvbInviteTables(); |
| | | $this->wpdb = $wpdb; |
| | | $this->prefix = $wpdb->prefix; |
| | | public function __construct() |
| | | { |
| | | $this->cacheName = 'invitations'; |
| | | parent::__construct(); |
| | | |
| | | // Add hooks for processing accepted invitations |
| | | add_action('user_register', [$this, 'checkInvitation'], 10, 1); |
| | | // Get invitation configuration |
| | | $this->inviteConfig = JVB()->invitations()->getInviteConfig(); |
| | | $this->table = CustomTable::for('invitations'); |
| | | |
| | | add_action('jvbLoginManagerInit', function($loginManager) { |
| | | $loginManager->registerTokenHandler('invite', function($token, $email, $user_id) { |
| | | JVB()->routes('invites')->acceptInvitation($token, $email, $user_id); |
| | | }); |
| | | |
| | | $loginManager->registerMessageHandler('invitation', |
| | | function() { |
| | | return '<h2>You\'ve been invited!</h2><p>Create your account to accept.</p>'; |
| | | }, |
| | | function() { |
| | | return isset($_GET['invite']); |
| | | } |
| | | ); |
| | | }); |
| | | |
| | | |
| | | |
| | | add_action('jvb_daily_maintenance', [$this, 'cleanupExpiredInvitations']); |
| | | |
| | | // Add filter for bulk operation handling |
| | | add_filter(BASE . 'handle_bulk_operation', [ $this, 'processOperation' ], 10, 3); |
| | | } |
| | | |
| | | /** |
| | | * Registers the routes for invitations |
| | | * @return void |
| | | */ |
| | | public function registerRoutes():void |
| | | { |
| | | register_rest_route($this->namespace, '/invitations', [ |
| | | [ |
| | | 'methods' => 'GET', |
| | | 'callback' => [$this, 'getInvitations'], |
| | | 'permission_callback' => [$this, 'checkPermission'] |
| | | ], |
| | | [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'createInvitationRequest'], |
| | | 'permission_callback' => [$this, 'checkPermission'] |
| | | ] |
| | | ]); |
| | | } |
| | | |
| | | protected function buildParams(object $request):array |
| | | { |
| | | $data = $request->get_params(); |
| | | $role = (array_key_exists('role', $data) && array_key_exists($data['role'], $this->tableNames)) ? $data['role'] : false; |
| | | $toTerm = (array_key_exists('to_term', $data)) ? (int)$data['to_term'] : false; |
| | | $taxonomy = (array_key_exists('taxonomy', $data) && in_array($data['taxonomy'], $this->inviteTypes[$role]['to_terms']??[])) ? $data['taxonomy'] : false; |
| | | |
| | | $args = [ |
| | | 'user' => (array_key_exists('user', $data)) ? (int)$data['user'] : false, |
| | | 'role' => $role, |
| | | 'to_term' => $toTerm, |
| | | 'taxonomy' => $taxonomy, |
| | | 'status' => array_key_exists('status', $data) && in_array($data['status'], ['all', 'pending', 'accepted', 'rejected', 'expired', 'revoked']) ? $data['status'] : 'all', |
| | | 'page' => array_key_exists('page', $data) ? (int)$data['page'] : 1, |
| | | ]; |
| | | |
| | | return $args; |
| | | } |
| | | /** |
| | | * @param object $request the request object |
| | | * |
| | | * @return WP_REST_Response |
| | | */ |
| | | public function getInvitations(object $request): WP_REST_Response |
| | | { |
| | | $args = $this->buildParams($request); |
| | | if ($args['user']) { |
| | | if (!$this->userCheck($args['user'])) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Looks like you are not who you say you are' |
| | | ]); |
| | | } |
| | | if (!$this->isVerifiedUser($args['user'])) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Sorry, you don\'t have permission to do this.', |
| | | ]); |
| | | } |
| | | return $this->getUserInvitations($args); |
| | | } elseif ($args['to_term']) { |
| | | if (!$this->checkTerm($args)) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Looks like this '.$args['taxonomy'].' does not exist' |
| | | ]); |
| | | } |
| | | return $this->getTermInvitations($args); |
| | | } |
| | | |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Invalid request' |
| | | ]); |
| | | } |
| | | |
| | | public function getTermInvitations(array $args):WP_REST_Response |
| | | { |
| | | if (!$this->checkTerm($args)) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Invalid shop' |
| | | ]); |
| | | } |
| | | |
| | | if (!user_can($args['user'], 'manage_'.$args['taxonomy'].'_'.$args['to_term'])) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'You do not have permission to view invitations for this '.$args['taxonomy'] |
| | | ]); |
| | | } |
| | | |
| | | $key = $this->cache->generateKey($args); |
| | | |
| | | $cache = $this->cache->get($key); |
| | | if ($cache) { |
| | | return new WP_REST_Response($cache); |
| | | } |
| | | |
| | | $per_page = 20; |
| | | |
| | | $conditions = []; |
| | | $params = []; |
| | | |
| | | //Filter by term |
| | | $conditions[] = "to_{$args['taxonomy']} = %d"; |
| | | $params[] = $args['to_term']; |
| | | |
| | | if ($args['status'] !== 'all') { |
| | | $conditions[] = "status = %s"; |
| | | $params[] = $args['status']; |
| | | } |
| | | |
| | | $where = !empty($conditions) ? " WHERE " .implode(' AND ', $conditions) : ""; |
| | | |
| | | //Count total for pagination |
| | | $count_query = "SELECT COUNT(*) FROM {$this->tableNames[$args['role']]} {$where}"; |
| | | $total = $this->wpdb->get_var($this->wpdb->prepare($count_query, $params)); |
| | | |
| | | //Get paginated invitations |
| | | $offset = ($args['page'] - 1) * $per_page; |
| | | $query = $count_query." ORDER BY created_at DESC LIMIT %d OFFSET %d"; |
| | | |
| | | //Add pagination |
| | | $pagination = array_merge($params, [$per_page, $offset]); |
| | | $invitations = $this->wpdb->get_results($this->wpdb->prepare($query, $pagination)); |
| | | |
| | | $formatted = []; |
| | | foreach ($invitations as $invitation) { |
| | | $formatted[] = $this->formatInvitation($invitation); |
| | | } |
| | | |
| | | $return = [ |
| | | 'invitations' => $formatted, |
| | | 'total' => (int)$total, |
| | | 'pages' => ceil($total /$per_page), |
| | | 'page' => $args['page'], |
| | | 'per_page' => $per_page |
| | | ]; |
| | | |
| | | $this->cache->set($key, $return); |
| | | return new WP_REST_Response($return); |
| | | } |
| | | |
| | | protected function buildInvitationArgs(object $request):array |
| | | { |
| | | $data = $request->get_params(); |
| | | |
| | | $user = (array_key_exists('user', $data) && $this->userCheck($data['user'])) ? (int) $data['user'] : false; |
| | | if (!$user) { |
| | | return []; |
| | | } |
| | | $role = jvbUserRole($user); |
| | | $args = [ |
| | | 'user' => $user, |
| | | 'role' => $role, |
| | | 'action' => (array_key_exists('action', $data) && in_array($data['action'], ['refresh', 'revoke', 'create'])) ? $data['action'] : false, |
| | | 'inviteID' => (array_key_exists('refresh', $data)) ? (int) $data['refresh'] : false, |
| | | ]; |
| | | |
| | | $allowed = $this->inviteTypes[$role]; |
| | | if (count($allowed) > 1) { |
| | | $inviteAs = (array_key_exists('type', $data) && in_array($data['type'], $allowed)) ? $data['type'] : false; |
| | | } else { |
| | | $invitedAs = $allowed[0]; |
| | | } |
| | | |
| | | if (array_key_exists('invites', $data)) { |
| | | $invites = []; |
| | | foreach ($data['invites'] as $invite) { |
| | | $temp = [ |
| | | 'invited_id' => (array_key_exists('invited_id', $invite) && $this->userCheck($invite['invited_id'])) ? $invite['invited_id'] : false, |
| | | 'to_term' => (array_key_exists('to_term', $invite)) ? (int) $invite['to_term'] : false, |
| | | 'taxonomy' => (array_key_exists('taxonomy', $invite) && in_array($invite['taxonomy'], $this->inviteTypes[$role]['to_terms']??[])) ? $invite['taxonomy'] : false, |
| | | 'invited_name' => (array_key_exists('name', $invite) && is_string($invite['name'])) ? sanitize_text_field($invite['name']) : false, |
| | | 'invited_email' => (array_key_exists('email', $invite) && is_email($invite['email'])) ? sanitize_email($invite['email']) : false, |
| | | ]; |
| | | if ($temp['invited_id'] || ($temp['invited_email'] && $temp['invited_name'])) { |
| | | $invites[$invitedAs][] = $data; |
| | | } |
| | | } |
| | | $args['invites'] = $invites; |
| | | } |
| | | if (!$invitedAs && !empty($args['invites'])) { |
| | | unset($args['invites']); |
| | | } |
| | | |
| | | return $args; |
| | | } |
| | | /** |
| | | * @param object $request The Request Object |
| | | * |
| | | * @return WP_REST_Response |
| | | */ |
| | | public function createInvitationRequest(object $request):WP_REST_Response |
| | | { |
| | | $args = $this->buildInvitationArgs($request); |
| | | |
| | | $error = ''; |
| | | if (!$args['user']) { |
| | | $error = 'User ID doesn\'t match up.... are you a bot?'; |
| | | } elseif (Features::forMembership()->has('member_verified') && !user_can($args['user'], 'skip_moderation')) { |
| | | $error = 'Only verified users can send invitations.'; |
| | | } elseif (!$args['role']) { |
| | | $error = 'It doesn\'t look like you can invite users.'; |
| | | } |
| | | if ($error !== '') { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => $error |
| | | ]); |
| | | } |
| | | |
| | | switch ($args['action']) { |
| | | case 'revoke': |
| | | return $this->revokeInvite($args); |
| | | case 'refresh': |
| | | return $this->resendInvite($args); |
| | | } |
| | | |
| | | //Inviting to content taxonomy (ie: shop) |
| | | $artist = jvbContentFromUser($args['user']); |
| | | foreach ($args['invites'] as $index => $invite) { |
| | | if ($invite['to_term'] && $invite['taxonomy']) { |
| | | if (!$artist[$invite['taxonomy']] || $artist[$invite['taxonomy']['id'] !== $invite['term_id']]) { |
| | | $args['invites'][$index]['to_term'] = false; |
| | | $args['invites'][$index]['taxonomy'] = false; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (!empty($args['invites']??[])) { |
| | | JVB()->queue()->queueOperation( |
| | | 'invitation_create', |
| | | $args['user'], |
| | | [ |
| | | 'invitations' => $args['invites'], |
| | | ], |
| | | [ |
| | | 'count' => count($args['invites']), |
| | | 'priority' => 'high', |
| | | 'chunk_size' => 20, |
| | | 'chunk_key' => 'invitations' |
| | | ] |
| | | ); |
| | | |
| | | return new WP_REST_Response([ |
| | | 'success' => true, |
| | | 'message' => 'Processing ' . count($args['invites']) . ' invitations', |
| | | ]); |
| | | } |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'No invitations sent.' |
| | | ]); |
| | | } |
| | | |
| | | /** |
| | | * Revoke an invitation |
| | | * |
| | | * @params array $args |
| | | * @return array Response with success or error message |
| | | */ |
| | | public function revokeInvite(array $args): array |
| | | { |
| | | $invitation = $this->getInvitationByUser($args); |
| | | |
| | | if (!$invitation || is_wp_error($invitation)) { |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => 'Invitation not found' |
| | | ]; |
| | | } |
| | | |
| | | // Check if invitation can be revoked (only pending invitations) |
| | | if ($invitation['status'] !== 'pending' && $invitation['status'] !== 'expired') { |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => 'Only pending or expired invitations can be revoked' |
| | | ]; |
| | | } |
| | | |
| | | // Check if the user is one of the inviters |
| | | $inviters = json_decode($invitation['inviters'], true); |
| | | $user_is_inviter = false; |
| | | $updated_inviters = []; |
| | | |
| | | foreach ($inviters as $inviter) { |
| | | if (intval($inviter['user_id']) === $args['user']) { |
| | | $user_is_inviter = true; |
| | | } else { |
| | | // Keep other inviters |
| | | $updated_inviters[] = $inviter; |
| | | } |
| | | } |
| | | |
| | | if (!$user_is_inviter) { |
| | | return [ |
| | | 'success' => false, |
| | | 'return' => 'You are not authorized to revoke this invitation' |
| | | ]; |
| | | } |
| | | |
| | | // If there are still other inviters, just update the inviters list |
| | | if (!empty($updated_inviters)) { |
| | | $this->wpdb->update( |
| | | $this->tableNames[$args['role']], |
| | | [ |
| | | 'inviters' => json_encode($updated_inviters), |
| | | 'updated_at' => current_time('mysql') |
| | | ], |
| | | ['id' => $invitation['id']] |
| | | ); |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => 'You have been removed from the inviters list but the invitation is still active with other inviters', |
| | | ]; |
| | | } |
| | | |
| | | // If no inviters left, mark the invitation as revoked |
| | | $this->wpdb->update( |
| | | $this->tableNames[$args['role']], |
| | | [ |
| | | 'status' => 'revoked', |
| | | 'updated_at' => current_time('mysql') |
| | | ], |
| | | ['id' => $invitation['id'] ] |
| | | ); |
| | | |
| | | $this->sendRevocationEmail($invitation['email'], $invitation['name']); |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => 'Invitation has been successfully revoked' |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Resend an expired invitation |
| | | * |
| | | * @param array $args Args, as defined in buildInvitationArgs()) |
| | | * @return WP_REST_Response Response with success or error message |
| | | */ |
| | | public function resendInvite(array $args): WP_REST_Response |
| | | { |
| | | $invitation_id = isset($args['inviteID']) ? intval($args['inviteID']) : 0; |
| | | $user_id = isset($args['user']) ? intval($args['user']) : 0; |
| | | |
| | | if (!$invitation_id || !$user_id) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Missing invitation ID or user ID' |
| | | ]); |
| | | } |
| | | |
| | | // Get the invitation |
| | | $invitation = $this->wpdb->get_row($this->wpdb->prepare( |
| | | "SELECT * FROM {$this->tableNames[$args['role']]} WHERE id = %d", |
| | | $invitation_id |
| | | ), ARRAY_A); |
| | | |
| | | if (!$invitation) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Invitation not found' |
| | | ]); |
| | | } |
| | | |
| | | // Check if the invitation is expired or pending |
| | | if (!in_array($invitation['status'], ['expired', 'pending'])) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Only expired or pending invitations can be resent' |
| | | ]); |
| | | } |
| | | |
| | | // Check if the user is one of the inviters |
| | | $inviters = json_decode($invitation['inviters'], true); |
| | | $user_is_inviter = false; |
| | | |
| | | foreach ($inviters as &$inviter) { |
| | | if (intval($inviter['user_id']) === $user_id) { |
| | | $user_is_inviter = true; |
| | | // Update the invited_at timestamp for this inviter |
| | | $inviter['invited_at'] = current_time('mysql'); |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if (!$user_is_inviter) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'You are not authorized to resend this invitation' |
| | | ]); |
| | | } |
| | | |
| | | // Generate a new token |
| | | $token = wp_generate_password(32, false); |
| | | |
| | | // Set new expiration date |
| | | $expires_at = date('Y-m-d H:i:s', strtotime("+{$this->expiryDays} days")); |
| | | |
| | | // Update the invitation |
| | | $this->wpdb->update( |
| | | $this->tableNames[$args['role']], |
| | | [ |
| | | 'invitation_token' => $token, |
| | | 'status' => 'pending', |
| | | 'expires_at' => $expires_at, |
| | | 'inviters' => json_encode($inviters), |
| | | 'updated_at' => current_time('mysql') |
| | | ], |
| | | ['id' => $invitation_id] |
| | | ); |
| | | |
| | | // Send the invitation email again |
| | | $name = $invitation['name']; |
| | | $email = $invitation['email']; |
| | | $role = $invitation['role']; |
| | | $terms = $this->getInvitationTerms($invitation, $role); |
| | | |
| | | |
| | | $result = $this->sendInvitationEmail($name, $email, $token, $user_id, $terms, $role); |
| | | |
| | | if (!$result) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Failed to send invitation email' |
| | | ]); |
| | | } |
| | | |
| | | return new WP_REST_Response([ |
| | | 'success' => true, |
| | | 'message' => 'Invitation has been successfully resent', |
| | | 'expires_at' => $expires_at |
| | | ]); |
| | | } |
| | | |
| | | protected function getInvitationTerms(object|array $invitation, string $role) { |
| | | if (is_object($invitation)) { |
| | | $invitation = json_decode(json_encode($invitation), true); |
| | | } |
| | | $terms = []; |
| | | foreach ($this->inviteTypes[$role]['to_terms'] as $taxonomy) { |
| | | $terms[$taxonomy] = $invitation['to_'.$taxonomy]; |
| | | } |
| | | return $terms; |
| | | // Cache connections |
| | | $this->cache |
| | | ->connect('user') |
| | | ->connect('taxonomy'); |
| | | } |
| | | |
| | | /** |
| | | * Create or update an invitation |
| | | * @param string $name Name of person being invited |
| | | * @param string $email Email of person being invited |
| | | * @param int $inviter_id User ID of the person inviting |
| | | * @param string|false $role |
| | | * @param int|false $termID Optional shop ID |
| | | * @param string|false $taxonomy Optional taxonomy |
| | | * @param bool $send_email whether to send email right away |
| | | * @return WP_Error|array |
| | | * |
| | | */ |
| | | public function createInvitation( |
| | | string $name, |
| | | string $email, |
| | | int $inviter_id, |
| | | string|false $role = false, |
| | | int|false $termID = false, |
| | | string|false $taxonomy = false, |
| | | bool $send_email = true |
| | | ):WP_Error|array { |
| | | error_log('Creating Invitation with data: '.print_r([ |
| | | 'name' => $name, |
| | | 'email' => $email, |
| | | 'inviter ID'=> $inviter_id, |
| | | 'termID' => $termID, |
| | | 'taxonomy' => $taxonomy, |
| | | 'role' => $role |
| | | ], true)); |
| | | // Sanitize and validate email |
| | | $email = sanitize_email($email); |
| | | if (!is_email($email)) { |
| | | error_log('Invalid email'); |
| | | return new WP_Error('invalid_email', 'Invalid email address'); |
| | | } |
| | | 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) |
| | | |
| | | // Check if inviter is verified |
| | | if (Features::forMembership()->has('member_verified') && !$this->isVerifiedUser($inviter_id)) { |
| | | error_log('Unverified Artist'); |
| | | return new WP_Error('unauthorized', 'Only verified artists can send invitations'); |
| | | } |
| | | ->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) |
| | | ->register(); |
| | | } |
| | | |
| | | if ($termID) { |
| | | // Check if shop exists if specified |
| | | if ($this->checkTerm(['term_id' => $termID, 'taxonomy' => $taxonomy])) { |
| | | error_log('Invalid Taxonomy'); |
| | | return new WP_Error('invalid_term', 'The specified term does not exist'); |
| | | } |
| | | } |
| | | /** |
| | | * 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'); |
| | | |
| | | if (!$role || !array_key_exists($role, $this->inviteTypes)) { |
| | | return new WP_Error('invalid_role', 'No role was set'); |
| | | } |
| | | |
| | | // Check if user already exists |
| | | $invite = !email_exists($email); |
| | | |
| | | // Get existing invitation if any |
| | | $existing = $this->wpdb->get_row($this->wpdb->prepare( |
| | | "SELECT * FROM {$this->tableNames[$role]} WHERE email = %s", |
| | | $email |
| | | ), ARRAY_A); |
| | | |
| | | // Generate token |
| | | $token = wp_generate_password(32, false); |
| | | |
| | | // Set expiration date |
| | | $expires_at = date('Y-m-d H:i:s', strtotime("+{$this->expiryDays} days")); |
| | | |
| | | if ($existing) { |
| | | // Update existing invitation |
| | | $inviters = json_decode($existing['inviters'], true); |
| | | |
| | | // Check if this inviter already invited |
| | | $inviter_exists = false; |
| | | foreach ($inviters as $inviter) { |
| | | if ($inviter['user_id'] == $inviter_id) { |
| | | $inviter_exists = true; |
| | | // Update the invited_at timestamp |
| | | $inviter['invited_at'] = current_time('mysql'); |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if (!$inviter_exists) { |
| | | // Add new inviter |
| | | $inviters[] = [ |
| | | 'user_id' => $inviter_id, |
| | | 'invited_at' => current_time('mysql') |
| | | ]; |
| | | } |
| | | |
| | | // Prepare update data |
| | | $update_data = [ |
| | | 'inviters' => json_encode($inviters), |
| | | 'status' => 'pending', |
| | | 'expires_at' => $expires_at, |
| | | 'updated_at' => current_time('mysql'), |
| | | ]; |
| | | // Set shop_id if provided and not already set |
| | | $check = 'to_'.$taxonomy; |
| | | if ($termID && $existing[$check] !== $termID) { |
| | | $update_data[$check] = $termID; |
| | | } |
| | | |
| | | // If invitation was expired, generate new token |
| | | if ($existing['status'] === 'expired') { |
| | | $update_data['invitation_token'] = $token; |
| | | } else { |
| | | $token = $existing['invitation_token']; |
| | | } |
| | | |
| | | $this->wpdb->update( |
| | | $this->tableNames[$role], |
| | | $update_data, |
| | | ['id' => $existing['id']] |
| | | ); |
| | | |
| | | $invitation_id = $existing['id']; |
| | | } else { |
| | | // Create new invitation |
| | | $inviters = [[ |
| | | 'user_id' => $inviter_id, |
| | | 'invited_at' => current_time('mysql') |
| | | ]]; |
| | | |
| | | $insert_data = [ |
| | | 'name' => sanitize_text_field($name), |
| | | 'email' => $email, |
| | | 'invitation_token' => $token, |
| | | 'status' => 'pending', |
| | | 'inviters' => json_encode($inviters), |
| | | 'expires_at' => $expires_at, |
| | | 'created_at' => current_time('mysql') |
| | | ]; |
| | | // Add shop if provided |
| | | if ($termID) { |
| | | $insert_data['to_'.$taxonomy] = $termID; |
| | | } |
| | | |
| | | $this->wpdb->insert( |
| | | $this->tableNames[$role], |
| | | $insert_data |
| | | ); |
| | | |
| | | $invitation_id = $this->wpdb->insert_id; |
| | | } |
| | | |
| | | error_log('On to invitation email send:'); |
| | | // Send invitation email |
| | | if ($invite && $send_email) { |
| | | $this->sendInvitationEmail($name, $email, $token, $inviter_id, [$taxonomy => $termID], $role); |
| | | } |
| | | |
| | | return [ |
| | | 'id' => $invitation_id, |
| | | 'email' => $email, |
| | | 'token' => $token, |
| | | 'expires_at' => $expires_at |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Validate an invitation token |
| | | * @param string $token the generated token |
| | | * @param string $email the email of the invited person |
| | | * @param string $role the role |
| | | * @return object $invitation or error |
| | | */ |
| | | public function validateInvitation(string $token, string $email, string $role):object |
| | | { |
| | | if (!array_key_exists($role, $this->inviteTypes)) { |
| | | return new WP_Error('invalid_role', 'Invalid role type'); |
| | | // Validate user |
| | | if (get_current_user_id() !== $userID) { |
| | | return $this->unauthorized('Invalid user'); |
| | | } |
| | | $table = $this->wpdb->prefix . $this->tableNames[$role]; |
| | | |
| | | // Get invitation by token and email |
| | | $invitation = $this->wpdb->get_row($this->wpdb->prepare( |
| | | "SELECT * FROM $table |
| | | WHERE invitation_token = %s |
| | | AND email = %s |
| | | AND status = 'pending'", |
| | | $token, |
| | | $email |
| | | )); |
| | | $args = [ |
| | | 'user' => $userID, |
| | | 'to_term' => $termID, |
| | | 'taxonomy' => $taxonomy ? jvbNoBase($taxonomy) : null, |
| | | 'status' => $request->get_param('status'), |
| | | 'page' => $request->get_param('page') |
| | | ]; |
| | | |
| | | if (!$invitation) { |
| | | return new WP_Error('invalid_invitation', 'Invalid invitation token or email'); |
| | | } |
| | | // Check cache |
| | | $key = $this->cache->generateKey($args); |
| | | if ($cached = $this->cache->get($key)) { |
| | | return $this->success($cached); |
| | | } |
| | | |
| | | // Check if expired |
| | | if (strtotime($invitation->expires_at) < time()) { |
| | | return new WP_Error('expired_invitation', 'This invitation has expired'); |
| | | } |
| | | // Get appropriate invitations |
| | | $result = ($args['to_term'] && $args['taxonomy']) |
| | | ? $this->getTermInvitations($args) |
| | | : $this->getUserInvitations($args); |
| | | |
| | | return $invitation; |
| | | } |
| | | // Cache result |
| | | $this->cache->set($key, $result); |
| | | |
| | | /** |
| | | * Send invitation email to the new artist |
| | | * @param string $name The invited person's name |
| | | * @param string $email The invited person's email |
| | | * @param string $token The randomly generated password |
| | | * @param int $inviter_id The User ID of the one inviting |
| | | * @param int|null $shopID The optional shop ID to be invited to |
| | | * @return bool Whether or not the invitation was sent successfully |
| | | */ |
| | | protected function sendInvitationEmail(string $name, string $email, string $token, int $inviter_id, array $terms, string|null $role = null):bool |
| | | { |
| | | $inviter = get_userdata($inviter_id); |
| | | $inviter_name = jvbGetUsername($inviter_id); |
| | | $inviter_name = $inviter_name ?: $inviter->display_name; |
| | | return $this->success($result); |
| | | } |
| | | |
| | | $siteName = get_bloginfo('name'); |
| | | /** |
| | | * 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(); |
| | | } |
| | | |
| | | $subject = apply_filters('jvbInvitationSubject', |
| | | sprintf( |
| | | "%s invited you to join %s!", |
| | | $inviter_name, |
| | | $siteName |
| | | ), |
| | | $inviter_name |
| | | $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'] |
| | | ); |
| | | |
| | | $signup_url = add_query_arg([ |
| | | 'invite' => $token, |
| | | 'email' => urlencode($email), |
| | | 'name' => $name, |
| | | 'role' => $role |
| | | ], wp_registration_url()); |
| | | 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']); |
| | | } |
| | | |
| | | |
| | | // Get shop name if applicable |
| | | $toContentTax = []; |
| | | if (!empty ($terms)) { |
| | | foreach ($terms as $taxonomy => $termID) { |
| | | |
| | | /** |
| | | * 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)) { |
| | | $toContentTax[] = sprintf( |
| | | "<p>%s has also invited you to join %s. You'll be automatically added to this %s when you register.</p>", |
| | | $inviter_name, |
| | | $term->name, |
| | | $taxonomy |
| | | ); |
| | | $formatted['term'] = [ |
| | | 'id' => $termID, |
| | | 'name' => $term->name, |
| | | 'taxonomy' => $taxonomy |
| | | ]; |
| | | break; // Only show first term |
| | | } |
| | | } |
| | | } |
| | | $toContentTax = implode(' ', $toContentTax); |
| | | |
| | | $button = jvbMailButton($signup_url, 'Join the Scene!'); |
| | | $link = jvbEmailLink($signup_url); |
| | | $signature = jvbSignature(); |
| | | |
| | | $message = sprintf( |
| | | '<p>Hi %s!</p> |
| | | <p>%s has invited you to join them on %s.</p> |
| | | |
| | | <h2>Interested?</h2> |
| | | <p>Join in by clicking the button below:</p> |
| | | %s |
| | | <p>Or by copying and pasting the link below into your browser:</p> |
| | | %s |
| | | <div class="divider"></div> |
| | | %s |
| | | <p>This invitation expires in %d days.</p> |
| | | <p>Ink on, %s</p> |
| | | %s |
| | | ', |
| | | $name, |
| | | $inviter_name, |
| | | $siteName, |
| | | $button, |
| | | $link, |
| | | $name, |
| | | $toContentTax, |
| | | $this->expiryDays, |
| | | $signature |
| | | ); |
| | | $message = apply_filters('jvbInvitationMessage', |
| | | $message, |
| | | $name, |
| | | $inviter_name, |
| | | $role, |
| | | $termID, |
| | | $taxonomy, |
| | | $toContentTax, |
| | | $this->expiryDays, |
| | | $button, |
| | | $link, |
| | | $signature, |
| | | ); |
| | | |
| | | |
| | | $success = jvbMail($email, $subject, $message); |
| | | |
| | | |
| | | if (!$success) { |
| | | // Log the invitation |
| | | JVB()->error()->log( |
| | | 'invitation_email', |
| | | 'Invitation not sent', |
| | | [ |
| | | 'email' => $email, |
| | | 'inviter_id' => $inviter_id, |
| | | 'token' => $token |
| | | ], |
| | | 'info' |
| | | ); |
| | | } |
| | | |
| | | return $success; |
| | | } |
| | | |
| | | /** |
| | | * Send revocation email notification |
| | | * @param string $email the invited person's email |
| | | * @param string $name the invited person's name |
| | | * @return bool Whether or not the email was sent |
| | | */ |
| | | protected function sendRevocationEmail(string $email, string $name):bool |
| | | { |
| | | $siteName = get_bloginfo('name'); |
| | | $subject = apply_filters( |
| | | 'jvbInvitationRevokedSubject', |
| | | sprintf( |
| | | '[%s] Your invitation has been revoked', |
| | | $siteName |
| | | ) |
| | | ); |
| | | $content = apply_filters( |
| | | 'jvbInvitationRevokedMessage', |
| | | sprintf( |
| | | '<p>Hey %s,</p> |
| | | <p>This is to let you know that your invitation to join %s has been revoked.</p> |
| | | <p>If you believe this was done in error, please contact the person who invited you, the site admin, or try registering yourself.</p>', |
| | | $name, |
| | | $siteName |
| | | ), |
| | | $name |
| | | ); |
| | | |
| | | $success = jvbMail($email, $subject, $content, 'INVITATION REVOKED'); |
| | | if (!$success) { |
| | | JVB()->error()->log( |
| | | 'invitation_revoke_email', |
| | | 'Invitation not sent', |
| | | [ |
| | | 'email' => $email, |
| | | 'name' => $name, |
| | | ], |
| | | 'info' |
| | | ); |
| | | } |
| | | return $success; |
| | | } |
| | | |
| | | /** |
| | | * Verify an invitation token |
| | | * @param string $token The randomly generated token |
| | | * @param string $email The invited person's email |
| | | * @return bool|object False on failure. Invitation object if success |
| | | */ |
| | | public function verifyInvitation(string $token, string $email, string $role):bool|object |
| | | { |
| | | $invitation = $this->wpdb->get_row($this->wpdb->prepare( |
| | | "SELECT * FROM {$this->tableNames[$role]} |
| | | WHERE invitation_token = %s AND email = %s AND status = 'pending' AND expires_at > NOW()", |
| | | $token, |
| | | $email |
| | | )); |
| | | |
| | | if (!$invitation) { |
| | | return false; |
| | | } |
| | | |
| | | return $invitation; |
| | | } |
| | | |
| | | /** |
| | | * Mark an invitation as accepted |
| | | * @param string $token The randomly generated token |
| | | * @param string $email The invited person's email |
| | | * @param int $user_id The invited person's user ID |
| | | * @return bool whether or not it was successfully accepted |
| | | */ |
| | | public function acceptInvitation(string $token, string $email, int $user_id):bool |
| | | { |
| | | $role = jvbUserRole($user_id); |
| | | $invitation = $this->verifyInvitation($token, $email, $role); |
| | | |
| | | if (!$invitation) { |
| | | return false; |
| | | } |
| | | |
| | | // Update invitation status |
| | | $this->wpdb->update( |
| | | $this->tableNames[$role], |
| | | [ |
| | | 'status' => 'accepted', |
| | | 'new_user_id' => $user_id, |
| | | 'accepted_at' => current_time('mysql'), |
| | | 'updated_at' => current_time('mysql') |
| | | ], |
| | | ['id' => $invitation->id] |
| | | ); |
| | | |
| | | // Set user role to artist with can_publish=false (needs verification) |
| | | $user = get_userdata($user_id); |
| | | // Set the user's verification status |
| | | $user->add_cap('skip_moderation', true); |
| | | |
| | | // If there's a shop to add the artist to, do that now |
| | | if (!empty($invitation->to_shop)) { |
| | | JVB()->routes('shopInvite')->addArtistToShop($user_id, $invitation->to_shop); |
| | | } |
| | | |
| | | // Notify inviters |
| | | $this->notifyInvitersOfAcceptance($invitation, $user_id); |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Notify all inviters that the invitation was accepted |
| | | * @param object $invitation The invitation object |
| | | * @param int $user_id the newly added user id |
| | | * @return void |
| | | */ |
| | | protected function notifyInvitersOfAcceptance(object $invitation, int $user_id):void |
| | | { |
| | | $inviters = json_decode($invitation->inviters, true); |
| | | $user_data = get_userdata($user_id); |
| | | |
| | | foreach ($inviters as $inviter) { |
| | | JVB()->notification()->addNotification( |
| | | $inviter['user_id'], |
| | | 'artist_joined', |
| | | [ |
| | | 'invited_email' => $invitation->email, |
| | | 'user_id' => $user_id, |
| | | 'display_name' => $user_data->display_name |
| | | ] |
| | | ); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Check if a registered user has a pending invitation. Accept invitation if so |
| | | * @param int $user_id The user ID to check |
| | | * @return void |
| | | */ |
| | | public function checkInvitation(int $user_id):void |
| | | { |
| | | $user = get_userdata($user_id); |
| | | |
| | | if (!$user) { |
| | | return; |
| | | } |
| | | |
| | | // Check if there's a token and email in the request |
| | | $token = isset($_GET['invite']) ? sanitize_text_field($_GET['invite']) : ''; |
| | | $email = isset($_GET['email']) ? sanitize_email($_GET['email']) : ''; |
| | | |
| | | if ($token && $email && $email === $user->user_email) { |
| | | $this->acceptInvitation($token, $email, $user_id); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Clean up expired invitations |
| | | * @return void |
| | | */ |
| | | public function cleanupExpiredInvitations():void |
| | | { |
| | | global $wpdb; |
| | | |
| | | $wpdb->query($wpdb->prepare( |
| | | "UPDATE {$this->tableName} |
| | | SET status = 'expired', updated_at = %s |
| | | WHERE status = 'pending' AND expires_at < NOW()", |
| | | current_time('mysql') |
| | | )); |
| | | } |
| | | |
| | | /** |
| | | * Get invitations sent by a specific user |
| | | * @param array $args built by buildParams() |
| | | * @return WP_REST_Response |
| | | */ |
| | | public function getUserInvitations(array $args):WP_REST_Response |
| | | { |
| | | if (!$this->checkUser($args['user'])) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Invalid user' |
| | | ]); |
| | | } |
| | | |
| | | $key = $this->cache->generateKey($args); |
| | | $cache = $this->cache->get($key); |
| | | $cache = false; |
| | | if ($cache) { |
| | | return new WP_REST_Response($cache); |
| | | } |
| | | |
| | | $per_page = 20; |
| | | |
| | | // Build query conditions |
| | | $conditions = []; |
| | | $params = []; |
| | | |
| | | $conditions[] = "inviters LIKE %s"; |
| | | $params[] = '%"'.$args['user'].'"%'; |
| | | |
| | | // Filter by status |
| | | if ($args['status'] !== 'all') { |
| | | $conditions[] = "status = %s"; |
| | | $params[] = $args['status']; |
| | | } |
| | | |
| | | $where = !empty($conditions) ? "WHERE " . implode(' AND ', $conditions) : ""; |
| | | |
| | | // Count total invitations for pagination |
| | | $count_query = "SELECT COUNT(*) FROM {$this->tableNames[$args['role']]} {$where}"; |
| | | $total = $this->wpdb->get_var($this->wpdb->prepare($count_query, $params)); |
| | | |
| | | // Get paginated invitations |
| | | $offset = ($args['page'] - 1) * $per_page; |
| | | $query = "SELECT * FROM {$this->tableNames[$args['role']]} {$where} ORDER BY created_at DESC LIMIT %d OFFSET %d"; |
| | | |
| | | // Add pagination parameters |
| | | $pagination_params = array_merge($params, [$per_page, $offset]); |
| | | $invitations = $this->wpdb->get_results($this->wpdb->prepare($query, $pagination_params)); |
| | | |
| | | // Format invitations for response |
| | | $formatted = []; |
| | | foreach ($invitations as $invitation) { |
| | | $formatted[] = $this->formatInvitation($invitation); |
| | | } |
| | | |
| | | $return = [ |
| | | 'invitations' => $formatted, |
| | | 'total' => (int)$total, |
| | | 'pages' => ceil($total / $per_page), |
| | | 'page' => $args['page'], |
| | | 'per_page' => $per_page |
| | | ]; |
| | | |
| | | $this->cache->set($key, $return); |
| | | |
| | | return new WP_REST_Response($return); |
| | | } |
| | | |
| | | /** |
| | | * Get a specific invitation by its ID |
| | | * |
| | | * @param int $invitationID The invitation ID to fetch |
| | | * @param string $role |
| | | * @return array|WP_Error The formatted invitation or an error |
| | | */ |
| | | protected function getInvitation(int $invitationID, string $role):array|WP_Error |
| | | { |
| | | // Validate invitation ID |
| | | $invitationID = intval($invitationID); |
| | | if (!$invitationID) { |
| | | return new WP_Error('invalid_id', 'Invalid invitation ID'); |
| | | } |
| | | |
| | | // Try to get from cache first |
| | | $cached = $this->cache->get($invitationID); |
| | | if ($cached) { |
| | | return $cached; |
| | | } |
| | | |
| | | // Query the database |
| | | $invitation = $this->wpdb->get_row($this->wpdb->prepare( |
| | | "SELECT * FROM {$this->tableNames[$role]} WHERE id = %d", |
| | | $invitationID |
| | | )); |
| | | |
| | | // Return error if not found |
| | | if (!$invitation) { |
| | | return new WP_Error('not_found', 'Invitation not found'); |
| | | } |
| | | |
| | | // Format the invitation for response |
| | | $formatted = $this->formatInvitation($invitation); |
| | | |
| | | // Cache the result |
| | | $this->cache->set($invitationID, $formatted); |
| | | |
| | | return $formatted; |
| | | } |
| | | |
| | | /** |
| | | * Get invitations for a specific user by their email or user ID |
| | | * |
| | | * @param int|string $identifier Either user ID or email of the invited person |
| | | * @param bool $include_token Whether to include the token in the response |
| | | * @return array|WP_Error The formatted invitations or an error |
| | | */ |
| | | public function getInvitationByUser(int|string $identifier):array|WP_Error |
| | | { |
| | | // Try to get from cache first |
| | | $cached = $this->cache->get($identifier); |
| | | if ($cached) { |
| | | return $cached; |
| | | } |
| | | global $wpdb; |
| | | |
| | | // Determine if we have a user ID or email |
| | | if (is_numeric($identifier)) { |
| | | // We have a user ID |
| | | $userID = intval($identifier); |
| | | |
| | | // Query by user ID |
| | | $invitation = $wpdb->get_row($wpdb->prepare( |
| | | "SELECT * FROM {$this->tableName} WHERE new_user_id = %d", |
| | | $userID |
| | | )); |
| | | } else { |
| | | // We have an email |
| | | $email = sanitize_email($identifier); |
| | | if (!is_email($email)) { |
| | | return new WP_Error('invalid_email', 'Invalid email address'); |
| | | } |
| | | |
| | | // Query by email |
| | | $invitation = $wpdb->get_row($wpdb->prepare( |
| | | "SELECT * FROM {$this->tableName} WHERE email = %s", |
| | | $email |
| | | )); |
| | | } |
| | | |
| | | // Return error if not found |
| | | if (!$invitation) { |
| | | return new WP_Error('not_found', 'No invitations found for this user'); |
| | | } |
| | | |
| | | // Format the invitation for response |
| | | $formattedInvitation = $this->formatInvitation($invitation); |
| | | |
| | | $this->cache->set($identifier, $formattedInvitation); |
| | | |
| | | return $formattedInvitation; |
| | | } |
| | | |
| | | /** |
| | | * Format invitation for API response |
| | | * @param object $invitation The invitation object |
| | | * @param bool $include_token whether or not to include the token in response |
| | | * @return array The formatted invitation |
| | | */ |
| | | protected function formatInvitation(object $invitation, bool $include_token = false):array |
| | | { |
| | | // Parse inviters JSON |
| | | $inviters = json_decode($invitation->inviters ?? '[]', true) ?: []; |
| | | |
| | | // Format inviters with names |
| | | $inviter_details = []; |
| | | foreach ($inviters as $inviter_id) { |
| | | $inviter_details[] = [ |
| | | 'id' => $inviter_id, |
| | | 'name' => jvbGetUsername($inviter_id) |
| | | ]; |
| | | } |
| | | |
| | | // Build formatted invitation |
| | | $formatted = [ |
| | | 'id' => $invitation->id, |
| | | 'name' => $invitation->name, |
| | | 'email' => $invitation->email, |
| | | 'status' => $invitation->status, |
| | | 'expires_at' => $invitation->expires_at, |
| | | 'accepted_at' => $invitation->accepted_at, |
| | | 'created_at' => $invitation->created_at, |
| | | 'updated_at' => $invitation->updated_at, |
| | | 'inviters' => $inviters |
| | | ]; |
| | | |
| | | // Include shop if assigned |
| | | if (!empty($invitation->to_shop)) { |
| | | $shop = get_term($invitation->to_shop, BASE . 'shop'); |
| | | if ($shop && !is_wp_error($shop)) { |
| | | $formatted['shop'] = [ |
| | | 'id' => $shop->term_id, |
| | | 'name' => $shop->name |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | // Include token if needed (only for validation) |
| | | if ($include_token) { |
| | | $formatted['token'] = $invitation->invitation_token; |
| | | } |
| | | |
| | | // Add registration URL for convenience |
| | | $formatted['registration_url'] = add_query_arg([ |
| | | 'token' => $invitation->invitation_token, |
| | | 'email' => urlencode($invitation->email) |
| | | ], home_url('/register/')); |
| | | |
| | | return $formatted; |
| | | } |
| | | |
| | | /** |
| | | * @param WP_Error|array $result The WP_Error to replace, if this is the operation type we're looking for |
| | | * @param object $operation The operation object |
| | | * @param array $data The data to process |
| | | * @return WP_Error|array WP_Error or array of processed data |
| | | * |
| | | */ |
| | | public function processOperation(WP_Error|array $result, object $operation, array $data):array|WP_Error |
| | | { |
| | | switch ($operation->type) { |
| | | case 'invitation_create': |
| | | return $this->processInvitations($data, $operation->user_id); |
| | | case 'invitation_revoke': |
| | | return $this->revokeInvite( |
| | | $data['invited'] |
| | | ); |
| | | } |
| | | return $result; |
| | | } |
| | | |
| | | /** |
| | | * Process a batch of invitations with transaction support |
| | | * |
| | | * @param array $data Array of invitation data ['role' => $invites ] |
| | | * @param int $user_id User ID of the inviter |
| | | * @return array Result data with success/failure information |
| | | */ |
| | | public function processInvitations(array $data, int $user_id):array |
| | | { |
| | | if (!$this->checkUser($user_id)) { |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => 'Invalid User', |
| | | ]; |
| | | } |
| | | |
| | | // Start transaction |
| | | $this->wpdb->query('START TRANSACTION'); |
| | | |
| | | $results = [ |
| | | 'success' => [], |
| | | 'failed' => [] |
| | | ]; |
| | | |
| | | try { |
| | | foreach ($data as $role => $invitations) { |
| | | foreach ($invitations as $invite) { |
| | | if (!$invite['invited_name'] || !$invite['invited_email']) { |
| | | $results['failed'][] = [ |
| | | 'email' => $invite['invited_email'], |
| | | 'name' => $invite['invited_name'], |
| | | 'reason' => 'Invalid name or email' |
| | | ]; |
| | | continue; |
| | | } |
| | | |
| | | if ($invite['to_term'] && !$this->checkTerm($invite)) { |
| | | $results['failed'][] = [ |
| | | 'email' => $invite['invited_email'], |
| | | 'name' => $invite['invited_name'], |
| | | 'reason' => 'Invalid taxonomy to add to' |
| | | ]; |
| | | } |
| | | |
| | | // Create invitation (modify your existing method to avoid sending emails yet) |
| | | $result = $this->createInvitation($invite['invited_name'], $invite['invited_email'], $user_id, $role, $invite['to_term'], $invite['taxonomy'], false); |
| | | |
| | | if (is_wp_error($result)) { |
| | | $results['failed'][] = [ |
| | | 'email' => $invite['invited_email'], |
| | | 'name' => $invite['invited_name'], |
| | | 'reason' => $result->get_error_message() |
| | | ]; |
| | | } else { |
| | | $results['success'][] = [ |
| | | 'email' => $invite['invited_email'], |
| | | 'name' => $invite['invited_name'], |
| | | 'id' => $result['id'], |
| | | 'to_term' => $invite['to_term'], |
| | | 'taxonomy' => $invite['taxonomy'], |
| | | 'role' => $role, |
| | | 'expires_at' => $result['expires_at'] |
| | | ]; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // If we've processed at least one invitation successfully, commit |
| | | if (!empty($results['success'])) { |
| | | $this->wpdb->query('COMMIT'); |
| | | |
| | | // Now send emails for successful invitations |
| | | foreach ($results['success'] as $invitation) { |
| | | $this->sendInvitationEmail( |
| | | $invitation['name'], |
| | | $invitation['email'], |
| | | $invitation['token'], |
| | | $user_id, |
| | | [$invitation['taxonomy'] => $invitation['to_term']], |
| | | $invitation['role'] |
| | | ); |
| | | } |
| | | } else { |
| | | // No successful invitations, roll back |
| | | $this->wpdb->query('ROLLBACK'); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => count($results['success']) > count($results['failed']), |
| | | 'results' => $results |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | // Handle error and roll back transaction |
| | | $this->wpdb->query('ROLLBACK'); |
| | | |
| | | JVB()->error()->log( |
| | | 'invitation_create', |
| | | 'Error processing batch invitations: ' . $e->getMessage(), |
| | | [ |
| | | 'user_id' => $user_id, |
| | | 'error' => $e->getMessage() |
| | | ], |
| | | 'error' |
| | | ); |
| | | return $formatted; |
| | | } |
| | | |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => [ |
| | | 'failed' => $invitations, |
| | | 'error' => $e->getMessage() |
| | | ] |
| | | ]; |
| | | } |
| | | } |
| | | } |