PluginProbe ʕ •ᴥ•ʔ
Kirki – Freeform Page Builder, Website Builder & Customizer / 6.3.0
Kirki – Freeform Page Builder, Website Builder & Customizer v6.3.0
6.3.0 6.2.5 6.2.4 6.2.3 6.2.2 6.2.1 6.2.0 6.1.1 6.1.0 6.0.14 6.0.13 6.0.12 6.0.11 6.0.10 6.0.9 6.0.8 6.0.7 6.0.6 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 4.0.19 4.0.20 4.0.21 4.0.22 4.0.23 4.0.24 4.1 4.2.0 5.0.0 5.1.0 5.1.1 5.2.0 5.2.1 5.2.2 5.2.3 6.0.0 trunk 3.0.40 3.0.41 3.0.42 3.0.43 3.0.44 3.0.45 3.1.0 3.1.1 3.1.2
kirki / includes / Ajax / Symbol.php
kirki / includes / Ajax Last commit date
Collaboration 1 month ago Apps.php 2 days ago Collection.php 2 months ago Comments.php 4 months ago DynamicContent.php 4 weeks ago ExportImport.php 2 weeks ago Form.php 2 months ago Media.php 2 days ago Page.php 1 month ago PageSettings.php 2 months ago RBAC.php 2 months ago Symbol.php 2 days ago Taxonomy.php 4 months ago TemplateExportImport.php 2 weeks ago UserData.php 2 months ago Users.php 3 weeks ago Walkthrough.php 1 month ago WordpressData.php 4 months ago WpAdmin.php 1 month ago
Symbol.php
1087 lines
1 <?php
2 /**
3 * Manage Symbol
4 *
5 * @package kirki
6 */
7
8 namespace Kirki\Ajax;
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit; // Exit if accessed directly.
12 }
13
14 use Kirki\HelperFunctions;
15
16
17 /**
18 * Symbol API Class
19 */
20 class Symbol {
21
22 /**
23 * Create/save a symbol
24 *
25 * @return void wp_send_json.
26 *
27 * @deprecated
28 * @see \Kirki\App\Managers\SymbolManager::save()
29 */
30 public static function save() {
31 //phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
32 $post_symbol_data = isset( $_POST['data'] ) ? $_POST['data'] : null;
33 if ( ! empty( $post_symbol_data ) ) {
34 $wp_post = array(
35 'post_type' => KIRKI_SYMBOL_TYPE,
36 );
37
38 $post_id = wp_insert_post( $wp_post );
39 if ( isset( $post_id ) ) {
40
41 $symbol_data = json_decode( stripslashes( $post_symbol_data ), true );
42 $symbol_data['data'][ $symbol_data['root'] ]['properties']['symbolId'] = $post_id;
43
44 add_post_meta( $post_id, 'kirki', $symbol_data );
45 add_post_meta( $post_id, KIRKI_META_NAME_FOR_POST_EDITOR_MODE, 'kirki' );
46
47 $data = array(
48 'id' => $post_id,
49 'symbolData' => $symbol_data,
50 'type' => isset( $symbol_data['category'] ) ? $symbol_data['category'] : 'other',
51 );
52 $data['rootSizeVariant'] = self::get_symbol_root_size_variant( $symbol_data );
53 $data['html'] = self::get_symbol_html_preview( $data );
54 wp_send_json( $data );
55 }
56 };
57
58 die();
59 }
60
61 /**
62 * @deprecated
63 * @see \Kirki\App\Managers\SymbolManager::save()
64 */
65 public static function save_to_db( $data ) {
66 //phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
67 $post_symbol_data = $data ? $data : null;
68
69 if ( ! empty( $post_symbol_data ) ) {
70 unset( $post_symbol_data['id'] );
71 unset( $post_symbol_data['elementId'] );
72
73 $post_symbol_data = $post_symbol_data['symbolData'];
74
75 $wp_post = array(
76 'post_type' => KIRKI_SYMBOL_TYPE,
77 );
78
79 $post_id = wp_insert_post( $wp_post );
80
81 if ( isset( $post_id ) ) {
82
83 $symbol_data = $post_symbol_data;
84 $symbol_data['data'][ $symbol_data['root'] ]['properties']['symbolId'] = $post_id;
85
86 add_post_meta( $post_id, 'kirki', $symbol_data );
87 add_post_meta( $post_id, KIRKI_META_NAME_FOR_POST_EDITOR_MODE, 'kirki' );
88
89 $data = array(
90 'id' => $post_id,
91 'symbolData' => $symbol_data,
92 'type' => isset( $symbol_data['category'] ) ? $symbol_data['category'] : 'other',
93 );
94 $data['rootSizeVariant'] = self::get_symbol_root_size_variant( $symbol_data );
95 $data['html'] = self::get_symbol_html_preview( $data );
96
97 return $data;
98 }
99 };
100
101 return null;
102
103 }
104
105 /**
106 * Fetch symbol list
107 * if $internal is true then it will return array
108 * otherwise it will return json for api call
109 *
110 * @param boolean $internal if function call from internally.
111 * @param boolean $html if need html string.
112 * @return array|void wp_send_json.
113 */
114 public static function fetch_list( $internal = false, $html = false ) {
115 $posts = get_posts(
116 array(
117 'post_type' => KIRKI_SYMBOL_TYPE,
118 'post_status' => 'draft',
119 'numberposts' => -1,
120 )
121 );
122
123 $symbols = array();
124
125 if ( ! empty( $posts ) ) {
126 $symbols = array_map(
127 function( $post ) use ( $html, $internal ) {
128 $single_symbol = self::get_single_symbol( $post->ID, true, $html );
129 if ( ! $internal ) {
130 try {
131 unset( $single_symbol['symbolData']['data'] );
132 unset( $single_symbol['symbolData']['styleBlocks'] );
133 } catch ( \Throwable $th ) {
134 // throw $th;
135 }
136 }
137 return $single_symbol;
138 },
139 $posts
140 );
141 }
142
143 if ( $internal ) {
144 return $symbols;
145 }
146 wp_send_json( $symbols );
147 }
148 /**
149 * Fetch symbol
150 *
151 * @return void wp_send_json.
152 */
153 public static function fetch_symbol() {
154 $symbol_id = HelperFunctions::sanitize_text( isset( $_POST['id'] ) ? $_POST['id'] : null );
155 $symbol_element_prop = isset( $_POST['symbolElementProp'] ) ? $_POST['symbolElementProp'] : null;
156 $component_field_values = isset( $_POST['componentFieldValues'] ) ? $_POST['componentFieldValues'] : null;
157 $collection_item = isset( $_POST['collectionItem'] ) ? $_POST['collectionItem'] : null;
158 $variable_css = isset( $_POST['variableCSS'] ) ? $_POST['variableCSS'] : true;
159 $options = $collection_item && 'false' !== $collection_item
160 ? json_decode( stripslashes( $collection_item ), true )
161 : array();
162 $options = is_array( $options ) ? $options : array();
163 foreach ( $options as $key => $value ) {
164 if ( is_array( $value ) && $key !== 'user' ) {
165 $options[ $key ] = json_decode( json_encode( $value ) );
166 }
167 }
168 self::get_single_symbol( $symbol_id, false, true, $symbol_element_prop, $options, $variable_css, $component_field_values );
169 }
170
171
172 /**
173 * get single symbol
174 * if $internal is true then it will return array
175 * otherwise it will return json for api call
176 *
177 * @param int $symbol_id symbol id.
178 * @param boolean $internal if the function call from internally.
179 * @param boolean $html if need html preview string.
180 * @param array $symbol_element_prop Legacy element-level instance overrides.
181 * @param array $options Rendering context such as the current collection item.
182 * @param boolean $variable_css Whether variable CSS is required.
183 * @param array $component_field_values Public values keyed by component-field ID.
184 * @return array|void
185 */
186 public static function get_single_symbol(
187 $symbol_id = null,
188 $internal = false,
189 $html = false,
190 $symbol_element_prop = false,
191 $options = array(),
192 $variable_css = true,
193 $component_field_values = false
194 ) {
195 //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated,WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
196 $symbol_id = $symbol_id ? $symbol_id : HelperFunctions::sanitize_text( isset( $_GET['symbol_id'] ) ? $_GET['symbol_id'] : null );
197 $post = get_post( $symbol_id );
198 $symbol = null;
199 if ( $post && $post->post_type == KIRKI_SYMBOL_TYPE ) {
200 $symbol = array();
201 $symbol_data = get_post_meta( $post->ID, 'kirki', true );
202 $symbol['id'] = $post->ID;
203 $symbol['symbolData'] = $symbol_data;
204 $symbol['type'] = isset( $symbol_data['category'] ) ? $symbol_data['category'] : 'other';
205 $symbol['setAs'] = isset( $symbol_data['setAs'] ) ? $symbol_data['setAs'] : '';
206 $symbol['rootSizeVariant'] = self::get_symbol_root_size_variant( $symbol_data );
207
208 if ( ! $internal ) {
209 $symbol_element_prop = json_decode( stripslashes( $symbol_element_prop ), true );
210 $component_field_values = json_decode( stripslashes( $component_field_values ), true );
211 }
212 if ( $symbol_element_prop !== false && is_array( $symbol_element_prop ) ) {
213 // Append instance-only root styles after the master's styles so explicit overrides win.
214 $root_style_override = isset( $symbol_element_prop['__rootStyle']['variant'] )
215 ? array_filter(
216 array_map(
217 array( self::class, 'get_symbol_root_size_css' ),
218 $symbol_element_prop['__rootStyle']['variant']
219 )
220 )
221 : array();
222 $root_id = isset( $symbol['symbolData']['root'] ) ? $symbol['symbolData']['root'] : null;
223
224 if ( $root_id && ! empty( $root_style_override ) && isset( $symbol['symbolData']['data'][ $root_id ] ) ) {
225 $override_style_id = uniqid( 'kirki-instance-style-' );
226 $symbol['symbolData']['styleBlocks'][ $override_style_id ] = array(
227 'id' => $override_style_id,
228 'type' => 'class',
229 // Two classes make the instance rule more specific than master rules
230 // injected later by another instance of the same symbol.
231 'name' => array( $override_style_id, 'kirki-symbol-root-size-override' ),
232 'variant' => $root_style_override,
233 );
234 $root_style_ids = isset( $symbol['symbolData']['data'][ $root_id ]['styleIds'] )
235 ? $symbol['symbolData']['data'][ $root_id ]['styleIds']
236 : array();
237 $symbol['symbolData']['data'][ $root_id ]['styleIds'] = array_merge(
238 $root_style_ids,
239 array( $override_style_id )
240 );
241 }
242
243 foreach ( $symbol_element_prop as $key => $value ) {
244 foreach ( $symbol['symbolData']['data'] as $key2 => $value2 ) {
245 $has_symbol_el_prop_id = isset( $value2['properties']['symbolElPropId'] )
246 && '' !== $value2['properties']['symbolElPropId'];
247 $matches_symbol_el_prop = $has_symbol_el_prop_id && $value2['properties']['symbolElPropId'] == $key;
248 $matches_legacy_element = ! $has_symbol_el_prop_id && (
249 $key2 == $key || ( isset( $value2['id'] ) && $value2['id'] == $key )
250 );
251 if ( $matches_symbol_el_prop || $matches_legacy_element ) {
252 if ( isset( $value['contents'] ) ) {
253 $symbol['symbolData']['data'][ $key2 ]['properties']['contents'] = $value['contents'];
254 }
255 if ( isset( $value['contentElement'] ) ) {
256 foreach ( $value['contentElement'] as $new_item ) {
257 if ( isset( $new_item['id'] ) ) {
258 $symbol['symbolData']['data'][ $new_item['id'] ] = $new_item;
259 }
260 }
261 }
262 if ( isset( $value['attributes'] ) ) {
263 $symbol['symbolData']['data'][ $key2 ]['properties']['attributes'] = $value['attributes'];
264 }
265 if ( isset( $value['wp_attachment_id'] ) ) {
266 $symbol['symbolData']['data'][ $key2 ]['properties']['wp_attachment_id'] = $value['wp_attachment_id'];
267 }
268 if ( isset( $value['lottie'] ) ) {
269 $symbol['symbolData']['data'][ $key2 ]['properties']['lottie'] = $value['lottie'];
270 }
271 if ( isset( $value['svgOuterHtml'] ) ) {
272 $symbol['symbolData']['data'][ $key2 ]['properties']['svgOuterHtml'] = $value['svgOuterHtml'];
273 }
274 if ( isset( $value['tag'] ) ) {
275 $symbol['symbolData']['data'][ $key2 ]['properties']['tag'] = $value['tag'];
276 }
277 if ( isset( $value['type'] ) ) {
278 $symbol['symbolData']['data'][ $key2 ]['properties']['type'] = $value['type'];
279 }
280 if ( isset( $value['isActive'] ) ) {
281 $symbol['symbolData']['data'][ $key2 ]['properties']['isActive'] = $value['isActive'];
282 }
283 if ( isset( $value['preload'] ) ) {
284 $symbol['symbolData']['data'][ $key2 ]['properties']['preload'] = $value['preload'];
285 }
286 if ( isset( $value['dynamicContent'] ) ) {
287 $symbol['symbolData']['data'][ $key2 ]['properties']['dynamicContent'] = $value['dynamicContent'];
288 }
289 if ( isset( $value['symbolElProps'] ) && is_array( $value['symbolElProps'] ) ) {
290 $current_nested_props = isset( $symbol['symbolData']['data'][ $key2 ]['properties']['symbolElProps'] )
291 ? $symbol['symbolData']['data'][ $key2 ]['properties']['symbolElProps']
292 : array();
293 $symbol['symbolData']['data'][ $key2 ]['properties']['symbolElProps'] = array_replace_recursive(
294 $current_nested_props,
295 $value['symbolElProps']
296 );
297 }
298 if (
299 isset( $symbol['symbolData']['data'][ $key2 ]['properties']['dynamicContent'] ) &&
300 $symbol['symbolData']['data'][ $key2 ]['properties']['dynamicContent']['type'] === 'manual'
301 ) {
302 unset( $symbol['symbolData']['data'][ $key2 ]['properties']['dynamicContent'] );
303 }
304 }
305 }
306 }
307 }
308
309 if ( is_array( $component_field_values ) ) {
310 $symbol['symbolData'] = self::apply_component_field_values(
311 $symbol['symbolData'],
312 $component_field_values
313 );
314 }
315
316 if ( $html === true ) {
317 $symbol['html'] = self::get_symbol_html_preview( $symbol, $options, $variable_css );
318 }
319 }
320
321 if ( $internal ) {
322 return $symbol;
323 }
324 wp_send_json( $symbol );
325 }
326
327 /**
328 * Resolve public component-field values into the symbol's internal elements.
329 *
330 * @param array $symbol_data Symbol definition.
331 * @param array $field_values Values keyed by public component-field ID.
332 * @return array
333 */
334 public static function apply_component_field_values( $symbol_data, $field_values ) {
335 if ( empty( $symbol_data['data'] ) || ! is_array( $field_values ) ) {
336 return $symbol_data;
337 }
338
339 $effective_values = array();
340 foreach ( isset( $symbol_data['fields'] ) ? $symbol_data['fields'] : array() as $field ) {
341 if ( ! isset( $field['id'] ) || ! array_key_exists( $field['id'], $field_values ) ) {
342 continue;
343 }
344
345 $effective_values[ $field['id'] ] = $field_values[ $field['id'] ];
346 }
347
348 if ( isset( $field_values['__nested'] ) && is_array( $field_values['__nested'] ) ) {
349 self::apply_nested_component_field_values( $symbol_data['data'], $field_values['__nested'] );
350 }
351
352 foreach ( $symbol_data['data'] as &$element ) {
353 $bindings = isset( $element['properties']['componentFieldBindings'] )
354 ? $element['properties']['componentFieldBindings']
355 : array();
356
357 foreach ( $bindings as $slot => $stored_field_ids ) {
358 $field_ids = is_array( $stored_field_ids ) ? $stored_field_ids : array( $stored_field_ids );
359 foreach ( $field_ids as $field_id ) {
360 if ( null === $field_id || ! array_key_exists( $field_id, $effective_values ) ) {
361 continue;
362 }
363
364 if ( 0 === strpos( $slot, 'instanceField:' ) ) {
365 self::assign_nested_component_field_value(
366 $element,
367 $slot,
368 $effective_values[ $field_id ]
369 );
370 continue;
371 }
372
373 self::apply_component_field_value_to_element(
374 $element,
375 $slot,
376 $effective_values[ $field_id ]
377 );
378 }
379 }
380 }
381 unset( $element );
382
383 return $symbol_data;
384 }
385
386 /**
387 * Attach values intended for deeper nested instances to their immediate owner.
388 *
389 * @param array $data Symbol elements.
390 * @param array $nested_values Nested values keyed by symbolElPropId.
391 * @return void
392 */
393 private static function apply_nested_component_field_values( &$data, $nested_values ) {
394 foreach ( $data as &$element ) {
395 $symbol_el_prop_id = isset( $element['properties']['symbolElPropId'] )
396 ? $element['properties']['symbolElPropId']
397 : null;
398
399 if ( ! $symbol_el_prop_id || ! isset( $nested_values[ $symbol_el_prop_id ] ) ) {
400 continue;
401 }
402
403 $current_values = isset( $element['properties']['componentFieldValues'] )
404 ? $element['properties']['componentFieldValues']
405 : array();
406 $element['properties']['componentFieldValues'] = self::merge_component_field_values(
407 $current_values,
408 $nested_values[ $symbol_el_prop_id ]
409 );
410 }
411 unset( $element );
412 }
413
414 /**
415 * Merge nested routing maps while replacing each field entry as one value.
416 *
417 * @param array $current_values Existing values on the nested instance.
418 * @param array $next_values Incoming field values.
419 * @return array
420 */
421 private static function merge_component_field_values( $current_values, $next_values ) {
422 $result = is_array( $current_values ) ? $current_values : array();
423 if ( ! is_array( $next_values ) ) {
424 return $result;
425 }
426
427 foreach ( $next_values as $key => $value ) {
428 if ( '__nested' === $key && is_array( $value ) ) {
429 if ( ! isset( $result['__nested'] ) || ! is_array( $result['__nested'] ) ) {
430 $result['__nested'] = array();
431 }
432
433 foreach ( $value as $symbol_el_prop_id => $nested_field_values ) {
434 $current_nested_values = isset( $result['__nested'][ $symbol_el_prop_id ] )
435 ? $result['__nested'][ $symbol_el_prop_id ]
436 : array();
437 $result['__nested'][ $symbol_el_prop_id ] = self::merge_component_field_values(
438 $current_nested_values,
439 $nested_field_values
440 );
441 }
442 continue;
443 }
444
445 $result[ $key ] = $value;
446 }
447
448 return $result;
449 }
450
451 /**
452 * Convert an instanceField binding path into nested component field values.
453 *
454 * @param array $element Nested symbol instance element.
455 * @param string $slot Binding slot.
456 * @param mixed $value Field value.
457 * @return void
458 */
459 private static function assign_nested_component_field_value( &$element, $slot, $value ) {
460 $path = array_values(
461 array_filter(
462 explode( ':', substr( $slot, strlen( 'instanceField:' ) ) ),
463 'strlen'
464 )
465 );
466 $field_id = array_pop( $path );
467
468 if ( ! $field_id ) {
469 return;
470 }
471
472 if ( ! isset( $element['properties']['componentFieldValues'] ) ) {
473 $element['properties']['componentFieldValues'] = array();
474 }
475
476 $target_values =& $element['properties']['componentFieldValues'];
477 foreach ( $path as $symbol_el_prop_id ) {
478 if ( ! isset( $target_values['__nested'][ $symbol_el_prop_id ] ) ) {
479 $target_values['__nested'][ $symbol_el_prop_id ] = array();
480 }
481 $target_values =& $target_values['__nested'][ $symbol_el_prop_id ];
482 }
483 $target_values[ $field_id ] = $value;
484 }
485
486 /**
487 * Unwrap a compact component-field entry.
488 *
489 * @param mixed $entry Stored entry.
490 * @return array
491 */
492 private static function get_component_field_entry( $entry ) {
493 $is_wrapped = is_array( $entry )
494 && isset( $entry['__componentFieldValue'] )
495 && true === $entry['__componentFieldValue'];
496
497 return array(
498 'value' => $is_wrapped && array_key_exists( 'value', $entry ) ? $entry['value'] : $entry,
499 'dynamicContent' => $is_wrapped && isset( $entry['dynamicContent'] ) ? $entry['dynamicContent'] : null,
500 );
501 }
502
503 /**
504 * Apply one typed field value to an element property slot.
505 *
506 * @param array $element Element data.
507 * @param string $slot Component field slot.
508 * @param mixed $entry Compact field value entry.
509 * @return void
510 */
511 private static function apply_component_field_value_to_element( &$element, $slot, $entry ) {
512 $field_entry = self::get_component_field_entry( $entry );
513 $value = $field_entry['value'];
514
515 if ( ! isset( $element['properties'] ) ) {
516 $element['properties'] = array();
517 }
518 if ( ! isset( $element['properties']['attributes'] ) ) {
519 $element['properties']['attributes'] = array();
520 }
521
522 if ( null !== $field_entry['dynamicContent'] ) {
523 $dynamic_property = 'imageAlt' === $slot ? 'dynamicAlt' : 'dynamicContent';
524 $element['properties'][ $dynamic_property ] = $field_entry['dynamicContent'];
525 return;
526 }
527
528 // An explicit static instance value must win over an older dynamic override.
529 $dynamic_property = 'imageAlt' === $slot ? 'dynamicAlt' : 'dynamicContent';
530 unset( $element['properties'][ $dynamic_property ] );
531
532 switch ( $slot ) {
533 case 'content':
534 $element['properties']['contents'] = array( (string) $value );
535 break;
536
537 case 'imageSource':
538 case 'videoSource':
539 $media = is_array( $value ) ? $value : array( 'url' => $value );
540 $src = isset( $media['url'] ) ? $media['url'] : ( isset( $media['sources']['original'] ) ? $media['sources']['original'] : '' );
541 $element['properties']['attributes']['src'] = $src;
542 if ( isset( $media['name'] ) ) {
543 $element['properties']['attributes']['name'] = $media['name'];
544 }
545 $element['properties']['wp_attachment_id'] = isset( $media['id'] ) ? $media['id'] : '';
546 break;
547
548 case 'imageAlt':
549 $element['properties']['attributes']['alt'] = (string) $value;
550 break;
551
552 case 'linkUrl':
553 $link = is_array( $value ) ? $value : array();
554 $element['properties']['type'] = isset( $link['type'] ) ? $link['type'] : 'href';
555 $element['properties']['preload'] = isset( $link['preload'] ) ? $link['preload'] : 'default';
556 $element['properties']['attributes'] = isset( $link['attributes'] )
557 ? array_replace( $element['properties']['attributes'], $link['attributes'] )
558 : array_replace(
559 $element['properties']['attributes'],
560 array(
561 'href' => (string) $value,
562 'target' => '',
563 'rel' => array( '' ),
564 )
565 );
566 break;
567
568 case 'icon':
569 $icon = is_array( $value ) ? $value : array();
570 $element['properties']['tag'] = 'svg';
571 $element['properties']['name'] = isset( $icon['name'] ) ? $icon['name'] : '';
572 $element['properties']['svgOuterHtml'] = isset( $icon['svgOuterHtml'] ) ? $icon['svgOuterHtml'] : '';
573 $element['properties']['library'] = isset( $icon['library'] ) ? $icon['library'] : '';
574 break;
575
576 case 'svg':
577 $element['properties']['svgOuterHtml'] = (string) $value;
578 break;
579
580 case 'color':
581 case 'fill':
582 self::apply_component_fill_value( $element, $slot, $value );
583 break;
584 }
585 }
586
587 /**
588 * Apply color/fill values to an element's inline declarations.
589 *
590 * @param array $element Element data.
591 * @param string $slot Color or fill slot.
592 * @param mixed $value Fill value.
593 * @return void
594 */
595 private static function apply_component_fill_value( &$element, $slot, $value ) {
596 $fill = is_array( $value ) ? $value : array( 'type' => 'solid', 'value' => $value );
597 $type = isset( $fill['type'] ) ? $fill['type'] : null;
598 if ( ! $type ) {
599 return;
600 }
601
602 $element['properties']['value'] = $value;
603 $background_image = self::get_component_background_image( $fill );
604 if ( 'color' === $slot ) {
605 $declarations = 'solid' === $type
606 ? array(
607 'color' => isset( $fill['value'] ) ? $fill['value'] : '',
608 'background-color' => 'transparent',
609 'background-image' => 'none',
610 'background-clip' => 'unset',
611 '-webkit-background-clip' => 'unset',
612 '-webkit-text-fill-color' => 'unset',
613 )
614 : array(
615 'color' => 'transparent',
616 'background-color' => 'transparent',
617 'background-image' => $background_image,
618 'background-clip' => 'text',
619 '-webkit-background-clip' => 'text',
620 '-webkit-text-fill-color' => 'transparent',
621 );
622 } else {
623 $declarations = 'solid' === $type
624 ? array(
625 'background-color' => isset( $fill['value'] ) ? $fill['value'] : '',
626 'background-image' => 'none',
627 )
628 : array(
629 'background-color' => 'transparent',
630 'background-image' => $background_image,
631 );
632 }
633
634 $current_style = isset( $element['properties']['attributes']['style'] )
635 ? $element['properties']['attributes']['style']
636 : '';
637 $element['properties']['attributes']['style'] = self::update_inline_style( $current_style, $declarations );
638 }
639
640 /**
641 * Convert a fill object to a CSS background image.
642 *
643 * @param array $fill Fill configuration.
644 * @return string
645 */
646 private static function get_component_background_image( $fill ) {
647 $type = isset( $fill['type'] ) ? $fill['type'] : '';
648 if ( 'image' === $type ) {
649 return isset( $fill['value'] ) && $fill['value'] ? $fill['value'] : 'url("#")';
650 }
651 if ( 'none' === $type ) {
652 return 'none';
653 }
654
655 $stops = array();
656 foreach ( isset( $fill['stops'] ) ? $fill['stops'] : array() as $stop ) {
657 $stops[] = ( isset( $stop['color'] ) ? $stop['color'] : '' ) . ' ' . self::add_component_css_unit( isset( $stop['position'] ) ? $stop['position'] : array() );
658 }
659 $color_stops = implode( ', ', $stops );
660 $repeating = ! empty( $fill['repeating'] ) ? 'repeating-' : '';
661
662 if ( 'linear' === $type ) {
663 $angle = isset( $fill['angle'] ) ? $fill['angle'] : array( 'value' => 90, 'unit' => 'deg' );
664 return $repeating . 'linear-gradient(' . self::add_component_css_unit( $angle, 90, 'deg' ) . ', ' . $color_stops . ')';
665 }
666 if ( 'radial' === $type ) {
667 $position = isset( $fill['position'] ) ? $fill['position'] : array();
668 $size = isset( $fill['size'] ) ? $fill['size'] : array();
669 return $repeating . 'radial-gradient(ellipse '
670 . self::add_component_css_unit( isset( $size['x'] ) ? $size['x'] : array() ) . ' '
671 . self::add_component_css_unit( isset( $size['y'] ) ? $size['y'] : array() ) . ' at '
672 . self::add_component_css_unit( isset( $position['x'] ) ? $position['x'] : array() ) . ' '
673 . self::add_component_css_unit( isset( $position['y'] ) ? $position['y'] : array() ) . ', '
674 . $color_stops . ')';
675 }
676 if ( 'conic' === $type ) {
677 $position = isset( $fill['position'] ) ? $fill['position'] : array();
678 $angle = isset( $fill['conicAngle'] ) ? $fill['conicAngle'] : 0;
679 return $repeating . 'conic-gradient(from ' . $angle . ' at '
680 . self::add_component_css_unit( isset( $position['x'] ) ? $position['x'] : array() ) . ' '
681 . self::add_component_css_unit( isset( $position['y'] ) ? $position['y'] : array() ) . ', '
682 . $color_stops . ')';
683 }
684
685 return '';
686 }
687
688 /**
689 * Add a unit to a component fill value.
690 *
691 * @param mixed $value Value or value/unit pair.
692 * @param mixed $default_value Default numeric value.
693 * @param string $default_unit Default unit.
694 * @return string
695 */
696 private static function add_component_css_unit( $value, $default_value = 0, $default_unit = '%' ) {
697 if ( is_array( $value ) ) {
698 $number = isset( $value['value'] ) ? $value['value'] : $default_value;
699 $unit = isset( $value['unit'] ) ? $value['unit'] : $default_unit;
700 return $number . $unit;
701 }
702
703 return (string) $value;
704 }
705
706 /**
707 * Merge CSS declarations into an inline style string.
708 *
709 * @param string $style Existing style.
710 * @param array $declarations Declarations to set.
711 * @return string
712 */
713 private static function update_inline_style( $style, $declarations ) {
714 $style_map = array();
715 foreach ( explode( ';', (string) $style ) as $declaration ) {
716 $parts = explode( ':', $declaration, 2 );
717 if ( 2 === count( $parts ) && '' !== trim( $parts[0] ) ) {
718 $style_map[ trim( $parts[0] ) ] = trim( $parts[1] );
719 }
720 }
721
722 foreach ( $declarations as $property => $property_value ) {
723 if ( '' !== $property_value && null !== $property_value ) {
724 $style_map[ $property ] = $property_value;
725 } else {
726 unset( $style_map[ $property ] );
727 }
728 }
729
730 $next_style = array();
731 foreach ( $style_map as $property => $property_value ) {
732 $next_style[] = $property . ': ' . $property_value;
733 }
734 return implode( '; ', $next_style );
735 }
736
737 /**
738 * Get the CSS properties that control a symbol instance root's size.
739 *
740 * @return array
741 */
742 private static function get_symbol_root_size_properties() {
743 return array( 'width', 'height', 'min-width', 'max-width', 'min-height', 'max-height' );
744 }
745
746 /**
747 * Keep only root sizing declarations for a symbol instance root.
748 *
749 * @param string $css CSS declarations.
750 * @return string
751 */
752 public static function get_symbol_root_size_css( $css ) {
753 $property_pattern = implode( '|', self::get_symbol_root_size_properties() );
754
755 preg_match_all(
756 '/(?:^|;)\s*(' . $property_pattern . ')\s*:\s*([^;]*)(?=;|$)/i',
757 $css,
758 $matches,
759 PREG_SET_ORDER
760 );
761
762 $size_css = '';
763 foreach ( $matches as $match ) {
764 $value = trim( $match[2] );
765 if ( '' !== $value ) {
766 $size_css .= strtolower( $match[1] ) . ':' . $value . ';';
767 }
768 }
769
770 return $size_css;
771 }
772
773 /**
774 * Get one root size declaration from CSS.
775 *
776 * @param string $css CSS declarations.
777 * @param string $property Root sizing property.
778 * @return string
779 */
780 public static function get_symbol_root_size_property_css( $css, $property ) {
781 if ( ! in_array( $property, self::get_symbol_root_size_properties(), true ) ) {
782 return '';
783 }
784
785 preg_match(
786 '/(?:^|;)\s*' . $property . '\s*:\s*([^;]*)(?=;|$)/i',
787 $css,
788 $match
789 );
790 $value = isset( $match[1] ) ? trim( $match[1] ) : '';
791
792 return '' !== $value ? $property . ':' . $value . ';' : '';
793 }
794
795 /**
796 * Remove root sizing declarations from a CSS declaration string.
797 *
798 * @param string $css CSS declarations.
799 * @return string
800 */
801 public static function remove_symbol_root_size_css( $css ) {
802 $property_pattern = implode( '|', self::get_symbol_root_size_properties() );
803 $css = ';' . $css;
804 $css = preg_replace(
805 '/;\s*(?:' . $property_pattern . ')\s*:\s*[^;]*(?=;|$)/i',
806 '',
807 $css
808 );
809
810 return ltrim( $css, ';' );
811 }
812
813 /**
814 * Replace inherited root sizes while retaining unrelated instance CSS.
815 *
816 * @param array $current_variant Current instance variants.
817 * @param array $root_size_variant Current master size variants.
818 * @param array $overrides Explicit instance overrides by variant.
819 * @return array
820 */
821 public static function merge_symbol_root_size_variant( $current_variant, $root_size_variant, $overrides = array() ) {
822 $current_variant = is_array( $current_variant ) ? $current_variant : array();
823 $root_size_variant = is_array( $root_size_variant ) ? $root_size_variant : array();
824 $overrides = is_array( $overrides ) ? $overrides : array();
825 $variant_keys = array_unique( array_merge( array_keys( $current_variant ), array_keys( $root_size_variant ) ) );
826 $next_variant = array();
827
828 foreach ( $variant_keys as $variant_key ) {
829 $current_css = isset( $current_variant[ $variant_key ] ) ? $current_variant[ $variant_key ] : '';
830 $master_css = isset( $root_size_variant[ $variant_key ] ) ? $root_size_variant[ $variant_key ] : '';
831 $variant_overrides = isset( $overrides[ $variant_key ] ) && is_array( $overrides[ $variant_key ] )
832 ? $overrides[ $variant_key ]
833 : array();
834 $css = self::remove_symbol_root_size_css( $current_css );
835
836 foreach ( self::get_symbol_root_size_properties() as $property ) {
837 $source_css = in_array( $property, $variant_overrides, true ) ? $current_css : $master_css;
838 $css .= self::get_symbol_root_size_property_css( $source_css, $property );
839 }
840
841 if ( '' !== $css ) {
842 $next_variant[ $variant_key ] = $css;
843 }
844 }
845
846 return $next_variant;
847 }
848
849 /**
850 * Get the responsive root sizing declarations from a symbol master root.
851 *
852 * @param array $symbol_data Symbol data payload.
853 * @return array
854 */
855 public static function get_symbol_root_size_variant( $symbol_data ) {
856 $root_id = isset( $symbol_data['root'] ) ? $symbol_data['root'] : null;
857 $root = $root_id && isset( $symbol_data['data'][ $root_id ] )
858 ? $symbol_data['data'][ $root_id ]
859 : null;
860
861 if ( ! $root ) {
862 return array();
863 }
864
865 $properties = isset( $root['properties'] ) ? $root['properties'] : array();
866 $style_ids = array_merge(
867 isset( $properties['classesIds'] ) ? $properties['classesIds'] : array(),
868 isset( $root['styleIds'] ) ? $root['styleIds'] : array()
869 );
870 $variants = array();
871
872 foreach ( $style_ids as $style_id ) {
873 if ( empty( $symbol_data['styleBlocks'][ $style_id ]['variant'] ) ) {
874 continue;
875 }
876
877 foreach ( $symbol_data['styleBlocks'][ $style_id ]['variant'] as $variant_key => $css ) {
878 $size_css = self::get_symbol_root_size_css( $css );
879 if ( '' === $size_css ) {
880 continue;
881 }
882
883 $variants[ $variant_key ] = isset( $variants[ $variant_key ] )
884 ? $variants[ $variant_key ] . $size_css
885 : $size_css;
886 }
887 }
888
889 return $variants;
890 }
891
892 /**
893 * @deprecated
894 * @see \Kirki\App\Managers\SymbolManager::get_preview_html()
895 */
896 private static function get_symbol_html_preview( $symbol, $options = array(), $variable_css = true ) {
897 if ( ! $symbol['symbolData'] ) {
898 return '';
899 }
900 $symbol_data = $symbol['symbolData'];
901
902 $params = array(
903 'blocks' => $symbol_data['data'],
904 'style_blocks' => $symbol_data['styleBlocks'],
905 'root' => $symbol_data['root'],
906 'post_id' => $symbol['id'],
907 'options' => array_merge(
908 $options,
909 array( 'include_component_field_metadata' => true )
910 ),
911 'get_style' => true,
912 'get_variable' => HelperFunctions::isTruthy( $variable_css ),
913 'should_take_app_script' => false,
914 'prefix' => 'kirki-s' . $symbol['id'],
915 );
916
917 $s = HelperFunctions::get_html_using_preview_script( $params );
918 return $s;
919 }
920
921 /**
922 * Update Symbol data
923 *
924 * @return void wp_send_json.
925 */
926 public static function update() {
927 //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated,WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
928 $symbol_id = HelperFunctions::sanitize_text( isset( $_POST['symbol_id'] ) ? $_POST['symbol_id'] : null );
929 //phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
930 $post_symbol_data = isset( $_POST['data'] ) ? $_POST['data'] : null;
931 if ( $symbol_id && ! empty( $post_symbol_data ) ) {
932 $data = json_decode( stripslashes( $post_symbol_data ), true );
933 $symbol_data = get_post_meta( $symbol_id, 'kirki', true );
934
935 if ( $symbol_data ) {
936 if ( isset( $data['data'] ) ) {
937 $symbol_data['data'] = $data['data'];
938 }
939 if ( isset( $data['styleBlocks'] ) ) {
940 $symbol_data['styleBlocks'] = $data['styleBlocks'];
941 }
942 if ( isset( $data['name'] ) ) {
943 $symbol_data['name'] = $data['name'];
944 }
945 if ( isset( $data['category'] ) ) {
946 $symbol_data['category'] = $data['category'];
947 }
948 if ( isset( $data['root'] ) ) {
949 $symbol_data['root'] = $data['root'];
950 }
951 if ( array_key_exists( 'fields', $data ) ) {
952 $symbol_data['fields'] = $data['fields'];
953 }
954 // if ( isset( $data['conditions'] ) ) {
955 // $symbol_data['conditions'] = $data['conditions'];
956 // }
957
958 $set_as_toggle_success = true;
959 if ( isset( $data['setAs'] ) ) {
960 $symbol_data['setAs'] = $data['setAs'];
961
962 if ( $data['setAs'] != '' ) {
963 $all_symbol = self::fetch_list( true );
964 foreach ( $all_symbol as $key => $value ) {
965 if ( $value['id'] != $symbol_id && isset( $value['symbolData'], $value['symbolData']['setAs'] ) && $value['symbolData']['setAs'] == $data['setAs'] ) {
966 $all_symbol[ $key ]['symbolData']['setAs'] = '';
967 $set_as_toggle_success = update_post_meta( $value['id'], 'kirki', $all_symbol[ $key ]['symbolData'] );
968 }
969 }
970 }
971 }
972
973 if ( $set_as_toggle_success !== true ) {
974 wp_send_json( false );
975 }
976
977 $updated = update_post_meta( $symbol_id, 'kirki', $symbol_data );
978
979 $updated === true && $set_as_toggle_success === true ? wp_send_json( self::get_single_symbol( $symbol_id, true, true ) ) : wp_send_json( false );
980 } else {
981 wp_send_json( false );
982 }
983 }
984 die();
985 }
986
987 /**
988 * Delete a symbol
989 *
990 * @return void
991 */
992 public static function delete() {
993 //phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
994 $symbol_id = HelperFunctions::sanitize_text( isset( $_POST['symbol_id'] ) ? $_POST['symbol_id'] : null );
995 if ( $symbol_id ) {
996 $post = wp_delete_post( $symbol_id );
997 isset( $post ) ? wp_send_json( true ) : wp_send_json( false );
998 }
999 die();
1000 }
1001
1002 public static function get_pre_built_html_using_url() {
1003 $raw_url = isset( $_GET['elementUrl'] ) ? wp_unslash( $_GET['elementUrl'] ) : '';
1004
1005 if ( empty( $raw_url ) ) {
1006 wp_send_json_error( 'Missing elementUrl', 400 );
1007 }
1008 $allowed_host = wp_parse_url( home_url(), PHP_URL_HOST );
1009 $requested = wp_parse_url( $raw_url );
1010
1011 if (
1012 ! $requested ||
1013 empty( $requested['host'] ) ||
1014 strtolower( $requested['host'] ) !== strtolower( $allowed_host )
1015 ) {
1016 wp_send_json_error( 'URL not permitted', 403 );
1017 }
1018
1019 $resolved_ip = gethostbyname( $requested['host'] );
1020
1021 if ( self::is_private_or_loopback_ip( $resolved_ip ) ) {
1022 wp_send_json_error( 'URL not permitted', 403 );
1023 }
1024
1025 $safe_url = self::rebuild_url( $requested );
1026
1027 $response = HelperFunctions::http_get( $safe_url, array(
1028 'timeout' => 30,
1029 'sslverify' => false,
1030 'redirection' => 0,
1031 ));
1032
1033 if ( is_wp_error( $response ) ) {
1034 wp_send_json_error( 'Request failed', 502 );
1035 }
1036
1037 $body = wp_remote_retrieve_body( $response );
1038 $data = json_decode( $body, true );
1039
1040
1041 $params = array(
1042 'blocks' => $data['blocks'],
1043 'style_blocks' => $data['styles'],
1044 'root' => $data['root'],
1045 'post_id' => false,
1046 'options' => array(),
1047 'get_style' => true,
1048 'get_variable' => false,
1049 'should_take_app_script' => false,
1050 );
1051
1052 $html = HelperFunctions::get_html_using_preview_script( $params );
1053
1054 wp_send_json_success( $html );
1055 }
1056
1057 /**
1058 * Returns true for any IP that should never be reached from a server-side fetch.
1059 * Covers loopback, RFC 1918, link-local (APIPA / cloud metadata), and IPv6 equivalents.
1060 */
1061 private static function is_private_or_loopback_ip( string $ip ): bool {
1062 if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
1063 return true;
1064 }
1065
1066 return ! filter_var(
1067 $ip,
1068 FILTER_VALIDATE_IP,
1069 FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
1070 );
1071 }
1072
1073
1074 /**
1075 * Reconstructs a URL from wp_parse_url parts so the raw user string
1076 * is never passed directly to the HTTP client.
1077 */
1078 private static function rebuild_url( array $parts ): string {
1079 $scheme = isset( $parts['scheme'] ) && strtolower( $parts['scheme'] ) === 'https' ? 'https' : 'https';
1080 $host = $parts['host'];
1081 $path = isset( $parts['path'] ) ? $parts['path'] : '/';
1082 $query = isset( $parts['query'] ) ? '?' . $parts['query'] : '';
1083
1084 return "{$scheme}://{$host}{$path}{$query}";
1085 }
1086 }
1087