summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-runtime.php')
-rw-r--r--plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-runtime.php794
1 files changed, 794 insertions, 0 deletions
diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-runtime.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-runtime.php
new file mode 100644
index 00000000..19206821
--- /dev/null
+++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-runtime.php
@@ -0,0 +1,794 @@
+<?php
+/**
+ * Runtime for Jetpack Waf
+ *
+ * @package automattic/jetpack-waf
+ */
+
+namespace Automattic\Jetpack\Waf;
+
+require_once __DIR__ . '/functions.php';
+
+// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This class is all about sanitizing input.
+
+/**
+ * The environment variable that defined the WAF running mode.
+ *
+ * @var string JETPACK_WAF_MODE
+ */
+
+/**
+ * Waf_Runtime class
+ */
+class Waf_Runtime {
+
+ /**
+ * Last rule.
+ *
+ * @var string
+ */
+ public $last_rule = '';
+ /**
+ * Matched vars.
+ *
+ * @var array
+ */
+ public $matched_vars = array();
+ /**
+ * Matched var.
+ *
+ * @var string
+ */
+ public $matched_var = '';
+ /**
+ * Matched var names.
+ *
+ * @var array
+ */
+ public $matched_var_names = array();
+ /**
+ * Matched var name.
+ *
+ * @var string
+ */
+ public $matched_var_name = '';
+
+ /**
+ * State.
+ *
+ * @var array
+ */
+ private $state = array();
+ /**
+ * Metadata.
+ *
+ * @var array
+ */
+ private $metadata = array();
+
+ /**
+ * Transforms.
+ *
+ * @var Waf_Transforms[]
+ */
+ private $transforms;
+ /**
+ * Operators.
+ *
+ * @var Waf_Operators[]
+ */
+ private $operators;
+
+ /**
+ * Rules to remove.
+ *
+ * @var array[]
+ */
+ private $rules_to_remove = array(
+ 'id' => array(),
+ 'tag' => array(),
+ );
+
+ /**
+ * Targets to remove.
+ *
+ * @var array[]
+ */
+ private $targets_to_remove = array(
+ 'id' => array(),
+ 'tag' => array(),
+ );
+
+ /**
+ * Constructor method.
+ *
+ * @param Waf_Transforms $transforms Transforms.
+ * @param Waf_Operators $operators Operators.
+ */
+ public function __construct( $transforms, $operators ) {
+ $this->transforms = $transforms;
+ $this->operators = $operators;
+ }
+
+ /**
+ * Rule removed method.
+ *
+ * @param string $id Ids.
+ * @param string[] $tags Tags.
+ */
+ public function rule_removed( $id, $tags ) {
+ if ( isset( $this->rules_to_remove['id'][ $id ] ) ) {
+ return true;
+ }
+ foreach ( $tags as $tag ) {
+ if ( isset( $this->rules_to_remove['tag'][ $tag ] ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Update Targets.
+ *
+ * @param array $targets Targets.
+ * @param string $rule_id Rule id.
+ * @param string[] $rule_tags Rule tags.
+ */
+ public function update_targets( $targets, $rule_id, $rule_tags ) {
+ $updates = array();
+ // look for target updates based on the rule's ID.
+ if ( isset( $this->targets_to_remove['id'][ $rule_id ] ) ) {
+ foreach ( $this->targets_to_remove['id'][ $rule_id ] as $name => $props ) {
+ $updates[] = array( $name, $props );
+ }
+ }
+ // look for target updates based on the rule's tags.
+ foreach ( $rule_tags as $tag ) {
+ if ( isset( $this->targets_to_remove['tag'][ $tag ] ) ) {
+ foreach ( $this->targets_to_remove['tag'][ $tag ] as $name => $props ) {
+ $updates[] = array( $name, $props );
+ }
+ }
+ }
+ // apply any found target updates.
+
+ foreach ( $updates as list( $name, $props ) ) {
+ if ( isset( $targets[ $name ] ) ) {
+ // we only need to remove targets that exist.
+ if ( true === $props ) {
+ // if the entire target is being removed, remove it.
+ unset( $targets[ $name ] );
+ } else {
+ // otherwise just mark single props to ignore.
+ $targets[ $name ]['except'] = array_merge(
+ isset( $targets[ $name ]['except'] ) ? $targets[ $name ]['except'] : array(),
+ $props
+ );
+ }
+ }
+ }
+ return $targets;
+ }
+
+ /**
+ * Return TRUE if at least one of the targets matches the rule.
+ *
+ * @param string[] $transforms One of the transform methods defined in the Jetpack Waf_Transforms class.
+ * @param mixed $targets Targets.
+ * @param string $match_operator Match operator.
+ * @param mixed $match_value Match value.
+ * @param bool $match_not Match not.
+ * @param bool $capture Capture.
+ * @return bool
+ */
+ public function match_targets( $transforms, $targets, $match_operator, $match_value, $match_not, $capture = false ) {
+ $this->matched_vars = array();
+ $this->matched_var_names = array();
+ $this->matched_var = '';
+ $this->matched_var_name = '';
+ $match_found = false;
+
+ // get values.
+ $values = $this->normalize_targets( $targets );
+
+ // apply transforms.
+ foreach ( $transforms as $t ) {
+ foreach ( $values as &$v ) {
+ $v['value'] = $this->transforms->$t( $v['value'] );
+ }
+ }
+
+ // pass each target value to the operator to find any that match.
+ $matched = array();
+ $captures = array();
+ foreach ( $values as $v ) {
+ $match = $this->operators->{$match_operator}( $v['value'], $match_value );
+ $did_match = false !== $match;
+ if ( $match_not !== $did_match ) {
+ // If either:
+ // - rule is negated ("not" flag set) and the target was not matched
+ // - rule not negated and the target was matched
+ // then this is considered a match.
+ $match_found = true;
+ $this->matched_var_names[] = $v['source'];
+ $this->matched_vars[] = $v['value'];
+ $this->matched_var_name = end( $this->matched_var_names );
+ $this->matched_var = end( $this->matched_vars );
+ $matched[] = array( $v, $match );
+ // Set any captured matches into state if the rule has the "capture" flag.
+ if ( $capture ) {
+ $captures = is_array( $match ) ? $match : array( $match );
+ foreach ( array_slice( $captures, 0, 10 ) as $i => $c ) {
+ $this->set_var( "tx.$i", $c );
+ }
+ }
+ }
+ }
+
+ return $match_found;
+ }
+
+ /**
+ * Block.
+ *
+ * @param string $action Action.
+ * @param string $rule_id Rule id.
+ * @param string $reason Block reason.
+ * @param int $status_code Http status code.
+ */
+ public function block( $action, $rule_id, $reason, $status_code = 403 ) {
+ if ( ! $reason ) {
+ $reason = "rule $rule_id";
+ } else {
+ $reason = $this->sanitize_output( $reason );
+ }
+
+ $this->write_blocklog( $rule_id, $reason );
+ error_log( "Jetpack WAF Blocked Request\t$action\t$rule_id\t$status_code\t$reason" );
+ header( "X-JetpackWAF-Blocked: $status_code - rule $rule_id" );
+ if ( defined( 'JETPACK_WAF_MODE' ) && 'normal' === JETPACK_WAF_MODE ) {
+ $protocol = isset( $_SERVER['SERVER_PROTOCOL'] ) ? wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) : 'HTTP';
+ header( $protocol . ' 403 Forbidden', true, $status_code );
+ die( "rule $rule_id - reason $reason" );
+ }
+ }
+
+ /**
+ * Write block logs. We won't write to the file if it exceeds 100 mb.
+ *
+ * @param string $rule_id Rule id.
+ * @param string $reason Block reason.
+ */
+ public function write_blocklog( $rule_id, $reason ) {
+ $log_data = array();
+ $log_data['rule_id'] = $rule_id;
+ $log_data['reason'] = $reason;
+ $log_data['timestamp'] = gmdate( 'Y-m-d H:i:s' );
+
+ if ( defined( 'JETPACK_WAF_SHARE_DATA' ) && JETPACK_WAF_SHARE_DATA ) {
+ $file_path = JETPACK_WAF_DIR . '/waf-blocklog';
+ $file_exists = file_exists( $file_path );
+
+ if ( ! $file_exists || filesize( $file_path ) < ( 100 * 1024 * 1024 ) ) {
+ $fp = fopen( $file_path, 'a+' );
+
+ if ( $fp ) {
+ try {
+ fwrite( $fp, json_encode( $log_data ) . "\n" );
+ } finally {
+ fclose( $fp );
+ }
+ }
+ }
+ }
+
+ $this->write_blocklog_row( $log_data );
+ }
+
+ /**
+ * Write block logs to database.
+ *
+ * @param array $log_data Log data.
+ */
+ private function write_blocklog_row( $log_data ) {
+ $conn = $this->connect_to_wordpress_db();
+
+ if ( ! $conn ) {
+ return;
+ }
+
+ global $table_prefix;
+
+ $statement = $conn->prepare( "INSERT INTO {$table_prefix}jetpack_waf_blocklog(reason,rule_id, timestamp) VALUES (?, ?, ?)" );
+ if ( false !== $statement ) {
+ $statement->bind_param( 'sis', $log_data['reason'], $log_data['rule_id'], $log_data['timestamp'] );
+ $statement->execute();
+
+ if ( $conn->insert_id > 100 ) {
+ $conn->query( "DELETE FROM {$table_prefix}jetpack_waf_blocklog ORDER BY log_id LIMIT 1" );
+ }
+ }
+ }
+
+ /**
+ * Connect to WordPress database.
+ */
+ private function connect_to_wordpress_db() {
+ if ( ! file_exists( JETPACK_WAF_WPCONFIG ) ) {
+ return;
+ }
+
+ require_once JETPACK_WAF_WPCONFIG;
+ $conn = new \mysqli( DB_HOST, DB_USER, DB_PASSWORD, DB_NAME ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__mysqli
+
+ if ( $conn->connect_error ) {
+ error_log( 'Could not connect to the database:' . $conn->connect_error );
+ return null;
+ }
+
+ return $conn;
+ }
+
+ /**
+ * Redirect.
+ *
+ * @param string $rule_id Rule id.
+ * @param string $url Url.
+ */
+ public function redirect( $rule_id, $url ) {
+ error_log( "Jetpack WAF Redirected Request.\tRule:$rule_id\t$url" );
+ header( "Location: $url" );
+ exit;
+ }
+
+ /**
+ * Flag rule for removal.
+ *
+ * @param string $prop Prop.
+ * @param string $value Value.
+ */
+ public function flag_rule_for_removal( $prop, $value ) {
+ if ( 'id' === $prop ) {
+ $this->rules_to_remove['id'][ $value ] = true;
+ } else {
+ $this->rules_to_remove['tag'][ $value ] = true;
+ }
+ }
+
+ /**
+ * Flag target for removal.
+ *
+ * @param string $id_or_tag Id or tag.
+ * @param string $id_or_tag_value Id or tag value.
+ * @param string $name Name.
+ * @param string $prop Prop.
+ */
+ public function flag_target_for_removal( $id_or_tag, $id_or_tag_value, $name, $prop = null ) {
+ if ( null === $prop ) {
+ $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ] = true;
+ } else {
+ if (
+ ! isset( $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ] )
+ // if the entire target is already being removed then it would be redundant to remove a single property.
+ || true !== $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ]
+ ) {
+ $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ][] = $prop;
+ }
+ }
+ }
+
+ /**
+ * Get variable value.
+ *
+ * @param string $key Key.
+ */
+ public function get_var( $key ) {
+ return isset( $this->state[ $key ] )
+ ? $this->state[ $key ]
+ : '';
+ }
+
+ /**
+ * Set variable value.
+ *
+ * @param string $key Key.
+ * @param string $value Value.
+ */
+ public function set_var( $key, $value ) {
+ $this->state[ $key ] = $value;
+ }
+
+ /**
+ * Increment variable.
+ *
+ * @param string $key Key.
+ * @param mixed $value Value.
+ */
+ public function inc_var( $key, $value ) {
+ if ( ! isset( $this->state[ $key ] ) ) {
+ $this->state[ $key ] = 0;
+ }
+ $this->state[ $key ] += floatval( $value );
+ }
+
+ /**
+ * Decrement variable.
+ *
+ * @param string $key Key.
+ * @param mixed $value Value.
+ */
+ public function dec_var( $key, $value ) {
+ if ( ! isset( $this->state[ $key ] ) ) {
+ $this->state[ $key ] = 0;
+ }
+ $this->state[ $key ] -= floatval( $value );
+ }
+
+ /**
+ * Unset variable.
+ *
+ * @param string $key Key.
+ */
+ public function unset_var( $key ) {
+ unset( $this->state[ $key ] );
+ }
+
+ /**
+ * Meta.
+ *
+ * @param string $key Key.
+ * @param string $prop Prop.
+ */
+ public function meta( $key, $prop = false ) {
+ if ( ! isset( $this->metadata[ $key ] ) ) {
+ $value = null;
+ switch ( $key ) {
+ case 'headers':
+ $value = array();
+ foreach ( $_SERVER as $k => $v ) {
+ $k = strtolower( $k );
+ if ( 'http_' === substr( $k, 0, 5 ) ) {
+ $value[ $this->normalize_header_name( substr( $k, 5 ) ) ] = $v;
+ } elseif ( 'content_type' === $k ) {
+ $value['content-type'] = $v;
+ } elseif ( 'content_length' === $k ) {
+ $value['content-length'] = $v;
+ }
+ }
+ $value['content-type'] = ( ! isset( $value['content-type'] ) || '' === $value['content-type'] )
+ // default Content-Type per RFC 7231 section 3.1.5.5.
+ ? 'application/octet-stream'
+ : $value['content-type'];
+ $value['content-length'] = ( isset( $value['content-length'] ) && '' !== $value['content-length'] )
+ ? $value['content-length']
+ // if the content-length header is missing, default it to zero.
+ : '0';
+ break;
+ case 'remote_addr':
+ $value = '';
+ if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
+ $value = wp_unslash( $_SERVER['HTTP_CLIENT_IP'] );
+ } elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
+ $value = wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] );
+ } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
+ $value = wp_unslash( $_SERVER['REMOTE_ADDR'] );
+ }
+ break;
+ case 'request_method':
+ $value = empty( $_SERVER['REQUEST_METHOD'] )
+ ? 'GET'
+ : wp_unslash( $_SERVER['REQUEST_METHOD'] );
+ break;
+ case 'request_protocol':
+ $value = empty( $_SERVER['SERVER_PROTOCOL'] )
+ ? ( empty( $_SERVER['HTTPS'] ) ? 'HTTP' : 'HTTPS' )
+ : wp_unslash( $_SERVER['SERVER_PROTOCOL'] );
+ break;
+ case 'request_uri':
+ $value = isset( $_SERVER['REQUEST_URI'] )
+ ? wp_unslash( $_SERVER['REQUEST_URI'] )
+ : '';
+ break;
+ case 'request_uri_raw':
+ $value = ( isset( $_SERVER['https'] ) ? 'https://' : 'http://' ) . ( isset( $_SERVER['SERVER_NAME'] ) ? wp_unslash( $_SERVER['SERVER_NAME'] ) : '' ) . $this->meta( 'request_uri' );
+ break;
+ case 'request_filename':
+ $value = strtok(
+ isset( $_SERVER['REQUEST_URI'] )
+ ? wp_unslash( $_SERVER['REQUEST_URI'] )
+ : '',
+ '?'
+ );
+ break;
+ case 'request_line':
+ $value = sprintf(
+ '%s %s %s',
+ $this->meta( 'request_method' ),
+ $this->meta( 'request_uri' ),
+ $this->meta( 'request_protocol' )
+ );
+ break;
+ case 'request_basename':
+ $value = basename( $this->meta( 'request_filename' ) );
+ break;
+ case 'request_body':
+ $value = file_get_contents( 'php://input' );
+ break;
+ case 'query_string':
+ $value = isset( $_SERVER['QUERY_STRING'] ) ? wp_unslash( $_SERVER['QUERY_STRING'] ) : '';
+ }
+ $this->metadata[ $key ] = $value;
+ }
+
+ return false === $prop
+ ? $this->metadata[ $key ]
+ : ( isset( $this->metadata[ $key ][ $prop ] ) ? $this->metadata[ $key ][ $prop ] : '' );
+ }
+
+ /**
+ * State values.
+ *
+ * @param string $prefix Prefix.
+ */
+ private function state_values( $prefix ) {
+ $output = array();
+ $len = strlen( $prefix );
+ foreach ( $this->state as $k => $v ) {
+ if ( 0 === stripos( $k, $prefix ) ) {
+ $output[ substr( $k, $len ) ] = $v;
+ }
+ }
+
+ return $output;
+ }
+
+ /**
+ * Change a string to all lowercase and replace spaces and underscores with dashes.
+ *
+ * @param string $name Name.
+ * @return string
+ */
+ public function normalize_header_name( $name ) {
+ return str_replace( array( ' ', '_' ), '-', strtolower( $name ) );
+ }
+
+ /**
+ * Normalize targets.
+ *
+ * @param array $targets Targets.
+ */
+ public function normalize_targets( $targets ) {
+ $return = array();
+ foreach ( $targets as $k => $v ) {
+ $count_only = isset( $v['count'] );
+ $only = isset( $v['only'] ) ? $v['only'] : array();
+ $except = isset( $v['except'] ) ? $v['except'] : array();
+ $_k = strtolower( $k );
+ switch ( $_k ) {
+ case 'request_headers':
+ $only = array_map(
+ function ( $t ) {
+ return '/' === $t[0] ? $t : $this->normalize_header_name( $t );
+ },
+ $only
+ );
+ $except = array_map(
+ function ( $t ) {
+ return '/' === $t[0] ? $t : $this->normalize_header_name( $t );
+ },
+ $except
+ );
+ $this->normalize_array_target( $this->meta( 'headers' ), $only, $except, $k, $return, $count_only );
+ continue 2;
+ case 'request_headers_names':
+ $this->normalize_array_target( array_keys( $this->meta( 'headers' ) ), array(), array(), $k, $return, $count_only );
+ continue 2;
+ case 'request_method':
+ case 'request_protocol':
+ case 'request_uri':
+ case 'request_uri_raw':
+ case 'request_filename':
+ case 'remote_addr':
+ case 'request_basename':
+ case 'request_body':
+ case 'query_string':
+ case 'request_line':
+ $v = $this->meta( $_k );
+ break;
+ case 'tx':
+ case 'ip':
+ $this->normalize_array_target( $this->state_values( "$k." ), $only, $except, $k, $return, $count_only );
+ continue 2;
+ case 'request_cookies':
+ $this->normalize_array_target( $_COOKIE, $only, $except, $k, $return, $count_only );
+ continue 2;
+ case 'request_cookies_names':
+ $this->normalize_array_target( array_keys( $_COOKIE ), array(), array(), $k, $return, $count_only );
+ continue 2;
+ case 'args':
+ $this->normalize_array_target( $_REQUEST, $only, $except, $k, $return, $count_only );
+ continue 2;
+ case 'args_names':
+ $this->normalize_array_target( array_keys( $_REQUEST ), array(), array(), $k, $return, $count_only );
+ continue 2;
+ case 'args_get':
+ $this->normalize_array_target( $_GET, $only, $except, $k, $return, $count_only );
+ continue 2;
+ case 'args_get_names':
+ $this->normalize_array_target( array_keys( $_GET ), array(), array(), $k, $return, $count_only );
+ continue 2;
+ case 'args_post':
+ $this->normalize_array_target( $_POST, $only, $except, $k, $return, $count_only );
+ continue 2;
+ case 'args_post_names':
+ $this->normalize_array_target( array_keys( $_POST ), array(), array(), $k, $return, $count_only );
+ continue 2;
+ case 'files':
+ $names = array_map(
+ function ( $f ) {
+ return $f['name'];
+ },
+ $_FILES
+ );
+ $this->normalize_array_target( $names, $only, $except, $k, $return, $count_only );
+ continue 2;
+ case 'files_names':
+ $this->normalize_array_target( array_keys( $_FILES ), $only, $except, $k, $return, $count_only );
+ continue 2;
+ default:
+ var_dump( 'Unknown target', $k, $v );
+ exit;
+ }
+ $return[] = array(
+ 'name' => $k,
+ 'value' => $v,
+ 'source' => $k,
+ );
+ }
+
+ return $return;
+ }
+
+ /**
+ * Verifies is ip from request is in an array.
+ *
+ * @param array $array Array to verify ip against.
+ */
+ public function is_ip_in_array( $array ) {
+ $request = new Waf_Request();
+
+ $real_ip = $request->get_real_user_ip_address();
+
+ return in_array( $real_ip, $array, true );
+ }
+
+ /**
+ * Normalize array target.
+ *
+ * @param array $source Source.
+ * @param array $only Only.
+ * @param array $excl Excl.
+ * @param string $name Name.
+ * @param array $results Results.
+ * @param bool $count_only Count only.
+ */
+ private function normalize_array_target( $source, $only, $excl, $name, &$results, $count_only ) {
+ $output = array();
+ $has_only = isset( $only[0] );
+ $has_excl = isset( $excl[0] );
+
+ if ( $has_only ) {
+ foreach ( $only as $prop ) {
+ if ( isset( $source[ $prop ] ) && $this->key_matches( $prop, $only ) ) {
+ $output[ $prop ] = $source[ $prop ];
+ }
+ }
+ } else {
+ $output = $source;
+ }
+
+ if ( $has_excl ) {
+ foreach ( array_keys( $output ) as $k ) {
+ if ( $this->key_matches( $k, $excl ) ) {
+ unset( $output[ $k ] );
+ }
+ }
+ }
+
+ if ( $count_only ) {
+ $results[] = array(
+ 'name' => $name,
+ 'value' => count( $output ),
+ 'source' => '&' . $name,
+ );
+ } else {
+ foreach ( $output as $tk => $tv ) {
+ if ( is_array( $tv ) ) {
+ // flatten it so we get all the values considered
+ $flat_values = $this->array_flatten( $tv );
+ foreach ( $flat_values as $fv ) {
+ $results[] = array(
+ // force names to strings
+ // we don't care about the nested keys here, just the overall variable name
+ 'name' => '' . $tk,
+ 'value' => $fv,
+ 'source' => "$name:$tk",
+ );
+ }
+ } else {
+ $results[] = array(
+ // force names to strings
+ 'name' => '' . $tk,
+ 'value' => $tv,
+ 'source' => "$name:$tk",
+ );
+ }
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Basic array flatten with array_merge; no-op on non-array targets.
+ *
+ * @param array $source Array to flatten.
+ * @return array The flattened array.
+ */
+ private function array_flatten( $source ) {
+ if ( ! is_array( $source ) ) {
+ return $source;
+ }
+
+ $return = array();
+
+ foreach ( $source as $v ) {
+ if ( is_array( $v ) ) {
+ $return = array_merge( $return, $this->array_flatten( $v ) );
+ } else {
+ $return[] = $v;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Key matches.
+ *
+ * @param string $input Input.
+ * @param array $patterns Patterns.
+ */
+ private function key_matches( $input, $patterns ) {
+ foreach ( $patterns as $p ) {
+ if ( '/' === $p[0] ) {
+ if ( 1 === preg_match( $p, $input ) ) {
+ return true;
+ }
+ } else {
+ if ( 0 === strcasecmp( $p, $input ) ) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Sanitize output generated from the request that was blocked.
+ *
+ * @param string $output Output to sanitize.
+ */
+ public function sanitize_output( $output ) {
+ $url_decoded_output = rawurldecode( $output );
+ $html_entities_output = htmlentities( $url_decoded_output, ENT_QUOTES, 'UTF-8' );
+ // @phpcs:disable Squiz.Strings.DoubleQuoteUsage.NotRequired
+ $escapers = array( "\\", "/", "\"", "\n", "\r", "\t", "\x08", "\x0c" );
+ $replacements = array( "\\\\", "\\/", "\\\"", "\\n", "\\r", "\\t", "\\f", "\\b" );
+ // @phpcs:enable Squiz.Strings.DoubleQuoteUsage.NotRequired
+
+ return( str_replace( $escapers, $replacements, $html_entities_output ) );
+ }
+}