PluginProbe
Blocks – Reusable Content, Shortcodes & Site Variables / trunk
Blocks – Reusable Content, Shortcodes & Site Variables vtrunk
26.09.03.15 26.08.31.21 26.08.22.20 26.08.22.22 26.08.22.17 26.08.22.13 26.08.21.19 26.08.07.23 26.07.19.14 26.07.13.21 26.07.13.17 26.07.12.13 026.07.07.21 026.07.05.18 026.06.26.20 026.06.26.21 026.06.08.20 026.05.13.14 026.04.29.10 trunk 026.02.22.22 026.03.16.23 026.04.23.13
blocks / includes / signls / src / ObservationSanitizer.php

ObservationSanitizer.php in Blocks – Reusable Content, Shortcodes & Site Variables trunk, at includes/signls/src/ObservationSanitizer.php

192 lines 6.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Declarative rich-observation sanitizer.
4 *
5 * @package signls-sdk
6 * @license GPL-3.0-or-later
7 */
8
9 namespace Signls\Sdk\V1;
10
11 defined( 'ABSPATH' ) || exit;
12
13 final class ObservationSanitizer {
14
15 private const TYPES = array(
16 'object',
17 'list',
18 'map',
19 'bool',
20 'uint',
21 'int',
22 'decimal',
23 'enum',
24 'slug',
25 'version',
26 'text',
27 'url',
28 'sha256',
29 );
30
31 public static function sanitize( array $source, array $schema ): array {
32 if ( 'object' !== self::type( $schema ) ) {
33 return array();
34 }
35 $value = self::node( $source, $schema );
36 return is_array( $value ) ? $value : array();
37 }
38
39 private static function node( $value, array $schema ) {
40 $type = self::type( $schema );
41 $nullable = ! empty( $schema['nullable'] );
42 if ( null === $value && $nullable ) {
43 return null;
44 }
45
46 switch ( $type ) {
47 case 'object':
48 return self::object( $value, $schema );
49 case 'list':
50 return self::list_value( $value, $schema );
51 case 'map':
52 return self::map_value( $value, $schema );
53 case 'bool':
54 return Sanitizer::bool( $value );
55 case 'uint':
56 return Sanitizer::int( $value, 0, self::integer_bound( $schema, 'max', 4294967295 ) );
57 case 'int':
58 return Sanitizer::int(
59 $value,
60 self::integer_bound( $schema, 'min', -2147483648 ),
61 self::integer_bound( $schema, 'max', 2147483647 )
62 );
63 case 'decimal':
64 return Sanitizer::decimal(
65 $value,
66 self::float_bound( $schema, 'min', 0.0 ),
67 self::float_bound( $schema, 'max', 4294967295.0 ),
68 self::integer_bound( $schema, 'scale', 4 )
69 );
70 case 'enum':
71 $allowed = self::allowed( $schema );
72 $fallback = isset( $schema['fallback'] ) && is_string( $schema['fallback'] ) ? $schema['fallback'] : '';
73 return Sanitizer::enum( $value, $allowed, $fallback );
74 case 'slug':
75 return Sanitizer::slug( $value, self::length( $schema, 48 ), self::fallback( $schema ) );
76 case 'version':
77 return Sanitizer::version( $value, self::length( $schema, 20 ) );
78 case 'text':
79 return Sanitizer::text( $value, self::length( $schema, 191 ) );
80 case 'url':
81 return Sanitizer::url( $value, self::length( $schema, 500 ) );
82 case 'sha256':
83 return Sanitizer::sha256( $value );
84 default:
85 return $nullable ? null : '';
86 }
87 }
88
89 private static function object( $value, array $schema ): array {
90 $source = is_array( $value ) ? $value : array();
91 $properties = isset( $schema['properties'] ) && is_array( $schema['properties'] ) ? $schema['properties'] : array();
92 $result = array();
93 foreach ( $properties as $key => $property ) {
94 if ( ! is_string( $key ) || ! is_array( $property ) || ! array_key_exists( $key, $source ) ) {
95 continue;
96 }
97 $result[ $key ] = self::node( $source[ $key ], $property );
98 }
99 return $result;
100 }
101
102 private static function list_value( $value, array $schema ) {
103 $accounted = ! empty( $schema['with_accounting'] );
104 $source = is_array( $value ) ? $value : array();
105 $rows = $accounted && isset( $source['items'] ) && is_array( $source['items'] ) ? $source['items'] : $source;
106 $item = isset( $schema['items'] ) && is_array( $schema['items'] ) ? $schema['items'] : array();
107 $maximum = max( 1, min( 1000, self::integer_bound( $schema, 'max_items', 1 ) ) );
108 $clean = array();
109 foreach ( array_slice( $rows, 0, $maximum ) as $row ) {
110 $clean[] = self::node( $row, $item );
111 }
112
113 $sort_key = isset( $schema['sort_key'] ) && is_string( $schema['sort_key'] ) ? $schema['sort_key'] : '';
114 if ( '' !== $sort_key ) {
115 usort(
116 $clean,
117 static function ( $left, $right ) use ( $sort_key ) {
118 $left_candidate = is_array( $left ) && isset( $left[ $sort_key ] ) ? $left[ $sort_key ] : '';
119 $right_candidate = is_array( $right ) && isset( $right[ $sort_key ] ) ? $right[ $sort_key ] : '';
120 $left_value = is_scalar( $left_candidate ) ? (string) $left_candidate : '';
121 $right_value = is_scalar( $right_candidate ) ? (string) $right_candidate : '';
122 return strcmp( $left_value, $right_value );
123 }
124 );
125 }
126
127 if ( ! $accounted ) {
128 return $clean;
129 }
130 $reported = isset( $source['reported_total'] ) ? Sanitizer::counter( $source['reported_total'] ) : count( $rows );
131 $reported = max( $reported, count( $rows ) );
132 return array(
133 'items' => $clean,
134 'reported_total' => $reported,
135 'truncated' => ! empty( $source['truncated'] ) || $reported > count( $clean ),
136 );
137 }
138
139 private static function map_value( $value, array $schema ): array {
140 $source = is_array( $value ) ? $value : array();
141 $value_rule = isset( $schema['values'] ) && is_array( $schema['values'] ) ? $schema['values'] : array();
142 $maximum = max( 1, min( 1000, self::integer_bound( $schema, 'max_items', 1 ) ) );
143 $key_length = max( 1, min( 100, self::integer_bound( $schema, 'max_key_length', 48 ) ) );
144 $result = array();
145 ksort( $source, SORT_STRING );
146 foreach ( $source as $key => $item ) {
147 $key = Sanitizer::slug( $key, $key_length );
148 if ( '' === $key || isset( $result[ $key ] ) ) {
149 continue;
150 }
151 $result[ $key ] = self::node( $item, $value_rule );
152 if ( count( $result ) >= $maximum ) {
153 break;
154 }
155 }
156 return $result;
157 }
158
159 private static function type( array $schema ): string {
160 $type = isset( $schema['type'] ) && is_string( $schema['type'] ) ? $schema['type'] : '';
161 return in_array( $type, self::TYPES, true ) ? $type : '';
162 }
163
164 private static function allowed( array $schema ): array {
165 $values = isset( $schema['allowed'] ) && is_array( $schema['allowed'] ) ? $schema['allowed'] : array();
166 return array_values(
167 array_filter(
168 $values,
169 static function ( $value ) {
170 return is_string( $value );
171 }
172 )
173 );
174 }
175
176 private static function fallback( array $schema ): string {
177 return isset( $schema['fallback'] ) && is_string( $schema['fallback'] ) ? $schema['fallback'] : '';
178 }
179
180 private static function length( array $schema, int $fallback ): int {
181 return max( 1, min( 4096, self::integer_bound( $schema, 'max_length', $fallback ) ) );
182 }
183
184 private static function integer_bound( array $schema, string $key, int $fallback ): int {
185 return isset( $schema[ $key ] ) && is_int( $schema[ $key ] ) ? $schema[ $key ] : $fallback;
186 }
187
188 private static function float_bound( array $schema, string $key, float $fallback ): float {
189 return isset( $schema[ $key ] ) && is_numeric( $schema[ $key ] ) ? (float) $schema[ $key ] : $fallback;
190 }
191 }
192