JoryHogeveen/view-admin-as

View on GitHub
modules/class-role-defaults.php

Summary

Maintainability
D
2 days
Test Coverage
<?php
/**
 * View Admin As - Role Defaults Module
 *
 * @author  Jory Hogeveen <info@keraweb.nl>
 * @package View_Admin_As
 */

if ( ! defined( 'VIEW_ADMIN_AS_DIR' ) ) {
    die();
}

/**
 * Set default screen settings for roles and apply them on users through various bulk actions.
 *
 * Disable some PHPMD checks for this class.
 * @SuppressWarnings(PHPMD.ExcessiveClassLength)
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
 * @todo Refactor to enable above checks?
 *
 * @author  Jory Hogeveen <info@keraweb.nl>
 * @package View_Admin_As
 * @since   1.4.0
 * @version 1.8.4
 * @uses    \VAA_View_Admin_As_Module Extends class
 */
final class VAA_View_Admin_As_Role_Defaults extends VAA_View_Admin_As_Module
{
    /**
     * The single instance of the class.
     *
     * @since  1.5.0
     * @static
     * @var    \VAA_View_Admin_As_Role_Defaults
     */
    private static $_instance = null;

    /**
     * Module key.
     *
     * @since  1.7.2
     * @var    string
     */
    protected $moduleKey = 'role_defaults';

    /**
     * Option key.
     *
     * @since  1.4.0
     * @var    string
     */
    protected $optionKey = 'vaa_role_defaults';

    /**
     * Array of meta strings that influence the screen settings.
     *
     * @since  1.4.0
     * @see    $meta_default
     * @var    bool[]  Meta key as array key.
     */
    private $meta = array();

    /**
     * Array of default meta strings.
     * %% stands for a wildcard and can be anything.
     *
     * @since  1.4.0
     * @since  1.5.2  Set both values and keys to fix problem with unsetting a key through the filter.
     * @var    bool[]  Meta key as array key.
     */
    private $meta_default = array(
        'admin_color'           => true, // The admin color.
        'rich_editing'          => true, // Enable/Disable rich editing.
        'metaboxhidden_%%'      => true, // Hidden metaboxes.
        'meta-box-order_%%'     => true, // Metabox order and locations.
        'closedpostboxes_%%'    => true, // Hidden post boxes.
        'edit_%%_per_page'      => true, // Amount of items per page in edit pages (overview).
        'manage%%columnshidden' => true, // Hidden columns in overview pages.
        'screen_layout_%%'      => true, // Screen layout (num of columns).
    );

    /**
     * Array of forbidden meta strings.
     * %% gets replaced with the table prefix and added to this array on class construction.
     *
     * @since  1.5.2
     * @var    string[]
     */
    private $meta_forbidden = array(
        'vaa-view-admin-as', // Meta value for this plugin.
        '%%capabilities',    // User capabilities.
        '%%user_level',      // User user level.
        'session_tokens',    // User session tokens.
        'nickname',          // User nickname.
        'first_name',        // User first name.
        'last_name',         // User last name.
        'description',       // User description.
    );

    /**
     * Construct function.
     * Protected to make sure it isn't declared elsewhere.
     *
     * @since   1.4.0
     * @since   1.6.1  `$vaa` param.
     * @access  protected
     * @param   \VAA_View_Admin_As  $vaa  The main VAA object.
     */
    protected function __construct( $vaa ) {
        self::$_instance = $this;
        parent::__construct( $vaa );

        // Add this class to the modules in the main class.
        $this->vaa->register_module( array(
            'id'       => $this->moduleKey,
            'instance' => self::$_instance,
        ) );

        /**
         * Add capabilities for this module.
         *
         * @since  1.6.0
         */
        $this->capabilities = array( 'view_admin_as_role_defaults' );
        $this->add_filter( 'view_admin_as_add_capabilities', array( $this, 'add_capabilities' ) );

        // Load data.
        $this->set_optionData( get_option( $this->get_optionKey() ) );

        /**
         * Checks if the management part of module should be enabled.
         *
         * @since  1.4.0  Validate option data.
         * @since  1.6.0  Also calls `init()`.
         */
        $this->set_enable( (bool) $this->get_optionData( 'enable' ), false );

        $this->init();

        /**
         * Only allow settings for admin users or users with the correct capabilities.
         *
         * @since  1.5.2    Validate custom capability view_admin_as_role_defaults.
         * @since  1.5.2.1  Validate is_super_admin (bug in 1.5.2).
         * @since  1.5.3    Disable for network pages.
         */
        if ( ! is_network_admin() && $this->current_user_can( 'view_admin_as_role_defaults' ) ) {
            $this->add_action( 'vaa_view_admin_as_init', array( $this, 'vaa_init' ) );
            $this->add_filter( 'view_admin_as_handle_ajax_' . $this->moduleKey, array( $this, 'ajax_handler' ), 10, 2 );
            // @since  1.8.2  Filter ajax search return.
            $this->add_filter( 'view_admin_as_ajax_search_users_return_' . $this->moduleKey, array( $this, 'ajax_search_users_return' ), 10, 4 );
        }
    }

    /**
     * Init function for global functions (not user dependent).
     *
     * @since   1.4.0
     * @access  private
     * @global  \wpdb  $wpdb
     * @return  void
     */
    private function init() {
        global $wpdb;
        static $done = false;

        /**
         * Replace %% with the current table prefix and add it to the array of forbidden meta keys.
         *
         * @since 1.5.2
         */
        foreach ( $this->meta_forbidden as $key => $meta_key ) {
            if ( false !== strpos( $meta_key, '%%' ) ) {
                $this->meta_forbidden[] = str_replace( '%%', (string) $wpdb->get_blog_prefix(), $meta_key );
            }
        }

        /**
         * Allow users to overwrite the default meta keys.
         *
         * @since   1.4.0
         * @param   array  $meta  Default metadata.
         * @return  array  $meta
         */
        $this->meta_default = $this->validate_meta( apply_filters( 'view_admin_as_role_defaults_meta', $this->meta_default ) );

        /**
         * Get metakeys optionData and merge it with the default meta.
         *
         * @since  1.6.3
         */
        $this->set_meta( array_merge( $this->meta_default, (array) $this->get_optionData( 'meta' ) ) );

        // Don't go further if this module is disabled or if it already was initialized.
        if ( $done || ! $this->is_enabled() ) {
            return;
        }

        // Setting: Automatically apply defaults to new users.
        if ( $this->get_optionData( 'apply_defaults_on_register' ) ) {
            if ( is_multisite() ) {
                $this->add_action( 'add_user_to_blog', array( $this, 'update_user_with_role_defaults_multisite_register' ), 100, 3 );
            } else {
                $this->add_action( 'user_register', array( $this, 'update_user_with_role_defaults' ), 100, 1 );
            }
        }

        // Setting: Hide the screen options for all users who can't access role defaults.
        if ( $this->get_optionData( 'disable_user_screen_options' ) && ! $this->current_user_can( 'view_admin_as_role_defaults' ) ) {
            $this->add_filter( 'screen_options_show_screen', '__return_false', 100 );
        }

        /**
         * Print script in the admin header.
         * Also handles the lock_meta_boxes setting.
         *
         * @since  1.6.0
         * @since  1.6.2  Move to footer (changed hook).
         */
        $this->add_action( 'admin_print_footer_scripts', array( $this, 'admin_print_footer_scripts' ), 100 );

        $done = true;
    }

    /**
     * init function to store data from the main class and enable functionality based on the current view.
     *
     * @since   1.4.0
     * @access  public
     * @return  void
     */
    public function vaa_init() {

        // Enabling this module can only be done by a super admin.
        if ( VAA_API::is_super_admin() ) {

            // Add adminbar menu items in settings section.
            $this->add_action( 'vaa_admin_bar_modules', array( $this, 'admin_bar_menu_modules' ), 10, 2 );
        }

        // Add adminbar menu items in role section.
        if ( $this->is_enabled() ) {

            // Enable storage of role default settings.
            $this->init_store_role_defaults();

            // Show the admin bar node.
            $this->add_action( 'vaa_admin_bar_menu', array( $this, 'admin_bar_menu' ), 5, 2 );
        }
    }

    /**
     * Print scripts in the admin section.
     *
     * @since   1.6.0
     * @access  public
     */
    public function admin_print_footer_scripts() {

        /**
         * Setting: Lock meta box order and locations for all users who can't access role defaults.
         *
         * @since  1.6.0
         * @since  1.6.2  Improved conditions + check if sortable is enqueued and active.
         */
        if (
            $this->get_optionData( 'lock_meta_boxes' )
            && ! $this->current_user_can( 'view_admin_as_role_defaults' )
            && wp_script_is( 'jquery-ui-sortable', 'enqueued' )
        ) {
            ?>
            <script type="text/javascript">
                jQuery(document).ready( function($) {
                    if ( $.fn.sortable && $('.ui-sortable').length ) {
                        /**
                         * Lock meta boxes in position by disabling sorting.
                         *
                         * Credits - Chris Van Patten:
                         * http://wordpress.stackexchange.com/a/44539
                         */
                        $('.meta-box-sortables').sortable( { disabled: true } );
                        $('.postbox .hndle').css( 'cursor', 'pointer' );
                    }
                });
            </script>
            <?php
        }
    }

