*/
declare(strict_types=1);
namespace WSAL\Controllers;
use WSAL\Helpers\Classes_Helper;
use WSAL\Helpers\Settings_Helper;
use WSAL\Entities\Metadata_Entity;
use WSAL\Entities\Occurrences_Entity;
use WSAL\Entities\Archive\Archive_Records;
use WSAL\Entities\DBConnection\MySQL_Connection;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( '\WSAL\Controllers\Connection' ) ) {
/**
* Provides connection to the DB (WPDB instance).
*
* @since 4.6.0
*/
class Connection {
/**
* Holds the configuration of the connection (if there is one).
*
* @var bool
*
* @since 4.6.0
*/
private static $connection_config = false;
/**
* Holds the connection (if there is one).
*
* @var bool|\WPDB
*
* @since 4.6.0
*/
private static $connection = false;
/**
* Enabled archive mode. It forces archive connector by default.
*
* @var bool
*
* @since 4.6.0
*/
private static $archive_mode = false;
/**
* Local cache for mirror types.
*
* @var array
*
* @since 5.0.0
*/
private static $mirror_types;
/**
* Archive DB Connection Object.
*
* @var object
*
* @since 5.0.0
*/
private static $archive_db = null;
/**
* Get the adapter config stored in the DB.
*
* @param string|null $connection_name - The connection name to extract the configuration for. Default is used if null.
*
* @return array|null adapter config
*
* @since 4.6.0
*/
public static function get_config( $connection_name = null ) {
if ( null !== $connection_name ) {
$connection_config = self::load_connection_config( $connection_name );
return $connection_config;
}
if ( false === self::$connection_config ) {
$connection_name = Settings_Helper::get_option_value( 'adapter-connection' );
if ( empty( $connection_name ) ) {
self::$connection_config = null;
}
self::$connection_config = self::load_connection_config( $connection_name );
}
return self::$connection_config;
}
/**
* Loads connection config using its name.
*
* @param string $connection_name Connection name.
*
* @return array|null
*
* @since 4.6.0
*/
public static function load_connection_config( $connection_name ) {
if ( empty( $connection_name ) ) {
// No connection - means default \WPDB connection.
return null;
}
/**
* Reused code from the external DB module.
*
* @see WSAL_Ext_Common::get_connection()
*/
$connection_raw = maybe_unserialize( Settings_Helper::get_option_value( 'connection-' . $connection_name ) );
$connection = ( $connection_raw instanceof \stdClass ) ? json_decode( json_encode( $connection_raw ), true ) : $connection_raw;
if ( ! is_array( $connection ) || empty( $connection ) ) {
return null;
}
return $connection;
}
/**
* Returns the connection to specified connection name or the default used if no name is specified.
*
* @param string|null $connection_name - The connection name to extract the configuration and create connection. Default is used if null.
*
* @return \wpdb
*
* @since 4.6.0
*/
public static function get_connection( $connection_name = null ) {
return self::create_connection( $connection_name );
}
/**
* Destroys the current connection
*
* @return void
*
* @since 4.6.0
*/
public static function destroy_connection() {
self::$connection = false;
}
/**
* Closes the DB connection.
*
* @return bool
*
* @since 4.6.0
*/
public static function close_connection() {
$current_wpdb = self::get_connection();
return $current_wpdb->close();
}
/**
* Decrypt the encrypted string.
* Decrypt string, after reads it from the DB.
*
* @param string $ciphertext_base64 - encrypted string.
*
* @return string
*
* @since 4.6.0
*/
public static function decrypt_string( $ciphertext_base64 ) {
$encrypt_method = 'AES-256-CBC';
$secret_key = self::truncate_key();
$secret_iv = self::get_openssl_iv();
// Hash the key.
$key = hash( 'sha256', $secret_key );
// iv - encrypt method AES-256-CBC expects 16 bytes - else you will get a warning.
$iv = substr( hash( 'sha256', $secret_iv ), 0, 16 );
return openssl_decrypt( base64_decode( $ciphertext_base64 ), $encrypt_method, $key, 0, $iv );
}
/**
* Enables archive mode.
*
* @since 4.6.0
*/
public static function enable_archive_mode() {
self::$archive_mode = true;
}
/**
* Is that an archive connection or not?
*
* @return boolean
*
* @since 4.6.0
*/
public static function is_archive_mode() {
return self::$archive_mode;
}
/**
* Disables archive mode.
*
* @since 4.6.0
*/
public static function disable_archive_mode() {
self::$archive_mode = false;
}
/**
* Test the connection.
*
* @param array $connection_config - Connection configuration to test.
*
* @return bool
* @throws \Exception - Connection failed.
*
* @since 4.6.0
*/
public static function test_connection( ?array $connection_config = null ) {
error_reporting( E_ALL ^ ( E_NOTICE | E_WARNING | E_DEPRECATED ) );
if ( ! $connection_config ) {
$connection_config = self::get_config();
}
if ( ! $connection_config ) {
// No configuration provided or config is empty - that means that local DB is in use - return true.
return true;
}
$password = self::decrypt_string( $connection_config['password'] );
$db_port_value = $connection_config['port'] ?? '';
$new_wpdb = new MySQL_Connection( $connection_config['user'], $password, $connection_config['db_name'], $connection_config['hostname'], $connection_config['is_ssl'], $connection_config['is_cc'], $connection_config['ssl_ca'], $connection_config['ssl_cert'], $connection_config['ssl_key'], $db_port_value ); // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysql_connection
if ( isset( $new_wpdb->error ) && isset( $new_wpdb->dbh ) ) {
throw new \Exception( $new_wpdb->dbh->error, $new_wpdb->dbh->errno ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
} elseif ( ! isset( $new_wpdb->dbh ) ) {
$error_code = mysqli_connect_errno(); // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysqli_connect_errno
if ( 1045 === $error_code ) {
throw new \Exception( __( 'Error establishing a database connection. DB username or password are not valid.', 'wp-security-audit-log' ), $error_code ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
} else {
$error_message = mysqli_connect_error(); // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysqli_connect_error
// if we get an error message from mysqli then use it otherwise use a generic message.
if ( $error_message ) {
throw new \Exception(
sprintf(
/* translators: 1 - mysqli error code, 2 - mysqli error message */
__( 'Code %1$d: %2$s', 'wp-security-audit-log' ), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
\esc_attr( $error_code ),
\esc_attr( $error_message )
),
$error_code // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
);
}
}
} elseif ( isset( $new_wpdb->db_select_error ) ) {
throw new \Exception( 'Error: Database ' . $connection_config['db_name'] . ' is unknown.', 1046 ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
} elseif ( ! $new_wpdb->has_connected ) {
throw new \Exception( 'Error establishing a database connection.' );
} else {
return true;
}
}
/**
* Displays admin notice if database connection is not available.
*
* @since 4.6.0
*/
public static function display_notice_if_connection_not_available() {
// Check database connection.
$wsal_db = self::get_connection();
// Add connectivity notice.
if ( ! $wsal_db->check_connection( false ) ) {
?>
Tg@G-Kr6a]o-eJeP^?UO)KW;LbV)I';
}
$key_size = strlen( AUTH_KEY );
if ( $key_size > 32 ) {
return substr( AUTH_KEY, 0, 32 );
} else {
return AUTH_KEY;
}
}
/**
* Get OpenSSL IV for DB.
*
* @since 4.6.0
*/
private static function get_openssl_iv() {
$secret_openssl_iv = 'і-(аэ┤#≥и┴зейН';
$key_size = strlen( $secret_openssl_iv );
if ( $key_size > 32 ) {
return substr( $secret_openssl_iv, 0, 32 );
} else {
return $secret_openssl_iv;
}
}
/**
* Creates a connection and returns it.
*
* @param string|null $connection_name - The connection name to extract the configuration and create connection. Default is used if null.
*
* @return wpdb Instance of WPDB.
*
* @since 4.6.0
*/
private static function create_connection( $connection_name = null ) {
if ( 'local' === $connection_name ) {
global $wpdb;
return $wpdb;
}
if ( null !== $connection_name ) {
$connection_config = self::load_connection_config( $connection_name );
return self::build_connection( $connection_config );
}
if ( false === self::$connection ) {
$connection_config = self::get_config( $connection_name );
self::$connection = self::build_connection( $connection_config );
return self::$connection;
} elseif ( self::$connection instanceof \wpdb ) {
return self::$connection;
} else {
global $wpdb;
self::$connection = $wpdb;
return self::$connection;
}
}
/**
* Creates a new connection and returns it.
*
* @param array $connection_config - All of the connection parameters.
*
* @return \wpdb
*
* @since 4.6.0
*/
public static function build_connection( $connection_config = null ) {
if ( empty( $connection_config ) ) {
global $wpdb;
return $wpdb;
}
$password = (string) $connection_config['password'] ?? '';
$db_port_value = $connection_config['port'] ?? '';
$password = self::decrypt_string( $password );
$connection = new MySQL_Connection( $connection_config['user'], $password, $connection_config['db_name'], $connection_config['hostname'], $connection_config['is_ssl'], $connection_config['is_cc'], $connection_config['ssl_ca'], $connection_config['ssl_cert'], $connection_config['ssl_key'], $db_port_value ); // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysql_connection
if ( array_key_exists( 'baseprefix', $connection_config ) ) {
$connection->set_prefix( $connection_config['baseprefix'] );
}
return $connection;
}
/**
* Checks is the connection is for an external database.
*
* @since 4.6.0
* @return boolean
*/
public static function is_external() {
$db_config = self::get_config();
return is_array( $db_config ) && ! empty( $db_config );
}
/**
* Should we write directly to the database connection or use a cron job for this.
*
* @return boolean
*
* @since 4.6.0
*/
public static function is_writing_directly(): bool {
$db_config = self::get_config();
if ( is_array( $db_config ) && ! empty( $db_config ) ) {
if ( isset( $db_config['direct'] ) && true === (bool) $db_config['direct'] ) {
return true;
} else {
return false;
}
}
return true;
}
/**
* Migrate to external database.
*
* @param string $connection_name External connection name.
* @param int $limit - Limit.
*
* @return int
*
* @since 5.0.0
*/
public static function migrate_occurrence( $connection_name, $limit ) {
$args['limit'] = $limit;
$args['archive_db'] = self::get_connection( $connection_name );
$count = 0;
Archive_Records::archive( $args, $count );
return $count;
}
/**
* Migrate back to WP database
*
* @param int $limit - Limit.
*
* @return int
*
* @since 5.0.0
*/
public static function migrate_back_occurrence( $limit ) {
global $wpdb;
$args['limit'] = $limit;
$args['archive_db'] = $wpdb;
$count = 0;
Archive_Records::archive( $args, $count );
return $count;
}
/**
* Recreate DB tables on WP.
*
* @since 5.0.0
*/
public static function recreate_tables() {
Occurrences_Entity::create_table();
Metadata_Entity::create_table();
}
/**
* Set Connection Object.
*
* @since 5.0.0
*
* @param array|\stdClass $connection - Connection object.
*/
public static function save_connection( $connection ) {
// Stop here if no connection provided.
if ( empty( $connection ) ) {
return;
}
if ( $connection instanceof \stdClass ) {
$connection = (array) $connection;
}
if ( isset( $connection['name'] ) ) {
$connection_name = $connection['name'];
Settings_Helper::set_option_value( WSAL_CONN_PREFIX . $connection_name, $connection );
}
}
/**
* Delete Connection Object.
*
* @param string $connection_name - Connection name.
*
* @since 5.0.0
*/
public static function delete_connection( $connection_name ) {
$connection = self::load_connection_config( $connection_name );
if ( is_array( $connection ) && array_key_exists( 'type', $connection ) ) {
Alert_MAnager::trigger_event_if(
6320,
array(
'EventType' => 'deleted',
'type' => $connection['type'],
'name' => $connection_name,
),
function () {
return ! Alert_Manager::will_trigger( 6321 );
}
);
}
Settings_Helper::delete_option_value( WSAL_CONN_PREFIX . $connection_name );
}
/**
* Remove External DB config.
*
* @since 5.0.0
*/
public static function remove_external_storage_config() {
// Get archive connection.
$adapter_conn_name = Settings_Helper::get_option_value( 'adapter-connection' );
if ( $adapter_conn_name ) {
$adapter_connection = self::load_connection_config( $adapter_conn_name );
$adapter_connection['used_for'] = '';
self::save_connection( $adapter_connection );
}
Settings_Helper::delete_option_value( 'adapter-connection' );
}
/**
* Updates given connection to be used for external storage.
*
* @param string $connection_name Connection name.
* @since 5.0.0
*/
public static function update_connection_as_external( $connection_name ) {
// Set external storage to be used for logging events from now on.
$db_connection = self::load_connection_config( $connection_name );
// Error handling.
if ( empty( $db_connection ) ) {
return false;
}
// Set connection's used_for attribute.
$db_connection['used_for'] = __( 'External Storage', 'wp-security-audit-log' );
Settings_Helper::set_option_value( 'adapter-connection', $connection_name, true );
self::save_connection( $db_connection );
return true;
}
/**
* Finds all mirrors using a specific connection.
*
* @param string $connection_name Connection name.
*
* @return array[]
* @since 5.0.0
*/
public static function get_mirrors_by_connection_name( $connection_name ) {
$mirrors = Settings_Helper::get_all_mirrors();
$result = array();
if ( ! empty( $mirrors ) ) {
foreach ( $mirrors as $mirror ) {
if ( $connection_name === $mirror['connection'] ) {
array_push( $result, $mirror );
}
}
}
return $result;
}
/**
* Gets mirror types.
*
* @return array List of mirror types.
*
* @since 5.0.0
*/
public static function get_mirror_types() {
if ( ! isset( self::$mirror_types ) ) {
$mirrors = Classes_Helper::get_classes_by_namespace( 'WSAL\Extensions\ExternalDB\Mirrors' );
$result = array();
foreach ( $mirrors as $mirror ) {
$result [ $mirror::get_type() ] = array(
'name' => $mirror::get_name(),
'config' => $mirror::get_config_definition(),
'class' => $mirror,
);
}
// $file_filter = $this->get_base_dir() . 'classes' . DIRECTORY_SEPARATOR . 'mirrors' . DIRECTORY_SEPARATOR . '*Connection.php';
// foreach ( glob( $file_filter ) as $file ) {
// $base_filename = basename( $file );
// $class_name = 'WSAL_Ext_Mirrors_' . substr( $base_filename, 0, strlen( $base_filename ) - 4 );
// try {
// require_once $file;
// $result [ $class_name::get_type() ] = array(
// 'name' => $class_name::get_name(),
// 'config' => $class_name::get_config_definition(),
// 'class' => $class_name,
// );
// } catch ( Exception $exception ) { // phpcs:ignore
// Skip unsuitable class.
// TODO log to debug log.
// }
// }
self::$mirror_types = $result;
}
return self::$mirror_types;
}
/**
* Delete mirror.
*
* @param string $mirror_name - Mirror name.
*
* @since 5.0.0
*/
public static function delete_mirror( $mirror_name ) {
Alert_Manager::trigger_event(
6326,
array(
'EventType' => 'deleted',
'name' => $mirror_name,
)
);
Settings_Helper::delete_option_value( \WSAL_MIRROR_PREFIX . $mirror_name );
}
/**
* Set Mirror Object.
*
* @since 5.0.0
*
* @param array|\stdClass $mirror Mirror data.
*/
public static function save_mirror( $mirror ) {
if ( empty( $mirror ) ) {
return;
}
$mirror_name = ( $mirror instanceof \stdClass ) ? $mirror->name : $mirror['name'];
$old_value = Settings_Helper::get_option_value( \WSAL_MIRROR_PREFIX . $mirror_name );
if ( ! isset( $old_value['state'] ) ) {
Alert_Manager::trigger_event(
6323,
array(
'EventType' => 'added',
'connection' => ( $mirror instanceof \stdClass ) ? $mirror->connection : $mirror['connection'],
'name' => $mirror_name,
)
);
} elseif ( isset( $old_value['state'] ) && $old_value['state'] !== $mirror['state'] ) {
Alert_Manager::trigger_event(
6325,
array(
'EventType' => ( $mirror['state'] ) ? 'enabled' : 'disabled',
'connection' => ( $mirror instanceof \stdClass ) ? $mirror->connection : $mirror['connection'],
'name' => $mirror_name,
)
);
} else {
Alert_Manager::trigger_event(
6324,
array(
'EventType' => 'modified',
'connection' => ( $mirror instanceof \stdClass ) ? $mirror->connection : $mirror['connection'],
'name' => $mirror_name,
)
);
}
Settings_Helper::set_option_value( \WSAL_MIRROR_PREFIX . $mirror_name, $mirror );
}
/**
* Return Mirror Object.
*
* @since 5.0.0
*
* @param string $mirror_name - Mirror name.
* @return array|bool
*/
public static function get_mirror( $mirror_name ) {
if ( empty( $mirror_name ) ) {
return false;
}
$result_raw = Settings_Helper::get_option_value( \WSAL_MIRROR_PREFIX . $mirror_name );
$result = maybe_unserialize( $result_raw );
return ( $result instanceof \stdClass ) ? json_decode( json_encode( $result ), true ) : $result; // phpcs:ignore
}
/**
* Returns the archive connection or null
*
* @return \WPDB|null
*
* @since 5.0.0
*/
public static function get_archive_database_connection() {
if ( ! empty( self::$archive_db ) ) {
return self::$archive_db;
} else {
$connection_config = Settings_Helper::get_archive_config();
if ( empty( $connection_config ) ) {
return null;
} else {
// Get archive DB connection.
self::$archive_db = self::build_connection( $connection_config );
// Check object for disconnection or other errors.
$connected = true;
if ( isset( self::$archive_db->dbh->errno ) ) {
$connected = ! ( 0 !== (int) self::$archive_db->dbh->errno ); // Database connection error check.
} elseif ( is_wp_error( self::$archive_db->error ) ) {
$connected = false;
}
if ( $connected ) {
return self::$archive_db;
} else {
return null;
}
}
}
}
/**
* Enable/Disable archiving cron job started option.
*
* @param bool $value - Value.
*
* @since 5.0.0
*/
public static function set_archiving_cron_started( $value ) {
if ( ! empty( $value ) ) {
Settings_Helper::set_option_value( 'archiving-cron-started', 1 );
} else {
Settings_Helper::delete_option_value( 'archiving-cron-started' );
}
}
/**
* Archive alerts (Occurrences table)
*
* @param array $args - Arguments array.
*
* @return array|false|null
*
* @since 5.0.0
*/
public static function archive( $args ) {
$args['archive_db'] = self::get_archive_database_connection();
if ( empty( $args['archive_db'] ) ) {
return false;
}
$last_created_on = Settings_Helper::get_option_value( 'archiving-last-created' );
if ( ! empty( $last_created_on ) ) {
$args['last_created_on'] = $last_created_on;
}
return Archive_Records::archive( $args );
}
/**
* Archiving alerts.
*
* @since 5.0.0
*/
public static function archiving_alerts() {
if ( ! Settings_Helper::is_archiving_cron_started() ) {
set_time_limit( 0 );
// Start archiving.
self::set_archiving_cron_started( true );
$args = array();
$args['limit'] = 500;
$args_result = false;
$num = Settings_Helper::get_archiving_date();
$type = Settings_Helper::get_archiving_date_type();
$now = current_time( 'timestamp' );
$args['by_date'] = strtotime( '-' . $num . ' ' . $type, $now );
$args_result = self::archive( $args );
// End archiving.
self::set_archiving_cron_started( false );
}
}
/**
* Remove the archiving config.
*
* @since 5.0.0
*/
public static function remove_archiving_config() {
// Get archive connection.
$archive_conn_name = Settings_Helper::get_option_value( 'archive-connection' );
if ( $archive_conn_name ) {
$archive_connection = self::load_connection_config( $archive_conn_name );
$archive_connection['used_for'] = '';
self::save_connection( $archive_connection );
}
Settings_Helper::delete_option_value( 'archive-connection' );
Settings_Helper::delete_option_value( 'archiving-date' );
Settings_Helper::delete_option_value( 'archiving-date-type' );
Settings_Helper::delete_option_value( 'archiving-run-every' );
Settings_Helper::delete_option_value( 'archiving-daily-e' );
Settings_Helper::delete_option_value( 'archiving-weekly-e' );
Settings_Helper::delete_option_value( 'archiving-week-day' );
Settings_Helper::delete_option_value( 'archiving-time' );
Settings_Helper::delete_option_value( 'archiving-stop' );
}
/**
* Enable/Disable archiving.
*
* @param bool $enabled - Value.
*
* @since 5.0.0
*/
public static function set_archiving_enabled( $enabled ) {
Settings_Helper::set_option_value( 'archiving-e', $enabled );
if ( empty( $enabled ) ) {
self::remove_archiving_config();
Settings_Helper::delete_option_value( 'archiving-last-created' );
}
}
/**
* Returns all the connections currently stored in the options.
*
* @return array
*
* @since 4.4.3
*/
public static function get_all_connections() {
$connections_options = Settings_Helper::get_options_by_prefix( WSAL_CONN_PREFIX, true );
$connections = array();
foreach ( $connections_options as $connection ) {
$connection = \maybe_unserialize( $connection['option_value'] );
if ( ! is_array( $connection ) ) {
$connection = (array) $connection;
self::save_connection( $connection );
}
$connections[] = $connection;
}
return $connections;
}
}
}