<?php
/**
 * Role capabilities editor class
 *
 * @package    User-Role-Editor
 * @subpackage Editor
 * @author     Vladimir Garagulya <support@role-editor.com>
 * @copyright  Copyright (c) 2010 - 2021, Vladimir Garagulia
 **/
class URE_Editor {

    private static $instance = null;
    
    protected $lib = null;

    protected $role_additional_options = null;    
    protected $apply_to_all = 0;
    protected $capabilities_to_save = null;
    protected $caps_columns_quant = 1;
    protected $caps_readable = false;
    protected $current_role = '';
    protected $current_role_name = '';
    protected $full_capabilities = false;
    protected $notification = '';   // notification message to show on page    
    protected $roles = null;
    protected $show_deprecated_caps = false;    
    protected $ure_object = 'role';  // what to process, 'role' or 'user'      
    protected $user_to_edit = null;
    protected $wp_default_role = '';
    
    
    public static function get_instance() {
        
        if (self::$instance === null) {            
            // new static() will work too
            self::$instance = new URE_Editor();
        }

        return self::$instance;
    }
    // end of get_instance()
    
    
    private function __construct() {
        
        $this->lib = URE_Lib::get_instance();
    }
    // end of __construct()
    
    
    public function get($property_name) {
        
        if (!property_exists($this, $property_name)) {
            syslog(LOG_ERR, 'URE_Editor class does not have such property '. $property_name);
            return null;
        }
        
        return $this->$property_name;
    }
    // end of get_property()

    
    public function get_edit_user_caps_mode() {
        
        $multisite = $this->lib->get('multisite');
        if ($multisite && $this->lib->is_super_admin()) {
            return 1;
        }
        
        $edit_user_caps = $this->lib->get_option('edit_user_caps', 1);
        
        return $edit_user_caps;
    }
    // end of get_edit_user_caps_mode()
    
    
    
    // Validate information about user we intend to edit
    protected function check_user_to_edit() {

        if ( $this->ure_object !=='user' ) {
            return true;
        }
        
        if ( isset( $_REQUEST['user_id'] ) ) {
            $user_id = filter_var( $_REQUEST['user_id'], FILTER_VALIDATE_INT );
        } elseif ( isset( $_POST['values']['user_id'] ) ) {
            $user_id = filter_var( $_POST['values']['user_id'], FILTER_VALIDATE_INT );
        } else {
            return false;    // user_id value is missed
        }
        if ( empty( $user_id ) ) {
            return false;
        }

        $this->user_to_edit = get_user_to_edit( $user_id );
        if ( empty( $this->user_to_edit ) ) {
            return false;
        }
                            
        return true;
    }
    // end of check_user_to_edit()
    
    
    protected function get_caps_columns_quant() {
        
        if ( isset( $_POST['caps_columns_quant'] ) && in_array( $_POST['caps_columns_quant'], array(1,2,3) ) ) {
            $value = (int) filter_var( $_POST['caps_columns_quant'], FILTER_VALIDATE_INT );
            set_site_transient( 'ure_caps_columns_quant', $value, URE_Lib::TRANSIENT_EXPIRATION );
        } else {
            $value = get_site_transient( 'ure_caps_columns_quant' );
            if ( $value===false ) {
                $value = $this->lib->get_option( 'caps_columns_quant', 1 );
            }
        }
        
        return $value;        
    }
    // end of get_caps_columns_quant()

    
    protected function _init0() {
        // could be sent as via POST, as via GET
        if ( isset( $_REQUEST['object'] ) ) {
            $this->ure_object = $_REQUEST['object'];
        } elseif ( isset( $_POST['values']['object'] ) ) {  // AJAX POST
            $this->ure_object = $_POST['values']['object'];
        } else {
            $this->ure_object = 'role';
        }
        
        if ( $this->ure_object=='user') {
            if ( !$this->check_user_to_edit() ) {
                return false;
            }
        }
        
        $this->apply_to_all = isset( $_POST['values']['ure_apply_to_all']) ? true : false;
        if ( empty( $this->apply_to_all ) && isset( $_POST['ure_apply_to_all'] ) ) {
            $this->apply_to_all = true;
        }
        
        return true;
    }
    // end of _init0()
        
    
    protected function init0() {
        
        if ( !$this->_init0() ) {
            return false;
        }

        $this->caps_readable = get_site_transient( 'ure_caps_readable' );
        if ( false === $this->caps_readable ) {
            $this->caps_readable = $this->lib->get_option( 'ure_caps_readable' );
            set_site_transient( 'ure_caps_readable', $this->caps_readable, URE_Lib::TRANSIENT_EXPIRATION );
        }
        $this->show_deprecated_caps = get_site_transient( 'ure_show_deprecated_caps' );
        if ( false === $this->show_deprecated_caps ) {
            $this->show_deprecated_caps = $this->lib->get_option( 'ure_show_deprecated_caps' );
            set_site_transient( 'ure_show_deprecated_caps', $this->show_deprecated_caps, URE_Lib::TRANSIENT_EXPIRATION );
        }

        $this->hide_pro_banner = $this->lib->get_option( 'ure_hide_pro_banner', 0 );
        $this->wp_default_role = get_option( 'default_role' );        
        $this->caps_columns_quant = $this->get_caps_columns_quant();
        

        return true;
    }
    // end of init0() 


