| 1 |
<?php |
| 2 |
/** |
| 3 |
* Global colours, typography and site-level globals. |
| 4 |
* |
| 5 |
* ── How the browser does it ───────────────────────────────────────────────── |
| 6 |
* |
| 7 |
* import_temp_main.js#get_global_data() walks every template in the kit and collects each |
| 8 |
* template's `global_data.color` and `global_data.typography`, de-duplicating by `_id` |
| 9 |
* (Elementor) or `id` (Gutenberg), and builds `font_family` from the typography titles. |
| 10 |
* The user can then recolour them on the preview screen; the result lands on |
| 11 |
* `site_obj.kit_global` / `site_obj.font_family`. |
| 12 |
* |
| 13 |
* import_loader.js then calls two separate handlers: |
| 14 |
* |
| 15 |
* update_global_val → global colours + typography |
| 16 |
* wdkit_update_site_setting → container width, body background, `__globals__` |
| 17 |
* |
| 18 |
* Both are now reachable as services on Wdkit_Api_Call, so this class contains no copy of |
| 19 |
* their WordPress writes — it only decides *what* to send. |
| 20 |
* |
| 21 |
* ── Idempotency ──────────────────────────────────────────────────────────── |
| 22 |
* |
| 23 |
* The Elementor colour/typography write merges with `array_merge( $new, $existing )` over |
| 24 |
* numeric-keyed lists, which appends. Run the browser import twice and the kit accumulates |
| 25 |
* duplicate palette entries. That behaviour is preserved in the AJAX path, but this class |
| 26 |
* de-duplicates by `_id` against what the kit already holds before calling in, so a PHP run |
| 27 |
* is idempotent and never grows the palette on retry. |
| 28 |
* |
| 29 |
* Unrelated existing globals are preserved: entries the kit does not define are left alone, |
| 30 |
* because the underlying write merges rather than replaces. |
| 31 |
* |
| 32 |
* @package Wdesignkit |
| 33 |
* @since 2.6.5 |
| 34 |
*/ |
| 35 |
|
| 36 |
if ( ! defined( 'ABSPATH' ) ) { |
| 37 |
exit; |
| 38 |
} |
| 39 |
|
| 40 |
if ( ! class_exists( 'Wdkit_Import_Globals' ) ) { |
| 41 |
|
| 42 |
/** |
| 43 |
* Global style application. |
| 44 |
*/ |
| 45 |
class Wdkit_Import_Globals { |
| 46 |
|
| 47 |
/** |
| 48 |
* Collect global colours and typography from a kit's decoded templates. |
| 49 |
* |
| 50 |
* Port of get_global_data() in import_temp_main.js. De-duplicates on `_id` |
| 51 |
* (Elementor) / `id` (Gutenberg), keeping the first definition seen — the same rule |
| 52 |
* the browser applies via findIndex(). |
| 53 |
* |
| 54 |
* @param array[] $decoded_templates Decoded template payloads. |
| 55 |
* @param string $builder 'elementor'|'gutenberg'. |
| 56 |
* @return array{color:array,typo:array,font_family:array} |
| 57 |
*/ |
| 58 |
/** |
| 59 |
* The preset an import writes. Recognised when reading, so a later import never treats |
| 60 |
* an earlier one's output as the site's own palette. |
| 61 |
* |
| 62 |
* @var string |
| 63 |
*/ |
| 64 |
const PRESET_KEY = 'wdk_preset'; |
| 65 |
|
| 66 |
public static function collect( $decoded_templates, $builder = 'elementor' ) { |
| 67 |
$id_key = ( 'gutenberg' === $builder ) ? 'id' : '_id'; |
| 68 |
|
| 69 |
$colors = array(); |
| 70 |
$typo = array(); |
| 71 |
$fonts = array(); |
| 72 |
|
| 73 |
foreach ( (array) $decoded_templates as $decoded ) { |
| 74 |
if ( ! is_array( $decoded ) || empty( $decoded['global_data'] ) || ! is_array( $decoded['global_data'] ) ) { |
| 75 |
continue; |
| 76 |
} |
| 77 |
|
| 78 |
$global = $decoded['global_data']; |
| 79 |
|
| 80 |
foreach ( ( ! empty( $global['color'] ) && is_array( $global['color'] ) ? $global['color'] : array() ) as $entry ) { |
| 81 |
if ( ! is_array( $entry ) || ! self::has_id( $entry, $id_key ) ) { |
| 82 |
continue; |
| 83 |
} |
| 84 |
|
| 85 |
$key = (string) $entry[ $id_key ]; |
| 86 |
|
| 87 |
if ( ! isset( $colors[ $key ] ) ) { |
| 88 |
$colors[ $key ] = $entry; |
| 89 |
} |
| 90 |
} |
| 91 |
|
| 92 |
foreach ( ( ! empty( $global['typography'] ) && is_array( $global['typography'] ) ? $global['typography'] : array() ) as $entry ) { |
| 93 |
if ( ! is_array( $entry ) || ! self::has_id( $entry, $id_key ) ) { |
| 94 |
continue; |
| 95 |
} |
| 96 |
|
| 97 |
$key = (string) $entry[ $id_key ]; |
| 98 |
|
| 99 |
if ( ! isset( $typo[ $key ] ) ) { |
| 100 |
$typo[ $key ] = $entry; |
| 101 |
} |
| 102 |
|
| 103 |
$family = self::font_family_of( $entry, $builder ); |
| 104 |
|
| 105 |
if ( '' !== $family && ! isset( $fonts[ $family ] ) ) { |
| 106 |
$fonts[ $family ] = array( |
| 107 |
'original' => $family, |
| 108 |
'replace' => $family, |
| 109 |
); |
| 110 |
} |
| 111 |
} |
| 112 |
} |
| 113 |
|
| 114 |
/* Gutenberg ids are positions in the source preset, so ordering by id makes the |
| 115 |
* imported palette read in the kit's own order rather than in the order the walk |
| 116 |
* happened to meet each colour. Nothing depends on the old order: from_kit() and |
| 117 |
* rewrite_kit_references() both index this same array, so they stay in step. */ |
| 118 |
if ( 'id' === $id_key ) { |
| 119 |
ksort( $colors, SORT_NUMERIC ); |
| 120 |
ksort( $typo, SORT_NUMERIC ); |
| 121 |
} |
| 122 |
|
| 123 |
return array( |
| 124 |
'color' => array_values( $colors ), |
| 125 |
'typo' => array_values( $typo ), |
| 126 |
'font_family' => array_values( $fonts ), |
| 127 |
); |
| 128 |
} |
| 129 |
|
| 130 |
/** |
| 131 |
* Whether a global entry carries a usable id. |
| 132 |
* |
| 133 |
* `empty()` is wrong here and cost the kit its primary colour. A gutenberg kit's |
| 134 |
* `global_data` numbers entries by POSITION IN THE SOURCE PRESET, zero-based - id 0 is |
| 135 |
* C1, id 8 is C9 - so `empty( $entry['id'] )` threw away the first slot of every kit. |
| 136 |
* Downstream the effect was silent: rewrite_kit_references() built no map entry for |
| 137 |
* C1, so every `var(--tpgb-C1)` in the imported blocks was left pointing at the site's |
| 138 |
* own stock C1 and a kit whose brand colour was a dark green rendered in the plugin's |
| 139 |
* default purple. |
| 140 |
* |
| 141 |
* Elementor's `_id` is a hash string, where the only unusable value is the empty one. |
| 142 |
* |
| 143 |
* @since 2.6.5 |
| 144 |
* |
| 145 |
* @param array $entry Global entry. |
| 146 |
* @param string $id_key 'id' for gutenberg, '_id' for elementor. |
| 147 |
* @return bool |
| 148 |
*/ |
| 149 |
private static function has_id( $entry, $id_key ) { |
| 150 |
if ( ! isset( $entry[ $id_key ] ) || is_array( $entry[ $id_key ] ) ) { |
| 151 |
return false; |
| 152 |
} |
| 153 |
|
| 154 |
return '' !== (string) $entry[ $id_key ]; |
| 155 |
} |
| 156 |
|
| 157 |
/** |
| 158 |
* The font family named by one typography entry. |
| 159 |
* |
| 160 |
* Elementor stores it flat as `typography_font_family`; Gutenberg nests it under |
| 161 |
* `value.fontFamily.family`. Mirrors the two branches in get_global_data(). |
| 162 |
* |
| 163 |
* @param array $entry Typography entry. |
| 164 |
* @param string $builder Builder. |
| 165 |
* @return string |
| 166 |
*/ |
| 167 |
private static function font_family_of( $entry, $builder ) { |
| 168 |
if ( 'gutenberg' === $builder ) { |
| 169 |
return isset( $entry['value']['fontFamily']['family'] ) && is_string( $entry['value']['fontFamily']['family'] ) |
| 170 |
? $entry['value']['fontFamily']['family'] |
| 171 |
: ''; |
| 172 |
} |
| 173 |
|
| 174 |
return isset( $entry['typography_font_family'] ) && is_string( $entry['typography_font_family'] ) |
| 175 |
? $entry['typography_font_family'] |
| 176 |
: ''; |
| 177 |
} |
| 178 |
|
| 179 |
/** |
| 180 |
* Apply collected globals, skipping anything the kit already defines. |
| 181 |
* |
| 182 |
* @param array $globals Output of collect(). |
| 183 |
* @param string $builder Builder. |
| 184 |
* @param array $site_data Optional site-level globals (container width, body bg). |
| 185 |
* @return array{colors:int,typography:int,site:bool,skipped:bool} |
| 186 |
*/ |
| 187 |
/** |
| 188 |
* Normalise the kit's own global palette into the shape apply() consumes. |
| 189 |
* |
| 190 |
* The wizard carries it as `site_obj.kit_global` = { color: [...], typo: [...] }, and |
| 191 |
* each colour holds its intended value in `new_color` — collect() cannot read that, |
| 192 |
* because collect() reads `global_data` off decoded TEMPLATES, where the keys are |
| 193 |
* `color`/`typography` and the value is already in place. |
| 194 |
* |
| 195 |
* Missing this is why an imported site came out unstyled: the kit's pages reference |
| 196 |
* `--tpgb-C12`, `--tpgb-T15-*` and friends, but plus-global.css only ever contained the |
| 197 |
* plugin's own defaults (C1-C5, T1-T7), so every colour and font fell back. |
| 198 |
* |
| 199 |
* ── Why the ids are preserved verbatim ───────────────────────────── |
| 200 |
* |
| 201 |
* The block markup references globals BY ID (`--tpgb-C12`). The browser path reassigns |
| 202 |
* gutenberg ids sequentially against the existing site palette, which only works because |
| 203 |
* it also rewrites every reference as it goes. Nothing here rewrites references, so the |
| 204 |
* kit's ids are kept exactly as the kit shipped them — that is what makes `C12` resolve. |
| 205 |
* |
| 206 |
* @param mixed $kit_global site_obj.kit_global. |
| 207 |
* @param string $builder 'elementor'|'gutenberg'. |
| 208 |
* @param mixed $font_pairs site_obj.font_family — [{original, replace}]. |
| 209 |
* @return array{color:array,typo:array} |
| 210 |
*/ |
| 211 |
public static function from_kit( $kit_global, $builder = 'elementor', $font_pairs = array(), $site_global = array(), $session_id = '' ) { |
| 212 |
$out = array( |
| 213 |
'color' => array(), |
| 214 |
'typo' => array(), |
| 215 |
'preset' => array(), |
| 216 |
); |
| 217 |
|
| 218 |
if ( ! is_array( $kit_global ) ) { |
| 219 |
return $out; |
| 220 |
} |
| 221 |
|
| 222 |
$builder = ( 'gutenberg' === $builder ) ? 'gutenberg' : 'elementor'; |
| 223 |
|
| 224 |
/* Font swaps chosen on the fonts screen: [{original, replace}]. */ |
| 225 |
$pairs = array(); |
| 226 |
|
| 227 |
foreach ( (array) $font_pairs as $pair ) { |
| 228 |
if ( is_array( $pair ) && ! empty( $pair['original'] ) && ! empty( $pair['replace'] ) ) { |
| 229 |
$pairs[ (string) $pair['original'] ] = (string) $pair['replace']; |
| 230 |
} |
| 231 |
} |
| 232 |
|
| 233 |
$colors = ( ! empty( $kit_global['color'] ) && is_array( $kit_global['color'] ) ) ? $kit_global['color'] : array(); |
| 234 |
$typos = ( ! empty( $kit_global['typo'] ) && is_array( $kit_global['typo'] ) ) ? $kit_global['typo'] : array(); |
| 235 |
|
| 236 |
if ( 'gutenberg' === $builder ) { |
| 237 |
/* ── Faithful port of the gutenberg branch of import_globla_data() ── |
| 238 |
* |
| 239 |
* The palette is NOT stored as a flat list. tpgb keeps it inside |
| 240 |
* `tpgb_global_options.presets[]`, and the browser builds one preset — |
| 241 |
* key `wdk_preset` — whose `colors` and `typography` arrays are POSITIONAL: |
| 242 |
* the ids are stripped, so the first entry is C1, the twelfth is C12. That |
| 243 |
* positional list is what generates the `--tpgb-C*` / `--tpgb-T*` custom |
| 244 |
* properties in plus-global.css. |
| 245 |
* |
| 246 |
* Building no preset at all is why the imported site was unstyled: the colours |
| 247 |
* were accepted, `presets` kept only the plugin's stock typography, `active` |
| 248 |
* stayed empty, and every `var(--tpgb-C12)` in the page CSS resolved to nothing. |
| 249 |
*/ |
| 250 |
/* The site's existing palette stays, and the kit's entries are appended after it - |
| 251 |
* exactly what import_globla_data() does with site_g_color / site_g_typo. */ |
| 252 |
$baseline = self::baseline( $session_id ); |
| 253 |
|
| 254 |
$seq = count( $baseline['colors'] ); |
| 255 |
|
| 256 |
foreach ( $colors as $entry ) { |
| 257 |
if ( ! is_array( $entry ) ) { |
| 258 |
continue; |
| 259 |
} |
| 260 |
|
| 261 |
if ( isset( $entry['new_color'] ) && '' !== $entry['new_color'] ) { |
| 262 |
$entry['value'] = $entry['new_color']; |
| 263 |
} |
| 264 |
|
| 265 |
unset( $entry['new_color'] ); |
| 266 |
|
| 267 |
$entry['id'] = ++$seq; |
| 268 |
$out['color'][] = $entry; |
| 269 |
} |
| 270 |
|
| 271 |
$seq = count( $baseline['typography'] ); |
| 272 |
|
| 273 |
foreach ( $typos as $entry ) { |
| 274 |
if ( ! is_array( $entry ) ) { |
| 275 |
continue; |
| 276 |
} |
| 277 |
|
| 278 |
/* The family sits at value.fontFamily.family, and a swapped font also has |
| 279 |
* the "| Original" suffix trimmed off its label — both as the browser does. */ |
| 280 |
$family = $entry['value']['fontFamily']['family'] ?? ''; |
| 281 |
|
| 282 |
if ( '' !== $family && isset( $pairs[ $family ] ) ) { |
| 283 |
$entry['value']['fontFamily']['family'] = $pairs[ $family ]; |
| 284 |
|
| 285 |
if ( ! empty( $entry['label'] ) && is_string( $entry['label'] ) ) { |
| 286 |
$parts = explode( '|', $entry['label'] ); |
| 287 |
$entry['label'] = trim( $parts[0] ); |
| 288 |
} |
| 289 |
} |
| 290 |
|
| 291 |
$entry['id'] = ++$seq; |
| 292 |
$out['typo'][] = $entry; |
| 293 |
} |
| 294 |
|
| 295 |
/* Positional lists for the preset: same entries, ids removed. */ |
| 296 |
$preset_colors = $baseline['colors']; |
| 297 |
$preset_typo = $baseline['typography']; |
| 298 |
|
| 299 |
foreach ( $out['color'] as $entry ) { |
| 300 |
unset( $entry['id'] ); |
| 301 |
$preset_colors[] = $entry; |
| 302 |
} |
| 303 |
|
| 304 |
foreach ( $out['typo'] as $entry ) { |
| 305 |
unset( $entry['id'] ); |
| 306 |
$preset_typo[] = $entry; |
| 307 |
} |
| 308 |
|
| 309 |
/* Keep the rest of the active preset - gradients, spacing, box shadows, |
| 310 |
* globalContainer - rather than starting from an empty object, which is what |
| 311 |
* `Object.assign({}, site_obj?.site_global, {...})` preserves in the browser. */ |
| 312 |
$preset = ( ! empty( $site_global ) && is_array( $site_global ) ) |
| 313 |
? $site_global |
| 314 |
: ( isset( $baseline['preset'] ) && is_array( $baseline['preset'] ) ? $baseline['preset'] : array() ); |
| 315 |
|
| 316 |
unset( $preset['color'] ); |
| 317 |
|
| 318 |
$preset['colors'] = $preset_colors; |
| 319 |
$preset['typography'] = $preset_typo; |
| 320 |
$preset['key'] = self::PRESET_KEY; |
| 321 |
$preset['name'] = 'WDK Preset'; |
| 322 |
|
| 323 |
$out['preset'] = $preset; |
| 324 |
|
| 325 |
return $out; |
| 326 |
} |
| 327 |
|
| 328 |
/* Elementor: the value lives in `color`, and ids stay as the kit shipped them — |
| 329 |
* Elementor references globals by id, not by position — EXCEPT where the kit's id |
| 330 |
* already belongs to a global on this site. See elementor_id_map(). */ |
| 331 |
$id_map = self::elementor_id_map( $kit_global, $session_id ); |
| 332 |
|
| 333 |
foreach ( $colors as $entry ) { |
| 334 |
if ( ! is_array( $entry ) ) { |
| 335 |
continue; |
| 336 |
} |
| 337 |
|
| 338 |
if ( isset( $entry['new_color'] ) && '' !== $entry['new_color'] ) { |
| 339 |
$entry['color'] = $entry['new_color']; |
| 340 |
} |
| 341 |
|
| 342 |
unset( $entry['new_color'] ); |
| 343 |
|
| 344 |
if ( ! empty( $entry['_id'] ) && isset( $id_map['color'][ (string) $entry['_id'] ] ) ) { |
| 345 |
$entry['_id'] = $id_map['color'][ (string) $entry['_id'] ]; |
| 346 |
} |
| 347 |
|
| 348 |
$out['color'][] = $entry; |
| 349 |
} |
| 350 |
|
| 351 |
foreach ( $typos as $entry ) { |
| 352 |
if ( ! is_array( $entry ) ) { |
| 353 |
continue; |
| 354 |
} |
| 355 |
|
| 356 |
if ( ! empty( $entry['typography_font_family'] ) && isset( $pairs[ $entry['typography_font_family'] ] ) ) { |
| 357 |
$entry['typography_font_family'] = $pairs[ $entry['typography_font_family'] ]; |
| 358 |
} |
| 359 |
|
| 360 |
if ( ! empty( $entry['_id'] ) && isset( $id_map['typo'][ (string) $entry['_id'] ] ) ) { |
| 361 |
$entry['_id'] = $id_map['typo'][ (string) $entry['_id'] ]; |
| 362 |
} |
| 363 |
|
| 364 |
$out['typo'][] = $entry; |
| 365 |
} |
| 366 |
|
| 367 |
return $out; |
| 368 |
} |
| 369 |
|
| 370 |
/** |
| 371 |
* Replacement `_id`s for kit globals whose id is already taken on this site. |
| 372 |
* |
| 373 |
* ── The problem ──────────────────────────────────────────────────────────── |
| 374 |
* |
| 375 |
* Elementor resolves a global by id: page content carries |
| 376 |
* `__globals__: { text_color: "globals/colors?id=72e09b4" }`. A kit ships its own ids, |
| 377 |
* and on a fresh site they are free, so the references resolve to the kit's own palette |
| 378 |
* and nothing needs remapping — which is why this never bit on a first import. |
| 379 |
* |
| 380 |
* On a site that ALREADY has Elementor globals, an id can collide. `apply()` then hands |
| 381 |
* the colliding entry to without_known(), which drops it as "already present" — so the |
| 382 |
* kit's colour is never added, and every page that references that id renders in the |
| 383 |
* SITE's colour instead. The page looks imported and is quietly wrong. |
| 384 |
* |
| 385 |
* import_globla_data() in the browser handles this by minting a fresh `_id` for the |
| 386 |
* colliding entry and recording `{temp_id, site_id}`, which Extract_elementor_global() |
| 387 |
* then swaps through the page content. This is that behaviour, server-side. |
| 388 |
* |
| 389 |
* ── Why the id is derived, not random ────────────────────────────────────── |
| 390 |
* |
| 391 |
* keyUniqueID() is random, which is fine in a browser that computes the map once and |
| 392 |
* holds it in memory. The runner has neither luxury: the map is needed in |
| 393 |
* stage_content (to rewrite each page as it is written) and again in stage_setup (to |
| 394 |
* write the globals themselves), across separate requests and up to RUNNER_LANES |
| 395 |
* concurrent processes. A random id would differ between them and the references would |
| 396 |
* point at nothing. |
| 397 |
* |
| 398 |
* Hashing the session id with the old id instead makes the map a pure function of |
| 399 |
* inputs every lane already has: same answer everywhere, no persistence, no locking, and |
| 400 |
* stable across a resume. Shape matches keyUniqueID() — six lowercase base-36 characters |
| 401 |
* plus the two-digit year. |
| 402 |
* |
| 403 |
* @since 2.7.2 |
| 404 |
* |
| 405 |
* @param array $kit_global The kit's palette, `{color:[], typo:[]}`. |
| 406 |
* @param string $session_id Session id, the per-run salt. |
| 407 |
* @return array{color:array<string,string>,typo:array<string,string>} old id => new id. |
| 408 |
*/ |
| 409 |
public static function elementor_id_map( $kit_global, $session_id = '' ) { |
| 410 |
$map = array( |
| 411 |
'color' => array(), |
| 412 |
'typo' => array(), |
| 413 |
); |
| 414 |
|
| 415 |
if ( ! is_array( $kit_global ) ) { |
| 416 |
return $map; |
| 417 |
} |
| 418 |
|
| 419 |
/* Memoized per request: the page importer asks once per template, and the answer |
| 420 |
* cannot change within a request — the site's globals are not written until |
| 421 |
* stage_setup, which is a later one. Keyed so a second kit in the same process (the |
| 422 |
* remote job executor runs several) cannot read the first one's map. */ |
| 423 |
static $memo = array(); |
| 424 |
|
| 425 |
$memo_key = md5( $session_id . '|' . (string) wp_json_encode( $kit_global ) ); |
| 426 |
|
| 427 |
if ( isset( $memo[ $memo_key ] ) ) { |
| 428 |
return $memo[ $memo_key ]; |
| 429 |
} |
| 430 |
|
| 431 |
$existing = self::existing_elementor_globals(); |
| 432 |
|
| 433 |
/* `taken` is keyed by id and holds the entry, not just `true`, because whether an id |
| 434 |
* is a COLLISION depends on what is sitting on it. See is_same_global(). */ |
| 435 |
$taken = array(); |
| 436 |
|
| 437 |
foreach ( array( 'custom_colors', 'custom_typography' ) as $group ) { |
| 438 |
foreach ( (array) $existing[ $group ] as $entry ) { |
| 439 |
if ( is_array( $entry ) && ! empty( $entry['_id'] ) ) { |
| 440 |
$taken[ (string) $entry['_id'] ] = $entry; |
| 441 |
} |
| 442 |
} |
| 443 |
} |
| 444 |
|
| 445 |
if ( empty( $taken ) ) { |
| 446 |
$memo[ $memo_key ] = $map; |
| 447 |
|
| 448 |
return $map; |
| 449 |
} |
| 450 |
|
| 451 |
$groups = array( |
| 452 |
'color' => ( ! empty( $kit_global['color'] ) && is_array( $kit_global['color'] ) ) ? $kit_global['color'] : array(), |
| 453 |
'typo' => ( ! empty( $kit_global['typo'] ) && is_array( $kit_global['typo'] ) ) ? $kit_global['typo'] : array(), |
| 454 |
); |
| 455 |
|
| 456 |
$year = gmdate( 'y' ); |
| 457 |
|
| 458 |
$candidate = function ( $key, $old, $attempt ) use ( $session_id, $year ) { |
| 459 |
return substr( md5( $session_id . '|' . $key . '|' . $old . '|' . $attempt ), 0, 6 ) . $year; |
| 460 |
}; |
| 461 |
|
| 462 |
/* Every id this map could ever mint, before deciding anything. |
| 463 |
* |
| 464 |
* The re-hash loop below has to skip an id that is already taken — but by stage_setup |
| 465 |
* this run has WRITTEN its own replacements into the site kit, so on a retry that |
| 466 |
* re-imports a page afterwards they come back as "taken" and the loop would move on |
| 467 |
* to attempt 1, producing a different id than the pages already reference. Knowing our |
| 468 |
* own outputs up front lets the loop treat them as reserved-by-us rather than |
| 469 |
* occupied, which is what makes this map a pure function of (session, kit) and |
| 470 |
* identical no matter when it is asked. */ |
| 471 |
$ours = array(); |
| 472 |
|
| 473 |
foreach ( $groups as $key => $entries ) { |
| 474 |
foreach ( $entries as $entry ) { |
| 475 |
if ( is_array( $entry ) && ! empty( $entry['_id'] ) ) { |
| 476 |
for ( $attempt = 0; $attempt < 8; $attempt++ ) { |
| 477 |
$ours[ $candidate( $key, (string) $entry['_id'], $attempt ) ] = true; |
| 478 |
} |
| 479 |
} |
| 480 |
} |
| 481 |
} |
| 482 |
|
| 483 |
foreach ( $groups as $key => $entries ) { |
| 484 |
foreach ( $entries as $entry ) { |
| 485 |
if ( ! is_array( $entry ) || empty( $entry['_id'] ) ) { |
| 486 |
continue; |
| 487 |
} |
| 488 |
|
| 489 |
$old = (string) $entry['_id']; |
| 490 |
|
| 491 |
if ( ! isset( $taken[ $old ] ) || isset( $map[ $key ][ $old ] ) ) { |
| 492 |
continue; |
| 493 |
} |
| 494 |
|
| 495 |
/* The id is taken by THIS SAME global — almost always because an earlier |
| 496 |
* import of this kit put it there. Not a collision: remapping it would mint a |
| 497 |
* second copy of a global the site already has, `without_known()` would no |
| 498 |
* longer recognise it as known, and every re-import would grow the palette by |
| 499 |
* the size of the kit (observed: 15 colours -> 29 on a second run). Leave the |
| 500 |
* id alone and let without_known() do its job. */ |
| 501 |
if ( self::is_same_global( $entry, $taken[ $old ], $key ) ) { |
| 502 |
continue; |
| 503 |
} |
| 504 |
|
| 505 |
/* Re-hash on the (vanishingly unlikely) chance the derived id belongs to a |
| 506 |
* global that was on this site BEFORE the import. Bounded so a pathological |
| 507 |
* site cannot spin here. */ |
| 508 |
$new = ''; |
| 509 |
|
| 510 |
for ( $attempt = 0; $attempt < 8; $attempt++ ) { |
| 511 |
$try = $candidate( $key, $old, $attempt ); |
| 512 |
|
| 513 |
if ( ! isset( $taken[ $try ] ) || isset( $ours[ $try ] ) ) { |
| 514 |
$new = $try; |
| 515 |
break; |
| 516 |
} |
| 517 |
} |
| 518 |
|
| 519 |
if ( '' === $new ) { |
| 520 |
continue; |
| 521 |
} |
| 522 |
|
| 523 |
$map[ $key ][ $old ] = $new; |
| 524 |
} |
| 525 |
} |
| 526 |
|
| 527 |
$memo[ $memo_key ] = $map; |
| 528 |
|
| 529 |
return $map; |
| 530 |
} |
| 531 |
|
| 532 |
/** |
| 533 |
* Are these two global entries the same global, rather than two different ones that happen |
| 534 |
* to share an `_id`? |
| 535 |
* |
| 536 |
* Only the fields that decide what the global RENDERS AS are compared — a colour's value, a |
| 537 |
* typography entry's family — plus the title, because two entries with the same value and |
| 538 |
* different names are still meaningfully different to whoever named them. The kit entry is |
| 539 |
* read through both spellings: `new_color` is where the preview screen puts a recoloured |
| 540 |
* swatch, `color` is where it ends up after from_kit(). |
| 541 |
* |
| 542 |
* Used only to tell a re-import (same kit, globals already applied) apart from a genuine id |
| 543 |
* clash with an unrelated global. Erring toward "these are different" costs one duplicated |
| 544 |
* palette entry; erring the other way costs a page rendering in the wrong colour — so when |
| 545 |
* this cannot tell, it says different. |
| 546 |
* |
| 547 |
* @since 2.7.2 |
| 548 |
* |
| 549 |
* @param array $kit_entry Entry from the kit's palette. |
| 550 |
* @param array $existing_entry Entry already on the site under the same `_id`. |
| 551 |
* @param string $group 'color' or 'typo'. |
| 552 |
* @return bool |
| 553 |
*/ |
| 554 |
private static function is_same_global( $kit_entry, $existing_entry, $group ) { |
| 555 |
if ( ! is_array( $kit_entry ) || ! is_array( $existing_entry ) ) { |
| 556 |
return false; |
| 557 |
} |
| 558 |
|
| 559 |
$title_kit = isset( $kit_entry['title'] ) ? (string) $kit_entry['title'] : ''; |
| 560 |
$title_existing = isset( $existing_entry['title'] ) ? (string) $existing_entry['title'] : ''; |
| 561 |
|
| 562 |
if ( $title_kit !== $title_existing ) { |
| 563 |
return false; |
| 564 |
} |
| 565 |
|
| 566 |
if ( 'color' === $group ) { |
| 567 |
$kit_color = ''; |
| 568 |
|
| 569 |
foreach ( array( 'new_color', 'color' ) as $field ) { |
| 570 |
if ( ! empty( $kit_entry[ $field ] ) ) { |
| 571 |
$kit_color = (string) $kit_entry[ $field ]; |
| 572 |
break; |
| 573 |
} |
| 574 |
} |
| 575 |
|
| 576 |
$existing_color = isset( $existing_entry['color'] ) ? (string) $existing_entry['color'] : ''; |
| 577 |
|
| 578 |
return '' !== $kit_color && 0 === strcasecmp( $kit_color, $existing_color ); |
| 579 |
} |
| 580 |
|
| 581 |
$kit_family = isset( $kit_entry['typography_font_family'] ) ? (string) $kit_entry['typography_font_family'] : ''; |
| 582 |
$existing_family = isset( $existing_entry['typography_font_family'] ) ? (string) $existing_entry['typography_font_family'] : ''; |
| 583 |
|
| 584 |
/* A typography entry with no family on either side is still the same entry when the |
| 585 |
* titles match — the kit ships plenty that only set size and weight. */ |
| 586 |
return $kit_family === $existing_family; |
| 587 |
} |
| 588 |
|
| 589 |
/** |
| 590 |
* Swap remapped global ids through one page's Elementor content. |
| 591 |
* |
| 592 |
* The PHP counterpart of Extract_elementor_global()'s `status == 'update'` branch, and |
| 593 |
* deliberately only that branch: elementor_id_map() produces nothing else. The browser's |
| 594 |
* other branch (`remove`, which inlines a global's literal values and deletes the |
| 595 |
* reference) belongs to a case import_globla_data() reaches and the runner does not. |
| 596 |
* |
| 597 |
* Operates on the raw JSON string rather than the decoded tree on purpose. The reference |
| 598 |
* is a URL inside a `__globals__` value — `globals/colors?id=<id>` — so an anchored |
| 599 |
* string swap touches exactly the references and nothing that merely happens to contain |
| 600 |
* the same seven characters. Decoding, walking and re-encoding a 200KB tree to change a |
| 601 |
* handful of ids would cost far more and risk re-encoding differences. |
| 602 |
* |
| 603 |
* @since 2.7.2 |
| 604 |
* |
| 605 |
* @param string $content Elementor content, JSON-encoded. |
| 606 |
* @param array $map As elementor_id_map() returns it. |
| 607 |
* @return string Content with the references remapped; unchanged when there is nothing to do. |
| 608 |
*/ |
| 609 |
public static function rewrite_elementor_global_ids( $content, $map ) { |
| 610 |
if ( ! is_string( $content ) || '' === $content || ! is_array( $map ) ) { |
| 611 |
return $content; |
| 612 |
} |
| 613 |
|
| 614 |
$prefixes = array( |
| 615 |
'color' => 'colors?id=', |
| 616 |
'typo' => 'typography?id=', |
| 617 |
); |
| 618 |
|
| 619 |
$search = array(); |
| 620 |
$replace = array(); |
| 621 |
|
| 622 |
foreach ( $prefixes as $key => $prefix ) { |
| 623 |
if ( empty( $map[ $key ] ) || ! is_array( $map[ $key ] ) ) { |
| 624 |
continue; |
| 625 |
} |
| 626 |
|
| 627 |
foreach ( $map[ $key ] as $old => $new ) { |
| 628 |
/* Anchored on `colors?id=` / `typography?id=` rather than on the full |
| 629 |
* `globals/colors?id=`, because the slash in front of it is not literal in |
| 630 |
* this string. `_elementor_data` is JSON, and wp_json_encode() escapes |
| 631 |
* forward slashes — the reference is stored as `globals\/colors?id=<id>`, so |
| 632 |
* a needle containing a bare `/` matches nothing at all. Rather than carry |
| 633 |
* both spellings and hope no third one exists, the needle starts after the |
| 634 |
* slash. `colors?id=` plus a specific 7-8 character id is still far too |
| 635 |
* distinctive to collide with anything else in a page. */ |
| 636 |
$search[] = $prefix . $old; |
| 637 |
$replace[] = $prefix . $new; |
| 638 |
} |
| 639 |
} |
| 640 |
|
| 641 |
if ( empty( $search ) ) { |
| 642 |
return $content; |
| 643 |
} |
| 644 |
|
| 645 |
return str_replace( $search, $replace, $content ); |
| 646 |
} |
| 647 |
|
| 648 |
/** |
| 649 |
* Merge two {color, typo} sets, first occurrence of an id winning. |
| 650 |
* |
| 651 |
* @param array $primary Takes precedence. |
| 652 |
* @param array $fallback Fills gaps. |
| 653 |
* @param string $builder Builder, which decides the id key. |
| 654 |
* @return array |
| 655 |
*/ |
| 656 |
public static function merge_globals( $primary, $fallback, $builder = 'elementor' ) { |
| 657 |
$id_key = ( 'gutenberg' === $builder ) ? 'id' : '_id'; |
| 658 |
$out = array( |
| 659 |
'color' => array(), |
| 660 |
'typo' => array(), |
| 661 |
); |
| 662 |
|
| 663 |
/* The preset is positional and built as a whole; it cannot be merged entry by |
| 664 |
* entry, so the primary's preset wins outright. */ |
| 665 |
if ( ! empty( $primary['preset'] ) ) { |
| 666 |
$out['preset'] = $primary['preset']; |
| 667 |
} elseif ( ! empty( $fallback['preset'] ) ) { |
| 668 |
$out['preset'] = $fallback['preset']; |
| 669 |
} |
| 670 |
|
| 671 |
foreach ( array( 'color', 'typo' ) as $bucket ) { |
| 672 |
$seen = array(); |
| 673 |
|
| 674 |
foreach ( array( $primary, $fallback ) as $set ) { |
| 675 |
foreach ( ( ! empty( $set[ $bucket ] ) && is_array( $set[ $bucket ] ) ? $set[ $bucket ] : array() ) as $entry ) { |
| 676 |
if ( ! is_array( $entry ) ) { |
| 677 |
continue; |
| 678 |
} |
| 679 |
|
| 680 |
$key = isset( $entry[ $id_key ] ) ? (string) $entry[ $id_key ] : wp_json_encode( $entry ); |
| 681 |
|
| 682 |
if ( isset( $seen[ $key ] ) ) { |
| 683 |
continue; |
| 684 |
} |
| 685 |
|
| 686 |
$seen[ $key ] = true; |
| 687 |
$out[ $bucket ][] = $entry; |
| 688 |
} |
| 689 |
} |
| 690 |
} |
| 691 |
|
| 692 |
return $out; |
| 693 |
} |
| 694 |
|
| 695 |
public static function apply( $globals, $builder = 'elementor', $site_data = array() ) { |
| 696 |
$result = array( |
| 697 |
'colors' => 0, |
| 698 |
'typography' => 0, |
| 699 |
'site' => false, |
| 700 |
'skipped' => false, |
| 701 |
); |
| 702 |
|
| 703 |
if ( ! class_exists( 'Wdkit_Api_Call' ) ) { |
| 704 |
$result['skipped'] = true; |
| 705 |
|
| 706 |
return $result; |
| 707 |
} |
| 708 |
|
| 709 |
$api = Wdkit_Api_Call::get_instance(); |
| 710 |
$builder = ( 'gutenberg' === $builder ) ? 'gutenberg' : 'elementor'; |
| 711 |
|
| 712 |
$colors = ! empty( $globals['color'] ) && is_array( $globals['color'] ) ? $globals['color'] : array(); |
| 713 |
$typo = ! empty( $globals['typo'] ) && is_array( $globals['typo'] ) ? $globals['typo'] : array(); |
| 714 |
|
| 715 |
if ( 'elementor' === $builder ) { |
| 716 |
/* Drop anything already in the kit so a repeat run does not append duplicates |
| 717 |
* through the underlying array_merge. */ |
| 718 |
$existing = self::existing_elementor_globals(); |
| 719 |
|
| 720 |
$colors = self::without_known( $colors, $existing['custom_colors'], '_id' ); |
| 721 |
$typo = self::without_known( $typo, $existing['custom_typography'], '_id' ); |
| 722 |
} |
| 723 |
|
| 724 |
if ( ! empty( $colors ) || ! empty( $typo ) ) { |
| 725 |
$response = $api->wdkit_apply_global_values_data( $builder, $colors, $typo, self::gutenberg_preset( $globals, $builder ) ); |
| 726 |
|
| 727 |
if ( ! empty( $response['success'] ) ) { |
| 728 |
$result['colors'] = count( $colors ); |
| 729 |
$result['typography'] = count( $typo ); |
| 730 |
} |
| 731 |
} |
| 732 |
|
| 733 |
if ( ! empty( $site_data ) && is_array( $site_data ) ) { |
| 734 |
$clean = self::sanitize_site_data( $site_data, $builder ); |
| 735 |
|
| 736 |
/* Nothing recognised survived the whitelist. Writing it anyway would replace a |
| 737 |
* real container width with an empty value, which is exactly the regression this |
| 738 |
* guard exists to stop — leaving the site's current setting alone is the safe |
| 739 |
* outcome, and it is logged so a kit shipping an unexpected shape is visible. */ |
| 740 |
if ( empty( $clean ) ) { |
| 741 |
if ( class_exists( 'Wdkit_Import_Log' ) ) { |
| 742 |
Wdkit_Import_Log::add( |
| 743 |
'globals', |
| 744 |
array( |
| 745 |
'what' => 'site_data', |
| 746 |
'skipped' => 1, |
| 747 |
'builder' => $builder, |
| 748 |
'keys' => implode( ',', array_slice( array_keys( $site_data ), 0, 8 ) ), |
| 749 |
) |
| 750 |
); |
| 751 |
} |
| 752 |
} else { |
| 753 |
$site_response = $api->wdkit_apply_site_globals_data( $builder, $clean ); |
| 754 |
$result['site'] = ! empty( $site_response['success'] ); |
| 755 |
} |
| 756 |
} |
| 757 |
|
| 758 |
return $result; |
| 759 |
} |
| 760 |
|
| 761 |
/** |
| 762 |
* What the active Elementor kit already holds. |
| 763 |
* |
| 764 |
* @return array{custom_colors:array,custom_typography:array} |
| 765 |
*/ |
| 766 |
private static function existing_elementor_globals() { |
| 767 |
$empty = array( |
| 768 |
'custom_colors' => array(), |
| 769 |
'custom_typography' => array(), |
| 770 |
); |
| 771 |
|
| 772 |
$kit_id = get_option( 'elementor_active_kit' ); |
| 773 |
|
| 774 |
if ( ! $kit_id ) { |
| 775 |
return $empty; |
| 776 |
} |
| 777 |
|
| 778 |
$meta = get_post_meta( $kit_id, '_elementor_page_settings', true ); |
| 779 |
|
| 780 |
if ( ! is_array( $meta ) ) { |
| 781 |
return $empty; |
| 782 |
} |
| 783 |
|
| 784 |
return array( |
| 785 |
'custom_colors' => ! empty( $meta['custom_colors'] ) && is_array( $meta['custom_colors'] ) ? $meta['custom_colors'] : array(), |
| 786 |
'custom_typography' => ! empty( $meta['custom_typography'] ) && is_array( $meta['custom_typography'] ) ? $meta['custom_typography'] : array(), |
| 787 |
); |
| 788 |
} |
| 789 |
|
| 790 |
/** |
| 791 |
* Remove entries whose id already exists in the target. |
| 792 |
* |
| 793 |
* @param array $entries Candidate entries. |
| 794 |
* @param array $existing Entries already stored. |
| 795 |
* @param string $id_key Identity key. |
| 796 |
* @return array |
| 797 |
*/ |
| 798 |
private static function without_known( $entries, $existing, $id_key ) { |
| 799 |
if ( empty( $existing ) ) { |
| 800 |
return $entries; |
| 801 |
} |
| 802 |
|
| 803 |
$known = array(); |
| 804 |
|
| 805 |
foreach ( $existing as $entry ) { |
| 806 |
if ( is_array( $entry ) && ! empty( $entry[ $id_key ] ) ) { |
| 807 |
$known[ (string) $entry[ $id_key ] ] = true; |
| 808 |
} |
| 809 |
} |
| 810 |
|
| 811 |
$out = array(); |
| 812 |
|
| 813 |
foreach ( $entries as $entry ) { |
| 814 |
if ( ! is_array( $entry ) || empty( $entry[ $id_key ] ) ) { |
| 815 |
continue; |
| 816 |
} |
| 817 |
|
| 818 |
if ( isset( $known[ (string) $entry[ $id_key ] ] ) ) { |
| 819 |
continue; |
| 820 |
} |
| 821 |
|
| 822 |
$out[] = $entry; |
| 823 |
} |
| 824 |
|
| 825 |
return $out; |
| 826 |
} |
| 827 |
|
| 828 |
/** |
| 829 |
* The Gutenberg preset envelope, which is keyed and therefore already idempotent. |
| 830 |
* |
| 831 |
* @param array $globals Collected globals. |
| 832 |
* @param string $builder Builder. |
| 833 |
* @return array |
| 834 |
*/ |
| 835 |
private static function gutenberg_preset( $globals, $builder ) { |
| 836 |
if ( 'gutenberg' !== $builder ) { |
| 837 |
return array(); |
| 838 |
} |
| 839 |
|
| 840 |
if ( ! empty( $globals['preset'] ) && is_array( $globals['preset'] ) && ! empty( $globals['preset']['key'] ) ) { |
| 841 |
return $globals['preset']; |
| 842 |
} |
| 843 |
|
| 844 |
return array(); |
| 845 |
} |
| 846 |
|
| 847 |
/** |
| 848 |
* Keep only the site-level keys the underlying write recognises, per builder. |
| 849 |
* |
| 850 |
* Anything else is dropped rather than forwarded, so a future payload cannot smuggle |
| 851 |
* extra keys into `_elementor_page_settings` or `tpgb_global_options`. |
| 852 |
* |
| 853 |
* The two builders ship DIFFERENT shapes in the same `site_settings` block, and the |
| 854 |
* whitelist used to describe only Elementor's: |
| 855 |
* |
| 856 |
* elementor {container_width:{unit,size,sizes}, globals:{...}, body_background_color:[]} |
| 857 |
* gutenberg {md:"1240", xs:"1240", unit:"px"} |
| 858 |
* |
| 859 |
* So for a Gutenberg kit every key was dropped and the caller wrote an EMPTY array over |
| 860 |
* `tpgb_global_options.globalContainer` — the site then rendered at Nexter's default |
| 861 |
* 1140px instead of the kit's 1240px. Gutenberg's values are scalars, not arrays, which |
| 862 |
* is why the `is_array()` test alone could never have passed them through. |
| 863 |
* |
| 864 |
* @param array $site_data Raw site data. |
| 865 |
* @param string $builder 'elementor'|'gutenberg'. |
| 866 |
* @return array |
| 867 |
*/ |
| 868 |
private static function sanitize_site_data( $site_data, $builder = 'elementor' ) { |
| 869 |
$clean = array(); |
| 870 |
|
| 871 |
if ( ! is_array( $site_data ) ) { |
| 872 |
return $clean; |
| 873 |
} |
| 874 |
|
| 875 |
if ( 'gutenberg' === $builder ) { |
| 876 |
/* Breakpoint widths plus the unit they are expressed in. Scalars only — a |
| 877 |
* nested structure here is not something Nexter reads, so it is dropped. */ |
| 878 |
foreach ( array( 'xs', 'sm', 'md', 'lg', 'unit' ) as $key ) { |
| 879 |
if ( isset( $site_data[ $key ] ) && is_scalar( $site_data[ $key ] ) ) { |
| 880 |
$clean[ $key ] = sanitize_text_field( (string) $site_data[ $key ] ); |
| 881 |
} |
| 882 |
} |
| 883 |
|
| 884 |
return $clean; |
| 885 |
} |
| 886 |
|
| 887 |
foreach ( array( 'container_width', 'globals', 'body_background_color' ) as $key ) { |
| 888 |
if ( isset( $site_data[ $key ] ) && is_array( $site_data[ $key ] ) ) { |
| 889 |
$clean[ $key ] = $site_data[ $key ]; |
| 890 |
} |
| 891 |
} |
| 892 |
|
| 893 |
return $clean; |
| 894 |
} |
| 895 |
|
| 896 |
/** |
| 897 |
* Point a kit's global references at the ids this site assigned. |
| 898 |
* |
| 899 |
* ── Why this is here and not in the browser ──────────────────────────────── |
| 900 |
* |
| 901 |
* The kit's blocks reference globals by the KIT's own ids, which are sparse |
| 902 |
* (`typo: [14,7,5,17,16,13,12,9,8,11,6,15]`). from_kit() writes the palette |
| 903 |
* positionally, so the same entries land at 1..n. Every reference in the content has to |
| 904 |
* move with them or it points at a palette slot that holds something else - or, for an id |
| 905 |
* past the end, at nothing at all, which is what left T13..T18 undefined and the headings |
| 906 |
* unstyled. |
| 907 |
* |
| 908 |
* The browser does this with Extract_gutenberg_global(). Handing the page back for it to |
| 909 |
* transform and re-save cost one extra round trip per page and, more to the point, could |
| 910 |
* not be tested without running a full import: it regex-matches block comments and |
| 911 |
* JSON.parses the capture, so its behaviour depends on the exact serialisation it is |
| 912 |
* handed. Doing it here works on the parsed block array the runner already holds, before |
| 913 |
* it is ever serialised, and is checkable from the CLI. |
| 914 |
* |
| 915 |
* The RULES are the browser's, kept deliberately narrow: |
| 916 |
* |
| 917 |
* colour a string `var(--tpgb-C{n})` becomes `var(--tpgb-C{site})`. |
| 918 |
* typography a `globalTypo: n` object is REPLACED by the kit global's own typography |
| 919 |
* value with `globalTypo` set to the site id - the browser assigns |
| 920 |
* `new_data = typo_data`, so the block ends up carrying the resolved |
| 921 |
* typography, not just a renumbered pointer. The matching |
| 922 |
* `var(--tpgb-{n}-font-size)` style references are renumbered too. |
| 923 |
* GC/S/BS left exactly as they are. The shipped call sites pass neither gradient, |
| 924 |
* spacing nor boxshadow, so the browser leaves these alone as well; inventing |
| 925 |
* a mapping here would be new behaviour, not a port. |
| 926 |
* |
| 927 |
* A reference the kit's own global data does not describe is left untouched rather than |
| 928 |
* guessed at - same as the browser, where the lookup simply misses. |
| 929 |
* |
| 930 |
* @since 2.6.5 |
| 931 |
* |
| 932 |
* @param array $blocks Parsed blocks, by reference. |
| 933 |
* @param mixed $kit_global site_obj.kit_global - {color, typo}. |
| 934 |
* @return array oldId => newId for the typography ids that moved, for the string pass. |
| 935 |
*/ |
| 936 |
/** |
| 937 |
* The site's own global palette, as it stood before this import touched anything. |
| 938 |
* |
| 939 |
* The browser reads this with the `get_global_val` action at the top of |
| 940 |
* import_globla_data() and then APPENDS the kit's entries to it: |
| 941 |
* |
| 942 |
* let new_color = Object.assign({}, color, { id: (site_g_color.length + 1) }) |
| 943 |
* site_g_color.push(new_color) |
| 944 |
* |
| 945 |
* So a site whose active preset already holds five colours gets the kit's first colour |
| 946 |
* at C6, and the finished preset is [site's five] + [kit's thirteen]. Numbering the kit |
| 947 |
* from 1 instead - which is what this class did - silently replaces the site's own |
| 948 |
* palette, so any content already on the site that referenced C1..C5 changes colour. |
| 949 |
* |
| 950 |
* Captured ONCE per session and kept there. Pages are imported before the globals step |
| 951 |
* runs, and after it runs the active preset is the new one, so re-reading the option |
| 952 |
* later would give a different, larger baseline and the second half of a resumed import |
| 953 |
* would be numbered against it. |
| 954 |
* |
| 955 |
* @since 2.6.5 |
| 956 |
* |
| 957 |
* @param string $session_id Session id. Empty reads live without caching. |
| 958 |
* @return array{colors:array,typography:array} |
| 959 |
*/ |
| 960 |
public static function baseline( $session_id = '' ) { |
| 961 |
if ( '' !== $session_id ) { |
| 962 |
$session = Wdkit_Import_Session::get( $session_id ); |
| 963 |
|
| 964 |
if ( is_array( $session ) && isset( $session['global_baseline'] ) && is_array( $session['global_baseline'] ) ) { |
| 965 |
return $session['global_baseline']; |
| 966 |
} |
| 967 |
} |
| 968 |
|
| 969 |
$baseline = self::read_active_preset(); |
| 970 |
|
| 971 |
if ( '' !== $session_id ) { |
| 972 |
Wdkit_Import_Session::update( $session_id, array( 'global_baseline' => $baseline ) ); |
| 973 |
} |
| 974 |
|
| 975 |
return $baseline; |
| 976 |
} |
| 977 |
|
| 978 |
/** |
| 979 |
* Read the active tpgb preset's colour and typography lists. |
| 980 |
* |
| 981 |
* Same option and same selection rule `get_global_val` uses - `presets[ active ]` - but |
| 982 |
* without the emit-and-die, so the runner can call it. |
| 983 |
* |
| 984 |
* @return array{colors:array,typography:array} |
| 985 |
*/ |
| 986 |
private static function read_active_preset() { |
| 987 |
$empty = array( |
| 988 |
'colors' => array(), |
| 989 |
'typography' => array(), |
| 990 |
'preset' => array(), |
| 991 |
); |
| 992 |
|
| 993 |
$raw = get_option( 'tpgb_global_options' ); |
| 994 |
|
| 995 |
/* Nothing has written the option yet, which on a fresh site is the normal state. |
| 996 |
* |
| 997 |
* Returning empty here is not harmless: the kit's palette is APPENDED to the baseline, |
| 998 |
* so with no baseline the site ends up with only the kit's own entries — a headless |
| 999 |
* import produced 13 colours instead of 18, 12 type entries instead of 19, and no |
| 1000 |
* gradients, spacing or box shadows at all, while the same kit through the wizard came |
| 1001 |
* out complete. The wizard differs only by accident: its globals screen calls |
| 1002 |
* wdkit_get_global_val() first, and that seeds the option as a side effect. |
| 1003 |
* |
| 1004 |
* Seed it here with the same record, so both paths build on the same baseline. */ |
| 1005 |
if ( empty( $raw ) && class_exists( 'Wdkit_Api_Call' ) && method_exists( 'Wdkit_Api_Call', 'wdkit_default_gutenberg_globals' ) ) { |
| 1006 |
$defaults = Wdkit_Api_Call::wdkit_default_gutenberg_globals(); |
| 1007 |
|
| 1008 |
if ( ! empty( $defaults ) ) { |
| 1009 |
update_option( 'tpgb_global_options', wp_json_encode( $defaults ) ); |
| 1010 |
|
| 1011 |
$raw = get_option( 'tpgb_global_options' ); |
| 1012 |
} |
| 1013 |
} |
| 1014 |
|
| 1015 |
if ( empty( $raw ) ) { |
| 1016 |
return $empty; |
| 1017 |
} |
| 1018 |
|
| 1019 |
$settings = is_string( $raw ) ? json_decode( $raw, true ) : $raw; |
| 1020 |
|
| 1021 |
if ( ! is_array( $settings ) || empty( $settings['presets'] ) || ! is_array( $settings['presets'] ) ) { |
| 1022 |
return $empty; |
| 1023 |
} |
| 1024 |
|
| 1025 |
$active = isset( $settings['active'] ) ? (string) $settings['active'] : ''; |
| 1026 |
|
| 1027 |
/* Never build on top of a previous import's own preset. |
| 1028 |
* |
| 1029 |
* The kit's palette is appended after whatever the site already had, and the block |
| 1030 |
* CSS refers to colours BY POSITION — C1, C2, C12. Taking `wdk_preset` as the |
| 1031 |
* baseline meant every import stacked on the last one: 5 stock colours, then 12, |
| 1032 |
* then 24, then 37, with the kit's real palette pushed further out each time while |
| 1033 |
* its content still asked for the low positions. The result on a site imported |
| 1034 |
* three times was dark text on a dark background, because C1-C5 were no longer the |
| 1035 |
* kit's colours at all. |
| 1036 |
* |
| 1037 |
* So a previous import's preset is skipped in favour of a stock one, which makes |
| 1038 |
* the palette the same size and the same shape however many times a site is |
| 1039 |
* imported to. */ |
| 1040 |
if ( self::PRESET_KEY === $active ) { |
| 1041 |
foreach ( array( 'preset1', 'preset2' ) as $stock ) { |
| 1042 |
if ( isset( $settings['presets'][ $stock ] ) && is_array( $settings['presets'][ $stock ] ) ) { |
| 1043 |
$active = $stock; |
| 1044 |
|
| 1045 |
break; |
| 1046 |
} |
| 1047 |
} |
| 1048 |
|
| 1049 |
/* Nothing stock left to fall back on — start clean rather than compounding. */ |
| 1050 |
if ( self::PRESET_KEY === $active ) { |
| 1051 |
return $empty; |
| 1052 |
} |
| 1053 |
} |
| 1054 |
|
| 1055 |
if ( '' === $active || ! isset( $settings['presets'][ $active ] ) || ! is_array( $settings['presets'][ $active ] ) ) { |
| 1056 |
return $empty; |
| 1057 |
} |
| 1058 |
|
| 1059 |
$preset = $settings['presets'][ $active ]; |
| 1060 |
|
| 1061 |
return array( |
| 1062 |
'colors' => ( ! empty( $preset['colors'] ) && is_array( $preset['colors'] ) ) ? array_values( $preset['colors'] ) : array(), |
| 1063 |
'typography' => ( ! empty( $preset['typography'] ) && is_array( $preset['typography'] ) ) ? array_values( $preset['typography'] ) : array(), |
| 1064 |
'preset' => $preset, |
| 1065 |
); |
| 1066 |
} |
| 1067 |
|
| 1068 |
public static function rewrite_kit_references( &$blocks, $kit_global, $session_id = '' ) { |
| 1069 |
if ( ! is_array( $blocks ) || ! is_array( $kit_global ) ) { |
| 1070 |
return array(); |
| 1071 |
} |
| 1072 |
|
| 1073 |
$colors = ( ! empty( $kit_global['color'] ) && is_array( $kit_global['color'] ) ) ? $kit_global['color'] : array(); |
| 1074 |
$typos = ( ! empty( $kit_global['typo'] ) && is_array( $kit_global['typo'] ) ) ? $kit_global['typo'] : array(); |
| 1075 |
|
| 1076 |
if ( empty( $colors ) && empty( $typos ) ) { |
| 1077 |
return array(); |
| 1078 |
} |
| 1079 |
|
| 1080 |
/* The browser matches with `Number(temp_id) + 1 == n`, so C11 means the entry whose |
| 1081 |
* kit id is 10. Keyed the same way here so a kit whose ids start at 0 still lines up. */ |
| 1082 |
/* Same offset from_kit() uses, from the same captured baseline, so the id a block is |
| 1083 |
* pointed at is the slot the palette actually puts that entry in. Reading the baseline |
| 1084 |
* separately in each place would work only for as long as the two happened to agree. */ |
| 1085 |
$baseline = self::baseline( $session_id ); |
| 1086 |
$color_offset = count( $baseline['colors'] ); |
| 1087 |
$typo_offset = count( $baseline['typography'] ); |
| 1088 |
|
| 1089 |
$color_map = array(); |
| 1090 |
|
| 1091 |
foreach ( $colors as $i => $entry ) { |
| 1092 |
if ( is_array( $entry ) && isset( $entry['id'] ) ) { |
| 1093 |
$color_map[ (int) $entry['id'] + 1 ] = $color_offset + $i + 1; |
| 1094 |
} |
| 1095 |
} |
| 1096 |
|
| 1097 |
$typo_map = array(); |
| 1098 |
|
| 1099 |
foreach ( $typos as $i => $entry ) { |
| 1100 |
if ( is_array( $entry ) && isset( $entry['id'] ) ) { |
| 1101 |
$typo_map[ (int) $entry['id'] + 1 ] = array( |
| 1102 |
'site_id' => $typo_offset + $i + 1, |
| 1103 |
'value' => ( isset( $entry['value'] ) && is_array( $entry['value'] ) ) ? $entry['value'] : array(), |
| 1104 |
); |
| 1105 |
} |
| 1106 |
} |
| 1107 |
|
| 1108 |
$moved = array(); |
| 1109 |
|
| 1110 |
self::walk_kit_references( $blocks, $color_map, $typo_map, $moved ); |
| 1111 |
|
| 1112 |
return $moved; |
| 1113 |
} |
| 1114 |
|
| 1115 |
/** |
| 1116 |
* Recurse through blocks and attributes applying the reference rewrite. |
| 1117 |
* |
| 1118 |
* @param array $nodes Blocks or attribute values, by reference. |
| 1119 |
* @param array $color_map n => site id. |
| 1120 |
* @param array $typo_map n => {site_id, value}. |
| 1121 |
* @param array $moved Collects oldId => newId, by reference. |
| 1122 |
* @return void |
| 1123 |
*/ |
| 1124 |
private static function walk_kit_references( &$nodes, $color_map, $typo_map, &$moved ) { |
| 1125 |
foreach ( $nodes as $key => &$value ) { |
| 1126 |
if ( is_string( $value ) ) { |
| 1127 |
$nodes[ $key ] = self::swap_colour_refs( $value, $color_map ); |
| 1128 |
continue; |
| 1129 |
} |
| 1130 |
|
| 1131 |
if ( ! is_array( $value ) ) { |
| 1132 |
continue; |
| 1133 |
} |
| 1134 |
|
| 1135 |
/* A typography object: swap the whole thing for the global it points at, exactly as |
| 1136 |
* Extract_gutenberg_global()'s `new_data = typo_data` does. */ |
| 1137 |
if ( isset( $value['globalTypo'] ) && ! is_array( $value['globalTypo'] ) ) { |
| 1138 |
$old = (int) $value['globalTypo']; |
| 1139 |
|
| 1140 |
if ( isset( $typo_map[ $old ] ) ) { |
| 1141 |
$resolved = $typo_map[ $old ]['value']; |
| 1142 |
|
| 1143 |
$resolved['globalTypo'] = $typo_map[ $old ]['site_id']; |
| 1144 |
|
| 1145 |
$moved[ $old ] = $typo_map[ $old ]['site_id']; |
| 1146 |
|
| 1147 |
$nodes[ $key ] = $resolved; |
| 1148 |
|
| 1149 |
continue; |
| 1150 |
} |
| 1151 |
} |
| 1152 |
|
| 1153 |
self::walk_kit_references( $value, $color_map, $typo_map, $moved ); |
| 1154 |
} |
| 1155 |
|
| 1156 |
unset( $value ); |
| 1157 |
} |
| 1158 |
|
| 1159 |
/** |
| 1160 |
* Rewrite `var(--tpgb-C{n})` inside one string. |
| 1161 |
* |
| 1162 |
* @param string $subject Attribute value. |
| 1163 |
* @param array $color_map n => site id. |
| 1164 |
* @return string |
| 1165 |
*/ |
| 1166 |
private static function swap_colour_refs( $subject, $color_map ) { |
| 1167 |
if ( empty( $color_map ) || false === strpos( $subject, 'tpgb-C' ) ) { |
| 1168 |
return $subject; |
| 1169 |
} |
| 1170 |
|
| 1171 |
return preg_replace_callback( |
| 1172 |
'/var\\(\\s*--tpgb-C(\\d+)\\s*\\)/', |
| 1173 |
function ( $m ) use ( $color_map ) { |
| 1174 |
$n = (int) $m[1]; |
| 1175 |
|
| 1176 |
return isset( $color_map[ $n ] ) ? 'var(--tpgb-C' . $color_map[ $n ] . ')' : $m[0]; |
| 1177 |
}, |
| 1178 |
$subject |
| 1179 |
); |
| 1180 |
} |
| 1181 |
|
| 1182 |
/** |
| 1183 |
* Renumber the per-property typography variables in serialised markup. |
| 1184 |
* |
| 1185 |
* These read `var(--tpgb-15-font-size)` - the kit's typography id, no `T` prefix - and |
| 1186 |
* they turn up in innerHTML as well as in attributes, which is why this runs over the |
| 1187 |
* serialised string rather than the block array. |
| 1188 |
* |
| 1189 |
* Two passes through a sentinel, because the id spaces overlap: renumbering 15 to 1 |
| 1190 |
* directly and then 1 to something else would move the same reference twice. |
| 1191 |
* |
| 1192 |
* @param string $content Serialised block markup. |
| 1193 |
* @param array $moved oldId => newId. |
| 1194 |
* @return string |
| 1195 |
*/ |
| 1196 |
public static function renumber_typo_vars( $content, $moved ) { |
| 1197 |
if ( ! is_string( $content ) || empty( $moved ) ) { |
| 1198 |
return (string) $content; |
| 1199 |
} |
| 1200 |
|
| 1201 |
$props = array( |
| 1202 |
'font-family', |
| 1203 |
'font-weight', |
| 1204 |
'font-style', |
| 1205 |
'font-size', |
| 1206 |
'line-height', |
| 1207 |
'letter-spacing', |
| 1208 |
'text-transform', |
| 1209 |
'text-decoration', |
| 1210 |
); |
| 1211 |
|
| 1212 |
$search = array(); |
| 1213 |
$replace = array(); |
| 1214 |
|
| 1215 |
foreach ( $moved as $old => $new ) { |
| 1216 |
foreach ( $props as $prop ) { |
| 1217 |
$search[] = '--tpgb-' . $old . '-' . $prop; |
| 1218 |
$replace[] = '--tpgb-@@' . $new . '@@-' . $prop; |
| 1219 |
} |
| 1220 |
} |
| 1221 |
|
| 1222 |
$content = str_replace( $search, $replace, $content ); |
| 1223 |
|
| 1224 |
return str_replace( array( '--tpgb-@@', '@@-' ), array( '--tpgb-', '-' ), $content ); |
| 1225 |
} |
| 1226 |
} |
| 1227 |
} |
| 1228 |
|