<?php

use WP_CLI\CommandWithDBObject;
use WP_CLI\Fetchers\User as UserFetcher;
use WP_CLI\Formatter;
use WP_CLI\Iterators\CSV as CsvIterator;
use WP_CLI\Utils;

/**
 * Manages users, along with their roles, capabilities, and meta.
 *
 * See references for [Roles and Capabilities](https://codex.wordpress.org/Roles_and_Capabilities) and [WP User class](https://codex.wordpress.org/Class_Reference/WP_User).
 *
 * ## EXAMPLES
 *
 *     # List user IDs
 *     $ wp user list --field=ID
 *     1
 *
 *     # Create a new user.
 *     $ wp user create bob bob@example.com --role=author
 *     Success: Created user 3.
 *     Password: k9**&I4vNH(&
 *
 *     # Update an existing user.
 *     $ wp user update 123 --display_name=Mary --user_pass=marypass
 *     Success: Updated user 123.
 *
 *     # Delete user 123 and reassign posts to user 567
 *     $ wp user delete 123 --reassign=567
 *     Success: Removed user 123 from http://example.com.
 *
 * @package wp-cli
 */
class User_Command extends CommandWithDBObject {

	protected $obj_type   = 'user';
	protected $obj_fields = [
		'ID',
		'user_login',
		'display_name',
		'user_email',
		'user_registered',
		'roles',
	];

	private $cap_fields = [
		'name',
	];
	private $fetcher;

	public function __construct() {
		$this->fetcher = new UserFetcher();
	}

	/**
	 * Lists users.
	 *
	 * Display WordPress users based on all arguments supported by
	 * [WP_User_Query()](https://developer.wordpress.org/reference/classes/wp_user_query/prepare_query/).
	 *
	 * ## OPTIONS
	 *
	 * [--role=<role>]
	 * : Only display users with a certain role.
	 *
	 * [--<field>=<value>]
	 * : Control output by one or more arguments of WP_User_Query().
	 *
	 * [--network]
	 * : List all users in the network for multisite.
	 *
	 * [--field=<field>]
	 * : Prints the value of a single field for each user.
	 *
	 * [--fields=<fields>]
	 * : Limit the output to specific object fields.
	 *
	 * [--format=<format>]
	 * : Render output in a particular format.
	 * ---
	 * default: table
	 * options:
	 *   - table
	 *   - csv
	 *   - ids
	 *   - json
	 *   - count
	 *   - yaml
	 * ---
	 *
	 * ## AVAILABLE FIELDS
	 *
	 * These fields will be displayed by default for each user:
	 *
	 * * ID
	 * * user_login
	 * * display_name
	 * * user_email
	 * * user_registered
	 * * roles
	 *
	 * These fields are optionally available:
	 *
	 * * user_pass
	 * * user_nicename
	 * * user_url
	 * * user_activation_key
	 * * user_status
	 * * spam
	 * * deleted
	 * * caps
	 * * cap_key
	 * * allcaps
	 * * filter
	 * * url
	 *
	 * ## EXAMPLES
	 *
	 *     # List user IDs
	 *     $ wp user list --field=ID
	 *     1
	 *
	 *     # List users with administrator role
	 *     $ wp user list --role=administrator --format=csv
	 *     ID,user_login,display_name,user_email,user_registered,roles
	 *     1,supervisor,supervisor,supervisor@gmail.com,"2016-06-03 04:37:00",administrator
	 *
	 *     # List users with only given fields
	 *     $ wp user list --fields=display_name,user_email --format=json
	 *     [{"display_name":"supervisor","user_email":"supervisor@gmail.com"}]
	 *
	 *     # List users ordered by the 'last_activity' meta value.
	 *     $ wp user list --meta_key=last_activity --orderby=meta_value_num
	 *
	 * @subcommand list
	 */
	public function list_( $args, $assoc_args ) {

		if ( Utils\get_flag_value( $assoc_args, 'network' ) ) {
			if ( ! is_multisite() ) {
				WP_CLI::error( 'This is not a multisite installation.' );
			}
			$assoc_args['blog_id'] = 0;
			if ( isset( $assoc_args['fields'] ) ) {
				$fields               = explode( ',', $assoc_args['fields'] );
				$assoc_args['fields'] = array_diff( $fields, [ 'roles' ] );
			} else {
				$assoc_args['fields'] = array_diff( $this->obj_fields, [ 'roles' ] );
			}
		}

		$formatter = $this->get_formatter( $assoc_args );

		if ( in_array( $formatter->format, [ 'ids', 'count' ], true ) ) {
			$assoc_args['fields'] = 'ids';
		} else {
			$assoc_args['fields'] = 'all_with_meta';
		}

		$assoc_args['count_total'] = false;
		$assoc_args                = self::process_csv_arguments_to_arrays( $assoc_args );

		if ( ! empty( $assoc_args['role'] ) && 'none' === $assoc_args['role'] ) {
			$norole_user_ids = wp_get_users_with_no_role();

			if ( ! empty( $norole_user_ids ) ) {
				$assoc_args['include'] = $norole_user_ids;
				unset( $assoc_args['role'] );
			}
		}

		$users = get_users( $assoc_args );

		if ( 'ids' === $formatter->format ) {
			echo implode( ' ', $users );
		} elseif ( 'count' === $formatter->format ) {
			$formatter->display_items( $users );
		} else {
			$iterator = Utils\iterator_map(
				$users,
				function ( $user ) {
					if ( ! is_object( $user ) ) {
						return $user;
					}

					$user->roles = implode( ',', $user->roles );
					$user->url   = get_author_posts_url( $user->ID, $user->user_nicename );
					return $user;
				}
			);

			$formatter->display_items( $iterator );
		}
	}

