PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / trunk
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! vtrunk
3.8.0 3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 All 112 releases
templately / modules / block-recovery / AttributeSnapshot.php

AttributeSnapshot.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! trunk, at modules/block-recovery/AttributeSnapshot.php

199 lines 6.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Templately\Modules\BlockRecovery;
4
5 /**
6 * Keeps the attributes a rebuild would otherwise delete.
7 *
8 * Regenerating a block does NOT rewrite only its markup — it rewrites the block comment too.
9 * `parse` drops every attribute the installed block type does not declare, `createBlock` builds
10 * from what survived, and `serialize` writes the comment from that. Measured on the SaaStark
11 * pack with Essential Blocks Pro inactive: a rebuild took `data-gsap` 13 -> 0 in the markup AND
12 * `"ebGsap"` 13 -> 0 in the comments. The animation config was simply gone, and activating Pro
13 * afterwards could not bring it back.
14 *
15 * So the rebuild happens in two halves:
16 *
17 * 1. SNAPSHOT (here, before the editor saves) — read the stored comment JSON and remember
18 * every attribute, keyed by `blockId`.
19 * 2. RESTORE (here, after the editor saves) — merge back any key the rebuild dropped.
20 *
21 * The result is exactly "regenerate the rendered HTML, leave the attributes alone": the markup
22 * is what THIS site's `save()` produces, so the block validates, while the comment still carries
23 * the Pro/newer-plugin configuration for the day that plugin shows up. PHP's `serialize_block()`
24 * writes whatever is in `attrs` — unlike the JS serializer, it does not filter against the
25 * registered block type — which is why the restore has to happen on this side.
26 */
27 class AttributeSnapshot {
28
29 /** Where a post's pre-rebuild attributes live between the two halves. */
30 const META_KEY = '_templately_block_attr_snapshot';
31
32 /**
33 * Record every block's stored attributes, keyed by `blockId`.
34 *
35 * `blockId` rather than document position: it is the only identifier shared between the
36 * stored markup and the editor's parsed tree that survives a deprecation migration, and
37 * every block that can carry an unknown attribute (Essential Blocks and friends) has one.
38 * A block without one is not snapshotted, and the gate keeps refusing to rebuild it.
39 *
40 * @param int $post_id
41 * @return int Number of blocks recorded.
42 */
43 public static function capture( int $post_id ): int {
44 $post = get_post( $post_id );
45
46 if ( ! $post ) {
47 return 0;
48 }
49
50 $map = self::attributes_by_block_id( (string) $post->post_content );
51
52 if ( empty( $map ) ) {
53 delete_post_meta( $post_id, self::META_KEY );
54
55 return 0;
56 }
57
58 // wp_slash: update_post_meta unslashes, and these values are JSON-ish arrays that can
59 // legitimately contain backslashes (unicode escapes in EB's generated CSS).
60 update_post_meta( $post_id, self::META_KEY, wp_slash( wp_json_encode( $map ) ) );
61
62 return count( $map );
63 }
64
65 /**
66 * Merge back whatever the rebuild dropped, then rewrite the post if anything changed.
67 *
68 * Only ADDS keys that are missing. A key the rebuild kept is left exactly as the editor
69 * wrote it — the editor is authoritative for anything this site understands; the snapshot is
70 * authoritative only for what it could not see.
71 *
72 * @param int $post_id
73 * @return array{restored:int,blocks:int} Attributes restored, and how many blocks got them.
74 */
75 public static function restore( int $post_id ): array {
76 $result = [ 'restored' => 0, 'blocks' => 0 ];
77
78 $post = get_post( $post_id );
79 $raw = get_post_meta( $post_id, self::META_KEY, true );
80
81 if ( ! $post || empty( $raw ) ) {
82 return $result;
83 }
84
85 $snapshot = json_decode( (string) $raw, true );
86
87 if ( ! is_array( $snapshot ) || empty( $snapshot ) ) {
88 delete_post_meta( $post_id, self::META_KEY );
89
90 return $result;
91 }
92
93 $blocks = parse_blocks( (string) $post->post_content );
94 $changed = false;
95
96 $walk = function ( array &$list ) use ( &$walk, $snapshot, &$result, &$changed ) {
97 foreach ( $list as &$block ) {
98 $block_id = $block['attrs']['blockId'] ?? null;
99
100 if ( is_string( $block_id ) && isset( $snapshot[ $block_id ] ) && is_array( $snapshot[ $block_id ] ) ) {
101 $missing = array_diff_key( $snapshot[ $block_id ], $block['attrs'] );
102
103 if ( ! empty( $missing ) ) {
104 $block['attrs'] = array_merge( $block['attrs'], $missing );
105 $result['restored'] += count( $missing );
106 $result['blocks']++;
107 $changed = true;
108 }
109 }
110
111 if ( ! empty( $block['innerBlocks'] ) ) {
112 $walk( $block['innerBlocks'] );
113 }
114 }
115 };
116 $walk( $blocks );
117
118 if ( $changed ) {
119 // Bypass kses for this write. On MULTISITE `unfiltered_html` belongs to super admins
120 // only, so an ordinary site administrator running the pass would have their own
121 // already-stored content re-filtered on the way back in — and kses can only ever
122 // remove from it. This write adds nothing new: every byte is either what the editor
123 // just saved or what was already in the post when we snapshotted it, so re-sanitising
124 // it buys no safety and risks corrupting block markup.
125 $kses_was_active = has_filter( 'content_save_pre', 'wp_filter_post_kses' );
126
127 if ( $kses_was_active ) {
128 kses_remove_filters();
129 }
130
131 wp_update_post(
132 [
133 'ID' => $post_id,
134 'post_content' => wp_slash( serialize_blocks( $blocks ) ),
135 ]
136 );
137
138 if ( $kses_was_active ) {
139 kses_init_filters();
140 }
141 }
142
143 delete_post_meta( $post_id, self::META_KEY );
144
145 return $result;
146 }
147
148 /**
149 * Attributes of every block comment that carries a `blockId`, keyed by it.
150 *
151 * Reads the RAW comments rather than `parse_blocks()` output: `parse_blocks` hands back
152 * whatever the JSON decoded to, which is what we want here — but going through the parser
153 * would tie this to WP's block registry, and the whole point is to capture attributes the
154 * registry knows nothing about.
155 *
156 * @param string $content
157 * @return array<string,array<string,mixed>>
158 */
159 public static function attributes_by_block_id( string $content ): array {
160 $map = [];
161
162 if ( '' === $content ) {
163 return $map;
164 }
165
166 // Block comments are single-line and never nest, so a non-greedy match to the closing
167 // delimiter cannot swallow the following block.
168 if ( ! preg_match_all( '#<!--\s+wp:[a-z0-9-]+/[a-z0-9-]+\s+(\{.*?\})\s*/?-->#s', $content, $matches ) ) {
169 return $map;
170 }
171
172 $seen = [];
173
174 foreach ( $matches[1] as $json ) {
175 $attrs = json_decode( $json, true );
176
177 if ( ! is_array( $attrs ) || ! isset( $attrs['blockId'] ) || ! is_string( $attrs['blockId'] ) ) {
178 continue;
179 }
180
181 $block_id = $attrs['blockId'];
182
183 // A duplicated blockId (copy/paste in the editor, or a pack that repeats a section)
184 // would make the restore apply one block's attributes to a different block. There is
185 // no way to tell them apart afterwards, so drop the id entirely rather than guess —
186 // those blocks keep the conservative treatment and are left as imported.
187 if ( isset( $seen[ $block_id ] ) ) {
188 unset( $map[ $block_id ] );
189 continue;
190 }
191
192 $seen[ $block_id ] = true;
193 $map[ $block_id ] = $attrs;
194 }
195
196 return $map;
197 }
198 }
199