<?php
/*
	Copyright (C) 2015-25 CERBER TECH INC., https://wpcerber.com

    Licenced under the GNU GPL.

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

if ( ! defined( 'WPINC' ) ) { exit; }

final class CRB_DS {
	private static $setting = '_crb_ds_shadowing';
	private static $config = null;
	private static $the_user = null;
	private static $update_user = null;
	private static $acc_owner = false;
	private static $user_blocked = false;
	private static $user_fields = array( 'user_login' => 'lgn', 'user_pass' => 'pwd', 'user_email' => 'eml' );
	private static $opt_cache = array();
	private static $no_user_meta_shadow = ''; // Do not return user meta from shadow when meta is updating.
	//private static $user_metas = array( 'capabilities' );
	private static $setting_id; // A controlling plugin setting (last used)

	static function enable_shadowing( $type ) {

		if ( self::get_config( $type ) || ! lab_lab() ) {
			return;
		}

		$conf = self::get_config();

		if ( ! $conf ) {
			$conf = array();
		}

		$data    = array();
		$data[0] = time();
		$data[1] = 0;
		$data[2] = get_current_user_id();
		$data[3] = get_wp_cerber()->getRequestID();

		switch ( $type ) {
			case 1: // Users data

				if ( defined( 'CRB_USHD_KEY' ) && is_string( CRB_USHD_KEY ) ) {
					$data[5] = CRB_USHD_KEY; // If users' tables are shared among mutiple websites, define it in the wp-config.php on all websites before (!) activation shadowing
				}
				else {
					$data[5] = crb_random_string( 14, 16, false, false );
				}

				$conf[ $type ] = $data;
				self::save_config( $conf );

				self::update_user_shadow( get_current_user_id(), null, null, self::is_meta_preserve() );

				cerber_bg_task_add( '_crb_ds_background', array( 'exec_until' => 'done' ) );

				break;

			case 2: // Roles
			case 3: // Settings

				$data[1] = time(); // Should be set after all shadows has created
				$data[5] = crb_random_string( 10, 12, false, false );
				$data[6] = crb_random_string( 8, 14, true, false );

				$conf[ $type ] = $data;
				self::save_config( $conf );

				foreach ( self::get_protected_settings()[ $type ] as $item ) {
					self::update_setting_shadow( $item, get_option( $item ) );
				}

				break;
		}
	}

	static function disable_shadowing( $type ) {
		global $wpdb;

		if ( ! $type_conf = self::get_config( $type ) ) {
			return;
		}

		if ( ! is_super_admin() && ! nexus_is_valid_request() ) {
			return;
		}

		$conf = self::get_config();
		unset( $conf[ $type ] );
		self::save_config( $conf );

		switch ( $type ) {
			case 1:
				cerber_db_query( 'DELETE FROM ' . $wpdb->usermeta . ' WHERE meta_key ="' . $type_conf[5] . '"' );
				break;
			case 2:
			case 3:
				cerber_db_query( 'DELETE FROM ' . cerber_get_db_prefix() . CERBER_SETS_TABLE . ' WHERE the_key LIKE "' . $type_conf[5] . '%"' );
				break;
		}

	}

	static function is_ready( $type ) {

		if ( lab_lab() && ( $conf = self::get_config( $type ) ) && $conf[1] ) {
			return true;
		}

		return false;
	}

	private static function save_config( $conf ) {
		self::$config = $conf;

		// Encoding

		reset( $conf );
		$lenght = count( $conf );

		$b = rand( 1, 10 );
		//$config           = array_fill( $b, rand( 12, 14 ), 0 );
		$config = array_fill( $b, rand( $lenght + 8, $lenght + 10 ), 0 );
		$s = rand( 3, 5 );
		$config[ $b + 1 ] = $s; // crucial for extracting
		$config[ $b + $s ] = $conf;
		$config[ $b + $s + 1 ] = array( array( $b ) ); // is not used
		$config[] = $b + 3; // crucial for verification

		update_site_option( self::$setting, $config );
	}

	private static function get_config( $type = null ) {

		if ( ! isset( self::$config ) ) {

			$s = get_site_option( self::$setting );

			if ( ! $s || ! is_array( $s ) ) {
				return false;
			}

			reset( $s );
			if ( key( $s ) != ( end( $s ) - 3 ) ) {
				return false;
			}

			$s = array_values( $s );

			self::$config = $s[ $s[1] ];
		}

		if ( $type ) {
			if ( isset( self::$config[ $type ] ) ) {
				return self::$config[ $type ];
			}

			return false;
		}

		return self::$config;
	}

	/**
	 * Creating shadow in bulk
	 *
	 * @return bool true if not completed, false = completed or nothing to do
	 */
	static function iterate_users() {
		global $wpdb;

		if ( ( ! $type_conf = self::get_config( 1 ) ) || self::is_ready( 1 ) ) {
			return false;
		}

		$to_do = cerber_db_get_col( 'SELECT DISTINCT ID FROM ' . $wpdb->users . ' WHERE ID NOT IN (SELECT DISTINCT user_id FROM ' . $wpdb->usermeta . ' WHERE meta_key = "' . $type_conf[5] . '")' );

		if ( ! $to_do ) { // Creating shadow completed
			$conf       = self::get_config();
			$conf[1][1] = time();
			self::save_config( $conf );
			//cerber_diag_log( 'Create shadow completed' );

			return false;
		}

		foreach ( $to_do as $user_id ) {
			self::update_user_shadow( $user_id, null, null, self::is_meta_preserve() );
			//wp_cache_delete( $user_id, 'user_meta' );
		}

		return true;
	}

	private static function get_user_shadow( $user_id ) {
		if ( ! $user_id || ! $conf = self::get_config( 1 ) ) {
			return false;
		}

		$um = get_user_meta( $user_id, $conf[5], true );

		if ( ! $um ) {
			return array();
		}

		$val = self::decode( $um );
		$ret = crb_unserialize( $val );

		if ( ! $ret || ! is_array( $ret ) ) {
			$ret = array();
		}

		return $ret;
	}

	private static function update_user_shadow( $user_id, $fields = array(), $meta_data = array(), $meta_keys = array() ) {

		if ( ! $user_id || ! $conf = self::get_config( 1 ) ) {
			return false;
		}

		if ( ! $data = get_userdata( $user_id ) ) {
			return false; // Not a valid user
		}

		$sh = self::get_user_shadow( $user_id );

		$sh[0] = $user_id;

		if ( empty( $sh[1] ) ) {
			$sh[1] = array();
		}

		if ( $fields ) {
			$list = array_intersect_key( self::$user_fields, array_flip( $fields ) );
		}
		else {
			$list = self::$user_fields;
		}

		foreach ( $list as $user_field => $key ) {
			$sh[1][ $key ] = $data->data->$user_field;
		}

		if ( empty( $sh[2] ) ) {
			$sh[2] = array();
		}

		/*if ( ! $fields ) { // We use $fields only for updating password
			if ( empty( $sh[2] ) ) { // New user
				foreach ( self::is_meta_preserve() as $key ) {
					$sh[2][ $key ] = get_user_meta( $user_id, $key, true );
				}
			}
		}*/

		if ( ! empty( $meta_keys ) ) {
			foreach ( $meta_keys as $key ) {
				$sh[2][ $key ] = get_user_meta( $user_id, $key, true );
			}
		}

		if ( $meta_data ) {
			$sh[2] = array_merge( $sh[2], $meta_data );
		}

		return update_user_meta( $user_id, $conf[5], self::encode( serialize( $sh ) ) );
	}

	static function is_user_valid( $user_id = null ) {
		if ( ! self::is_ready( 1 ) ) {
			return true;
		}

		if ( ! $user_id ) {
			$user_id = get_current_user_id();
		}

		if ( ( $sh = self::get_user_shadow( $user_id ) )
		     && $sh[0] == $user_id ) {
			return true;
		}

		return false;
	}

	/**
	 * Return the password hash of a given user.
	 * Make sure that CRB_DS::is_ready() returns true before using result
	 *
	 * @param null $user_id
	 *
	 * @return string
	 */
	static function get_user_pass( $user_id ) {
		if ( ! $user_id ) {
			return '';
		}

		if ( ! ( $sh = self::get_user_shadow( $user_id ) )
		     || $sh[0] != $user_id
		     || ! ( $ret = crb_array_get( $sh[1], self::$user_fields['user_pass'] ) ) ) {
			return '';
		}

		return $ret;
	}

	/**
	 * @param $mode
	 * @param $user_id
	 * @param null $data User data from 'wp_pre_insert_user_data' filter
	 */
	static function acc_processor( $mode, $user_id, $data = null ) {

		if ( $mode == 'pass' ) {
			self::update_user_shadow( $user_id, array( 'user_pass' ) );

			return;
		}

		//$update_permitted = self::acc_update_permitted_by_ip();
		$update_permitted = ( crb_get_settings( 'ds_4acc_acl' ) && crb_acl_is_white() );

		switch ( $mode ) {
			case 'new':
				self::$update_user = null;
				if ( ! $update_permitted ) {
					$update_permitted = self::acc_new( $user_id );
				}
				if ( $update_permitted ) {
					self::update_user_shadow( $user_id );
				}
				break;

			case 'update':
				self::$update_user = $user_id;
				if ( ! $update_permitted ) {
					$update_permitted = self::acc_update( $user_id, $data );
				}
				if ( $update_permitted ) {
					// Must be deferred till user's data is saved to DB
					add_action( 'profile_update', function ( $user_id ) {
						CRB_DS::update_helper( $user_id );
					} );
				}
				break;
		}

	}

	static function update_helper( $user_id ) {
		if ( ( self::$update_user != $user_id )
		     || ( self::$user_blocked && ! self::$acc_owner ) ) {
			return;
		}

		self::update_user_shadow( $user_id );
	}

	/**
	 * Protect DB from an unauthorized user creation
	 *
	 * @param $user_id
	 *
	 * @return bool true if this operation is permitted
	 */
	private static function acc_new( $user_id ) {

		$set = crb_get_settings();
		self::$user_blocked = false;

		// Due to lack of a hook in the wp_insert_user() we are forced to check permissions and use wp_delete_user() after the user was created
		if ( ! is_user_logged_in() ) {
			if ( ! crb_user_has_role_strict( $set['ds_regs_roles'], $user_id ) ) {
				CRB_Globals::set_act_status( 32, 'ds_regs_roles' );
				self::$user_blocked = true;
			}
		}
		else {
			if ( ! crb_wp_user_has_role( $set['ds_add_acc'] ) ) {
				CRB_Globals::set_act_status( 33, 'ds_add_acc' );
				self::$user_blocked = true;
			}
		}

		if ( self::$user_blocked ) {
			require_once( ABSPATH . 'wp-admin/includes/user.php' );
			wp_delete_user( $user_id );
			cerber_log( 72 );
			remove_action( 'register_new_user', 'wp_send_new_user_notifications' );
			remove_action( 'edit_user_created_user', 'wp_send_new_user_notifications', 10 );
		}
		elseif ( ! has_filter( 'register_new_user', 'wp_send_new_user_notifications' ) ) {
			// this is needed in the case of a bulk user import
			add_action( 'register_new_user', 'wp_send_new_user_notifications' );
			add_action( 'edit_user_created_user', 'wp_send_new_user_notifications', 10 );
		}

		return ( ! self::$user_blocked );
	}

	/**
	 * Protect user's data from non-authorized modification
	 *
	 * @param int $user_id
	 * @param array $data User data from 'wp_pre_insert_user_data' filter
	 *
	 * @return bool true if this operation is permitted
	 */
	private static function acc_update( $user_id, $data ) {
		global $wpdb;

		$cid = get_current_user_id();

		self::$acc_owner = ( $user_id == $cid );
		self::$user_blocked = false;

		if ( ! crb_wp_user_has_role( crb_get_settings( 'ds_edit_acc' ) ) ) {

			// An exception: password reset requested (since WP 5.3)
			if ( ! empty( $data['user_activation_key'] ) && cerber_is_http_post() ) {
				// These fields cannot be changed during a normal password reset process
				$protected = array('user_pass','user_nicename','user_email','user_url','user_registered','display_name');
				$ok = true;
				if ( $row = cerber_db_get_row( 'SELECT * FROM ' . $wpdb->users . ' WHERE ID = ' . $user_id ) ) {
					foreach ( $protected as $field ) {
						if ( $row[ $field ] != $data[ $field ] ) {
							$ok = false;
							break;
						}
					}
				}

				if ( $ok ) {
					return true;
				}
			}

			self::$user_blocked = true;
			self::$setting_id = 'ds_edit_acc';

			if ( ! self::$acc_owner ) {
				// Protect the user's row in the users table
				add_filter( 'query', 'crb_empty_query', PHP_INT_MAX );
				add_filter( 'pre_get_col_charset', 'crb_return_wp_error', PHP_INT_MAX );
				CRB_Globals::set_act_status( ( ! $cid ) ? 34 : 33, 'ds_edit_acc' );
				cerber_log( 73 );
			}

			if ( ! has_filter( 'insert_user_meta', array( 'CRB_DS', 'user_meta' ) ) ) {
				add_filter( 'insert_user_meta', array( 'CRB_DS', 'user_meta' ), 0, 3 );
			}

		}

		return ( ! self::$user_blocked );
	}

	static function user_meta( $meta, $user, $update ) {
		self::$the_user = $user;

		if ( self::$user_blocked == true ) {
			if ( ! self::$acc_owner ) {
				// Must be removed after a single use for a given user
				remove_filter( 'query', 'crb_empty_query', PHP_INT_MAX );
				remove_filter( 'pre_get_col_charset', 'crb_return_wp_error', PHP_INT_MAX );
			}

			//return array(); // No user meta to update
		}

		return $meta;
	}

	/**
	 * Restricts updating if not allowed.
	 * Updates shadow of user meta (and roles) if allowed.
	 *
	 * @param $user_id
	 * @param $meta_key
	 * @param $meta_value
	 *
	 * @return bool
	 */
	static function update_user_meta( $user_id, $meta_key, $meta_value ) {

		// A user is not permitted to be created or updated?
		if ( self::$user_blocked ) {
			if ( self::is_meta_protected( $meta_key ) ) { // User roles are here
				CRB_Globals::set_act_status( is_user_logged_in() ? 33 : 34, self::$setting_id );
				cerber_log( 76 );
				self::$no_user_meta_shadow = '';

				return false;
			}
		}

		if ( true === self::is_meta_preserve( $meta_key ) ) {
			$ok = false;

			if ( crb_wp_user_has_role( crb_get_settings( 'ds_edit_acc' ) ) ) {
				$ok = true;
			}
			// Makes sense for user's role meta ONLY
			elseif ( is_array( $meta_value )
			         && ( $reg_roles = (array) crb_get_settings( 'ds_regs_roles' ) )
			         && ! array_diff_key( $meta_value, array_flip( $reg_roles ) ) ) {
				$ok = true;
			}

			if ( $ok ) {
				self::$no_user_meta_shadow = $meta_key;
				self::update_user_shadow( $user_id, null, array( $meta_key => $meta_value ) );
			}
		}

		return true;
	}

	private static function is_meta_preserve( $meta_key = null ) {
		global $wpdb;

		// TODO: add support for multisite via $wpdb->get_blog_prefix()

		$list = array( $wpdb->base_prefix . 'capabilities', $wpdb->base_prefix . 'user_level' );

		if ( $meta_key && in_array( $meta_key, $list ) ) {
			return true;
		}

		return $list;
	}

	private static function is_meta_protected( $meta_key ) {
		global $wpdb;

		if ( ( isset( self::$the_user ) && ( $meta_key == self::$the_user->cap_key ) ) // User roles are here
		     || $meta_key == $wpdb->get_blog_prefix() . 'user_level' ) {
			return true;
		}

		/*
		$metas = array();
		if ( in_array( $meta_key, $metas ) ) {
			return true;
		}*/

		return false;
	}

	static function get_shadow_user_meta( $user_id, $meta_key, $single ) {

		if ( self::$no_user_meta_shadow == $meta_key
		     || ( ( $conf = self::get_config( 1 ) ) && $conf[5] == $meta_key ) ) {
			return false; // Skip use shadow meta (infinite loop protection)
		}

		$sh = self::get_user_shadow( $user_id );
		if ( isset( $sh[2][ $meta_key ] ) ) {
			return array( $sh[2][ $meta_key ] );
		}

		return false;
	}

	/**
	 * Process settings updates. Updates shadow if permitted.
	 *
	 * @param $value
	 * @param $option
	 * @param $old_value
	 *
	 * @return mixed The old value if update is not permitted
	 */
	static function setting_processor( &$value, $option, &$old_value ) {

		if ( empty( self::get_protected_settings()[3][ $option ] ) ) {
			return $value;
		}

		if ( $value == $old_value
		     || ( is_array( $value ) && is_array( $old_value )
		          && ( serialize( $value ) === serialize( $old_value ) ) ) ) {
			return $value;
		}

		if ( crb_get_settings( 'ds_4opts_acl' ) && crb_acl_is_white() ) {
			self::update_setting_shadow( $option, $value );

			return $value;
		}

		if ( ! cerber_is_ip_allowed() ) {
			cerber_log( 75 );

			return $old_value;
		}

		$roles = crb_get_settings( 'ds_4opts_roles' );

		if ( ! $roles || ! crb_wp_user_has_role( $roles ) ) {
			CRB_Globals::set_act_status( is_user_logged_in() ? 33 : 34, 'ds_4opts_roles' );
			cerber_log( 75 );

			return $old_value;
		}

		self::update_setting_shadow( $option, $value );

		return $value;
	}

	/**
	 * Restrict access to updating roles data
	 *
	 * @param mixed $value Normally, it's an array containing all roles data
	 * @param string $option
	 * @param mixed $old_value Normally, it's an array containing all roles data
	 *
	 * @return array|mixed
	 */
	static function role_processor( &$value, $option, &$old_value ) {

		$role_key = wp_roles()->role_key;

		if ( $option !== $role_key ) {
			return $value;
		}

		if ( is_array( $value ) && is_array( $old_value )
		     && serialize( $value ) === serialize( $old_value ) ) {
			return $value;
		}

		CRB_Globals::set_act_status( 0 );

		if ( ! self::role_update_permitted( $value, $old_value ) ) {

			CRB_Globals::set_act_status_if( is_user_logged_in() ? 33 : 34, self::$setting_id );
			cerber_log( 74 );

			return $old_value;
		}

		self::update_setting_shadow( $option, $value );

		return $value;
	}

	/**
	 * Check if role data can be updated
	 *
	 * @param array $value
	 * @param array $old_value
	 *
	 * @return bool True if update is permitted
	 */
	static function role_update_permitted( &$value, &$old_value ): bool {

		if ( crb_get_settings( 'ds_4roles_acl' ) && crb_acl_is_white() ) {
			return true;
		}

		if ( ! cerber_is_ip_allowed() ) {
			return false;
		}

		$add  = crb_get_settings( 'ds_add_role' );
		$edit = crb_get_settings( 'ds_edit_role' );

		// Are there new or deleted roles?

		if ( crb_array_diff_keys( $value, $old_value ) ) {
			if ( ! $add || ! crb_wp_user_has_role( $add ) ) {
				self::$setting_id = 'ds_add_role';
				return false;
			}

			return true;
		}

		// There are some changes in capabilities or names

		if ( ! $edit || ! crb_wp_user_has_role( $edit ) ) {
			self::$setting_id = 'ds_edit_role';
			return false;
		}

		return true;
	}

	/*static function acc_update_permitted_by_ip() {
		if ( crb_get_settings( 'ds_4acc_acl' ) && crb_acl_is_white() ) {
			return true;
		}

		return false;
	}*/

	/**
	 * Install hooks for retrieving data of protected settings
	 *
	 */
	static function settings_hooks( $type ) {

		if ( ! self::is_ready( $type ) ) {
			return;
		}

		$list = self::get_protected_settings()[ $type ];

		foreach ( $list as $option ) {

			add_filter( "pre_option_{$option}", function ( $var, $option, $default ) {

				$value = CRB_DS::get_setting_shadow( $option );

				if ( $value ) {
					return $value;
				}

				/*add_filter( "option_{$option}", function ( $value, $option ) {
					CRB_DS::update_setting_shadow( $option, $value );

					return $value;
				}, 10, 2 );*/

				return $var;

			}, PHP_INT_MAX, 3 );

		}

	}

	static function get_setting_shadow( $option ) {
		// TODO: implement WP caching via wp_cache_add() / wp_cache_set()?
		if ( ! isset( self::$opt_cache[ $option ] ) ) {
			self::$opt_cache[ $option ] = cerber_get_set( self::get_setting_key( $option ) );
		}

		return self::$opt_cache[ $option ];
	}

	private static function update_setting_shadow( $option, $value ) {
		self::$opt_cache[ $option ] = $value;

		return cerber_update_set( self::get_setting_key( $option ), $value );
	}

	private static function get_setting_key( $option ) {
		global $wpdb;
		if ( $conf = self::get_config( 2 ) ) {
			return $conf[5] . sha1( $option . $conf[6] . $wpdb->get_blog_prefix() );
		}

		return '';
	}

	/**
	 * @return array
	 */
	private static function get_protected_settings() {
		global $wpdb;

		$list = array();

		$list[2] = array( $wpdb->get_blog_prefix() . 'user_roles' );

		if ( $set = crb_get_settings( 'ds_4opts_list' ) ) {
			$list[3] = $set;
		}
		else {
			$list[3] = array();
		}

		return $list;

	}

	/**
	 * A list of settings to protect
	 *
	 * @param bool $ui_labels
	 *
	 * @return array
	 */
	static function get_settings_list( $ui_labels = true ) {
		$set = array(
			'admin_email'        => __( 'Administration Email Address' ),
			'default_role'       => __( 'New User Default Role' ),
			'home'               => __( 'Site Address (URL)' ),
			'siteurl'            => __( 'WordPress Address (URL)' ),
			'users_can_register' => __( 'Anyone can register' ),
			'active_plugins'     => __( 'Active Plugins' ),
			'template'           => __( 'Active Theme' ),
		);

		if ( $ui_labels ) {
			return $set;
		}

		array_walk( $set, function ( &$e ) {
			$e = 1; // Default value is ON
		} );

		return $set;
	}

	private static function encode( $str ) {
		$str = base64_encode( $str );
		$s   = strlen( $str );
		$str = rtrim( $str, '=' );
		$num = $s - strlen( $str );
		$ret = $num . $str;

		return $ret;
	}

	private static function decode( $str ) {
		static $equs = array( '', '=', '==' );
		$num = substr( $str, 0, 1 );
		$str = ltrim( $str, (string) $num );

		return base64_decode( $str . $equs[ $num ] );
	}

	static function get_status() {
		$ret = array();

		if ( crb_get_settings( 'ds_4acc' ) ) {
			self::get_type_status( 1, $msg );
			$ret [] = 'Enabled for user accounts. ' . $msg;
		}
		if ( crb_get_settings( 'ds_4roles' ) ) {
			self::get_type_status( 2, $msg );
			$ret [] = 'Enabled for user roles. ' . $msg;
		}
		if ( crb_get_settings( 'ds_4opts' ) ) {
			self::get_type_status( 3, $msg );
			$ret [] = 'Enabled for site settings. ' . $msg;
		}

		return $ret;
	}

	private static function get_type_status( $n, &$msg = '' ) {
		if ( $conf = self::get_config( $n ) ) {
			if ( $conf[1] ) {
				$msg = 'Active since ' . cerber_date( $conf[1] ) . '.';
				return true;
			}
			else {
				$msg = 'Creating shadow data in progress. ';
				return true;
			}
		}

		$msg = 'Configuration has been corrupted. Please re-enable protection in the Data Shield settings.';
		return false;
	}

	// TODO: implement error notification

	/**
	 * @return WP_Error|false
	 */
	static function check_errors() {
		/*if () {
			return new WP_Error();
		}*/
		return false;
	}
}