    /**
     * Get the metadata for meta compare.
     *
     * @since   1.5.0
     * @access  public
     * @return  bool[]  $this->meta  The meta keys as array keys.
     */
    public function get_meta() {
        return $this->meta;
    }

    /**
     * Set the metadata for meta compare.
     * Used to enforce only 1 level depth array of strings.
     *
     * @since   1.5.0
     * @access  public
     * @param   array   $var  The new meta keys.
     * @return  void
     */
    public function set_meta( $var ) {
        if ( is_array( $var ) ) {
            $this->meta = array_merge( $this->meta_default, $this->validate_meta( $var ) );
            ksort( $this->meta );
        }
    }

    /**
     * Validates meta keys in case forbidden or invalid meta keys are added.
     *
     * @since   1.5.2
     * @access  public
     * @param   array   $metas  The meta keys.
     * @return  string[]  Meta key as array key.
     */
    public function validate_meta( $metas ) {
        if ( is_array( $metas ) ) {
            foreach ( $metas as $meta_key => $meta_value ) {
                // Remove forbidden or invalid meta keys.
                if (
                    in_array( $meta_key, $this->meta_forbidden, true )
                    || strpos( $meta_key, ' ' ) !== false
                    || ! is_string( $meta_key )
                ) {
                    unset( $metas[ $meta_key ] );
                    continue;
                }
                // Validate meta value.
                $metas[ $meta_key ] = (bool) $meta_value;
            }
            return $metas;
        }
        return array();
    }

    /**
     * Data update handler (Ajax probably), called from main handler.
     *
     * Disable some PHPMD checks for this method.
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     * @SuppressWarnings(PHPMD.NPathComplexity)
     * @todo Refactor to enable above checks?
     *
     * @since   1.4.0
     * @access  public
     * @param   null   $null  Null.
     * @param   array  $data  The ajax data for this module.
     * @return  array|string|bool
     */
    public function ajax_handler( $null, $data ) {

        if ( ! $this->is_valid_ajax() ) {
            return $null;
        }

        $success = false;

        // Validate super admin.
        if ( VAA_API::is_super_admin() ) {

            if ( isset( $data['enable'] ) ) {
                $success = $this->set_enable( (bool) $data['enable'] );
                // Prevent further processing.
                return $success;
            }

        }

        // From here all features need this module enabled.
        if ( ! $this->is_enabled() ) {
            return $success;
        }

        /**
         * Simple true/false settings.
         */

        $bool_options = array(
            // @since  1.4.0  Apply defaults to a new users (on register).
            'apply_defaults_on_register',
            // @since  1.5.1  Disable the screen options.
            'disable_user_screen_options',
            // @since  1.6.0  Lock the locations of meta boxes.
            'lock_meta_boxes',
        );

        foreach ( $bool_options as $option ) {
            if ( isset( $data[ $option ] ) ) {
                $success = $this->update_optionData( (bool) $data[ $option ], $option, true );
            }
        }

        // @since  1.6.3  Update metakeys.
        if ( isset( $data['update_meta'] ) ) {
            $value = $this->validate_meta( $data['update_meta'] );
            if ( ! empty( $value ) ) {
                $this->update_optionData( $value, 'meta', true );
                $success = true;
            } else {
                $success = array(
                    'success' => false,
                    'data'    => __( 'Invalid meta key(s)', VIEW_ADMIN_AS_DOMAIN ),
                );
            }
        }

        /**
         * Bulk actions
         */

        // @since  1.4.0  Apply defaults to users.
        if ( VAA_API::array_has( $data, 'apply_defaults_to_users', array( 'validation' => 'is_array' ) ) ) {
            $errors = array();
            foreach ( $data['apply_defaults_to_users'] as $key => $user_data ) {
                // @todo Send as JSON?
                $user_data      = explode( '|', $user_data );
                $errors[ $key ] = false;
                if ( is_numeric( $user_data[0] ) && VAA_API::array_has( $user_data, 1, array( 'validation' => 'is_string' ) ) ) {
                    $success = $this->update_user_with_role_defaults( intval( $user_data[0] ), $user_data[1] );
                    if ( is_string( $success ) ) {
                        // Add error notice.
                        $errors[ $key ] = $success;
                    }
                } else {
                    $errors[ $key ] = esc_attr__( 'No valid data found', VIEW_ADMIN_AS_DOMAIN )
                                      . ': <code>' . implode( '|', $user_data ) . ' (user_id|role)</code>';
                }
            }
            $success = true;
            $errors  = array_unique( array_filter( $errors ) );
            if ( ! empty( $errors ) ) {
                $success = $this->ajax_data_popup(
                    false,
                    array(
                        'text' => esc_attr__( 'There were some errors', VIEW_ADMIN_AS_DOMAIN ) . ':',
                        'list' => $errors,
                    ),
                    'error'
                );
            }
        }

        // @since  1.4.0  Apply defaults to users by role.
        if ( VAA_API::array_has( $data, 'apply_defaults_to_users_by_role', array( 'validation' => 'is_string' ) ) ) {
            // @todo notify of errors in updates.
            $success = $this->apply_defaults_to_users_by_role( wp_strip_all_tags( $data['apply_defaults_to_users_by_role'] ) );
        }

        // @since  1.4.0  Clear defaults for a role.
        if ( VAA_API::array_has( $data, 'clear_role_defaults', array( 'validation' => 'is_string' ) ) ) {
            $success = $this->clear_role_defaults( wp_strip_all_tags( $data['clear_role_defaults'] ) );
        }

        // @since  1.5.0  Export.
        if ( VAA_API::array_has( $data, 'export_role_defaults', array( 'validation' => 'is_string' ) ) ) {
            $content = $this->export_role_defaults( wp_strip_all_tags( $data['export_role_defaults'] ) );
            if ( is_array( $content ) ) {
                $success = $this->ajax_data_popup(
                    true,
                    array(
                        'text'     => esc_attr__( 'Copy code', VIEW_ADMIN_AS_DOMAIN ) . ': ',
                        'textarea' => wp_json_encode( $content ),
                        'filename' => esc_html__( 'Role defaults', VIEW_ADMIN_AS_DOMAIN ) . '.json',
                    )
                );
            } else {
                $success = $this->ajax_data_notice( false, array( 'text' => $content ), 'error' );
            }
        }

        // @since  1.5.0  Import.
        if ( VAA_API::array_has( $data, 'import_role_defaults', array( 'validation' => 'is_array' ) ) ) {
            $success = false;
            if ( ! empty( $data['import_role_defaults']['data'] ) ) {
                $method = ( ! empty( $data['import_role_defaults']['method'] ) ) ? (string) $data['import_role_defaults']['method'] : '';
                // $content format: array( 'text' => **text**, 'errors' => **error array** ).
                $content = $this->import_role_defaults( $data['import_role_defaults']['data'], $method );
                if ( true === $content ) {
                    $success = true;
                } else {
                    $success = $this->ajax_data_popup( false, (array) $content, 'error' );
                }
            }
        }

        // @since  1.7.0  Copy.
        if ( VAA_API::array_has( $data, 'copy_role_defaults', array( 'validation' => 'is_array' ) ) ) {
            if ( isset( $data['copy_role_defaults']['from'] ) && isset( $data['copy_role_defaults']['to'] ) ) {
                $method = ( ! empty( $data['copy_role_defaults']['method'] ) ) ? (string) $data['copy_role_defaults']['method'] : '';
                // $content format: array( 'text' => **text**, 'errors' => **error array** ).
                $content = $this->copy_role_defaults(
                    $data['copy_role_defaults']['from'],
                    $data['copy_role_defaults']['to'],
                    $method
                );
                if ( true === $content ) {
                    $success = true;
                } else {
                    $success = $this->ajax_data_popup( false, (array) $content, 'error' );
                }
            }
        }

        return $success;
    }

    /**
     * Filters the ajax return content for role defaults.
     *
     * @since  1.8.2
     *
     * @param  string                    $return
     * @param  \WP_User[]                $users
     * @return string
     */
    public function ajax_search_users_return( $return, $users ) {
        $content = $this->get_users_bulk_checkbox_html( $users, 'vaa-ajax' );

        if ( $content ) {
            $return = '<div class="ab-item ab-empty-item">' . $content . '</div>';
        }

        return $return;
    }

