PluginProbe
Photo Gallery by Re Gallery: Responsive Image Gallery with AI SEO / trunk
Photo Gallery by Re Gallery: Responsive Image Gallery with AI SEO vtrunk
1.20.14 1.20.13 1.20.12 1.20.11 1.20.10 1.20.9 1.20.8 1.20.7 1.20.6 1.20.5 1.20.4 1.20.3 1.20.2 1.20.1 1.20.0 1.19.2 1.19.1 1.19.0 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.11.0 All 135 releases
regallery / includes / migration.php

migration.php in Photo Gallery by Re Gallery: Responsive Image Gallery with AI SEO trunk, at includes/migration.php

719 lines 27.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 defined('ABSPATH') || die('Access Denied');
4
5 require_once REACG_PLUGIN_DIR . '/includes/migration/interface-provider.php';
6 require_once REACG_PLUGIN_DIR . '/includes/migration/provider-10web.php';
7 require_once REACG_PLUGIN_DIR . '/includes/migration/provider-envira.php';
8 require_once REACG_PLUGIN_DIR . '/includes/migration/provider-foogallery.php';
9 require_once REACG_PLUGIN_DIR . '/includes/migration/provider-gutenberg.php';
10 require_once REACG_PLUGIN_DIR . '/includes/migration/engine.php';
11
12 class REACG_Migration {
13 private $obj;
14 private $engine;
15 private $page_slug = 'reacg-migration';
16
17 public function __construct($that) {
18 $this->obj = $that;
19 $this->engine = new REACG_Migration_Engine([
20 new REACG_Migration_Provider_10Web(),
21 new REACG_Migration_Provider_Envira(),
22 new REACG_Migration_Provider_FooGallery(),
23 new REACG_Migration_Provider_Gutenberg(),
24 ]);
25
26 add_action('admin_menu', [$this, 'add_submenu']);
27 add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
28
29 add_action('wp_ajax_reacg_migration_sources', [$this, 'ajax_sources']);
30 add_action('wp_ajax_reacg_migration_galleries', [$this, 'ajax_galleries']);
31 add_action('wp_ajax_reacg_migration_import', [$this, 'ajax_import']);
32 add_action('wp_ajax_reacg_migration_replace_shortcode', [$this, 'ajax_replace_shortcode']);
33 }
34
35 public function add_submenu() {
36 add_submenu_page(
37 'edit.php?post_type=' . REACG_CUSTOM_POST_TYPE,
38 __('Migrate Galleries', 'regallery'),
39 __('Migrate Galleries', 'regallery'),
40 'manage_options',
41 $this->page_slug,
42 [$this, 'render_page'],
43 2
44 );
45 }
46
47 public function enqueue_assets($hook) {
48 if (empty($_GET['page']) || sanitize_key(wp_unslash($_GET['page'])) !== $this->page_slug) {
49 return;
50 }
51
52 wp_enqueue_style(
53 REACG_PREFIX . '_migration',
54 REACG_PLUGIN_URL . '/assets/css/migration.css',
55 [REACG_PREFIX . '_admin'],
56 REACG_VERSION
57 );
58
59 wp_enqueue_script(
60 REACG_PREFIX . '_migration',
61 REACG_PLUGIN_URL . '/assets/js/migration.js',
62 ['jquery'],
63 REACG_VERSION,
64 true
65 );
66
67 wp_localize_script(REACG_PREFIX . '_migration', 'reacg_migration', [
68 'ajax_url' => admin_url('admin-ajax.php'),
69 'nonce_key' => REACG_NONCE,
70 'nonce' => wp_create_nonce(REACG_NONCE),
71 'edit_url' => admin_url('post.php?action=edit&post='),
72 'i18n' => [
73 'loading' => __('Loading...', 'regallery'),
74 'loading_sources' => __('Loading sources...', 'regallery'),
75 'loading_galleries' => __('Loading galleries...', 'regallery'),
76 'importing' => __('Importing galleries...', 'regallery'),
77 'load_galleries' => __('Load galleries', 'regallery'),
78 'import_selected' => __('Import selected', 'regallery'),
79 'no_data' => __('No galleries found.', 'regallery'),
80 'select_source' => __('Select source', 'regallery'),
81 'select_galleries' => __('Select galleries to import.', 'regallery'),
82 'select_not_migrated' => __('Only not migrated galleries were selected. Select at least one gallery that has not been migrated yet, or confirm importing migrated galleries too.', 'regallery'),
83 'migration_done' => __('Migration finished.', 'regallery'),
84 'force_new_alert' => __('One or more selected galleries have already been migrated. Confirm to migrate them again, or cancel to migrate only galleries that have not been migrated yet.', 'regallery'),
85 'alert_title' => __('Migration notice', 'regallery'),
86 'alert_confirm' => __('Migrate all selected', 'regallery'),
87 'alert_cancel' => __('Only not migrated', 'regallery'),
88 'alert_close' => __('Close dialog', 'regallery'),
89 'not_installed' => __('Not installed', 'regallery'),
90 'all_sources' => __('All sources', 'regallery'),
91 'column_title' => __('Gallery title', 'regallery'),
92 'column_source' => __('Source', 'regallery'),
93 'column_images' => __('Images count', 'regallery'),
94 'column_status' => __('Status', 'regallery'),
95 'column_replace' => __('Replace shortcode', 'regallery'),
96 'status_migrated' => __('Migrated', 'regallery'),
97 'status_not_migrated' => __('Not migrated', 'regallery'),
98 'replace_shortcode' => __('Replace', 'regallery'),
99 'replace_replaced' => __('Replaced', 'regallery'),
100 'replace_already_replaced' => __('Replaced', 'regallery'),
101 'replace_not_supported' => __('Not supported', 'regallery'),
102 'replacing_shortcode' => __('Replacing shortcodes...', 'regallery'),
103 'replace_done' => __('Galleries replaced successfully.', 'regallery'),
104 'replace_none_found' => __('No matching galleries were found.', 'regallery'),
105 'replace_confirm_title' => __('Confirm replacement', 'regallery'),
106 'replace_confirm_message' => __('Replace all matching galleries with the migrated gallery?', 'regallery'),
107 'replace_confirm_button' => __('Replace now', 'regallery'),
108 'replace_cancel_button' => __('Cancel', 'regallery'),
109 'items' => __('items', 'regallery'),
110 'first_page' => __('First page', 'regallery'),
111 'prev' => __('Previous page', 'regallery'),
112 'next' => __('Next page', 'regallery'),
113 'last_page' => __('Last page', 'regallery'),
114 'current_page' => __('Current page', 'regallery'),
115 'of' => __('of', 'regallery'),
116 'error' => __('Something went wrong.', 'regallery'),
117 ],
118 ]);
119 }
120
121 public function render_page() {
122 if (!current_user_can('manage_options')) {
123 wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'regallery'));
124 }
125 ?>
126 <div class="wrap reacg-migration-page">
127 <h1><?php esc_html_e('Universal Gallery Migration', 'regallery'); ?></h1>
128 <div id="reacg-migration-progress" class="reacg-migration-progress" style="display:none;">
129 <span class="spinner is-active" aria-hidden="true"></span>
130 <span id="reacg-migration-progress-text"></span>
131 <div class="reacg-migration-progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
132 <div id="reacg-migration-progress-fill" class="reacg-migration-progress-fill"></div>
133 </div>
134 </div>
135
136 <div id="reacg-migration-status" class="notice inline" style="display:none;"></div>
137
138 <p class="search-box">
139 <label class="screen-reader-text" for="reacg-migration-search"><?php esc_html_e('Search Galleries', 'regallery'); ?></label>
140 <input type="search" id="reacg-migration-search" />
141 <button type="button" class="button" id="reacg-migration-search-submit"><?php esc_html_e('Search Galleries', 'regallery'); ?></button>
142 </p>
143 <div class="tablenav top">
144 <div class="alignleft actions">
145 <label class="screen-reader-text" for="reacg-migration-source"><?php esc_html_e('Filter by source', 'regallery'); ?></label>
146 <select id="reacg-migration-source"></select>
147 <button type="button" class="button action" id="reacg-migration-load">
148 <?php esc_html_e('Filter', 'regallery'); ?>
149 </button>
150 <input type="hidden" id="reacg-migration-force-new" value="0" />
151 </div>
152 <div class="tablenav-pages" id="reacg-migration-pages-top"></div>
153 </div>
154
155 <table class="wp-list-table widefat fixed striped table-view-list reacg-migration-table">
156 <thead>
157 <tr>
158 <td class="manage-column column-cb check-column"><input type="checkbox" id="reacg-migration-toggle-all" /></td>
159 <th scope="col" class="manage-column sorted desc" data-sort="title" aria-sort="ascending">
160 <a href="#"><span><?php esc_html_e('Gallery title', 'regallery'); ?></span><span class="sorting-indicators"><span class="sorting-indicator asc" aria-hidden="true"></span><span class="sorting-indicator desc" aria-hidden="true"></span></span></a>
161 </th>
162 <th scope="col" class="manage-column sortable desc" data-sort="source" aria-sort="none">
163 <a href="#"><span><?php esc_html_e('Source', 'regallery'); ?></span><span class="sorting-indicators"><span class="sorting-indicator asc" aria-hidden="true"></span><span class="sorting-indicator desc" aria-hidden="true"></span></span></a>
164 </th>
165 <th scope="col" class="manage-column sortable desc" data-sort="image_count" aria-sort="none">
166 <a href="#"><span><?php esc_html_e('Images count', 'regallery'); ?></span><span class="sorting-indicators"><span class="sorting-indicator asc" aria-hidden="true"></span><span class="sorting-indicator desc" aria-hidden="true"></span></span></a>
167 </th>
168 <th scope="col" class="manage-column sortable desc" data-sort="status" aria-sort="none">
169 <a href="#"><span><?php esc_html_e('Status', 'regallery'); ?></span><span class="sorting-indicators"><span class="sorting-indicator asc" aria-hidden="true"></span><span class="sorting-indicator desc" aria-hidden="true"></span></span></a>
170 </th>
171 <th scope="col" class="manage-column">
172 <?php esc_html_e('Replace gallery', 'regallery'); ?>
173 </th>
174 </tr>
175 </thead>
176 <tbody id="reacg-migration-list"></tbody>
177 </table>
178
179 <div class="tablenav bottom">
180 <div class="alignleft actions">
181 <button type="button" class="button button-primary" id="reacg-migration-import" disabled>
182 <?php esc_html_e('Migrate', 'regallery'); ?>
183 </button>
184 <span id="reacg-migration-import-spinner" class="spinner" aria-hidden="true"></span>
185 </div>
186 <div class="tablenav-pages" id="reacg-migration-pages-bottom"></div>
187 </div>
188
189 <div id="reacg-migration-results"></div>
190
191 <div id="reacg-migration-alert" class="reacg-migration-alert" style="display:none;" aria-hidden="true">
192 <div class="reacg-migration-alert__backdrop"></div>
193 <div class="reacg-migration-alert__dialog" role="dialog" aria-modal="true" aria-labelledby="reacg-migration-alert-title" aria-describedby="reacg-migration-alert-message">
194 <button type="button" class="reacg-migration-alert__close" id="reacg-migration-alert-close" aria-label="<?php esc_attr_e('Close dialog', 'regallery'); ?>">&times;</button>
195 <h2 id="reacg-migration-alert-title"><?php esc_html_e('Migration notice', 'regallery'); ?></h2>
196 <p id="reacg-migration-alert-message"></p>
197 <div class="reacg-migration-alert__actions">
198 <button type="button" class="button" id="reacg-migration-alert-cancel"><?php esc_html_e('Only migrate not migrated', 'regallery'); ?></button>
199 <button type="button" class="button button-primary" id="reacg-migration-alert-confirm"><?php esc_html_e('Migrate all selected', 'regallery'); ?></button>
200 </div>
201 </div>
202 </div>
203 </div>
204 <?php
205 }
206
207 public function ajax_sources() {
208 $this->authorize_request();
209
210 wp_send_json_success([
211 'sources' => $this->engine->get_sources(),
212 ]);
213 }
214
215 public function ajax_galleries() {
216 $this->authorize_request();
217
218 $source = !empty($_POST['source']) ? sanitize_key(wp_unslash($_POST['source'])) : '';
219 $page = !empty($_POST['page']) ? intval($_POST['page']) : 1;
220 $per_page = !empty($_POST['per_page']) ? intval($_POST['per_page']) : 50;
221 $search = !empty($_POST['search']) ? sanitize_text_field(wp_unslash($_POST['search'])) : '';
222
223 if (empty($source)) {
224 $result = $this->engine->list_all_galleries([
225 'search' => $search,
226 ]);
227 } else {
228 $result = $this->engine->list_galleries($source, [
229 'page' => $page,
230 'per_page' => $per_page,
231 'search' => $search,
232 ]);
233
234 if (!is_wp_error($result)) {
235 $items = !empty($result['items']) ? $result['items'] : [];
236 foreach ($items as &$item) {
237 $item['source'] = $source;
238 $item['source_label'] = $this->get_source_label($source);
239 }
240 $result['items'] = $items;
241 }
242 }
243
244 if (is_wp_error($result)) {
245 wp_send_json_error([
246 'message' => $result->get_error_message(),
247 ], 400);
248 }
249
250 wp_send_json_success($result);
251 }
252
253 public function ajax_import() {
254 $this->authorize_request();
255
256 $source = !empty($_POST['source']) ? sanitize_key(wp_unslash($_POST['source'])) : '';
257 $force_new = !empty($_POST['force_new']);
258
259 $gallery_ids = [];
260 if (!empty($_POST['gallery_ids'])) {
261 $raw = wp_unslash($_POST['gallery_ids']);
262 if (is_string($raw)) {
263 $decoded = json_decode($raw, true);
264 if (is_array($decoded)) {
265 $gallery_ids = $decoded;
266 } else {
267 $gallery_ids = array_filter(array_map('trim', explode(',', $raw)));
268 }
269 } elseif (is_array($raw)) {
270 $gallery_ids = $raw;
271 }
272 }
273
274 if (empty($gallery_ids)) {
275 wp_send_json_error([
276 'message' => __('Please provide gallery IDs to import.', 'regallery'),
277 ], 400);
278 }
279
280 $result = $this->engine->import_galleries($source, $gallery_ids, [
281 'force_new' => $force_new,
282 ]);
283
284 if (is_wp_error($result)) {
285 wp_send_json_error([
286 'message' => $result->get_error_message(),
287 ], 400);
288 }
289
290 wp_send_json_success($result);
291 }
292
293 public function ajax_replace_shortcode() {
294 $this->authorize_request();
295
296 $source = !empty($_POST['source']) ? sanitize_key(wp_unslash($_POST['source'])) : '';
297 $source_gallery_id = isset($_POST['source_gallery_id'])
298 ? sanitize_text_field(wp_unslash($_POST['source_gallery_id']))
299 : '';
300 $migrated_gallery_id = !empty($_POST['migrated_gallery_id'])
301 ? intval($_POST['migrated_gallery_id'])
302 : 0;
303
304 if ($source === '' || $source_gallery_id === '') {
305 wp_send_json_error([
306 'message' => __('Missing source or gallery IDs.', 'regallery'),
307 ], 400);
308 }
309
310 if ($migrated_gallery_id <= 0) {
311 $migrated_gallery_id = $this->find_migrated_gallery_id($source, $source_gallery_id);
312 }
313
314 if ($migrated_gallery_id <= 0) {
315 wp_send_json_error([
316 'message' => __('No migrated gallery was found for this source gallery.', 'regallery'),
317 ], 400);
318 }
319
320 $result = $this->replace_source_shortcodes($source, $source_gallery_id, $migrated_gallery_id);
321 if (is_wp_error($result)) {
322 wp_send_json_error([
323 'message' => $result->get_error_message(),
324 ], 400);
325 }
326
327 wp_send_json_success($result);
328 }
329
330 private function replace_source_shortcodes($source, $source_gallery_id, $migrated_gallery_id) {
331 $source = sanitize_key($source);
332 $source_gallery_id = trim((string) $source_gallery_id);
333
334 $provider = $this->engine->get_provider($source);
335 if (is_wp_error($provider)) {
336 return $provider;
337 }
338
339 $replacement_shortcode = '[REACG id="' . intval($migrated_gallery_id) . '"]';
340 $replacement_block = $this->build_reacg_gutenberg_block($migrated_gallery_id);
341
342 $specific_replacement = $provider->replace_specific_gallery(
343 $source_gallery_id,
344 $migrated_gallery_id,
345 $replacement_shortcode,
346 $replacement_block
347 );
348 if ($specific_replacement !== null) {
349 return $specific_replacement;
350 }
351
352 $shortcode_patterns = $provider->get_shortcode_patterns($source_gallery_id);
353 $block_namespaces = $provider->get_block_namespaces();
354 $block_id_attrs = $provider->get_block_id_attributes();
355
356 if (empty($shortcode_patterns) && empty($block_namespaces)) {
357 return new WP_Error('reacg_migration_replace_unsupported', __('Shortcode/block replacement is not supported for this source.', 'regallery'));
358 }
359
360 $post_types = $this->get_replaceable_post_types();
361 if (empty($post_types)) {
362 return [
363 'updated_posts' => 0,
364 'replaced_shortcodes' => 0,
365 ];
366 }
367
368 $query = new WP_Query([
369 'post_type' => array_values($post_types),
370 'post_status' => ['publish', 'draft', 'private', 'pending', 'future'],
371 'posts_per_page' => -1,
372 'fields' => 'ids',
373 'no_found_rows' => true,
374 'orderby' => 'ID',
375 'order' => 'ASC',
376 ]);
377
378 $updated_posts = 0;
379 $replaced_shortcodes = 0;
380
381 foreach ((array) $query->posts as $post_id) {
382 $post = get_post($post_id);
383 if (!$post) {
384 continue;
385 }
386
387 $updated_content = (string) $post->post_content;
388 $local_replacements = 0;
389
390 // Replace Gutenberg blocks from this source with reacg/gallery block
391 if ($updated_content !== '' && !empty($block_namespaces)) {
392 $block_replacements = 0;
393 $updated_content = $this->replace_source_blocks_in_content(
394 $updated_content,
395 $block_namespaces,
396 $block_id_attrs,
397 $source_gallery_id,
398 $replacement_block,
399 $block_replacements
400 );
401 $local_replacements += $block_replacements;
402 }
403
404 // Replace plain shortcodes (only when no block was found and replaced).
405 // Use preg_replace_callback so the replacement string is treated as a
406 // literal — avoiding backreference issues with preg_replace.
407 if ($updated_content !== '' && !empty($shortcode_patterns) && $local_replacements === 0) {
408 // Some providers should preserve shortcode-based integrations even in
409 // block-based content, while others can upgrade to the REACG block.
410 $shortcode_replacement = $provider->prefer_shortcode_replacement()
411 ? $replacement_shortcode
412 : ($this->post_is_block_based($updated_content)
413 ? $replacement_block
414 : $replacement_shortcode);
415
416 foreach ($shortcode_patterns as $pattern) {
417 $count = 0;
418 $updated_content = preg_replace_callback(
419 $pattern,
420 function () use ($shortcode_replacement, &$count) {
421 $count++;
422 return $shortcode_replacement;
423 },
424 $updated_content
425 );
426 if ($count > 0) {
427 $local_replacements += $count;
428 }
429 }
430 }
431
432 // If a source shortcode inside a wp:shortcode block was replaced by the
433 // REACG block, remove the now-redundant wp:shortcode wrapper.
434 if ($updated_content !== '' && $local_replacements > 0) {
435 $updated_content = preg_replace_callback(
436 '/<!--\s+wp:shortcode\s+-->\s*(<!--\s+wp:reacg\/gallery[\s\S]*?<!--\s+\/wp:reacg\/gallery\s+-->)\s*<!--\s+\/wp:shortcode\s+-->/i',
437 function ($m) { return $m[1]; },
438 $updated_content
439 );
440 }
441
442 $meta_replacements = $provider->replace_post_meta_references(
443 intval($post_id),
444 $source_gallery_id,
445 $migrated_gallery_id
446 );
447 $local_replacements += intval($meta_replacements);
448
449 if ($local_replacements <= 0 || ($updated_content === $post->post_content && intval($meta_replacements) === 0)) {
450 continue;
451 }
452
453 if ($updated_content !== $post->post_content) {
454 wp_update_post([
455 'ID' => intval($post_id),
456 // wp_update_post internally calls wp_unslash/stripslashes on content;
457 // wp_slash pre-escapes so the backslashes in \u0022 etc. survive.
458 'post_content' => wp_slash($updated_content),
459 ]);
460 }
461
462 $updated_posts += 1;
463 $replaced_shortcodes += $local_replacements;
464 }
465
466 if ($replaced_shortcodes > 0 && $migrated_gallery_id > 0) {
467 update_post_meta(intval($migrated_gallery_id), '_reacg_migration_source_has_placements', '1');
468 }
469
470 return [
471 'updated_posts' => $updated_posts,
472 'replaced_shortcodes' => $replaced_shortcodes,
473 'replacement_shortcode' => $replacement_shortcode,
474 ];
475 }
476
477 private function build_reacg_gutenberg_block($migrated_gallery_id) {
478 $gallery_id = intval($migrated_gallery_id);
479 $shortcode = '[REACG id="' . $gallery_id . '"]';
480 $attributes = [
481 'shortcode' => $shortcode,
482 'shortcode_id' => $gallery_id,
483 'hidePreview' => true,
484 ];
485
486 if (function_exists('serialize_block')) {
487 $serialized = serialize_block([
488 'blockName' => 'reacg/gallery',
489 'attrs' => $attributes,
490 'innerBlocks' => [],
491 'innerHTML' => $shortcode,
492 'innerContent' => [$shortcode],
493 ]);
494
495 if (is_string($serialized) && $serialized !== '') {
496 return $serialized;
497 }
498 }
499
500 $json_attributes = wp_json_encode($attributes);
501 if (!is_string($json_attributes) || $json_attributes === '') {
502 $json_attributes = '{}';
503 }
504
505 return '<!-- wp:reacg/gallery ' . $json_attributes . ' -->' . $shortcode . '<!-- /wp:reacg/gallery -->';
506 }
507
508
509 /**
510 * Returns true when the post content is Gutenberg-block-based, meaning it
511 * contains block comments beyond simple wp:shortcode wrappers.
512 * Used to decide whether a plain-shortcode replacement should stay as a
513 * shortcode or be upgraded to a block.
514 */
515 private function post_is_block_based($content) {
516 // Strip wp:shortcode wrappers — they appear in classic-editor posts too.
517 $stripped = preg_replace(
518 '/<!--\s+wp:shortcode\s+-->[\s\S]*?<!--\s+\/wp:shortcode\s+-->/i',
519 '',
520 (string) $content
521 );
522 return (bool) preg_match('/<!--\s+wp:[a-zA-Z]/', $stripped);
523 }
524
525 private function replace_source_blocks_in_content($content, $namespaces, $id_attrs, $source_gallery_id, $replacement_block, &$replacements) {
526 if (function_exists('parse_blocks') && function_exists('serialize_blocks')) {
527 $blocks = parse_blocks((string) $content);
528 if (!empty($blocks)) {
529 $replacement_parsed = parse_blocks((string) $replacement_block);
530 $replacement_node = null;
531 foreach ($replacement_parsed as $rp) {
532 if (!empty($rp['blockName'])) {
533 $replacement_node = $rp;
534 break;
535 }
536 }
537
538 if ($replacement_node) {
539 $changed = $this->replace_source_blocks_in_tree($blocks, $namespaces, $id_attrs, $source_gallery_id, $replacement_node, $replacements);
540 if ($changed) {
541 return serialize_blocks($blocks);
542 }
543 // Tree walker found nothing — fall through to the regex fallback below.
544 }
545 }
546 }
547
548 // Regex fallback: also runs when parse_blocks found no matching block.
549 // Matches any block comment in a known namespace that contains the gallery
550 // ID anywhere in its JSON attributes, including self-closing blocks.
551 $id_numeric = intval($source_gallery_id);
552 $id_escaped = preg_quote((string) $source_gallery_id, '/');
553 $id_value_pattern = ':\s*(?:' . $id_numeric . '(?=[\s,}])|"' . $id_escaped . '")';
554
555 foreach ($namespaces as $namespace) {
556 $ns = preg_quote($namespace, '/');
557 $pattern = '/<!--\s+wp:' . $ns . '\/[a-z0-9_-]+\s+\{[^}]*' . $id_value_pattern . '[^}]*\}[\s\S]*?(?:<!--\s+\/wp:' . $ns . '\/[a-z0-9_-]+\s+-->|\s*\/-->)/i';
558 $count = 0;
559 $content = preg_replace_callback(
560 $pattern,
561 function () use ($replacement_block, &$count) {
562 $count++;
563 return $replacement_block;
564 },
565 $content
566 );
567 if ($count > 0) {
568 $replacements += $count;
569 }
570 }
571
572 return $content;
573 }
574
575 private function replace_source_blocks_in_tree(&$blocks, $namespaces, $id_attrs, $source_gallery_id, $replacement_node, &$replacements) {
576 $changed = false;
577 $id_numeric = intval($source_gallery_id);
578 $id_string = (string) $source_gallery_id;
579
580 foreach ($blocks as &$block) {
581 $block_name = !empty($block['blockName']) ? (string) $block['blockName'] : '';
582
583 if ($block_name !== '') {
584 $slash_pos = strpos($block_name, '/');
585 $namespace = $slash_pos !== false ? substr($block_name, 0, $slash_pos) : $block_name;
586
587 if (in_array($namespace, $namespaces, true)) {
588 $attrs = isset($block['attrs']) && is_array($block['attrs']) ? $block['attrs'] : [];
589 $matched = false;
590
591 foreach ($id_attrs as $id_attr) {
592 if (!array_key_exists($id_attr, $attrs)) {
593 continue;
594 }
595 $val = $attrs[$id_attr];
596 if ($val === $id_numeric || $val === $id_string || (string) $val === $id_string) {
597 $matched = true;
598 break;
599 }
600 }
601
602 if (!$matched) {
603 // Fallback: check every scalar attribute value so we match even
604 // if the attribute key name is different from what we expect.
605 foreach ($attrs as $attr_val) {
606 if (!is_int($attr_val) && !is_string($attr_val)) {
607 continue;
608 }
609 if ($attr_val === $id_numeric || $attr_val === $id_string || (string) $attr_val === $id_string) {
610 $matched = true;
611 break;
612 }
613 }
614 }
615
616 if ($matched) {
617 $block = $replacement_node;
618 $replacements += 1;
619 $changed = true;
620 continue;
621 }
622 }
623 }
624
625 if (!empty($block['innerBlocks']) && is_array($block['innerBlocks'])) {
626 if ($this->replace_source_blocks_in_tree($block['innerBlocks'], $namespaces, $id_attrs, $source_gallery_id, $replacement_node, $replacements)) {
627 $changed = true;
628 }
629 }
630 }
631
632 return $changed;
633 }
634
635 private function find_migrated_gallery_id($source, $source_gallery_id) {
636 $query = new WP_Query([
637 'post_type' => REACG_CUSTOM_POST_TYPE,
638 'post_status' => ['publish', 'draft', 'private'],
639 'posts_per_page' => 1,
640 'fields' => 'ids',
641 'meta_query' => [
642 'relation' => 'AND',
643 [
644 'key' => '_reacg_migration_source',
645 'value' => sanitize_key((string) $source),
646 ],
647 [
648 'key' => '_reacg_migration_source_gallery_id',
649 'value' => sanitize_text_field((string) $source_gallery_id),
650 ],
651 ],
652 ]);
653
654 return !empty($query->posts[0]) ? intval($query->posts[0]) : 0;
655 }
656
657 private function get_replaceable_post_types() {
658 $post_types = get_post_types([], 'names');
659 if (empty($post_types)) {
660 return [];
661 }
662
663 $excluded = [
664 'attachment',
665 'revision',
666 'nav_menu_item',
667 'custom_css',
668 'customize_changeset',
669 'oembed_cache',
670 'user_request',
671 REACG_CUSTOM_POST_TYPE,
672 ];
673
674 return array_values(array_filter($post_types, function ($post_type) use ($excluded) {
675 if (!is_string($post_type) || $post_type === '' || in_array($post_type, $excluded, true)) {
676 return false;
677 }
678
679 $object = get_post_type_object($post_type);
680 if (!$object) {
681 return false;
682 }
683
684 return post_type_supports($post_type, 'editor') || in_array($post_type, ['wp_block', 'wp_template', 'wp_template_part', 'elementor_library'], true);
685 }));
686 }
687
688 private function authorize_request() {
689 if (!current_user_can('edit_posts')) {
690 wp_send_json_error([
691 'message' => __('Permission denied.', 'regallery'),
692 ], 403);
693 }
694
695 if (empty($_POST[REACG_NONCE])) {
696 wp_send_json_error([
697 'message' => __('Missing nonce.', 'regallery'),
698 ], 400);
699 }
700
701 $nonce = sanitize_text_field(wp_unslash($_POST[REACG_NONCE]));
702 if (!wp_verify_nonce($nonce, REACG_NONCE)) {
703 wp_send_json_error([
704 'message' => __('Invalid nonce.', 'regallery'),
705 ], 403);
706 }
707 }
708
709 private function get_source_label($source_key) {
710 foreach ($this->engine->get_sources() as $source) {
711 if (!empty($source['key']) && $source['key'] === $source_key) {
712 return !empty($source['label']) ? $source['label'] : $source_key;
713 }
714 }
715
716 return $source_key;
717 }
718 }
719