    protected function valid_nonce() {
        
        if ( empty( $_POST['ure_nonce'] ) || !wp_verify_nonce( $_POST['ure_nonce'], 'user-role-editor' ) ) {
            echo '<h3>Wrong or older request (invalid nonce value). Action prohibited.</h3>';
            return false;
        }
        
        return true;
    }
    // end of check_nonce()
    
    
    protected function set_caps_readable() {
        
        if ($this->caps_readable) {
            $this->caps_readable = 0;					
        } else {
            $this->caps_readable = 1;
        }
        set_site_transient( 'ure_caps_readable', $this->caps_readable, URE_Lib::TRANSIENT_EXPIRATION );        
        
    }
    // end of caps_readable()
    
    
    protected function set_show_deprecated_caps() {
        if ($this->show_deprecated_caps) {
            $this->show_deprecated_caps = 0;
        } else {
            $this->show_deprecated_caps = 1;
        }
        set_site_transient( 'ure_show_deprecated_caps', $this->show_deprecated_caps, URE_Lib::TRANSIENT_EXPIRATION );
    }
    // end of set_show_deprecated_caps()
            
    
    private function get_role_id() {
        
        if ( isset( $_POST['values']['user_role'] ) ) {
            $role_id = $_POST['values']['user_role'];
        } elseif ( isset( $_POST['user_role'] ) ) {
            $role_id = $_POST['user_role'];
        } else {
            $role_id = false;
        }
        
        return $role_id;
        
    }
    // end of get_role_id()
    
    
    public function init_current_role_name() {

        $role_id = $this->get_role_id();
        $this->current_role = '';
        $this->current_role_name = '';
        if ( empty( $role_id ) ) {
            $mess = esc_html__('Error: ', 'user-role-editor') . esc_html__('Wrong request!', 'user-role-editor');
        } else if ( !isset( $this->roles[$role_id] ) ) {
            $mess = esc_html__('Error: ', 'user-role-editor') . esc_html__('Role', 'user-role-editor') . ' <em>' . esc_html( $role_id ) . '</em> ' . 
                    esc_html__('does not exist', 'user-role-editor');
        } else {
            $this->current_role = $role_id;
            $roles = $this->lib->get_user_roles();
            $this->current_role_name = $roles[$this->current_role]['name'];
            $mess = '';
        }
        
        return $mess;        
    }
    // end of init_current_role_name()
    
    
    // Add existing WPBakery Visial Composer () plugin capabilities from this role to the list of capabilities for save with this role update -
    // Visual Composer capabilities are excluded from a role update as they may store not boolean values.
    protected function restore_visual_composer_caps() {
        
        if ( !isset( $this->roles[$this->current_role] ) || 
             !is_array( $this->roles[$this->current_role]['capabilities'] ) ) {
            return false;
        }
        
        foreach($this->roles[$this->current_role]['capabilities'] as $cap=>$value) {
            if (strpos($cap, 'vc_access_rules_')!==false) {
                $this->capabilities_to_save[$cap] = $value;
            }
        }
        
        return true;
    }
    // end of restore_visual_composer_caps()
    

    /**
     *  prepare capabilities from user input to save at the database
     */
    protected function prepare_capabilities_to_save() {
        
        $this->capabilities_to_save = array();
        if (empty($this->full_capabilities)) {
            return; // There is no valid initialization
        }
                
        foreach ( $this->full_capabilities as $cap ) {
            $cap_id_esc = URE_Capability::escape( $cap['inner'] );
            if ( isset( $_POST['values'][$cap_id_esc] ) || isset( $_POST[$cap_id_esc] ) ) {
                $this->capabilities_to_save[ $cap['inner'] ] = true;
            }
        }
        
        $this->restore_visual_composer_caps();
    }
    // end of prepare_capabilities_to_save()

    
    /**
     * Make full synchronization of roles for all sites with roles from the main site directly updating database records
     * 
     * @return boolean
     */
    protected function is_full_network_synch() {
        
        /* is_network_admin() returns wrong result for the AJAX calls from the network admin
         * so have to use this way
         */
        $network_admin = $this->lib->get_request_var('network_admin', 'post', 'int');
        if ( $network_admin==1 ) {   // for Pro version only
            $result = true;
        } else {
            $result = defined('URE_MULTISITE_DIRECT_UPDATE') && URE_MULTISITE_DIRECT_UPDATE == 1;
        }
        
        return $result;
    }
    // end of is_full_network_synch()    
    

    protected function last_check_before_update() {        
        
        if ( empty($this->roles ) || !is_array( $this->roles ) || count( $this->roles )===0 ) { 
            // Nothing to save - something goes wrong - stop execution...            
            return false;
        }
        
        $key_capability = URE_Own_Capabilities::get_key_capability();
        if ( current_user_can( $key_capability ) ) {    
            // current user is an URE admin
            return true;
        }
        
        if ( !current_user_can( 'ure_edit_roles' ) ) {
            // Not enough permissions
            return false;
        }
        
        $current_user = wp_get_current_user();
        if ( in_array( $this->current_role, $current_user->roles ) ) {
            // Do not allow to non-admin user without full access to URE update his own role
            return false;
        }
        
        return true;
    }
    // end of last_check_before_update()
            

    /**
     * Return true if $capability is included to the list of capabilities allowed for the single site administrator
     * @param string $capability - capability ID
     * @param boolean $ignore_super_admin - if 
     * @return boolean
     */
    public function block_cap_for_single_admin($capability, $ignore_super_admin=false) {
        
        if (!$this->lib->is_pro()) {    
            // this functionality is for the Pro version only.
            return false;
        }        
        $multisite = $this->lib->get('multisite');
        if ( !$multisite ) {    // work for multisite only
            return false;
        }
        if ( !$ignore_super_admin && $this->lib->is_super_admin() ) { 
            // Do not block superadmin
            return false;
        }
        $caps_access_restrict_for_simple_admin = $this->lib->get_option( 'caps_access_restrict_for_simple_admin', 0 );
        if ( !$caps_access_restrict_for_simple_admin ) {
            return false;
        }
        
        $allowed_caps = $this->lib->get_option( 'caps_allowed_for_single_admin', array() );
        if (in_array( $capability, $allowed_caps ) ) {
            $block_this_cap = false;
        } else {
            $block_this_cap = true;
        }
        
        return $block_this_cap;
    }
    // end of block_cap_for_single_admin()


