PluginProbe
Extendify / 3.1.5
Extendify v3.1.5
3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 0.7.0 All 126 releases
extendify / app / QuickEdit / Services / BlockFingerprint.php

BlockFingerprint.php in Extendify 3.1.5, at app/QuickEdit/Services/BlockFingerprint.php

99 lines 4.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Extendify\QuickEdit\Services;
4
5 defined('ABSPATH') || die('No direct access.');
6
7 // Render-time identity check for a clicked block. The client reads a block's
8 // integer id from a render-time data attribute on the live DOM node it
9 // clicked; the server re-derives that id by counting the parsed post. The two
10 // can disagree (synced patterns, nested navs, dynamic expansion) and land on a
11 // different block of the same type — past the type guard. The client also
12 // sends a fingerprint of the block it actually clicked, and this refuses the
13 // save when the resolved block doesn't carry it.
14 //
15 // The fingerprint is read from the LIVE element on the client, never the
16 // cached block source — that source is resolved by the same count as the save,
17 // so it would echo a misresolve and the check would pass on the wrong block.
18 class BlockFingerprint
19 {
20 // True when every field the client provided matches the resolved block.
21 // Fields are independent and additive (text, service); an absent field
22 // doesn't constrain the match, so a client that sends nothing fails open.
23 //
24 // The client reads text from the rendered DOM, so when a shortcode or other
25 // the_content transform expands it differently than the stored markup, the
26 // caller can pass the block's rendered HTML as an extra text candidate.
27 //
28 // $prefix matches when the fingerprint is only a *prefix* of the block's
29 // text. A block-level shortcode render (e.g. [products] -> a <div>) can't
30 // nest in a <p>, so the browser splits the paragraph and the live element's
31 // text is truncated at that point — the fingerprint becomes a prefix of the
32 // stored block. Used as a last-resort recovery, unique-match only.
33 public static function matches(
34 array $block,
35 array $fingerprint,
36 string $renderedText = '',
37 bool $prefix = false
38 ): bool {
39 if (isset($fingerprint['service'])) {
40 $service = (string) ($block['attrs']['service'] ?? '');
41 if ($service !== (string) $fingerprint['service']) {
42 return false;
43 }
44 }
45
46 if (isset($fingerprint['text'])) {
47 $want = self::normalize((string) $fingerprint['text']);
48 // Visible text lives in attrs.label for dynamic items (nav
49 // link/submenu) and in innerHTML for static text blocks (paragraph,
50 // heading, button) — accept either, plus the rendered text if given.
51 $candidates = [
52 self::normalize((string) ($block['attrs']['label'] ?? '')),
53 self::normalize(self::stripText((string) ($block['innerHTML'] ?? ''))),
54 ];
55 if ($renderedText !== '') {
56 $candidates[] = self::normalize(self::stripText($renderedText));
57 }
58 $textOk = false;
59 foreach ($candidates as $candidate) {
60 $hit = $prefix
61 ? ($want !== '' && strncmp($candidate, $want, strlen($want)) === 0)
62 : ($candidate === $want);
63 if ($hit) {
64 $textOk = true;
65 break;
66 }
67 }
68 if (!$textOk) {
69 return false;
70 }
71 }
72
73 return true;
74 }
75
76 private static function stripText(string $html): string
77 {
78 return html_entity_decode(wp_strip_all_tags($html), ENT_QUOTES);
79 }
80
81 // Fold wptexturize's typographic substitutions back to ASCII: the client
82 // reads the block's text from the rendered (texturized) DOM while this
83 // fingerprints the raw stored markup, so without folding an apostrophe
84 // alone ("Woody's" vs "Woody’s") falses a 409. Must stay in lockstep with
85 // normalizeText in src/QuickEdit/lib/fingerprint.js.
86 private static function normalize(string $value): string
87 {
88 $value = strtr($value, [
89 "\u{2018}" => "'", "\u{2019}" => "'", "\u{201A}" => "'", "\u{201B}" => "'",
90 "\u{201C}" => '"', "\u{201D}" => '"', "\u{201E}" => '"', "\u{201F}" => '"',
91 "\u{2012}" => '-', "\u{2013}" => '-', "\u{2014}" => '-', "\u{2015}" => '-',
92 "\u{2026}" => '...',
93 "\u{00A0}" => ' ', "\u{2009}" => ' ', "\u{202F}" => ' ',
94 ]);
95 $value = (string) preg_replace('/-{2,}/', '-', $value);
96 return trim((string) preg_replace('/\s+/', ' ', $value));
97 }
98 }
99