<?php
/**
 * Name: Multiple User Roles
 * Description: Allow assigning multiple roles to single user instead of WordPress default one-role limitation for complex permission needs like author and editor simultaneously
 * Category: User Management
 * Icon: -
  * UseCases: [
 *   {
 *     "title": "Flexible Permissions",
 *     "description": "Assign multiple roles to users who need access to features from different user levels, like editors who also manage store orders."
 *   },
 *   {
 *     "title": "Team Management",
 *     "description": "Handle complex permission scenarios for team members who wear multiple hats without creating custom roles."
 *   }
 * ]
 * Added: 2025-08-28
 * Tags: roles, users, permissions, capabilities, multiple
 * WhereToFind: {
 *   "location": "Users → Edit User",
 *   "url": "user-edit.php",
 *   "instructions": "Role checkboxes replace single dropdown - select multiple",
 *   "newTab": false
 * }
 */

// Prevent direct access
if (!defined('ABSPATH')) exit;

class SwitchboardMultipleUserRoles {
    
    public function __construct() {
        add_action('show_user_profile', [$this, 'add_role_checkboxes']);
        add_action('edit_user_profile', [$this, 'add_role_checkboxes']);

        // Prevent WordPress from overriding our multiple roles with single role
        add_action('personal_options_update', [$this, 'prevent_wp_role_override'], 5);
        add_action('edit_user_profile_update', [$this, 'prevent_wp_role_override'], 5);

        // Use priority 20 to run AFTER WordPress's default role update (priority 10)
        add_action('personal_options_update', [$this, 'save_roles'], 20);
        add_action('edit_user_profile_update', [$this, 'save_roles'], 20);

        add_filter('user_has_cap', [$this, 'merge_capabilities'], 10, 4);

        // Hide the default WordPress role dropdown since we're using checkboxes
        add_action('admin_head', [$this, 'hide_default_role_dropdown']);
    }

    public function prevent_wp_role_override($user_id) {
        // Check if our checkboxes were submitted
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified in save_multiple_roles(), this just prevents WP role override
        if (isset($_POST['switchboard_roles_nonce'])) {
            // Remove the 'role' field from $_POST to prevent WordPress from processing it
            // This stops WordPress from overriding our multiple roles with a single role
            unset($_POST['role']);
        }
    }

    public function hide_default_role_dropdown() {
        $screen = get_current_screen();
        if ($screen && ($screen->id === 'user-edit' || $screen->id === 'profile')) {
            echo '<style>
                .user-role-wrap { display: none !important; }
            </style>';
        }
    }
    
    public function add_role_checkboxes($user) {
        if (!current_user_can('edit_users')) {
            return;
        }
        
        $user_roles = (array) $user->roles;
        $all_roles = wp_roles()->roles;
        ?>
        <h3><?php esc_html_e('Additional Roles', 'wpswitchboard'); ?> <span class="switchboard-badge">SWITCHBOARD</span></h3>
        <table class="form-table">
            <tr>
                <th><?php esc_html_e('Assign Multiple Roles', 'wpswitchboard'); ?></th>
                <td>
                    <?php
                    wp_nonce_field('switchboard_save_roles_' . $user->ID, 'switchboard_roles_nonce');
                    foreach ($all_roles as $role_key => $role): ?>
                        <label>
                            <input type="checkbox" name="switchboard_roles[]" value="<?php echo esc_attr($role_key); ?>"
                                   <?php checked(in_array($role_key, $user_roles)); ?>>
                            <?php echo esc_html($role['name']); ?>
                        </label><br>
                    <?php endforeach; ?>
                    <p class="description"><?php esc_html_e('User will have combined capabilities from all selected roles.', 'wpswitchboard'); ?></p>
                    <p class="description" style="margin-top: 8px; color: #b26200;"><strong><?php esc_html_e('Note:', 'wpswitchboard'); ?></strong> <?php esc_html_e('Role assignments persist even if this module is disabled or the plugin is uninstalled.', 'wpswitchboard'); ?></p>
                </td>
            </tr>
        </table>
        <?php
    }
    
    public function save_roles($user_id) {
        if (!current_user_can('edit_user', $user_id)) {
            return;
        }

        // Verify nonce for CSRF protection - if no nonce, this save wasn't triggered by our form
        if (!isset($_POST['switchboard_roles_nonce']) ||
            !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['switchboard_roles_nonce'])), 'switchboard_save_roles_' . $user_id)) {
            return;
        }

        // SECURITY: Only administrators can assign multiple roles
        if (!current_user_can('manage_options')) {
            add_action('admin_notices', function() {
                echo '<div class="notice notice-error"><p>' . esc_html__('Only administrators can modify user roles.', 'wpswitchboard') . '</p></div>';
            });
            return;
        }

        $user = new WP_User($user_id);
        $current_user = wp_get_current_user();

        // SECURITY: Prevent users from editing their own roles (self-escalation)
        if ($user_id === $current_user->ID) {
            // Don't show error - just silently skip. The checkboxes are visible but clicking save on own profile shouldn't error.
            return;
        }

        // Get current roles and requested roles
        $current_roles = (array) $user->roles;
        $requested_roles = [];
        if (isset($_POST['switchboard_roles']) && is_array($_POST['switchboard_roles'])) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized per-item below via sanitize_key
            foreach (wp_unslash($_POST['switchboard_roles']) as $role) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each role sanitized via sanitize_key below
                $role = sanitize_key($role);
                // Verify role exists
                if (get_role($role)) {
                    $requested_roles[] = $role;
                }
            }
        }

        // Default to subscriber if nothing selected
        if (empty($requested_roles)) {
            $requested_roles = ['subscriber'];
        }

        // Sort both arrays and compare - if same, skip the update
        sort($current_roles);
        sort($requested_roles);
        if ($current_roles === $requested_roles) {
            // No changes needed
            return;
        }

        // Remove all current roles
        foreach ($user->roles as $role) {
            $user->remove_role($role);
        }

        // Add selected roles
        foreach ($requested_roles as $role) {
            $user->add_role($role);
        }

        // Add success notice
        $role_names = array_map(function($role_key) {
            $role = get_role($role_key);
            return $role ? wp_roles()->roles[$role_key]['name'] : $role_key;
        }, $requested_roles);

        add_action('admin_notices', function() use ($role_names) {
            /* translators: %s: comma-separated list of role names */
            echo '<div class="notice notice-success is-dismissible"><p><strong>' . esc_html__('User roles updated:', 'wpswitchboard') . '</strong> ' . esc_html(implode(', ', $role_names)) . '</p></div>';
        });
    }
    
    public function merge_capabilities($allcaps, $caps, $args, $user) {
        // Capabilities are automatically merged by WordPress when multiple roles exist
        return $allcaps;
    }

    /**
     * Register data locations for cleanup system
     */
    public function register_data_locations() {
        return [
            'options' => [],
            'option_patterns' => [],
            'tables' => [],
            'post_meta' => [],
            'user_meta' => [],
        ];
    }
}

$switchboard_instance = new SwitchboardMultipleUserRoles();
Switchboard_Data_Registry::get_instance()->auto_register($switchboard_instance, __FILE__);
