get_option( 'patchstack_license_free', 0 ) == 1 ) { return; } add_action( 'updated_option', [ $this, 'update_option_extras' ], 10, 3 ); } /** * If option is updated, write to .htaccess file. * * @param string $option_name * @param string $option_name * @param mixed $value * @return void */ public function update_option_extras( $option_name, $old_value, $value ) { if ( !in_array( $option_name, [ 'patchstack_prevent_default_file_access', 'patchstack_basic_firewall', 'patchstack_pingback_protection', 'patchstack_block_debug_log_access', 'patchstack_block_fake_bots', 'patchstack_index_views', 'patchstack_trace_and_track', 'patchstack_proxy_comment_posting', 'patchstack_image_hotlinking', 'patchstack_firewall_custom_rules' ] ) ) { return; } if ( $old_value == $value ) { return; } $this->plugin->rules->post_firewall_rules(); } /** * Get the turned on .htaccess firewall settings. * * @return array */ public function get_firewall_rule_settings() { $settings = []; $options = [ 'patchstack_prevent_default_file_access', 'patchstack_basic_firewall', 'patchstack_block_debug_log_access', 'patchstack_block_fake_bots', 'patchstack_index_views', 'patchstack_proxy_comment_posting', 'patchstack_image_hotlinking', 'patchstack_basicscanblock' ]; foreach ( $options as $option ) { if ( get_site_option( $option ) ) { $settings[] = ( $option == 'patchstack_basicscanblock' ? 'webarx_wpscan_block' : str_replace( 'patchstack_', 'webarx_', $option ) ); } } return $settings; } /** * Determine the current state of the firewall. * * @return boolean */ public function firewall() { // Get the firewall state. $sum_of_firewall = 0; foreach ( [ 'patchstack_prevent_default_file_access', 'patchstack_basic_firewall', 'patchstack_pingback_protection', 'patchstack_block_debug_log_access', 'patchstack_block_fake_bots', 'patchstack_index_views', 'patchstack_trace_and_track', 'patchstack_proxy_comment_posting', 'patchstack_image_hotlinking' ] as $option ) { $value = get_site_option( $option, 0 ); $sum_of_firewall += empty( $value ) ? 0 : 1; } // Update the options. $onoff = $sum_of_firewall > 1 ? 0 : 1; foreach ( [ 'patchstack_prevent_default_file_access', 'patchstack_basic_firewall', 'patchstack_block_debug_log_access', 'patchstack_index_views', 'patchstack_proxy_comment_posting' ] as $option ) { update_site_option( $option, $onoff ); } update_site_option( 'patchstack_block_fake_bots', 0 ); update_site_option( 'patchstack_image_hotlinking', 0 ); // Pull the rules or cleanup the .htaccess file? if ( $onoff == 1 ) { $this->plugin->rules->post_firewall_rules(); } else { $this->cleanup_htaccess_file(); } return true; } /** * Write the .htaccess firewall rules to the .htaccess file. * * @param string $rules * @return void */ public function write_rules_to_htaccess( $rules = '' ) { if ( ! $this->is_server_supported() || get_site_option( 'patchstack_disable_htaccess', 0 ) || ( defined( 'PS_DISABLE_HTACCESS' ) && PS_DISABLE_HTACCESS ) ) { return false; } // Determine if the .htaccess file exists. require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; $fs = new WP_Filesystem_Direct( '' ); if ( ! $fs->exists( ABSPATH . '.htaccess' ) ) { $fs->touch( ABSPATH . '.htaccess' ); // Don't continue if .htaccess does not exist or cannot be written to. if ( ! $fs->exists( ABSPATH . '.htaccess' ) ) { return false; } } // Get the current rules. $current = $old = $fs->get_contents( ABSPATH . '.htaccess' ); $current = $this->delete_all_between( '# Patchstack Firewall Start', "# Patchstack Firewall End\r\n", $current ); // If no rules, then we delete the old ones. if ( $rules != '' ) { $current = "# Patchstack Firewall Start\r\n\r\nRewriteEngine On\r\n" . $rules . "\r\n\r\n# Patchstack Firewall End\r\n" . $current; } // Put the contents into the .htaccess file. $fs->put_contents( ABSPATH . '.htaccess', $current, FS_CHMOD_FILE ); // Check if the new rules work. // 500 internal server error - did not work. Restore old rules. $status = $this->get_site_status_code(); if ( $status == '' || $status >= 500 ) { $fs->put_contents( ABSPATH . '.htaccess', $old, FS_CHMOD_FILE ); return false; } return true; } /** * Write given rules to the .htaccess file. * * @param string $rules * @return boolean */ public function write_to_htaccess( $rules = '' ) { if ( ! $this->is_server_supported() || get_site_option( 'patchstack_disable_htaccess', 0 ) || ( defined( 'PS_DISABLE_HTACCESS' ) && PS_DISABLE_HTACCESS ) ) { return false; } require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; $fs = new WP_Filesystem_Direct( '' ); if ( ! $fs->exists( ABSPATH . '.htaccess' ) ) { $fs->touch( ABSPATH . '.htaccess' ); } return $this->plugin->htaccess->self_check( $rules ); } /** * Get the web-server type. * * @return boolean */ public function is_server_supported() { $server = strtolower( $_SERVER['SERVER_SOFTWARE'] ); foreach ( [ 'apache', 'nginx', 'litespeed' ] as $webserver ) { if ( strstr( $server, $webserver ) ) { return true; } } return false; } /** * Write .htaccess directly without causing a server missconfiguration (500) * (PHP will end the process even if the browser-window was closed.) * * @param string $new_rules * @return boolean */ public function self_check( $new_rules ) { // Don't continue if we have no rules if ( empty( $new_rules ) ) { return false; } $new_rules = PHP_EOL . PHP_EOL . '# BEGIN Patchstack' . PHP_EOL . $new_rules . PHP_EOL . '# END Patchstack' . PHP_EOL . PHP_EOL; // Require the filesystem libraries. require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; $fs = new WP_Filesystem_Direct( '' ); // Don't continue if .htaccess does not exist or cannot be written to. if ( ! $fs->exists( ABSPATH . '.htaccess' ) ) { return false; } // Get the current data in the .htaccess file. $current_rules = $old = $fs->get_contents( ABSPATH . '.htaccess' ); // Delete all Patchstack related stuff so we can properly re-inject it. $current_rules = $this->delete_all_between( '# BEGIN WebARX', '# END WebARX', $current_rules ); $current_rules = $this->delete_all_between( '# BEGIN Patchstack', '# END Patchstack', $current_rules ); $current_rules = $this->delete_all_between( '# CUSTOM WEBARX RULES', '# END CUSTOM WEBARX RULES', $current_rules ); $current_rules = $this->delete_all_between( '# CUSTOM PATCHSTACK RULES', '# END CUSTOM PATCHSTACK RULES', $current_rules ); $new_rules = $this->delete_all_between( '# BEGIN WordPress', '# END WordPress', $new_rules ); // Get the custom .htaccess rules, if any are set. $custom = $this->get_custom_rules(); if ( get_site_option( 'patchstack_firewall_custom_rules_loc', 'bottom' ) == 'top' ) { $new_rules = $custom . $new_rules; } else { $new_rules = $new_rules . $custom; } // Determine if the new rules even need to be saved. $rules_hash = sha1( $new_rules ); if ( get_site_option( 'patchstack_htaccess_rules_hash', '' ) == $rules_hash && ( stripos( $old, 'begin webarx' ) !== false || stripos( $old, 'begin patchstack' ) !== false ) ) { return false; } // Save the new rules and adjust # and newline of our own rules. update_site_option( 'patchstack_htaccess_rules_hash', $rules_hash ); $new_rules = preg_replace( "/[\r\n]+/", "\r\n", $new_rules ); $new_rules = preg_replace( '/#/', "\r\n#", $new_rules ); // In order to support all Patchstack plugin versions with newline fix, we have to remove this part ourselves. $new_rules = str_replace( "\r\n\r\n# BEGIN Patchstack", '# BEGIN Patchstack', $new_rules ); $new_rules = str_replace( "# END Patchstack\r\n", '# END Patchstack', $new_rules ); // Remove RewriteBase / from the Patchstack rules. $new_rules = str_replace( "\r\n RewriteBase /", '', $new_rules ); $new_rules = str_replace( '/index.php', 'index.php', $new_rules ); // Merge the rules together. $new_rules = $new_rules . "\n" . $current_rules; // Determine if the Patchstack rules starts on its own line. $lines = explode( "\n", $new_rules ); foreach ( $lines as $line ) { if ( stripos( $line, 'begin patchstack' ) !== false && trim( strtolower( $line ) ) != '# begin patchstack' ) { $new_rules = str_replace( '# BEGIN Patchstack', "\r\n# BEGIN Patchstack", $new_rules ); } } // Put the contents into the .htaccess file. $fs->put_contents( ABSPATH . '.htaccess', $new_rules, FS_CHMOD_FILE ); // Check if the new rules work. // 500 internal server error - did not work. Restore old rules. $status = $this->get_site_status_code(); if ( $status == '' || $status >= 500 ) { $fs->put_contents( ABSPATH . '.htaccess', $old, FS_CHMOD_FILE ); update_site_option( 'patchstack_firewall_custom_rules', '' ); } return $status < 500; } /** * Retrieve the custom .htaccess rules and inject into the .htaccess file. * * @return string */ public function get_custom_rules() { $custom = get_site_option( 'patchstack_firewall_custom_rules', '' ); if ( empty( $custom ) || is_array( $custom ) || $custom == 'Array' ) { $custom = ''; } // Do we have any custom rules to inject? $tmp = ''; if ( $custom != '' ) { $tmp = PHP_EOL . '# CUSTOM PATCHSTACK RULES' . PHP_EOL; $tmp .= $custom . PHP_EOL; $tmp .= '# END CUSTOM PATCHSTACK RULES'; } return $tmp; } /** * Retrieve the status code of the site. * This is done to determine if the .htaccess rules do not trigger an error. * * @return integer */ public function get_site_status_code() { $response = wp_remote_get( get_site_url() ); $http_code = wp_remote_retrieve_response_code( $response ); return $http_code; } /** * Remove all Patchstack rules from the .htaccess file. * * @return void */ public function cleanup_htaccess_file() { require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; $fs = new WP_Filesystem_Direct( '' ); if ( $fs->exists( ABSPATH . '.htaccess' ) ) { $curdata = $fs->get_contents( ABSPATH . '.htaccess' ); $rules = $this->delete_all_between( '# BEGIN Patchstack', '# END Patchstack', $curdata ); $rules = $this->delete_all_between( '# CUSTOM PATCHSTACK RULES', '# END CUSTOM PATCHSTACK RULES', $rules ); $fs->put_contents( ABSPATH . '.htaccess', $rules, FS_CHMOD_FILE ); } } /** * Delete characters between a begin and end string. * * @param string $begin * @param string $end * @param string $string * @return string */ public function delete_all_between( $begin, $end, $string ) { $begin_pos = strpos( $string, $begin ); $end_pos = strpos( $string, $end ); if ( $begin_pos === false || $end_pos === false ) { return $string; } $delete = substr( $string, $begin_pos, ( $end_pos + strlen( $end ) ) - $begin_pos ); return str_replace( $delete, '', $string ); } }