	/**
	 * Gets details about a user.
	 *
	 * ## OPTIONS
	 *
	 * <user>
	 * : User ID, user email, or user login.
	 *
	 * [--field=<field>]
	 * : Instead of returning the whole user, returns the value of a single field.
	 *
	 * [--fields=<fields>]
	 * : Get a specific subset of the user's fields.
	 *
	 * [--format=<format>]
	 * : Render output in a particular format.
	 * ---
	 * default: table
	 * options:
	 *   - table
	 *   - csv
	 *   - json
	 *   - yaml
	 * ---
	 *
	 * ## EXAMPLES
	 *
	 *     # Get user
	 *     $ wp user get 12 --field=login
	 *     supervisor
	 *
	 *     # Get user and export to JSON file
	 *     $ wp user get bob --format=json > bob.json
	 */
	public function get( $args, $assoc_args ) {
		$user               = $this->fetcher->get_check( $args[0] );
		$user_data          = $user->to_array();
		$user_data['roles'] = implode( ', ', $user->roles );

		$formatter = $this->get_formatter( $assoc_args );
		$formatter->display_item( $user_data );
	}

	/**
	 * Deletes one or more users from the current site.
	 *
	 * On multisite, `wp user delete` only removes the user from the current
	 * site. Include `--network` to also remove the user from the database, but
	 * make sure to reassign their posts prior to deleting the user.
	 *
	 * ## OPTIONS
	 *
	 * <user>...
	 * : The user login, user email, or user ID of the user(s) to delete.
	 *
	 * [--network]
	 * : On multisite, delete the user from the entire network.
	 *
	 * [--reassign=<user-id>]
	 * : User ID to reassign the posts to.
	 *
	 * [--yes]
	 * : Answer yes to any confirmation prompts.
	 *
	 * ## EXAMPLES
	 *
	 *     # Delete user 123 and reassign posts to user 567
	 *     $ wp user delete 123 --reassign=567
	 *     Success: Removed user 123 from http://example.com.
	 *
	 *     # Delete all contributors and reassign their posts to user 2
	 *     $ wp user delete $(wp user list --role=contributor --field=ID) --reassign=2
	 *     Success: Removed user 813 from http://example.com.
	 *     Success: Removed user 578 from http://example.com.
	 *
	 *     # Delete all contributors in batches of 100 (avoid error: argument list too long: wp)
	 *     $ wp user delete $(wp user list --role=contributor --field=ID | head -n 100)
	 */
	public function delete( $args, $assoc_args ) {
		$network  = Utils\get_flag_value( $assoc_args, 'network' ) && is_multisite();
		$reassign = Utils\get_flag_value( $assoc_args, 'reassign' );

		if ( $network && $reassign ) {
			WP_CLI::error( 'Reassigning content to a different user is not supported on multisite.' );
		}

		$is_reassign_valid = ( $reassign && false === get_userdata( $reassign ) ) ? false : true;

		if ( ! $reassign ) {
			WP_CLI::confirm( '--reassign parameter not passed. All associated posts will be deleted. Proceed?', $assoc_args );
		} elseif ( ! $is_reassign_valid ) {
			WP_CLI::confirm( '--reassign parameter is invalid. All associated posts will be deleted. Proceed?', $assoc_args );
		}

		$users = $this->fetcher->get_many( $args );

		parent::_delete(
			$users,
			$assoc_args,
			function ( $user ) use ( $network, $reassign ) {
				$user_id = $user->ID;

				if ( $network ) {
					$result  = wpmu_delete_user( $user_id );
					$message = "Deleted user {$user_id}.";
				} elseif ( is_multisite() && empty( $user->roles ) ) {
					$message = "No roles found for user {$user_id} on " . home_url() . ', no users deleted.';
					return [ 'error', $message ];
				} else {
					$result  = wp_delete_user( $user_id, $reassign );
					$message = "Removed user {$user_id} from " . home_url() . '.';
				}

				if ( ! $result ) {
					$message = "Failed deleting user {$user_id}.";

					if ( is_multisite() && is_super_admin( $user_id ) ) {
						$message .= ' The user is a super admin.';
					}

					return [ 'error', $message ];
				}

				return [ 'success', $message ];
			}
		);
	}