    /**
     * Update user settings with the a role default.
     * When no role is provided this function only checks the first existing user role. If the user has multiple roles, the other roles are ignored.
     *
     * @since   1.4.0
     * @since   1.8.2  Return error when user or role is not found.
     * @access  public
     * @see     \VAA_View_Admin_As_Role_Defaults::update_user_with_role_defaults_multisite_register()
     * @see     \VAA_View_Admin_As_Role_Defaults::apply_defaults_to_users_by_role()
     * @see     \VAA_View_Admin_As_Role_Defaults::ajax_handler()
     *
     * @see     'user_register' action
     * @link    https://developer.wordpress.org/reference/hooks/user_register/
     *
     * @param   int     $user_id  The user ID.
     * @param   string  $role     (optional) The user role name.
     * @param   int     $blog_id  (optional) The blog ID.
     * @return  bool|string
     */
    public function update_user_with_role_defaults( $user_id, $role = null, $blog_id = null ) {
        $success = true;
        $user    = get_user_by( 'id', $user_id );
        if ( $user ) {
            if ( is_numeric( $blog_id ) ) {
                $option_data = get_blog_option( $blog_id, $this->get_optionKey() );
            } else {
                $option_data = get_option( $this->get_optionKey() );
            }
            // If no role was set, use the first role found for this user.
            if ( ! $role && isset( $user->roles[0] ) ) {
                $role = $user->roles[0];
            }
            if ( ! empty( $option_data['roles'][ $role ] ) ) {
                foreach ( $option_data['roles'][ $role ] as $meta_key => $meta_value ) {
                    update_user_meta( $user_id, $meta_key, $meta_value );
                    // Do not return update_user_meta results since it's highly possible to be false (values are often the same).
                    // @todo check other way of validation
                }
            } else {
                $success = esc_html__( 'No data found for role', VIEW_ADMIN_AS_DOMAIN ) . ': ' . $role;
            }
        } else {
            $success = esc_html__( 'User not found', VIEW_ADMIN_AS_DOMAIN );
        }
        return $success;
    }

    /**
     * In case of a multisite register, check if the user has multiple blogs.
     * If true, it is an existing user and it will not get the role defaults.
     * If false, it is most likely a new user and it will get the role defaults.
     *
     * @since   1.4.0
     * @access  public
     * @see     'add_user_to_blog' action
     * @link    https://developer.wordpress.org/reference/hooks/add_user_to_blog/
     *
     * @param   int     $user_id  The user ID.
     * @param   string  $role     The user role name.
     * @param   int     $blog_id  The blog ID.
     * @return  bool|string
     */
    public function update_user_with_role_defaults_multisite_register( $user_id, $role, $blog_id ) {
        $user_blogs = get_blogs_of_user( $user_id );
        if ( 1 === count( $user_blogs ) ) {
            // If the user has access to one blog only it is safe to set defaults since it is most likely a new user.
            return $this->update_user_with_role_defaults( $user_id, $role, $blog_id );
        }
        return false;
    }

    /**
     * Apply default settings to all users of a role.
     *
     * @since   1.4.0
     * @since   1.7.2  Renamed "all" wildcard to "__all__".
     * @access  private
     * @param   string|string[]  $role  Role name, an array of role names or just "__all__" for all roles.
     * @return  bool
     */
    private function apply_defaults_to_users_by_role( $role ) {
        $success = true;
        $roles   = array();
        if ( '__all__' === $role ) {
            $roles = array_keys( (array) $this->store->get_roles() );
        } else {
            foreach ( (array) $role as $role_name ) {
                if ( array_key_exists( $role_name, $this->store->get_roles() ) ) {
                    $roles[] = $role_name;
                }
            }
        }
        if ( ! empty( $roles ) ) {
            foreach ( $roles as $role ) {
                $users = get_users( array( 'role' => $role ) );
                if ( ! empty( $users ) ) {
                    foreach ( $users as $user ) {
                        $this->update_user_with_role_defaults( $user->ID, $role );
                        // @todo notify of errors in updates.
                    }
                }
            }
        }
        return $success;
    }

    /**
     * Initialize the sync functionality (store defaults).
     * Init function/action to load necessary data and register all used hooks.
     * IMPORTANT! This function should ONLY be used when a role view is selected!
     *
     * @since   1.4.0
     * @access  private
     * @see     \VAA_View_Admin_As_Role_Defaults::vaa_init()
     * @return  void
     */
    private function init_store_role_defaults() {
        if ( $this->store->get_view( 'role' ) && $this->is_enabled() ) {
            $this->add_filter( 'get_user_metadata', array( $this, 'filter_get_user_metadata' ), 10, 4 );
            $this->add_filter( 'update_user_metadata', array( $this, 'filter_update_user_metadata' ), 10, 5 );
            $this->add_filter( 'vaa_admin_bar_title', array( $this, 'vaa_title_recording_role_defaults' ), 999 );
        }
    }

    /**
     * Add a role defaults icon to indicate screen changes are being recorded to role defaults.
     *
     * @since   1.7.2
     * @access  public
     * @param   string  $title  The current title.
     * @return  string
     */
    public function vaa_title_recording_role_defaults( $title ) {
        $role = $this->store->get_view( 'role' );
        if ( ! $role ) {
            return $title;
        }
        $title .= VAA_View_Admin_As_Form::do_icon(
            'dashicons-welcome-view-site',
            array(
                'title' => __( 'Recording screen changes for role defaults', VIEW_ADMIN_AS_DOMAIN )
                           . ': ' . $this->store->get_rolenames( $role ),
                'class' => 'alignright',
            )
        );
        return $title;
    }

    /**
     * Check if the meta_key matches one of the predefined metakeys in the role defaults.
     * If there is a match and the role default value is set, return this value instead of the current user value.
     *
     * IMPORTANT! This filter should ONLY be used when a role view is selected!
     *
     * @since   1.4.0
     * @since   1.5.3   Stop checking `$single` parameter.
     * @since   1.8.4   Only return overwrite if found.
     * @access  public
     * @see     \VAA_View_Admin_As_Role_Defaults::init_store_role_defaults()
     *
     * @see     'get_user_metadata' filter
     * @link    https://codex.wordpress.org/Plugin_API/Filter_Reference/get_(meta_type)_metadata
     * @link    http://hookr.io/filters/get_user_metadata/
     *
     * @param   null    $null       The value update_metadata() should return.
     * @param   int     $object_id  Object ID.
     * @param   string  $meta_key   Meta key.
     * param   bool    $single     (not used) Return a single value or an array?
     * @return  mixed
     */
    public function filter_get_user_metadata( $null, $object_id, $meta_key ) {
        if ( true === $this->compare_metakey( $meta_key ) && (int) $object_id === (int) $this->store->get_curUser()->ID ) {
            $new_meta = $this->get_role_defaults( $this->store->get_view( 'role' ), $meta_key );
            // @todo Maybe define default values?
            if ( null !== $new_meta ) {
                // Do not check `$single`, this logic is in `wp-includes/meta.php` line 487.
                return array( $new_meta );
            }
        }
        return $null; // Go on as normal.
    }

    /**
     * Check if the meta_key matches one of the predefined metakeys to store as defaults.
     * If there is a match, store the update to the defaults and cancel the update for the current user.
     *
     * IMPORTANT! This filter should ONLY be used when a role view is selected!
     *
     * @since   1.4.0
     * @access  public
     * @see     \VAA_View_Admin_As_Role_Defaults::init_store_role_defaults()
     *
     * @see     'update_user_metadata' filter
     * @link    https://codex.wordpress.org/Plugin_API/Filter_Reference/update_(meta_type)_metadata
     * @link    http://hookr.io/filters/update_user_metadata/
     *
     * @param   null    $null        Whether to allow updating metadata for the given type.
     * @param   int     $object_id   Object ID.
     * @param   string  $meta_key    Meta key.
     * @param   string  $meta_value  Meta value.
     * param   string  $prev_value  (not used) Previous meta value.
     * @return  mixed
     */
    public function filter_update_user_metadata( $null, $object_id, $meta_key, $meta_value ) {
        if ( true === $this->compare_metakey( $meta_key ) && (int) $object_id === (int) $this->store->get_curUser()->ID ) {
            $this->update_role_defaults_metadata( $this->store->get_view( 'role' ), $meta_key, $meta_value );
            return false; // Do not update current user meta.
        }
        return $null; // Go on as normal.
    }

    /**
     * Get defaults of a role.
     *
     * @since   1.4.0
     * @since   1.6.3  Multiple get methods (parameters are now optional).
     * @access  public
     *
     * @param   string  $role      (optional) Role name.
     * @param   string  $meta_key  (optional) Meta key.
     * @return  mixed
     */
    public function get_role_defaults( $role = null, $meta_key = null ) {
        $defaults = $this->get_optionData( 'roles' );
        if ( null === $role ) {
            return $defaults;
        } elseif ( isset( $defaults[ $role ] ) ) {
            return VAA_API::get_array_data( $defaults[ $role ], $meta_key );
        }
        return null;
    }

    /**
     * Set the role default.
     * Iterates over each role and sets the new values with an optional method.
     * By default it fully overwrites the previous values.
     *
     * @since   1.7.0
     * @access  private
     * @param   array   $new_defaults  New role defaults (requires a full array of roles with data).
     * @param   string  $method        (optional) Method to be used. (merge, append, default).
     */
    private function set_role_defaults( $new_defaults, $method = '' ) {
        if ( empty( $new_defaults ) ) {
            return;
        }
        $role_defaults = $this->get_role_defaults();
        foreach ( $new_defaults as $role => $role_data ) {
            if ( empty( $role_defaults[ $role ] ) ) {
                $role_defaults[ $role ] = array();
            }
            // @since  1.6.2  Multiple import methods.
            switch ( $method ) {
                case 'merge':
                    // Merge and the existing data (keep data that doesn't exist in the import data).
                    $role_defaults[ $role ] = array_merge( $role_defaults[ $role ], $role_data );
                    break;
                case 'append':
                    // Append new data without overwriting the existing data.
                    $role_defaults[ $role ] = array_merge( $role_data, $role_defaults[ $role ] );
                    break;
                default:
                    // Fully Overwrite data for each supplied role.
                    $role_defaults[ $role ] = $role_data;
                    break;
            }
        }
        $this->update_optionData( $role_defaults, 'roles', true );
    }