if ( crb_get_settings( 'ds_4acc' ) && CRB_DS::is_ready( 1 ) ) {

	add_action( 'user_register', function ( $user_id ) {

		CRB_DS::acc_processor( 'new', $user_id );

	}, 0 );

	add_filter( 'wp_pre_insert_user_data', function ( $data, $update, $user_id ) {

		if ( $update ) {
			CRB_DS::acc_processor( 'update', $user_id, $data );
		}

		return $data;
	}, PHP_INT_MAX, 3 );

	add_filter( 'update_user_metadata', function ( $var, $object_id, $meta_key, $meta_value ) {
		// apply_filters( "update_{$meta_type}_metadata", null, $object_id, $meta_key, $meta_value, $prev_value );

		$allowed = CRB_DS::update_user_meta( $object_id, $meta_key, $meta_value );

		if ( ! $allowed ) {
			return true;
		}

		return $var;

	}, PHP_INT_MAX, 4 );

	add_filter( 'get_user_metadata', function ( $var, $object_id, $meta_key, $single ) {
		//$check = apply_filters( "get_{$meta_type}_metadata", null, $object_id, $meta_key, $single );

		if ( $meta = CRB_DS::get_shadow_user_meta( $object_id, $meta_key, $single ) ) {
			return $meta;
		}

		return $var;

	}, PHP_INT_MAX, 4 );

	add_action( 'crb_after_reset', function ( $null, $user_id ) {

		CRB_DS::acc_processor( 'pass', $user_id );

	}, 10, 2 );
}

if ( crb_get_settings( 'ds_4roles' ) && CRB_DS::is_ready( 2 ) ) {

	CRB_DS::settings_hooks( 2 );

	add_filter( 'pre_update_option', function ( $value, $option, $old_value ) {

		return CRB_DS::role_processor( $value, $option, $old_value );

	}, PHP_INT_MAX, 3 );

}

if ( crb_get_settings( 'ds_4opts' ) && CRB_DS::is_ready( 3 ) ) {

	CRB_DS::settings_hooks( 3 );

	add_filter( 'pre_update_option', function ( $value, $option, $old_value ) {

		return CRB_DS::setting_processor( $value, $option, $old_value );

	}, PHP_INT_MAX, 3 );

}

/**
 * A special SQL clause that produces empty result
 *
 * @return string
 */
function crb_empty_query() {
	global $wpdb;

	return 'SELECT 0 FROM ' . $wpdb->users;
}

/**
 * This helps to get rid of PHP warnings
 *
 * @return WP_Error
 */
function crb_return_wp_error() {
	return new WP_Error();
}

function _crb_ds_background() {
	if ( ! CRB_DS::iterate_users() ) {
		return 'done';
	}

	return 1;
}