PluginProbe ʕ •ᴥ•ʔ
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI / 3.6.0
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI v3.6.0
3.6.0 3.5.3 3.5.2 3.5.1 3.5.0 3.4.8 3.4.7 3.4.6 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.5.1 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.10 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.6.1 1.6.7 1.7.0 1.7.0.1 1.7.0.2 1.7.0.3 1.7.1 1.7.2 1.7.2.1 1.7.2.2 1.7.3 1.7.4 1.7.5 1.7.5.1 1.7.5.2 1.7.6 1.7.7 1.7.7.1 1.7.7.2 1.7.8 1.7.9 1.8.0 1.8.0.1 1.8.1 1.8.2 1.8.2.1 1.8.2.2 1.8.2.3 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.0.1 1.9.1 1.9.2 1.9.3 1.9.4 1.9.4.1 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.0.1 2.0.1 2.0.2 2.0.3 2.0.3.1 2.0.4 2.0.4.1 2.0.5 2.0.6 2.0.7 2.0.8 2.0.8.1 2.0.9 3.0.0 3.0.0.1 3.0.1 3.0.2 3.0.3 3.0.3.1 3.0.4 3.0.4.1 3.0.4.2 3.0.5 3.0.5.1 3.0.5.2 3.0.6 3.0.6.1 3.0.7.1 3.0.8 3.0.8.1 3.0.9 3.0.9.1 3.0.9.2 3.0.9.3 3.0.9.4 3.0.9.5 3.1.0 3.1.1 3.1.2 3.2.0 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.3.0 3.4.0 3.4.1 3.4.2 3.4.2.1 3.4.3 3.4.4 3.4.5 trunk 1.0 1.0.1 1.0.2 1.0.3
everest-forms / addons / StyleCustomizer / V2 / Sanitizer.php
everest-forms / addons / StyleCustomizer / V2 Last commit date
assets 1 week ago BuilderPanel.php 1 week ago Compiler.php 1 week ago Engine.php 1 week ago FrontendEnqueue.php 1 week ago Migrator.php 1 week ago Palettes.php 1 week ago PreviewDraft.php 1 week ago RestController.php 1 week ago Sanitizer.php 1 week ago Schema.php 1 week ago Templates.php 1 week ago
Sanitizer.php
559 lines
1 <?php
2 /**
3 * Style Customizer v2 — Sanitizer.
4 *
5 * The single write-path guard: no value reaches the stored record (and therefore the
6 * compiler) unless it is valid for its token's type. Driven entirely by {@see Schema}
7 * so it can never drift from the control set.
8 *
9 * Record shape:
10 * array(
11 * 'schema_version' => 1,
12 * 'tokens' => array( '<key>' => array( 'desktop' => v, 'tablet' => v, 'mobile' => v ) ),
13 * 'palette' => '<palette-id>',
14 * 'template' => '<template-id>',
15 * 'custom_css' => '<css>',
16 * '_updated_at' => <timestamp>,
17 * )
18 * Only `desktop` is kept for non-responsive tokens; `tablet`/`mobile` survive only on the
19 * responsive (spacing) tokens.
20 *
21 * @package EverestForms\Addons\StyleCustomizer\V2
22 * @since x.x.x
23 */
24
25 namespace EverestForms\Addons\StyleCustomizer\V2;
26
27 defined( 'ABSPATH' ) || exit;
28
29 /**
30 * Schema-driven sanitizer.
31 */
32 final class Sanitizer {
33
34 /**
35 * Sanitize a full v2 record.
36 *
37 * @param array $record Raw (untrusted) record.
38 * @param bool $check_contrast Also run {@see ensure_text_contrast()}. Only the AI style
39 * launcher opts in; a normal save must never silently revert a
40 * colour a person deliberately chose.
41 * @return array Clean record, always stamped with the current schema version.
42 */
43 public static function sanitize_record( $record, $check_contrast = false ) {
44 $record = is_array( $record ) ? $record : array();
45 $clean = array( 'schema_version' => Schema::version() );
46
47 $pro_active = Engine::pro_active();
48
49 // Palette first: on a non-Pro site the palette-driven ("free") tokens below are DERIVED
50 // from this id, never trusted raw from the client — see the loop below for why.
51 $palette_id = isset( $record['palette'] ) ? self::sanitize_palette_id( $record['palette'] ) : '';
52 if ( isset( $record['palette'] ) ) {
53 $clean['palette'] = $palette_id;
54 }
55 $free_derived = ( ! $pro_active && '' !== $palette_id ) ? Schema::palette_token_values( $palette_id ) : array();
56
57 $in_tokens = isset( $record['tokens'] ) && is_array( $record['tokens'] ) ? $record['tokens'] : array();
58 $out_tokens = array();
59 foreach ( Schema::tokens() as $token ) {
60 $key = $token['key'];
61
62 if ( ! $pro_active && isset( $token['tier'] ) && 'free' === $token['tier'] ) {
63 // Free tier: the ONLY sanctioned customisation is picking one of the 2 free
64 // palettes — never trust a raw client-submitted value for these keys, or a
65 // crafted request (an AI response, a hand-built REST call) could paint an
66 // arbitrary custom colour scheme through keys that exist only to let the
67 // palette picker render at all (see EVF-2708).
68 if ( isset( $free_derived[ $key ] ) ) {
69 $out_tokens[ $key ] = self::sanitize_token( $token, $free_derived[ $key ] );
70 }
71 continue;
72 }
73
74 if ( ! array_key_exists( $key, $in_tokens ) ) {
75 continue; // Absent → the compiler falls back to the schema default.
76 }
77 // A pro-tier value can never be persisted on a site without Pro.
78 if ( ! $pro_active && isset( $token['tier'] ) && 'pro' === $token['tier'] ) {
79 continue;
80 }
81 $out_tokens[ $key ] = self::sanitize_token( $token, $in_tokens[ $key ] );
82 }
83 if ( $check_contrast ) {
84 self::ensure_text_contrast( $out_tokens );
85 }
86
87 $clean['tokens'] = $out_tokens;
88
89 if ( isset( $record['template'] ) ) {
90 $template = sanitize_key( $record['template'] );
91 // A Pro template (or a user/custom template) can't be selected without Pro.
92 if ( ! $pro_active && ! Templates::is_free_template_id( $template ) ) {
93 $template = '';
94 }
95 $clean['template'] = $template;
96 }
97 if ( isset( $record['custom_css'] ) ) {
98 $clean['custom_css'] = self::sanitize_css( $record['custom_css'] );
99 }
100
101 $clean['_updated_at'] = time();
102
103 return $clean;
104 }
105
106 /**
107 * Re-attach any token the OLD record already implied but this save's clean output doesn't,
108 * if Pro is currently inactive — so a save made while Pro is merely undetected doesn't
109 * silently erase pro-tier customisation the form already had. The old record may still be
110 * legacy shape, so it is re-derived through {@see Migrator::migrate_record()} when needed.
111 *
112 * This restores stale FREE-tier (palette-driven) tokens too, not just pro-tier ones: since
113 * {@see self::sanitize_record()} now only ever DERIVES those from a registered free palette id
114 * (never trusting a raw client value — see EVF-2708), a legacy form migrating to v2 with a
115 * bespoke v1 custom colour set that matches no registered palette would otherwise have that
116 * migrated colour dropped on its very first v2 save. Restoring it here (from the same
117 * migrated-legacy `$old_record`) keeps that one-time migration lossless without reopening the
118 * free/pro bypass, since this only ever re-attaches a value the form ALREADY had — it can
119 * never introduce one a fresh crafted request invented this turn.
120 *
121 * @param array $clean Freshly sanitized record (from self::sanitize_record()).
122 * @param array $old_record The record as it was stored before this save (legacy or v2 shape).
123 * @return array $clean, with any stale token the new save doesn't mention restored.
124 */
125 public static function preserve_stale_pro_tokens( array $clean, array $old_record ) {
126 if ( Engine::pro_active() ) {
127 return $clean;
128 }
129
130 if ( Engine::is_v2_record( $old_record ) ) {
131 $old_tokens = isset( $old_record['tokens'] ) && is_array( $old_record['tokens'] ) ? $old_record['tokens'] : array();
132 } elseif ( ! empty( $old_record ) ) {
133 $migrated = Migrator::migrate_record( $old_record );
134 $old_tokens = isset( $migrated['tokens'] ) && is_array( $migrated['tokens'] ) ? $migrated['tokens'] : array();
135 } else {
136 $old_tokens = array();
137 }
138
139 foreach ( $old_tokens as $old_key => $old_value ) {
140 if ( ! isset( $clean['tokens'][ $old_key ] ) ) {
141 $clean['tokens'][ $old_key ] = $old_value;
142 }
143 }
144
145 return $clean;
146 }
147
148 /**
149 * Drop an AI-generated text-colour override that would be unreadable against the form's own
150 * background (e.g. white label.color with no matching dark wrap.bg). Threshold is low (2:1)
151 * to catch only "vanishes entirely" pairings — a human's deliberate low-but-visible choice
152 * must never be reverted, so only {@see RestController::ai_style()} opts into this.
153 *
154 * @param array $out_tokens Sanitized token map, keyed by token key, modified in place.
155 */
156 private static function ensure_text_contrast( array &$out_tokens ) {
157 // A background image covers wrap.bg visually, so skip the check rather than judge a
158 // colour against a background it isn't actually rendered against.
159 if ( ! empty( $out_tokens['wrap.bgImage']['desktop'] ) ) {
160 return;
161 }
162
163 $bg = isset( $out_tokens['wrap.bg']['desktop'] ) ? $out_tokens['wrap.bg']['desktop'] : '#ffffff';
164
165 foreach ( array( 'label.color', 'sub.color', 'desc.color', 'title.color' ) as $key ) {
166 if ( ! isset( $out_tokens[ $key ]['desktop'] ) ) {
167 continue;
168 }
169 $fg = $out_tokens[ $key ]['desktop'];
170 if ( ! is_string( $fg ) || ! is_string( $bg ) ) {
171 continue;
172 }
173 if ( self::contrast_ratio( $fg, $bg ) < 2.0 ) {
174 unset( $out_tokens[ $key ] ); // Falls back to the token's own (coherent) schema default.
175 }
176 }
177 }
178
179 /**
180 * WCAG relative-luminance contrast ratio between two hex colours (1 = identical, 21 = max).
181 *
182 * @param string $hex1 First colour, e.g. "#ffffff".
183 * @param string $hex2 Second colour.
184 * @return float
185 */
186 private static function contrast_ratio( $hex1, $hex2 ) {
187 $l1 = self::relative_luminance( $hex1 );
188 $l2 = self::relative_luminance( $hex2 );
189 if ( null === $l1 || null === $l2 ) {
190 return 21.0; // Unparseable colour (e.g. a CSS var/keyword) — don't second-guess it.
191 }
192 $lighter = max( $l1, $l2 );
193 $darker = min( $l1, $l2 );
194 return ( $lighter + 0.05 ) / ( $darker + 0.05 );
195 }
196
197 /**
198 * WCAG relative luminance of a hex colour, or null if it isn't a parseable #rrggbb/#rgb value.
199 *
200 * @param string $hex Colour, e.g. "#1a1a2e".
201 * @return float|null
202 */
203 private static function relative_luminance( $hex ) {
204 $hex = ltrim( trim( (string) $hex ), '#' );
205 if ( 3 === strlen( $hex ) ) {
206 $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
207 }
208 if ( 6 !== strlen( $hex ) || ! ctype_xdigit( $hex ) ) {
209 return null;
210 }
211 $channels = array();
212 foreach ( array( 0, 2, 4 ) as $i ) {
213 $c = hexdec( substr( $hex, $i, 2 ) ) / 255;
214 $channels[] = $c <= 0.03928 ? $c / 12.92 : pow( ( $c + 0.055 ) / 1.055, 2.4 );
215 }
216 return 0.2126 * $channels[0] + 0.7152 * $channels[1] + 0.0722 * $channels[2];
217 }
218
219 /**
220 * Sanitize one token's per-device value bag.
221 *
222 * @param array $token Token definition.
223 * @param mixed $stored Stored value (device bag, or a bare value we wrap into desktop).
224 * @return array Device bag with at least `desktop`.
225 */
226 public static function sanitize_token( $token, $stored ) {
227 // Accept a bare value (e.g. legacy import) by treating it as the desktop base.
228 if ( ! is_array( $stored ) || ! self::is_device_bag( $stored ) ) {
229 $stored = array( 'desktop' => $stored );
230 }
231
232 $devices = ! empty( $token['responsive'] ) ? array( 'desktop', 'tablet', 'mobile' ) : array( 'desktop' );
233 $out = array();
234 foreach ( $devices as $device ) {
235 if ( ! array_key_exists( $device, $stored ) ) {
236 continue; // Missing device → inherits desktop via the CSS cascade.
237 }
238 $out[ $device ] = self::sanitize_scalar( $token, $stored[ $device ] );
239 }
240
241 if ( ! isset( $out['desktop'] ) ) {
242 $out['desktop'] = $token['default'];
243 }
244
245 return $out;
246 }
247
248 /**
249 * Sanitize a single value according to the token type.
250 *
251 * @param array $token Token definition.
252 * @param mixed $value Raw value.
253 * @return mixed Clean value (token default on anything invalid).
254 */
255 public static function sanitize_scalar( $token, $value ) {
256 // The font-family select is data-driven (Google Fonts, not baked into schema options),
257 // so validate as a font-family string rather than against the fallback option list.
258 if ( ! empty( $token['source'] ) && 'google_fonts' === $token['source'] ) {
259 return self::sanitize_font_family( $value );
260 }
261
262 switch ( $token['type'] ) {
263 case 'slider':
264 return self::sanitize_number( $token, $value );
265
266 case 'color':
267 return self::sanitize_color( $value, $token['default'], ! empty( $token['gradientable'] ) );
268
269 case 'box4':
270 return self::sanitize_box4( $token, $value );
271
272 case 'select':
273 case 'align':
274 return self::sanitize_choice( $token, $value );
275
276 case 'fontstyle':
277 return self::sanitize_fontstyle( $token, $value );
278
279 case 'toggle':
280 return (bool) $value;
281
282 case 'media':
283 return $value ? esc_url_raw( (string) $value ) : '';
284
285 default:
286 return sanitize_text_field( (string) $value );
287 }
288 }
289
290 /* --------------------------------------------------------------------- *
291 * Type handlers
292 * --------------------------------------------------------------------- */
293
294 /**
295 * Clamp a slider value to [min,max]; integer unless the step is fractional.
296 *
297 * @param array $token Token definition.
298 * @param mixed $value Raw value.
299 * @return int|float
300 */
301 protected static function sanitize_number( $token, $value ) {
302 if ( ! is_numeric( $value ) ) {
303 return $token['default'];
304 }
305 $min = isset( $token['min'] ) ? $token['min'] : 0;
306 $max = isset( $token['max'] ) ? $token['max'] : 9999;
307 $step = isset( $token['step'] ) ? $token['step'] : 1;
308 $n = ( $step < 1 ) ? (float) $value : (int) round( $value );
309 return max( $min, min( $max, $n ) );
310 }
311
312 /**
313 * Public façade over {@see self::sanitize_color()} for other engine classes (e.g. {@see Palettes}).
314 *
315 * @param mixed $value Raw value.
316 * @param string $default Fallback.
317 * @return string
318 */
319 public static function color( $value, $default = '#000000' ) {
320 return self::sanitize_color( $value, $default );
321 }
322
323 /**
324 * Validate a colour: #rgb / #rrggbb (+ 8-digit alpha) or rgb()/rgba(). Falls back to
325 * the default; alpha is preserved so legacy `rgba()` borders survive. When `$gradientable`
326 * is true (only set on background tokens whose CSS rule uses the `background` shorthand,
327 * not `background-color` — see frontend.css), a `linear-gradient()`/`radial-gradient()`
328 * value built from solid colour stops is accepted too.
329 *
330 * @param mixed $value Raw value.
331 * @param string $default Fallback.
332 * @param bool $gradientable Whether a gradient value is acceptable for this token.
333 * @return string
334 */
335 protected static function sanitize_color( $value, $default, $gradientable = false ) {
336 $value = is_string( $value ) ? trim( $value ) : '';
337 if ( preg_match( '/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i', $value ) ) {
338 return strtolower( $value );
339 }
340 if ( preg_match( '/^rgba?\(\s*[\d.,%\s\/]+\)$/i', $value ) ) {
341 return $value;
342 }
343 if ( $gradientable && self::is_valid_gradient( $value ) ) {
344 return $value;
345 }
346 return $default;
347 }
348
349 /**
350 * Whether `$value` is a `linear-gradient()`/`radial-gradient()` built entirely from solid
351 * colour stops (each stop optionally followed by a `<percentage>` position) — e.g.
352 * `linear-gradient(135deg, #3366cc, #7a5cff)`. Deliberately strict: this string is emitted
353 * verbatim into an inline `<style>` block ({@see Compiler::css_safe}), so anything short of
354 * "every part matches a known-safe shape" is rejected rather than guessed at.
355 *
356 * @param string $value Raw value.
357 * @return bool
358 */
359 protected static function is_valid_gradient( $value ) {
360 if ( ! preg_match( '/^(linear|radial)-gradient\((.*)\)$/is', $value, $m ) ) {
361 return false;
362 }
363 $type = strtolower( $m[1] );
364 $parts = self::split_top_level_commas( $m[2] );
365 if ( count( $parts ) < 3 ) { // header + at least 2 colour stops.
366 return false;
367 }
368 $header = trim( $parts[0] );
369 $is_header = ( 'linear' === $type && (
370 preg_match( '/^-?\d+(\.\d+)?deg$/i', $header ) ||
371 preg_match( '/^to\s+(top|bottom|left|right)(\s+(top|bottom|left|right))?$/i', $header )
372 ) ) || ( 'radial' === $type && preg_match( '/^(circle|ellipse)(\s+at\s+.+)?$/i', $header ) );
373 $stops = $is_header ? array_slice( $parts, 1 ) : $parts;
374 if ( count( $stops ) < 2 ) {
375 return false;
376 }
377 foreach ( $stops as $stop ) {
378 $stop = trim( $stop );
379 if ( ! preg_match(
380 '/^(#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})|rgba?\([\d.,%\s\/]+\))(\s+-?\d+(\.\d+)?%)?$/i',
381 $stop
382 ) ) {
383 return false;
384 }
385 }
386 return true;
387 }
388
389 /**
390 * Split on top-level commas only — skips commas nested inside an `rgba()`/`hsla()` stop
391 * (e.g. `rgba(0,0,0,.5)` inside a gradient's stop list must not be split apart).
392 *
393 * @param string $str String to split.
394 * @return string[]
395 */
396 protected static function split_top_level_commas( $str ) {
397 $parts = array();
398 $depth = 0;
399 $cur = '';
400 $len = strlen( $str );
401 for ( $i = 0; $i < $len; $i++ ) {
402 $ch = $str[ $i ];
403 if ( '(' === $ch ) {
404 $depth++;
405 } elseif ( ')' === $ch ) {
406 $depth--;
407 }
408 if ( ',' === $ch && 0 === $depth ) {
409 $parts[] = $cur;
410 $cur = '';
411 continue;
412 }
413 $cur .= $ch;
414 }
415 $parts[] = $cur;
416 return $parts;
417 }
418
419 /**
420 * Clean a 4-side box, keeping the legacy associative shape {top,right,bottom,left}
421 * (+ `unit` for a radius). Margins may be negative; everything else is clamped at 0.
422 *
423 * @param array $token Token definition.
424 * @param mixed $value Raw value.
425 * @return array
426 */
427 protected static function sanitize_box4( $token, $value ) {
428 if ( ! is_array( $value ) ) {
429 return $token['default'];
430 }
431 $allow_neg = false !== strpos( $token['key'], 'margin' );
432 $floor = isset( $token['min'] ) ? (int) $token['min'] : ( $allow_neg ? -1000 : 0 );
433 $max = isset( $token['max'] ) ? $token['max'] : 1000;
434
435 $out = array();
436 foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) {
437 $n = isset( $value[ $side ] ) && is_numeric( $value[ $side ] ) ? (int) $value[ $side ] : 0;
438 $out[ $side ] = max( $floor, min( $max, $n ) );
439 }
440
441 if ( ! empty( $token['units'] ) ) {
442 $out['unit'] = ( isset( $value['unit'] ) && in_array( $value['unit'], $token['units'], true ) ) ? $value['unit'] : $token['units'][0];
443 }
444 return $out;
445 }
446
447 /**
448 * Ensure a select/align value is one of the allowed options.
449 *
450 * @param array $token Token definition.
451 * @param mixed $value Raw value.
452 * @return string
453 */
454 protected static function sanitize_choice( $token, $value ) {
455 $value = (string) $value;
456 if ( 'align' === $token['type'] ) {
457 return in_array( $value, array( 'left', 'center', 'right' ), true ) ? $value : $token['default'];
458 }
459 $allowed = array();
460 foreach ( ( isset( $token['options'] ) ? $token['options'] : array() ) as $opt ) {
461 $allowed[] = (string) $opt['value'];
462 }
463 return in_array( $value, $allowed, true ) ? $value : $token['default'];
464 }
465
466 /**
467 * Sanitize a font-family value: the empty string (theme font) or a plain family name.
468 *
469 * @param mixed $value Raw value.
470 * @return string
471 */
472 protected static function sanitize_font_family( $value ) {
473 $value = sanitize_text_field( (string) $value );
474 $value = preg_replace( '/[^A-Za-z0-9 ,\-\'"]/', '', $value );
475 return trim( $value );
476 }
477
478 /**
479 * Normalize a font-style flag set to four booleans (old customizer keys), plus an optional
480 * explicit `weight` — validated against the token's own `weight_options`; anything else
481 * (unknown value, or simply absent, true for every pre-existing stored value) becomes ''
482 * ("Auto"), which keeps deriving font-weight from `bold` exactly as before this was added.
483 *
484 * @param array $token Token definition (for weight_options).
485 * @param mixed $value Raw value.
486 * @return array
487 */
488 protected static function sanitize_fontstyle( $token, $value ) {
489 $value = is_array( $value ) ? $value : array();
490 $out = array();
491 foreach ( array( 'bold', 'italic', 'underline', 'uppercase' ) as $flag ) {
492 $out[ $flag ] = ! empty( $value[ $flag ] );
493 }
494 $allowed_weights = array();
495 foreach ( ( isset( $token['weight_options'] ) ? $token['weight_options'] : array() ) as $opt ) {
496 $allowed_weights[] = (string) $opt['value'];
497 }
498 $weight = isset( $value['weight'] ) ? (string) $value['weight'] : '';
499 $out['weight'] = in_array( $weight, $allowed_weights, true ) ? $weight : '';
500 return $out;
501 }
502
503 /* --------------------------------------------------------------------- *
504 * Record-level helpers
505 * --------------------------------------------------------------------- */
506
507 /**
508 * A palette id must match one of the registered palettes, else empty (custom/none). A Pro
509 * palette is rejected unless the Pro tier is active.
510 *
511 * @param mixed $value Raw value.
512 * @return string
513 */
514 protected static function sanitize_palette_id( $value ) {
515 $value = sanitize_key( $value );
516 $pro_active = Engine::pro_active();
517 foreach ( Schema::palettes() as $palette ) {
518 if ( $palette['id'] === $value ) {
519 if ( ! empty( $palette['is_pro'] ) && ! $pro_active ) {
520 return '';
521 }
522 return $value;
523 }
524 }
525 return '';
526 }
527
528 /**
529 * Strip anything dangerous from user CSS ({@see Compiler::scope_custom_css()} scopes it).
530 *
531 * @param mixed $css Raw CSS.
532 * @return string
533 */
534 protected static function sanitize_css( $css ) {
535 $css = (string) $css;
536 $css = wp_strip_all_tags( $css );
537 $css = preg_replace( '/@import\b[^;]+;?/i', '', $css );
538 $css = preg_replace( '/expression\s*\(/i', '', $css );
539 $css = preg_replace( '/url\(\s*[\'"]?\s*javascript:/i', 'url(', $css );
540 return trim( $css );
541 }
542
543 /**
544 * Is this array a per-device bag (has desktop/tablet/mobile keys) rather than a value
545 * that just happens to be an array (like a box4)?
546 *
547 * @param array $value Value.
548 * @return bool
549 */
550 protected static function is_device_bag( $value ) {
551 foreach ( array( 'desktop', 'tablet', 'mobile' ) as $device ) {
552 if ( array_key_exists( $device, $value ) ) {
553 return true;
554 }
555 }
556 return false;
557 }
558 }
559