    /**
     * Update a role with new defaults.
     *
     * @since   1.4.0
     * @access  private
     *
     * @param   string  $role        Role name.
     * @param   string  $meta_key    Meta key.
     * @param   string  $meta_value  Meta value.
     * @return  bool
     */
    private function update_role_defaults_metadata( $role, $meta_key, $meta_value ) {
        $role_defaults = $this->get_role_defaults();
        if ( ! isset( $role_defaults[ $role ] ) ) {
            $role_defaults[ $role ] = array();
        }
        $role_defaults[ $role ][ $meta_key ] = $meta_value;
        return $this->update_optionData( $role_defaults, 'roles', true );
    }

    /**
     * Copy defaults from one role to another (or multiple).
     *
     * @since   1.7.0
     * @access  public
     *
     * @param   string        $from_role  The source role defaults.
     * @param   string|array  $to_role    The role(s) to copy to.
     * @param   string        $method     (optional) Clone method.
     * @return  array|bool
     */
    public function copy_role_defaults( $from_role, $to_role, $method = '' ) {
        $to_role       = (array) $to_role;
        $error_list    = array();
        $role_defaults = $this->get_role_defaults();
        if ( ! empty( $role_defaults[ $from_role ] ) ) {
            foreach ( $to_role as $role ) {
                if ( $this->store->get_roles( $role ) ) {
                    $role_defaults[ $role ] = $role_defaults[ $from_role ];
                } else {
                    $error_list[] = esc_attr__( 'Role not found', VIEW_ADMIN_AS_DOMAIN ) . ': ' . (string) $role;
                }
            }

            $this->set_role_defaults( $role_defaults, $method );

            if ( ! empty( $error_list ) ) {
                return array(
                    'text' => esc_attr__( 'Data copied but there were some errors', VIEW_ADMIN_AS_DOMAIN ) . ':',
                    'list' => $error_list,
                );
            }
            return true;
        }
        return array(
            'text' => esc_attr__( 'No valid data found', VIEW_ADMIN_AS_DOMAIN ) . ':',
            'list' => $error_list,
        );
    }

    /**
     * Remove defaults of a role.
     *
     * @since   1.4.0
     * @since   1.7.2  Renamed "all" wildcard to "__all__".
     * @access  public
     * @param   string|string[]  $role  Role name, an array of role names or just "__all__" for all roles.
     * @return  bool
     */
    public function clear_role_defaults( $role ) {
        $role_defaults = $this->get_role_defaults();
        if ( ! is_array( $role ) ) {
            if ( isset( $role_defaults ) && '__all__' === $role ) {
                $role_defaults = array();
            } else {
                $roles = array( $role );
            }
        } else {
            $roles = $role;
        }
        if ( isset( $roles ) ) {
            foreach ( $roles as $role ) {
                if ( isset( $role_defaults[ $role ] ) ) {
                    unset( $role_defaults[ $role ] );
                }
            }
        }
        if ( $this->get_role_defaults() !== $role_defaults ) {
            return $this->update_optionData( $role_defaults, 'roles' );
        }
        // @todo Currently still returns true when a role doesn't exists. Maybe return false?
        return true; // No changes needed.
    }

    /**
     * Export role defaults.
     * Note: Export always returns a full array by default (role as array key) even if you only export a single role.
     *
     * @since   1.5.0
     * @since   1.7.2  Renamed "all" wildcard to "__all__".
     * @access  public
     * @param   string  $role  (optional) Role name or "__all__" for all roles.
     * @return  mixed
     */
    public function export_role_defaults( $role = '__all__' ) {
        $role_defaults = $this->get_role_defaults();
        if ( '__all__' !== $role && isset( $role_defaults[ $role ] ) ) {
            $data = $role_defaults[ $role ];
            $data = array( $role => $data );
        } elseif ( '__all__' === $role && ! empty( $role_defaults ) ) {
            $data = $role_defaults;
        } else {
            $data = esc_attr__( 'No valid data found', VIEW_ADMIN_AS_DOMAIN );
        }
        return $data;
    }

    /**
     * Import role defaults.
     *
     * @since   1.5.0
     * @since   1.6.2  Add extra import methods.
     * @access  public
     * @param   array   $data    Data to import.
     * @param   string  $method  (optional) Import method.
     * @return  mixed
     */
    public function import_role_defaults( $data, $method = '' ) {
        $new_defaults = array();
        $error_list   = array();
        if ( empty( $data ) || ! is_array( $data ) ) {
            return array( 'text' => esc_attr__( 'No valid data found', VIEW_ADMIN_AS_DOMAIN ) );
        }
        foreach ( $data as $role => $role_data ) {
            // Make sure the role exists.
            if ( $this->store->get_roles( $role ) ) {
                // Add the role to the new defaults.
                $new_defaults[ $role ] = array();
                foreach ( $role_data as $data_key => $data_value ) {
                    // Make sure the import data are valid meta keys.
                    if ( true === $this->compare_metakey( $data_key ) ) {
                        // Add the key and data.
                        $new_defaults[ $role ][ $data_key ] = $data_value;
                    } else {
                        $error_list[] = esc_attr__( 'Key not allowed', VIEW_ADMIN_AS_DOMAIN ) . ': ' . $data_key;
                    }
                }
            } else {
                $error_list[] = esc_attr__( 'Role not found', VIEW_ADMIN_AS_DOMAIN ) . ': ' . $role;
            }
        }
        if ( ! empty( $new_defaults ) ) {

            $this->set_role_defaults( $new_defaults, $method );

            if ( ! empty( $error_list ) ) {
                // Close enough!
                return array(
                    'text' => esc_attr__( 'Data imported but there were some errors', VIEW_ADMIN_AS_DOMAIN ) . ':',
                    'list' => $error_list,
                );
            }
            return true; // Yay!
        }
        // Nope..
        return array(
            'text' => esc_attr__( 'No valid data found', VIEW_ADMIN_AS_DOMAIN ) . ':',
            'list' => $error_list,
        );
    }

    /**
     * Match the meta key with predefined metakeys.
     * %% stands for a wildcard. This function only supports one wildcard!
     *
     * Disable some PHPMD checks for this method.
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     * @SuppressWarnings(PHPMD.NPathComplexity)
     * @todo Refactor to enable above checks?
     *
     * @since   1.4.0
     * @access  public
     * @param   string  $meta_key_compare  Meta key.
     * @return  bool
     */
    public function compare_metakey( $meta_key_compare ) {
        $meta_keys = (array) $this->get_meta();
        foreach ( $meta_keys as $meta_key => $enabled ) {
            if ( empty( $enabled ) || ! is_string( $meta_key ) ) {
                continue;
            }

            if ( false === strpos( $meta_key, '%%' ) ) {
                // No need for start/end checks. If it's the same, return true, otherwise check the next key.
                if ( $meta_key === $meta_key_compare ) {
                    return true;
                }
                continue;
            }

            // @since 1.7.3 `edit_%%_per_page` would otherwise be valid for: `edit_%%_per_page`.
            if ( $meta_key === $meta_key_compare ) {
                // This is never valid, don't even check other keys.
                return false;
            }

            $meta_key_parts = explode( '%%', $meta_key );

            /**
             * Double checks.
             * Also trims underscores, dashes and spaces.
             *
             * - `edit_per_page` would otherwise be valid for: `edit_%%_per_page`.
             * - `edit__per_page` would otherwise be valid for: `edit_%%_per_page`.
             * - `metaboxhidden_` would otherwise be valid for: `metaboxhidden_%%`.
             *
             * Above checks will validate to true if the keys have been explicitly added.
             *
             * @since 1.7.3
             */
            $trim = '_- ';
            // Create compare check without %%.
            $compare_check = implode( '', $meta_key_parts );
            $compare_arr   = array(
                $compare_check,
                trim( $compare_check, $trim ),
            );
            // Replace double underscores and dashes.
            $compare_check = str_replace( array( '__', '--' ), array( '_', '-' ), $compare_check );
            $compare_arr[] = $compare_check;
            $compare_arr[] = trim( $compare_check, $trim );
            if ( in_array( $meta_key_compare, $compare_arr, true ) ) {
                continue;
            }

            $compare_start = true;
            if ( ! empty( $meta_key_parts[0] ) ) {
                $compare_start = VAA_API::starts_with( $meta_key_compare, $meta_key_parts[0] );
            }

            $compare_end = true;
            if ( ! empty( $meta_key_parts[1] ) ) {
                $compare_end = VAA_API::ends_with( $meta_key_compare, $meta_key_parts[1] );
            }

            if ( true === $compare_start && true === $compare_end ) {
                return true;
            }
        }
        return false;
    }