    /**
     * Returns array without capabilities blocked for single site administrators
     * @param array $capabilities
     * @return array
     */
    protected function remove_caps_not_allowed_for_single_admin( $capabilities ) {
        if (!$this->lib->is_pro()) {    
            // this functionality is for the Pro version only.
            return $capabilities;
        }
        
        foreach( array_keys( $capabilities ) as $cap ) {
            if ( $this->block_cap_for_single_admin( $cap ) ) {
                unset( $capabilities[$cap] );
            }
        }
        
        return $capabilities;
    }
    // end of remove_caps_not_allowed_for_single_admin()

    
    protected function role_contains_caps_not_allowed_for_simple_admin( $role_id ) {
        
        $result = false;
        if (!$this->lib->is_pro()) {    
            // this functionality is for the Pro version only.
            return $result;
        }

        if ( !isset( $this->roles[$role_id]) ) {
            return false;
        }
        
        $role = $this->roles[$role_id];
        if ( !is_array( $role['capabilities'] ) ) {
            return false;
        }
        foreach ( array_keys( $role['capabilities'] ) as $cap ) {
            if ( $this->block_cap_for_single_admin( $cap ) ) {
                $result = true;
                break;
            }
        }
        
        return $result;
    } 
    // end of role_contains_caps_not_allowed_for_simple_admin()                
    
    
    // Save Roles to database
    protected function save_roles() {
        global $wpdb;

        if ( !$this->last_check_before_update() ) {
            return false;
        }
        
        if ( !isset($this->roles[$this->current_role]) ) {
            return false;
        }
        
        $this->capabilities_to_save = $this->remove_caps_not_allowed_for_single_admin( $this->capabilities_to_save );                
        $this->roles[$this->current_role]['name'] = $this->current_role_name;
        $this->roles[$this->current_role]['capabilities'] = $this->capabilities_to_save;
        $option_name = $wpdb->prefix . 'user_roles';

        update_option($option_name, $this->roles);

        // save additional options for the current role
        if (empty($this->role_additional_options)) {
            $this->role_additional_options = URE_Role_Additional_Options::get_instance($this->lib);
        }
        $this->role_additional_options->save($this->current_role);
        
        return true;
    }
    // end of save_roles()    


    protected function leave_roles_for_blog( $blog_id, $leave_roles ) {
        global $wpdb;
        
        $prefix = $wpdb->get_blog_prefix( $blog_id );
        $options_table_name = $prefix . 'options';
        $option_name = $prefix . 'user_roles';
        
        $query = "SELECT option_value
                    FROM {$options_table_name}
                    WHERE option_name='$option_name'
                    LIMIT 1";
        $value = $wpdb->get_var( $query );
        if ( empty( $value ) ) {
            return $this->roles;
        }
        $blog_roles = unserialize( $value );
        
        $roles = $this->roles;
        foreach( $leave_roles as $role_id ) {
            $roles[$role_id] = $blog_roles[$role_id];
        }
        $serialized_roles = serialize( $roles );
        
        return $serialized_roles;
    }
    // end of leave_roles_for_blog()
    
    
    /**
     * Update roles for all network using direct database access - quicker in several times
     * Execution speed is critical for large multi-site networks.
     * @global wpdb $wpdb
     * @return boolean
     */
    protected function direct_network_roles_update() {
        global $wpdb;

        $multisite = $this->lib->get( 'multisite' );
        if (!$multisite) {
            return false;
        }

        if ( !$this->last_check_before_update() ) {
            return false;
        }
        
        if ( !empty( $this->current_role ) ) {
            $this->roles[$this->current_role]['name'] = $this->current_role_name;
            $this->roles[$this->current_role]['capabilities'] = $this->capabilities_to_save;
        }

        $serialized_roles = serialize( $this->roles );
        $blog_ids = $this->lib->get_blog_ids();
        /*
         * Roles to leave unchanged during network update which overwrites all roles by default
         * $leave_roles = array( blog_id => array('role_id1', 'role_id2', ... );
         */
        $leave_roles = apply_filters('ure_network_update_leave_roles', array(), 10, 1 );
        if ( empty( $leave_roles ) || !is_array( $leave_roles ) ) {
            $leave_roles = array();
        }
        foreach ( $blog_ids as $blog_id ) {
            $prefix = $wpdb->get_blog_prefix( $blog_id );
            $options_table_name = $prefix . 'options';
            $option_name = $prefix . 'user_roles';
            if ( !isset( $leave_roles[$blog_id] ) ) {
                // overwrites all roles
                $roles = $serialized_roles;
            } else {
                // overwrite except roles you have to leave untouched for this blog
                $roles = $this->leave_roles_for_blog( $blog_id, $leave_roles[$blog_id] );
            }
            $query = "UPDATE {$options_table_name}
                SET option_value='$roles'
                WHERE option_name='$option_name'
                LIMIT 1";
            $wpdb->query( $query );
            if ( $wpdb->last_error ) {
                return false;
            }
            // @TODO: save role additional options     
                        
        }
        
        do_action( 'ure_direct_network_roles_update' );
        
        return true;
    }
    // end of direct_network_roles_update()        
    
    
    protected function wp_api_network_roles_update() {
        global $wpdb;
                
        $old_blog = $wpdb->blogid;
        $blog_ids = $this->lib->get_blog_ids();
        if (empty( $blog_ids ) ) {
            return false;
        }
        
        $result = true;
        foreach ( $blog_ids as $blog_id ) {
            switch_to_blog( $blog_id );
            $this->roles = $this->lib->get_user_roles();
            if ( !isset( $this->roles[$this->current_role] ) ) { // add new role to this blog
                $this->roles[$this->current_role] = array('name' => $this->current_role_name, 'capabilities' => array('read' => true));
            }
            if ( !$this->save_roles() ) {
                $result = false;
                break;
            }
        }
        $this->lib->restore_after_blog_switching( $old_blog );
        $this->roles = $this->lib->get_user_roles();
        
        return $result;
    }
    // end of wp_api_network_roles_update()    
    
    
    /**
     * Update role for all network using WordPress API
     * 
     * @return boolean
     */
    protected function multisite_update_roles() {
        
        $multisite = $this->lib->get('multisite');
        if (!$multisite) {
            return false;
        }
        
        $debug = $this->lib->get('debug');
        if ( $debug ) {
            $time_shot = microtime();
        }
        
        if ( $this->is_full_network_synch() ) {
            $result = $this->direct_network_roles_update();
        } else {
            $result = $this->wp_api_network_roles_update();            
        }
        
        // Use this action to hook a code to execute after roles were updated at all subsites of the multisite network
        do_action('ure_after_network_roles_update');

        if ($debug) {
            echo '<div class="updated fade below-h2">Roles updated for ' . ( microtime() - $time_shot ) . ' milliseconds</div>';
        }

        return $result;
    }
    // end of multisite_update_roles()    
    
    
    /**
     * Process user request on update roles
     * 
     * @global WP_Roles $wp_roles
     * @return boolean
     */
    protected function update_roles() {
        global $wp_roles;
        
        $multisite = $this->lib->get( 'multisite' );
        if ( $multisite && $this->lib->is_super_admin() && $this->apply_to_all ) {  
            // update Role for all blogs/sites in the network (permitted to superadmin only)
            if (!$this->multisite_update_roles()) {
                return false;
            }
        } else {
            if (!$this->save_roles()) {
                return false;
            }
        }

        // refresh global $wp_roles
        $wp_roles = new WP_Roles();
        
        return true;
    }
    // end of update_roles()    

   
    /**
     * if existing user was not added to the current blog - add him
     * @global type $blog_id
     * @param type $user
     * @return bool
     */
    protected function check_blog_user( $user ) {
        global $blog_id;
        
        $result = true;
        if ( is_network_admin() ) {
            if (!array_key_exists( $blog_id, get_blogs_of_user( $user->ID ) ) ) {
                $result = add_existing_user_to_blog( array( 'user_id' => $user->ID, 'role' => 'subscriber' ) );
            }
        }

        return $result;
    }
    // end of check_blog_user()

    
    private function get_user_primary_role( $user ) {
        
        $role = ( is_array( $user->roles ) && count( $user->roles )>0 ) ? $user->roles[0] : '';
        
        return $role;
    }
    
    
    private function get_new_primary_role( $user ) {
        $wp_roles = wp_roles();
        
        $select_primary_role = apply_filters( 'ure_users_select_primary_role', true );
        if ( $select_primary_role  || $this->lib->is_super_admin()) {
            $role = isset( $_POST['values']['primary_role'] ) ? URE_Base_Lib::filter_string_var( $_POST['values']['primary_role'] ) : false;  
            if ( empty( $role ) || !isset( $wp_roles->roles[$role] ) ) {
                $role = '';
            }
        } else {
            $role = $this->get_user_primary_role( $user );
        }
        
        return $role;
    }
    // end of get_new_primary_role()
    
    
    private function get_bbpress_role( $user ) {
        
        $bbpress = $this->lib->get_bbpress();        
        if ( $bbpress->is_active() ) {
            $role = bbp_get_user_role( $user->ID );
        } else {
            $role = '';
        }
        
        return $role;
    }
    // end of get_bbpress_role()
    
