PluginProbe
Themify Builder / trunk
Themify Builder vtrunk
7.8.1 7.8.0 7.7.9 7.7.8 7.7.7 7.7.6 7.7.5 7.7.4 7.7.3 7.7.2 trunk 7.6.0 7.6.1 7.6.2 7.6.3 7.6.4 7.6.5 7.6.6 7.6.7 7.6.8 7.6.9 7.7.0 7.7.1
themify-builder / themify / class-tf-sv.php

class-tf-sv.php in Themify Builder trunk, at themify/class-tf-sv.php

930 lines 37.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 exit;
4 }
5
6 final class TF_SV_Framework {
7
8 const OPTION_KEY = 'tf_sv_vars';
9 const SORT_KEY = 'tf_sv_sort_order';
10
11 public static function init() {
12 add_filter( 'themify_builder_active_vars', [ __CLASS__, 'builder_active_vars' ] );
13 add_action( 'wp_ajax_tf_sv_save_vars', [ __CLASS__, 'ajax_save_vars' ] );
14 add_action( 'wp_ajax_tf_sv_refresh_vars', [ __CLASS__, 'ajax_refresh_vars' ] );
15 add_action( 'wp_ajax_tf_sv_theme_native_colors', [ __CLASS__, 'ajax_theme_native_colors' ] );
16 add_action( 'wp_ajax_tf_sv_import_missing_vars', [ __CLASS__, 'ajax_import_missing_vars' ] );
17 add_action( 'wp_ajax_tf_sv_collect_used', [ __CLASS__, 'ajax_collect_used' ] );
18 add_action( 'wp_ajax_tf_sv_save_sort_order', [ __CLASS__, 'ajax_save_sort_order' ] );
19 add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_frontend_assets' ] );
20 add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue_admin_assets' ], 30 );
21 add_action( 'customize_controls_enqueue_scripts', [ __CLASS__, 'enqueue_customizer_controls' ], 30 );
22 add_action( 'customize_preview_init', [ __CLASS__, 'enqueue_customizer_preview' ], 30 );
23 add_action( 'admin_head', [ __CLASS__, 'print_root_vars' ], 1 );
24 if ( ! function_exists( 'themify_is_themify_theme' ) || ! themify_is_themify_theme() ) {
25 add_action( 'wp_head', [ __CLASS__, 'print_root_vars' ], 999 );
26 }
27 }
28
29 public static function get_frontend_head_fragment(): string {
30 return self::get_root_vars_style_markup() . self::get_google_fonts_links_markup();
31 }
32
33 private static function get_root_vars_style_markup(): string {
34 $css = self::build_root_css();
35 return '' !== $css ? '<style id="tf-sv-root-vars">' . $css . '</style>' . PHP_EOL : '';
36 }
37
38 private static function tf_sv_is_google_catalog_family( string $font_name ): bool {
39 static $lookup = null;
40 if ( null === $lookup ) {
41 $lookup = [];
42 if ( function_exists( 'themify_get_google_font_lists' ) ) {
43 foreach ( array_keys( themify_get_google_font_lists() ) as $name ) {
44 $lookup[ strtolower( (string) $name ) ] = true;
45 }
46 }
47 }
48 return isset( $lookup[ strtolower( $font_name ) ] );
49 }
50
51 /** Google Fonts URL: catalog families only; skip cf/websafe/non-Google JSON. */
52 private static function google_font_family_for_enqueue( string $raw, array $safe_fonts ): string {
53 $raw = trim( wp_unslash( $raw ) );
54 if ( '' === $raw ) {
55 return '';
56 }
57 $trust_google = false;
58 if ( 0 === strpos( $raw, '{' ) ) {
59 $obj = json_decode( $raw, true );
60 if ( ! is_array( $obj ) || empty( $obj['name'] ) ) {
61 return '';
62 }
63 $ft = isset( $obj['fonttype'] ) ? strtolower( (string) $obj['fonttype'] ) : '';
64 if ( in_array( $ft, [ 'cf', 'websafe' ], true ) ) {
65 return '';
66 }
67 if ( '' !== $ft && ! in_array( $ft, [ 'google', 'var' ], true ) ) {
68 return '';
69 }
70 if ( 'google' === $ft ) {
71 $trust_google = true;
72 }
73 $font_name = self::sanitize_value( (string) $obj['name'], 'font' );
74 } else {
75 $font_name = self::sanitize_value( $raw, 'font' );
76 }
77 if ( '' === $font_name || 0 === strpos( $font_name, '--' ) || 0 === strpos( $font_name, 'var(' ) ) {
78 return '';
79 }
80 if ( false !== strpos( $font_name, ',' ) ) {
81 $font_name = trim( preg_replace( '/^["\']+|["\']+$/', '', explode( ',', $font_name, 2 )[0] ) );
82 }
83 if ( '' === $font_name || 0 === strpos( $font_name, 'var(' ) ) {
84 return '';
85 }
86 if ( in_array( strtolower( $font_name ), $safe_fonts, true ) ) {
87 return '';
88 }
89 if ( ! $trust_google && ! self::tf_sv_is_google_catalog_family( $font_name ) ) {
90 return '';
91 }
92 return $font_name;
93 }
94
95 private static function get_google_fonts_links_markup(): string {
96 $items = self::get_merged_vars();
97 if ( empty( $items ) ) {
98 return '';
99 }
100 $safe_fonts = [
101 'arial', 'helvetica', 'verdana', 'georgia', 'times new roman',
102 'courier new', 'tahoma', 'trebuchet ms', 'palatino', 'lucida sans unicode',
103 'impact', 'comic sans ms', 'sans-serif', 'serif', 'monospace', 'cursive', 'fantasy',
104 ];
105 $google_families = [];
106 foreach ( $items as $item ) {
107 if ( empty( $item['type'] ) || 'font' !== $item['type'] || empty( $item['values'] ) || ! is_array( $item['values'] ) ) {
108 continue;
109 }
110 foreach ( $item['values'] as $raw_font ) {
111 $font_name = self::google_font_family_for_enqueue( (string) $raw_font, $safe_fonts );
112 if ( '' !== $font_name ) {
113 $google_families[] = $font_name;
114 }
115 }
116 }
117 $google_families = array_values( array_unique( $google_families ) );
118 if ( empty( $google_families ) ) {
119 return '';
120 }
121 $families = implode( '&family=', array_map( 'urlencode', $google_families ) );
122
123 return '<link rel="stylesheet" id="tf-sv-google-fonts" href="https://fonts.googleapis.com/css2?family=' . $families . '&display=swap">' . PHP_EOL;
124 }
125
126 public static function builder_active_vars( array $vars ):array {
127 $vars['tf_sv'] = self::get_js_data();
128 return $vars;
129 }
130
131 public static function enqueue_frontend_assets() {
132 if ( ! class_exists( 'Themify_Builder_Model', false )
133 || ! Themify_Builder_Model::is_front_builder_activate() ) {
134 return;
135 }
136 self::enqueue_shared_assets( 'builder_frontend' );
137 }
138
139 public static function enqueue_admin_assets( $hook='' ) {
140 if ( is_customize_preview() || 'customize.php' === $hook || 'post.php' === $hook || 'post-new.php' === $hook || ! empty( $_GET['themify_builder'] ) ) {
141 self::enqueue_shared_assets( 'admin' );
142 }
143 }
144
145 public static function enqueue_customizer_controls() {
146 self::enqueue_shared_assets( 'customizer_controls' );
147 }
148
149 public static function enqueue_customizer_preview() {
150 self::enqueue_shared_assets( 'customizer_preview' );
151 }
152
153 private static function enqueue_shared_assets( string $context ) {
154 static $done = [];
155 if ( isset( $done[ $context ] ) ) {
156 return;
157 }
158 $done[ $context ] = true;
159
160 $style = 'tf-sv-ui';
161 $script = 'tf-sv-ui';
162
163 $style_path = THEMIFY_DIR . '/css/tf-sv-ui.css';
164 $script_path = THEMIFY_DIR . '/js/tf-sv-ui.js';
165 $style_ver = file_exists( $style_path ) ? (string) filemtime( $style_path ) : THEMIFY_VERSION;
166 $script_ver = file_exists( $script_path ) ? (string) filemtime( $script_path ) : THEMIFY_VERSION;
167
168 $is_customizer = 0 === strpos( $context, 'customizer' );
169 $color_style = 'tf-sv-colorpicker';
170 $color_script = 'tf-sv-colorpicker';
171 $combo_style = $is_customizer ? 'tf-sv-scombobox' : 'tf-sv-builder-combobox';
172 $combo_script = $combo_style;
173 $main_script_dep = wp_script_is( 'themify-main-script', 'registered' ) || wp_script_is( 'themify-main-script', 'enqueued' ) ? 'themify-main-script' : 'jquery';
174
175 if ( ! wp_style_is( $color_style, 'registered' ) ) {
176 wp_register_style( $color_style, THEMIFY_METABOX_URI . 'css/themify.minicolors.css', [], THEMIFY_VERSION );
177 }
178 if ( ! wp_script_is( $color_script, 'registered' ) ) {
179 wp_register_script( $color_script, THEMIFY_METABOX_URI . 'js/themify.minicolors.js', [ $main_script_dep ], THEMIFY_VERSION, true );
180 }
181 if ( $is_customizer ) {
182 if ( ! defined( 'THEMIFY_CUSTOMIZER_URI' ) && defined( 'THEMIFY_URI' ) && defined( 'THEMIFY_DIR' ) && is_dir( THEMIFY_DIR . '/customizer' ) ) {
183 if ( ! defined( 'THEMIFY_CUSTOMIZER_DIR' ) ) {
184 define( 'THEMIFY_CUSTOMIZER_DIR', THEMIFY_DIR . '/customizer' );
185 }
186 define( 'THEMIFY_CUSTOMIZER_URI', THEMIFY_URI . '/customizer' );
187 }
188 $scombo_style_url = null;
189 $scombo_script_url = null;
190 if ( defined( 'THEMIFY_CUSTOMIZER_URI' ) && is_file( THEMIFY_DIR . '/customizer/css/jquery-scombobox.css' ) ) {
191 $scombo_style_url = THEMIFY_CUSTOMIZER_URI . '/css/jquery-scombobox.css';
192 $scombo_script_url = THEMIFY_CUSTOMIZER_URI . '/js/jquery-scombobox.min.js';
193 } elseif ( defined( 'THEMIFY_BUILDER_URI' ) && defined( 'THEMIFY_BUILDER_DIR' ) && is_file( THEMIFY_BUILDER_DIR . '/css/editor/themify-combobox.css' ) && is_file( THEMIFY_BUILDER_DIR . '/js/editor/themify-combobox.min.js' ) ) {
194 $scombo_style_url = THEMIFY_BUILDER_URI . '/css/editor/themify-combobox.css';
195 $scombo_script_url = THEMIFY_BUILDER_URI . '/js/editor/themify-combobox.min.js';
196 }
197 if ( $scombo_style_url && $scombo_script_url ) {
198 if ( ! wp_style_is( $combo_style, 'registered' ) ) {
199 wp_register_style( $combo_style, $scombo_style_url, [], THEMIFY_VERSION );
200 }
201 if ( ! wp_script_is( $combo_script, 'registered' ) ) {
202 wp_register_script( $combo_script, $scombo_script_url, [ $main_script_dep ], THEMIFY_VERSION, true );
203 }
204 }
205 } else {
206 if ( ! wp_style_is( $combo_style, 'registered' ) ) {
207 wp_register_style( $combo_style, THEMIFY_BUILDER_URI . '/css/editor/themify-combobox.css', [], THEMIFY_VERSION );
208 }
209 if ( ! wp_script_is( $combo_script, 'registered' ) ) {
210 wp_register_script( $combo_script, THEMIFY_BUILDER_URI . '/js/editor/themify-combobox.min.js', [ $main_script_dep ], THEMIFY_VERSION, true );
211 }
212 }
213 wp_register_style( $style, THEMIFY_URI . '/css/tf-sv-ui.css', [ $color_style, $combo_style ], $style_ver );
214 if ( class_exists( 'Themify_Builder_Style_Variables', false ) ) {
215 $preset_css = Themify_Builder_Style_Variables::get_preset_root_css();
216 if ( '' !== $preset_css ) {
217 wp_add_inline_style( $style, $preset_css );
218 }
219 }
220 wp_register_script( $script, THEMIFY_URI . '/js/tf-sv-ui.js', [ 'jquery', $color_script, $combo_script ], $script_ver, true );
221 wp_enqueue_style( $color_style );
222 wp_enqueue_style( $combo_style );
223 wp_enqueue_script( $color_script );
224 wp_enqueue_script( $combo_script );
225 wp_enqueue_style( $style );
226 wp_enqueue_script( $script );
227 wp_add_inline_script( $script, 'window.tfSVData = ' . wp_json_encode( self::get_js_data() ) . ';', 'before' );
228 }
229
230 public static function ajax_save_vars() {
231 check_ajax_referer( 'tf_sv_nonce', 'nonce' );
232 if ( ! current_user_can( 'edit_theme_options' ) ) {
233 wp_send_json_error( [ 'message' => __( 'Permission denied.', 'themify' ) ], 403 );
234 }
235 $vars = [];
236 if ( isset( $_POST['vars'] ) ) {
237 $decoded = json_decode( wp_unslash( $_POST['vars'] ), true );
238 if ( is_array( $decoded ) ) {
239 $vars = self::sanitize_vars( $decoded );
240 }
241 }
242 set_theme_mod( self::OPTION_KEY, $vars );
243 wp_send_json_success( [
244 'vars' => self::get_js_vars(),
245 'css' => self::build_root_css(),
246 'message' => __( 'Style Variables saved.', 'themify' )
247 ] );
248 }
249
250 public static function get_sort_order(): array {
251 $order = get_theme_mod( self::SORT_KEY, [] );
252 return is_array( $order ) ? $order : [];
253 }
254
255 public static function ajax_save_sort_order() {
256 check_ajax_referer( 'tf_sv_nonce', 'nonce' );
257 if ( ! current_user_can( 'edit_theme_options' ) ) {
258 wp_send_json_error( [ 'message' => __( 'Permission denied.', 'themify' ) ], 403 );
259 }
260 $order = [];
261 if ( isset( $_POST['order'] ) ) {
262 $decoded = json_decode( wp_unslash( $_POST['order'] ), true );
263 if ( is_array( $decoded ) ) {
264 foreach ( $decoded as $type => $names ) {
265 if ( is_string( $type ) && is_array( $names ) ) {
266 $order[ sanitize_key( $type ) ] = array_values( array_filter( array_map( 'sanitize_text_field', $names ) ) );
267 }
268 }
269 }
270 }
271 set_theme_mod( self::SORT_KEY, $order );
272 wp_send_json_success( [ 'order' => $order ] );
273 }
274
275 public static function ajax_refresh_vars() {
276 check_ajax_referer( 'tf_sv_nonce', 'nonce' );
277 if ( ! current_user_can( 'edit_theme_options' ) ) {
278 wp_send_json_error( [ 'message' => __( 'Permission denied.', 'themify' ) ], 403 );
279 }
280 wp_send_json_success( [
281 'theme' => self::get_theme_vars(),
282 ] );
283 }
284
285 public static function ajax_theme_native_colors() {
286 check_ajax_referer( 'tf_sv_nonce', 'nonce' );
287 if ( ! current_user_can( 'edit_theme_options' ) ) {
288 wp_send_json_error( [ 'message' => __( 'Permission denied.', 'themify' ) ], 403 );
289 }
290 $names = [];
291 if ( isset( $_POST['names'] ) ) {
292 $decoded = json_decode( wp_unslash( $_POST['names'] ), true );
293 if ( is_array( $decoded ) ) {
294 foreach ( $decoded as $n ) {
295 $n = self::sanitize_name( is_string( $n ) ? $n : '' );
296 if ( '' !== $n ) {
297 $names[] = $n;
298 }
299 }
300 }
301 }
302 $names = array_values( array_unique( $names ) );
303
304 $css_vars = self::get_theme_root_css_vars();
305 $colors = [];
306 foreach ( $names as $name ) {
307 $raw = isset( $css_vars[ $name ] ) ? (string) $css_vars[ $name ] : '';
308 $colors[ $name ] = '' !== $raw ? self::sanitize_value( $raw, 'color' ) : '';
309 }
310 wp_send_json_success( [ 'colors' => $colors ] );
311 }
312
313 private static function get_theme_root_css_vars(): array {
314 static $cache = null;
315 if ( $cache !== null ) {
316 return $cache;
317 }
318 $cache = [];
319
320 $sources = [];
321
322 $theme_css = get_template_directory() . '/style.css';
323 if ( is_file( $theme_css ) ) {
324 $sources[] = [ 'path' => $theme_css ];
325 }
326
327 if ( function_exists( 'themify_get_skin' ) ) {
328 $skin = themify_get_skin();
329 if ( $skin ) {
330 $skin_css = get_template_directory() . '/skins/' . $skin . '/style.css';
331 if ( is_file( $skin_css ) ) {
332 $sources[] = [ 'path' => $skin_css ];
333 }
334 }
335 }
336
337 if ( get_stylesheet_directory() !== get_template_directory() ) {
338 $child_css = get_stylesheet_directory() . '/style.css';
339 if ( is_file( $child_css ) ) {
340 $sources[] = [ 'path' => $child_css ];
341 }
342 $child_custom_css = get_stylesheet_directory() . '/custom_style.css';
343 if ( is_file( $child_custom_css ) ) {
344 $sources[] = [ 'path' => $child_custom_css ];
345 }
346 }
347
348 $theme_custom_css = get_template_directory() . '/custom_style.css';
349 if ( is_file( $theme_custom_css ) ) {
350 $sources[] = [ 'path' => $theme_custom_css ];
351 }
352
353 $inline_custom_css = themify_get( 'setting-custom_css', false, true );
354 if ( is_string( $inline_custom_css ) && '' !== trim( $inline_custom_css ) ) {
355 $sources[] = [ 'inline' => $inline_custom_css ];
356 }
357
358 if ( empty( $sources ) ) {
359 $cache = [];
360 return $cache;
361 }
362
363 $normal = [];
364 $important = [];
365
366 foreach ( $sources as $source ) {
367 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
368 $css = isset( $source['path'] ) ? @file_get_contents( $source['path'] ) : ( $source['inline'] ?? '' );
369 if ( false === $css || '' === $css ) {
370 continue;
371 }
372 self::parse_root_css_vars( $css, $normal, $important );
373 }
374
375 $raw_vars = array_merge( $normal, $important );
376
377 // One-level var() expand for chained custom properties.
378 $resolved = [];
379 foreach ( $raw_vars as $name => $value ) {
380 $resolved[ $name ] = trim( (string) preg_replace_callback(
381 '/var\(\s*--([a-zA-Z0-9_-]+)\s*\)/',
382 static fn( $m ) => $raw_vars[ strtolower( trim( $m[1] ) ) ] ?? $m[0],
383 $value
384 ) );
385 }
386
387 // Fill in registered Style Variable color defaults (e.g. Builder preset colors)
388 // for names the theme does not hardcode in its CSS, so editor color swatches
389 // resolve on every theme. Theme-defined values keep priority.
390 foreach ( self::get_merged_vars() as $sv_item ) {
391 if ( 'color' !== ( $sv_item['type'] ?? '' ) ) {
392 continue;
393 }
394 $sv_name = $sv_item['name'] ?? '';
395 if ( '' === $sv_name || isset( $resolved[ $sv_name ] ) ) {
396 continue;
397 }
398 $sv_val = is_array( $sv_item['values'] ?? null ) ? trim( (string) ( $sv_item['values']['desktop'] ?? '' ) ) : '';
399 if ( '' !== $sv_val ) {
400 $resolved[ $sv_name ] = $sv_val;
401 }
402 }
403
404 $cache = $resolved;
405 return $cache;
406 }
407
408 private static function parse_root_css_vars( string $css, array &$normal, array &$important ): void {
409 $css = preg_replace( '/\/\*.*?\*\//s', '', $css );
410
411 $css = preg_replace( '/@[a-zA-Z][^{]*\{(?:[^{}]*|\{[^}]*\})*\}/s', '', $css );
412
413 if ( ! preg_match_all( '/:root\s*\{([^}]+)\}/', $css, $blocks ) ) {
414 return;
415 }
416
417 foreach ( $blocks[1] as $block ) {
418 if ( ! preg_match_all( '/--([a-zA-Z0-9_-]+)\s*:\s*([^;]+);/', $block, $decls, PREG_SET_ORDER ) ) {
419 continue;
420 }
421 foreach ( $decls as $decl ) {
422 $name = self::sanitize_name( $decl[1] );
423 $raw = trim( $decl[2] );
424 if ( '' === $name || '' === $raw ) {
425 continue;
426 }
427 if ( preg_match( '/!important\s*$/i', $raw ) ) {
428 $important[ $name ] = trim( preg_replace( '/\s*!important\s*$/i', '', $raw ) );
429 } else {
430 $normal[ $name ] = $raw;
431 }
432 }
433 }
434 }
435
436 public static function print_root_vars() {
437 echo self::get_root_vars_style_markup();
438 echo self::get_google_fonts_links_markup();
439 }
440
441 private static function build_root_css():string {
442 $items = self::get_merged_vars();
443 if ( empty( $items ) ) {
444 return '';
445 }
446 $breakpoints = self::get_breakpoints_payload();
447 $css = [];
448
449 $base_rules = '';
450 foreach ( $items as $item ) {
451 if ( empty( $item['name'] ) || empty( $item['values'] ) || ! is_array( $item['values'] ) ) {
452 continue;
453 }
454 $value = self::get_effective_value( $item['values'], 'desktop' );
455 if ( '' !== $value ) {
456 $base_rules .= '--' . $item['name'] . ':' . $value . ';';
457 }
458 }
459 if ( '' !== $base_rules ) {
460 $css[] = ':root{' . $base_rules . '}';
461 }
462
463 foreach ( [ 'tablet_landscape', 'tablet', 'mobile' ] as $bp ) {
464 $rules = '';
465 foreach ( $items as $item ) {
466 if ( empty( $item['name'] ) || empty( $item['values'] ) || ! is_array( $item['values'] ) ) {
467 continue;
468 }
469 if ( empty( $item['values'][ $bp ] ) ) {
470 continue;
471 }
472 $value = self::get_effective_value( $item['values'], $bp );
473 if ( '' !== $value ) {
474 $rules .= '--' . $item['name'] . ':' . $value . ';';
475 }
476 }
477 if ( '' !== $rules && ! empty( $breakpoints[ $bp ]['max'] ) ) {
478 $css[] = '@media (max-width:' . intval( $breakpoints[ $bp ]['max'] ) . 'px){:root{' . $rules . '}}';
479 }
480 }
481
482 return implode( '', $css );
483 }
484
485 private static function build_font_catalog_payload(): array {
486 $fonts = [];
487 $google = [];
488 $cf = [];
489 if ( function_exists( 'themify_get_web_safe_font_list' ) ) {
490 $f = themify_get_web_safe_font_list();
491 unset( $f[0], $f[1] );
492 foreach ( $f as $v ) {
493 if ( ! empty( $v['value'] ) ) {
494 $fonts[] = [
495 'name' => $v['name'],
496 'value' => $v['value'],
497 ];
498 }
499 }
500 }
501 if ( function_exists( 'themify_get_google_font_lists' ) ) {
502 $themify_gfonts = themify_get_google_font_lists();
503 if ( ! empty( $themify_gfonts ) && is_array( $themify_gfonts ) ) {
504 foreach ( $themify_gfonts as $font_name => $v ) {
505 $variants = is_array( $v ) ? $v[1] : [];
506 foreach ( $variants as $key => $variant_value ) {
507 if ( 'r' === $variant_value ) {
508 $variants[ $key ] = '400';
509 } elseif ( 'i' === $variant_value ) {
510 $variants[ $key ] = '400i';
511 }
512 }
513 $google[] = [
514 'name' => $font_name,
515 'variants' => $variants,
516 ];
517 }
518 }
519 }
520 if ( class_exists( 'Themify_Custom_Fonts', false ) ) {
521 $cf_list = Themify_Custom_Fonts::get_list( 'customizer' );
522 if ( ! empty( $cf_list ) && is_array( $cf_list ) ) {
523 foreach ( $cf_list as $v ) {
524 $variant = ! empty( $v['variant'] ) ? str_replace( [ 'regular', 'normal', 'bold' ], [ '400', '400', '700' ], $v['variant'] ) : '';
525 $cf[] = [
526 'value' => $v['value'],
527 'name' => $v['name'],
528 'variants' => $variant,
529 ];
530 }
531 }
532 }
533 return [
534 'fonts' => $fonts,
535 'google' => $google,
536 'cf' => $cf,
537 ];
538 }
539
540 public static function get_js_data():array {
541 return [
542 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
543 'nonce' => wp_create_nonce( 'tf_sv_nonce' ),
544 'vars' => self::get_js_vars(),
545 'sortOrder' => self::get_sort_order(),
546 'context' => is_customize_preview() ? 'customizer_preview' : ( is_admin() ? 'admin' : 'front' ),
547 'fontCatalog' => self::build_font_catalog_payload(),
548 'breakpoints' => self::get_breakpoints_payload(),
549 'i18n' => [
550 'title' => __( 'Style Variables', 'themify' ),
551 'colors' => __( 'Colors', 'themify' ),
552 'fonts' => __( 'Fonts', 'themify' ),
553 'numbers' => __( 'Numbers', 'themify' ),
554 'addNew' => __( '+ Add new', 'themify' ),
555 'editVariables' => __( 'Edit', 'themify' ),
556 'noVariablesYet' => __( 'No variables yet', 'themify' ),
557 'save' => __( 'Save', 'themify' ),
558 'close' => __( 'Close', 'themify' ),
559 'import' => __( 'Import', 'themify' ),
560 'export' => __( 'Export', 'themify' ),
561 'variableName' => __( 'Variable Name', 'themify' ),
562 'variableNameTaken' => __( 'This variable name is already taken. Please enter a new one.', 'themify' ),
563 'saved' => __( 'Saved', 'themify' ),
564 'invalidFile' => __( 'Invalid Style Variables file.', 'themify' ),
565 'delete' => __( 'Delete', 'themify' ),
566 'deleteVariableConfirm' => __( 'Once a variable is deleted, any layout using it will no longer render properly. This cannot be undone.', 'themify' ),
567 'renameVariable' => __( 'Rename variable', 'themify' ),
568 'renameVariableHint' => __( 'Click to rename variable. Renaming a variable will affect any layout that uses it.', 'themify' ),
569 'themeVarLockLabel' => __( 'Theme variable', 'themify' ),
570 'themeVarLockHint' => __( 'Theme variables can not be renamed.', 'themify' ),
571 'desktop' => __( 'Desktop', 'themify' ),
572 'tabletLandscape' => __( 'Tablet landscape', 'themify' ),
573 'tablet' => __( 'Tablet portrait', 'themify' ),
574 'mobile' => __( 'Mobile', 'themify' ),
575 'inherited' => __( 'Inherited', 'themify' ),
576 ]
577 ];
578 }
579
580 private static function get_js_vars():array {
581 return [
582 'theme' => self::get_theme_vars(),
583 'user' => self::get_user_vars(),
584 'all' => self::get_merged_vars()
585 ];
586 }
587
588 private static function get_theme_vars():array {
589 $vars = apply_filters( 'tf_sv_theme_vars', [] );
590 return self::sanitize_vars( is_array( $vars ) ? $vars : [], false );
591 }
592
593 private static function get_user_vars():array {
594 $vars = get_theme_mod( self::OPTION_KEY, [] );
595 return self::sanitize_vars( is_array( $vars ) ? $vars : [], false );
596 }
597
598 private static function merge_sv_values( array $base, array $overlay ): array {
599 foreach ( $overlay as $bp => $val ) {
600 if ( '' !== (string) $val ) {
601 $base[ $bp ] = $val;
602 }
603 }
604 return $base;
605 }
606
607 private static function get_merged_vars():array {
608 $theme = self::get_theme_vars();
609 $user = self::get_user_vars();
610 $map = [];
611 $order = [];
612
613 foreach ( $theme as $item ) {
614 $map[ $item['name'] ] = $item;
615 $order[] = $item['name'];
616 }
617 foreach ( $user as $item ) {
618 if ( isset( $map[ $item['name'] ] ) ) {
619 $map[ $item['name'] ]['values'] = self::merge_sv_values( $map[ $item['name'] ]['values'], $item['values'] );
620 $map[ $item['name'] ]['type'] = $item['type'];
621 } else {
622 $map[ $item['name'] ] = $item;
623 $order[] = $item['name'];
624 }
625 }
626
627 $out = [];
628 foreach ( $order as $name ) {
629 if ( isset( $map[ $name ] ) ) {
630 $out[] = $map[ $name ];
631 }
632 }
633 return $out;
634 }
635
636 private static function sanitize_vars( array $vars, bool $require_value = true ):array {
637 $out = [];
638 foreach ( $vars as $item ) {
639 if ( ! is_array( $item ) ) {
640 continue;
641 }
642 $name = self::sanitize_name( $item['name'] ?? '' );
643 $type = self::sanitize_type( $item['type'] ?? '' );
644 $values = self::sanitize_responsive_values( is_array( $item['values'] ?? null ) ? $item['values'] : [], $type );
645 if ( ! self::has_any_values( $values ) && isset( $item['value'] ) && ( is_string( $item['value'] ) || is_numeric( $item['value'] ) ) ) {
646 $values = self::sanitize_responsive_values( [ 'desktop' => (string) $item['value'] ], $type );
647 }
648 if ( '' === $name || '' === $type || ( $require_value && ! self::has_any_values( $values ) ) ) {
649 continue;
650 }
651 $out[] = [
652 'name' => $name,
653 'type' => $type,
654 'values' => $values
655 ];
656 }
657 return $out;
658 }
659
660 private static function sanitize_name( string $name ):string {
661 $name = strtolower( trim( $name ) );
662 $name = ltrim( $name, '-' );
663 $name = preg_replace( '/\s+/', '-', $name );
664 $name = preg_replace( '/[^a-z0-9_-]/', '', $name );
665 return trim( (string) $name, '-' );
666 }
667
668 private static function sanitize_type( string $type ):string {
669 $type = strtolower( trim( $type ) );
670 return in_array( $type, [ 'color', 'font', 'number' ], true ) ? $type : '';
671 }
672
673 private static function sanitize_value( string $value, string $type ):string {
674 $value = trim( wp_strip_all_tags( $value ) );
675 if ( 'font' === $type && 0 === strpos( $value, '{' ) ) {
676 $obj = json_decode( $value, true );
677 if ( is_array( $obj ) && ! empty( $obj['name'] ) ) {
678 $value = $obj['name'];
679 }
680 }
681 $value = preg_replace( '/[;<>]/', '', $value );
682 $value = str_replace( [ "", "\r\n", "\n", "\r" ], ' ', $value );
683 $value = preg_replace( '/\s{2,}/', ' ', $value );
684 if ( 'font' === $type ) {
685 $value = trim( $value, "\"'" );
686 if ( '---' === $value ) {
687 return '';
688 }
689 }
690 if ( '' === $value ) {
691 return '';
692 }
693 if ( 'number' === $type ) {
694 $value = preg_replace( '/\s+/', '', $value );
695 }
696 return $value;
697 }
698
699
700 private static function sanitize_responsive_values( array $values, string $type ):array {
701 $out = [
702 'desktop' => '',
703 'tablet_landscape' => '',
704 'tablet' => '',
705 'mobile' => '',
706 ];
707 foreach ( $out as $bp => $empty ) {
708 if ( array_key_exists( $bp, $values ) ) {
709 $out[ $bp ] = self::sanitize_value( (string) $values[ $bp ], $type );
710 }
711 }
712 return $out;
713 }
714
715 private static function has_any_values( array $values ):bool {
716 foreach ( $values as $value ) {
717 if ( '' !== (string) $value ) {
718 return true;
719 }
720 }
721 return false;
722 }
723
724 private static function get_effective_value( array $values, string $breakpoint ):string {
725 $order = [ 'desktop', 'tablet_landscape', 'tablet', 'mobile' ];
726 $current = '';
727 foreach ( $order as $bp ) {
728 if ( isset( $values[ $bp ] ) && '' !== (string) $values[ $bp ] ) {
729 $current = (string) $values[ $bp ];
730 }
731 if ( $bp === $breakpoint ) {
732 break;
733 }
734 }
735 return $current;
736 }
737
738 private static function get_breakpoints_payload():array {
739 $bps = function_exists( 'themify_get_breakpoints' ) ? themify_get_breakpoints() : [
740 'tablet_landscape' => [ 769, 1280 ],
741 'tablet' => [ 681, 768 ],
742 'mobile' => 680,
743 ];
744 return [
745 'desktop' => [ 'min' => null, 'max' => null ],
746 'tablet_landscape' => [ 'min' => isset( $bps['tablet_landscape'][0] ) ? (int) $bps['tablet_landscape'][0] : 769, 'max' => isset( $bps['tablet_landscape'][1] ) ? (int) $bps['tablet_landscape'][1] : 1280 ],
747 'tablet' => [ 'min' => isset( $bps['tablet'][0] ) ? (int) $bps['tablet'][0] : 681, 'max' => isset( $bps['tablet'][1] ) ? (int) $bps['tablet'][1] : 768 ],
748 'mobile' => [ 'min' => 0, 'max' => isset( $bps['mobile'] ) ? (int) $bps['mobile'] : 680 ],
749 ];
750 }
751
752 private static function normalize_var_reference( string $name ):string {
753 $name = trim( $name );
754 if ( '' === $name ) {
755 return '';
756 }
757 if ( 0 === strpos( $name, '#var(' ) ) {
758 $name = substr( $name, 1 );
759 }
760 if ( 0 === strpos( $name, 'var(' ) ) {
761 $name = preg_replace( '/^var\(\s*/', '', $name );
762 $name = preg_replace( '/\s*\)$/', '', $name );
763 }
764 $name = ltrim( $name, '#' );
765 $name = preg_replace( '/^--+/', '', $name );
766 if ( 0 === strpos( $name, 'tf_sv_' ) ) {
767 $name = substr( $name, 6 );
768 }
769 return self::sanitize_name( $name );
770 }
771
772 private static function collect_var_names_from_data( $data, array &$names ):void {
773 if ( is_array( $data ) ) {
774 foreach ( $data as $key => $value ) {
775 if ( is_string( $key ) && 0 === strpos( $key, 'tf_sv_' ) && ( is_string( $value ) || is_numeric( $value ) ) ) {
776 $name = self::normalize_var_reference( (string) $value );
777 if ( '' !== $name ) {
778 $names[ $name ] = true;
779 }
780 }
781 self::collect_var_names_from_data( $value, $names );
782 }
783 return;
784 }
785 if ( is_object( $data ) ) {
786 self::collect_var_names_from_data( get_object_vars( $data ), $names );
787 return;
788 }
789 if ( is_string( $data ) && '' !== $data ) {
790 if ( false !== strpos( $data, 'var(' ) ) {
791 preg_match_all( '/var\(\s*--([a-z0-9_-]+)\s*(?:,[^)]+)?\)/i', $data, $matches );
792 if ( ! empty( $matches[1] ) ) {
793 foreach ( $matches[1] as $match ) {
794 $name = self::normalize_var_reference( (string) $match );
795 if ( '' !== $name ) {
796 $names[ $name ] = true;
797 }
798 }
799 }
800 }
801 // Themify Customizer JSON uses bare "--slug" tokens (no var() wrapper) for colors/fonts.
802 if ( false !== strpos( $data, '--' ) ) {
803 preg_match_all( '/"(\-\-[a-zA-Z0-9_-]+)"/', $data, $bare );
804 if ( ! empty( $bare[1] ) ) {
805 foreach ( $bare[1] as $token ) {
806 $name = self::normalize_var_reference( (string) $token );
807 if ( '' !== $name ) {
808 $names[ $name ] = true;
809 }
810 }
811 }
812 }
813 }
814 }
815
816 public static function collect_used_vars( $builder_data ):array {
817 if ( empty( $builder_data ) ) {
818 return [];
819 }
820 if ( is_string( $builder_data ) ) {
821 $decoded = json_decode( $builder_data, true );
822 if ( is_array( $decoded ) ) {
823 $builder_data = $decoded;
824 }
825 }
826 $names = [];
827 self::collect_var_names_from_data( $builder_data, $names );
828 if ( empty( $names ) ) {
829 return [];
830 }
831 $all = self::get_merged_vars();
832 $by_name = [];
833 foreach ( $all as $item ) {
834 if ( ! empty( $item['name'] ) ) {
835 $by_name[ $item['name'] ] = $item;
836 }
837 }
838 $used = [];
839 foreach ( array_keys( $names ) as $name ) {
840 if ( isset( $by_name[ $name ] ) ) {
841 $used[] = $by_name[ $name ];
842 }
843 }
844 return $used;
845 }
846
847 public static function get_all_vars():array {
848 return self::get_merged_vars();
849 }
850
851 public static function import_missing_vars( array $vars ) {
852 if ( empty( $vars ) ) {
853 return;
854 }
855 foreach ( $vars as $k => $var ) {
856 if ( ! is_array( $var ) || empty( $var['name'] ) ) {
857 continue;
858 }
859 $vars[ $k ]['name'] = self::normalize_var_reference( (string) $var['name'] );
860 }
861 $vars = self::sanitize_vars( $vars, false );
862 if ( empty( $vars ) ) {
863 return;
864 }
865 $user = self::get_user_vars();
866 $user_by_name = [];
867 foreach ( $user as $i => $item ) {
868 if ( ! empty( $item['name'] ) ) {
869 $user_by_name[ $item['name'] ] = $i;
870 }
871 }
872 $added = false;
873 foreach ( $vars as $var ) {
874 if ( empty( $var['name'] ) ) {
875 continue;
876 }
877 $n = $var['name'];
878 if ( isset( $user_by_name[ $n ] ) ) {
879 $i = $user_by_name[ $n ];
880 $user[ $i ] = [
881 'name' => $n,
882 'type' => $var['type'],
883 'values' => self::merge_sv_values( $user[ $i ]['values'], $var['values'] ),
884 ];
885 $added = true;
886 continue;
887 }
888 $user[] = $var;
889 $user_by_name[ $n ] = count( $user ) - 1;
890 $added = true;
891 }
892 if ( $added ) {
893 set_theme_mod( self::OPTION_KEY, $user );
894 }
895 }
896
897 public static function ajax_collect_used() {
898 check_ajax_referer( 'tf_nonce', 'nonce' );
899 if ( ! current_user_can( 'edit_posts' ) ) {
900 wp_send_json_error( [], 403 );
901 }
902 $data = isset( $_POST['builder_json'] ) ? wp_unslash( $_POST['builder_json'] ) : '';
903 wp_send_json_success( [ 'used_sv' => self::collect_used_vars( $data ) ] );
904 }
905
906 public static function ajax_import_missing_vars() {
907 check_ajax_referer( 'tf_nonce', 'nonce' );
908 if ( ! current_user_can( 'edit_posts' ) ) {
909 wp_send_json_error( [ 'message' => __( 'Permission denied.', 'themify' ) ], 403 );
910 }
911 $vars = [];
912 if ( isset( $_POST['vars'] ) ) {
913 $decoded = json_decode( wp_unslash( $_POST['vars'] ), true );
914 if ( is_array( $decoded ) ) {
915 $vars = $decoded;
916 }
917 }
918 $before = self::get_user_vars();
919 self::import_missing_vars( $vars );
920 $after = self::get_user_vars();
921 wp_send_json_success( [
922 'vars' => self::get_js_vars(),
923 'css' => self::build_root_css(),
924 'imported' => max( 0, count( $after ) - count( $before ) ),
925 'message' => __( 'Style Variables imported.', 'themify' )
926 ] );
927 }
928 }
929
930 TF_SV_Framework::init();