File "class-itsec-lib-distributed-storage.php"
Full Path: /home/siazco/grocery.siazco.se/wp-content/plugins/better-wp-security/core/lib/class-itsec-lib-distributed-storage.php
File size: 14.82 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* Class ITSEC_Lib_Distributed_Storage
*/
class ITSEC_Lib_Distributed_Storage {
/* --- Config --- */
/** @var string */
private $name;
/** @var array */
private $config = array();
/* --- Instance --- */
/** @var array */
private $data = array();
/**
* ITSEC_Lib_Distributed_Storage constructor.
*
* @param string $name
* @param array $config
*/
public function __construct( $name, array $config ) {
$this->name = $name;
foreach ( $config as $key => $value ) {
$valid = false;
if ( array_key_exists( 'serialize', $value ) || array_key_exists( 'unserialize', $value ) ) {
if ( ! isset( $value['serialize'] ) ) {
_doing_it_wrong( __CLASS__, 'Solid Security: Serialize function required when using unserialize.', '4.5.0' );
} elseif ( ! is_callable( $value['serialize'] ) ) {
_doing_it_wrong( __CLASS__, 'Solid Security: Serialize function must be callable.', '4.5.0' );
} else {
$valid = true;
}
if ( ! isset( $value['unserialize'] ) ) {
_doing_it_wrong( __CLASS__, 'Solid Security: Unserialize function required when using serialize.', '4.5.0' );
} elseif ( ! is_callable( $value['unserialize'] ) ) {
_doing_it_wrong( __CLASS__, 'Solid Security: Unserialize function must be callable.', '4.5.0' );
} else {
$valid = true;
}
} else {
$valid = true;
}
if ( $valid ) {
$this->config[ $key ] = wp_parse_args( $value, array(
'split' => false,
'default' => null,
'serialize' => 'serialize',
'unserialize' => 'unserialize',
'chunk' => false,
) );
}
}
}
/**
* Get the value for a given key.
*
* @param string $key
*
* @return mixed|false
*/
public function get( $key ) {
if ( ! isset( $this->config[ $key ] ) ) {
_doing_it_wrong( __METHOD__, "Solid Security: Unsupported key '{$key}' for '{$this->name}' storage.", '4.5.0' );
return false;
}
if ( array_key_exists( $key, $this->data ) ) {
return $this->data[ $key ];
}
$this->load( $key );
return $this->data[ $key ];
}
/**
* Get a cursor to paginate over a chunked resource.
*
* @param string $key
*
* @return ITSEC_Lib_Distributed_Storage_Cursor|null
*/
public function get_cursor( $key ) {
if ( ! isset( $this->config[ $key ] ) ) {
_doing_it_wrong( __METHOD__, "Solid Security: Unsupported key '{$key}' for '{$this->name}' storage.", '4.5.0' );
return null;
}
if ( ! $this->config[ $key ]['chunk'] ) {
return null;
}
$data = $this->_load_chunk( $key, 0 );
$data = null === $data ? array() : $data;
return new ITSEC_Lib_Distributed_Storage_Cursor( $this, $key, $data );
}
/**
* Set the value for a given key.
*
* @param string $key
* @param mixed $value
*
* @return bool
*/
public function set( $key, $value ) {
global $wpdb;
if ( ! isset( $this->config[ $key ] ) ) {
_doing_it_wrong( __METHOD__, "Solid Security: Unsupported key '{$key}' for '{$this->name}' storage.", '4.5.0' );
return false;
}
$this->data[ $key ] = $value;
$config = $this->config[ $key ];
if ( ! $config['split'] ) {
$update = array();
foreach ( $this->config as $config_key => $config_value ) {
if ( ! $config_value['split'] ) {
if ( $key === $config_key ) {
$update[ $key ] = $value;
} else {
$update[ $config_key ] = $this->get( $config_key );
}
}
}
return $this->update_row( serialize( $update ) );
}
if ( $value === $config['default'] ) {
$wpdb->query( $wpdb->prepare(
"DELETE FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s AND `storage_key` = %s",
$this->name, $key
) );
return $wpdb->last_error ? false : true;
}
if ( ! $config['chunk'] ) {
return $this->update_row( call_user_func( $config['serialize'], $value ), $key );
}
$r = true;
$highest = 0;
foreach ( array_chunk( $value, $config['chunk'], true ) as $i => $chunk ) {
$r_ = $this->update_row( call_user_func( $config['serialize'], $chunk ), $key, $i );
$highest = $i;
$r = $r && $r_;
}
$this->clean_chunk_options( $key, $highest );
return $r;
}
/**
* Append values to the end of a chunked storage key.
*
* @param string $key
* @param array $value
*
* @return bool
*/
public function append( $key, $value ) {
if ( ! isset( $this->config[ $key ] ) ) {
_doing_it_wrong( __METHOD__, "Solid Security: Unsupported key '{$key}' for '{$this->name}' storage.", '4.5.0' );
return false;
}
$config = $this->config[ $key ];
if ( ! $config['chunk'] ) {
_doing_it_wrong( __METHOD__, "Solid Security: Cannot append to non-chunked key '{$key}' for '{$this->name}' storage.", '4.5.0' );
return false;
}
if ( array_key_exists( $key, $this->data ) ) {
$this->data[ $key ] = array_merge( $this->data[ $key ], $value );
}
global $wpdb;
$last_chunk = $wpdb->get_results( $wpdb->prepare(
"SELECT `storage_chunk`, `storage_data` FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s AND `storage_key` = %s ORDER BY `storage_chunk` DESC LIMIT 1",
$this->name, $key
) );
if ( empty( $last_chunk ) ) {
return $this->update_row( call_user_func( $config['serialize'], $value ), $key );
}
$last_chunk_num = $last_chunk[0]->storage_chunk;
$last_chunk_data = call_user_func( $config['unserialize'], $last_chunk[0]->storage_data );
if ( count( $last_chunk_data ) === $config['chunk'] ) {
return $this->update_row( call_user_func( $config['serialize'], $value ), $key, $last_chunk_num + 1 );
}
$to_fill = $config['chunk'] - count( $last_chunk_data );
$append = array_slice( $value, 0, $to_fill, true );
$merged = array_merge( $last_chunk_data, $append );
if ( ! $this->update_row( call_user_func( $config['serialize'], $merged ), $key, $last_chunk_num ) ) {
return false;
}
if ( ! $new = array_slice( $value, $to_fill, null, true ) ) {
return true;
}
$r = true;
foreach ( array_chunk( $new, $config['chunk'], true ) as $i => $chunk ) {
$r_ = $this->update_row( call_user_func( $config['serialize'], $chunk ), $key, $last_chunk_num + 1 + $i );
$r = $r && $r_;
}
return $r;
}
/**
* Update a chunked option from an iterator.
*
* This will be more performant than using ::set() and iterator_to_array() as the whole
* array won't be loaded into memory. Instead, it will continuously iterate over the values
* and persist the data to the database whenever it hits the chunk size.
*
* @param string $key
* @param iterable $iterator
*
* @return bool
*/
public function set_from_iterator( $key, $iterator ) {
if ( ! isset( $this->config[ $key ] ) ) {
_doing_it_wrong( __METHOD__, "Solid Security: Unsupported key '{$key}' for '{$this->name}' storage.", '4.5.0' );
return false;
}
$config = $this->config[ $key ];
if ( ! $config['chunk'] ) {
_doing_it_wrong( __METHOD__, "Solid Security: Cannot set from iterator to non-chunked key '{$key}' for '{$this->name}' storage.", '4.5.0' );
return false;
}
unset( $this->data[ $key ] );
$i = 0;
$chunk = 0;
$chunked = array();
$r = true;
foreach ( $iterator as $item => $value ) {
$i ++;
$chunked[ $item ] = $value;
if ( $i === $config['chunk'] ) {
$r_ = $this->update_row( call_user_func( $config['serialize'], $chunked ), $key, $chunk );
$r = $r && $r_;
$chunked = array();
$chunk ++;
$i = 0;
}
}
if ( $chunked ) {
$this->update_row( call_user_func( $config['serialize'], $chunked ), $key, $chunk );
} else {
// The last chunk allocated was not used.
$chunk --;
}
$this->clean_chunk_options( $key, $chunk );
return $r;
}
/**
* Get the most recent time any key in this storage set has been updated.
*
* @return int|false
*/
public function health_check() {
global $wpdb;
$date = $wpdb->get_var( $wpdb->prepare(
"SELECT `storage_updated` FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s ORDER BY `storage_updated` DESC LIMIT 1",
$this->name
) );
if ( $date ) {
return strtotime( $date );
}
return false;
}
/**
* Clear the entire storage bucket.
*
* @return bool
*/
public function clear() {
if ( self::clear_group( $this->name ) ) {
$this->data = array();
return true;
}
return false;
}
/**
* check if there are any recorded values in storage.
*
* @return bool
*/
public function is_empty() {
global $wpdb;
return ! $wpdb->get_var( $wpdb->prepare(
"SELECT `storage_id` FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s LIMIT 1",
$this->name
) );
}
/**
* Perform an insert or update to the distributed storage data.
*
* @param string $serialized
* @param string $key
* @param int $chunk
*
* @return bool
*/
private function update_row( $serialized, $key = '', $chunk = 0 ) {
global $wpdb;
$wpdb->query( $wpdb->prepare(
"INSERT INTO {$wpdb->base_prefix}itsec_distributed_storage (`storage_group`, `storage_key`, `storage_chunk`, `storage_data`, `storage_updated`) VALUES (%s, %s, %d, %s, %s) " .
'ON DUPLICATE KEY UPDATE `storage_group` = %s, `storage_key` = %s, `storage_chunk` = %d, `storage_data` = %s, `storage_updated` = %s',
$this->name, $key, $chunk, $serialized, date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ),
$this->name, $key, $chunk, $serialized, date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() )
) );
return $wpdb->last_error ? false : true;
}
/**
* Remove unused chunks.
*
* @param string $key The chunked key to clean.
* @param int $after_chunk Delete all rows with a chunk value higher than this.
*/
private function clean_chunk_options( $key, $after_chunk ) {
global $wpdb;
$wpdb->query( $wpdb->prepare(
"DELETE FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s AND `storage_key` = %s AND `storage_chunk` > %d",
$this->name, $key, $after_chunk
) );
}
/**
* Load the values into memory for a given key.
*
* @param string $key
*/
private function load( $key ) {
$config = $this->config[ $key ];
if ( $config['split'] ) {
$this->load_split_option( $key );
return;
}
global $wpdb;
$option = $wpdb->get_var( $wpdb->prepare(
"SELECT `storage_data` FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s AND `storage_key` = %s",
$this->name, ''
) );
if ( is_serialized( $option ) ) {
$option = unserialize( $option );
} else {
$option = array();
}
foreach ( $this->config as $config_key => $config_value ) {
if ( ! $config_value['split'] ) {
if ( is_array( $option ) && array_key_exists( $config_key, $option ) ) {
$this->data[ $config_key ] = $option[ $config_key ];
} elseif ( ! array_key_exists( $config_key, $this->data ) ) {
$this->data[ $config_key ] = $config_value['default'];
}
}
}
}
/**
* Load a split option into memory.
*
* Will automatically iterate all chunks into memory as well.
*
* @param string $key
* @param int $chunk
*/
private function load_split_option( $key, $chunk = 0 ) {
global $wpdb;
$config = $this->config[ $key ];
if ( $chunk ) {
$option = $wpdb->get_var( $wpdb->prepare(
"SELECT `storage_data` FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s AND `storage_key` = %s AND `storage_chunk` = %d",
$this->name, $key, $chunk
) );
} else {
$option = $wpdb->get_var( $wpdb->prepare(
"SELECT `storage_data` FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s AND `storage_key` = %s",
$this->name, $key
) );
}
if ( null === $option ) {
if ( ! array_key_exists( $key, $this->data ) ) {
$this->data[ $key ] = $config['default'];
}
return;
}
$option = call_user_func( $config['unserialize'], $option );
if ( ! $config['chunk'] ) {
$this->data[ $key ] = $option;
return;
}
if ( ! is_array( $option ) ) {
trigger_error( "Solid Security: Non-array value encountered for chunked key '{$key}' in storage '{$this->name}'." );
return;
}
if ( array_key_exists( $key, $this->data ) ) {
$this->data[ $key ] = array_merge( $this->data[ $key ], $option );
} else {
$this->data[ $key ] = $option;
}
// Greater than should never occur, bu to be safe
if ( count( $option ) >= $config['chunk'] ) {
$this->load_split_option( $key, $chunk + 1 );
}
}
/**
* Load data for a specific chunk.
*
* Ideally this would be replaced with a closure passed to the storage cursor.
*
* @param string $key
* @param int $chunk
*
* @return mixed|null
*/
public function _load_chunk( $key, $chunk ) {
global $wpdb;
$option = $wpdb->get_var( $wpdb->prepare(
"SELECT `storage_data` FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s AND `storage_key` = %s AND `storage_chunk` = %d",
$this->name, $key, $chunk
) );
if ( null === $option ) {
return null;
}
$config = $this->config[ $key ];
return call_user_func( $config['unserialize'], $option );
}
/**
* Clear all the storage for a given group name.
*
* @param string $name
*
* @return bool
*/
public static function clear_group( $name ) {
global $wpdb;
$wpdb->query( $wpdb->prepare(
"DELETE FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s",
$name
) );
return $wpdb->last_error ? false : true;
}
}
class ITSEC_Lib_Distributed_Storage_Cursor implements Iterator {
/** @var ITSEC_Lib_Distributed_Storage */
private $storage;
/** @var string */
private $key;
/** @var int */
private $chunk = 0;
/** @var array */
private $data;
/** @var int */
private $iterated_count = 0;
/**
* ITSEC_Lib_Distributed_Storage_Cursor constructor.
*
* @param ITSEC_Lib_Distributed_Storage $storage
* @param string $key
* @param array $data
*/
public function __construct( ITSEC_Lib_Distributed_Storage $storage, $key, array $data ) {
$this->storage = $storage;
$this->key = $key;
$this->data = $data;
}
#[ReturnTypeWillChange]
public function current() {
return current( $this->data );
}
public function next(): void {
if ( $this->iterated_count === count( $this->data ) - 1 ) {
$data = $this->storage->_load_chunk( $this->key, $this->chunk + 1 );
if ( null !== $data ) {
$this->data = $data;
$this->iterated_count = 0;
$this->chunk ++;
return;
}
}
$this->iterated_count ++;
next( $this->data );
}
#[ReturnTypeWillChange]
public function key() {
return key( $this->data );
}
public function valid(): bool {
return $this->iterated_count < count( $this->data );
}
public function rewind(): void {
$this->iterated_count = 0;
if ( 0 === $this->chunk ) {
reset( $this->data );
} else {
$data = $this->storage->_load_chunk( $this->key, 0 );
$this->data = null === $data ? array() : $data;
$this->chunk = 0;
}
}
}