]{0,2000}/i'; /** src attribute value inside one iframe tag. Subject: one IFRAME_TAG match. */ const IFRAME_SRC = '/\bsrc[ \t\r\n]{0,10}=[ \t\r\n]{0,10}["\']?([^"\'\s>]{1,2000})/i'; /** File header line, like get_file_data(). %s = preg_quote()d header name. Subject: HEADER_BYTES. */ const HEADER = '/^(?:[ \t]{0,20}<\?php)?[ \t\/*#@]{0,40}%s:([^\r\n]{0,500})$/mi'; /** Opening container, lowercased and collapsed (Assumptions::UPLOADS_ACCESS_CONTAINER_CLOSE). Subject: one line (LINE_MAX_CHARS). */ const IFMODULE_OPEN = '/^$/'; /** Opening / container, lowercased and collapsed (D-01). Subject: one line (LINE_MAX_CHARS). */ const FILES_OPEN = '/^]{1,500}>$/'; /** Octal (\0nn) or hex (\xnn) escape in an include/require path (D-12). Subject: one WP_CONFIG_INCLUDE match. */ const ESCAPED_PATH = '/\\\\(?:0[0-7]{2}|x[0-9a-fA-F]{2})/'; /** Call with 1 to 10 blanks before the parenthesis (D-12). %s = preg_quote()d function name. Subject: wp-config.php (CONTENT_MAX_BYTES). */ const SPACED_CALL = '/\b%s[ \t\r\n]{1,10}\(/i'; /** Patterns checked by the adversarial test (HEADER with a real header name). */ public static function all(): array { return array( 'FILES_OPEN' => self::FILES_OPEN, 'ESCAPED_PATH' => self::ESCAPED_PATH, 'SPACED_CALL' => sprintf( self::SPACED_CALL, preg_quote( 'eval', '/' ) ), 'IFMODULE_OPEN' => self::IFMODULE_OPEN, 'WS_SPLIT' => self::WS_SPLIT, 'WS_COLLAPSE' => self::WS_COLLAPSE, 'WP_CONFIG_INCLUDE' => self::WP_CONFIG_INCLUDE, 'IFRAME_TAG' => self::IFRAME_TAG, 'IFRAME_SRC' => self::IFRAME_SRC, 'HEADER' => sprintf( self::HEADER, preg_quote( 'Plugin Name', '/' ) ), ); } /** @return string[] */ public static function tokens( string $line ): array { $line = trim( substr( $line, 0, Assumptions::LINE_MAX_CHARS ) ); $parts = '' === $line ? array() : preg_split( self::WS_SPLIT, $line ); return is_array( $parts ) ? $parts : array(); } /** True for an opening line already lowercased and collapsed. */ public static function isIfModuleOpen( string $collapsed ): bool { return 1 === preg_match( self::IFMODULE_OPEN, substr( $collapsed, 0, Assumptions::LINE_MAX_CHARS ) ); } /** True for an opening or line already lowercased and collapsed. */ public static function isFilesOpen( string $collapsed ): bool { return 1 === preg_match( self::FILES_OPEN, substr( $collapsed, 0, Assumptions::LINE_MAX_CHARS ) ); } /** True when an include/require statement writes its path with \0nn or \xnn escapes. */ public static function hasEscapedPath( string $statement ): bool { return 1 === preg_match( self::ESCAPED_PATH, substr( $statement, 0, Assumptions::LINE_MAX_CHARS ) ); } /** * Offset of "name (" with 1 to 10 blanks before the parenthesis, name as a whole word, or null. * * @return int|null */ public static function spacedCall( string $text, string $name ) { $text = substr( $text, 0, Assumptions::CONTENT_MAX_BYTES ); if ( preg_match( sprintf( self::SPACED_CALL, preg_quote( $name, '/' ) ), $text, $m, PREG_OFFSET_CAPTURE ) ) { return (int) $m[0][1]; } return null; } public static function collapse( string $line ): string { $out = preg_replace( self::WS_COLLAPSE, ' ', substr( $line, 0, Assumptions::LINE_MAX_CHARS ) ); return trim( is_string( $out ) ? $out : '' ); } /** @return string[] include/require statements found in wp-config.php. */ public static function includeStatements( string $content ): array { $content = substr( $content, 0, Assumptions::CONTENT_MAX_BYTES ); if ( preg_match_all( self::WP_CONFIG_INCLUDE, $content, $m ) ) { return $m[0]; } return array(); } /** * src values of the iframes of a content. * * @return string[] One entry per iframe ('' when it has no src). */ public static function iframeSources( string $content ): array { $content = substr( $content, 0, Assumptions::CONTENT_MAX_BYTES ); $out = array(); if ( preg_match_all( self::IFRAME_TAG, $content, $m ) ) { foreach ( $m[0] as $tag ) { $out[] = preg_match( self::IFRAME_SRC, $tag, $s ) ? $s[1] : ''; } } return $out; } /** Value of a file header ("Plugin Name", "Author"), '' when absent. */ public static function header( string $head, string $name ): string { $head = substr( $head, 0, Assumptions::HEADER_BYTES ); $re = sprintf( self::HEADER, preg_quote( $name, '/' ) ); if ( preg_match( $re, $head, $m ) ) { // Same clean-up as _cleanup_header_comment(): drop a trailing comment closer. $v = trim( $m[1] ); if ( Text::endsWith( $v, '*/' ) ) { $v = trim( substr( $v, 0, -2 ) ); } return $v; } return ''; } }