    /**
     * Add admin bar module setting items.
     *
     * @since   1.5.0
     * @access  public
     * @see     'vaa_admin_bar_modules' action
     *
     * @param   \WP_Admin_Bar  $admin_bar  The toolbar object.
     * @param   string         $root       The root item (vaa-settings).
     * @return  void
     */
    public function admin_bar_menu_modules( $admin_bar, $root ) {

        $root_prefix = $root . '-role-defaults';

        $admin_bar->add_node( array(
            'id'     => $root_prefix . '-enable',
            'parent' => $root,
            'title'  => VAA_View_Admin_As_Form::do_checkbox( array(
                'name'        => $root_prefix . '-enable',
                'value'       => $this->get_optionData( 'enable' ),
                'compare'     => true,
                'label'       => __( 'Enable role defaults', VIEW_ADMIN_AS_DOMAIN ),
                'description' => __( 'Set default screen settings for roles and apply them on users through various bulk and automatic actions', VIEW_ADMIN_AS_DOMAIN ),
                'auto_js'     => array(
                    'setting' => $this->moduleKey,
                    'key'     => 'enable',
                    'refresh' => true,
                ),
            ) ),
            'href'   => false,
            'meta'   => array(
                'class' => 'auto-height',
            ),
        ) );

    }

    /**
     * Add admin bar menu's.
     *
     * Disable some PHPMD checks for this method.
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
     * @todo Refactor to enable above checks?
     *
     * @since   1.4.0
     * @since   1.5.2   Changed hook to `vaa_admin_bar_settings_after` (previous: `vaa_admin_bar_roles_before`).
     * @since   1.6.1   Changed hook to `vaa_admin_bar_menu`.
     * @access  public
     * @see     'vaa_admin_bar_menu' action
     *
     * @param   \WP_Admin_Bar  $admin_bar  The toolbar object.
     * @param   string         $root       The root item (vaa).
     * @return  void
     */
    public function admin_bar_menu( $admin_bar, $root ) {

        $admin_bar->add_node( array(
            'id'     => $root . '-role-defaults',
            'parent' => $root,
            'title'  => VAA_View_Admin_As_Form::do_icon( 'dashicons-welcome-view-site' ) . __( 'Role defaults', VIEW_ADMIN_AS_DOMAIN ),
            'href'   => false,
            'meta'   => array(
                'class'    => 'vaa-has-icon',
                'tabindex' => '0',
            ),
        ) );

        $root = $root . '-role-defaults';

        $admin_bar->add_node( array(
            'id'     => $root . '-docs',
            'parent' => $root,
            'title'  => VAA_View_Admin_As_Form::do_icon( 'dashicons-book-alt' ) . __( 'Documentation', VIEW_ADMIN_AS_DOMAIN ),
            'href'   => 'https://github.com/JoryHogeveen/view-admin-as/wiki/Role-Defaults',
            'meta'   => array(
                'class'  => 'auto-height vaa-has-icon',
                'target' => '_blank',
            ),
        ) );

        // This module requires the role view type to enable all it's features.
        if ( ! VAA_API::is_view_type_enabled( 'role' ) ) {
            $view_type = view_admin_as()->get_view_types( 'role' );
            if ( $view_type instanceof VAA_View_Admin_As_Roles ) {
                $admin_bar->add_node( array(
                    'id'     => $root . '-dependency',
                    'parent' => $root,
                    'title'  => VAA_View_Admin_As_Form::do_button( array(
                        'name'    => $root . 'dependency-role',
                        'value'   => true,
                        'label'   => VAA_View_Admin_As_Form::do_icon( 'dashicons-warning' )
                                     // Translators: %s stands for the translated view type label "Roles".
                                     . sprintf( __( 'Please enable the "%s" view type to set role defaults', VIEW_ADMIN_AS_DOMAIN ), $view_type->get_label() ),
                        'auto_js' => array(
                            'setting' => 'setting',
                            'key'     => 'view_types',
                            'values'  => array(
                                'role' => array(
                                    'values' => array(
                                        'enabled' => array(),
                                    ),
                                ),
                            ),
                            'refresh' => true,
                        ),
                    ) ),
                    'href'   => false,
                    'meta'   => array(
                        'class' => 'vaa-button-container',
                    ),
                ) );
            }
        }

        // @since  1.4.0  Enable apply defaults on register.
        $admin_bar->add_node( array(
            'id'     => $root . '-setting-register-enable',
            'parent' => $root,
            'title'  => VAA_View_Admin_As_Form::do_checkbox( array(
                'name'    => $root . '-setting-register-enable',
                'value'   => $this->get_optionData( 'apply_defaults_on_register' ),
                'compare' => true,
                'label'   => __( 'Automatically apply defaults to new users', VIEW_ADMIN_AS_DOMAIN ),
                'auto_js' => array(
                    'setting' => $this->moduleKey,
                    'key'     => 'apply_defaults_on_register',
                    'refresh' => false,
                ),
            ) ),
            'href'   => false,
            'meta'   => array(
                'class' => 'auto-height',
            ),
        ) );
        // @since  1.5.3  Disable screen settings for users who can't access this plugin.
        $admin_bar->add_node( array(
            'id'     => $root . '-setting-disable-user-screen-options',
            'parent' => $root,
            'title'  => VAA_View_Admin_As_Form::do_checkbox( array(
                'name'          => $root . '-setting-disable-user-screen-options',
                'value'         => $this->get_optionData( 'disable_user_screen_options' ),
                'compare'       => true,
                'label'         => __( 'Disable screen options', VIEW_ADMIN_AS_DOMAIN ),
                'description'   => __( "Hide the screen options for all users who can't access role defaults", VIEW_ADMIN_AS_DOMAIN ),
                'help'          => true,
                'auto_showhide' => true,
                'auto_js'       => array(
                    'setting' => $this->moduleKey,
                    'key'     => 'disable_user_screen_options',
                    'refresh' => false,
                ),
            ) ),
            'href'   => false,
            'meta'   => array(
                'class' => 'auto-height',
            ),
        ) );
        // @since  1.6.0  Lock meta box order and locations for users who can't access this plugin.
        $admin_bar->add_node( array(
            'id'     => $root . '-setting-lock-meta-boxes',
            'parent' => $root,
            'title'  => VAA_View_Admin_As_Form::do_checkbox( array(
                'name'          => $root . '-setting-lock-meta-boxes',
                'value'         => $this->get_optionData( 'lock_meta_boxes' ),
                'compare'       => true,
                'label'         => __( 'Lock meta boxes', VIEW_ADMIN_AS_DOMAIN ),
                'description'   => __( "Lock meta box order and locations for all users who can't access role defaults", VIEW_ADMIN_AS_DOMAIN ),
                'help'          => true,
                'auto_showhide' => true,
                'auto_js'       => array(
                    'setting' => $this->moduleKey,
                    'key'     => 'lock_meta_boxes',
                    'refresh' => false,
                ),
            ) ),
            'href'   => false,
            'meta'   => array(
                'class' => 'auto-height',
            ),
        ) );

        /**
         * Manage metakeys.
         *
         * @since  1.6.3
         */
        $admin_bar->add_group( array(
            'id'     => $root . '-meta',
            'parent' => $root,
            'meta'   => array(
                'class' => 'ab-sub-secondary vaa-toggle-group',
            ),
        ) );
        $admin_bar->add_node( array(
            'id'     => $root . '-meta-title',
            'parent' => $root . '-meta',
            'title'  => VAA_View_Admin_As_Form::do_icon( 'dashicons-admin-tools' ) . __( 'Manage meta sync', VIEW_ADMIN_AS_DOMAIN ),
            'href'   => false,
            'meta'   => array(
                'class'    => 'ab-bold vaa-has-icon ab-vaa-toggle',
                'tabindex' => '0',
            ),
        ) );
        $admin_bar->add_node( array(
            'id'     => $root . '-meta-docs',
            'parent' => $root . '-meta',
            'title'  => VAA_View_Admin_As_Form::do_icon( 'dashicons-book-alt' ) . __( 'Documentation', VIEW_ADMIN_AS_DOMAIN ),
            'href'   => 'https://github.com/JoryHogeveen/view-admin-as/wiki/Role-Defaults#what-data-is-stored-for-role-defaults-and-how-can-this-be-changed',
            'meta'   => array(
                'class'  => 'auto-height vaa-has-icon',
                'target' => '_blank',
            ),
        ) );
        $admin_bar->add_node( array(
            'id'     => $root . '-meta-add',
            'parent' => $root . '-meta',
            'title'  => VAA_View_Admin_As_Form::do_input( array(
                'name'        => $root . '-meta-new',
                'placeholder' => esc_attr__( 'Add meta key', VIEW_ADMIN_AS_DOMAIN ),
            ) ) . VAA_View_Admin_As_Form::do_button( array(
                'name'  => $root . '-meta-add',
                'label' => __( 'Add', VIEW_ADMIN_AS_DOMAIN ),
                'class' => 'button-primary input-overlay',
            ) )
            . '<div id="' . $root . '-meta-template" style="display: none;"><div class="ab-item vaa-item">'
            . VAA_View_Admin_As_Form::do_checkbox( array(
                'name'           => 'role-defaults-meta-select[]',
                'id'             => $root . '-meta-select-vaa_new_item',
                'value'          => true,
                'compare'        => true,
                'checkbox_value' => 'vaa_new_item',
                'label'          => 'vaa_new_item',
                'removable'      => true,
            ) ) . '</div></div>',
            'href'   => false,
            'meta'   => array(
                'class' => 'ab-vaa-input',
            ),
        ) );
        $meta_select_content = '';
        foreach ( $this->get_meta() as $metakey => $enabled ) {
            $meta_select_content .=
                '<div class="ab-item vaa-item">'
                . VAA_View_Admin_As_Form::do_checkbox( array(
                    'name'           => 'role-defaults-meta-select[]',
                    'id'             => $root . '-meta-select-' . $metakey,
                    'value'          => $enabled,
                    'compare'        => true,
                    'checkbox_value' => $metakey,
                    'label'          => $metakey,
                    'removable'      => ( array_key_exists( $metakey, $this->meta_default ) ) ? false : true,
                ) )
                . '</div>';
        }
        $admin_bar->add_node( array(
            'id'     => $root . '-meta-select',
            'parent' => $root . '-meta',
            'title'  => $meta_select_content,
            'href'   => false,
            'meta'   => array(
                'class' => 'ab-vaa-multipleselect vaa-small vaa-resizable',
            ),
        ) );
        $admin_bar->add_node( array(
            'id'     => $root . '-meta-apply',
            'parent' => $root . '-meta',
            'title'  => VAA_View_Admin_As_Form::do_button( array(
                'name'    => $root . '-meta-apply',
                'label'   => __( 'Apply', VIEW_ADMIN_AS_DOMAIN ),
                'class'   => 'button-primary',
                'auto_js' => array(
                    'setting' => $this->moduleKey,
                    'key'     => 'update_meta',
                    'refresh' => false,
                    'value'   => array(
                        'element' => '#wp-admin-bar-' . $root . '-meta-select .ab-item.vaa-item input',
                        'parser'  => 'multi',
                    ),
                ),
            ) ),
            'href'   => false,
            'meta'   => array(
                'class' => 'vaa-button-container',
            ),
        ) );

        $this->admin_bar_menu_bulk_actions( $admin_bar, $root );
    }