    /**
     * Add other roles to roles array for this user, extracting selected values from the POST
     * 
     * @param array $roles
     */
    private function add_other_roles( $roles ) {
        
        $wp_roles = wp_roles();
        
        $post_values = isset( $_POST['values'] ) && is_array( $_POST['values'] ) ? $_POST['values'] : array();
        foreach ( $post_values as $key => $value ) {
            $result = preg_match( '/^wp_role_(.+)/', $key, $match );
            if ( $result !== 1 ) {
                continue;
            }
            $role = $match[1];
            if ( !isset( $wp_roles->roles[$role] ) ) {
                // Skip role if it does not exist
                continue;
            }
            if ( !in_array( $role, $roles ) ) {
                $roles[] = $role;
            }
        }

        return $roles;
    }
    // end of add_other_roles()
    
    
    /**
     * Provide a valid placement of a primary role - 1st element of roles array
     * 
     * @param WP_User $user
     * @param array $new_roles
     */    
    private function set_primary_role( $user, $new_primary_role ) {

        
        if ( count( $user->roles )<=1 ) {
            // User does not have roles at all or has only single one
            return;
        }
                        
        $current_primary_role = reset( $user->roles );
        if ( $current_primary_role === $new_primary_role ) {
            // Current primary role is equal to a new one - nothing was changed
            return;
        }                
        
        // remove primary role from user capabilities array
        unset( $user->caps[$new_primary_role] );    
        // insert new primary role as the 1st elemnt of user capabilities array
        $user->caps = array($new_primary_role=>true) + $user->caps;
        
        // update user permissions ar WordPress internal data structures - exactly the same way as WordPress itself does at WP_User::add_role()
        update_user_meta( $user->ID, $user->cap_key, $user->caps );
	$user->get_role_caps();
        $user->update_user_level_from_caps();
        
    }
    // end of set_primary_role()
    
    
    private function update_user_roles( $user, $new_roles ) {
        
        foreach( $user->roles as $role ) {
            if ( !in_array( $role, $new_roles ) ) {
                $user->remove_role( $role );
            }
        }
        
        foreach( $new_roles as $role ) {
            if ( !in_array( $role, $user->roles ) ) {
                $user->add_role( $role );
            }
        }               
        
        if ( !empty( $new_roles ) ) {
            $this->set_primary_role( $user, $new_roles[0] );
        }
                
    }
    // end of update_user_roles()

