PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.8.0
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.8.0
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 / Queue.php

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

259 lines 7.9 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 use Templately\Modules\FullSiteImport\Utils\SessionData;
6 use Templately\Utils\Base;
7
8 /**
9 * The list of imported posts worth re-opening in a block editor after an import finishes.
10 *
11 * Written by PHP, consumed by the browser: `save()` is JS-only, so the repair itself cannot
12 * happen here (see module.php). Persisted rather than passed through the SSE result because the
13 * pass runs after the import request is gone, may be interrupted by the user closing the tab,
14 * and must survive that — the queue is the resume point.
15 */
16 class Queue extends Base {
17
18 /** Where the pending queue lives. Autoload off — it is read on demand, by one screen. */
19 const OPTION = 'templately_block_rebuild_queue';
20
21 /** Post types that never carry block markup a rebuild could repair. */
22 const SKIPPED_POST_TYPES = [ 'attachment', 'revision', 'nav_menu_item', 'customize_changeset' ];
23
24 /**
25 * Queue entries older than this are dropped. Matches SessionData's own retention: a queue
26 * pointing at a week-old import is not worth acting on, and stale entries would otherwise
27 * accumulate on a site that imports repeatedly.
28 */
29 const MAX_AGE = 7 * DAY_IN_SECONDS;
30
31 public function __construct() {
32 add_action( 'templately_fsi_import_complete', [ $this, 'capture' ] );
33
34 // The generic producer seam. A single-template import writes a post to disk exactly the
35 // way a full-site import does, so it needs the same follow-up — but hooking it onto
36 // `templately_fsi_import_complete` would be a lie about where the content came from, and
37 // FSI's listener reads FSI session state a single import does not have.
38 add_action( 'templately_blocks_imported', [ $this, 'capture_posts' ], 10, 2 );
39 }
40
41 /**
42 * Queue specific posts, appending to whatever is already pending.
43 *
44 * APPEND, not replace: single imports arrive one at a time and each one is its own event —
45 * replacing would silently drop the previous page the moment the user imported a second.
46 * (A full site import DOES replace, via `capture()`: a new site supersedes the old queue.)
47 *
48 * @param int[] $post_ids Posts just written.
49 * @param string $context Where they came from, e.g. 'single' — recorded for reporting only.
50 */
51 public function capture_posts( $post_ids, $context = 'single' ) {
52 $items = self::eligible_items( is_array( $post_ids ) ? $post_ids : [ $post_ids ] );
53
54 if ( empty( $items ) ) {
55 return;
56 }
57
58 $queue = self::get();
59
60 if ( empty( $queue ) ) {
61 $queue = [
62 'session_id' => null,
63 'created_at' => time(),
64 'items' => [],
65 'parity_warnings' => [],
66 'done' => [],
67 ];
68 }
69
70 $known = array_column( $queue['items'], 'id' );
71 foreach ( $items as $item ) {
72 if ( ! in_array( $item['id'], $known, true ) ) {
73 $item['context'] = (string) $context;
74 $queue['items'][] = $item;
75 }
76 }
77
78 // A re-queued post must be visited again even if an earlier pass marked it done — its
79 // content just changed.
80 $queue['done'] = array_values( array_diff(
81 array_map( 'intval', $queue['done'] ?? [] ),
82 array_column( $items, 'id' )
83 ) );
84
85 self::put( $queue );
86 }
87
88 /**
89 * Filter post ids down to the ones a block editor could actually validate.
90 *
91 * @param array $post_ids
92 * @return array<int,array{id:int,type:string}>
93 */
94 private static function eligible_items( array $post_ids ): array {
95 $items = [];
96
97 foreach ( array_unique( array_map( 'intval', $post_ids ) ) as $post_id ) {
98 $post = get_post( $post_id );
99
100 if ( ! $post || in_array( $post->post_type, self::SKIPPED_POST_TYPES, true ) ) {
101 continue;
102 }
103
104 // No block delimiters means nothing for a block editor to validate.
105 if ( false === strpos( (string) $post->post_content, '<!-- wp:' ) ) {
106 continue;
107 }
108
109 $items[] = [
110 'id' => $post_id,
111 'type' => $post->post_type,
112 ];
113 }
114
115 return $items;
116 }
117
118 /**
119 * Record what this import created, so the browser pass knows where to look.
120 *
121 * Deliberately records EVERY imported post rather than only the ones we suspect: the
122 * version-drift failure (cause A) happens in content this plugin never touched, so a
123 * suspicion list built from our own writes would miss exactly the case that motivated the
124 * pass. The merge-time parity warnings ride along as priority information, not as a filter.
125 */
126 public function capture() {
127 // Elementor content is JSON in post meta and has no `save()` validation, so there is
128 // nothing for a block editor to repair. An Elementor pack still ships a few posts with
129 // core-block markup (blog posts, WooCommerce pages), and queueing those cost an editor
130 // boot per post for a pass that could never find anything — the whole "Optimising
131 // imported blocks… (1/4)" wait on an Elementor import came from exactly that.
132 if ( self::imported_platform() === 'elementor' ) {
133 return;
134 }
135
136 $session_id = SessionData::get_session_id();
137
138 $imported = get_option( 'templately_fsi_imported_list', [] );
139 $post_ids = is_array( $imported ) && ! empty( $imported['posts'] ) ? $imported['posts'] : [];
140
141 $items = self::eligible_items( $post_ids );
142
143 if ( empty( $items ) ) {
144 return;
145 }
146
147 $warnings = $session_id ? SessionData::get( $session_id, 'parity_warnings' ) : [];
148
149 self::put(
150 [
151 'session_id' => $session_id,
152 'created_at' => time(),
153 'items' => $items,
154 'parity_warnings' => is_array( $warnings ) ? $warnings : [],
155 'done' => [],
156 ]
157 );
158 }
159
160 /**
161 * Which editor the import that just finished was built for.
162 *
163 * Read from the option the import writes as it starts (`RunsImport`), because the
164 * `templately_fsi_import_complete` payload does not carry the platform.
165 */
166 private static function imported_platform(): string {
167 return (string) get_option( 'templately_import_platform', '' );
168 }
169
170 /**
171 * The current queue, or an empty array when there is nothing pending or it has expired.
172 *
173 * @return array
174 */
175 public static function get(): array {
176 $queue = get_option( self::OPTION, [] );
177
178 if ( empty( $queue['items'] ) || ! is_array( $queue['items'] ) ) {
179 return [];
180 }
181
182 if ( empty( $queue['created_at'] ) || ( time() - (int) $queue['created_at'] ) > self::MAX_AGE ) {
183 self::clear();
184
185 return [];
186 }
187
188 return $queue;
189 }
190
191 /**
192 * Items not yet reported as handled — what a (re-)run should actually visit.
193 *
194 * @return array<int,array{id:int,type:string}>
195 */
196 public static function pending(): array {
197 $queue = self::get();
198
199 if ( empty( $queue ) ) {
200 return [];
201 }
202
203 $done = ! empty( $queue['done'] ) && is_array( $queue['done'] ) ? array_map( 'intval', $queue['done'] ) : [];
204
205 return array_values( array_filter( $queue['items'], function ( $item ) use ( $done ) {
206 return ! in_array( (int) $item['id'], $done, true );
207 } ) );
208 }
209
210 /**
211 * Is this one post still waiting for a visit?
212 *
213 * The on-open trigger's half of eligibility (060 FR-002): a post with no provenance yet —
214 * imported before single imports recorded it — is still repairable while its queue entry
215 * lives.
216 */
217 public static function is_pending( int $post_id ): bool {
218 foreach ( self::pending() as $item ) {
219 if ( (int) $item['id'] === $post_id ) {
220 return true;
221 }
222 }
223
224 return false;
225 }
226
227 /**
228 * Mark one post as handled, whatever the outcome.
229 *
230 * A post that failed is still marked done: re-opening an editor that already refused to boot
231 * on the next run would just spend the same time again for the same answer, and the failure
232 * is reported separately.
233 */
234 public static function mark_done( int $post_id ) {
235 $queue = self::get();
236
237 if ( empty( $queue ) ) {
238 return;
239 }
240
241 $done = ! empty( $queue['done'] ) && is_array( $queue['done'] ) ? $queue['done'] : [];
242
243 if ( ! in_array( $post_id, array_map( 'intval', $done ), true ) ) {
244 $done[] = $post_id;
245 }
246
247 $queue['done'] = $done;
248 self::put( $queue );
249 }
250
251 public static function put( array $queue ) {
252 update_option( self::OPTION, $queue, false );
253 }
254
255 public static function clear() {
256 delete_option( self::OPTION );
257 }
258 }
259