	/**
	 * Creates a new user.
	 *
	 * ## OPTIONS
	 *
	 * <user-login>
	 * : The login of the user to create.
	 *
	 * <user-email>
	 * : The email address of the user to create.
	 *
	 * [--role=<role>]
	 * : The role of the user to create. Default: default role. Possible values
	 * include 'administrator', 'editor', 'author', 'contributor', 'subscriber'.
	 *
	 * [--user_pass=<password>]
	 * : The user password. Default: randomly generated.
	 *
	 * [--user_registered=<yyyy-mm-dd-hh-ii-ss>]
	 * : The date the user registered. Default: current date.
	 *
	 * [--display_name=<name>]
	 * : The display name.
	 *
	 * [--user_nicename=<nice_name>]
	 * : A string that contains a URL-friendly name for the user. The default is the user's username.
	 *
	 * [--user_url=<url>]
	 * : A string containing the user's URL for the user's web site.
	 *
	 * [--nickname=<nickname>]
	 * : The user's nickname, defaults to the user's username.
	 *
	 * [--first_name=<first_name>]
	 * : The user's first name.
	 *
	 * [--last_name=<last_name>]
	 * : The user's last name.
	 *
	 * [--description=<description>]
	 * : A string containing content about the user.
	 *
	 * [--rich_editing=<rich_editing>]
	 * : A string for whether to enable the rich editor or not. False if not empty.
	 *
	 * [--send-email]
	 * : Send an email to the user with their new account details.
	 *
	 * [--porcelain]
	 * : Output just the new user id.
	 *
	 * ## EXAMPLES
	 *
	 *     # Create user
	 *     $ wp user create bob bob@example.com --role=author
	 *     Success: Created user 3.
	 *     Password: k9**&I4vNH(&
	 *
	 *     # Create user without showing password upon success
	 *     $ wp user create ann ann@example.com --porcelain
	 *     4
	 */
	public function create( $args, $assoc_args ) {
		$user = new stdClass();

		list( $user->user_login, $user->user_email ) = $args;

		$assoc_args = wp_slash( $assoc_args );

		if ( username_exists( $user->user_login ) ) {
			WP_CLI::error( "The '{$user->user_login}' username is already registered." );
		}

		if ( ! is_email( $user->user_email ) ) {
			WP_CLI::error( "The '{$user->user_email}' email address is invalid." );
		}

		$user->user_registered = Utils\get_flag_value(
			$assoc_args,
			'user_registered',
			current_time( 'mysql', true )
		);

		$user->display_name = Utils\get_flag_value( $assoc_args, 'display_name', false );

		$user->nickname = Utils\get_flag_value( $assoc_args, 'nickname', false );

		$user->first_name = Utils\get_flag_value( $assoc_args, 'first_name', false );

		$user->last_name = Utils\get_flag_value( $assoc_args, 'last_name', false );

		$user->description = Utils\get_flag_value( $assoc_args, 'description', false );

		$user->user_url = Utils\get_flag_value( $assoc_args, 'user_url', false );

		if ( isset( $assoc_args['user_pass'] ) ) {
			$user->user_pass = $assoc_args['user_pass'];
		} else {
			$user->user_pass = wp_generate_password( 24 );
			$generated_pass  = true;
		}

		$user->role = Utils\get_flag_value( $assoc_args, 'role', get_option( 'default_role' ) );
		self::validate_role( $user->role );

		if ( ! Utils\get_flag_value( $assoc_args, 'send-email' ) ) {
			add_filter( 'send_password_change_email', '__return_false' );
			add_filter( 'send_email_change_email', '__return_false' );
		}

		if ( is_multisite() ) {
			$result = wpmu_validate_user_signup( $user->user_login, $user->user_email );
			if ( is_wp_error( $result['errors'] ) && ! empty( $result['errors']->errors ) ) {
				WP_CLI::error( $result['errors'] );
			}
			$user_id = wpmu_create_user( $user->user_login, $user->user_pass, $user->user_email );
			if ( ! $user_id ) {
				WP_CLI::error( 'Unknown error creating new user.' );
			}
			$user->ID = $user_id;
			$user_id  = wp_update_user( $user );
			if ( is_wp_error( $user_id ) ) {
				WP_CLI::error( $user_id );
			}
		} else {
			$user_id = wp_insert_user( $user );
		}

		if ( ! $user_id || is_wp_error( $user_id ) ) {
			if ( ! $user_id ) {
				$user_id = 'Unknown error creating new user.';
			}
			WP_CLI::error( $user_id );
		} elseif ( false === $user->role ) {
				delete_user_option( $user_id, 'capabilities' );
				delete_user_option( $user_id, 'user_level' );
		}

		if ( Utils\get_flag_value( $assoc_args, 'send-email' ) ) {
			self::wp_new_user_notification( $user_id, $user->user_pass );
		}

		if ( Utils\get_flag_value( $assoc_args, 'porcelain' ) ) {
			WP_CLI::line( $user_id );
		} else {
			WP_CLI::success( "Created user {$user_id}." );
			if ( isset( $generated_pass ) ) {
				WP_CLI::line( "Password: {$user->user_pass}" );
			}
		}
	}

	/**
	 * Updates an existing user.
	 *
	 * ## OPTIONS
	 *
	 * <user>...
	 * : The user login, user email or user ID of the user(s) to update.
	 *
	 * [--user_pass=<password>]
	 * : A string that contains the plain text password for the user.
	 *
	 * [--user_nicename=<nice_name>]
	 * : A string that contains a URL-friendly name for the user. The default is the user's username.
	 *
	 * [--user_url=<url>]
	 * : A string containing the user's URL for the user's web site.
	 *
	 * [--user_email=<email>]
	 * : A string containing the user's email address.
	 *
	 * [--display_name=<display_name>]
	 * : A string that will be shown on the site. Defaults to user's username.
	 *
	 * [--nickname=<nickname>]
	 * : The user's nickname, defaults to the user's username.
	 *
	 * [--first_name=<first_name>]
	 * : The user's first name.
	 *
	 * [--last_name=<last_name>]
	 * : The user's last name.
	 *
	 * [--description=<description>]
	 * : A string containing content about the user.
	 *
	 * [--rich_editing=<rich_editing>]
	 * : A string for whether to enable the rich editor or not. False if not empty.
	 *
	 * [--user_registered=<yyyy-mm-dd-hh-ii-ss>]
	 * : The date the user registered.
	 *
	 * [--role=<role>]
	 * : A string used to set the user's role.
	 *
	 * --<field>=<value>
	 * : One or more fields to update. For accepted fields, see wp_update_user().
	 *
	 * [--skip-email]
	 * : Don't send an email notification to the user.
	 *
	 * ## EXAMPLES
	 *
	 *     # Update user
	 *     $ wp user update 123 --display_name=Mary --user_pass=marypass
	 *     Success: Updated user 123.
	 */
	public function update( $args, $assoc_args ) {
		if ( isset( $assoc_args['user_login'] ) ) {
			WP_CLI::warning( "User logins can't be changed." );
			unset( $assoc_args['user_login'] );
		}

		if ( isset( $assoc_args['role'] ) ) {
			self::validate_role( $assoc_args['role'], true );
		}

		$user_ids = [];
		foreach ( $this->fetcher->get_many( $args ) as $user ) {
			$user_ids[] = $user->ID;
		}

		if ( empty( $user_ids ) ) {
			WP_CLI::error( 'No valid users found.' );
		}

		$skip_email = Utils\get_flag_value( $assoc_args, 'skip-email' );
		if ( $skip_email ) {
			add_filter( 'send_email_change_email', '__return_false' );
			add_filter( 'send_password_change_email', '__return_false' );
		}

		$assoc_args = wp_slash( $assoc_args );
		parent::_update( $user_ids, $assoc_args, 'wp_update_user' );

		if ( $skip_email ) {
			remove_filter( 'send_email_change_email', '__return_false' );
			remove_filter( 'send_password_change_email', '__return_false' );
		}
	}

