1056 lines
28 KiB
PHP
1056 lines
28 KiB
PHP
<?php
|
|
defined( 'ABSPATH' ) || die();
|
|
|
|
use RSSSL\Security\RSSSL_Htaccess_File_Manager;
|
|
use RSSSL\Pro\Security\WordPress\Rsssl_Geo_Block;
|
|
|
|
/**
|
|
* Class to handle the creation and include of the firewall
|
|
*/
|
|
class rsssl_firewall_manager {
|
|
/**
|
|
* Marker string for .htaccess rules related to auto prepend file.
|
|
*/
|
|
private const HTACCESS_MARKER_PREPEND = 'Really Simple Auto Prepend File';
|
|
/**
|
|
* Firewall object
|
|
*/
|
|
private static rsssl_firewall_manager $this;
|
|
|
|
/**
|
|
* The htaccess file manager
|
|
*/
|
|
public RSSSL_Htaccess_File_Manager $htAccessFile;
|
|
/**
|
|
* File
|
|
*
|
|
* @var string
|
|
*/
|
|
private $file;
|
|
/**
|
|
* If we can use a dynamic path
|
|
*
|
|
* @var bool
|
|
*/
|
|
private $dynamic_path;
|
|
/**
|
|
* Path to the firewall.php file, filterable.
|
|
*
|
|
* @var string
|
|
*/
|
|
private string $firewall_file_path;
|
|
/**
|
|
* Rules to add to the firewall.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $rules;
|
|
/**
|
|
* The WP_Filesystem instance, used for file operations.
|
|
*
|
|
*/
|
|
private $wp_filesystem;
|
|
|
|
public function __construct(RSSSL_Htaccess_File_Manager $htaccessFile) {
|
|
|
|
if ( isset( self::$this ) ) {
|
|
wp_die();
|
|
}
|
|
self::$this = $this;
|
|
|
|
// Store the injected htaccess file manager
|
|
$this->htAccessFile = $htaccessFile;
|
|
|
|
// Set dynamic path detection dynamically to handle environment changes
|
|
$this->dynamic_path = $this->get_dynamic_path();
|
|
|
|
// Determine firewall.php path, allowing custom content dir or fallback.
|
|
if ( $this->dynamic_path ) {
|
|
$wpContentPath = ABSPATH . 'wp-content/';
|
|
} else {
|
|
$wpContentPath = WP_CONTENT_DIR . '/';
|
|
}
|
|
|
|
$this->firewall_file_path = apply_filters(
|
|
'rsssl_firewall_file_path',
|
|
$wpContentPath . 'firewall.php'
|
|
);
|
|
|
|
// Set the file path dynamically so we can detect WP_CONTENT_DIR changes
|
|
$this->file = $this->get_advanced_headers_path();
|
|
|
|
// trigger this action to force rules update
|
|
add_action( 'rsssl_update_rules', array( $this, 'install' ), 10 );
|
|
add_action( 'rsssl_after_saved_fields', array( $this, 'install' ), 100 );
|
|
add_action( 'rsssl_deactivate', array( $this, 'uninstall' ), 20 );
|
|
|
|
// Proactively check for environment changes on admin loads
|
|
add_action( 'admin_init', array( $this, 'maybe_regenerate_firewall' ), 5 );
|
|
|
|
add_filter( 'rsssl_notices', array( $this, 'notices' ) );
|
|
|
|
//handle activation and deactivation of wp rocket
|
|
add_action( 'rocket_activation', array( $this, 'remove_prepend_file_in_htaccess' ) );
|
|
add_action( 'rocket_deactivation', array( $this, 'include_prepend_file_in_htaccess' ) );
|
|
|
|
if ( ! defined( 'RSSSL_IS_WP_ENGINE' ) ) {
|
|
define( 'RSSSL_IS_WP_ENGINE', isset( $_SERVER['IS_WPE'] ) );
|
|
}
|
|
if ( ! defined( 'RSSSL_IS_FLYWHEEL' ) ) {
|
|
define( 'RSSSL_IS_FLYWHEEL', isset( $_SERVER['SERVER_SOFTWARE'] ) && strpos( $_SERVER['SERVER_SOFTWARE'], 'Flywheel/' ) === 0 );
|
|
}
|
|
if ( ! defined( 'RSSSL_IS_PRESSABLE' ) ) {
|
|
define( 'RSSSL_IS_PRESSABLE', ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) || ( defined( 'IS_PRESSABLE' ) && IS_PRESSABLE ) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main installer for the firewall file
|
|
*
|
|
* @return void
|
|
*/
|
|
public function install(): void {
|
|
// Don't regenerate files during deactivation
|
|
if ( doing_action( 'rsssl_deactivate' ) ) {
|
|
return;
|
|
}
|
|
|
|
if ( ! rsssl_admin_logged_in() ) {
|
|
return;
|
|
}
|
|
|
|
if ( wp_doing_ajax() ) {
|
|
return;
|
|
}
|
|
|
|
if ( empty( $this->rules ) ) {
|
|
$this->rules = apply_filters( 'rsssl_firewall_rules', '' );
|
|
}
|
|
|
|
// no rules? remove the file.
|
|
if ( empty( trim( $this->rules ) ) ) {
|
|
$this->remove_prepend_file_in_htaccess();
|
|
$this->remove_prepend_file_in_wp_config();
|
|
return;
|
|
}
|
|
|
|
// update the file to be included.
|
|
$this->update_firewall( $this->rules );
|
|
|
|
$this->include_prepend_file_in_wp_config();
|
|
if ( $this->uses_htaccess() ) {
|
|
// only include in the admin_init, to prevent issues with the htaccess file not being writable.
|
|
if( current_filter() !== 'plugins_loaded' ) {
|
|
$this->include_prepend_file_in_htaccess();
|
|
}
|
|
}
|
|
|
|
if ( $this->has_user_ini_file() ) {
|
|
$this->include_prepend_file_in_user_ini();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove file and file inclusions
|
|
*
|
|
* @return void
|
|
*/
|
|
public function uninstall(): void {
|
|
if ( ! rsssl_user_can_manage() ) {
|
|
return;
|
|
}
|
|
|
|
if ( wp_doing_ajax() ) {
|
|
return;
|
|
}
|
|
|
|
$this->remove_prepend_file_in_htaccess();
|
|
$this->remove_prepend_file_in_wp_config();
|
|
$this->remove_auto_prepend_file_in_user_ini();
|
|
|
|
$this->empty_file();
|
|
$this->delete_test_file();
|
|
|
|
// Delete firewall.php file using the existing handler
|
|
if ( class_exists( '\RSSSL\Pro\Security\WordPress\Firewall\Rsssl_Firewall_File_Handler' ) ) {
|
|
$firewall_handler = new \RSSSL\Pro\Security\WordPress\Firewall\Rsssl_Firewall_File_Handler();
|
|
$firewall_handler->delete();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Proactively check for environment changes on admin loads
|
|
* This ensures firewall regeneration after site clones/migrations
|
|
*
|
|
* @return void
|
|
*/
|
|
public function maybe_regenerate_firewall(): void {
|
|
|
|
if ( ! rsssl_user_can_manage() ) {
|
|
return;
|
|
}
|
|
|
|
// Only check if we have firewall rules that need to be active
|
|
if ( ! $this->has_rules() ) {
|
|
return;
|
|
}
|
|
|
|
// Only run the check if environment has changed
|
|
if ( $this->should_regenerate_firewall() ) {
|
|
// Trigger the full installation process for firewall.php
|
|
$this->install();
|
|
// Also generate the Geo Block firewall settings
|
|
$fireWallSettingIsEnabled = rsssl_get_option( 'enable_firewall', false );
|
|
if ( $fireWallSettingIsEnabled ) {
|
|
$geoBlock = Rsssl_Geo_Block::get_instance();
|
|
$geoBlock->generate_firewall_rules();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if our firewall file exists
|
|
*
|
|
* @param string $file // filename, including path
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function file_exists( string $file ): bool {
|
|
$wp_filesystem = $this->get_file_system();
|
|
|
|
// Use WP Filesystem if available, otherwise fall back to direct operations
|
|
return $wp_filesystem ? $wp_filesystem->is_file( $file ) : file_exists( $file );
|
|
}
|
|
|
|
/**
|
|
* Get the WP_Filesystem instance with lazy loading
|
|
*
|
|
* @return false|WP_Filesystem_Base
|
|
*/
|
|
private function get_file_system() {
|
|
// Return cached instance if available
|
|
if ( $this->wp_filesystem !== null ) {
|
|
return $this->wp_filesystem;
|
|
}
|
|
|
|
if ( ! function_exists( 'WP_Filesystem' ) ) {
|
|
include_once ABSPATH . 'wp-admin/includes/file.php';
|
|
}
|
|
if ( false === ( $creds = request_filesystem_credentials( site_url(), '', false, false, null ) ) ) {
|
|
$this->wp_filesystem = false;
|
|
return false; // stop processing here.
|
|
}
|
|
global $wp_filesystem;
|
|
if ( ! WP_Filesystem( $creds ) ) {
|
|
// request_filesystem_credentials(site_url(), '', true, false, null);//phpcs:ingore
|
|
$this->wp_filesystem = false;
|
|
return false;
|
|
}
|
|
|
|
// Cache the instance
|
|
$this->wp_filesystem = $wp_filesystem;
|
|
return $wp_filesystem;
|
|
}
|
|
|
|
/**
|
|
* Update the file that contains the firewall rules, advanced-headers.php
|
|
*
|
|
* @param string $rules //rules to add to the firewall.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function update_firewall( string $rules ): void
|
|
{
|
|
if ( ! rsssl_admin_logged_in() ) {
|
|
return;
|
|
}
|
|
|
|
$contents = '<?php' . "\n";
|
|
$contents .= '/**' . "\n";
|
|
$contents .= '* This file is created by Really Simple Security' . "\n";
|
|
$contents .= '*/' . "\n\n";
|
|
$contents .= 'if (defined("SHORTINIT") && SHORTINIT) return;' . "\n\n";
|
|
$contents .= '$base_path = dirname(__FILE__);' . "\n";
|
|
$contents .= 'if( file_exists( $base_path . "/rsssl-safe-mode.lock" ) ) {' . "\n";
|
|
$contents .= ' if ( ! defined( "RSSSL_SAFE_MODE" ) ) {' . "\n";
|
|
$contents .= ' define( "RSSSL_SAFE_MODE", true );' . "\n";
|
|
$contents .= ' }' . "\n";
|
|
$contents .= ' return;' . "\n";
|
|
$contents .= '}' . "\n\n";
|
|
// allow disabling of headers for detection purposes.
|
|
$contents .= 'if ( isset($_GET["rsssl_header_test"]) && (int) $_GET["rsssl_header_test"] === ' . $this->get_headers_nonce() . ' ) return;' . "\n\n";
|
|
//if already included at some point, don't execute again.
|
|
$contents .= 'if ( defined("RSSSL_HEADERS_ACTIVE" ) ) return;' . "\n";
|
|
$contents .= 'define( "RSSSL_HEADERS_ACTIVE", true );' . "\n";
|
|
|
|
// If the main firewall (firewall.php) is enabled, add the include directive for it.
|
|
if ( rsssl_get_option( 'enable_firewall', false ) ) {
|
|
$firewallFilePath = $this->firewall_file_path;
|
|
$contents .= 'if ( file_exists( "' . $firewallFilePath . '" ) ) {' . "\n";
|
|
$contents .= ' require_once "' . $firewallFilePath . '";' . "\n";
|
|
$contents .= '}' . "\n\n";
|
|
}
|
|
|
|
$contents .= "//RULES START\n" . $rules;
|
|
|
|
$this->put_contents( $this->file, $contents );
|
|
}
|
|
|
|
/**
|
|
* Save data
|
|
*
|
|
* @param string $file //filename, including path.
|
|
* @param string $contents //data to save.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function put_contents( $file, $contents ): void {
|
|
if ( ! rsssl_admin_logged_in() ) {
|
|
return;
|
|
}
|
|
|
|
// Check if file is writable (or doesn't exist yet, which is fine)
|
|
if ( $this->file_exists( $file ) && ! $this->is_writable( $file ) ) {
|
|
return;
|
|
}
|
|
|
|
$wp_filesystem = $this->get_file_system();
|
|
|
|
if ( $wp_filesystem === false ) {
|
|
file_put_contents( $file, $contents );//phpcs:ignore
|
|
return;
|
|
}
|
|
|
|
$wp_filesystem->put_contents( $file, $contents );
|
|
|
|
// Only chmod files other than .htaccess and wp-config.php
|
|
if ( strpos($file, 'htaccess') === false && strpos($file, 'wp-config.php') === false ) {
|
|
$wp_filesystem->chmod( $file, 0644 );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the contents of a file
|
|
*
|
|
* @param string $file //filename, including path.
|
|
*
|
|
* @return string
|
|
*/
|
|
private function get_contents( string $file ): string {
|
|
// Validate that file path is not empty
|
|
if ( empty( $file ) ) {
|
|
return '';
|
|
}
|
|
|
|
$wp_filesystem = $this->get_file_system();
|
|
|
|
if ( $wp_filesystem === false ) {
|
|
return file_exists( $file ) ? file_get_contents( $file ) : '';//phpcs:ignore
|
|
}
|
|
|
|
$result = $wp_filesystem->get_contents( $file );
|
|
return $result ? $result : '';
|
|
}
|
|
|
|
/**
|
|
* Empty the advanced-headers.php file instead of deleting it.
|
|
* This prevents 500 errors when user.ini is cached and still references this file.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function empty_file(): void {
|
|
if ( ! rsssl_user_can_manage() ) {
|
|
return;
|
|
}
|
|
|
|
$contents = <<<PHP
|
|
<?php
|
|
// This file was created by Really Simple Security
|
|
// It is no longer used and safe to delete
|
|
PHP;
|
|
|
|
$this->put_contents( $this->file, $contents );
|
|
}
|
|
|
|
/**
|
|
* Get the path to the advanced-headers-test.php file.
|
|
*
|
|
* @return string
|
|
*/
|
|
private function get_test_file_path(): string {
|
|
return WP_CONTENT_DIR . '/advanced-headers-test.php';
|
|
}
|
|
|
|
/**
|
|
* Delete the advanced-headers-test.php file if it exists.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function delete_test_file(): void {
|
|
if ( ! rsssl_user_can_manage() ) {
|
|
return;
|
|
}
|
|
|
|
$test_file = $this->get_test_file_path();
|
|
if ( ! file_exists( $test_file ) ) {
|
|
return;
|
|
}
|
|
|
|
$wp_filesystem = $this->get_file_system();
|
|
if ( $wp_filesystem === false ) {
|
|
unlink( $test_file );//phpcs:ignore
|
|
return;
|
|
}
|
|
|
|
$wp_filesystem->delete( $test_file );
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*
|
|
* Check if installation uses htaccess.conf (Bitnami)
|
|
*/
|
|
private function uses_htaccess_conf() {
|
|
$htaccess_conf_file = dirname( ABSPATH ) . '/conf/htaccess.conf';
|
|
//conf/htaccess.conf can be outside of open basedir, return false if so
|
|
$open_basedir = ini_get( 'open_basedir' );
|
|
if ( ! empty( $open_basedir ) ) {
|
|
return false;
|
|
}
|
|
return is_file( $htaccess_conf_file );
|
|
}
|
|
|
|
/**
|
|
* Get the .htaccess path
|
|
*
|
|
* @return string
|
|
*/
|
|
private function htaccess_path(): string {
|
|
|
|
if ( $this->uses_htaccess_conf() ) {
|
|
$htaccess_file = realpath( dirname( ABSPATH ) . '/conf/htaccess.conf' );
|
|
} else {
|
|
$htaccess_file = $this->get_home_path() . '.htaccess';
|
|
}
|
|
|
|
return $htaccess_file;
|
|
|
|
}
|
|
|
|
/**
|
|
* Get the home path
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_home_path(): string {
|
|
if ( ! function_exists( 'get_home_path' ) ) {
|
|
include_once ABSPATH . 'wp-admin/includes/file.php';
|
|
}
|
|
if ( defined('RSSSL_IS_FLYWHEEL') && RSSSL_IS_FLYWHEEL && isset( $_SERVER['DOCUMENT_ROOT'] ) ) {
|
|
return trailingslashit( $this->sanitize_path( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) );
|
|
}
|
|
return get_home_path();
|
|
}
|
|
|
|
/**
|
|
* Sanitize a path
|
|
*
|
|
* @param string $path //string to sanitize.
|
|
*
|
|
* @return string
|
|
*/
|
|
private function sanitize_path( $path ): string {
|
|
// prevent path traversal.
|
|
return str_replace( '../', '/', realpath( sanitize_text_field( $path ) ) );
|
|
}
|
|
|
|
/**
|
|
* Check if this server uses .htaccess. Not by checking the server header, but simply by checking
|
|
* if the htaccess file exists.
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function uses_htaccess(): bool {
|
|
return $this->file_exists( $this->htaccess_path() );
|
|
}
|
|
|
|
/**
|
|
* Include the prepend file in the .htaccess
|
|
*
|
|
* @return void
|
|
*/
|
|
public function include_prepend_file_in_htaccess(): void
|
|
{
|
|
if ( ! $this->file_exists( $this->file ) ) {
|
|
return;
|
|
}
|
|
|
|
// check if the wp-config contains the if constant condition, to prevent duplicate loading. If not, try upgrading. If that fails, skip.
|
|
if ( ! $this->wp_config_contains_latest() ) {
|
|
return;
|
|
}
|
|
|
|
$htaccess_file = $this->htaccess_path();
|
|
if ( !$this->file_exists($htaccess_file) || !$this->is_writable($htaccess_file) ) {
|
|
return;
|
|
}
|
|
|
|
$htaccess_manager = new RSSSL_Htaccess_File_Manager();
|
|
$rules_string = $this->get_htaccess_rules();
|
|
|
|
$rule_definition = [
|
|
'marker' => self::HTACCESS_MARKER_PREPEND,
|
|
'lines' => empty(trim($rules_string)) ? [] : explode("\n", $rules_string),
|
|
];
|
|
$htaccess_manager->write_rule($rule_definition, 'include prepend file in htaccess');
|
|
}
|
|
|
|
/**
|
|
* Get the .htaccess rules for the prepend file
|
|
* Add user.ini blocking rules if user.ini filename exist.
|
|
*
|
|
* @return string //the string containing the lines of rules
|
|
*/
|
|
private function get_htaccess_rules() : string
|
|
{
|
|
if ( defined('RSSSL_HTACCESS_SKIP_AUTO_PREPEND') && RSSSL_HTACCESS_SKIP_AUTO_PREPEND ) {
|
|
return '';
|
|
}
|
|
if (isset(RSSSL()->server) ) {
|
|
$config = RSSSL()->server->auto_prepend_config();
|
|
} else {
|
|
$config = get_option('rsssl_auto_prepend_config');
|
|
if (empty($config)) {
|
|
return '';
|
|
}
|
|
}
|
|
$file = addcslashes($this->file, "'");
|
|
switch ($config) {
|
|
case 'litespeed':
|
|
$rules = array(
|
|
'<IfModule LiteSpeed>',
|
|
'php_value auto_prepend_file ' . $file ,
|
|
'</IfModule>',
|
|
'<IfModule lsapi_module>',
|
|
'php_value auto_prepend_file ' . $file,
|
|
'</IfModule>',
|
|
);
|
|
break;
|
|
case 'apache-mod_php':
|
|
default:
|
|
$rules = array(
|
|
'<IfModule mod_php7.c>',
|
|
'php_value auto_prepend_file ' . $file ,
|
|
'</IfModule>',
|
|
'<IfModule mod_php.c>',
|
|
'php_value auto_prepend_file ' . $file,
|
|
'</IfModule>',
|
|
);
|
|
}
|
|
|
|
$userIni = ini_get('user_ini.filename');
|
|
if ($userIni) {
|
|
array_push(
|
|
$rules,
|
|
sprintf( '<Files "%s">',
|
|
addcslashes( $userIni, '"' ) ),
|
|
'<IfModule mod_authz_core.c>',
|
|
'Require all denied', '</IfModule>',
|
|
'<IfModule !mod_authz_core.c>',
|
|
'Order deny,allow', 'Deny from all',
|
|
'</IfModule>',
|
|
'</Files>'
|
|
);
|
|
}
|
|
|
|
return implode( "\n", $rules );
|
|
}
|
|
|
|
/**
|
|
* Include the file in the wp-config
|
|
*
|
|
* @return void
|
|
*/
|
|
private function include_prepend_file_in_wp_config(): void {
|
|
if ( ! rsssl_user_can_manage() ) {
|
|
return;
|
|
}
|
|
$file = $this->wpconfig_path();
|
|
if ( empty( $file ) ) {
|
|
update_option( 'rsssl_firewall_error', 'wpconfig-notfound', false );
|
|
return;
|
|
}
|
|
$content = $this->get_contents( $file );
|
|
if ( strpos( $content, 'advanced-headers.php' ) === false ) {
|
|
$rule = $this->get_wp_config_rule();
|
|
|
|
// if RSSSL comment is found, insert after.
|
|
$rsssl_comment = '//END Really Simple Security Server variable fix';
|
|
if ( strpos( $content, $rsssl_comment ) !== false ) {
|
|
$pos = strrpos( $content, $rsssl_comment );
|
|
$updated = substr_replace( $content, $rsssl_comment . "\n" . $rule . "\n", $pos, strlen( $rsssl_comment ) );
|
|
} else {
|
|
$updated = preg_replace( '/<\?php/', "<?php\n" . $rule . "\n", $content, 1 );
|
|
}
|
|
|
|
if ( strpos( $updated, "\n\n\n" ) !== false ) {
|
|
$updated = str_replace( "\n\n\n", "\n\n", $updated );
|
|
}
|
|
|
|
$this->put_contents( $file, $updated );
|
|
}
|
|
|
|
// save errors.
|
|
if ( $this->is_writable( WP_CONTENT_DIR ) && ( $this->is_writable( $file ) || strpos( $content, 'advanced-headers.php' ) !== false ) ) {
|
|
update_option( 'rsssl_firewall_error', false, false );
|
|
} elseif ( ! $this->is_writable( $file ) ) {
|
|
update_option( 'rsssl_firewall_error', 'wpconfig-notwritable', false );
|
|
} elseif ( ! $this->is_writable( WP_CONTENT_DIR ) ) {
|
|
update_option( 'rsssl_firewall_error', 'advanced-headers-notwritable', false );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear the rules
|
|
*
|
|
* @return void
|
|
*/
|
|
public function remove_prepend_file_in_htaccess(): void
|
|
{
|
|
if ( ! rsssl_user_can_manage() ) {
|
|
return;
|
|
}
|
|
|
|
// Initialize htAccessFile if not set
|
|
if ( ! isset($this->htAccessFile) ) {
|
|
$this->htAccessFile = new RSSSL_Htaccess_File_Manager();
|
|
}
|
|
|
|
// Determine the correct .htaccess file path this instance of firewall manager should use.
|
|
$specific_htaccess_path = $this->htaccess_path();
|
|
|
|
// Ensure the injected htaccess_file_manager service instance is configured to use this specific path.
|
|
$this->htAccessFile->set_htaccess_file_path($specific_htaccess_path);
|
|
|
|
// Call clear_rule on the htaccess_file_manager service.
|
|
// The service itself is responsible for handling file existence and writability.
|
|
$this->htAccessFile->clear_rule(self::HTACCESS_MARKER_PREPEND, 'testregel');
|
|
}
|
|
|
|
/**
|
|
* Remove the prepend file from the config
|
|
*
|
|
* @return void
|
|
*/
|
|
private function remove_prepend_file_in_wp_config(): void {
|
|
if ( ! rsssl_user_can_manage() ) {
|
|
return;
|
|
}
|
|
|
|
$file = $this->wpconfig_path();
|
|
if ( empty( $file ) ) {
|
|
return;
|
|
}
|
|
if ( $this->is_writable( $file ) ) {
|
|
$content = $this->get_contents( $file );
|
|
$rule = $this->get_wp_config_rule();
|
|
if ( strpos( $content, $rule ) !== false ) {
|
|
$content = str_replace( $rule, '', $content );
|
|
if ( strpos( $content, "\n\n\n" ) !== false ) {
|
|
$content = str_replace( "\n\n\n", "\n\n", $content );
|
|
}
|
|
$this->put_contents( $file, $content );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wrapper function
|
|
*
|
|
* @param string $file // filename, including path.
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function is_writable( $file ): bool {
|
|
$wp_filesystem = $this->get_file_system();
|
|
|
|
// Use WP Filesystem if available, otherwise fall back to direct operations
|
|
return $wp_filesystem ? $wp_filesystem->is_writable( $file ) : is_writable( $file );//phpcs:ignore
|
|
}
|
|
|
|
/**
|
|
* This class has it's own settings page, to ensure it can always be called
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_settings_page() {
|
|
if ( rsssl_is_logged_in_rest() ) {
|
|
return true;
|
|
}
|
|
|
|
if ( isset( $_GET['page'] ) && 'really-simple-security' === $_GET['page'] ) {//phpcs:ignore
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Generate and return a random nonce
|
|
*
|
|
* @return int
|
|
*/
|
|
public function get_headers_nonce() {
|
|
if ( ! get_site_option( 'rsssl_header_detection_nonce' ) ) {
|
|
update_site_option( 'rsssl_header_detection_nonce', wp_rand( 1000, 999999999 ) );
|
|
}
|
|
return (int) get_site_option( 'rsssl_header_detection_nonce' );
|
|
}
|
|
|
|
/**
|
|
* Check if any rules were added
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function has_rules() {
|
|
|
|
if ( empty( $this->rules ) ) {
|
|
$this->rules = apply_filters( 'rsssl_firewall_rules', '' );
|
|
}
|
|
return ! empty( trim( $this->rules ) );
|
|
}
|
|
|
|
/**
|
|
* Get the status for the firewall rules writing
|
|
*
|
|
* @return false|string
|
|
*/
|
|
public function firewall_write_error() {
|
|
return get_site_option( 'rsssl_firewall_error' );
|
|
}
|
|
|
|
/**
|
|
* Get the status for the firewall
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function firewall_active_error() {
|
|
if ( ! $this->has_rules() ) {
|
|
return false;
|
|
}
|
|
return ! defined( 'RSSSL_HEADERS_ACTIVE' );
|
|
}
|
|
|
|
/**
|
|
* Show some notices
|
|
*
|
|
* @param array $notices //array of notices.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function notices( $notices ) {
|
|
$notices['firewall-error'] = array(
|
|
'callback' => 'RSSSL_SECURITY()->firewall_manager->firewall_write_error',
|
|
'score' => 5,
|
|
'output' => array(
|
|
'wpconfig-notwritable' => array(
|
|
'title' => __( 'Firewall', 'really-simple-ssl' ),
|
|
'msg' => __( 'A firewall rule was enabled, but the wp-config.php is not writable.', 'really-simple-ssl' ) . ' ' . __( 'Please set the wp-config.php to writable until the rule has been written.', 'really-simple-ssl' ),
|
|
'icon' => 'open',
|
|
'dismissible' => true,
|
|
),
|
|
'advanced-headers-notwritable' => array(
|
|
'title' => __( 'Firewall', 'really-simple-ssl' ),
|
|
'msg' => __( 'A firewall rule was enabled, but /the wp-content/ folder is not writable.', 'really-simple-ssl' ) . ' ' . __( 'Please set the wp-content folder to writable:', 'really-simple-ssl' ),
|
|
'icon' => 'open',
|
|
'dismissible' => true,
|
|
),
|
|
),
|
|
'show_with_options' => array(
|
|
'disable_http_methods',
|
|
),
|
|
);
|
|
$notices['firewall-active'] = array(
|
|
'condition' => array( 'RSSSL_SECURITY()->firewall_manager->firewall_active_error' ),
|
|
'callback' => '_true_',
|
|
'score' => 5,
|
|
'output' => array(
|
|
'true' => array(
|
|
'title' => __( 'Firewall', 'really-simple-ssl' ),
|
|
'msg' => __( 'A firewall rule was enabled, but the firewall does not seem to get loaded correctly.', 'really-simple-ssl' ) . ' ' . __( 'Please check if the advanced-headers.php file is included in the wp-config.php, and exists in the wp-content folder.', 'really-simple-ssl' ),
|
|
'icon' => 'open',
|
|
'dismissible' => true,
|
|
),
|
|
),
|
|
'show_with_options' => array(
|
|
'disable_http_methods',
|
|
),
|
|
);
|
|
return $notices;
|
|
}
|
|
|
|
/**
|
|
* // As WP_CONTENT_DIR is not defined at this point in the wp-config, we can't use that.
|
|
* // for those setups where the WP_CONTENT_DIR is not in the default location, we hardcode the path.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_wp_config_rule() {
|
|
if ( $this->dynamic_path ) {
|
|
$rule = 'if (!defined("RSSSL_HEADERS_ACTIVE") && file_exists( ABSPATH . "wp-content/advanced-headers.php")) {' . "\n";
|
|
$rule .= "\t" . 'require_once ABSPATH . "wp-content/advanced-headers.php";' . "\n" . '}';
|
|
} else {
|
|
$rule = 'if (!defined("RSSSL_HEADERS_ACTIVE") && file_exists(\'' . WP_CONTENT_DIR . '/advanced-headers.php\')) {' . "\n";
|
|
$rule .= "\t" . 'require_once \'' . WP_CONTENT_DIR . '/advanced-headers.php\';' . "\n" . '}';
|
|
}
|
|
return $rule;
|
|
}
|
|
|
|
/**
|
|
* Check if the wp-config contains the if constant condition, to prevent duplicate loading. If not, try upgrading. If that fails, skip.
|
|
* Wrapper function added for clearer purpose in code
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function wp_config_contains_latest(): bool {
|
|
return $this->update_wp_config_rule();
|
|
}
|
|
|
|
/**
|
|
* Called in upgrade.php, to upgrade older rules to the latest.
|
|
* Returns true if the wpconfig contains the upgraded lines
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function update_wp_config_rule(): bool {
|
|
$file = $this->wpconfig_path();
|
|
if ( ! $file ) {
|
|
return false;
|
|
}
|
|
|
|
$content = $this->get_contents( $file );
|
|
$find = '(file_exists( ABSPATH . "wp-content/advanced-headers.php"))';
|
|
if ( false !== strpos( $content, $find ) ) {
|
|
if ( ! $this->is_writable( $file ) ) {
|
|
return false;
|
|
}
|
|
$replace = '(!defined("RSSSL_HEADERS_ACTIVE") && file_exists( ABSPATH . "wp-content/advanced-headers.php"))';
|
|
$content = str_replace( $find, $replace, $content );
|
|
$this->put_contents( $file, $content );
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Admin is not always loaded here, so we define our own function
|
|
*
|
|
* @return string|null
|
|
*/
|
|
public function wpconfig_path() {
|
|
|
|
// Allow the wp-config.php path to be overridden via a filter.
|
|
$filtered_path = apply_filters( 'rsssl_wpconfig_path', '' );
|
|
|
|
// If a filtered path is provided, validate it.
|
|
if ( ! empty( $filtered_path ) ) {
|
|
$directory = dirname( $filtered_path );
|
|
|
|
// Ensure the directory exists before checking for the file.
|
|
if ( is_dir( $directory ) && $this->file_exists( $filtered_path ) ) {
|
|
return $filtered_path;
|
|
}
|
|
}
|
|
|
|
// Limit number of iterations to 5.
|
|
$i = 0;
|
|
$maxiterations = 5;
|
|
$dir = ABSPATH;
|
|
do {
|
|
++ $i;
|
|
if ( $this->file_exists( $dir . 'wp-config.php' ) ) {
|
|
return $dir . 'wp-config.php';
|
|
}
|
|
} while ( ( $dir = realpath( "$dir/.." ) ) && ( $i < $maxiterations ) );//phpcs:ignore
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Clear the headers
|
|
*
|
|
* @return void
|
|
*/
|
|
public function remove_advanced_headers() {
|
|
$this->uninstall();
|
|
}
|
|
|
|
/**
|
|
* Check if the firewall file should be regenerated
|
|
* This detects environment changes like WP Engine clones
|
|
* Also returns true if the file does not exist yet
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function should_regenerate_firewall(): bool {
|
|
|
|
if ( ! $this->file_exists( $this->file ) ) {
|
|
return true;
|
|
}
|
|
|
|
// Check if we have stored environment signature
|
|
$stored_signature = get_option( 'rsssl_firewall_environment_signature' );
|
|
$current_signature = $this->get_environment_signature();
|
|
|
|
// If no stored signature, store it and regenerate
|
|
if ( ! $stored_signature ) {
|
|
update_option( 'rsssl_firewall_environment_signature', $current_signature, false );
|
|
return true;
|
|
}
|
|
|
|
// If signature changed, update it and regenerate
|
|
if ( $stored_signature !== $current_signature ) {
|
|
update_option( 'rsssl_firewall_environment_signature', $current_signature, false );
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Generate a signature of the current environment
|
|
* Used to detect when the site has been cloned or migrated
|
|
*
|
|
* @return string
|
|
*/
|
|
private function get_environment_signature(): string {
|
|
$signature_parts = array(
|
|
WP_CONTENT_DIR,
|
|
ABSPATH,
|
|
get_home_url(),
|
|
get_site_url(),
|
|
);
|
|
|
|
return md5( implode( '|', $signature_parts ) );
|
|
}
|
|
|
|
/**
|
|
* Get the advanced headers file path
|
|
* Always uses WP_CONTENT_DIR which is dynamically set by WordPress
|
|
*
|
|
* @return string
|
|
*/
|
|
private function get_advanced_headers_path(): string {
|
|
return WP_CONTENT_DIR . '/advanced-headers.php';
|
|
}
|
|
|
|
/**
|
|
* Check if we can use a dynamic path for the advanced headers file
|
|
* @return string
|
|
*/
|
|
private function get_dynamic_path(): string {
|
|
return WP_CONTENT_DIR === ABSPATH . 'wp-content';
|
|
}
|
|
|
|
/**
|
|
* Check if a user.ini file exists or is in user.
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function has_user_ini_file():bool {
|
|
$userIni = ini_get('user_ini.filename');
|
|
if ( $userIni ) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Add auto prepend file to user.ini
|
|
*
|
|
* @return void
|
|
*/
|
|
private function include_prepend_file_in_user_ini():void{
|
|
if ( ! rsssl_user_can_manage() ) {
|
|
return;
|
|
}
|
|
|
|
if ( defined('RSSSL_HTACCESS_SKIP_AUTO_PREPEND') && RSSSL_HTACCESS_SKIP_AUTO_PREPEND ) {
|
|
return;
|
|
}
|
|
|
|
$config = RSSSL()->server->auto_prepend_config();
|
|
if ( !$this->has_user_ini_file() ) {
|
|
return;
|
|
}
|
|
$autoPrependIni = '';
|
|
$userIniPath = $this->get_user_ini_path();
|
|
// .user.ini configuration
|
|
switch ($config) {
|
|
case 'cgi':
|
|
case 'nginx':
|
|
case 'apache-suphp':
|
|
case 'litespeed':
|
|
case 'iis':
|
|
$autoPrependIni = sprintf("; BEGIN Really Simple Auto Prepend File
|
|
auto_prepend_file = '%s'
|
|
; END Really Simple Auto Prepend File", addcslashes($this->file, "'"));
|
|
break;
|
|
}
|
|
|
|
if ( !empty($autoPrependIni) ) {
|
|
// Modify .user.ini
|
|
$userIniContent = $this->get_contents($userIniPath);
|
|
if ( $userIniContent ) {
|
|
$userIniContent = str_replace('auto_prepend_file', ';auto_prepend_file', $userIniContent);
|
|
$regex = '/; BEGIN Really Simple Auto Prepend File.*?; END Really Simple Auto Prepend File/is';
|
|
if (preg_match($regex, $userIniContent, $matches)) {
|
|
$userIniContent = preg_replace($regex, $autoPrependIni, $userIniContent);
|
|
} else {
|
|
$userIniContent .= "\n" . $autoPrependIni;
|
|
}
|
|
} else {
|
|
$userIniContent = $autoPrependIni;
|
|
}
|
|
|
|
$this->put_contents($userIniPath, $userIniContent);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the user.ini path
|
|
*
|
|
* @return false|string
|
|
*/
|
|
public function get_user_ini_path() {
|
|
$userIni = ini_get('user_ini.filename');
|
|
if ($userIni) {
|
|
return $this->get_home_path() . $userIni;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Remove the added auto prepend file
|
|
*
|
|
* @return void
|
|
*/
|
|
private function remove_auto_prepend_file_in_user_ini() {
|
|
if ( ! rsssl_user_can_manage() ) {
|
|
return;
|
|
}
|
|
|
|
if ( ! $this->has_user_ini_file() ) {
|
|
return;
|
|
}
|
|
|
|
$userIniPath = $this->get_user_ini_path();
|
|
if ($userIniPath === null) {
|
|
return;
|
|
}
|
|
$userIniContent = $this->get_contents( $userIniPath );
|
|
$userIniContent = preg_replace( '/; BEGIN Really Simple Auto Prepend File.*?; END Really Simple Auto Prepend File/is', '', $userIniContent );
|
|
$userIniContent = str_replace( 'auto_prepend_file', ';auto_prepend_file', $userIniContent );
|
|
$this->put_contents( $userIniPath, $userIniContent );
|
|
}
|
|
|
|
} |