summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src')
-rw-r--r--plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-cli.php165
-rw-r--r--plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-constants.php27
-rw-r--r--plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-endpoints.php111
-rw-r--r--plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-initializer.php39
-rw-r--r--plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-operators.php286
-rw-r--r--plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-request.php106
-rw-r--r--plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-runner.php469
-rw-r--r--plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-runtime.php794
-rw-r--r--plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-standalone-bootstrap.php160
-rw-r--r--plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-transforms.php342
-rw-r--r--plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/functions.php27
11 files changed, 2526 insertions, 0 deletions
diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-cli.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-cli.php
new file mode 100644
index 00000000..5ce887dc
--- /dev/null
+++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-cli.php
@@ -0,0 +1,165 @@
+<?php
+/**
+ * CLI handler for Jetpack Waf.
+ *
+ * @package automattic/jetpack-waf
+ */
+
+namespace Automattic\Jetpack\Waf;
+
+use \WP_CLI;
+use \WP_CLI_Command;
+
+/**
+ * Just a few sample commands to learn how WP-CLI works
+ */
+class CLI extends WP_CLI_Command {
+ /**
+ * View or set the current mode of the WAF.
+ * ## OPTIONS
+ *
+ * [<mode>]
+ * : The new mode to be set.
+ * ---
+ * options:
+ * - silent
+ * - normal
+ * ---
+ *
+ * @param array $args Arguments passed to CLI.
+ * @return void|null
+ * @throws WP_CLI\ExitException If there is an error switching the mode.
+ */
+ public function mode( $args ) {
+ if ( count( $args ) > 1 ) {
+
+ return WP_CLI::error( __( 'Only one mode may be specified.', 'jetpack-waf' ) );
+ }
+ if ( count( $args ) === 1 ) {
+ if ( ! Waf_Runner::is_allowed_mode( $args[0] ) ) {
+
+ return WP_CLI::error(
+ sprintf(
+ /* translators: %1$s is the mode that was actually found. Also note that the expected "silent" and "normal" are hard-coded strings and must therefore stay the same in any translation. */
+ __( 'Invalid mode: %1$s. Expected "silent" or "normal".', 'jetpack-waf' ),
+ $args[0]
+ )
+ );
+ }
+
+ update_option( Waf_Runner::MODE_OPTION_NAME, $args[0] );
+
+ try {
+ ( new Waf_Standalone_Bootstrap() )->generate();
+ } catch ( \Exception $e ) {
+ WP_CLI::warning(
+ sprintf(
+ /* translators: %1$s is the unexpected error message. */
+ __( 'Unable to generate waf bootstrap - standalone mode may not work properly: %1$s', 'jetpack-waf' ),
+ $e->getMessage()
+ )
+ );
+ }
+
+ return WP_CLI::success(
+ sprintf(
+ /* translators: %1$s is the name of the mode that was just switched to. */
+ __( 'Jetpack WAF mode switched to "%1$s".', 'jetpack-waf' ),
+ get_option( Waf_Runner::MODE_OPTION_NAME )
+ )
+ );
+ }
+ WP_CLI::line(
+ sprintf(
+ /* translators: %1$s is the name of the mode that the waf is currently running in. */
+ __( 'Jetpack WAF is running in "%1$s" mode.', 'jetpack-waf' ),
+ get_option( Waf_Runner::MODE_OPTION_NAME )
+ )
+ );
+ }
+
+ /**
+ * Setup the WAF to run.
+ * ## OPTIONS
+ *
+ * [<mode>]
+ * : The new mode to be set.
+ * ---
+ * options:
+ * - silent
+ * - normal
+ * ---
+ *
+ * @param array $args Arguments passed to CLI.
+ * @return void|null
+ * @throws WP_CLI\ExitException If there is an error switching the mode.
+ */
+ public function setup( $args ) {
+ // Let is_allowed_mode know we are running from the CLI
+ define( 'WAF_CLI_MODE', $args[0] );
+
+ // Set the mode and generate the bootstrap
+ $this->mode( array( $args[0] ) );
+
+ try {
+ // Add relevant options and generate the rules.php file
+ Waf_Runner::activate();
+ } catch ( \Exception $e ) {
+
+ return WP_CLI::error(
+ sprintf(
+ /* translators: %1$s is the unexpected error message. */
+ __( 'Jetpack WAF rules file failed to generate: %1$s', 'jetpack-waf' ),
+ $e->getMessage()
+ )
+ );
+ }
+
+ return WP_CLI::success( __( 'Jetpack WAF has successfully been setup.', 'jetpack-waf' ) );
+ }
+
+ /**
+ * Delete the WAF options.
+ *
+ * @return void|null
+ * @throws WP_CLI\ExitException If deactivating has failures.
+ */
+ public function teardown() {
+ try {
+ Waf_Runner::deactivate();
+ } catch ( \Exception $e ) {
+ WP_CLI::error( __( 'Jetpack WAF failed to fully deactivate.', 'jetpack-waf' ) );
+ }
+
+ return WP_CLI::success( __( 'Jetpack WAF has been deactivated.', 'jetpack-waf' ) );
+ }
+
+ /**
+ * Generate the rules.php file with latest rules for the WAF.
+ *
+ * @return void|null
+ * @throws WP_CLI\ExitException If there is an error switching the mode.
+ */
+ public function generate_rules() {
+ try {
+ Waf_Runner::generate_rules();
+ } catch ( \Exception $e ) {
+
+ return WP_CLI::error(
+ sprintf(
+ /* translators: %1$s is the unexpected error message. */
+ __( 'Jetpack WAF rules file failed to generate: %1$s', 'jetpack-waf' ),
+ $e->getMessage()
+ )
+ );
+ }
+
+ return WP_CLI::success(
+ sprintf(
+ /* translators: %1$s is the name of the mode that was just switched to. */
+ __( 'Jetpack WAF rules successfully created to: "%1$s".', 'jetpack-waf' ),
+ Waf_Runner::RULES_FILE
+ )
+ );
+ }
+}
diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-constants.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-constants.php
new file mode 100644
index 00000000..0cfa3446
--- /dev/null
+++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-constants.php
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Class use to define the constants used by the WAF
+ *
+ * @package automattic/jetpack-waf
+ */
+
+namespace Automattic\Jetpack\Waf;
+
+/**
+ * Defines our constants.
+ */
+class Waf_Constants {
+ /**
+ * Initializes the constants required for generating the bootstrap, if they have not been initialized yet.
+ *
+ * @return void
+ */
+ public static function initialize_constants() {
+ if ( ! defined( 'JETPACK_WAF_DIR' ) ) {
+ define( 'JETPACK_WAF_DIR', trailingslashit( WP_CONTENT_DIR ) . 'jetpack-waf' );
+ }
+ if ( ! defined( 'JETPACK_WAF_WPCONFIG' ) ) {
+ define( 'JETPACK_WAF_WPCONFIG', trailingslashit( WP_CONTENT_DIR ) . '../wp-config.php' );
+ }
+ }
+}
diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-endpoints.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-endpoints.php
new file mode 100644
index 00000000..2aff96a5
--- /dev/null
+++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-endpoints.php
@@ -0,0 +1,111 @@
+<?php
+/**
+ * Class use to register REST API endpoints used by the WAF
+ *
+ * @package automattic/jetpack-waf
+ */
+
+namespace Automattic\Jetpack\Waf;
+
+use Automattic\Jetpack\Connection\REST_Connector;
+use WP_REST_Server;
+
+/**
+ * Defines our endponts.
+ */
+class Waf_Endpoints {
+ /**
+ * Get Bootstrap File Path
+ *
+ * @return string The path to the Jetpack Firewall's bootstrap.php file.
+ */
+ private static function get_bootstrap_file_path() {
+ $bootstrap = new Waf_Standalone_Bootstrap();
+ return $bootstrap->get_bootstrap_file_path();
+ }
+
+ /**
+ * Has Rules Access
+ *
+ * @return bool True when the current site has access to latest firewall rules.
+ */
+ private static function has_rules_access() {
+ // any site with Jetpack Scan can download new WAF rules
+ return \Jetpack_Plan::supports( 'scan' );
+ }
+
+ /**
+ * Register REST API endpoints.
+ */
+ public static function register_endpoints() {
+ register_rest_route(
+ 'jetpack/v4',
+ '/waf',
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::waf',
+ 'permission_callback' => __CLASS__ . '::waf_permissions_callback',
+ )
+ );
+ register_rest_route(
+ 'jetpack/v4',
+ '/waf/update-rules',
+ array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::update_rules',
+ 'permission_callback' => __CLASS__ . '::waf_permissions_callback',
+ )
+ );
+ }
+
+ /**
+ * Update rules endpoint
+ */
+ public static function update_rules() {
+ $success = true;
+ $message = 'Rules updated succesfully';
+
+ try {
+ Waf_Runner::generate_rules();
+ } catch ( Exception $e ) {
+ $success = false;
+ $message = $e->getMessage();
+ }
+
+ return rest_ensure_response(
+ array(
+ 'success' => $success,
+ 'message' => $message,
+ )
+ );
+ }
+
+ /**
+ * WAF Endpoint
+ */
+ public static function waf() {
+ return rest_ensure_response(
+ array(
+ 'bootstrapPath' => self::get_bootstrap_file_path(),
+ 'hasRulesAccess' => self::has_rules_access(),
+ )
+ );
+ }
+
+ /**
+ * WAF Endpoint Permissions Callback
+ *
+ * @return bool|WP_Error True if user can view the Jetpack admin page.
+ */
+ public static function waf_permissions_callback() {
+ if ( current_user_can( 'jetpack_manage_modules' ) ) {
+ return true;
+ }
+
+ return new WP_Error(
+ 'invalid_user_permission_manage_modules',
+ REST_Connector::get_user_permissions_error_msg(),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+}
diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-initializer.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-initializer.php
new file mode 100644
index 00000000..662a2ca2
--- /dev/null
+++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-initializer.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Class use to initialize the WAF module.
+ *
+ * @package automattic/jetpack-waf
+ */
+
+namespace Automattic\Jetpack\Waf;
+
+/**
+ * Initializes the module
+ */
+class Waf_Initializer {
+ /**
+ * Initializes the configurations needed for the waf module.
+ *
+ * @return void
+ */
+ public static function init() {
+ add_action( 'jetpack_activate_module_waf', __CLASS__ . '::on_activation' );
+ add_action( 'jetpack_deactivate_module_waf', __CLASS__ . '::on_deactivation' );
+ }
+
+ /**
+ * On module activation set up waf mode
+ */
+ public static function on_activation() {
+ update_option( Waf_Runner::MODE_OPTION_NAME, 'normal' );
+ Waf_Runner::activate();
+ ( new Waf_Standalone_Bootstrap() )->generate();
+ }
+
+ /**
+ * On module deactivation, unset waf mode
+ */
+ public static function on_deactivation() {
+ Waf_Runner::deactivate();
+ }
+}
diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-operators.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-operators.php
new file mode 100644
index 00000000..503fe797
--- /dev/null
+++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-operators.php
@@ -0,0 +1,286 @@
+<?php
+/**
+ * Rule compiler for Jetpack Waf.
+ *
+ * @package automattic/jetpack-waf
+ */
+
+namespace Automattic\Jetpack\Waf;
+
+/**
+ * Waf_Operators class
+ */
+class Waf_Operators {
+ /**
+ * Returns true if the test string is found at the beginning of the input.
+ *
+ * @param string $input Input.
+ * @param string $test Test.
+ * @return string|false
+ */
+ public function begins_with( $input, $test ) {
+ if ( '' === $input && '' === $test ) {
+ return '';
+ }
+
+ return substr( $input, 0, strlen( $test ) ) === $test
+ ? $test
+ : false;
+ }
+
+ /**
+ * Returns true if the test string is found anywhere in the input.
+ *
+ * @param string $input Input.
+ * @param string $test Test.
+ * @return string|false
+ */
+ public function contains( $input, $test ) {
+ if ( empty( $input ) || empty( $test ) ) {
+ return false;
+ }
+
+ return strpos( $input, $test ) !== false
+ ? $test
+ : false;
+ }
+
+ /**
+ * Returns true if the test string with word boundaries is found anywhere in the input.
+ *
+ * @param string $input Input.
+ * @param string $test Test.
+ * @return string|false
+ */
+ public function contains_word( $input, $test ) {
+ return ( $input === $test || 1 === preg_match( '/\b' . preg_quote( $test, '/' ) . '\b/Ds', $input ) )
+ ? $test
+ : false;
+ }
+
+ /**
+ * Returns true if the test string is found at the end of the input.
+ *
+ * @param string $input Input.
+ * @param string $test Test.
+ * @return string|false
+ */
+ public function ends_with( $input, $test ) {
+ return ( '' === $test || substr( $input, -1 * strlen( $test ) ) === $test )
+ ? $test
+ : false;
+ }
+
+ /**
+ * Returns true if the input value is equal to the test value.
+ * If either value cannot be converted to an int it will be treated as 0.
+ *
+ * @param mixed $input Input.
+ * @param mixed $test Test.
+ * @return int|false
+ */
+ public function eq( $input, $test ) {
+ return intval( $input ) === intval( $test )
+ ? $input
+ : false;
+ }
+
+ /**
+ * Returns true if the input value is greater than or equal to the test value.
+ * If either value cannot be converted to an int it will be treated as 0.
+ *
+ * @param mixed $input Input.
+ * @param mixed $test Test.
+ * @return int|false
+ */
+ public function ge( $input, $test ) {
+ return intval( $input ) >= intval( $test )
+ ? $input
+ : false;
+ }
+
+ /**
+ * Returns true if the input value is greater than the test value.
+ * If either value cannot be converted to an int it will be treated as 0.
+ *
+ * @param mixed $input Input.
+ * @param mixed $test Test.
+ * @return int|false
+ */
+ public function gt( $input, $test ) {
+ return intval( $input ) > intval( $test )
+ ? $input
+ : false;
+ }
+
+ /**
+ * Returns true if the input value is less than or equal to the test value.
+ * If either value cannot be converted to an int it will be treated as 0.
+ *
+ * @param mixed $input Input.
+ * @param mixed $test Test.
+ * @return int|false
+ */
+ public function le( $input, $test ) {
+ return intval( $input ) <= intval( $test )
+ ? $input
+ : false;
+ }
+
+ /**
+ * Returns true if the input value is less than the test value.
+ * If either value cannot be converted to an int it will be treated as 0.
+ *
+ * @param mixed $input Input.
+ * @param mixed $test Test.
+ * @return int|false
+ */
+ public function lt( $input, $test ) {
+ return intval( $input ) < intval( $test )
+ ? $input
+ : false;
+ }
+
+ /**
+ * Returns false.
+ *
+ * @return false
+ */
+ public function no_match() {
+ return false;
+ }
+
+ /**
+ * Uses a multi-string matching algorithm to search through $input for a number of given $words.
+ *
+ * @param string $input Input.
+ * @param string[] $words \AhoCorasick\MultiStringMatcher $matcher.
+ * @return string[]|false Returns the words that were found in $input, or FALSE if no words were found.
+ */
+ public function pm( $input, $words ) {
+ $results = $this->get_multi_string_matcher( $words )->searchIn( $input );
+
+ return isset( $results[0] )
+ ? array_map(
+ function ( $r ) {
+ return $r[1]; },
+ $results
+ )
+ : false;
+ }
+
+ /**
+ * The last-used pattern-matching algorithm.
+ *
+ * @var array
+ */
+ private $last_multi_string_matcher = array( null, null );
+
+ /**
+ * Creates a matcher that uses the Aho-Corasick algorithm to efficiently find a number of words in an input string.
+ * Caches the last-used matcher so that the same word list doesn't have to be compiled multiple times.
+ *
+ * @param string[] $words Words.
+ * @return \AhoCorasick\MultiStringMatcher
+ */
+ private function get_multi_string_matcher( $words ) {
+ // only create a new matcher entity if we don't have one already for this word list.
+ if ( $this->last_multi_string_matcher[0] !== $words ) {
+ $this->last_multi_string_matcher = array( $words, new \AhoCorasick\MultiStringMatcher( $words ) );
+ }
+
+ return $this->last_multi_string_matcher[1];
+ }
+
+ /**
+ * Performs a regular expression match on the input subject using the given pattern.
+ * Returns false if the pattern does not match, or the substring(s) of the input
+ * that were matched by the pattern.
+ *
+ * @param string $subject Subject.
+ * @param string $pattern Pattern.
+ * @return string[]|false
+ */
+ public function rx( $subject, $pattern ) {
+ $matched = preg_match( $pattern, $subject, $matches );
+ return 1 === $matched
+ ? $matches
+ : false;
+ }
+
+ /**
+ * Returns true if the given input string matches the test string.
+ *
+ * @param string $input Input.
+ * @param string $test Test.
+ * @return string|false
+ */
+ public function streq( $input, $test ) {
+ return $input === $test
+ ? $test
+ : false;
+ }
+
+ /**
+ * Returns true.
+ *
+ * @param string $input Input.
+ * @return bool
+ */
+ public function unconditional_match( $input ) {
+ return $input;
+ }
+
+ /**
+ * Checks to see if the input string only contains characters within the given byte range
+ *
+ * @param string $input Input.
+ * @param array $valid_range Valid range.
+ * @return string
+ */
+ public function validate_byte_range( $input, $valid_range ) {
+ if ( '' === $input ) {
+ // an empty string is considered "valid".
+ return false;
+ }
+ $i = 0;
+ while ( isset( $input[ $i ] ) ) {
+ $n = ord( $input[ $i ] );
+ if ( $n < $valid_range['min'] || $n > $valid_range['max'] ) {
+ return $input[ $i ];
+ }
+ $valid = false;
+ foreach ( $valid_range['range'] as $b ) {
+ if ( $n === $b || is_array( $b ) && $n >= $b[0] && $n <= $b[1] ) {
+ $valid = true;
+ break;
+ }
+ }
+ if ( ! $valid ) {
+ return $input[ $i ];
+ }
+ $i++;
+ }
+
+ // if there weren't any invalid bytes, return false.
+ return false;
+ }
+
+ /**
+ * Returns true if the input value is found anywhere inside the test value
+ * (i.e. the inverse of @contains)
+ *
+ * @param mixed $input Input.
+ * @param mixed $test Test.
+ * @return string|false
+ */
+ public function within( $input, $test ) {
+ if ( '' === $input || '' === $test ) {
+ return false;
+ }
+
+ return stripos( $test, $input ) !== false
+ ? $input
+ : false;
+ }
+}
diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-request.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-request.php
new file mode 100644
index 00000000..279fd84e
--- /dev/null
+++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-request.php
@@ -0,0 +1,106 @@
+<?php
+/**
+ * HTTP request representation specific for the WAF.
+ *
+ * @package automattic/jetpack-waf
+ */
+
+namespace Automattic\Jetpack\Waf;
+
+require_once __DIR__ . '/functions.php';
+
+/**
+ * Request representation.
+ */
+class Waf_Request {
+
+ /**
+ * Trusted proxies.
+ *
+ * @var array List of trusted proxy IP addresses.
+ */
+ private $trusted_proxies = array();
+
+ /**
+ * Trusted headers.
+ *
+ * @var array List of headers to trust from the trusted proxies.
+ */
+ private $trusted_headers = array();
+
+ /**
+ * Sets the list of IP addresses for the proxies to trust. Trusted headers will only be accepted as the
+ * user IP address from these IP adresses.
+ *
+ * Popular choices include:
+ * - 192.168.0.1
+ * - 10.0.0.1
+ *
+ * @param array $proxies List of proxy IP addresses.
+ * @return void
+ */
+ public function set_trusted_proxies( $proxies ) {
+ $this->trusted_proxies = (array) $proxies;
+ }
+
+ /**
+ * Sets the list of headers to be trusted from the proxies. These headers will only be taken into account
+ * if the request comes from a trusted proxy as configured with set_trusted_proxies().
+ *
+ * Popular choices include:
+ * - HTTP_CLIENT_IP
+ * - HTTP_X_FORWARDED_FOR
+ * - HTTP_X_FORWARDED
+ * - HTTP_X_CLUSTER_CLIENT_IP
+ * - HTTP_FORWARDED_FOR
+ * - HTTP_FORWARDED
+ *
+ * @param array $headers List of HTTP header strings.
+ * @return void
+ */
+ public function set_trusted_headers( $headers ) {
+ $this->trusted_headers = (array) $headers;
+ }
+
+ /**
+ * Determines the users real IP address based on the settings passed to set_trusted_proxies() and
+ * set_trusted_headers() before. On CLI, this will be null.
+ *
+ * @return string|null
+ */
+ public function get_real_user_ip_address() {
+ $remote_addr = ! empty( $_SERVER['REMOTE_ADDR'] ) ? wp_unslash( $_SERVER['REMOTE_ADDR'] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+
+ if ( in_array( $remote_addr, $this->trusted_proxies, true ) ) {
+ $ip_by_header = $this->get_ip_by_header( array_merge( $this->trusted_headers, array( 'REMOTE_ADDR' ) ) );
+ if ( ! empty( $ip_by_header ) ) {
+ return $ip_by_header;
+ }
+ }
+
+ return $remote_addr;
+ }
+
+ /**
+ * Iterates through a given list of HTTP headers and attempts to get the IP address from the header that
+ * a proxy sends along. Make sure you trust the IP address before calling this method.
+ *
+ * @param array $headers The list of headers to check.
+ * @return string|null
+ */
+ private function get_ip_by_header( $headers ) {
+ foreach ( $headers as $key ) {
+ if ( isset( $_SERVER[ $key ] ) ) {
+ foreach ( explode( ',', wp_unslash( $_SERVER[ $key ] ) ) as $ip ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- filter_var is applied below.
+ $ip = trim( $ip );
+
+ if ( filter_var( $ip, FILTER_VALIDATE_IP ) !== false ) {
+ return $ip;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-runner.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-runner.php
new file mode 100644
index 00000000..389f8b2a
--- /dev/null
+++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-runner.php
@@ -0,0 +1,469 @@
+<?php
+/**
+ * Entrypoint for actually executing the WAF.
+ *
+ * @package automattic/jetpack-waf
+ */
+
+namespace Automattic\Jetpack\Waf;
+
+use Automattic\Jetpack\Connection\Client;
+use Automattic\Jetpack\Modules;
+use Jetpack_Options;
+
+/**
+ * Executes the WAF.
+ */
+class Waf_Runner {
+
+ const WAF_RULES_VERSION = '1.0.0';
+ const MODE_OPTION_NAME = 'jetpack_waf_mode';
+ const IP_LISTS_ENABLED_OPTION_NAME = 'jetpack_waf_ip_list';
+ const IP_ALLOW_LIST_OPTION_NAME = 'jetpack_waf_ip_allow_list';
+ const IP_BLOCK_LIST_OPTION_NAME = 'jetpack_waf_ip_block_list';
+ const RULES_FILE = __DIR__ . '/../rules/rules.php';
+ const ALLOW_IP_FILE = __DIR__ . '/../rules/allow-ip.php';
+ const BLOCK_IP_FILE = __DIR__ . '/../rules/block-ip.php';
+ const VERSION_OPTION_NAME = 'jetpack_waf_rules_version';
+ const RULE_LAST_UPDATED_OPTION_NAME = 'jetpack_waf_last_updated_timestamp';
+ const SHARE_DATA_OPTION_NAME = 'jetpack_waf_share_data';
+
+ /**
+ * Set the mode definition if it has not been set.
+ *
+ * @return void
+ */
+ public static function define_mode() {
+ if ( ! defined( 'JETPACK_WAF_MODE' ) ) {
+ $mode_option = get_option( self::MODE_OPTION_NAME );
+ define( 'JETPACK_WAF_MODE', $mode_option );
+ }
+ }
+
+ /**
+ * Set the mode definition if it has not been set.
+ *
+ * @return void
+ */
+ public static function define_share_data() {
+ if ( ! defined( 'JETPACK_WAF_SHARE_DATA' ) ) {
+ $share_data_option = get_option( self::SHARE_DATA_OPTION_NAME, false );
+ define( 'JETPACK_WAF_SHARE_DATA', $share_data_option );
+ }
+ }
+
+ /**
+ * Did the WAF run yet or not?
+ *
+ * @return bool
+ */
+ public static function did_run() {
+ return defined( 'JETPACK_WAF_RUN' );
+ }
+
+ /**
+ * Determines if the passed $option is one of the allowed WAF operation modes.
+ *
+ * @param string $option The mode option.
+ * @return bool
+ */
+ public static function is_allowed_mode( $option ) {
+ // Normal constants are defined prior to WP_CLI running causing problems for activation
+ if ( defined( 'WAF_CLI_MODE' ) ) {
+ $option = WAF_CLI_MODE;
+ }
+
+ $allowed_modes = array(
+ 'normal',
+ 'silent',
+ );
+
+ return in_array( $option, $allowed_modes, true );
+ }
+
+ /**
+ * Determines if the WAF module is enabled on the site.
+ *
+ * @return bool
+ */
+ public static function is_enabled() {
+ // if ABSPATH is defined, then WordPress has already been instantiated,
+ // so we can check to see if the waf module is activated.
+ if ( defined( 'ABSPATH' ) ) {
+ return ( new Modules() )->is_active( 'waf' );
+ }
+
+ return true;
+ }
+
+ /**
+ * Runs the WAF and potentially stops the request if a problem is found.
+ *
+ * @return void
+ */
+ public static function run() {
+ // Make double-sure we are only running once.
+ if ( self::did_run() ) {
+ return;
+ }
+
+ Waf_Constants::initialize_constants();
+
+ // if ABSPATH is defined, then WordPress has already been instantiated,
+ // and we're running as a plugin (meh). Otherwise, we're running via something
+ // like PHP's prepend_file setting (yay!).
+ define( 'JETPACK_WAF_RUN', defined( 'ABSPATH' ) ? 'plugin' : 'preload' );
+
+ // if the WAF is being run before a command line script, don't try to execute rules (there's no request).
+ if ( PHP_SAPI === 'cli' ) {
+ return;
+ }
+
+ // if something terrible happens during the WAF running, we don't want to interfere with the rest of the site,
+ // so we intercept errors ONLY while the WAF is running, then we remove our handler after the WAF finishes.
+ $display_errors = ini_get( 'display_errors' );
+ // phpcs:ignore
+ ini_set( 'display_errors', 'Off' );
+ // phpcs:ignore
+ set_error_handler( array( self::class, 'errorHandler' ) );
+
+ try {
+
+ // phpcs:ignore
+ $waf = new Waf_Runtime( new Waf_Transforms(), new Waf_Operators() );
+
+ // execute waf rules.
+ // phpcs:ignore
+ include self::RULES_FILE;
+ } catch ( \Exception $err ) { // phpcs:ignore
+ // Intentionally doing nothing.
+ }
+
+ // remove the custom error handler, so we don't interfere with the site.
+ restore_error_handler();
+ // phpcs:ignore
+ ini_set( 'display_errors', $display_errors );
+ }
+
+ /**
+ * Error handler to be used while the WAF is being executed.
+ *
+ * @param int $code The error code.
+ * @param string $message The error message.
+ * @param string $file File with the error.
+ * @param string $line Line of the error.
+ * @return void
+ */
+ public static function errorHandler( $code, $message, $file, $line ) { // phpcs:ignore
+ // Intentionally doing nothing for now.
+ }
+
+ /**
+ * Initializes the WP filesystem.
+ *
+ * @return void
+ * @throws \Exception If filesystem is unavailable.
+ */
+ public static function initialize_filesystem() {
+ if ( ! function_exists( '\\WP_Filesystem' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ }
+
+ if ( ! \WP_Filesystem() ) {
+ throw new \Exception( 'No filesystem available.' );
+ }
+ }
+
+ /**
+ * Activates the WAF by generating the rules script and setting the version
+ *
+ * @return void
+ */
+ public static function activate() {
+ self::define_mode();
+ if ( ! self::is_allowed_mode( JETPACK_WAF_MODE ) ) {
+ return;
+ }
+ $version = get_option( self::VERSION_OPTION_NAME );
+ if ( ! $version ) {
+ add_option( self::VERSION_OPTION_NAME, self::WAF_RULES_VERSION );
+ }
+
+ add_option( self::SHARE_DATA_OPTION_NAME, true );
+
+ self::initialize_filesystem();
+ self::create_waf_directory();
+ self::generate_ip_rules();
+ self::create_blocklog_table();
+ self::generate_rules();
+ }
+
+ /**
+ * Created the waf directory on activation.
+ *
+ * @return void
+ * @throws \Exception In case there's a problem when creating the directory.
+ */
+ public static function create_waf_directory() {
+ WP_Filesystem();
+ Waf_Constants::initialize_constants();
+
+ global $wp_filesystem;
+ if ( ! $wp_filesystem ) {
+ throw new \Exception( 'Can not work without the file system being initialized.' );
+ }
+
+ if ( ! $wp_filesystem->is_dir( JETPACK_WAF_DIR ) ) {
+ if ( ! $wp_filesystem->mkdir( JETPACK_WAF_DIR ) ) {
+ throw new \Exception( 'Failed creating WAF standalone bootstrap file directory: ' . JETPACK_WAF_DIR );
+ }
+ }
+ }
+
+ /**
+ * Create the log table when plugin is activated.
+ *
+ * @return void
+ */
+ public static function create_blocklog_table() {
+ global $wpdb;
+
+ require_once ABSPATH . 'wp-admin/includes/upgrade.php';
+
+ $sql = "
+ CREATE TABLE {$wpdb->prefix}jetpack_waf_blocklog (
+ log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ timestamp datetime NOT NULL,
+ rule_id BIGINT NOT NULL,
+ reason longtext NOT NULL,
+ PRIMARY KEY (log_id),
+ KEY timestamp (timestamp)
+ )
+ ";
+
+ dbDelta( $sql );
+ }
+
+ /**
+ * Deactivates the WAF by deleting the relevant options and emptying rules file.
+ *
+ * @return void
+ * @throws \Exception If file writing fails.
+ */
+ public static function deactivate() {
+ delete_option( self::MODE_OPTION_NAME );
+ delete_option( self::VERSION_OPTION_NAME );
+
+ global $wp_filesystem;
+
+ self::initialize_filesystem();
+
+ if ( ! $wp_filesystem->put_contents( self::RULES_FILE, "<?php\n" ) ) {
+ throw new \Exception( 'Failed to empty rules.php file.' );
+ }
+ }
+
+ /**
+ * Tries periodically to update the rules using our API.
+ *
+ * @return void
+ */
+ public static function update_rules_cron() {
+ self::define_mode();
+ if ( ! self::is_allowed_mode( JETPACK_WAF_MODE ) ) {
+ return;
+ }
+
+ self::generate_rules();
+ update_option( self::RULE_LAST_UPDATED_OPTION_NAME, time() );
+ }
+
+ /**
+ * Updates the rule set if rules version has changed
+ *
+ * @return void
+ */
+ public static function update_rules_if_changed() {
+ self::define_mode();
+ if ( ! self::is_allowed_mode( JETPACK_WAF_MODE ) ) {
+ return;
+ }
+ $version = get_option( self::VERSION_OPTION_NAME );
+ if ( self::WAF_RULES_VERSION !== $version ) {
+ update_option( self::VERSION_OPTION_NAME, self::WAF_RULES_VERSION );
+ self::generate_rules();
+ }
+ }
+
+ /**
+ * Retrieve rules from the API
+ *
+ * @throws \Exception If site is not registered.
+ * @throws \Exception If API did not respond 200.
+ * @throws \Exception If data is missing from response.
+ * @return array
+ */
+ public static function get_rules_from_api() {
+ $blog_id = Jetpack_Options::get_option( 'id' );
+ if ( ! $blog_id ) {
+ throw new \Exception( 'Site is not registered' );
+ }
+
+ $response = Client::wpcom_json_api_request_as_blog(
+ sprintf( '/sites/%s/waf-rules', $blog_id ),
+ '2',
+ array(),
+ null,
+ 'wpcom'
+ );
+
+ $response_code = wp_remote_retrieve_response_code( $response );
+
+ if ( 200 !== $response_code ) {
+ throw new \Exception( 'API connection failed.', $response_code );
+ }
+
+ $rules_json = wp_remote_retrieve_body( $response );
+ $rules = json_decode( $rules_json, true );
+
+ if ( empty( $rules['data'] ) ) {
+ throw new \Exception( 'Data missing from response.' );
+ }
+
+ return $rules['data'];
+ }
+
+ /**
+ * Generates the rules.php script
+ *
+ * @throws \Exception If file writing fails.
+ * @return void
+ */
+ public static function generate_rules() {
+ /**
+ * WordPress filesystem abstraction.
+ *
+ * @var \WP_Filesystem_Base $wp_filesystem
+ */
+ global $wp_filesystem;
+
+ self::initialize_filesystem();
+
+ $api_exception = null;
+ $throw_api_exception = true;
+ try {
+ $rules = self::get_rules_from_api();
+ } catch ( \Exception $e ) {
+ if ( 401 === $e->getCode() ) {
+ // do not throw API exceptions for users who do not have access
+ $throw_api_exception = false;
+ }
+
+ if ( $wp_filesystem->exists( self::RULES_FILE ) && $throw_api_exception ) {
+ throw $e;
+ }
+
+ $rules = "<?php\n";
+ $api_exception = $e;
+ }
+
+ // Ensure that the folder exists.
+ if ( ! $wp_filesystem->is_dir( dirname( self::RULES_FILE ) ) ) {
+ $wp_filesystem->mkdir( dirname( self::RULES_FILE ) );
+ }
+
+ $ip_allow_rules = self::ALLOW_IP_FILE;
+ $ip_block_rules = self::BLOCK_IP_FILE;
+
+ $ip_list_code = "if ( require('$ip_allow_rules') ) { return; }\n" .
+ "if ( require('$ip_block_rules') ) { return \$waf->block('block', -1, 'ip block list'); }\n";
+
+ $rules_divided_by_line = explode( "\n", $rules );
+ array_splice( $rules_divided_by_line, 1, 0, $ip_list_code );
+
+ $rules = implode( "\n", $rules_divided_by_line );
+
+ if ( ! $wp_filesystem->put_contents( self::RULES_FILE, $rules ) ) {
+ throw new \Exception( 'Failed writing rules file to: ' . self::RULES_FILE );
+ }
+
+ if ( null !== $api_exception && $throw_api_exception ) {
+ throw $api_exception;
+ }
+ }
+
+ /**
+ * We allow for both, one IP per line or comma-; semicolon; or whitespace-separated lists. This also validates the IP addresses
+ * and only returns the ones that look valid.
+ *
+ * @param string $ips List of ips - example: "8.8.8.8\n4.4.4.4,2.2.2.2;1.1.1.1 9.9.9.9,5555.5555.5555.5555".
+ * @return array List of valid IP addresses. - example based on input example: array('8.8.8.8', '4.4.4.4', '2.2.2.2', '1.1.1.1', '9.9.9.9')
+ */
+ private static function ip_option_to_array( $ips ) {
+ $ips = (string) $ips;
+ $ips = preg_split( '/[\s,;]/', $ips );
+
+ $result = array();
+
+ foreach ( $ips as $ip ) {
+ if ( filter_var( $ip, FILTER_VALIDATE_IP ) !== false ) {
+ $result[] = $ip;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Generates the rules.php script
+ *
+ * @throws \Exception If filesystem is not available.
+ * @throws \Exception If file writing fails.
+ * @return void
+ */
+ public static function generate_ip_rules() {
+ /**
+ * WordPress filesystem abstraction.
+ *
+ * @var \WP_Filesystem_Base $wp_filesystem
+ */
+ global $wp_filesystem;
+
+ self::initialize_filesystem();
+
+ // Ensure that the folder exists.
+ if ( ! $wp_filesystem->is_dir( dirname( self::RULES_FILE ) ) ) {
+ $wp_filesystem->mkdir( dirname( self::RULES_FILE ) );
+ }
+
+ $allow_list = self::ip_option_to_array( get_option( self::IP_ALLOW_LIST_OPTION_NAME ) );
+ $block_list = self::ip_option_to_array( get_option( self::IP_BLOCK_LIST_OPTION_NAME ) );
+
+ $lists_enabled = (bool) get_option( self::IP_LISTS_ENABLED_OPTION_NAME );
+ if ( false === $lists_enabled ) {
+ // Making the lists empty effectively disabled the feature while still keeping the other WAF rules evaluation active.
+ $allow_list = array();
+ $block_list = array();
+ }
+
+ $allow_rules_content = '';
+ // phpcs:disable WordPress.PHP.DevelopmentFunctions
+ $allow_rules_content .= '$waf_allow_list = ' . var_export( $allow_list, true ) . ";\n";
+ // phpcs:enable
+ $allow_rules_content .= 'return $waf->is_ip_in_array( $waf_allow_list );' . "\n";
+
+ if ( ! $wp_filesystem->put_contents( self::ALLOW_IP_FILE, "<?php\n$allow_rules_content" ) ) {
+ throw new \Exception( 'Failed writing allow list file to: ' . self::ALLOW_IP_FILE );
+ }
+
+ $block_rules_content = '';
+ // phpcs:disable WordPress.PHP.DevelopmentFunctions
+ $block_rules_content .= '$waf_block_list = ' . var_export( $block_list, true ) . ";\n";
+ // phpcs:enable
+ $block_rules_content .= 'return $waf->is_ip_in_array( $waf_block_list );' . "\n";
+
+ if ( ! $wp_filesystem->put_contents( self::BLOCK_IP_FILE, "<?php\n$block_rules_content" ) ) {
+ throw new \Exception( 'Failed writing block list file to: ' . self::BLOCK_IP_FILE );
+ }
+ }
+}
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 ) );
+ }
+}
diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-standalone-bootstrap.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-standalone-bootstrap.php
new file mode 100644
index 00000000..26e8f053
--- /dev/null
+++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-standalone-bootstrap.php
@@ -0,0 +1,160 @@
+<?php
+/**
+ * Handles generation and deletion of the bootstrap for the standalone WAF mode.
+ *
+ * @package automattic/jetpack-waf
+ */
+
+namespace Automattic\Jetpack\Waf;
+
+use Composer\InstalledVersions;
+use Exception;
+
+/**
+ * Handles the bootstrap.
+ */
+class Waf_Standalone_Bootstrap {
+
+ /**
+ * Ensures that constants are initialized if this class is used.
+ */
+ public function __construct() {
+ $this->guard_against_missing_abspath();
+ $this->initialize_constants();
+ }
+
+ /**
+ * Ensures that this class is not used unless we are in the right context.
+ *
+ * @return void
+ * @throws Exception If we are outside of WordPress.
+ */
+ private function guard_against_missing_abspath() {
+
+ if ( ! defined( 'ABSPATH' ) ) {
+ throw new Exception( 'Cannot generate the WAF bootstrap if we are not running in WordPress context.' );
+ }
+ }
+
+ /**
+ * Initializes the constants required for generating the bootstrap, if they have not been initialized yet.
+ *
+ * @return void
+ */
+ private function initialize_constants() {
+ Waf_Constants::initialize_constants();
+ }
+
+ /**
+ * Initialized the WP filesystem and serves as a mocking hook for tests.
+ *
+ * @return void
+ */
+ protected function initialize_filesystem() {
+ if ( ! function_exists( '\\WP_Filesystem' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ }
+
+ WP_Filesystem();
+ }
+
+ /**
+ * Finds the path to the autoloader, which can then be used to require the autoloader in the generated boostrap file.
+ *
+ * @return string|null
+ * @throws Exception In case the autoloader file can not be found.
+ */
+ private function locate_autoloader_file() {
+ global $jetpack_autoloader_loader;
+
+ $autoload_file = null;
+
+ // Try the Jetpack autoloader.
+ if ( isset( $jetpack_autoloader_loader ) ) {
+ $class_file = $jetpack_autoloader_loader->find_class_file( Waf_Runner::class );
+ if ( $class_file ) {
+ $autoload_file = dirname( dirname( dirname( dirname( dirname( $class_file ) ) ) ) ) . '/vendor/autoload.php';
+ }
+ }
+
+ // Try Composer's autoloader.
+ if ( null === $autoload_file
+ && is_callable( array( InstalledVersions::class, 'getInstallPath' ) )
+ && InstalledVersions::isInstalled( 'automattic/jetpack-waf' )
+ ) {
+ $package_file = InstalledVersions::getInstallPath( 'automattic/jetpack-waf' );
+ if ( substr( $package_file, -23 ) === '/automattic/jetpack-waf' ) {
+ $autoload_file = dirname( dirname( dirname( $package_file ) ) ) . '/vendor/autoload.php';
+ }
+ }
+
+ // Guess. First look for being in a `vendor/automattic/jetpack-waf/src/', then see if we're standalone with our own vendor dir.
+ if ( null === $autoload_file ) {
+ $autoload_file = dirname( dirname( dirname( dirname( __DIR__ ) ) ) ) . '/vendor/autoload.php';
+ if ( ! file_exists( $autoload_file ) ) {
+ $autoload_file = dirname( __DIR__ ) . '/vendor/autoload.php';
+ }
+ }
+
+ // Check that the determined file actually exists.
+ if ( ! file_exists( $autoload_file ) ) {
+ throw new Exception( 'Can not find autoloader, and the WAF standalone boostrap will not work without it.' );
+ }
+
+ return $autoload_file;
+ }
+
+ /**
+ * Gets the path to the bootstrap.php file.
+ *
+ * @return string The bootstrap.php file path.
+ */
+ public function get_bootstrap_file_path() {
+ return trailingslashit( JETPACK_WAF_DIR ) . 'bootstrap.php';
+ }
+
+ /**
+ * Generates the bootstrap file.
+ *
+ * @return string Absolute path to the bootstrap file.
+ * @throws Exception In case the file can not be written.
+ */
+ public function generate() {
+
+ $this->initialize_filesystem();
+
+ global $wp_filesystem;
+ if ( ! $wp_filesystem ) {
+ throw new Exception( 'Can not work without the file system being initialized.' );
+ }
+
+ $bootstrap_file = $this->get_bootstrap_file_path();
+ $mode_option = get_option( Waf_Runner::MODE_OPTION_NAME, false );
+ $share_data_option = get_option( Waf_Runner::SHARE_DATA_OPTION_NAME, false );
+
+ // phpcs:disable WordPress.PHP.DevelopmentFunctions
+ $code = "<?php\n"
+ . sprintf( "define( 'DISABLE_JETPACK_WAF', %s );\n", var_export( defined( 'DISABLE_JETPACK_WAF' ) && DISABLE_JETPACK_WAF, true ) )
+ . "if ( defined( 'DISABLE_JETPACK_WAF' ) && DISABLE_JETPACK_WAF ) return;\n"
+ . sprintf( "define( 'JETPACK_WAF_MODE', %s );\n", var_export( $mode_option ? $mode_option : 'silent', true ) )
+ . sprintf( "define( 'JETPACK_WAF_SHARE_DATA', %s );\n", var_export( $share_data_option, true ) )
+ . sprintf( "define( 'JETPACK_WAF_DIR', %s );\n", var_export( JETPACK_WAF_DIR, true ) )
+ . sprintf( "define( 'JETPACK_WAF_WPCONFIG', %s );\n", var_export( JETPACK_WAF_WPCONFIG, true ) )
+ . 'require_once ' . var_export( $this->locate_autoloader_file(), true ) . ";\n"
+ . 'include ' . var_export( dirname( __DIR__ ) . '/run.php', true ) . ";\n";
+ // phpcs:enable
+
+ if ( ! $wp_filesystem->is_dir( JETPACK_WAF_DIR ) ) {
+ if ( ! $wp_filesystem->mkdir( JETPACK_WAF_DIR ) ) {
+ throw new Exception( 'Failed creating WAF standalone bootstrap file directory: ' . JETPACK_WAF_DIR );
+ }
+ }
+
+ if ( ! $wp_filesystem->put_contents( $bootstrap_file, $code ) ) {
+ throw new Exception( 'Failed writing WAF standalone bootstrap file to: ' . $bootstrap_file );
+ }
+
+ return $bootstrap_file;
+ }
+
+}
diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-transforms.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-transforms.php
new file mode 100644
index 00000000..a559394f
--- /dev/null
+++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-transforms.php
@@ -0,0 +1,342 @@
+<?php
+/**
+ * Transforms for Jetpack Waf
+ *
+ * @package automattic/jetpack-waf
+ */
+
+namespace Automattic\Jetpack\Waf;
+
+/**
+ * Waf_Transforms class
+ */
+class Waf_Transforms {
+ /**
+ * Decode a Base64-encoded string.
+ *
+ * @param string $value value to be decoded.
+ * @return string
+ */
+ public function base64_decode( $value ) {
+ return base64_decode( $value );
+ }
+
+ /**
+ * Remove all characters that might escape a command line command
+ *
+ * @see https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-%28v2.x%29#cmdLine
+ * @param string $value value to be escaped.
+ * @return string
+ */
+ public function cmd_line( $value ) {
+ return strtolower(
+ preg_replace(
+ '/\s+/',
+ ' ',
+ str_replace(
+ array( ',', ';' ),
+ ' ',
+ preg_replace(
+ '/\s+(?=[\/\(])/',
+ '',
+ str_replace(
+ array( '^', "'", '"', '\\' ),
+ '',
+ $value
+ )
+ )
+ )
+ )
+ );
+ }
+
+ /**
+ * Decode a SQL hex string.
+ *
+ * @example 414243 decodes to "ABC"
+ * @param string $value value to be decoded.
+ * @return string
+ */
+ public function sql_hex_decode( $value ) {
+ return preg_replace_callback(
+ '/0x[a-f0-9]+/i',
+ function ( $matches ) {
+ $str = substr( $matches[0], 2 );
+ if ( 0 !== strlen( $str ) % 2 ) {
+ $str = '0' . $str;
+ }
+ return hex2bin( $str );
+ },
+ $value
+ );
+ }
+
+ /**
+ * Encode a string using Base64 encoding.
+ *
+ * @param string $value value to be decoded.
+ * @return string
+ */
+ public function base64_encode( $value ) {
+ return base64_encode( $value );
+ }
+
+ /**
+ * Convert all whitespace characters to a space and remove any repeated spaces.
+ *
+ * @param string $value value to be converted.
+ * @return string
+ */
+ public function compress_whitespace( $value ) {
+ return preg_replace( '/\s+/', ' ', $value );
+ }
+
+ /**
+ * Encode string (possibly containing binary characters) by replacing each input byte with two hexadecimal characters.
+ *
+ * @param string $value value to be encoded.
+ * @return string
+ */
+ public function hex_encode( $value ) {
+ return bin2hex( $value );
+ }
+
+ /**
+ * Decode string that was previously encoded by hexEncode()
+ *
+ * @param string $value value to be decoded.
+ * @return string
+ */
+ public function hex_decode( $value ) {
+ return pack( 'H*', $value );
+ }
+
+ /**
+ * Decode the characters encoded as HTML entities.
+ *
+ * @param mixed $value value do be decoded.
+ * @return string
+ */
+ public function html_entity_decode( $value ) {
+ return html_entity_decode( $value );
+ }
+
+ /**
+ * Return the length of the input string.
+ *
+ * @param string $value input string.
+ * @return int
+ */
+ public function length( $value ) {
+ return strlen( $value );
+ }
+
+ /**
+ * Convert all characters to lowercase.
+ *
+ * @param string $value string to be converted.
+ * @return string
+ */
+ public function lowercase( $value ) {
+ return strtolower( $value );
+ }
+
+ /**
+ * Calculate an md5 hash for the given data
+ *
+ * @param mixed $value value to be hashed.
+ * @return string
+ */
+ public function md5( $value ) {
+ return md5( $value, true );
+ }
+
+ /**
+ * Removes multiple slashes, directory self-references, and directory back-references (except when at the beginning of the input) from input string.
+ *
+ * @param string $value value to be normalized.
+ * @return string
+ */
+ public function normalize_path( $value ) {
+ $parts = explode(
+ '/',
+ // replace any duplicate slashes with a single one.
+ preg_replace( '~/{2,}~', '/', $value )
+ );
+
+ $i = 0;
+ while ( isset( $parts[ $i ] ) ) {
+ switch ( $parts[ $i ] ) {
+ // If this folder is a self-reference, remove it.
+ case '..':
+ // If this folder is a backreference, remove it unless we're already at the root.
+ if ( isset( $parts[ $i - 1 ] ) && ! in_array( $parts[ $i - 1 ], array( '', '..' ), true ) ) {
+ array_splice( $parts, $i - 1, 2 );
+ $i--;
+ continue 2;
+ }
+ break;
+ case '.':
+ array_splice( $parts, $i, 1 );
+ continue 2;
+ }
+ $i++;
+ }
+
+ return implode( '/', $parts );
+ }
+
+ /**
+ * Convert backslash characters to forward slashes, and then normalize using `normalizePath`
+ *
+ * @param string $value to be normalized.
+ * @return string
+ */
+ public function normalize_path_win( $value ) {
+ return $this->normalize_path( str_replace( '\\', '/', $value ) );
+ }
+
+ /**
+ * Removes all NUL bytes from input.
+ *
+ * @param string $value value to be filtered.
+ * @return string
+ */
+ public function remove_nulls( $value ) {
+ return str_replace( "\x0", '', $value );
+ }
+
+ /**
+ * Remove all whitespace characters from input.
+ *
+ * @param string $value value to be filtered.
+ * @return string
+ */
+ public function remove_whitespace( $value ) {
+ return preg_replace( '/\s/', '', $value );
+ }
+
+ /**
+ * Replaces each occurrence of a C-style comment (/ * ... * /) with a single space.
+ * Unterminated comments will also be replaced with a space. However, a standalone termination of a comment (* /) will not be acted upon.
+ *
+ * @param string $value value to be filtered.
+ * @return string
+ */
+ public function replace_comments( $value ) {
+ $value = preg_replace( '~/\*.*?\*/|/\*.*?$~Ds', ' ', $value );
+ return explode( '/*', $value, 2 )[0];
+ }
+
+ /**
+ * Removes common comments chars (/ *, * /, --, #).
+ *
+ * @param string $value value to be filtered.
+ * @return string
+ */
+ public function remove_comments_char( $value ) {
+ return preg_replace( '~/*|*/|--|#|//~', '', $value );
+ }
+
+ /**
+ * Replaces each NUL byte in input with a space.
+ *
+ * @param string $value value to be filtered.
+ * @return string
+ */
+ public function replace_nulls( $value ) {
+ return str_replace( "\x0", ' ', $value );
+ }
+
+ /**
+ * Decode a URL-encoded input string.
+ *
+ * @param string $value value to be decoded.
+ * @return string
+ */
+ public function url_decode( $value ) {
+ return urldecode( $value );
+ }
+
+ /**
+ * Decode a URL-encoded input string.
+ *
+ * @param string $value value to be decoded.
+ * @return string
+ */
+ public function url_decode_uni( $value ) {
+ error_log( 'JETPACKWAF TRANSFORM NOT IMPLEMENTED: urlDecodeUni' );
+ return $value;
+ }
+
+ /**
+ * Decode a json encoded input string.
+ *
+ * @param string $value value to be decoded.
+ * @return string
+ */
+ public function js_decode( $value ) {
+ error_log( 'JETPACKWAF TRANSFORM NOT IMPLEMENTED: jsDecode' );
+ return $value;
+ }
+
+ /**
+ * Convert all characters to uppercase.
+ *
+ * @param string $value value to be encoded.
+ * @return string
+ */
+ public function uppercase( $value ) {
+ return strtoupper( $value );
+ }
+
+ /**
+ * Calculate a SHA1 hash from the input string.
+ *
+ * @param mixed $value value to be hashed.
+ * @return string
+ */
+ public function sha1( $value ) {
+ return sha1( $value, true );
+ }
+
+ /**
+ * Remove whitespace from the left side of the input string.
+ *
+ * @param string $value value to be trimmed.
+ * @return string
+ */
+ public function trim_left( $value ) {
+ return ltrim( $value );
+ }
+
+ /**
+ * Remove whitespace from the right side of the input string.
+ *
+ * @param string $value value to be trimmed.
+ * @return string
+ */
+ public function trim_right( $value ) {
+ return rtrim( $value );
+ }
+
+ /**
+ * Remove whitespace from both sides of the input string.
+ *
+ * @param string $value value to be trimmed.
+ * @return string
+ */
+ public function trim( $value ) {
+ return trim( $value );
+ }
+
+ /**
+ * Convert utf-8 characters to unicode characters
+ *
+ * @param string $value value to be encoded.
+ * @return string
+ */
+ public function utf8_to_unicode( $value ) {
+ return preg_replace( '/\\\u(?=[a-f0-9]{4})/', '%u', substr( json_encode( $value ), 1, -1 ) );
+ }
+}
diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/functions.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/functions.php
new file mode 100644
index 00000000..a8112fd9
--- /dev/null
+++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/functions.php
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Utility functions for WAF.
+ *
+ * @package automattic/jetpack-waf
+ */
+
+namespace Automattic\Jetpack\Waf;
+
+/**
+ * A wrapper for WordPress's `wp_unslash()`.
+ *
+ * Even though PHP itself dropped the option to add slashes to superglobals a decade ago,
+ * WordPress still does it through some misguided extreme backwards compatibility. 🙄
+ *
+ * If WordPress's function exists, assume it needs to be called. If not, assume it doesn't.
+ *
+ * @param string|array $value String or array of data to unslash.
+ * @return string|array Possibly unslashed $value.
+ */
+function wp_unslash( $value ) {
+ if ( function_exists( '\\wp_unslash' ) ) {
+ return \wp_unslash( $value );
+ } else {
+ return $value;
+ }
+}