*/ public static function current() { $attribution = array(); if ( isset( $_COOKIE[ self::COOKIE ] ) && is_string( $_COOKIE[ self::COOKIE ] ) ) { // The value is JSON: decoded first, then each allowed value is sanitized in from_cookie(). $attribution = self::from_cookie( wp_unslash( $_COOKIE[ self::COOKIE ] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON: decoded in from_cookie(), which keeps only the allowed keys and sanitizes each value. } $referer = wp_get_raw_referer(); $url = self::page_url( is_string( $referer ) ? $referer : '', home_url( '/' ) ); if ( '' !== $url ) { $attribution['_url'] = $url; } return $attribution; } /** * The allowed keys from the cookie, each a sanitized string of at most 500 characters. * * @param string $raw The cookie's value. * @return array */ public static function from_cookie( $raw ) { $raw = is_string( $raw ) ? trim( $raw ) : ''; if ( '' === $raw || strlen( $raw ) > self::MAX_COOKIE ) { return array(); } $decoded = json_decode( $raw, true ); // A cookie that reached PHP still encoded (some proxies): decode it once more. if ( ! is_array( $decoded ) && 0 === strpos( $raw, '%7B' ) ) { $decoded = json_decode( rawurldecode( $raw ), true ); } if ( ! is_array( $decoded ) ) { return array(); } $attribution = array(); foreach ( self::KEYS as $key ) { if ( ! isset( $decoded[ $key ] ) || ! is_string( $decoded[ $key ] ) ) { continue; } $value = '_referrer' === $key ? self::clean_url( $decoded[ $key ], self::MAX_LENGTH ) : Connection::truncate( trim( sanitize_text_field( $decoded[ $key ] ) ), self::MAX_LENGTH ); if ( '' !== $value ) { $attribution[ $key ] = $value; } } return $attribution; } /** * The page a form was submitted from: the Referer (or the form's own `_wp_http_referer`, a path * on this site), only when it is on this site's host, without its query string or fragment. * * @param string $referer The Referer. * @param string $home This site's home URL. * @return string */ public static function page_url( $referer, $home ) { $referer = is_string( $referer ) ? trim( $referer ) : ''; $site = wp_parse_url( $home ); if ( '' === $referer || strlen( $referer ) > self::MAX_URL || ! is_array( $site ) || empty( $site['host'] ) ) { return ''; } // A path on this site (`/join/`), never a protocol-relative `//elsewhere`. if ( '/' === $referer[0] && ( ! isset( $referer[1] ) || '/' !== $referer[1] ) ) { $referer = ( isset( $site['scheme'] ) ? $site['scheme'] : 'https' ) . '://' . $site['host'] . ( isset( $site['port'] ) ? ':' . (int) $site['port'] : '' ) . $referer; } $page = wp_parse_url( $referer ); if ( ! is_array( $page ) || empty( $page['host'] ) || empty( $page['scheme'] ) ) { return ''; } $scheme = strtolower( $page['scheme'] ); if ( ( 'http' !== $scheme && 'https' !== $scheme ) || self::host( $page['host'] ) !== self::host( $site['host'] ) ) { return ''; } $url = $scheme . '://' . strtolower( $page['host'] ) . ( isset( $page['port'] ) ? ':' . (int) $page['port'] : '' ) . ( isset( $page['path'] ) ? $page['path'] : '/' ); return self::clean_url( $url, self::MAX_URL ); } /** * An http(s) URL kept as is (with its percent-encoding), or `""` when it is not one or is longer * than the limit: a URL is dropped, never cut, since a cut URL names another page. * * @param string $url The URL. * @param int $length The most characters allowed. * @return string */ private static function clean_url( $url, $length ) { $url = trim( (string) $url ); if ( ! preg_match( '#^https?://#i', $url ) || strlen( $url ) > $length ) { return ''; } return esc_url_raw( $url, array( 'http', 'https' ) ); } /** * A host compared the way the LaunchList loader compares them: lowercase, without `www.`. * * @param string $host The host. * @return string */ private static function host( $host ) { $host = strtolower( $host ); return 0 === strpos( $host, 'www.' ) ? substr( $host, 4 ) : $host; } }