	/**
	 * Generates some users.
	 *
	 * Creates a specified number of new users with dummy data.
	 *
	 * ## OPTIONS
	 *
	 * [--count=<number>]
	 * : How many users to generate?
	 * ---
	 * default: 100
	 * ---
	 *
	 * [--role=<role>]
	 * : The role of the generated users. Default: default role from WP
	 *
	 * [--format=<format>]
	 * : Render output in a particular format.
	 * ---
	 * default: progress
	 * options:
	 *   - progress
	 *   - ids
	 * ---
	 *
	 * ## EXAMPLES
	 *
	 *     # Add meta to every generated users.
	 *     $ wp user generate --format=ids --count=3 | xargs -d ' ' -I % wp user meta add % foo bar
	 *     Success: Added custom field.
	 *     Success: Added custom field.
	 *     Success: Added custom field.
	 */
	public function generate( $args, $assoc_args ) {
		global $blog_id;

		$defaults   = [
			'count' => 100,
			'role'  => get_option( 'default_role' ),
		];
		$assoc_args = array_merge( $defaults, $assoc_args );

		$role = $assoc_args['role'];

		if ( ! empty( $role ) ) {
			self::validate_role( $role );
		}

		$user_count = count_users();
		$total      = $user_count['total_users'];
		$limit      = $assoc_args['count'] + $total;

		$format = Utils\get_flag_value( $assoc_args, 'format', 'progress' );

		$notify = false;
		if ( 'progress' === $format ) {
			$notify = Utils\make_progress_bar( 'Generating users', $assoc_args['count'] );
		}

		for ( $index = $total; $index < $limit; $index++ ) {
			$login = "user_{$blog_id}_{$index}";
			$name  = "User {$index}";

			$user_id = wp_insert_user(
				[
					'user_login'   => $login,
					'user_pass'    => $login,
					'nickname'     => $name,
					'display_name' => $name,
					'role'         => $role,
				]
			);

			if ( false === $role ) {
				delete_user_option( $user_id, 'capabilities' );
				delete_user_option( $user_id, 'user_level' );
			}

			if ( 'progress' === $format ) {
				$notify->tick();
			} elseif ( 'ids' === $format ) {
				echo $user_id;
				if ( $index < $limit - 1 ) {
					echo ' ';
				}
			}
		}

		if ( 'progress' === $format ) {
			$notify->finish();
		}
	}

	/**
	 * Verifies whether a user exists.
	 *
	 * Displays a success message if the user does exist.
	 *
	 * ## OPTIONS
	 *
	 * <id>
	 * : The ID of the user to check.
	 *
	 * ## EXAMPLES
	 *
	 *     # The user exists.
	 *     $ wp user exists 1337
	 *     Success: User with ID 1337 exists.
	 *     $ echo $?
	 *     0
	 *
	 *     # The user does not exist.
	 *     $ wp user exists 10000
	 *     $ echo $?
	 *     1
	 */
	public function exists( $args ) {
		if ( $this->fetcher->get( $args[0] ) ) {
			WP_CLI::success( "User with ID {$args[0]} exists." );
		} else {
			WP_CLI::halt( 1 );
		}
	}

	/**
	 * Sets the user role.
	 *
	 * ## OPTIONS
	 *
	 * <user>
	 * : User ID, user email, or user login.
	 *
	 * [<role>]
	 * : Make the user have the specified role. If not passed, the default role is
	 * used.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp user set-role 12 author
	 *     Success: Added johndoe (12) to http://example.com as author.
	 *
	 * @subcommand set-role
	 */
	public function set_role( $args, $assoc_args ) {
		$user = $this->fetcher->get_check( $args[0] );

		$role = Utils\get_flag_value( $args, 1, get_option( 'default_role' ) );

		self::validate_role( $role );

		// Multisite
		if ( function_exists( 'add_user_to_blog' ) ) {
			add_user_to_blog( get_current_blog_id(), $user->ID, $role );
		} else {
			$user->set_role( $role );
		}

		WP_CLI::success( "Added {$user->user_login} ({$user->ID}) to " . site_url() . " as {$role}." );
	}

	/**
	 * Adds a role for a user.
	 *
	 * ## OPTIONS
	 *
	 * <user>
	 * : User ID, user email, or user login.
	 *
	 * [<role>...]
	 * : Add the specified role(s) to the user.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp user add-role 12 author
	 *     Success: Added 'author' role for johndoe (12).
	 *
	 *     $ wp user add-role 12 author editor
	 *     Success: Added 'author', 'editor' roles for johndoe (12).
	 *
	 * @subcommand add-role
	 */
	public function add_role( $args, $assoc_args ) {
		$user = $this->fetcher->get_check( $args[0] );

		$roles = $args;
		array_shift( $roles );

		if ( empty( $roles ) ) {
			WP_CLI::error( 'Please specify at least one role to add.' );
		}

		foreach ( $roles as $role ) {
			self::validate_role( $role );
		}

		foreach ( $roles as $role ) {
			$user->add_role( $role );
		}
		$message = implode( "', '", $roles );
		$label   = count( $roles ) > 1 ? 'roles' : 'role';
		WP_CLI::success( "Added '{$message}' {$label} for {$user->user_login} ({$user->ID})." );
	}