    /**
     * Add admin bar menu bulk actions.
     *
     * Disable some PHPMD checks for this method.
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     * @SuppressWarnings(PHPMD.NPathComplexity)
     * @todo Refactor to enable above checks?
     *
     * @since   1.7.0  Separated the tools from the main function.
     * @access  public
     * @see     \VAA_View_Admin_As_Role_Defaults::admin_bar_menu()
     *
     * @param   \WP_Admin_Bar  $admin_bar  The toolbar object.
     * @param   string         $root       The root item (vaa).
     * @return  void
     */
    private function admin_bar_menu_bulk_actions( $admin_bar, $root ) {

        $roles               = $this->store->get_roles();
        $role_check_content  = array();
        $role_select_options = array(
            ''        => array(
                'label' => ' --- ',
            ),
            '__all__' => array(
                'value' => '__all__',
                'label' => ' - ' . __( 'All roles', VIEW_ADMIN_AS_DOMAIN ) . ' - ',
            ),
        );
        foreach ( $this->store->get_rolenames() as $role_key => $role_name ) {
            $role_select_options[ $role_key ] = array(
                'value' => esc_attr( $role_key ),
                'label' => esc_html( $role_name ),
            );
            $role_check_content[ $role_key ]  =
                '<div class="ab-item vaa-item">'
                . VAA_View_Admin_As_Form::do_checkbox( array(
                    'name'           => 'role-defaults-bulk-roles-select[]',
                    'id'             => $root . '-bulk-roles-select-' . esc_attr( $role_key ),
                    'checkbox_value' => esc_attr( $role_key ),
                    'label'          => '<span class="user-name">' . esc_html( $role_name ) . '</span>',
                ) )
                . '</div>';
        }

        $user_ajax          = false;
        $user_check_content = '';

        /** @var \VAA_View_Admin_As_Users $user_view_type */
        $user_view_type = view_admin_as()->get_view_types( 'user' );
        if ( $user_view_type instanceof VAA_View_Admin_As_Users ) {
            $user_ajax          = $user_view_type->ajax_search();
            $user_check_content = $this->get_users_bulk_checkbox_html( $this->store->get_users(), $root );
        }

        $role_defaults = $this->get_role_defaults();

        /**
         * @since  1.4.0  Apply defaults to users.
         * @since  1.8.2  AJAX search compatibility.
         */
        if ( $user_check_content || $user_ajax ) {

            $admin_bar->add_group( array(
                'id'     => $root . '-bulk-users',
                'parent' => $root,
                'meta'   => array(
                    'class' => 'ab-sub-secondary vaa-toggle-group',
                ),
            ) );
            $admin_bar->add_node( array(
                'id'     => $root . '-bulk-users-title',
                'parent' => $root . '-bulk-users',
                'title'  => VAA_View_Admin_As_Form::do_icon( 'dashicons-admin-users' )
                            . __( 'Apply defaults to users', VIEW_ADMIN_AS_DOMAIN ),
                'href'   => false,
                'meta'   => array(
                    'class'    => 'ab-bold vaa-has-icon ab-vaa-toggle',
                    'tabindex' => '0',
                ),
            ) );
            if ( $user_ajax ) {
                $admin_bar->add_node( array(
                    'id'     => $root . '-bulk-users-search',
                    'parent' => $root . '-bulk-users',
                    'title'  => VAA_View_Admin_As_Form::do_select( array(
                        'name'   => $root . '-bulk-users-search-by',
                        'values' => $user_view_type->search_users_by_select_values(),
                        'class'  => 'vaa-wide',
                    ) ) . VAA_View_Admin_As_Form::do_input( array(
                        'name'        => $root . '-bulk-users-search',
                        'placeholder' => esc_attr__( 'Search', VIEW_ADMIN_AS_DOMAIN ),
                        'class'       => 'vaa-wide',
                    ) ),
                    'href'   => false,
                    'meta'   => array(
                        'class' => 'ab-vaa-search search-users search-ajax',
                    ),
                ) );
            } else {
                $admin_bar->add_node( array(
                    'id'     => $root . '-bulk-users-filter',
                    'parent' => $root . '-bulk-users',
                    'title'  => VAA_View_Admin_As_Form::do_input( array(
                        'name'        => $root . '-bulk-users-filter',
                        'placeholder' => esc_attr__( 'Filter', VIEW_ADMIN_AS_DOMAIN ) . ' (' . strtolower( __( 'Username' ) ) . ')',
                    ) ),
                    'href'   => false,
                    'meta'   => array(
                        'class' => 'ab-vaa-filter',
                    ),
                ) );
            }
            $admin_bar->add_node( array(
                'id'     => $root . '-bulk-users-select',
                'parent' => $root . '-bulk-users',
                'title'  => $user_check_content,
                'href'   => false,
                'meta'   => array(
                    'class' => 'ab-vaa-multipleselect vaa-small vaa-resizable',
                ),
            ) );
            $admin_bar->add_node( array(
                'id'     => $root . '-bulk-users-apply',
                'parent' => $root . '-bulk-users',
                'title'  => VAA_View_Admin_As_Form::do_button( array(
                    'name'    => $root . '-bulk-users-apply',
                    'label'   => __( 'Apply', VIEW_ADMIN_AS_DOMAIN ),
                    'class'   => 'button-primary',
                    'auto_js' => array(
                        'setting' => $this->moduleKey,
                        'key'     => 'apply_defaults_to_users',
                        'refresh' => false,
                        'value'   => array(
                            'element' => '#wp-admin-bar-' . $root . '-bulk-users-select .ab-item.vaa-item input',
                            'parser'  => 'selected',
                        ),
                    ),
                ) ),
                'href'   => false,
                'meta'   => array(
                    'class' => 'vaa-button-container',
                ),
            ) );
        } // End if().

        /**
         * @since  1.4.0  Apply defaults to all users for a role.
         */
        if ( $roles ) {

            $admin_bar->add_group( array(
                'id'     => $root . '-bulk-roles',
                'parent' => $root,
                'meta'   => array(
                    'class' => 'ab-sub-secondary vaa-toggle-group',
                ),
            ) );
            $admin_bar->add_node( array(
                'id'     => $root . '-bulk-roles-title',
                'parent' => $root . '-bulk-roles',
                'title'  => VAA_View_Admin_As_Form::do_icon( 'dashicons-groups' )
                            . __( 'Apply defaults to users by role', VIEW_ADMIN_AS_DOMAIN ),
                'href'   => false,
                'meta'   => array(
                    'class'    => 'ab-bold vaa-has-icon ab-vaa-toggle',
                    'tabindex' => '0',
                ),
            ) );
            $admin_bar->add_node( array(
                'id'     => $root . '-bulk-roles-select',
                'parent' => $root . '-bulk-roles',
                'title'  => VAA_View_Admin_As_Form::do_select( array(
                    'name'   => $root . '-bulk-roles-select',
                    'values' => $role_select_options,
                ) ),
                'href'   => false,
                'meta'   => array(
                    'class' => 'ab-vaa-select select-role', // vaa-column-one-half vaa-column-last .
                ),
            ) );
            $admin_bar->add_node( array(
                'id'     => $root . '-bulk-roles-apply',
                'parent' => $root . '-bulk-roles',
                'title'  => VAA_View_Admin_As_Form::do_button( array(
                    'name'    => $root . '-bulk-roles-apply',
                    'label'   => __( 'Apply', VIEW_ADMIN_AS_DOMAIN ),
                    'class'   => 'button-primary',
                    'auto_js' => array(
                        'setting' => $this->moduleKey,
                        'key'     => 'apply_defaults_to_users_by_role',
                        'refresh' => false,
                        'value'   => array(
                            'element' => '#wp-admin-bar-' . $root . '-bulk-roles-select select#' . $root . '-bulk-roles-select',
                            'parser'  => '', // Default.
                        ),
                    ),
                ) ),
                'href'   => false,
                'meta'   => array(
                    'class' => 'vaa-button-container',
                ),
            ) );
        } // End if().

        /**
         * Copy / Import / Export
         */
        if ( $roles ) {

            /**
             * @since  1.7.0  Copy actions.
             */
            $role_copy_options = $role_select_options;
            // Change first item label.
            $role_copy_options['']['label'] = '- ' . __( 'Select role source', VIEW_ADMIN_AS_DOMAIN ) . ' -';
            // Remove '__all__' option from copy list.
            unset( $role_copy_options['__all__'] );

            $admin_bar->add_group( array(
                'id'     => $root . '-copy',
                'parent' => $root,
                'meta'   => array(
                    'class' => 'ab-sub-secondary vaa-toggle-group',
                ),
            ) );
            $admin_bar->add_node( array(
                'id'     => $root . '-copy-roles',
                'parent' => $root . '-copy',
                'title'  => VAA_View_Admin_As_Form::do_icon( 'dashicons-pressthis' )
                            . __( 'Copy defaults to role', VIEW_ADMIN_AS_DOMAIN ),
                'href'   => false,
                'meta'   => array(
                    'class'    => 'ab-bold vaa-has-icon ab-vaa-toggle',
                    'tabindex' => '0',
                ),
            ) );
            $admin_bar->add_node( array(
                'id'     => $root . '-copy-roles-from',
                'parent' => $root . '-copy',
                'title'  => VAA_View_Admin_As_Form::do_select( array(
                    'name'   => $root . '-copy-roles-from',
                    'values' => $role_copy_options,
                ) ),
                'href'   => false,
                'meta'   => array(
                    'class' => 'ab-vaa-select select-role', // vaa-column-one-half vaa-column-last .
                ),
            ) );
            $admin_bar->add_node( array(
                'id'     => $root . '-copy-roles-to',
                'parent' => $root . '-copy',
                'title'  => implode( '', $role_check_content ),
                'href'   => false,
                'meta'   => array(
                    'class' => 'ab-vaa-multipleselect vaa-small vaa-resizable',
                ),
            ) );

            $auto_js = array(
                'setting' => $this->moduleKey,
                'key'     => 'copy_role_defaults',
                'refresh' => false,
                'values'  => array(
                    'from'   => array(
                        'element' => '#wp-admin-bar-' . $root . '-copy-roles-from select#' . $root . '-copy-roles-from',
                        'parser'  => '', // Default.
                    ),
                    'to'     => array(
                        'element' => '#wp-admin-bar-' . $root . '-copy-roles-to .ab-item.vaa-item input',
                        'parser'  => 'selected',
                    ),
                    'method' => array(
                        'attr' => 'vaa-method',
                    ),
                ),
            );
            $admin_bar->add_node( array(
                'id'     => $root . '-copy-roles-copy',
                'parent' => $root . '-copy',
                'title'  => VAA_View_Admin_As_Form::do_button( array(
                    'name'    => $root . '-copy-roles-copy',
                    'label'   => __( 'Copy', VIEW_ADMIN_AS_DOMAIN ),
                    'class'   => 'button-secondary ab-vaa-showhide vaa-copy-role-defaults',
                    'auto_js' => $auto_js,
                    'attr'    => array(
                        'vaa-method'   => 'copy',
                        'vaa-showhide' => 'p.vaa-copy-role-defaults-desc',
                    ),
                ) ) . ' '
                . VAA_View_Admin_As_Form::do_button( array(
                    'name'    => $root . '-copy-roles-copy-merge',
                    'label'   => __( 'Merge', VIEW_ADMIN_AS_DOMAIN ),
                    'class'   => 'button-secondary ab-vaa-showhide vaa-copy-role-defaults',
                    'auto_js' => $auto_js,
                    'attr'    => array(
                        'vaa-method'   => 'merge',
                        'vaa-showhide' => 'p.vaa-copy-role-defaults-merge-desc',
                    ),
                ) ) . ' '
                . VAA_View_Admin_As_Form::do_button( array(
                    'name'    => $root . '-copy-roles-copy-append',
                    'label'   => __( 'Append', VIEW_ADMIN_AS_DOMAIN ),
                    'class'   => 'button-secondary ab-vaa-showhide vaa-copy-role-defaults',
                    'auto_js' => $auto_js,
                    'attr'    => array(
                        'vaa-method'   => 'append',
                        'vaa-showhide' => 'p.vaa-copy-role-defaults-append-desc',
                    ),
                ) )
                . VAA_View_Admin_As_Form::do_description(
                    __( 'Fully overwrite data', VIEW_ADMIN_AS_DOMAIN ),
                    array( 'class' => 'vaa-copy-role-defaults-desc' )
                )
                . VAA_View_Admin_As_Form::do_description(
                    __( 'Merge and overwrite existing data', VIEW_ADMIN_AS_DOMAIN ),
                    array( 'class' => 'vaa-copy-role-defaults-merge-desc' )
                )
                . VAA_View_Admin_As_Form::do_description(
                    __( 'Append without overwriting the existing data', VIEW_ADMIN_AS_DOMAIN ),
                    array( 'class' => 'vaa-copy-role-defaults-append-desc' )
                ),
                'href'   => false,
                'meta'   => array(
                    'class' => 'vaa-button-container',
                ),
            ) );

            /**
             * @since  1.5.0  Export actions.
             */
            $admin_bar->add_group( array(
                'id'     => $root . '-export',
                'parent' => $root,
                'meta'   => array(
                    'class' => 'ab-sub-secondary vaa-toggle-group',
                ),
            ) );
            $admin_bar->add_node( array(
                'id'     => $root . '-export-roles',
                'parent' => $root . '-export',
                'title'  => VAA_View_Admin_As_Form::do_icon( 'dashicons-upload' )
                            . __( 'Export defaults for role', VIEW_ADMIN_AS_DOMAIN ),
                'href'   => false,
                'meta'   => array(
                    'class'    => 'ab-bold vaa-has-icon ab-vaa-toggle',
                    'tabindex' => '0',
                ),
            ) );
            $admin_bar->add_node( array(
                'id'     => $root . '-export-roles-select',
                'parent' => $root . '-export',
                'title'  => VAA_View_Admin_As_Form::do_select( array(
                    'name'   => $root . '-export-roles-select',
                    'values' => $role_select_options,
                ) ),
                'href'   => false,
                'meta'   => array(
                    'class' => 'ab-vaa-select select-role', // vaa-column-one-half vaa-column-last .
                ),
            ) );

            $auto_js = array(
                'setting' => $this->moduleKey,
                'key'     => 'export_role_defaults',
                'refresh' => false,
                'value'   => array(
                    'element' => '#wp-admin-bar-' . $root . '-export-roles-select select#' . $root . '-export-roles-select',
                    'parser'  => '', // Default.
                ),
            );
            $admin_bar->add_node( array(
                'id'     => $root . '-export-roles-export',
                'parent' => $root . '-export',
                'title'  => VAA_View_Admin_As_Form::do_button( array(
                    'name'    => $root . '-export-roles-export',
                    'label'   => __( 'Export', VIEW_ADMIN_AS_DOMAIN ),
                    'class'   => 'button-secondary',
                    'auto_js' => $auto_js,
                ) ) . ' ' . VAA_View_Admin_As_Form::do_button( array(
                    'name'    => $root . '-export-roles-download',
                    'label'   => __( 'Download', VIEW_ADMIN_AS_DOMAIN ),
                    'class'   => 'button-secondary',
                    'auto_js' => array_merge( $auto_js, array(
                        'download' => true,
                    ) ),
                ) ),
                'href'   => false,
                'meta'   => array(
                    'class' => 'vaa-button-container',
                ),
            ) );

            /**
             * @since  1.5.0  Import actions.
             */
            $admin_bar->add_group( array(
                'id'     => $root . '-import',
                'parent' => $root,
                'meta'   => array(
                    'class' => 'ab-sub-secondary vaa-toggle-group',
                ),
            ) );
            $admin_bar->add_node( array(
                'id'     => $root . '-import-roles',
                'parent' => $root . '-import',
                'title'  => VAA_View_Admin_As_Form::do_icon( 'dashicons-download' )
                            . __( 'Import defaults for role', VIEW_ADMIN_AS_DOMAIN ),
                'href'   => false,
                'meta'   => array(
                    'class'    => 'ab-bold vaa-has-icon ab-vaa-toggle',
                    'tabindex' => '0',
                ),
            ) );
            $admin_bar->add_node( array(
                'id'     => $root . '-import-roles-input',
                'parent' => $root . '-import',
                'title'  => '<textarea id="' . $root . '-import-roles-input" name="role-defaults-import-roles-input" placeholder="'
                            . esc_attr__( 'Paste code here or select a file below', VIEW_ADMIN_AS_DOMAIN ) . '"></textarea>',
                'href'   => false,
                'meta'   => array(
                    'class' => 'ab-vaa-textarea input-role',
                ),
            ) );
            $admin_bar->add_node( array(
                'id'     => $root . '-import-roles-file',
                'parent' => $root . '-import',
                'title'  => VAA_View_Admin_As_Form::do_input( array(
                    'name'    => $root . '-import-roles-file',
                    'type'    => 'file',
                    'auto_js' => array(
                        'callback' => 'assign_file_content',
                        'param'    => array(
                            'target'  => '#wp-admin-bar-' . $root . '-import-roles-input textarea#' . $root . '-import-roles-input',
                            'element' => '#wp-admin-bar-' . $root . '-import-roles-file input#' . $root . '-import-roles-file',
                        ),
                    ),
                    'attr'    => array(
                        'accept' => 'text/*,.json',
                    ),
                ) ),
                'href'   => false,
                'meta'   => array(
                    'class' => 'ab-vaa-file',
                ),
            ) );

            $auto_js = array(
                'setting' => $this->moduleKey,
                'key'     => 'import_role_defaults',
                'refresh' => false,
                'values'  => array(
                    'data'   => array(
                        'element' => '#wp-admin-bar-' . $root . '-import-roles-input textarea#' . $root . '-import-roles-input',
                        'parser'  => '', // Default.
                        'json'    => true,
                    ),
                    'method' => array(
                        'attr' => 'vaa-method',
                    ),
                ),
            );
            $admin_bar->add_node( array(
                'id'     => $root . '-import-roles-import',
                'parent' => $root . '-import',
                'title'  => VAA_View_Admin_As_Form::do_button( array(
                    'name'    => $root . '-import-roles-import',
                    'label'   => __( 'Import', VIEW_ADMIN_AS_DOMAIN ),
                    'class'   => 'button-secondary ab-vaa-showhide vaa-import-role-defaults',
                    'auto_js' => $auto_js,
                    'attr'    => array(
                        'vaa-method'   => 'import',
                        'vaa-showhide' => 'p.vaa-import-role-defaults-desc',
                    ),
                ) ) . ' '
                . VAA_View_Admin_As_Form::do_button( array(
                    'name'    => $root . '-import-roles-import-merge',
                    'label'   => __( 'Merge', VIEW_ADMIN_AS_DOMAIN ),
                    'class'   => 'button-secondary ab-vaa-showhide vaa-import-role-defaults',
                    'auto_js' => $auto_js,
                    'attr'    => array(
                        'vaa-method'   => 'merge',
                        'vaa-showhide' => 'p.vaa-import-role-defaults-merge-desc',
                    ),
                ) ) . ' '
                . VAA_View_Admin_As_Form::do_button( array(
                    'name'    => $root . '-import-roles-import-append',
                    'label'   => __( 'Append', VIEW_ADMIN_AS_DOMAIN ),
                    'class'   => 'button-secondary ab-vaa-showhide vaa-import-role-defaults',
                    'auto_js' => $auto_js,
                    'attr'    => array(
                        'vaa-method'   => 'append',
                        'vaa-showhide' => 'p.vaa-import-role-defaults-append-desc',
                    ),
                ) )
                . VAA_View_Admin_As_Form::do_description(
                    __( 'Fully overwrite data', VIEW_ADMIN_AS_DOMAIN ),
                    array( 'class' => 'vaa-import-role-defaults-desc' )
                )
                . VAA_View_Admin_As_Form::do_description(
                    __( 'Merge and overwrite existing data', VIEW_ADMIN_AS_DOMAIN ),
                    array( 'class' => 'vaa-import-role-defaults-merge-desc' )
                )
                . VAA_View_Admin_As_Form::do_description(
                    __( 'Append without overwriting the existing data', VIEW_ADMIN_AS_DOMAIN ),
                    array( 'class' => 'vaa-import-role-defaults-append-desc' )
                ),
                'href'   => false,
                'meta'   => array(
                    'class' => 'vaa-button-container',
                ),
            ) );

        } // End if().

        /**
         *  @since  1.4.0  Clear actions
         */

        /**
         * Add all existing roles from defaults to the clear list if they have been removed from WP.
         * Don't show roles that don't have data.
         *
         * @see    https://github.com/JoryHogeveen/view-admin-as/issues/22
         * @since  1.6.2
         */
        $role_clear_options = array(
            array(
                'label' => ' --- ',
            ),
            array(
                'value' => '__all__',
                'label' => ' - ' . __( 'All roles', VIEW_ADMIN_AS_DOMAIN ) . ' - ',
            ),
        );

        if ( ! empty( $role_defaults ) ) {
            foreach ( (array) $role_defaults as $role_key => $defaults ) {
                // get_rolenames will return key if it didn't find the role name.
                $role_name            = $this->store->get_rolenames( $role_key );
                $role_clear_options[] = array(
                    'value' => esc_attr( $role_key ),
                    'label' => $role_name,
                );
            }
        }

        $admin_bar->add_group( array(
            'id'     => $root . '-clear',
            'parent' => $root,
            'meta'   => array(
                'class' => 'ab-sub-secondary vaa-toggle-group vaa-sub-transparent',
            ),
        ) );
        $admin_bar->add_node( array(
            'id'     => $root . '-clear-roles',
            'parent' => $root . '-clear',
            'title'  => VAA_View_Admin_As_Form::do_icon( 'dashicons-trash' )
                        . __( 'Remove defaults for role', VIEW_ADMIN_AS_DOMAIN ),
            'href'   => false,
            'meta'   => array(
                'class'    => 'ab-bold vaa-has-icon ab-vaa-toggle',
                'tabindex' => '0',
            ),
        ) );
        $admin_bar->add_node( array(
            'id'     => $root . '-clear-roles-select',
            'parent' => $root . '-clear',
            'title'  => VAA_View_Admin_As_Form::do_select( array(
                'name'   => $root . '-clear-roles-select',
                'values' => $role_clear_options,
            ) ),
            'href'   => false,
            'meta'   => array(
                'class' => 'ab-vaa-select select-role', // vaa-column-one-half vaa-column-last .
            ),
        ) );
        $admin_bar->add_node( array(
            'id'     => $root . '-clear-roles-apply',
            'parent' => $root . '-clear',
            'title'  => VAA_View_Admin_As_Form::do_button( array(
                'name'    => $root . '-clear-roles-apply',
                'label'   => __( 'Apply', VIEW_ADMIN_AS_DOMAIN ),
                'class'   => 'button-secondary',
                'auto_js' => array(
                    'setting' => $this->moduleKey,
                    'key'     => 'clear_role_defaults',
                    'confirm' => true,
                    'refresh' => false,
                    'value'   => array(
                        'element' => '#wp-admin-bar-' . $root . '-clear-roles-select select#' . $root . '-clear-roles-select',
                        'parser'  => '', // Default.
                    ),
                ),
            ) ),
            'href'   => false,
            'meta'   => array(
                'class' => 'vaa-button-container',
            ),
        ) );

    }