    /**
     * Remove from user directly granted capabilities
     * 
     * @param WP_User $user 
     */
    private function remove_user_capabilities( $user, $caps_to_save ) {

        if ( empty( $user->caps ) ) {
            return;
        }
        
        $roles = wp_roles()->roles;
        $roles_id = array_keys( $roles );
        $caps = array_keys( $user->caps );
        foreach( $caps as $cap ) {
            if ( in_array( $cap, $roles_id ) ) {
                // It's a role ID, skip it
                continue;
            }            
            if ( !in_array( $cap, $caps_to_save ) ) {                
                $user->remove_cap( $cap );
            }
        }
        
    }
    // end of remove_user_capabilities()
    
    
    /**
     * Update individual capabilities of the user
     * 
     * @param WP_User $user
     */
    private function update_user_capabilities( $user ) {
        // $edit_user_caps = $this->get_edit_user_caps_mode();
        
        $caps_to_save = array_keys( $this->capabilities_to_save );        
        $this->remove_user_capabilities( $user, $caps_to_save );               
        $user_caps = array_keys( $user->caps );
        
        // process new added capabilities
        foreach ($caps_to_save as $cap ) {
            if ( !in_array( $cap, $user_caps ) ) {
                $user->add_cap( $cap );
            }
        }

    } 
    // end of update_user_capabilities()
    
    
    /**
     * Update user roles and capabilities
     * 
     * @global WP_Roles $wp_roles
     * @param WP_User $user
     * @return boolean
     */
    protected function update_user( $user ) {
           
        if ( !is_a( $user, 'WP_User') ) {
            return false;
        }
        
        do_action( 'ure_before_user_permissions_update', $user->ID );
                
        $multisite = $this->lib->get('multisite');
        if ($multisite) {
            if ( !$this->check_blog_user( $user ) ) {
                return false;
            }
        }
        
        $new_primary_role = $this->get_new_primary_role( $user );
        $bbp_role = $this->get_bbpress_role( $user );                                        
        // Build new roles array for the user
        $new_roles = array();
        
        // restore primary role
        if ( !empty( $new_primary_role ) ) {
            $new_roles[] = $new_primary_role;
        }
        
        // restore bbPress user role if user had one
        if ( !empty( $bbp_role ) ) {
            $new_roles[] = $bbp_role;
        }
        
        $new_roles = $this->add_other_roles( $new_roles );
        $this->update_user_roles( $user, $new_roles );        
        $this->update_user_capabilities( $user );                
        
        do_action('ure_user_permissions_update', $user->ID, $user);  // In order other plugins may hook to the user permissions update
                        
        return true;
    }
    // end of update_user()
        
    
    /**
     *  Save changes to the roles or user
     *  @param string $mess - notification message to the user
     *  @return string - notification message to the user
     */
    protected function permissions_object_update( $mess ) {

        if ( !empty( $mess ) ) {
            $mess .= '<br/>';
        }                
        
        if ( $this->ure_object === 'role' ) {  // save role changes to database
            if ($this->update_roles()) {                
                if (!$this->apply_to_all) {
                    $mess = esc_html__('Role is updated successfully', 'user-role-editor');
                } else {
                    $mess = esc_html__('Roles are updated for all network', 'user-role-editor');
                }
            } else {
                $mess = esc_html__('Error occurred during role(s) update', 'user-role-editor');
            }
        } else {
            if ($this->update_user($this->user_to_edit)) {
                $mess = esc_html__('User capabilities are updated successfully', 'user-role-editor');
            } else {
                $mess = esc_html__('Error occurred during user update', 'user-role-editor');
            }
        }
        
        return $mess;
    }
    // end of permissions_object_update()
    
    
    /**
     * Return WordPress user roles to its initial state, just like after installation
     * @global WP_Roles $wp_roles
     */
    protected function wp_roles_reinit() {
        global $wp_roles, $wp_user_roles;
        
        $wp_user_roles = null;
        $wp_roles->roles = array();
        $wp_roles->role_objects = array();
        $wp_roles->role_names = array();
        $wp_roles->use_db = true;

        require_once(ABSPATH . '/wp-admin/includes/schema.php');
        populate_roles();
        $wp_roles = new WP_Roles();
        
        $this->roles = $this->lib->get_user_roles();
        
    }
    // end of wp_roles_reinit()
    
    /**
     * Reset user roles to WordPress default roles
     */
    public function reset_user_roles() {
        
        if (!current_user_can('ure_reset_roles')) {            
            esc_html_e('Insufficient permissions to work with User Role Editor','user-role-editor');
            $debug = ( defined('WP_PHP_UNIT_TEST') && WP_PHP_UNIT_TEST==true );
            if ( !$debug ) {
                die;
            } else {
                return false;
            }
        }
              
        $this->wp_roles_reinit();
        URE_Own_Capabilities::init_caps();
        
        $multisite = $this->lib->get('multisite');
        if ( !$multisite ) {
            return true;
        }
        
        $this->apply_to_all = $this->lib->get_request_var('ure_apply_to_all', 'post', 'checkbox');
        if ($this->apply_to_all) {
            $this->current_role = '';
            $this->direct_network_roles_update();
        }
        
        return true;
    }
    // end of reset_user_roles()    
    
    
    protected function get_role_id_from_post() {
        
        $result = array('role_id'=>'', 'message'=>'');
        $role_id = $this->lib->get_request_var('user_role_id', 'post' );
        if ( empty( $role_id ) ) {
            $result['message'] = esc_html__('Error: Role ID is empty!', 'user-role-editor' );
            return $result;
        }        
        $role_id = utf8_decode( $role_id );
        // sanitize user input for security
        $match = array();
        $valid_name = preg_match( '/[A-Za-z0-9_\-]*/', $role_id, $match );
        if ( !$valid_name || ( $valid_name && ( $match[0] !== $role_id ) ) ) { 
            // Some non-alphanumeric charactes found!
            $result['message'] = esc_html__( 'Error: Role ID must contain latin characters, digits, hyphens or underscore only!', 'user-role-editor' );
            return $result;
        }
        $numeric_name = preg_match( '/[0-9]*/', $role_id, $match );
        if ( $numeric_name && ( $match[0] === $role_id ) ) { 
            // Numeric name discovered
            $result['message'] = esc_html__( 'Error: WordPress does not support numeric Role name (ID). Add latin characters to it.', 'user-role-editor' );
            return $result;
        }         
                
        $result['role_id'] = strtolower( $role_id );
        
        return $result;
    }
    // end of get_role_id_from_post()
    
    
    /**
     * Process new role creation request
     * 
     * @return array   - operation result data
     * 
     */
    public function add_new_role() {

        $response = array('result'=>'error', 'role_id'=>'', 'role_name'=>'', 'message'=>'');
        if ( !current_user_can( 'ure_create_roles' ) ) {
            $response['message'] = esc_html__( 'Insufficient permissions to work with User Role Editor','user-role-editor' );
            return $response;
        }
        
        $result = $this->get_role_id_from_post();
        if ( !empty( $result['message'] ) ) {
            $response['message'] = $result['message'];
            return $response;
        }
        
        $role_id = $result['role_id']; 
        $wp_roles = wp_roles();        
        if ( isset( $wp_roles->roles[$role_id] ) ) {
            $response['message'] = sprintf( 'Error! ' . esc_html__( 'Role %s exists already', 'user-role-editor' ), $role_id );
            return $response;
        }

        $role_name = $this->lib->get_request_var( 'user_role_name', 'post' );
        if ( empty( $role_name ) ) {
            $role_name = $role_id;  // as user role name is empty, use user role ID instead as a default value
        }
        $this->current_role = $role_id;
        $role_copy_from = $this->lib->get_request_var( 'user_role_copy_from', 'post' );
        if ( !empty( $role_copy_from ) && $role_copy_from!=='none' && $wp_roles->is_role( $role_copy_from ) ) {
            $role = $wp_roles->get_role($role_copy_from);
            $capabilities = $this->remove_caps_not_allowed_for_single_admin( $role->capabilities );
        } else {
            $capabilities = array('read' => true, 'level_0' => true);   // Use subscriber role permissions as a default value
        }        
        // add new role to the roles array      
        $result = add_role( $role_id, $role_name, $capabilities );
        if ( !isset( $result ) || empty( $result ) ) {
            $response['message'] = 'Error! ' . esc_html__('Error is encountered during new role create operation', 'user-role-editor' );
            return $response;
        }
        
        $response['result'] = 'success';
        $response['role_id'] = $role_id;
        $response['role_name'] = $role_name;
        $response['message'] = sprintf(esc_html__('Role %s is created successfully', 'user-role-editor'), $role_name );
                
        
        return $response;
    }
    // end of add_new_role()    
    