	/**
	 * Removes a user's role.
	 *
	 * ## OPTIONS
	 *
	 * <user>
	 * : User ID, user email, or user login.
	 *
	 * [<role>...]
	 * : Remove the specified role(s) from the user.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp user remove-role 12 author
	 *     Success: Removed 'author' role for johndoe (12).
	 *
	 *     $ wp user remove-role 12 author editor
	 *     Success: Removed 'author', 'editor' roles for johndoe (12).
	 *
	 * @subcommand remove-role
	 */
	public function remove_role( $args, $assoc_args ) {
		$user = $this->fetcher->get_check( $args[0] );

		if ( isset( $args[1] ) ) {
			$roles = $args;
			array_shift( $roles );

			foreach ( $roles as $role ) {
				self::validate_role( $role );
			}

			foreach ( $roles as $role ) {
				$user->remove_role( $role );
			}
			$message = implode( "', '", $roles );
			$label   = count( $roles ) > 1 ? 'roles' : 'role';
			WP_CLI::success( "Removed '{$message}' {$label} from {$user->user_login} ({$user->ID})." );
		} else {
			// Multisite
			if ( function_exists( 'remove_user_from_blog' ) ) {
				remove_user_from_blog( $user->ID, get_current_blog_id() );
			} else {
				$user->remove_all_caps();
			}

			WP_CLI::success( "Removed {$user->user_login} ({$user->ID}) from " . site_url() . '.' );
		}
	}

	/**
	 * Adds a capability to a user.
	 *
	 * ## OPTIONS
	 *
	 * <user>
	 * : User ID, user email, or user login.
	 *
	 * <cap>
	 * : The capability to add.
	 *
	 * ## EXAMPLES
	 *
	 *     # Add a capability for a user
	 *     $ wp user add-cap john create_premium_item
	 *     Success: Added 'create_premium_item' capability for john (16).
	 *
	 *     # Add a capability for a user
	 *     $ wp user add-cap 15 edit_product
	 *     Success: Added 'edit_product' capability for johndoe (15).
	 *
	 * @subcommand add-cap
	 */
	public function add_cap( $args, $assoc_args ) {
		$user = $this->fetcher->get_check( $args[0] );
		if ( $user ) {
			$cap = $args[1];
			$user->add_cap( $cap );

			WP_CLI::success( "Added '{$cap}' capability for {$user->user_login} ({$user->ID})." );
		}
	}

	/**
	 * Removes a user's capability.
	 *
	 * ## OPTIONS
	 *
	 * <user>
	 * : User ID, user email, or user login.
	 *
	 * <cap>
	 * : The capability to be removed.
	 *
	 * [--force]
	 * : Forcefully remove a capability.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp user remove-cap 11 publish_newsletters
	 *     Success: Removed 'publish_newsletters' cap for supervisor (11).
	 *
	 *     $ wp user remove-cap 11 publish_posts
	 *     Error: The 'publish_posts' cap for supervisor (11) is inherited from a role.
	 *
	 *     $ wp user remove-cap 11 nonexistent_cap
	 *     Error: No such 'nonexistent_cap' cap for supervisor (11).
	 *
	 *     $ wp user remove-cap 11 publish_newsletters --force
	 *     Success: Removed 'publish_newsletters' cap for supervisor (11).
	 *
	 * @subcommand remove-cap
	 */
	public function remove_cap( $args, $assoc_args ) {
		$user = $this->fetcher->get_check( $args[0] );
		if ( $user ) {
			$cap = $args[1];
			if ( ! isset( $user->caps[ $cap ] ) ) {
				if ( isset( $user->allcaps[ $cap ] ) ) {
					WP_CLI::error( "The '{$cap}' cap for {$user->user_login} ({$user->ID}) is inherited from a role." );
				}
				WP_CLI::error( "No such '{$cap}' cap for {$user->user_login} ({$user->ID})." );
			}

			$user_roles = $user->roles;
			if ( ! empty( $user_roles ) && in_array( $cap, $user_roles, true ) && ! Utils\get_flag_value( $assoc_args, 'force' ) ) {
				WP_CLI::error( "Aborting because a role has the same name as '{$cap}'. Use `wp user remove-cap {$user->ID} {$cap} --force` to proceed with the removal." );
			}
			$user->remove_cap( $cap );

			WP_CLI::success( "Removed '{$cap}' cap for {$user->user_login} ({$user->ID})." );
		}
	}

	/**
	 * Lists all capabilities for a user.
	 *
	 * ## OPTIONS
	 *
	 * <user>
	 * : User ID, user email, or login.
	 *
	 * [--format=<format>]
	 * : Render output in a particular format.
	 * ---
	 * default: list
	 * options:
	 *   - list
	 *   - table
	 *   - csv
	 *   - json
	 *   - count
	 *   - yaml
	 * ---
	 *
	 * [--origin=<origin>]
	 * : Render output in a particular format.
	 * ---
	 * default: all
	 * options:
	 *   - all
	 *   - user
	 *   - role
	 * ---
	 *
	 * [--exclude-role-names]
	 * : Exclude capabilities that match role names from output.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp user list-caps 21
	 *     edit_product
	 *     create_premium_item
	 *
	 * @subcommand list-caps
	 */
	public function list_caps( $args, $assoc_args ) {
		$user               = $this->fetcher->get_check( $args[0] );
		$exclude_role_names = Utils\get_flag_value( $assoc_args, 'exclude-role-names' );

		$active_user_cap_list = [];

		$user_roles = $user->roles;

		switch ( $assoc_args['origin'] ) {
			case 'all':
				$user->get_role_caps();
				$user_caps_list = $user->allcaps;

				foreach ( $user_caps_list as $capability => $active ) {
					if ( $exclude_role_names && in_array( $capability, $user_roles, true ) ) {
						continue;
					}
					if ( $active ) {
						$active_user_cap_list[] = $capability;
					}
				}
				break;
			case 'user':
				// Get the user's capabilities
				$user_capabilities = get_user_meta( $user->ID, 'wp_capabilities', true );

				// Loop through each capability and only return the non-inherited ones
				foreach ( $user_capabilities as $capability => $active ) {
					if ( true === $active && ! in_array( $capability, $user_roles, true ) ) {
						$active_user_cap_list[] = $capability;
					}
				}
				break;
			case 'role':
				// Get the user's capabilities
				$user_capabilities = get_user_meta( $user->ID, 'wp_capabilities', true );

				// Loop through each capability and only return the inherited ones (including the role name)
				foreach ( $user->get_role_caps() as $capability => $active ) {
					if ( true === $active && ! isset( $user_capabilities[ $capability ] ) ) {
						$active_user_cap_list[] = $capability;
					}
					if ( true === $active && ! $exclude_role_names && in_array( $capability, $user_roles, true ) ) {
						$active_user_cap_list[] = $capability;
					}
				}
				break;
		}

		if ( 'list' === $assoc_args['format'] ) {
			foreach ( $active_user_cap_list as $cap ) {
				WP_CLI::line( $cap );
			}
		} else {
			$output_caps = [];
			foreach ( $active_user_cap_list as $cap ) {
				$output_cap = new stdClass();

				$output_cap->name = $cap;

				$output_caps[] = $output_cap;
			}
			$formatter = new Formatter( $assoc_args, $this->cap_fields );
			$formatter->display_items( $output_caps );
		}
	}

