<?php
|
namespace JVBase\rest\routes;
|
|
use JVBase\JVB;
|
use JVBase\rest\RestRouteManager;
|
use Exception;
|
use JVBase\utility\Features;
|
use WP_REST_Response;
|
use WP_Error;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
// 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
|
*/
|
class Invitations extends RestRouteManager
|
{
|
protected string $tableName;
|
protected array $inviteTypes;
|
protected $wpdb;
|
protected string $prefix;
|
protected array $tableNames;
|
protected int $expiryDays = 14; // Invitations expire after 14 days
|
|
public function __construct()
|
{
|
$this->cache_name = 'invitations';
|
parent::__construct();
|
global $wpdb;
|
$this->inviteTypes = jvbInviteTableTypes();
|
$this->tableNames = jvbInviteTables();
|
$this->wpdb = $wpdb;
|
$this->prefix = $wpdb->prefix;
|
|
// Add hooks for processing accepted invitations
|
add_action('user_register', [$this, 'checkInvitation'], 10, 1);
|
|
|
add_filter('jvbLoginLabels', [$this, 'modifyLoginLabels'], 10, 2);
|
|
|
|
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;
|
|
return [
|
'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,
|
];
|
}
|
/**
|
* @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;
|
}
|
|
/**
|
* 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');
|
}
|
|
// 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');
|
}
|
|
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');
|
}
|
}
|
|
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');
|
}
|
$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
|
));
|
|
if (!$invitation) {
|
return new WP_Error('invalid_invitation', 'Invalid invitation token or email');
|
}
|
|
// Check if expired
|
if (strtotime($invitation->expires_at) < time()) {
|
return new WP_Error('expired_invitation', 'This invitation has expired');
|
}
|
|
return $invitation;
|
}
|
|
/**
|
* 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;
|
|
$siteName = get_bloginfo('name');
|
|
$subject = apply_filters('jvbInvitationSubject',
|
sprintf(
|
"%s invited you to join %s!",
|
$inviter_name,
|
$siteName
|
),
|
$inviter_name
|
);
|
|
$signup_url = add_query_arg([
|
'invite' => $token,
|
'email' => urlencode($email),
|
'name' => $name,
|
'role' => $role
|
], wp_registration_url());
|
|
|
// Get shop name if applicable
|
$toContentTax = [];
|
if (!empty ($terms)) {
|
foreach ($terms as $taxonomy => $termID) {
|
$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,
|
html_entity_decode($term->name),
|
$taxonomy
|
);
|
}
|
}
|
}
|
$toContentTax = implode(' ', $toContentTax);
|
|
$button = JVB()->email()->button($signup_url, 'Join the Scene!');
|
$link = JVB()->email()->link($signup_url);
|
$signature = JVB()->email()->signature();
|
|
$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 = JVB()->email()->sendEmail($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 = JVB()->email()->sendEmail($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);
|
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 [
|
'success' => false,
|
'result' => [
|
'failed' => $invitations,
|
'error' => $e->getMessage()
|
]
|
];
|
}
|
}
|
|
public function modifyLoginLabels(array $labels, array $get_params): array
|
{
|
// Only modify if invitation params present
|
if (!array_key_exists('invite', $get_params) || !array_key_exists('email', $get_params)) {
|
return $labels;
|
}
|
$email = sanitize_email($get_params['email']);
|
$token = sanitize_text_field($get_params['invite']);
|
$user = email_exists($email);
|
if (!$user) {
|
return $labels;
|
}
|
$role = jvbUserRole($user);
|
// Get invitation data
|
$data = $this->verifyInvitation(
|
$token,
|
$email,
|
$role,
|
);
|
|
if (!$data) {
|
return $labels;
|
}
|
|
// Build custom message
|
$inviters = json_decode($data->inviters, true);
|
$name = $data->name;
|
$names = array_map(function($inviter) {
|
$artist = jvbContentFromUser((int)$inviter['user_id']);
|
return $artist['name'] ?: $artist['display_name'];
|
}, $inviters);
|
|
$message = count($names) > 1
|
? 'are already here, and have invited you to join in!'
|
: ' is already here, and invited you to join in!';
|
|
// Modify labels
|
$labels['title'] = 'Join the Scene, ' . $data->name;
|
$labels['description'] = [jvbCommaList($names) . ' ' . $message];
|
|
return $labels;
|
}
|
}
|