Plugin Directory

source: jetpack/trunk/jetpack_vendor/automattic/jetpack-waf/src/class-waf-runtime.php @ 3056649

Last change on this file since 3056649 was 3056649, checked in by zinigor, 4 months ago

Updating trunk to version 13.2.2

File size: 23.0 KB
Line 
1<?php
2/**
3 * Runtime for Jetpack Waf
4 *
5 * @package automattic/jetpack-waf
6 */
7
8namespace Automattic\Jetpack\Waf;
9
10use Automattic\Jetpack\IP\Utils as IP_Utils;
11
12require_once __DIR__ . '/functions.php';
13
14// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This class is all about sanitizing input.
15
16/**
17 * The environment variable that defined the WAF running mode.
18 *
19 * @var string JETPACK_WAF_MODE
20 */
21
22/**
23 * Waf_Runtime class
24 *
25 * @template Target as array{ only?: string[], except?: string[], count?: boolean }
26 * @template TargetBag as array<string, Target>
27 */
28class Waf_Runtime {
29        /**
30         * If used, normalize_array_targets() will just return the number of matching values, instead of the values themselves.
31         */
32        const NORMALIZE_ARRAY_COUNT = 1;
33        /**
34         * If used, normalize_array_targets() will apply "only" and "except" filters to the values of the source array, instead of the keys.
35         */
36        const NORMALIZE_ARRAY_MATCH_VALUES = 2;
37
38        /**
39         * Last rule.
40         *
41         * @var string
42         */
43        public $last_rule = '';
44        /**
45         * Matched vars.
46         *
47         * @var array
48         */
49        public $matched_vars = array();
50        /**
51         * Matched var.
52         *
53         * @var string
54         */
55        public $matched_var = '';
56        /**
57         * Matched var names.
58         *
59         * @var array
60         */
61        public $matched_var_names = array();
62        /**
63         * Matched var name.
64         *
65         * @var string
66         */
67        public $matched_var_name = '';
68
69        /**
70         * State.
71         *
72         * @var array
73         */
74        private $state = array();
75        /**
76         * Metadata.
77         *
78         * @var array
79         */
80        private $metadata = array();
81
82        /**
83         * Transforms.
84         *
85         * @var Waf_Transforms
86         */
87        private $transforms;
88        /**
89         * Operators.
90         *
91         * @var Waf_Operators
92         */
93        private $operators;
94
95        /**
96         * The request
97         *
98         * @var Waf_Request
99         */
100        private $request;
101
102        /**
103         * Rules to remove.
104         *
105         * @var array[]
106         */
107        private $rules_to_remove = array(
108                'id'  => array(),
109                'tag' => array(),
110        );
111
112        /**
113         * Targets to remove.
114         *
115         * @var array[]
116         */
117        private $targets_to_remove = array(
118                'id'  => array(),
119                'tag' => array(),
120        );
121
122        /**
123         * Constructor method.
124         *
125         * @param Waf_Transforms $transforms Transforms.
126         * @param Waf_Operators  $operators  Operators.
127         * @param Waf_Request?   $request    Information about the request.
128         */
129        public function __construct( $transforms, $operators, $request = null ) {
130                $this->transforms = $transforms;
131                $this->operators  = $operators;
132                $this->request    = null === $request
133                        ? new Waf_Request()
134                        : $request;
135        }
136
137        /**
138         * Rule removed method.
139         *
140         * @param string   $id Ids.
141         * @param string[] $tags Tags.
142         */
143        public function rule_removed( $id, $tags ) {
144                if ( isset( $this->rules_to_remove['id'][ $id ] ) ) {
145                        return true;
146                }
147                foreach ( $tags as $tag ) {
148                        if ( isset( $this->rules_to_remove['tag'][ $tag ] ) ) {
149                                return true;
150                        }
151                }
152                return false;
153        }
154
155        /**
156         * Update Targets.
157         *
158         * @param array    $targets Targets.
159         * @param string   $rule_id Rule id.
160         * @param string[] $rule_tags Rule tags.
161         */
162        public function update_targets( $targets, $rule_id, $rule_tags ) {
163                $updates = array();
164                // look for target updates based on the rule's ID.
165                if ( isset( $this->targets_to_remove['id'][ $rule_id ] ) ) {
166                        foreach ( $this->targets_to_remove['id'][ $rule_id ] as $name => $props ) {
167                                $updates[] = array( $name, $props );
168                        }
169                }
170                // look for target updates based on the rule's tags.
171                foreach ( $rule_tags as $tag ) {
172                        if ( isset( $this->targets_to_remove['tag'][ $tag ] ) ) {
173                                foreach ( $this->targets_to_remove['tag'][ $tag ] as $name => $props ) {
174                                        $updates[] = array( $name, $props );
175                                }
176                        }
177                }
178                // apply any found target updates.
179
180                foreach ( $updates as list( $name, $props ) ) {
181                        if ( isset( $targets[ $name ] ) ) {
182                                // we only need to remove targets that exist.
183                                if ( true === $props ) {
184                                        // if the entire target is being removed, remove it.
185                                        unset( $targets[ $name ] );
186                                } else {
187                                        // otherwise just mark single props to ignore.
188                                        $targets[ $name ]['except'] = array_merge(
189                                                isset( $targets[ $name ]['except'] ) ? $targets[ $name ]['except'] : array(),
190                                                $props
191                                        );
192                                }
193                        }
194                }
195                return $targets;
196        }
197
198        /**
199         * Return TRUE if at least one of the targets matches the rule.
200         *
201         * @param string[]  $transforms One of the transform methods defined in the Jetpack Waf_Transforms class.
202         * @param TargetBag $targets Targets.
203         * @param string    $match_operator Match operator.
204         * @param mixed     $match_value Match value.
205         * @param bool      $match_not Match not.
206         * @param bool      $capture Capture.
207         * @return bool
208         */
209        public function match_targets( $transforms, $targets, $match_operator, $match_value, $match_not, $capture = false ) {
210                $this->matched_vars      = array();
211                $this->matched_var_names = array();
212                $this->matched_var       = '';
213                $this->matched_var_name  = '';
214                $match_found             = false;
215
216                // get values.
217                $values = $this->normalize_targets( $targets );
218
219                // apply transforms.
220                foreach ( $transforms as $t ) {
221                        foreach ( $values as &$v ) {
222                                $v['value'] = $this->transforms->$t( $v['value'] );
223                        }
224                }
225                unset( $v );
226                // pass each target value to the operator to find any that match.
227                $matched  = array();
228                $captures = array();
229                foreach ( $values as $v ) {
230                        $match     = $this->operators->{$match_operator}( $v['value'], $match_value );
231                        $did_match = false !== $match;
232                        if ( $match_not !== $did_match ) {
233                                // If either:
234                                // - rule is negated ("not" flag set) and the target was not matched
235                                // - rule not negated and the target was matched
236                                // then this is considered a match.
237                                $match_found               = true;
238                                $this->matched_var_names[] = $v['source'];
239                                $this->matched_vars[]      = $v['value'];
240                                $this->matched_var_name    = end( $this->matched_var_names );
241                                $this->matched_var         = end( $this->matched_vars );
242                                $matched[]                 = array( $v, $match );
243                                // Set any captured matches into state if the rule has the "capture" flag.
244                                if ( $capture ) {
245                                        $captures = is_array( $match ) ? $match : array( $match );
246                                        foreach ( array_slice( $captures, 0, 10 )  as $i => $c ) {
247                                                $this->set_var( "tx.$i", $c );
248                                        }
249                                }
250                        }
251                }
252
253                return $match_found;
254        }
255
256        /**
257         * Block.
258         *
259         * @param string $action Action.
260         * @param string $rule_id Rule id.
261         * @param string $reason Block reason.
262         * @param int    $status_code Http status code.
263         */
264        public function block( $action, $rule_id, $reason, $status_code = 403 ) {
265                if ( ! $reason ) {
266                        $reason = "rule $rule_id";
267                } else {
268                        $reason = $this->sanitize_output( $reason );
269                }
270
271                $this->write_blocklog( $rule_id, $reason );
272                error_log( "Jetpack WAF Blocked Request\t$action\t$rule_id\t$status_code\t$reason" );
273                header( "X-JetpackWAF-Blocked: $status_code - rule $rule_id" );
274                if ( defined( 'JETPACK_WAF_MODE' ) && 'normal' === JETPACK_WAF_MODE ) {
275                        $protocol = isset( $_SERVER['SERVER_PROTOCOL'] ) ? wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) : 'HTTP';
276                        header( $protocol . ' 403 Forbidden', true, $status_code );
277                        die( "rule $rule_id - reason $reason" );
278                }
279        }
280
281        /**
282         * Write block logs. We won't write to the file if it exceeds 100 mb.
283         *
284         * @param string $rule_id Rule id.
285         * @param string $reason Block reason.
286         */
287        public function write_blocklog( $rule_id, $reason ) {
288                $log_data              = array();
289                $log_data['rule_id']   = $rule_id;
290                $log_data['reason']    = $reason;
291                $log_data['timestamp'] = gmdate( 'Y-m-d H:i:s' );
292
293                if ( defined( 'JETPACK_WAF_SHARE_DATA' ) && JETPACK_WAF_SHARE_DATA ) {
294                        $file_path   = JETPACK_WAF_DIR . '/waf-blocklog';
295                        $file_exists = file_exists( $file_path );
296
297                        if ( ! $file_exists || filesize( $file_path ) < ( 100 * 1024 * 1024 ) ) {
298                                $fp = fopen( $file_path, 'a+' );
299
300                                if ( $fp ) {
301                                        try {
302                                                fwrite( $fp, json_encode( $log_data ) . "\n" );
303                                        } finally {
304                                                fclose( $fp );
305                                        }
306                                }
307                        }
308                }
309
310                $this->write_blocklog_row( $log_data );
311        }
312
313        /**
314         * Write block logs to database.
315         *
316         * @param array $log_data Log data.
317         */
318        private function write_blocklog_row( $log_data ) {
319                $conn = $this->connect_to_wordpress_db();
320
321                if ( ! $conn ) {
322                        return;
323                }
324
325                global $table_prefix;
326
327                $statement = $conn->prepare( "INSERT INTO {$table_prefix}jetpack_waf_blocklog(reason,rule_id, timestamp) VALUES (?, ?, ?)" );
328                if ( false !== $statement ) {
329                        $statement->bind_param( 'sis', $log_data['reason'], $log_data['rule_id'], $log_data['timestamp'] );
330                        $statement->execute();
331
332                        if ( $conn->insert_id > 100 ) {
333                                $conn->query( "DELETE FROM {$table_prefix}jetpack_waf_blocklog ORDER BY log_id LIMIT 1" );
334                        }
335                }
336        }
337
338        /**
339         * Connect to WordPress database.
340         */
341        private function connect_to_wordpress_db() {
342                if ( ! file_exists( JETPACK_WAF_WPCONFIG ) ) {
343                        return;
344                }
345
346                require_once JETPACK_WAF_WPCONFIG;
347                $conn = new \mysqli( DB_HOST, DB_USER, DB_PASSWORD, DB_NAME ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__mysqli
348
349                if ( $conn->connect_error ) {
350                        error_log( 'Could not connect to the database:' . $conn->connect_error );
351                        return null;
352                }
353
354                return $conn;
355        }
356
357        /**
358         * Redirect.
359         *
360         * @param string $rule_id Rule id.
361         * @param string $url Url.
362         */
363        public function redirect( $rule_id, $url ) {
364                error_log( "Jetpack WAF Redirected Request.\tRule:$rule_id\t$url" );
365                header( "Location: $url" );
366                exit;
367        }
368
369        /**
370         * Flag rule for removal.
371         *
372         * @param string $prop Prop.
373         * @param string $value Value.
374         */
375        public function flag_rule_for_removal( $prop, $value ) {
376                if ( 'id' === $prop ) {
377                        $this->rules_to_remove['id'][ $value ] = true;
378        ��       } else {
379                        $this->rules_to_remove['tag'][ $value ] = true;
380                }
381        }
382
383        /**
384         * Flag target for removal.
385         *
386         * @param string $id_or_tag Id or tag.
387         * @param string $id_or_tag_value Id or tag value.
388         * @param string $name Name.
389         * @param string $prop Prop.
390         */
391        public function flag_target_for_removal( $id_or_tag, $id_or_tag_value, $name, $prop = null ) {
392                if ( null === $prop ) {
393                        $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ] = true;
394                } elseif (
395                        ! isset( $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ] )
396                        // if the entire target is already being removed then it would be redundant to remove a single property.
397                        || true !== $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ]
398                ) {
399                        $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ][] = $prop;
400                }
401        }
402
403        /**
404         * Get variable value.
405         *
406         * @param string $key Key.
407         */
408        public function get_var( $key ) {
409                return isset( $this->state[ $key ] )
410                        ? $this->state[ $key ]
411                        : '';
412        }
413
414        /**
415         * Set variable value.
416         *
417         * @param string $key Key.
418         * @param string $value Value.
419         */
420        public function set_var( $key, $value ) {
421                $this->state[ $key ] = $value;
422        }
423
424        /**
425         * Increment variable.
426         *
427         * @param string $key Key.
428         * @param mixed  $value Value.
429         */
430        public function inc_var( $key, $value ) {
431                if ( ! isset( $this->state[ $key ] ) ) {
432                        $this->state[ $key ] = 0;
433                }
434                $this->state[ $key ] += floatval( $value );
435        }
436
437        /**
438         * Decrement variable.
439         *
440         * @param string $key Key.
441         * @param mixed  $value Value.
442         */
443        public function dec_var( $key, $value ) {
444                if ( ! isset( $this->state[ $key ] ) ) {
445                        $this->state[ $key ] = 0;
446                }
447                $this->state[ $key ] -= floatval( $value );
448        }
449
450        /**
451         * Unset variable.
452         *
453         * @param string $key Key.
454         */
455        public function unset_var( $key ) {
456                unset( $this->state[ $key ] );
457        }
458
459        /**
460         * A cache of metadata about the incoming request.
461         *
462         * @param string $key The type of metadata to request ('headers', 'request_method', etc.).
463         */
464        public function meta( $key ) {
465                if ( ! isset( $this->metadata[ $key ] ) ) {
466                        $value = null;
467                        switch ( $key ) {
468                                case 'headers':
469                                        $value = $this->request->get_headers();
470                                        break;
471                                case 'headers_names':
472                                        $value = $this->args_names( $this->meta( 'headers' ) );
473                                        break;
474                                case 'request_method':
475                                        $value = $this->request->get_method();
476                                        break;
477                                case 'request_protocol':
478                                        $value = $this->request->get_protocol();
479                                        break;
480                                case 'request_uri':
481                                        $value = $this->request->get_uri( false );
482                                        break;
483                                case 'request_uri_raw':
484                                        $value = $this->request->get_uri( true );
485                                        break;
486                                case 'request_filename':
487                                        $value = $this->request->get_filename();
488                                        break;
489                                case 'request_line':
490                                        $value = sprintf(
491                                                '%s %s %s',
492                                                $this->request->get_method(),
493                                                $this->request->get_uri( false ),
494                                                $this->request->get_protocol()
495                                        );
496                                        break;
497                                case 'request_basename':
498                                        $value = $this->request->get_basename();
499                                        break;
500                                case 'request_body':
501                                        $value = $this->request->get_body();
502                                        break;
503                                case 'query_string':
504                                        $value = $this->request->get_query_string();
505                                        break;
506                                case 'args_get':
507                                        $value = $this->request->get_get_vars();
508                                        break;
509                                case 'args_get_names':
510                                        $value = $this->args_names( $this->meta( 'args_get' ) );
511                                        break;
512                                case 'args_post':
513                                        $value = $this->request->get_post_vars();
514                                        break;
515                                case 'args_post_names':
516                                        $value = $this->args_names( $this->meta( 'args_post' ) );
517                                        break;
518                                case 'args':
519                                        $value = array_merge( $this->meta( 'args_get' ), $this->meta( 'args_post' ) );
520                                        break;
521                                case 'args_names':
522                                        $value = $this->args_names( $this->meta( 'args' ) );
523                                        break;
524                                case 'request_cookies':
525                                        $value = $this->request->get_cookies();
526                                        break;
527                                case 'request_cookies_names':
528                                        $value = $this->args_names( $this->meta( 'request_cookies' ) );
529                                        break;
530                                case 'files':
531                                        $value = array();
532                                        foreach ( $this->request->get_files() as $f ) {
533                                                $value[] = array( $f['name'], $f['filename'] );
534                                        }
535                                        break;
536                                case 'files_names':
537                                        $value = $this->args_names( $this->meta( 'files' ) );
538                                        break;
539                        }
540                        $this->metadata[ $key ] = $value;
541                }
542
543                return $this->metadata[ $key ];
544        }
545
546        /**
547         * State values.
548         *
549         * @param string $prefix Prefix.
550         */
551        private function state_values( $prefix ) {
552                $output = array();
553                $len    = strlen( $prefix );
554                foreach ( $this->state as $k => $v ) {
555                        if ( 0 === stripos( $k, $prefix ) ) {
556                                $output[ substr( $k, $len ) ] = $v;
557                        }
558                }
559
560                return $output;
561        }
562
563        /**
564         * Change a string to all lowercase and replace spaces and underscores with dashes.
565         *
566         * @param string $name Name.
567         * @return string
568         */
569        public function normalize_header_name( $name ) {
570                return str_replace( array( ' ', '_' ), '-', strtolower( $name ) );
571        }
572
573        /**
574         * Get match-able values from a collection of targets.
575         *
576         * This function expects an associative array of target items, and returns an array of possible values from those targets that can be used to match against.
577         * The key is the lowercase target name (i.e. `args`, `request_headers`, etc) - see https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v3.x)#Variables
578         * The value is an associative array of options that define how to narrow down the returned values for that target if it's an array (ARGS, for example). The possible options are:
579         *   count:  If `true`, then the returned value will a count of how many matched targets were found, rather then the actual values of those targets.
580         *           For example, &ARGS_GET will return the number of keys the query string.
581         *   only:   If specified, then only values in that target that match the given key will be returned.
582         *           For example, ARGS_GET:id|ARGS_GET:/^name/ will only return the values for `$_GET['id']` and any key in `$_GET` that starts with `name`
583         *   except: If specified, then values in that target will be left out from the returned values (even if they were included in an `only` option)
584         *           For example, ARGS_GET|!ARGS_GET:z will return every value from `$_GET` except for `$_GET['z']`.
585         *
586         * This function will return an array of associative arrays. Each with:
587         *   name:   The target name that this value came from (i.e. the key in the input `$targets` argument )
588         *   source: For targets that are associative arrays (like ARGS), this will be the target name AND the key in that target (i.e. "args:z" for ARGS:z)
589         *   value:  The value that was found in the associated target.
590         *
591         * @param TargetBag $targets An assoc. array with keys that are target name(s) and values are options for how to process that target (include/exclude rules, whether to return values or counts).
592         * @return array{ name: string, source: string, value: mixed }
593         */
594        public function normalize_targets( $targets ) {
595                $return = array();
596                foreach ( $targets as $k => $v ) {
597                        $count_only = isset( $v['count'] ) ? self::NORMALIZE_ARRAY_COUNT : 0;
598                        $only       = isset( $v['only'] ) ? $v['only'] : array();
599                        $except     = isset( $v['except'] ) ? $v['except'] : array();
600                        $_k         = strtolower( $k );
601                        switch ( $_k ) {
602                                case 'request_headers':
603                                        $this->normalize_array_target(
604                                                // get the headers that came in with this request
605                                                $this->meta( 'headers' ),
606                                                // ensure only and exclude filters are normalized
607                                                array_map( array( $this->request, 'normalize_header_name' ), $only ),
608                                                array_map( array( $this->request, 'normalize_header_name' ), $except ),
609                                                $k,
610                                                $return,
611                                                // flags
612                                                $count_only
613                                        );
614                                        continue 2;
615                                case 'request_headers_names':
616                                        $this->normalize_array_target( $this->meta( 'headers_names' ), $only, $except, $k, $return, $count_only | self::NORMALIZE_ARRAY_MATCH_VALUES );
617                                        continue 2;
618                                case 'request_method':
619                                case 'request_protocol':
620                                case 'request_uri':
621                                case 'request_uri_raw':
622                                case 'request_filename':
623                                case 'request_basename':
624                                case 'request_body':
625                                case 'query_string':
626                                case 'request_line':
627                                        $v = $this->meta( $_k );
628                                        break;
629                                case 'tx':
630                                case 'ip':
631                                        $this->normalize_array_target( $this->state_values( "$k." ), $only, $except, $k, $return, $count_only );
632                                        continue 2;
633                                case 'request_cookies':
634                                case 'args':
635                                case 'args_get':
636                                case 'args_post':
637                                case 'files':
638                                        $this->normalize_array_target( $this->meta( $_k ), $only, $except, $k, $return, $count_only );
639                                        continue 2;
640                                case 'request_cookies_names':
641                                case 'args_names':
642                                case 'args_get_names':
643                                case 'args_post_names':
644                                case 'files_names':
645                                        // get the "full" data (for 'args_names' get data for 'args') and stripe it down to just the key names
646                                        $data = array_map(
647                                                function ( $item ) {
648                                                        return $item[0]; },
649                                                $this->meta( substr( $_k, 0, -6 ) )
650                                        );
651                                        $this->normalize_array_target( $data, $only, $except, $k, $return, $count_only | self::NORMALIZE_ARRAY_MATCH_VALUES );
652                                        continue 2;
653                                default:
654                                        var_dump( 'Unknown target', $k, $v );
655                                        exit;
656                        }
657                        $return[] = array(
658                                'name'   => $k,
659                                'value'  => $v,
660                                'source' => $k,
661                        );
662                }
663
664                return $return;
665        }
666
667        /**
668         * Verifies if the IP from the current request is in an array.
669         *
670         * @param array $array Array of IP addresses to verify the request IP against.
671         * @return bool
672         */
673        public function is_ip_in_array( $array ) {
674                $real_ip      = $this->request->get_real_user_ip_address();
675                $array_length = count( $array );
676
677                for ( $i = 0; $i < $array_length; $i++ ) {
678                        // Check if the IP matches a provided range.
679                        $range = explode( '-', $array[ $i ] );
680                        if ( count( $range ) === 2 ) {
681                                if ( IP_Utils::ip_address_is_in_range( $real_ip, $range[0], $range[1] ) ) {
682                                        return true;
683                                }
684                                continue;
685                        }
686
687                        // Check if the IP is an exact match.
688                        if ( $real_ip === $array[ $i ] ) {
689                                return true;
690                        }
691                }
692
693                return false;
694        }
695
696        /**
697         * Extract values from an associative array, potentially applying filters and/or counting results.
698         *
699         * @param array{ 0: string, 1: scalar }|scalar[] $source      The source assoc. array of values (i.e. $_GET, $_SERVER, etc.).
700         * @param string[]                               $only        Only include the values for these keys in the output.
701         * @param string[]                               $excl        Never include the values for these keys in the output.
702         * @param string                                 $name        The name of this target (see https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v3.x)#Variables).
703         * @param array                                  $results     Array to add output values to, will be modified by this method.
704         * @param int                                    $flags       Any of the NORMALIZE_ARRAY_* constants defined at the top of the class.
705         */
706        private function normalize_array_target( $source, $only, $excl, $name, &$results, $flags = 0 ) {
707                $output   = array();
708                $has_only = isset( $only[0] );
709                $has_excl = isset( $excl[0] );
710
711                foreach ( $source as $source_key => $source_val ) {
712                        if ( is_array( $source_val ) ) {
713                                // if $source_val looks like a tuple from flatten_array(), then use the tuple as the key and value
714                                $source_key = $source_val[0];
715                                $source_val = $source_val[1];
716                        }
717                        $filter_match = ( $flags & self::NORMALIZE_ARRAY_MATCH_VALUES ) > 0 ? $source_val : $source_key;
718                        // if this key is on the "exclude" list, skip it
719                        if ( $has_excl && $this->key_matches( $filter_match, $excl ) ) {
720                                continue;
721                        }
722                        // if this key isn't in our "only" list, then skip it
723                        if ( $has_only && ! $this->key_matches( $filter_match, $only ) ) {
724                                continue;
725                        }
726                        // otherwise add this key/value to our output
727                        $output[] = array( $source_key, $source_val );
728                }
729
730                if ( ( $flags & self::NORMALIZE_ARRAY_COUNT ) > 0 ) {
731                        // If we've been told to just count the values, then just count them.
732                        $results[] = array(
733                                'name'   => (string) $name,
734                                'value'  => count( $output ),
735                                'source' => '&' . $name,
736                        );
737                } else {
738                        foreach ( $output as list( $item_name, $item_value ) ) {
739                                $results[] = array(
740                                        'name'   => (string) $item_name,
741                                        'value'  => $item_value,
742                                        'source' => "$name:$item_name",
743                                );
744                        }
745                }
746
747                return $results;
748        }
749
750        /**
751         * Given an array of tuples - probably from flatten_array() - return a new array
752         * consisting of only the first value (the key name) from each tuple.
753         *
754         * @param array{0:string, 1:scalar}[] $flat_array An array of tuples.
755         * @return string[]
756         */
757        private function args_names( $flat_array ) {
758                $names = array_map(
759                        function ( $tuple ) {
760                                return $tuple[0];
761                        },
762                        $flat_array
763                );
764                return array_unique( $names );
765        }
766
767        /**
768         * Return whether or not a given $input key matches one of the given $patterns.
769         *
770         * @param string   $input    Key name to test against patterns.
771         * @param string[] $patterns Patterns to test key name with.
772         * @return bool
773         */
774        private function key_matches( $input, $patterns ) {
775                foreach ( $patterns as $p ) {
776                        if ( '/' === $p[0] ) {
777                                if ( 1 === preg_match( $p, $input ) ) {
778                                        return true;
779                                }
780                        } elseif ( 0 === strcasecmp( $p, $input ) ) {
781                                return true;
782                        }
783                }
784
785                return false;
786        }
787
788        /**
789         * Sanitize output generated from the request that was blocked.
790         *
791         * @param string $output Output to sanitize.
792         */
793        public function sanitize_output( $output ) {
794                $url_decoded_output   = rawurldecode( $output );
795                $html_entities_output = htmlentities( $url_decoded_output, ENT_QUOTES, 'UTF-8' );
796                // @phpcs:disable Squiz.Strings.DoubleQuoteUsage.NotRequired
797                $escapers     = array( "\\", "/", "\"", "\n", "\r", "\t", "\x08", "\x0c" );
798                $replacements = array( "\\\\", "\\/", "\\\"", "\\n", "\\r", "\\t", "\\f", "\\b" );
799                // @phpcs:enable Squiz.Strings.DoubleQuoteUsage.NotRequired
800
801                return( str_replace( $escapers, $replacements, $html_entities_output ) );
802        }
803}
Note: See TracBrowser for help on using the repository browser.