	/**
	 * Imports users from a CSV file.
	 *
	 * If the user already exists (matching the email address or login), then
	 * the user is updated unless the `--skip-update` flag is used.
	 *
	 * ## OPTIONS
	 *
	 * <file>
	 * : The local or remote CSV file of users to import. If '-', then reads from STDIN.
	 *
	 * [--send-email]
	 * : Send an email to new users with their account details.
	 *
	 * [--skip-update]
	 * : Don't update users that already exist.
	 *
	 * ## EXAMPLES
	 *
	 *     # Import users from local CSV file
	 *     $ wp user import-csv /path/to/users.csv
	 *     Success: bobjones created.
	 *     Success: newuser1 created.
	 *     Success: existinguser created.
	 *
	 *     # Import users from remote CSV file
	 *     $ wp user import-csv http://example.com/users.csv
	 *
	 *     Sample users.csv file:
	 *
	 *     user_login,user_email,display_name,role
	 *     bobjones,bobjones@example.com,Bob Jones,contributor
	 *     newuser1,newuser1@example.com,New User,author
	 *     existinguser,existinguser@example.com,Existing User,administrator
	 *
	 * @subcommand import-csv
	 */
	public function import_csv( $args, $assoc_args ) {

		$blog_users = get_users();

		$filename = $args[0];

		if ( 0 === stripos( $filename, 'http://' ) || 0 === stripos( $filename, 'https://' ) ) {
			$response      = wp_remote_head( $filename );
			$response_code = (string) wp_remote_retrieve_response_code( $response );
			if ( in_array( (int) $response_code[0], [ 4, 5 ], true ) ) {
				WP_CLI::error( "Couldn't access remote CSV file (HTTP {$response_code} response)." );
			}
		} elseif ( '-' === $filename ) {
			if ( ! Utils\has_stdin() ) {
				WP_CLI::error( 'Unable to read content from STDIN.' );
			}
		} elseif ( ! file_exists( $filename ) ) {
			WP_CLI::error( "Missing file: {$filename}" );
		}

		// Don't send core's emails during the creation / update process
		add_filter( 'send_password_change_email', '__return_false' );
		add_filter( 'send_email_change_email', '__return_false' );

		if ( '-' === $filename ) {
			$file_object = new NoRewindIterator( new SplFileObject( 'php://stdin' ) );
			$file_object->setFlags( SplFileObject::READ_CSV );
			$csv_data = [];
			$indexes  = [];
			foreach ( $file_object as $line ) {
				if ( empty( $line[0] ) ) {
					continue;
				}

				if ( empty( $indexes ) ) {
					$indexes = $line;
					continue;
				}

				foreach ( $indexes as $n => $key ) {
					$data[ $key ] = $line[ $n ];
				}

				$csv_data[] = $data;
			}
		} else {
			$csv_data = new CsvIterator( $filename );
		}

		foreach ( $csv_data as $new_user ) {
			$defaults = [
				'role'            => get_option( 'default_role' ),
				'user_pass'       => wp_generate_password( 24 ),
				'user_registered' => current_time( 'mysql', true ),
				'display_name'    => false,
			];
			$new_user = array_merge( $defaults, $new_user );

			$secondary_roles = [];
			if ( ! empty( $new_user['roles'] ) ) {
				$roles        = array_map( 'trim', explode( ',', $new_user['roles'] ) );
				$invalid_role = false;
				foreach ( $roles as $role ) {
					if ( null === get_role( $role ) ) {
						WP_CLI::warning( "{$new_user['user_login']} has an invalid role." );
						$invalid_role = true;
						break;
					}
				}
				if ( $invalid_role ) {
					continue;
				}
				$new_user['role'] = array_shift( $roles );
				$secondary_roles  = $roles;
			} elseif ( 'none' === $new_user['role'] ) {
				$new_user['role'] = false;
			} elseif ( null === get_role( $new_user['role'] ) ) {
				WP_CLI::warning( "{$new_user['user_login']} has an invalid role." );
				continue;
			}

			// User already exists and we just need to add them to the site if they aren't already there
			$existing_user = get_user_by( 'email', $new_user['user_email'] );

			if ( ! $existing_user ) {
				$existing_user = get_user_by( 'login', $new_user['user_login'] );
			}

			if ( $existing_user && Utils\get_flag_value( $assoc_args, 'skip-update' ) ) {

				WP_CLI::log( "{$existing_user->user_login} exists and has been skipped." );
				continue;

			}

			if ( $existing_user ) {
				$new_user['ID'] = $existing_user->ID;
				$user_id        = wp_update_user( $new_user );

				if ( ! in_array( $existing_user->user_login, wp_list_pluck( $blog_users, 'user_login' ), true ) && is_multisite() && $new_user['role'] ) {
					add_user_to_blog( get_current_blog_id(), $existing_user->ID, $new_user['role'] );
					WP_CLI::log( "{$existing_user->user_login} added as {$new_user['role']}." );
				}

				// Create the user
			} else {
				unset( $new_user['ID'] ); // Unset else it will just return the ID

				if ( is_multisite() ) {
					$result = wpmu_validate_user_signup( $new_user['user_login'], $new_user['user_email'] );
					if ( is_wp_error( $result['errors'] ) && ! empty( $result['errors']->errors ) ) {
						WP_CLI::warning( $result['errors'] );
						continue;
					}
					$user_id = wpmu_create_user( $new_user['user_login'], $new_user['user_pass'], $new_user['user_email'] );
					if ( ! $user_id ) {
						WP_CLI::warning( 'Unknown error creating new user.' );
						continue;
					}
					$new_user['ID'] = $user_id;
					$user_id        = wp_update_user( $new_user );
					if ( is_wp_error( $user_id ) ) {
						WP_CLI::warning( $user_id );
						continue;
					}
				} else {
					$user_id = wp_insert_user( $new_user );
				}

				if ( Utils\get_flag_value( $assoc_args, 'send-email' ) ) {
					self::wp_new_user_notification( $user_id, $new_user['user_pass'] );
				}
			}

			if ( is_wp_error( $user_id ) ) {
				WP_CLI::warning( $user_id );
				continue;

			}

			if ( false === $new_user['role'] ) {
				delete_user_option( $user_id, 'capabilities' );
				delete_user_option( $user_id, 'user_level' );
			}

			$user = get_user_by( 'id', $user_id );
			foreach ( $secondary_roles as $secondary_role ) {
				$user->add_role( $secondary_role );
			}

			if ( ! empty( $existing_user ) ) {
				WP_CLI::success( $new_user['user_login'] . ' updated.' );
			} else {
				WP_CLI::success( $new_user['user_login'] . ' created.' );
			}
		}
	}