    /**
     * Get the multi-checkbox HTML for bulk apply defaults to users.
     *
     * @since   1.8.2
     *
     * @param   \WP_User[]  $users
     * @param   string      $root
     * @return  string
     */
    protected function get_users_bulk_checkbox_html( $users, $root = '' ) {
        if ( ! $users ) {
            return '';
        }
        if ( ! $root ) {
            $root = VAA_View_Admin_As_Admin_Bar::$root;
        }
        $content = '';

        foreach ( $users as $user ) {

            foreach ( $user->roles as $role ) {
                $role_data = $this->store->get_roles( $role );
                if ( $role_data instanceof WP_Role ) {
                    $role_name = $this->store->get_rolenames( $role );

                    $content .=
                        '<div class="ab-item vaa-item">'
                        . VAA_View_Admin_As_Form::do_checkbox( array(
                            'name'           => 'role-defaults-bulk-users-select[]',
                            'id'             => $root . '-role-defaults-bulk-users-select-' . $user->ID,
                            'checkbox_value' => $user->ID . '|' . $role,
                            'label'          => '<span class="user-name">' . $user->display_name . '</span> &nbsp; <span class="user-role">(' . $role_name . ')</span>',
                        ) )
                        . '</div>';
                }
            }
        }

        return $content;
    }

    /**
     * Main Instance.
     *
     * Ensures only one instance of this class is loaded or can be loaded.
     *
     * @since   1.5.0
     * @access  public
     * @static
     * @param   \VAA_View_Admin_As  $caller  The referrer class.
     * @return  \VAA_View_Admin_As_Role_Defaults  $this
     */
    public static function get_instance( $caller = null ) {
        if ( is_null( self::$_instance ) ) {
            self::$_instance = new self( $caller );
        }
        return self::$_instance;
    }

} // End class VAA_View_Admin_As_Role_Defaults.