File: /wordpress/plugins/jetpack/16.0/jetpack_vendor/automattic/jetpack-waf/src/class-waf-request.php
<?php
/**
* HTTP request representation specific for the WAF.
*
* @package automattic/jetpack-waf
*/
namespace Automattic\Jetpack\Waf;
require_once __DIR__ . '/functions.php';
<<<'PHAN'
@phan-type RequestFile = array{ name: string, filename: string }
PHAN;
/**
* Request representation.
*/
class Waf_Request {
/**
* The request URL, broken into three pieces: the host, the filename, and the query string
*
* @example for `https://wordpress.com/index.php?myvar=red`
* $this->url = [ 'https://wordpress.com', '/index.php', '?myvar=red' ]
* @var array{0: string, 1: string, 2: string}|null
*/
protected $url = null;
/**
* 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;
}
/**
* Returns the headers that were sent with this request
*
* @return array{0: string, 1: scalar}[]
*/
public function get_headers() {
$value = array();
$has_content_length = false;
foreach ( $_SERVER as $k => $v ) {
$k = strtolower( $k );
if ( 'http_' === substr( $k, 0, 5 ) ) {
$value[] = array( $this->normalize_header_name( substr( $k, 5 ) ), $v );
} elseif ( 'content_type' === $k && '' !== $v ) {
$value[] = array( 'content-type', $v );
} elseif ( 'content_length' === $k && '' !== $v ) {
$has_content_length = true;
$value[] = array( 'content-length', $v );
}
}
if ( ! $has_content_length ) {
$value[] = array( 'content-length', '0' );
}
return $value;
}
/**
* Returns the value of a specific header that was sent with this request
*
* @param string $name The name of the header to retrieve.
* @return string
*/
public function get_header( $name ) {
$name = $this->normalize_header_name( $name );
foreach ( $this->get_headers() as list( $header_name, $header_value ) ) {
if ( $header_name === $name ) {
return $header_value;
}
}
return '';
}
/**
* Change a header name to all-lowercase and replace spaces and underscores with dashes.
*
* @param string $name The header name to normalize.
* @return string
*/
public function normalize_header_name( $name ) {
return str_replace( array( ' ', '_' ), '-', strtolower( $name ) );
}
/**
* Get the method for this request (GET, POST, etc).
*
* @return string
*/
public function get_method() {
return isset( $_SERVER['REQUEST_METHOD'] )
? filter_var( wp_unslash( $_SERVER['REQUEST_METHOD'] ), FILTER_DEFAULT )
: '';
}
/**
* Get the protocol for this request (HTTP, HTTPS, etc)
*
* @return string
*/
public function get_protocol() {
return isset( $_SERVER['SERVER_PROTOCOL'] )
? filter_var( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ), FILTER_DEFAULT )
: '';
}
/**
* Returns the URL parts for this request.
*
* @see $this->url
* @return array{0: string, 1: string, 2: string}
*/
protected function get_url() {
if ( null !== $this->url ) {
return $this->url;
}
$uri = isset( $_SERVER['REQUEST_URI'] ) ? filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_DEFAULT ) : '/';
if ( false !== strpos( $uri, '?' ) ) {
// remove the query string (we'll pull it from elsewhere later)
$uri = urldecode( substr( $uri, 0, strpos( $uri, '?' ) ) );
} else {
$uri = urldecode( $uri );
}
$query_string = isset( $_SERVER['QUERY_STRING'] ) ? '?' . filter_var( wp_unslash( $_SERVER['QUERY_STRING'] ), FILTER_DEFAULT ) : '';
if ( 1 === preg_match( '/^https?:\/\//', $uri ) ) {
// sometimes $_SERVER[REQUEST_URI] already includes the full domain name
$uri_host = substr( $uri, 0, strpos( $uri, '/', 8 ) );
$uri_path = substr( $uri, strlen( $uri_host ) );
$this->url = array( $uri_host, $uri_path, $query_string );
} else {
// otherwise build the URI manually
$uri_scheme = ( ! empty( $_SERVER['HTTPS'] ) && 'off' !== $_SERVER['HTTPS'] )
? 'https'
: 'http';
$uri_host = isset( $_SERVER['HTTP_HOST'] )
? filter_var( wp_unslash( $_SERVER['HTTP_HOST'] ), FILTER_DEFAULT )
: (
isset( $_SERVER['SERVER_NAME'] )
? filter_var( wp_unslash( $_SERVER['SERVER_NAME'] ), FILTER_DEFAULT )
: ''
);
$uri_port = isset( $_SERVER['SERVER_PORT'] )
? filter_var( wp_unslash( $_SERVER['SERVER_PORT'] ), FILTER_SANITIZE_NUMBER_INT )
: '';
// we only need to include the port if it's non-standard
if ( $uri_port && ( 'http' === $uri_scheme && '80' !== $uri_port || 'https' === $uri_scheme && '443' !== $uri_port ) ) {
$uri_port = ':' . $uri_port;
} else {
$uri_port = '';
}
$this->url = array(
$uri_scheme . '://' . $uri_host . $uri_port,
$uri,
$query_string,
);
}
return $this->url;
}
/**
* Get the requested URI
*
* @param boolean $include_host If true, the scheme and domain will be included in the returned string (i.e. 'https://wordpress.com/index.php).
* If false, only the requested URI path will be returned (i.e. '/index.php').
* @return string
*/
public function get_uri( $include_host = false ) {
list( $host, $file, $query ) = $this->get_url();
return ( $include_host ? $host : '' ) . $file . $query;
}
/**
* Return the filename part of the request
*
* @example for 'https://wordpress.com/some/page?id=5', return '/some/page'
* @return string
*/
public function get_filename() {
return $this->get_url()[1];
}
/**
* Return the basename part of the request
*
* @example for 'https://wordpress.com/some/page.php?id=5', return 'page.php'
* @return string
*/
public function get_basename() {
// Get the filename part of the request
$filename = $this->get_filename();
// Normalize slashes
$filename = str_replace( '\\', '/', $filename );
// Remove trailing slashes
$filename = rtrim( $filename, '/' );
// Return the basename
$offset = strrpos( $filename, '/' );
return $offset !== false ? substr( $filename, $offset + 1 ) : $filename;
}
/**
* Return the query string. If present, it will be prefixed with '?'. Otherwise, it will be an empty string.
*
* @return string
*/
public function get_query_string() {
return $this->get_url()[2];
}
/**
* Returns the request body.
*
* @return string
*/
public function get_body() {
$body = file_get_contents( 'php://input' );
return false === $body ? '' : $body;
}
/**
* Returns the cookies
*
* @return array{string, scalar}[]
*/
public function get_cookies() {
return flatten_array( $_COOKIE );
}
/**
* Returns the GET variables
*
* @return array{string, scalar}[]
*/
public function get_get_vars() {
return flatten_array( $_GET );
}
/**
* Returns the POST variables from a JSON body
*
* @return array{string, scalar}[]
*/
private function get_json_post_vars() {
$decoded_json = json_decode( $this->get_body(), true ) ?? array();
return flatten_array( $decoded_json, 'json', true );
}
/**
* Returns the POST variables from a urlencoded body
*
* @return array{string, scalar}[]
*/
private function get_urlencoded_post_vars() {
parse_str( $this->get_body(), $params );
return flatten_array( $params );
}
/**
* Returns the POST variables
*
* @param string $body_processor Manually specifiy the method to use to process the body. Options are 'URLENCODED' and 'JSON'.
*
* @return array{string, scalar}[]
*/
public function get_post_vars( string $body_processor = '' ) {
$content_type = $this->get_header( 'content-type' );
// If the body processor is specified by the rules file, trust it.
if ( 'URLENCODED' === $body_processor ) {
return $this->get_urlencoded_post_vars();
}
if ( 'JSON' === $body_processor ) {
return $this->get_json_post_vars();
}
// Otherwise, use $_POST if it's not empty.
if ( ! empty( $_POST ) ) {
return flatten_array( $_POST );
}
// Lastly, try to parse the body based on the content type.
if ( strpos( $content_type, 'application/json' ) !== false ) {
return $this->get_json_post_vars();
}
if ( strpos( $content_type, 'application/x-www-form-urlencoded' ) !== false ) {
return $this->get_urlencoded_post_vars();
}
// Don't try to parse any other content types.
return array();
}
/**
* Returns the files that were uploaded with this request (i.e. what's in the $_FILES superglobal)
*
* @return RequestFile[]
*/
public function get_files() {
$files = array();
foreach ( $_FILES as $field_name => $arr ) {
// flatten the values in case we were given inputs with brackets
foreach ( flatten_array( $arr ) as list( $arr_key, $arr_value ) ) {
if ( $arr_key === 'name' ) {
// if this file was a simple (non-nested) name and unique, then just add it.
$files[] = array(
'name' => $field_name,
'filename' => $arr_value,
);
} elseif ( 'name[' === substr( $arr_key, 0, 5 ) ) {
// otherwise this was a file with a nested name and/or multiple files with the same name
$files[] = array(
'name' => $field_name . substr( $arr_key, 4 ),
'filename' => $arr_value,
);
}
}
}
return $files;
}
}