	/**
	 * Resets the password for one or more users.
	 *
	 * ## OPTIONS
	 *
	 * <user>...
	 * : one or more user logins or IDs.
	 *
	 * [--skip-email]
	 * : Don't send an email notification to the affected user(s).
	 *
	 * [--show-password]
	 * : Show the new password(s).
	 *
	 * [--porcelain]
	 * : Output only the new password(s).
	 *
	 * ## EXAMPLES
	 *
	 *     # Reset the password for two users and send them the change email.
	 *     $ wp user reset-password admin editor
	 *     Reset password for admin.
	 *     Reset password for editor.
	 *     Success: Passwords reset for 2 users.
	 *
	 *     # Reset and display the password.
	 *     $ wp user reset-password editor --show-password
	 *     Reset password for editor.
	 *     Password: N6hAau0fXZMN#rLCIirdEGOh
	 *     Success: Password reset for 1 user.
	 *
	 *     # Reset the password for one user, displaying only the new password, and not sending the change email.
	 *     $ wp user reset-password admin --skip-email --porcelain
	 *     yV6BP*!d70wg
	 *
	 *     # Reset password for all users.
	 *     $ wp user reset-password $(wp user list --format=ids)
	 *     Reset password for admin.
	 *     Reset password for editor.
	 *     Reset password for subscriber.
	 *     Success: Passwords reset for 3 users.
	 *
	 *     # Reset password for all users with a particular role.
	 *     $ wp user reset-password $(wp user list --format=ids --role=administrator)
	 *     Reset password for admin.
	 *     Success: Password reset for 1 user.
	 *
	 * @subcommand reset-password
	 */
	public function reset_password( $args, $assoc_args ) {
		$porcelain     = Utils\get_flag_value( $assoc_args, 'porcelain' );
		$skip_email    = Utils\get_flag_value( $assoc_args, 'skip-email' );
		$show_new_pass = Utils\get_flag_value( $assoc_args, 'show-password' );

		if ( $skip_email ) {
			add_filter( 'send_password_change_email', '__return_false' );
		}
		$fetcher = new UserFetcher();
		$users   = $fetcher->get_many( $args );
		foreach ( $users as $user ) {
			$new_pass = wp_generate_password( 24 );
			wp_update_user(
				[
					'ID'        => $user->ID,
					'user_pass' => $new_pass,
				]
			);
			if ( $porcelain ) {
				WP_CLI::line( "$new_pass" );
			} else {
				WP_CLI::log( "Reset password for {$user->user_login}." );
				if ( $show_new_pass ) {
					WP_CLI::line( "Password: $new_pass" );
				}
			}
		}

		$reset_user_count = count( $users );

		if ( ! $porcelain ) {
			if ( 1 === $reset_user_count ) {
				WP_CLI::success( "Password reset for {$reset_user_count} user." );
			} elseif ( $reset_user_count > 1 ) {
				WP_CLI::success( "Passwords reset for {$reset_user_count} users." );
			} else {
				WP_CLI::error( 'No user found to reset password.' );
			}
		}
	}

	/**
	 * Checks whether the role is valid
	 *
	 * @param $role string
	 * @param $warn_user_only bool
	 */
	private static function validate_role( $role, $warn_user_only = false ) {

		if ( ! empty( $role ) && null === get_role( $role ) ) {
			if ( $warn_user_only ) {
				WP_CLI::warning( "Role doesn't exist: {$role}" );
			} else {
				WP_CLI::error( "Role doesn't exist: {$role}" );
			}
		}
	}

