'lgn', 'user_pass' => 'pwd', 'user_email' => 'eml' ); private static $opt_cache = array(); //private static $user_metas = array( 'capabilities' ); 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] = substr( str_shuffle( 'abcdefghijklmnopqrstuvwxyz' ), 0, rand( 14, 16 ) ); } $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( array( 'func' => '_crb_ds_background', 'exec_until' => 'done' ) ); break; case 2: // Roles case 3: // Settings $data[1] = time(); // Should be set after all shadows has created $data[5] = substr( str_shuffle( 'abcdefghijklmnopqrstuvwxyz' ), 0, rand( 10, 12 ) ); $data[6] = substr( str_shuffle( '0123456789abcdefghijklmnopqrstuvwxyz' ), 0, rand( 8, 14 ) ); $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(); } $ret = @unserialize( self::decode( $um ) ); 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_shadow = false; if ( crb_get_settings( 'ds_4acc_acl' ) && crb_acl_is_white() ) { $update_shadow = true; } switch ( $mode ) { case 'new': self::$update_user = null; if ( ! $update_shadow ) { $update_shadow = self::acc_new( $user_id ); } if ( $update_shadow ) { self::update_user_shadow( $user_id ); } break; case 'update': self::$update_user = $user_id; self::acc_update( $user_id, $data ); // 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 authorized user creation * * @param $user_id * * @return bool true if this operation is permitted */ private static function acc_new( $user_id ) { global $cerber_status; $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 ) ) { $cerber_status = 32; self::$user_blocked = true; } } else { if ( ! cerber_user_has_role( $set['ds_add_acc'] ) ) { $cerber_status = 33; 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 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 $cerber_status, $wpdb; $cid = get_current_user_id(); self::$acc_owner = ( $user_id == $cid ) ? true : false; self::$user_blocked = false; if ( ! cerber_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; 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 ); $cerber_status = ( ! $cid ) ? 34 : 33; 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; } /** * Protect/process users metas and roles from being updated * * @param $var * @param $user_id * @param $meta_key * @param $meta_value * @param $prev_value * * @return bool|null */ static function protect_user_meta( $var, $user_id, $meta_key, $meta_value, $prev_value ) { global $cerber_status; // 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 $cerber_status = ( ! is_user_logged_in() ) ? 34 : 33; cerber_log( 76 ); return false; } } if ( true === self::is_meta_preserve( $meta_key ) ) { $ok = false; //if ( ( self::$update_user == $user_id ) if ( cerber_user_has_role( crb_get_settings( 'ds_edit_acc' ) ) ) { $ok = true; } // Makes sense for user's role meta ONLY elseif ( is_array( $meta_value ) && ! array_diff_key( $meta_value, array_flip( crb_get_settings( 'ds_regs_roles' ) ) ) ) { $ok = true; } if ( $ok ) { self::update_user_shadow( $user_id, null, array( $meta_key => $meta_value ) ); } } return $var; } 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_user_meta( $user_id, $meta_key, $single ) { if ( ( $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 ) { global $cerber_status; 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 || ! cerber_user_has_role( $roles ) ) { $cerber_status = ( is_user_logged_in() ) ? 33 : 34; cerber_log( 75 ); return $old_value; } self::update_setting_shadow( $option, $value ); return $value; } static function role_processor( &$value, $option, &$old_value ) { global $cerber_status; if ( ! is_array( $value ) || ( substr( $option, - 11 ) != '_user_roles' ) ) { return $value; } if ( serialize( $value ) === serialize( $old_value ) ) { return $value; } $cerber_status = 0; if ( ! self::role_update_permitted( $value, $old_value ) ) { if ( ! $cerber_status ) { $cerber_status = ( is_user_logged_in() ) ? 33 : 34; } cerber_log( 74 ); return $old_value; } self::update_setting_shadow( $option, $value ); return $value; } /** * Check if role data can be updated * * @param $value * @param $old_value * * @return bool True if update is permitted */ static function role_update_permitted( &$value, &$old_value ) { 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' ); if ( ! $add && ! $edit ) { return false; } // Are there new or deleted roles? if ( crb_array_diff_keys( $value, $old_value ) ) { if ( ! $add || ! cerber_user_has_role( $add ) ) { return false; } return true; } // There are some changes in capabilities or names if ( ! $edit || ! cerber_user_has_role( $edit ) ) { return false; } return true; } /** * 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 = ''; 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 static function check_errors( &$msg ) { $msg = '...'; 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, $prev_value ) { // apply_filters( "update_{$meta_type}_metadata", null, $object_id, $meta_key, $meta_value, $prev_value ); return CRB_DS::protect_user_meta( $var, $object_id, $meta_key, $meta_value, $prev_value ); }, PHP_INT_MAX, 5 ); 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_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; }