    public function update_role() {
        
        $response = array('result'=>'error', 'role_id'=>'', 'role_name'=>'', 'message'=>'');
        if ( !current_user_can( 'ure_edit_roles' ) ) {
            $response['message'] = esc_html__( 'Insufficient permissions to work with User Role Editor','user-role-editor' );
            return $response;
        }
        
        $this->_init0();                
        if ( $this->ure_object==='role') {
            $result = $this->get_role_id_from_post();
            if ( !empty( $result['message'] ) ) {
                $response['message'] = $result['message'];
                return $response;
            }
        }
        
        $this->roles = $this->lib->get_user_roles();
        $this->full_capabilities = $this->lib->init_full_capabilities( $this->ure_object );
        if ( isset( $_POST['values']['user_role'] ) ) {
            $this->notification = $this->init_current_role_name();
        }
        $this->prepare_capabilities_to_save();
        $this->notification = $this->permissions_object_update( $this->notification );
        
        $response['result'] = 'success';
        if ( $this->ure_object==='role') {
            $response['role_id'] = $result['role_id'];
            $response['role_name'] = $this->current_role_name;
        }
        $response['message'] = $this->notification;
        
        return $response;
    }
    // end of update_role()
    
    
    public function update_network() {
        
        $this->init0();
        $this->roles = $this->lib->get_user_roles();
        $this->full_capabilities = $this->lib->init_full_capabilities( $this->ure_object );
        if ( isset( $_POST['user_role'] ) ) {
            $this->notification = $this->init_current_role_name();
        }
        $this->prepare_capabilities_to_save();
        $this->notification = $this->permissions_object_update( $this->notification );
                
    }
    // end of update_network()
        
    
    /**
     * process rename role request
     * 
     * @global WP_Roles $wp_roles
     * 
     * @return string   - message about operation result
     * 
     */
    public function rename_role() {
        global $wp_roles;

        $response = array('result'=>'error', 'message'=>'', 'role_id'=>'', 'role_name'=>'');
        if ( !current_user_can( 'ure_edit_roles' ) ) {
            $response['message'] = esc_html__( 'Insufficient permissions to work with User Role Editor','user-role-editor' );
            return $response;
        }
        
        $result = $this->get_role_id_from_post();
        if ( !empty( $result['message'] ) ) {
            $response['message'] = $result['message'];
            return $response;
        }        
        
        $new_role_name = $this->lib->get_request_var('user_role_name', 'post' );        
        if ( empty( $new_role_name ) ) {            
            $response['message']  = esc_html__( 'Error: Empty role display name is not allowed.', 'user-role-editor' );
            return $response;
        }        
        
        $role_id = $result['role_id'];
        $wp_roles = wp_roles();
        if ( !isset( $wp_roles->roles[$role_id] ) ) {
            $response['message'] = sprintf('Error! ' . esc_html__('Role %s does not exists', 'user-role-editor'), $role_id);
            return $response;
        }
        
        $new_role_name = sanitize_text_field( $new_role_name );        
        $this->current_role = $role_id;
        $this->current_role_name = $new_role_name;

        $old_role_name = $wp_roles->roles[$role_id]['name'];
        $wp_roles->roles[$role_id]['name'] = $new_role_name;
        update_option( $wp_roles->role_key, $wp_roles->roles );
        
        $response['result'] = 'success';
        $response['message'] = sprintf( esc_html__('Role %s is renamed to %s successfully', 'user-role-editor'), $old_role_name, $new_role_name );
        $response['role_id'] = $role_id;
        $response['role_name'] = $new_role_name;
        
        return $response;
    }
    // end of rename_role()        
    
    
    protected function get_wp_built_in_roles() {
        
        $result = array('subscriber', 'contributor', 'author', 'editor', 'administrator');
        
        return $result;
    }
    // end of get_wp_built_in_roles()
    
    /**
     * return array with roles which we could delete, e.g self-created and not used with any blog user
     * 
     * @return array 
     */
    public function get_roles_can_delete() {

        $default_role = get_option( 'default_role' );
        $wp_built_in_roles = $this->get_wp_built_in_roles();
        $roles_can_delete = array();
        $users = count_users();
        $roles = $this->lib->get_user_roles();
        foreach ($roles as $key => $role) {
            $can_delete = true;
            // check if it is default role for new users
            if ( $key === $default_role ) {
                $can_delete = false;
                continue;
            }
            // Do not allow to delete WordPress built-in role
            if ( in_array( $key, $wp_built_in_roles ) ) {
                continue;
            }
            // check if role has capabilities prohibited for the single site administrator
            if ( $this->role_contains_caps_not_allowed_for_simple_admin( $key ) ) {
                continue;
            }                        
            if ( !isset( $users['avail_roles'][$key] ) ) {
                $roles_can_delete[$key] = $role['name'] . ' (' . $key . ')';
            }
        }
        
        return $roles_can_delete;
    }
    // end of get_roles_can_delete()


