lopment() ) { trigger_error( wp_sprintf( __( 'An attempt to register the %1$s module failed because it extends a non-registered module: %2$l', 'better-wp-security' ), $slug, $config->get_extends() ) ); return false; } $self->module_config[ $extends->get_id() ] = $extends->extend( $config ); if ( $config->get_status() === 'inherit' ) { $self->inherited_modules[ $slug ] = true; } } $self->_module_paths[ $slug ] = $path; $self->_available_modules = array_keys( $self->_module_paths ); if ( 'always-active' === $type ) { $self->_always_active_modules[ $slug ] = true; } elseif ( 'default-active' === $type ) { $self->_default_active_modules[ $slug ] = true; } // Clear cache. $self->_active_modules_list = false; self::initialize_feature_flags( $config ); return true; } /** * Deregisters a single module * * @static * * @param string $slug The unique slug to use for the module * * @return bool True on success, false otherwise. */ public static function deregister_module( $slug ) { $self = self::get_instance(); $slug = sanitize_title_with_dashes( $slug ); if ( isset( $self->_module_paths[ $slug ] ) ) { unset( $self->_module_paths[ $slug ], $self->module_config[ $slug ], $self->inherited_modules[ $slug ], $self->_always_active_modules[ $slug ], $self->_default_active_modules[ $slug ] ); $self->_available_modules = array_keys( $self->_module_paths ); return true; } return false; } /** * Update the stored paths for each module. * * This is predominantly used when changing the WordPress content directory. * * @param string $old_dir * @param string $new_dir */ public static function update_module_paths( $old_dir, $new_dir ) { $self = self::get_instance(); foreach ( $self->_module_paths as $slug => $path ) { $self->_module_paths[ $slug ] = str_replace( $old_dir, $new_dir, $path ); } } /** * Register a module's settings controller. * * @param ITSEC_Settings $settings */ public static function register_settings( $settings ) { self::get_instance()->_module_settings[ $settings->get_id() ] = $settings; } /** * Retrieve a module's settings controller. * * This will load a module's settings file if it has not yet been loaded. * * @param string $slug The module slug. * * @return ITSEC_Settings|null */ public static function get_settings_obj( $slug ) { $self = self::get_instance(); if ( ! isset( $self->_module_settings[ $slug ] ) ) { self::load_module_file( 'settings.php', $slug ); } if ( ! isset( $self->_module_settings[ $slug ] ) && ( $config = self::get_module_config( $slug ) ) && $config->get_settings() ) { $self->_module_settings[ $slug ] = new Config_Settings( $config ); } if ( ! isset( $self->_module_settings[ $slug ] ) ) { return null; } return $self->_module_settings[ $slug ]; } /** * Get the default settings for a module. * * @param string $slug The module slug. * * @return array */ public static function get_defaults( $slug ) { $settings_obj = self::get_settings_obj( $slug ); if ( $settings_obj ) { return $settings_obj->get_defaults(); } return array(); } /** * Retrieve the default value of specific setting in a module. * * @param string $slug The module slug. * @param string $name The name of the setting. * @param mixed|null $default Optionally, specify a default value to be used if the module did not declare one. * * @return mixed */ public static function get_default( $slug, $name, $default = null ) { $settings_obj = self::get_settings_obj( $slug ); if ( $settings_obj ) { return $settings_obj->get_default( $name, $default ); } return $default; } /** * Retrieve all of the settings for a module. * * @param string $slug The module slug. * * @return array */ public static function get_settings( $slug ) { $settings_obj = self::get_settings_obj( $slug ); if ( $settings_obj ) { return $settings_obj->get_all(); } return array(); } /** * Retrieve the value of a specific setting in a module. * * @param string $slug The module slug. * @param string $name The name of the setting. * @param mixed|null $default Optionally, specify a default value to be used if the requested setting does not * exist. * * @return mixed */ public static function get_setting( $slug, $name, $default = null ) { $settings_obj = self::get_settings_obj( $slug ); if ( $settings_obj ) { return $settings_obj->get( $name, $default ); } return $default; } /** * Update all of a module's settings at once. * * The values will be validated, updated in-memory, and persisted. * * @param string $slug The module slug. * @param array $settings New settings values. * * @return array|WP_Error */ public static function set_settings( $slug, $settings ) { $settings_obj = self::get_settings_obj( $slug ); if ( ! $settings_obj ) { $error = new WP_Error( 'itsec-modules-invalid-settings-object', sprintf( __( 'Unable to find a valid settings object for %s. Settings were unable to be saved.', 'better-wp-security' ), $slug ) ); ITSEC_Response::add_error( $error ); return $error; } return $settings_obj->set_all( $settings ); } /** * Update a single setting in a module. * * The new value will be validated and updated in memory. The change isn't persisted until * the end of the request or a manual call to {@see ITSEC_Storage::save()}. * * @param string $slug The module slug. * @param string $name The setting name to updated. * @param mixed $value The settings' new value. * * @return array|false */ public static function set_setting( $slug, $name, $value ) { $settings_obj = self::get_settings_obj( $slug ); if ( ! $settings_obj ) { trigger_error( sprintf( __( 'Unable to find a valid settings object for %s. Setting was unable to be saved.', 'better-wp-security' ), $slug ) ); return false; } return $settings_obj->set( $name, $value ); } /** * Reloads settings values from the database. */ public static function reload_settings() { foreach ( self::get_instance()->_module_settings as $settings_obj ) { $settings_obj->load(); } } /** * Register a module's validator controller. * * Only one validator per-module is supported. * * @param ITSEC_Validator $validator */ public static function register_validator( $validator ) { self::get_instance()->_module_validators[ $validator->get_id() ] = $validator; } /** * Retrieve the validator for a given module. * * This will load a module's validator component if not yet loaded. * * @param string $slug The module slug. * * @return ITSEC_Validator|null */ public static function get_validator( $slug ) { $self = self::get_instance(); if ( ! isset( $self->_module_validators[ $slug ] ) ) { require_once( ITSEC_Core::get_core_dir() . '/lib/validator.php' ); self::load_module_file( 'validator.php', $slug ); } if ( ! isset( $self->_module_validators[ $slug ] ) && ( $config = self::get_config( $slug ) ) && $config->get_settings() ) { $self->_module_validators[ $slug ] = new Config_Validator( $config ); } if ( ! isset( $self->_module_validators[ $slug ] ) ) { return null; } return $self->_module_validators[ $slug ]; } /** * Get's the config for a module. * * @param string $slug * * @return Module_Config|null */ public static function get_config( $slug ) { return isset( self::get_instance()->module_config[ $slug ] ) ? self::get_instance()->module_config[ $slug ] : null; } /** * Gets a list of module config objects. * * @param string $modules The module specifier. * * @return Module_Config[] */ public static function get_config_list( $modules = ':active' ): array { return array_filter( array_map( [ static::class, 'get_config' ], self::transform_modules_specifier( $modules ) ) ); } /** * Retrieve the slugs of all modules available to the plugin. * * This function is internally cached. * * @return string[] */ public static function get_available_modules() { $self = self::get_instance(); if ( false !== $self->_available_modules ) { return $self->_available_modules; } if ( ! is_array( $self->_module_paths ) ) { $self->_module_paths = array(); } $self->_available_modules = array_keys( $self->_module_paths ); return $self->_available_modules; } /** * Checks if the given module is available for use. * * @param string $module * * @return bool */ public static function is_available( $module ) { return isset( self::get_instance()->_module_paths[ $module ] ); } /** * Retrieve the slugs of all active modules. * * This includes user activated and default activated modules. The result is internally cached. * * @return string[] */ public static function get_active_modules() { $self = self::get_instance(); if ( is_array( $self->_active_modules_list ) ) { return $self->_active_modules_list; } $self->_active_modules = get_site_option( 'itsec_active_modules', array() ); if ( ! is_array( $self->_active_modules ) ) { $self->_active_modules = array(); } elseif ( isset( $self->_active_modules[0] ) ) { // Found data from an old format. foreach ( $self->_active_modules as $key => $value ) { if ( ! is_bool( $value ) ) { unset( $self->_active_modules[ $key ] ); if ( ! isset( $self->_active_modules[ $value ] ) ) { $self->_active_modules[ $value ] = true; } } } if ( is_multisite() ) { update_site_option( 'itsec_active_modules', $self->_active_modules ); } else { update_option( 'itsec_active_modules', $self->_active_modules ); } } $default_active_modules = apply_filters( 'itsec-default-active-modules', array_keys( $self->_default_active_modules ) ); if ( ! is_array( $default_active_modules ) ) { $default_active_modules = array(); } foreach ( $default_active_modules as $module ) { if ( ! isset( $self->_active_modules[ $module ] ) ) { $self->_active_modules[ $module ] = true; } } $self->_active_modules_list = array(); foreach ( $self->_active_modules as $module => $active ) { if ( $active && isset( self::get_instance()->module_config[ $module ] ) ) { $self->_active_modules_list[] = $module; } } return $self->_active_modules_list; } /** * Retrieve the slugs of all modules that are required to be active. * * @return string[] */ public static function get_always_active_modules() { $self = self::get_instance(); return array_keys( $self->_always_active_modules ); } /** * Check if a module is configured to be always active. * * @param string $module_id The module slug. * * @return bool */ public static function is_always_active( $module_id ) { $self = self::get_instance(); if ( ! empty( $self->_always_active_modules[ $module_id ] ) ) { return true; } return false; } /** * Check if a module is active. * * @param string $module_id The module slug. * * @return bool */ public static function is_active( $module_id ) { $self = self::get_instance(); if ( ! is_array( $self->_active_modules ) ) { self::get_active_modules(); } if ( ! empty( $self->_always_active_modules[ $module_id ] ) ) { return true; } if ( isset( $self->_active_modules[ $module_id ] ) ) { return $self->_active_modules[ $module_id ]; } return false; } /** * Activate a single module using its ID * * @param string $module_id The ID of the module to activate * @param array $args Additional arguments to customize behavior. * * @type bool $ignore_requirements Whether to skip evaluating module requirements. * * @return bool|WP_Error If the module can be activated, true if it was previously active and false if it was * previously inactive. If the module cannot be activated, a WP_Error object is returned. */ public static function activate( $module_id, array $args = [] ) { $self = self::get_instance(); if ( self::is_always_active( $module_id ) ) { return new WP_Error( 'itsec-modules-cannot-activate-always-active-module', sprintf( __( 'The %s module is a Core module and cannot be activated or deactivated.', 'better-wp-security' ), $module_id ) ); } if ( empty( $args['ignore_requirements'] ) ) { $validated = self::validate_module_requirements( $module_id, 'activate' ); if ( $validated->has_errors() ) { return new WP_Error( 'itsec-modules-cannot-activate-module-unsatisfied-requirements', __( 'Cannot activate module.', 'better-wp-security' ) . ' ' . implode( ' ', ITSEC_Lib::get_error_strings( $validated ) ), [ 'status' => WP_Http::BAD_REQUEST ] ); } } if ( ! is_array( $self->_active_modules ) ) { self::get_active_modules(); } $was_active = false; if ( isset( $self->_active_modules[ $module_id ] ) ) { $was_active = $self->_active_modules[ $module_id ]; } try { self::load_module_file( 'activate.php', $module_id ); } catch ( Unsatisfied_Module_Dependencies_Exception $e ) { return new WP_Error( 'itsec-modules-cannot-activate-module-unsatisfied-dependencies', $e->getMessage(), [ 'status' => WP_Http::INTERNAL_SERVER_ERROR, ] ); } $self->_active_modules[ $module_id ] = true; self::set_active_modules( $self->_active_modules ); ITSEC_Core::get_scheduler()->register_events_for_module( $module_id ); return $was_active; } /** * Deactivate a single module using its ID * * @param string $module_id The ID of the module to deactivate * * @return bool|WP_Error If the module can be deactivated, true if it was previously active and false if it was * previously inactive. If the module cannot be deactivated, a WP_Error object is returned. */ public static function deactivate( $module_id ) { $self = self::get_instance(); if ( self::is_always_active( $module_id ) ) { return new WP_Error( 'itsec-modules-cannot-activate-always-active-module', sprintf( __( 'The %s module is a Core module and cannot be activated or deactivated.', 'better-wp-security' ), $module_id ) ); } if ( ! is_array( $self->_active_modules ) ) { self::get_active_modules(); } $was_active = false; if ( isset( $self->_active_modules[ $module_id ] ) ) { $was_active = $self->_active_modules[ $module_id ]; } self::load_module_file( 'deactivate.php', $module_id ); $self->_active_modules[ $module_id ] = false; self::set_active_modules( $self->_active_modules ); if ( $config = self::get_config( $module_id ) ) { ITSEC_Core::get_scheduler()->unregister_events_for_config( $config ); } return $was_active; } /** * Change the active and deactivate modules in bulk. * * The deactivation routine for no-longer active modules will NOT be run. * * @param string[] $new_active_modules * * @return bool */ public static function set_active_modules( $new_active_modules ) { $self = self::get_instance(); if ( ! is_array( $new_active_modules ) ) { return false; } // Ensure that the new values are sane by using the current active modules as a starting set of defaults. self::get_active_modules(); $self->_active_modules = array_merge( $self->_active_modules, $new_active_modules ); $self->_active_modules_list = array(); foreach ( $self->_active_modules as $module => $active ) { if ( $active ) { $self->_active_modules_list[] = $module; } } if ( is_multisite() ) { update_site_option( 'itsec_active_modules', $self->_active_modules ); } else { update_option( 'itsec_active_modules', $self->_active_modules ); } return true; } /** * Attempt to load a module(s)'s file. * * The file will only be loaded once and will not error if does not exist. * * @param string $file The file name to load, including extension. * @param string|string[] $modules The modules to load the files from. Accepts either a module slug, an array of * module slugs, ':all' to load the files from all modules, or ':active' to load the * files from active modules. * @param callable|null $process Callback to run returned values from module files through. * * @return bool|WP_Error True if a module matching the $modules parameter is found, false otherwise. */ public static function load_module_file( $file, $modules = ':all', callable $process = null ) { if ( isset( self::DEPRECATED[ $file ] ) ) { _deprecated_file( $file, self::DEPRECATED[ $file ] ); return false; } $self = self::get_instance(); if ( ! $modules = self::transform_modules_specifier( $modules ) ) { return false; } foreach ( $modules as $module ) { if ( empty( $self->_module_paths[ $module ] ) ) { continue; } $config = self::get_config( $module ); if ( $config && $config->is_deprecated() ) { _deprecated_file( "{$self->_module_paths[ $module ]}/{$file}", $config->get_deprecated_version() ); } $self->load_container_definitions( $module ); $returned = null; if ( self::get_container()->has( "module.{$module}.files" ) ) { $files = self::get_container()->get( "module.{$module}.files" ); if ( isset( $files[ $file ] ) ) { $returned = $files[ $file ]; } } if ( ! $returned ) { $path = "{$self->_module_paths[$module]}/{$file}"; if ( ! file_exists( $path ) ) { continue; } if ( array_key_exists( $path, $self->returned_files ) ) { $returned = $self->returned_files[ $path ]; } else { $returned = include_once( $path ); $self->returned_files[ $path ] = $returned; } } if ( $returned ) { if ( $process ) { $process( $returned, $module ); } else { $self->run( $returned ); } } } return true; } /** * Gets the module's config definition. * * @param string $id The module id. * * @return Module_Config|null */ public static function get_module_config( $id ) { return isset( self::get_instance()->module_config[ $id ] ) ? self::get_instance()->module_config[ $id ] : null; } /** * Get a list of the active modules to run. * * @return string[] */ public static function get_active_modules_to_run() { if ( ITSEC_Core::is_temp_disable_modules_set() ) { $modules = array(); } else { $modules = self::get_active_modules(); } $modules = array_merge( $modules, array_keys( self::get_instance()->_always_active_modules ) ); $modules = array_unique( $modules ); $modules = array_filter( $modules, static function ( $module ) { return ! self::validate_module_requirements( $module, 'run' )->has_errors(); } ); foreach ( self::get_instance()->inherited_modules as $slug => $_ ) { if ( self::is_active( self::get_config( $slug )->get_extends() ) ) { $modules[] = $slug; } } return $modules; } /** * Fires an action to begin the registration of modules. */ public static function init_modules() { do_action( 'itsec-register-modules' ); } /** * Initialize the container. */ public static function initialize_container() { $load = require __DIR__ . '/container.php'; if ( is_callable( $load ) && ! self::get_instance()->initialized_container ) { $load( self::get_instance()->pimple ); self::get_instance()->container->get( Telemetry_Config::class ); Telemetry_Core::instance()->init( ITSEC_Core::get_plugin_file() ); } self::get_instance()->initialized_container = true; self::get_instance()->load_container_definitions( 'user-groups' ); } /** * Load and run all active modules. */ public static function run_active_modules() { self::initialize_container(); foreach ( self::get_active_modules_to_run() as $module ) { self::get_instance()->load_container_definitions( $module ); } self::load_module_file( 'active.php', 'global' ); if ( ITSEC_Core::is_loading_early() ) { self::load_module_file( 'active.php', ':active-early' ); add_action( 'plugins_loaded', function () { self::load_module_file( 'active.php', ':active-normal' ); }, - 95 ); } else { self::load_module_file( 'active.php', ':active' ); } } /** * Run the activation routine for all registered modules. */ public function run_activation() { self::load_module_file( 'setup.php' ); do_action( 'itsec_modules_do_plugin_activation' ); } /** * Run the deactivation routine for all registered modules. */ public function run_deactivation() { self::load_module_file( 'setup.php' ); do_action( 'itsec_modules_do_plugin_deactivation' ); } /** * Run the uninstall routine for all registered modules. */ public static function run_uninstall() { self::load_module_file( 'setup.php' ); do_action( 'itsec_modules_do_plugin_uninstall' ); } /** * Run the upgrade routine for all registered modules. * * @param int $old_version * @param int $new_version */ public function run_upgrade( $old_version, $new_version ) { self::load_module_file( 'setup.php' ); do_action( 'itsec_modules_do_plugin_upgrade', $old_version, $new_version ); } /** * Get the container. * * @return ContainerInterface */ public static function get_container() { if ( ! self::get_instance()->initialized_container ) { self::initialize_container(); } return self::get_instance()->container; } /** * Load the settings controller for all registered modules. * * This function can only be run once per-request. */ public function load_settings_page() { self::load_module_file( 'settings-page.php' ); } /** * Get labels for a module. * * @param string $module * * @return array */ public static function get_labels( $module ) { $config = self::get_config( $module ); if ( $config ) { return [ 'title' => $config->translate( Module_Config::T_ABOUT )->get_title(), 'description' => $config->translate( Module_Config::T_ABOUT )->get_description(), ]; } if ( ! isset( self::get_instance()->labels[ $module ] ) ) { self::get_instance()->labels[ $module ] = []; self::load_module_file( 'labels.php', $module, function ( $labels, $module ) { if ( is_array( $labels ) ) { self::get_instance()->labels[ $module ] = $labels; } } ); } return self::get_instance()->labels[ $module ]; } /** * Validates a module's requirements. * * @param string $module * @param string $mode The mode to evaluate for. Either 'activate' or 'run'. * * @return WP_Error */ public static function validate_module_requirements( string $module, string $mode ): WP_Error { if ( defined( 'ITSEC_IGNORE_MODULE_REQUIREMENTS' ) && ITSEC_IGNORE_MODULE_REQUIREMENTS ) { return new WP_Error(); } $config = self::get_config( $module ); if ( ! $config || ! $config->get_requirements() ) { return new WP_Error(); } $requirements = $config->get_requirements(); $check = []; if ( isset( $requirements['ssl'] ) && ( $mode === 'activate' || $requirements['ssl']['validate'] === $mode ) ) { $check['ssl'] = true; } if ( isset( $requirements['feature-flags'] ) && ( $mode === 'activate' || $requirements['feature-flags']['validate'] === $mode ) ) { $check['feature-flags'] = $requirements['feature-flags']['flags']; } if ( isset( $requirements['multisite'] ) && ( $mode === 'activate' || $requirements['multisite']['validate'] === $mode ) ) { $check['multisite'] = $requirements['multisite']['status']; } if ( isset( $requirements['server'] ) && ( $mode === 'activate' || $requirements['server']['validate'] === $mode ) ) { $check['server'] = [ 'php' => $requirements['server']['php'] ?? null, 'extensions' => $requirements['server']['extensions'] ?? [], ]; } if ( isset( $requirements['load'] ) && ( $mode === 'activate' || $requirements['load']['validate'] === $mode ) ) { $check['load'] = $requirements['load']['type']; } if ( isset( $requirements['ip'] ) && ( $mode === 'activate' || $requirements['ip']['validate'] === $mode ) ) { $check['ip'] = true; } return ITSEC_Lib::evaluate_requirements( $check, $mode === 'activate' ); } public function get_export_slug(): string { return 'modules'; } public function get_export_title(): string { return __( 'Features', 'better-wp-security' ); } public function get_export_description(): string { return __( 'List of active modules.', 'better-wp-security' ); } public function get_export_options_schema(): array { return []; } public function get_export_schema(): array { return [ 'type' => 'object', 'additionalProperties' => [ 'type' => 'string', 'enum' => [ 'active', 'inactive' ], ], ]; } public function get_transformations(): array { return []; } public function export( $options ): Result { $data = []; foreach ( self::get_available_modules() as $module ) { if ( ! self::is_always_active( $module ) ) { $data[ $module ] = self::is_active( $module ) ? 'active' : 'inactive'; } } return Result::success( $data ); } public function import( Export $from, Import_Context $context ): Result { $result = Result::success(); if ( ! $from->has_data( $this->get_export_slug() ) ) { return $result; } foreach ( $from->get_data( $this->get_export_slug() ) as $module => $status ) { $success = true; if ( $status === 'active' && ! self::is_active( $module ) ) { $success = self::activate( $module ); } elseif ( $status === 'inactive' && self::is_active( $module ) ) { $success = self::deactivate( $module ); } if ( is_wp_error( $success ) ) { $result->add_warning_message( ...ITSEC_Lib::get_error_strings( $success ) ); } } return $result; } /** * Validates a module's schema. * * @param array $config * * @return true|WP_Error */ private static function validate_module_config( $config ) { $self = self::get_instance(); if ( ! $self->module_schema ) { $self->module_schema = ITSEC_Lib::resolve_schema_refs( json_decode( file_get_contents( __DIR__ . '/module-schema.json' ), true ) ); } return rest_validate_value_from_schema( $config, $self->module_schema, 'config' ); } /** * Initializes the module's feature flags. * * @param Module_Config $config */ private static function initialize_feature_flags( Module_Config $config ) { if ( ! $config->get_feature_flags() ) { return; } $flags = $config->get_feature_flags(); if ( self::is_active( $config->get_id() ) ) { $register = array_keys( $flags ); } else { $register = $config->get_requirements()['feature-flags']['flags'] ?? []; } foreach ( $register as $flag ) { if ( isset( $flags[ $flag ] ) ) { ITSEC_Lib_Feature_Flags::register_flag( $flag, $flags[ $flag ] ); } } } /** * Checks if the given module has the requested file. * * @param string $module The module id. * @param string $file The filename to check. * * @return bool */ private static function module_has_file( $module, $file ) { return file_exists( self::get_instance()->_module_paths[ $module ] . '/' . $file ); } /** * Transforms a module specifier to a list of modules. * * @param string|array $modules The modules specifier. * * @return string[] */ private static function transform_modules_specifier( $modules ) { if ( ':all' === $modules ) { return self::get_available_modules(); } if ( ':active' === $modules ) { return self::get_active_modules_to_run(); } if ( ':active-early' === $modules ) { return array_filter( self::get_active_modules_to_run(), function ( $module ) { return self::get_instance()->module_config[ $module ]->can_load_early(); } ); } if ( ':active-normal' === $modules ) { return array_filter( self::get_active_modules_to_run(), function ( $module ) { return ! self::get_instance()->module_config[ $module ]->can_load_early(); } ); } if ( is_string( $modules ) ) { return [ $modules ]; } if ( is_array( $modules ) ) { return $modules; } return []; } private function run( $definition ) { if ( $definition && is_string( $definition ) ) { $object = $this->container->get( $definition ); if ( $object instanceof Runnable ) { $object->run(); } } } /** * Load the container definitions for a module. * * @param string $module */ private function load_container_definitions( $module ) { if ( ! isset( $this->loaded_containers[ $module ] ) && isset( $this->_module_paths[ $module ] ) ) { $path = $this->_module_paths[ $module ] . '/container.php'; if ( file_exists( $this->_module_paths[ $module ] . '/container.php' ) && $register = include( $path ) ) { $this->loaded_containers[ $module ] = true; $register( $this->pimple ); } else { $this->loaded_containers[ $module ] = false; } } } } ITSEC_Modules::get_instance();