do_invite_member(); } /** * Invite logic shared by the storefront and admin AJAX handlers. * Callers check their own nonce first; it can't be checked again here. */ private function do_invite_member() { $current_user_id = get_current_user_id(); $team_accounts = CrewCommerce_Team_Accounts::get_instance(); // A site administrator can pass a company; everyone else invites to // their own company. $posted_company = isset($_POST['company_id']) ? absint(wp_unslash($_POST['company_id'])) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified by the caller. $by_store_owner = false; if ($posted_company && current_user_can('manage_options')) { $company_id = $posted_company; $inviter_role = 'admin'; $by_store_owner = true; } else { $company_id = $team_accounts->get_user_company($current_user_id); $inviter_role = $team_accounts->get_user_role($current_user_id); } if (!$company_id || !in_array($inviter_role, array('admin', 'manager'), true)) { wp_send_json_error(__('Insufficient permissions.', 'crewcommerce')); } // phpcs:disable WordPress.Security.NonceVerification.Missing -- verified by the caller. $email = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : ''; $role = isset($_POST['role']) ? sanitize_key(wp_unslash($_POST['role'])) : 'requester'; // phpcs:enable $valid_roles = array('admin', 'manager', 'requester'); if (!in_array($role, $valid_roles, true)) { $role = 'requester'; } // Only a company Admin (or the store owner) can make someone an Admin. if ('admin' === $role && 'admin' !== $inviter_role) { wp_send_json_error(__('Only the company Admin can invite another Admin.', 'crewcommerce')); } if (!is_email($email)) { wp_send_json_error(__('Please enter a valid email address.', 'crewcommerce')); } $company = get_post($company_id); $inviter = get_user_by('ID', $current_user_id); $existing = get_user_by('email', $email); if (!$company) { wp_send_json_error(__('Your company could not be found. Please contact the site administrator.', 'crewcommerce')); } if ($existing) { $existing_company = (int) $team_accounts->get_user_company($existing->ID); if ($existing_company === (int) $company_id) { wp_send_json_error(__('That person is already on your team. Change their role in the team list.', 'crewcommerce')); } if ($existing_company) { wp_send_json_error(__('That user is already a member of another company.', 'crewcommerce')); } // The store owner adds people directly. if ($by_store_owner) { $team_accounts->add_team_member($existing->ID, $company_id, $role); $this->send_team_invite_email($existing->user_email, $existing->display_name, $inviter->display_name, $company->post_title, false, wc_get_account_endpoint_url('crewcommerce-dashboard'), $role, $existing->user_login); wp_send_json_success(__('Team member added and notified by email.', 'crewcommerce')); } // Anyone else's invitation has to be accepted: joining a company // lets its Managers approve, and see, that person's orders. $token = wp_generate_password(32, false); update_user_meta($existing->ID, self::invite_meta_key(), array( 'company_id' => (int) $company_id, 'role' => $role, 'token' => hash('sha256', $token), 'expires' => time() + self::INVITE_TTL, 'inviter_id' => (int) $current_user_id, )); $accept_link = add_query_arg('crewcommerce_invite', rawurlencode($token), wc_get_page_permalink('myaccount')); $this->send_team_invite_email($existing->user_email, $existing->display_name, $inviter->display_name, $company->post_title, false, $accept_link, $role, $existing->user_login); wp_send_json_success(__('Invitation sent. They join your team once they accept it from the email.', 'crewcommerce')); } // Create new WP user with a random password they'll reset $username = sanitize_user(strstr($email, '@', true), true); if (empty($username) || username_exists($username)) { $username = 'user_' . substr(md5($email . time()), 0, 8); } $random_password = wp_generate_password(16, false); $user_id = wp_insert_user(array( 'user_login' => $username, 'user_email' => $email, 'user_pass' => $random_password, 'role' => 'customer', )); if (is_wp_error($user_id)) { wp_send_json_error($user_id->get_error_message()); } $team_accounts->add_team_member($user_id, $company_id, $role); // Send password-reset style invite $reset_key = get_password_reset_key(get_user_by('ID', $user_id)); $reset_link = is_wp_error($reset_key) ? wc_get_page_permalink('myaccount') : add_query_arg(array('key' => $reset_key, 'login' => rawurlencode($username)), wc_get_page_permalink('myaccount') . 'lost-password/'); $this->send_team_invite_email($email, $username, $inviter->display_name, $company->post_title, true, $reset_link, $role, $username); wp_send_json_success(__('Invitation sent. The user will receive an email to set their password and join the team.', 'crewcommerce')); } /** * Send team invite email */ private function send_team_invite_email($to_email, $to_name, $inviter_name, $company_name, $is_new_user, $reset_link = '', $role = 'requester', $username = '') { $role_labels = array( 'admin' => __('Admin', 'crewcommerce'), 'manager' => __('Manager', 'crewcommerce'), 'requester' => __('Requester', 'crewcommerce'), ); $role_label = $role_labels[$role] ?? ucfirst($role); if ($is_new_user) { /* translators: 1: company name, 2: store name, 3: role */ $subject = sprintf(__('You have been invited to join %1$s on %2$s as a %3$s', 'crewcommerce'), $company_name, get_bloginfo('name'), $role_label); $message = sprintf( __("Hello,\n\n%1\$s has invited you to join their team at %2\$s on %3\$s as a %4\$s.\n\nYour username is: %5\$s\n\nTo get started, click the link below to set your password:\n%6\$s\n\nOnce logged in, you will have access to the team purchasing dashboard.", 'crewcommerce'), $inviter_name, $company_name, get_bloginfo('name'), $role_label, $username, $reset_link ); } else { /* translators: 1: company name, 2: store name, 3: role */ $subject = sprintf(__('You have been invited to join %1$s on %2$s as a %3$s', 'crewcommerce'), $company_name, get_bloginfo('name'), $role_label); $message = sprintf( /* translators: 1: person's name, 2: inviter's name, 3: company name, 4: store name, 5: role, 6: link */ __("Hello %1\$s,\n\n%2\$s has invited you to join the %3\$s team on %4\$s as a %5\$s. As a member, your orders may need approval from the company's managers, who can see them.\n\nTo accept, log in and open this link:\n%6\$s\n\nIf you weren't expecting this, ignore this email and nothing changes.", 'crewcommerce'), $to_name, $inviter_name, $company_name, get_bloginfo('name'), $role_label, $reset_link ); } wp_mail($to_email, $subject, $message); } /** * AJAX: Invite member from admin dashboard (crewcommerce_admin_nonce) */ public function ajax_admin_invite_member() { check_ajax_referer('crewcommerce_admin_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Insufficient permissions.', 'crewcommerce')); } $company_id = isset($_POST['company_id']) ? absint(wp_unslash($_POST['company_id'])) : 0; if (!$company_id || get_post_type($company_id) !== 'crewcommerce_company') { wp_send_json_error(__('Invalid company.', 'crewcommerce')); } $this->do_invite_member(); } /** * Accept an invitation from the link in the email. The link's token is * the proof; it only works for the account it was sent to. */ public function handle_invite_link() { if (!isset($_GET['crewcommerce_invite'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- the secret token is the proof. return; } $token = sanitize_text_field(wp_unslash($_GET['crewcommerce_invite'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if (!is_user_logged_in()) { // WooCommerce sends people back to this link after they log in. wc_add_notice(__('Log in to accept your team invitation.', 'crewcommerce'), 'notice'); return; } $user_id = get_current_user_id(); $team = CrewCommerce_Team_Accounts::get_instance(); $invite = get_user_meta($user_id, self::invite_meta_key(), true); $redirect = wc_get_page_permalink('myaccount'); $valid = is_array($invite) && !empty($invite['token']) && is_string($token) && '' !== $token && hash_equals((string) $invite['token'], hash('sha256', $token)) && (int) $invite['expires'] >= time(); if (!$valid) { wc_add_notice(__('This invitation link isn’t valid for this account or has expired. Ask for a new invitation.', 'crewcommerce'), 'error'); } elseif ($team->get_user_company($user_id)) { delete_user_meta($user_id, self::invite_meta_key()); wc_add_notice(__('You’re already part of a company, so this invitation can’t be accepted.', 'crewcommerce'), 'error'); } elseif ('crewcommerce_company' !== get_post_type((int) $invite['company_id'])) { delete_user_meta($user_id, self::invite_meta_key()); wc_add_notice(__('That company no longer exists.', 'crewcommerce'), 'error'); } else { $team->add_team_member($user_id, (int) $invite['company_id'], $invite['role']); delete_user_meta($user_id, self::invite_meta_key()); /* translators: %s: company name */ wc_add_notice(sprintf(__('You’ve joined %s.', 'crewcommerce'), get_the_title((int) $invite['company_id'])), 'success'); $redirect = wc_get_account_endpoint_url('crewcommerce-dashboard'); } wp_safe_redirect($redirect); exit; } }