← 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 | /** |