    /**
     * Deletes user role from the WP database
     */
    protected function delete_wp_roles( $roles_to_del ) {
        global $wp_roles;

        $result = array('result'=>false, 'deleted_roles'=>array(), 'message'=>'');        
        $roles_can_delete = $this->get_roles_can_delete();
        $wp_roles = wp_roles();
        foreach($roles_to_del as $role_id) {
            if ( !isset( $wp_roles->roles[$role_id] ) ) {
                $result['message'] = esc_html__('Role does not exist','user-role-editor') .' - '. $role_id;
                return $result;
            }
            if ( !isset( $roles_can_delete[$role_id]) ) {
                $result['message'] = esc_html__( 'You can not delete role', 'user-role-editor' ) .' - '.$role_id;
                return $result;
            }                                                        
            
            unset( $wp_roles->role_objects[$role_id] );
            unset( $wp_roles->role_names[$role_id] );
            unset( $wp_roles->roles[$role_id] );
            $result['deleted_roles'][] = $role_id;
            $result['result'] = true;
        }   // foreach()
        if ( $result['result'] ) {
            update_option( $wp_roles->role_key, $wp_roles->roles );            
        }
        
        return $result;
    }
    // end of delete_wp_roles()    
    
    
    protected function delete_all_unused_roles() {        
        
        $roles_to_del = array_keys( $this->get_roles_can_delete() );  
        if ( empty( $roles_to_del ) || !is_array( $roles_to_del ) ) {
            $result = array(
                'result'=>false, 
                'deleted_roles'=>array(), 
                'message'=>esc_html__( 'There are no roles for deletion','user-role-editor' ));
            return $result;
        }
        
        $result = $this->delete_wp_roles( $roles_to_del );
        
        return $result;        
    }
    // end of delete_all_unused_roles()
    
    
    /**
     * Process user request for user role deletion
     * @return string
     */
    public function delete_role() {        
        
        $response = array('result'=>'error', 'message'=>'', 'deleted_roles'=>array());
        if ( !current_user_can('ure_delete_roles') ) {
            $response['message'] = esc_html__('Insufficient permissions to work with User Role Editor','user-role-editor');
            return $response;
        }

        $role_id = $this->lib->get_request_var( 'user_role_id', 'post');
        if ( $role_id==-1 ) { // delete all unused roles
            $result = $this->delete_all_unused_roles();
        } else {
            if ( empty( $role_id ) ) {
                $response['message'] = esc_html__( 'Wrong role ID','user-role-editor');
                return $response;
            }
            $result = $this->delete_wp_roles( array( $role_id ) );
        }
        if ( $result['result']===true ) {
            $response['result'] = 'success';
            $response['deleted_roles'] = $result['deleted_roles'];
            if ( $role_id==-1 ) {
                $response['message'] = esc_html__( 'Unused roles are deleted successfully', 'user-role-editor' );
            } else {
                $response['message'] = sprintf( esc_html__( 'Role %s is deleted successfully', 'user-role-editor' ), $role_id );
            }
        } else {
            $response['message'] = $result['message'];
        }
        if ( isset( $_POST['user_role_id'] ) ) {            
            unset( $_POST['user_role_id'] );
        }

        return $response;
    }
    // end of delete_role()
    
    
    /**
     * Change default WordPress role
     * This method is only for a single site of WordPress multisite installation.
     * @return string
     */
    protected function change_default_role() {
        
        if ( !current_user_can('ure_delete_roles') ) {
            $mess = esc_html__('Insufficient permissions to work with User Role Editor','user-role-editor');
            return $mess;
        }
        
        $multisite = $this->lib->get('multisite');
        if ( !$multisite || is_network_admin() ) {
            $mess = esc_html__('This method is only for a single site of WordPress multisite installation.', 'user-role-editor');
            return $mess;
        }
        if ( empty( $_POST['user_role_id'] ) ) {
            $mess = esc_html__('Wrong request. Default role can not be empty', 'user-role-editor');
            return $mess;
        }
        
        $mess = '';
        $wp_roles = wp_roles();        
        $role_id = $this->lib->get_request_var('user_role_id', 'post');
        unset( $_POST['user_role_id'] );
        if ( isset( $wp_roles->role_objects[$role_id] ) && $role_id !== 'administrator' ) {
            update_option( 'default_role', $role_id );
            $this->wp_default_role = get_option( 'default_role' );
            if ($this->wp_default_role===$role_id) {
                $mess = sprintf(esc_html__('Default role for new users is set to %s successfully', 'user-role-editor'), $wp_roles->role_names[$role_id]);
            } else {
                $mess = 'Error! ' . esc_html__('Error encountered during default role change operation', 'user-role-editor');
            }
        } elseif ($role_id === 'administrator') {
            $mess = 'Error! ' . esc_html__('Can not set Administrator role as a default one', 'user-role-editor');
        } else {
            $mess = 'Error! ' . esc_html__('This role does not exist - ', 'user-role-editor') . esc_html($role_id);
        }
        

        return $mess;
    }
    // end of change_default_role()
    
    
    /**
     * Process user request
     */
    protected function process_user_request() {

        $this->notification = '';
        if ( !isset( $_POST['action'] ) ) {
            return false;
        }
        if ( !$this->valid_nonce() ) {
            if ( defined('WP_DEBUG') && WP_DEBUG ) {
                return false;
            } else {
                exit;
            }
        }
        
        $action = $this->lib->get_request_var('action', 'post');
        switch ( $action ) {
            case 'reset': {
                $this->reset_user_roles();
                exit;            
            }
            case 'change-default-role': {
                $this->notification = $this->change_default_role();
                break;
            }
            case 'caps-readable': {
                $this->set_caps_readable();                
                break;
            }
            case 'show-deprecated-caps': {
                $this->set_show_deprecated_caps();                
                break;
            }
            case 'roles_restore_note': {
                $this->notification = esc_html__( 'User Roles are restored to WordPress default values. ', 'user-role-editor' );
                break;
            }
            default: {
                do_action( 'ure_process_user_request' );
            }
        }   // switch ( $action ) ....
              
        return true;
    }
    // end of process_user_request()
    
    
    public function init1() {

        $this->roles = $this->lib->get_user_roles();
        $this->full_capabilities = $this->lib->init_full_capabilities( $this->ure_object );
        if ( empty( $this->role_additional_options ) ) {
            $this->role_additional_options = URE_Role_Additional_Options::get_instance( $this->lib );
        }
                
    }
    // end of editor_init1()    
    
    
    /**
     * Return id of role last in the list of sorted roles
     * 
     */
    protected function get_last_role_id() {
        
        // get the key of the last element in roles array
        $keys = array_keys( $this->roles );
        asort( $keys );
        $last_role_id = array_pop( $keys );
        
        return $last_role_id;
    }
    // end of get_last_role_id()

    
    protected function set_current_role() {
        
        if (!isset($this->current_role) || !$this->current_role) {
            if (isset($_REQUEST['user_role']) && $_REQUEST['user_role'] && isset($this->roles[$_REQUEST['user_role']])) {
                $this->current_role = $_REQUEST['user_role'];
            } else {
                $this->current_role = $this->get_last_role_id();
            }
            $roles = $this->lib->get_user_roles();
            $this->current_role_name = $roles[$this->current_role]['name'];
        }
        
    }
    // end of set_current_role()

    
    // returns true if editing user has $capability assigned through the roles or directly
    // returns true if editing user has role with name equal $capability
    public function user_can($capability) {
        
        if (isset($this->user_to_edit->caps[$capability])) {
            return true;
        }
        foreach ($this->user_to_edit->roles as $role) {
            if ($role===$capability) {
                return true;
            }
            if (!empty($this->roles[$role]['capabilities'][$capability])) {
                return true;
            }
        }
                
        return false;        
    }
    // end of user_can()
    
    
    protected function show_editor() {

        $this->lib->show_message( $this->notification );
        if ( $this->ure_object == 'user' ) {
            $view = new URE_User_View();
        } else {
            $this->set_current_role();
            $view = new URE_Role_View();
            $view->role_edit_prepare_html();
        }
        ?>
        <div class="wrap">
            <h1><?php _e('User Role Editor', 'user-role-editor'); ?></h1>
            <div id="ure_container">                
                <div id="user_role_editor" class="ure-table-cell" >
                    <form id="ure_form" method="post" action="<?php echo admin_url() . URE_PARENT . '?page=users-' . URE_PLUGIN_FILE; ?>" >			
                        <div id="ure_form_controls">
        <?php
        $view->display();
        wp_nonce_field( 'user-role-editor', 'ure_nonce' );
        ?>
                            <input type="hidden" name="action" value="update" />
                        </div>      
                    </form>		      
<?php
        if ( !$this->lib->is_pro() ) {
            $view->advertise_pro();
        }
?>
                </div>
<?php
/*
        if (!$this->lib->is_pro()) {
            $view->advertise_commercials();
        } 
 */
        $view->display_edit_dialogs();
        do_action( 'ure_dialogs_html' );
        URE_Role_View::output_confirmation_dialog();
?>            
            </div>
        </div>
        <?php
    }
    // end of show_editor()
    
    
    /**
     *  Show main page according to the context - role or user editor
     */
    public function show() {

        if (!$this->init0()) {
            $message = esc_html__( 'Error: wrong request', 'user-role-editor' );
            $this->lib->show_message( $message );
            return false;
        }                
        
        $this->process_user_request();
        $this->init1();
        $this->show_editor();
        
        return true;
    }
    // end of show()


