PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.9.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.9.0
2.12.0 2.11.0 2.10.0 2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk All 53 releases
← All changes | includes/admin/importers/class-snapshot-migrator.php +12 -147 trunk → 2.9.0 View file →
@@ -54,15 +54,8 @@
54 54 'twitter_description' => '_thinkrank_twitter_description',
55 55 'twitter_image' => '_thinkrank_twitter_image',
56 56 'primary_category' => '_thinkrank_primary_category',
57 57 'schema_type' => '_thinkrank_selected_schema_type',
58 - // WooCommerce product identifier (GTIN/MPN/ISBN). Free does not read
59 - // it; ThinkRank Pro's Product_Fields does, under this exact key, so
60 - // importing it here means the identifier is already in place when Pro
61 - // is activated. Without it Google reports "missing identifier" on every
62 - // product after a switch, which is a rich-result warning the user did
63 - // not have before they migrated (#715).
64 - 'product_identifier' => '_thinkrank_product_gtin',
65 58 ];
66 59
67 60 /**
68 61 * Canonical robots meta fields. Composed into JSON-encoded
@@ -79,17 +72,9 @@
79 72
80 73 /**
81 74 * Data types that are migratable (have post/term/user meta mappings)
82 75 */
83 - private const MIGRATABLE_TYPES = [
84 - 'postmeta',
85 - 'termmeta',
86 - 'usermeta',
87 - 'redirections',
88 - '404_logs',
89 - 'settings',
90 - Block_Converter::TYPE,
91 - ];
76 + private const MIGRATABLE_TYPES = ['postmeta', 'termmeta', 'usermeta', 'redirections', '404_logs', 'settings'];
92 77
93 78 /**
94 79 * Settings-record `extended` keys that either migrate today or are safe to
95 80 * discard on cleanup (raw_options is pure capture-all insurance; a fresh
@@ -174,12 +159,8 @@
174 159 if ($type === '404_logs') {
175 160 return $this->migrate_404_logs($plugin, $page);
176 161 }
177 162
178 - if ($type === Block_Converter::TYPE) {
179 - return $this->migrate_content_blocks($plugin, $page);
180 - }
181 -
182 163 $chunk = Snapshot_Store::read_chunk($plugin, $type, $page);
183 164 if ($chunk === null || empty($chunk)) {
184 165 // An empty chunk is not the end of the type. An exporter that pages
185 166 // one shared table and then splits the rows by kind writes nothing
@@ -305,12 +286,8 @@
305 286 if ($value === 0 && in_array($canonical_key, ['primary_category'], true)) {
306 287 continue;
307 288 }
308 289
309 - // The meta writers unslash their value, so every write below
310 - // slashes first. Unslashed, a title or description with a
311 - // backslash in it lost it on the way in, and a JSON value lost
312 - // the backslash of every `\"` and `\uXXXX` escape.
313 290 if ($object_type === 'post') {
314 291 // Never overwrite existing ThinkRank data
315 292 $existing = get_post_meta($object_id, $thinkrank_key, true);
316 293 if ($existing !== '' && $existing !== false && $existing !== null) {
@@ -316,9 +293,9 @@
316 293 if ($existing !== '' && $existing !== false && $existing !== null) {
317 294 continue;
318 295 }
319 296
320 - update_post_meta($object_id, $thinkrank_key, wp_slash($value));
297 + update_post_meta($object_id, $thinkrank_key, $value);
321 298 $record_had_writes = true;
322 299 } elseif ($object_type === 'term') {
323 300 $existing = get_term_meta($object_id, $thinkrank_key, true);
324 301 if ($existing !== '' && $existing !== false && $existing !== null) {
@@ -324,9 +301,9 @@
324 301 if ($existing !== '' && $existing !== false && $existing !== null) {
325 302 continue;
326 303 }
327 304
328 - update_term_meta($object_id, $thinkrank_key, wp_slash($value));
305 + update_term_meta($object_id, $thinkrank_key, $value);
329 306 $record_had_writes = true;
330 307 } elseif ($object_type === 'user') {
331 308 $existing = get_user_meta($object_id, $thinkrank_key, true);
332 309 if ($existing !== '' && $existing !== false && $existing !== null) {
@@ -332,9 +309,9 @@
332 309 if ($existing !== '' && $existing !== false && $existing !== null) {
333 310 continue;
334 311 }
335 312
336 - update_user_meta($object_id, $thinkrank_key, wp_slash($value));
313 + update_user_meta($object_id, $thinkrank_key, $value);
337 314 $record_had_writes = true;
338 315 }
339 316 }
340 317
@@ -652,22 +629,18 @@
652 629 // strings; but the restore's whole policy is to write the file's value
653 630 // verbatim, and a file holding one as an array would otherwise raise a
654 631 // TypeError that takes down the rest of the chunk with it. One bad key
655 632 // is worth skipping, not the records behind it.
656 - //
657 - // wp_slash() because the meta writers unslash: a restored JSON value
658 - // (schema form data, robots) would otherwise lose the backslash of
659 - // every escaped quote and come back as invalid JSON.
660 633 try {
661 634 switch ($object_type) {
662 635 case 'post':
663 - update_post_meta($object_id, $key, wp_slash($value));
636 + update_post_meta($object_id, $key, $value);
664 637 return true;
665 638 case 'term':
666 - update_term_meta($object_id, $key, wp_slash($value));
639 + update_term_meta($object_id, $key, $value);
667 640 return true;
668 641 case 'user':
669 - update_user_meta($object_id, $key, wp_slash($value));
642 + update_user_meta($object_id, $key, $value);
670 643 return true;
671 644 }
672 645 } catch (\Throwable $e) {
673 646 if (defined('WP_DEBUG') && WP_DEBUG) {
@@ -1441,9 +1414,9 @@
1441 1414 }
1442 1415
1443 1416 $robots['index'] = empty($robots['noindex']);
1444 1417
1445 - update_term_meta($term_id, '_thinkrank_robots_meta', wp_slash(wp_json_encode($robots)));
1418 + update_term_meta($term_id, '_thinkrank_robots_meta', wp_json_encode($robots));
1446 1419 update_term_meta($term_id, '_thinkrank_robots_meta_enabled', 1);
1447 1420
1448 1421 return true;
1449 1422 }
@@ -1602,9 +1575,9 @@
1602 1575 if (is_string($existing) && $existing !== '') {
1603 1576 return false;
1604 1577 }
1605 1578
1606 - update_post_meta($post_id, '_thinkrank_schema_form_data', wp_slash(wp_json_encode($review)));
1579 + update_post_meta($post_id, '_thinkrank_schema_form_data', wp_json_encode($review));
1607 1580
1608 1581 return true;
1609 1582 }
1610 1583
@@ -1636,9 +1609,9 @@
1636 1609 if (is_string($existing) && $existing !== '') {
1637 1610 return false;
1638 1611 }
1639 1612
1640 - update_post_meta($post_id, '_thinkrank_schema_form_data', wp_slash(wp_json_encode($video)));
1613 + update_post_meta($post_id, '_thinkrank_schema_form_data', wp_json_encode($video));
1641 1614
1642 1615 return true;
1643 1616 }
1644 1617
@@ -1718,13 +1691,13 @@
1718 1691 }
1719 1692
1720 1693 $wrote = false;
1721 1694 if (!empty($robots)) {
1722 - update_post_meta($post_id, '_thinkrank_robots_meta', wp_slash(wp_json_encode($robots)));
1695 + update_post_meta($post_id, '_thinkrank_robots_meta', wp_json_encode($robots));
1723 1696 $wrote = true;
1724 1697 }
1725 1698 if (!empty($advanced)) {
1726 - update_post_meta($post_id, '_thinkrank_advanced_robots_meta', wp_slash(wp_json_encode($advanced)));
1699 + update_post_meta($post_id, '_thinkrank_advanced_robots_meta', wp_json_encode($advanced));
1727 1700 $wrote = true;
1728 1701 }
1729 1702 if ($wrote) {
1730 1703 update_post_meta($post_id, '_thinkrank_robots_meta_enabled', 1);
@@ -2553,116 +2526,8 @@
2553 2526 'page' => $page,
2554 2527 'total_chunks' => $this->chunk_total($plugin, '404_logs'),
2555 2528 'processed' => $processed,
2556 2529 'skipped' => $skipped,
2557 - ];
2558 - }
2559 -
2560 - /**
2561 - * Rewrite a chunk of posts' Rank Math FAQ / HowTo blocks into ThinkRank's
2562 - * own blocks.
2563 - *
2564 - * Unlike every other type here this does not write meta — it edits
2565 - * `post_content` in place, because that is where the blocks live. The
2566 - * snapshot chunk carries post ids only, so the conversion always runs
2567 - * against the post as it stands now rather than a stale copy.
2568 - *
2569 - * The conflict strategy is deliberately ignored. A Rank Math block and a
2570 - * ThinkRank block are not two values competing for one field: the Rank Math
2571 - * one is broken markup that needs replacing, and any ThinkRank block
2572 - * already in the post is simply left alone by the converter.
2573 - *
2574 - * @param string $plugin Plugin slug
2575 - * @param int $page Chunk number
2576 - * @return array Migration result
2577 - */
2578 - private function migrate_content_blocks(string $plugin, int $page): array {
2579 - $chunk = Snapshot_Store::read_chunk($plugin, Block_Converter::TYPE, $page);
2580 - $total_chunks = $this->chunk_total($plugin, Block_Converter::TYPE);
2581 -
2582 - if ($chunk === null || empty($chunk)) {
2583 - $has_more = $page < $total_chunks;
2584 -
2585 - return [
2586 - 'status' => $has_more ? 'processing' : 'complete',
2587 - 'message' => 'No content blocks in chunk',
2588 - 'has_more' => $has_more,
2589 - 'page' => $page,
2590 - 'processed' => 0,
2591 - 'skipped' => 0,
2592 - 'failed' => 0,
2593 - 'failures' => [],
2594 - ];
2595 - }
2596 -
2597 - // Rewriting a few hundred posts is well past the default execution
2598 - // window on shared hosting, and a timeout mid-chunk would leave the
2599 - // migration looking stalled.
2600 - if (function_exists('set_time_limit')) {
2601 - @set_time_limit(300); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
2602 - }
2603 -
2604 - $processed = 0;
2605 - $skipped = 0;
2606 - $failed = 0;
2607 - $failures = [];
2608 - $blocks = 0;
2609 -
2610 - foreach ($chunk as $record) {
2611 - $post_id = (int) ($record['object_id'] ?? ($record['data']['post_id'] ?? 0));
2612 - if ($post_id < 1) {
2613 - $skipped++;
2614 - continue;
2615 - }
2616 -
2617 - $result = Block_Converter::convert_post($post_id);
2618 -
2619 - if ('converted' === $result['status']) {
2620 - $processed++;
2621 - $blocks += $result['converted'];
2622 - continue;
2623 - }
2624 -
2625 - // A post the converter refused (broken block markup, a PCRE
2626 - // failure) was left untouched and still holds its Rank Math
2627 - // blocks. Folding it into `skipped` made it indistinguishable from
2628 - // a post that was simply already converted, so it is counted and
2629 - // named on its own.
2630 - if ('error' === $result['status']) {
2631 - $failed++;
2632 - $failures[] = ['post_id' => $post_id, 'message' => $result['message']];
2633 - continue;
2634 - }
2635 -
2636 - // `unchanged` is the normal outcome of a re-run, not a failure.
2637 - $skipped++;
2638 - }
2639 -
2640 - $has_more = $page < $total_chunks;
2641 -
2642 - if (!$has_more) {
2643 - // The detector caches its scan for an hour; without clearing it the
2644 - // Migration screen keeps offering blocks that are no longer there.
2645 - (new Import_Detector())->clear_cache();
2646 - }
2647 -
2648 - return [
2649 - 'status' => $has_more ? 'processing' : 'complete',
2650 - 'message' => sprintf(
2651 - 'Converted %d FAQ/HowTo blocks in %d posts, skipped %d, failed %d (page %d)',
2652 - $blocks,
2653 - $processed,
2654 - $skipped,
2655 - $failed,
2656 - $page
2657 - ),
2658 - 'has_more' => $has_more,
2659 - 'page' => $page,
2660 - 'total_chunks' => $total_chunks,
2661 - 'processed' => $processed,
2662 - 'skipped' => $skipped,
2663 - 'failed' => $failed,
2664 - 'failures' => $failures,
2665 2530 ];
2666 2531 }
2667 2532
2668 2533 /**