	/**
	 * Accommodates three different behaviors for wp_new_user_notification()
	 * - 4.3.1 and above: expect second argument to be deprecated
	 * - 4.3: Second argument was repurposed as $notify
	 * - Below 4.3: Send the password in the notification
	 *
	 * @param string $user_id
	 * @param string $password
	 */
	public static function wp_new_user_notification( $user_id, $password ) {
		if ( Utils\wp_version_compare( '4.3.1', '>=' ) ) {
			wp_new_user_notification( $user_id, null, 'both' );
		} elseif ( Utils\wp_version_compare( '4.3', '>=' ) ) {
			// phpcs:ignore WordPress.WP.DeprecatedParameters.Wp_new_user_notificationParam2Found -- Only called in valid conditions.
			wp_new_user_notification( $user_id, 'both' );
		} else {
			// phpcs:ignore WordPress.WP.DeprecatedParameters.Wp_new_user_notificationParam2Found -- Only called in valid conditions.
			wp_new_user_notification( $user_id, $password );
		}
	}

	/**
	 * Marks one or more users as spam on multisite.
	 *
	 * ## OPTIONS
	 *
	 * <user>...
	 * : The user login, user email, or user ID of the user(s) to mark as spam.
	 *
	 * ## EXAMPLES
	 *
	 *     # Mark user as spam.
	 *     $ wp user spam 123
	 *     User 123 marked as spam.
	 *     Success: Spammed 1 of 1 users.
	 */
	public function spam( $args ) {
		$this->update_msuser_status( $args, 'spam', '1' );
	}

	/**
	 * Removes one or more users from spam on multisite.
	 *
	 * ## OPTIONS
	 *
	 * <user>...
	 * : The user login, user email, or user ID of the user(s) to remove from spam.
	 *
	 * ## EXAMPLES
	 *
	 *     # Remove user from spam.
	 *     $ wp user unspam 123
	 *     User 123 removed from spam.
	 *     Success: Unspamed 1 of 1 users.
	 */
	public function unspam( $args ) {
		$this->update_msuser_status( $args, 'spam', '0' );
	}

	/**
	 * Common command for updating user data.
	 */
	private function update_msuser_status( $user_ids, $pref, $value ) {

		// If site is not multisite, then stop execution.
		if ( ! is_multisite() ) {
			WP_CLI::error( 'This is not a multisite installation.' );
		}

		if ( 'spam' === $pref ) {
			$action = (int) $value ? 'marked as spam' : 'removed from spam';
			$verb   = (int) $value ? 'spam' : 'unspam';
		}

		$successes = 0;
		$errors    = 0;
		$users     = $this->fetcher->get_many( $user_ids );
		if ( count( $users ) < count( $user_ids ) ) {
			$errors = count( $user_ids ) - count( $users );
		}

		foreach ( $users as $user ) {
			$user_id = $user->ID;

			// Super admin should not be marked as spam.
			if ( is_super_admin( $user->ID ) ) {
				WP_CLI::warning( "User cannot be modified. The user {$user->ID} is a network administrator." );
				continue;
			}

			// Skip if user is already marked as spam and show warning.
			if ( $value === $user->spam ) {
				WP_CLI::warning( "User {$user_id} already {$action}." );
				continue;
			}

			// Make that user's blog as spam too.
			$blogs = (array) get_blogs_of_user( $user_id, true );
			foreach ( $blogs as $details ) {
				// Only mark site as spam if not main site.
				if ( ! is_main_site( $details->userblog_id, $details->site_id ) ) {
					update_blog_status( $details->userblog_id, $pref, $value );
				}
			}

			if ( Utils\wp_version_compare( '5.3', '<' ) ) {
				// phpcs:ignore WordPress.WP.DeprecatedFunctions.update_user_statusFound -- Fallback for older versions.
				update_user_status( $user_id, $pref, $value );
			} else {
				wp_update_user(
					[
						'ID'  => $user_id,
						$pref => $value,
					]
				);
			}

			WP_CLI::log( "User {$user_id} {$action}." );
			++$successes;
		}

		Utils\report_batch_operation_results( 'user', $verb, count( $user_ids ), $successes, $errors );
	}

	/**
	 * Checks if a user's password is valid or not.
	 *
	 * ## OPTIONS
	 *
	 * <user>
	 * : The user login, user email or user ID of the user to check credentials for.
	 *
	 * <user_pass>
	 * : A string that contains the plain text password for the user.
	 *
	 * [--escape-chars]
	 * : Escape password with `wp_slash()` to mimic the same behavior as `wp-login.php`.
	 *
	 * ## EXAMPLES
	 *
	 *     # Check whether given credentials are valid; exit status 0 if valid, otherwise 1
	 *     $ wp user check-password admin adminpass
	 *     $ echo $?
	 *     1
	 *
	 *     # Bash script for checking whether given credentials are valid or not
	 *     if ! $(wp user check-password admin adminpass); then
	 *      notify-send "Invalid Credentials";
	 *     fi
	 *
	 * @subcommand check-password
	 */
	public function check_password( $args, $assoc_args ) {
		$escape_chars = Utils\get_flag_value( $assoc_args, 'escape-chars', false );

		if ( ! $escape_chars && wp_slash( wp_unslash( $args[1] ) ) !== $args[1] ) {
			WP_CLI::warning( 'Password contains characters that need to be escaped. Please escape them manually or use the `--escape-chars` option.' );
		}

		$user      = $this->fetcher->get_check( $args[0] );
		$user_pass = $escape_chars ? wp_slash( $args[1] ) : $args[1];

		if ( wp_check_password( $user_pass, $user->data->user_pass, $user->ID ) ) {
			WP_CLI::halt( 0 );
		} else {
			WP_CLI::halt( 1 );
		}
	}
}