    public function set_notification($value) {
        
        $this->notification = $value;
        
    }
    // end of set_notification()
    

    /**
     * Not really used in the plugin - just storage for the translation strings
     */
    protected function translation_data() {
// for the translation purpose
        if (false) {
// Standard WordPress roles
            __('Editor', 'user-role-editor');
            __('Author', 'user-role-editor');
            __('Contributor', 'user-role-editor');
            __('Subscriber', 'user-role-editor');
// Standard WordPress capabilities
            __('Switch themes', 'user-role-editor');
            __('Edit themes', 'user-role-editor');
            __('Activate plugins', 'user-role-editor');
            __('Edit plugins', 'user-role-editor');
            __('Edit users', 'user-role-editor');
            __('Edit files', 'user-role-editor');
            __('Manage options', 'user-role-editor');
            __('Moderate comments', 'user-role-editor');
            __('Manage categories', 'user-role-editor');
            __('Manage links', 'user-role-editor');
            __('Upload files', 'user-role-editor');
            __('Import', 'user-role-editor');
            __('Unfiltered html', 'user-role-editor');
            __('Edit posts', 'user-role-editor');
            __('Edit others posts', 'user-role-editor');
            __('Edit published posts', 'user-role-editor');
            __('Publish posts', 'user-role-editor');
            __('Edit pages', 'user-role-editor');
            __('Read', 'user-role-editor');
            __('Level 10', 'user-role-editor');
            __('Level 9', 'user-role-editor');
            __('Level 8', 'user-role-editor');
            __('Level 7', 'user-role-editor');
            __('Level 6', 'user-role-editor');
            __('Level 5', 'user-role-editor');
            __('Level 4', 'user-role-editor');
            __('Level 3', 'user-role-editor');
            __('Level 2', 'user-role-editor');
            __('Level 1', 'user-role-editor');
            __('Level 0', 'user-role-editor');
            __('Edit others pages', 'user-role-editor');
            __('Edit published pages', 'user-role-editor');
            __('Publish pages', 'user-role-editor');
            __('Delete pages', 'user-role-editor');
            __('Delete others pages', 'user-role-editor');
            __('Delete published pages', 'user-role-editor');
            __('Delete posts', 'user-role-editor');
            __('Delete others posts', 'user-role-editor');
            __('Delete published posts', 'user-role-editor');
            __('Delete private posts', 'user-role-editor');
            __('Edit private posts', 'user-role-editor');
            __('Read private posts', 'user-role-editor');
            __('Delete private pages', 'user-role-editor');
            __('Edit private pages', 'user-role-editor');
            __('Read private pages', 'user-role-editor');
            __('Delete users', 'user-role-editor');
            __('Create users', 'user-role-editor');
            __('Unfiltered upload', 'user-role-editor');
            __('Edit dashboard', 'user-role-editor');
            __('Update plugins', 'user-role-editor');
            __('Delete plugins', 'user-role-editor');
            __('Install plugins', 'user-role-editor');
            __('Update themes', 'user-role-editor');
            __('Install themes', 'user-role-editor');
            __('Update core', 'user-role-editor');
            __('List users', 'user-role-editor');
            __('Remove users', 'user-role-editor');
            __('Add users', 'user-role-editor');
            __('Promote users', 'user-role-editor');
            __('Edit theme options', 'user-role-editor');
            __('Delete themes', 'user-role-editor');
            __('Export', 'user-role-editor');
        }
    }
    // end of translation_data()    
    
    
    
    /**
     * Prevent cloning of the instance of the *Singleton* instance.
     *
     * @return void
     */
    public function __clone() {
        throw new \Exception('Do not clone a singleton instance.');
    }
    // end of __clone()
    
    /**
     * Prevent unserializing of the *Singleton* instance.
     *
     * @return void
     */
    public function __wakeup() {
        throw new \Exception('Do not unserialize a singleton instance.');
    }
    // end of __wakeup()    
    
}
// end of URE_Editor class
