register_company_post_type(); add_action('wp_ajax_crewcommerce_add_team_member', array($this, 'ajax_add_team_member')); add_action('wp_ajax_crewcommerce_remove_team_member', array($this, 'ajax_remove_team_member')); add_action('wp_ajax_crewcommerce_update_member_role', array($this, 'ajax_update_member_role')); add_action('wp_ajax_crewcommerce_add_company', array($this, 'ajax_add_company')); add_action('wp_ajax_crewcommerce_delete_company', array($this, 'ajax_delete_company')); add_action('before_delete_post', array($this, 'handle_company_post_deletion')); // Remove the team row when a user is deleted: delete_user for a site, // wpmu_delete_user for the whole network. add_action('delete_user', array($this, 'handle_single_site_user_deletion')); add_action('wpmu_delete_user', array($this, 'handle_network_user_deletion')); add_action('show_user_profile', array($this, 'add_user_profile_fields')); add_action('edit_user_profile', array($this, 'add_user_profile_fields')); add_action('personal_options_update', array($this, 'save_user_profile_fields')); add_action('edit_user_profile_update', array($this, 'save_user_profile_fields')); } public function register_company_post_type() { register_post_type('crewcommerce_company', array( 'label' => __('Companies', 'crewcommerce'), 'public' => false, 'show_ui' => true, 'show_in_menu' => false, 'capability_type' => 'post', 'supports' => array('title', 'editor'), 'labels' => array( 'name' => __('Companies', 'crewcommerce'), 'singular_name' => __('Company', 'crewcommerce'), 'add_new' => __('Add New Company', 'crewcommerce'), 'add_new_item' => __('Add New Company', 'crewcommerce'), 'edit_item' => __('Edit Company', 'crewcommerce'), 'new_item' => __('New Company', 'crewcommerce'), 'view_item' => __('View Company', 'crewcommerce'), 'search_items' => __('Search Companies', 'crewcommerce'), 'not_found' => __('No companies found', 'crewcommerce'), 'not_found_in_trash' => __('No companies found in trash', 'crewcommerce'), ), )); } /** * Create a company with $admin_user_id as its Admin. Returns the company * ID, or false. */ public function create_company($name, $admin_user_id, $description = '') { $company_id = wp_insert_post(array( 'post_title' => sanitize_text_field($name), 'post_content' => sanitize_textarea_field($description), 'post_status' => 'publish', 'post_type' => 'crewcommerce_company', )); if ($company_id && !is_wp_error($company_id)) { $this->add_team_member($admin_user_id, $company_id, 'admin'); update_post_meta($company_id, '_company_admin', $admin_user_id); /** * Fires after a company is created. * * @param int $company_id The new company's ID. * @param int $admin_user_id The user who is its Admin. */ do_action('crewcommerce_company_created', $company_id, $admin_user_id); return $company_id; } return false; } /** * Whether a published company with this exact name already exists. */ public function company_name_exists($name) { $existing = get_posts(array( 'post_type' => 'crewcommerce_company', 'title' => $name, 'post_status' => 'publish', 'numberposts' => 1, 'fields' => 'ids', )); return !empty($existing); } /** * Add a user to a company. If they're already a member, their role is * updated instead. */ public function add_team_member($user_id, $company_id, $role = 'requester', $permissions = array()) { global $wpdb; if (!in_array($role, array('admin', 'manager', 'requester'), true)) { $role = 'requester'; } $permissions = array_merge($this->get_default_permissions($role), $permissions); $existing = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery "SELECT id FROM {$wpdb->prefix}crewcommerce_team_members WHERE user_id = %d AND company_id = %d", $user_id, $company_id )); if ($existing) { return $this->update_member_role($user_id, $company_id, $role); } $result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->prefix . 'crewcommerce_team_members', array( 'user_id' => $user_id, 'company_id' => $company_id, 'role' => $role, 'permissions' => wp_json_encode($permissions), 'created_at' => current_time('mysql', true), ), array('%d', '%d', '%s', '%s', '%s') ); if ($result) { update_user_meta($user_id, crewcommerce_site_meta_key('_crewcommerce_company_id'), $company_id); update_user_meta($user_id, crewcommerce_site_meta_key('_crewcommerce_role'), $role); $this->record_activity($user_id, $company_id, 'added_to_team', array('role' => $role)); return true; } return false; } /** * Remove a user from a company. Returns true, false or a WP_Error when * the removal would leave the company without an Admin. */ public function remove_team_member($user_id, $company_id) { global $wpdb; $company_admin_id = get_post_meta($company_id, '_company_admin', true); if ($company_admin_id == $user_id) { $result = $this->handle_admin_removal($user_id, $company_id); if (is_wp_error($result)) { return $result; } } $result = $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->prefix . 'crewcommerce_team_members', array('user_id' => $user_id, 'company_id' => $company_id), array('%d', '%d') ); if ($result) { delete_user_meta($user_id, crewcommerce_site_meta_key('_crewcommerce_company_id')); delete_user_meta($user_id, crewcommerce_site_meta_key('_crewcommerce_role')); $this->record_activity($user_id, $company_id, 'removed_from_team', array()); return true; } return false; } public function handle_single_site_user_deletion($user_id) { $this->cleanup_team_membership_for_deleted_user($user_id); } public function handle_network_user_deletion($user_id) { if (is_multisite() && function_exists('get_blogs_of_user')) { foreach (get_blogs_of_user($user_id) as $blog) { switch_to_blog($blog->userblog_id); $this->cleanup_team_membership_for_deleted_user($user_id); restore_current_blog(); } return; } $this->cleanup_team_membership_for_deleted_user($user_id); } /** * The account is being deleted, so unlike remove_team_member() this never * blocks: hand on the Admin seat if possible, then remove the row. */ private function cleanup_team_membership_for_deleted_user($user_id) { global $wpdb; $company_id = $this->get_user_company($user_id); if (!$company_id) { return; } if (get_post_meta($company_id, '_company_admin', true) == $user_id) { $this->handle_admin_removal($user_id, $company_id); } $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->prefix . 'crewcommerce_team_members', array('user_id' => $user_id, 'company_id' => $company_id), array('%d', '%d') ); delete_user_meta($user_id, crewcommerce_site_meta_key('_crewcommerce_company_id')); delete_user_meta($user_id, crewcommerce_site_meta_key('_crewcommerce_role')); $this->record_activity($user_id, $company_id, 'removed_from_team', array('reason' => 'account_deleted')); } /** * Hand the Admin seat to someone else before the current Admin leaves: * another Admin if there is one, otherwise the longest-serving Manager. */ private function handle_admin_removal($admin_user_id, $company_id) { global $wpdb; $total_members = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery "SELECT COUNT(*) FROM {$wpdb->prefix}crewcommerce_team_members WHERE company_id = %d", $company_id )); if ($total_members <= 1) { return new WP_Error( 'cannot_remove_last_member', __('Cannot remove the last team member. Delete the company instead if you want to remove everyone.', 'crewcommerce') ); } $new_admin_id = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery "SELECT user_id FROM {$wpdb->prefix}crewcommerce_team_members WHERE company_id = %d AND role = 'admin' AND user_id != %d ORDER BY created_at ASC LIMIT 1", $company_id, $admin_user_id )); if ($new_admin_id) { update_post_meta($company_id, '_company_admin', $new_admin_id); $this->notify_new_company_admin($new_admin_id, $company_id); return true; } $manager_to_promote = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery "SELECT user_id FROM {$wpdb->prefix}crewcommerce_team_members WHERE company_id = %d AND role = 'manager' AND user_id != %d ORDER BY created_at ASC LIMIT 1", $company_id, $admin_user_id )); if ($manager_to_promote) { $this->update_member_role($manager_to_promote, $company_id, 'admin'); update_post_meta($company_id, '_company_admin', $manager_to_promote); $this->notify_promoted_to_admin($manager_to_promote, $company_id); return true; } return new WP_Error( 'cannot_remove_only_admin', __('Cannot remove the only Admin. Make another team member a Manager first, or delete the company.', 'crewcommerce') ); } private function notify_promoted_to_admin($user_id, $company_id) { $user = get_user_by('ID', $user_id); $company = get_post($company_id); if ($user && $company) { /* translators: %s: company name */ $subject = sprintf(__('You are now the Admin of %s', 'crewcommerce'), $company->post_title); $message = sprintf( /* translators: 1: user name, 2: company name, 3: dashboard URL */ __("Hello %1\$s,\n\nThe previous Admin of %2\$s was removed, so you have been made its Admin.\n\nYou can now manage the team and approve orders.\n\nYour team dashboard: %3\$s", 'crewcommerce'), $user->display_name, $company->post_title, wc_get_account_endpoint_url('crewcommerce-dashboard') ); wp_mail($user->user_email, $subject, $message); } } /** * Change a member's role. Refuses to demote a company's only Admin. */ public function update_member_role($user_id, $company_id, $new_role) { global $wpdb; if (!in_array($new_role, array('admin', 'manager', 'requester'), true)) { return new WP_Error('invalid_role', __('Invalid role specified.', 'crewcommerce')); } $company_admin_id = get_post_meta($company_id, '_company_admin', true); if ($company_admin_id == $user_id && 'admin' !== $new_role) { $other_admins = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery "SELECT COUNT(*) FROM {$wpdb->prefix}crewcommerce_team_members WHERE company_id = %d AND role = 'admin' AND user_id != %d", $company_id, $user_id )); if (0 == $other_admins) { return new WP_Error('cannot_demote_only_admin', __('This person is the only company Admin. Make someone else Admin first.', 'crewcommerce')); } } // $wpdb->update() returns 0, not false, when no row matches, so check // the membership exists first. $member_exists = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery "SELECT id FROM {$wpdb->prefix}crewcommerce_team_members WHERE user_id = %d AND company_id = %d", $user_id, $company_id )); if (!$member_exists) { return new WP_Error('not_a_member', __('This user is not a member of this company.', 'crewcommerce')); } $result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->prefix . 'crewcommerce_team_members', array( 'role' => $new_role, 'permissions' => wp_json_encode($this->get_default_permissions($new_role)), ), array('user_id' => $user_id, 'company_id' => $company_id), array('%s', '%s'), array('%d', '%d') ); if (false !== $result) { update_user_meta($user_id, crewcommerce_site_meta_key('_crewcommerce_role'), $new_role); if ($company_admin_id == $user_id && 'admin' !== $new_role) { $this->assign_new_company_admin($company_id); } $this->record_activity($user_id, $company_id, 'role_updated', array('new_role' => $new_role)); return true; } return false; } private function assign_new_company_admin($company_id) { global $wpdb; $new_admin = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery "SELECT user_id FROM {$wpdb->prefix}crewcommerce_team_members WHERE company_id = %d AND role = 'admin' ORDER BY created_at ASC LIMIT 1", $company_id )); if ($new_admin) { update_post_meta($company_id, '_company_admin', $new_admin); $this->notify_new_company_admin($new_admin, $company_id); } } private function notify_new_company_admin($user_id, $company_id) { $user = get_user_by('ID', $user_id); $company = get_post($company_id); if ($user && $company) { /* translators: %s: company name */ $subject = sprintf(__('You are now the Admin of %s', 'crewcommerce'), $company->post_title); $message = sprintf( /* translators: 1: user name, 2: company name, 3: dashboard URL */ __("Hello %1\$s,\n\nYou are now the Admin of %2\$s.\n\nYou can manage the team and approve orders.\n\nYour team dashboard: %3\$s", 'crewcommerce'), $user->display_name, $company->post_title, wc_get_account_endpoint_url('crewcommerce-dashboard') ); wp_mail($user->user_email, $subject, $message); } } /** * A company's members with their name and email, Admins first. */ public function get_team_members($company_id) { global $wpdb; return $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery "SELECT tm.*, u.display_name, u.user_email FROM {$wpdb->prefix}crewcommerce_team_members tm JOIN {$wpdb->users} u ON tm.user_id = u.ID WHERE tm.company_id = %d ORDER BY tm.role DESC, u.display_name ASC", $company_id )); } /** * Companies with no Manager or Admin, whose orders that need approval * have nobody at the company to approve them. Returns WP_Post objects. */ public function get_companies_missing_approvers() { global $wpdb; $companies = get_posts(array( 'post_type' => 'crewcommerce_company', 'posts_per_page' => -1, 'post_status' => 'publish', )); if (empty($companies)) { return array(); } $company_ids = wp_list_pluck($companies, 'ID'); $placeholders = implode(',', array_fill(0, count($company_ids), '%d')); // Join wp_users so a row left behind by a deleted account doesn't // count as a real approver. // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- only %d placeholders are interpolated. $with_approver = $wpdb->get_col($wpdb->prepare( "SELECT DISTINCT tm.company_id FROM {$wpdb->prefix}crewcommerce_team_members tm JOIN {$wpdb->users} u ON tm.user_id = u.ID WHERE tm.company_id IN ({$placeholders}) AND tm.role IN ('manager', 'admin')", ...$company_ids )); // phpcs:enable $with_approver = array_map('intval', $with_approver); return array_values(array_filter($companies, function ($company) use ($with_approver) { return !in_array($company->ID, $with_approver, true); })); } /** * When a person joined their company, as a Unix timestamp, or 0 if * they aren't a member. Stored in UTC. */ public function get_member_since($user_id, $company_id) { global $wpdb; $created = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery "SELECT created_at FROM {$wpdb->prefix}crewcommerce_team_members WHERE user_id = %d AND company_id = %d", $user_id, $company_id )); return $created ? (int) strtotime($created . ' UTC') : 0; } public function get_user_company($user_id) { return get_user_meta($user_id, crewcommerce_site_meta_key('_crewcommerce_company_id'), true); } public function get_user_role($user_id) { return get_user_meta($user_id, crewcommerce_site_meta_key('_crewcommerce_role'), true); } public function user_has_permission($user_id, $permission) { global $wpdb; $company_id = $this->get_user_company($user_id); if (!$company_id) { return false; } $member = $wpdb->get_row($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery "SELECT permissions FROM {$wpdb->prefix}crewcommerce_team_members WHERE user_id = %d AND company_id = %d", $user_id, $company_id )); if ($member) { $permissions = json_decode($member->permissions, true); return is_array($permissions) && in_array($permission, $permissions, true); } return false; } private function get_default_permissions($role) { switch ($role) { case 'admin': $permissions = array('view_products', 'add_to_cart', 'submit_orders', 'approve_orders', 'manage_team'); break; case 'manager': $permissions = array('view_products', 'add_to_cart', 'submit_orders', 'approve_orders'); break; case 'requester': default: $permissions = array('view_products', 'add_to_cart', 'submit_orders'); break; } return apply_filters('crewcommerce_default_permissions', $permissions, $role); } /** * Announce a change to a company's team so add-ons can record it. * * @param int $user_id The member the change is about. * @param int $company_id The company. * @param string $action added_to_team, removed_from_team or role_updated. * @param array $data Details, such as the new role. */ private function record_activity($user_id, $company_id, $action, $data = array()) { do_action('crewcommerce_team_activity', (int) $user_id, (int) $company_id, $action, $data); } /** * Company and role fields on the WordPress user profile screen, for site * administrators. */ public function add_user_profile_fields($user) { if (!current_user_can('manage_options')) { return; } $company_id = $this->get_user_company($user->ID); $role = $this->get_user_role($user->ID); $companies = get_posts(array( 'post_type' => 'crewcommerce_company', 'posts_per_page' => -1, 'post_status' => 'publish', 'orderby' => 'title', 'order' => 'ASC', )); ?>