PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.13.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.13.0
2.13.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 All 54 releases
thinkrank / includes / abilities / import / class-preview-seo-import.php

class-preview-seo-import.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.13.0, at includes/abilities/import/class-preview-seo-import.php

294 lines 7.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Preview SEO import (dry-run) ability.
4 *
5 * @package ThinkRank\Abilities\Import
6 */
7
8 declare(strict_types=1);
9
10 namespace ThinkRank\Abilities\Import;
11
12 use ThinkRank\Abilities\Ability_Base;
13 use ThinkRank\Admin\Importers\AIOSEO_Exporter;
14 use ThinkRank\Admin\Importers\Rankmath_Exporter;
15 use ThinkRank\Admin\Importers\SEOPress_Exporter;
16 use ThinkRank\Admin\Importers\Slim_SEO_Exporter;
17 use ThinkRank\Admin\Importers\Snapshot_Migrator;
18 use ThinkRank\Admin\Importers\Squirrly_Exporter;
19 use ThinkRank\Admin\Importers\Yoast_Exporter;
20
21 if ( ! defined( 'ABSPATH' ) ) {
22 exit; // Exit if accessed directly.
23 }
24
25 /**
26 * Dry-run an SEO import and report what it WOULD do, writing nothing to
27 * ThinkRank (see #190).
28 *
29 * It exports the chosen source (Yoast/RankMath/SEOPress/AIOSEO) into a snapshot
30 * — the same non-destructive read phase the real import uses — then classifies
31 * every object-meta record without migrating: how many would be written, how
32 * many conflict with an existing ThinkRank value that a migrate would not
33 * overwrite, and how many reference posts/terms/users that no longer exist.
34 * Run this before `run-seo-import` to see the impact up front.
35 */
36 class Preview_Seo_Import extends Ability_Base {
37 /**
38 * Source plugins that can be previewed.
39 *
40 * @var string[]
41 */
42 private const ALLOWED_PLUGINS = [ 'yoast', 'rankmath', 'seopress', 'aioseo', 'squirrly', 'slimseo' ];
43
44 /**
45 * Data types exported before previewing, in pipeline order.
46 *
47 * @var string[]
48 */
49 private const EXPORT_TYPES = [ 'postmeta', 'termmeta', 'usermeta', 'redirections', 'settings' ];
50
51 /**
52 * Object-meta types the dry-run classifies (settings/redirections/404 logs
53 * are migrated wholesale and aren't previewed per-record).
54 *
55 * @var string[]
56 */
57 private const PREVIEW_TYPES = [ 'postmeta', 'termmeta', 'usermeta' ];
58
59 /**
60 * Safety cap on chunk iterations per type.
61 */
62 private const MAX_PAGES = 1000;
63
64 /**
65 * Constructor.
66 */
67 public function __construct() {
68 $this->id = 'thinkrank/preview-seo-import';
69 $this->label = __( 'Preview SEO Import (Dry Run)', 'thinkrank' );
70 $this->description = __( 'Dry-run an SEO import from another plugin (Yoast, RankMath, SEOPress, AIOSEO, Squirrly, or Slim SEO) and report what it would do without writing anything to ThinkRank: counts of records that would be written, records that conflict with existing ThinkRank values (which are never overwritten), and records whose post/term/user no longer exists. Returns totals plus small samples. Run before run-seo-import.', 'thinkrank' );
71 }
72
73 /**
74 * {@inheritDoc}
75 *
76 * @return array<string, bool|float|string>
77 */
78 public function get_annotations() {
79 return [
80 'readonly' => false,
81 'destructive' => false,
82 'idempotent' => true,
83 'priority' => 1.5,
84 'openWorldHint' => false,
85 ];
86 }
87
88 /**
89 * {@inheritDoc}
90 *
91 * @return array<string, mixed>
92 */
93 public function get_input_schema() {
94 return [
95 'type' => 'object',
96 'additionalProperties' => false,
97 'properties' => [
98 'plugin' => [
99 'type' => 'string',
100 'description' => __( 'The source SEO plugin to preview an import from.', 'thinkrank' ),
101 'enum' => self::ALLOWED_PLUGINS,
102 ],
103 ],
104 'required' => [ 'plugin' ],
105 ];
106 }
107
108 /**
109 * {@inheritDoc}
110 *
111 * @return array<string, mixed>
112 */
113 public function get_output_schema() {
114 return [
115 'type' => 'object',
116 'properties' => [
117 'plugin' => [ 'type' => 'string' ],
118 'records' => [ 'type' => 'integer' ],
119 'would_write' => [ 'type' => 'integer' ],
120 'conflicts' => [ 'type' => 'integer' ],
121 'unmatched' => [ 'type' => 'integer' ],
122 'skipped' => [ 'type' => 'integer' ],
123 'by_type' => [
124 'type' => 'object',
125 'additionalProperties' => true,
126 ],
127 'samples' => [
128 'type' => 'object',
129 'additionalProperties' => true,
130 ],
131 'errors' => [
132 'type' => 'array',
133 'items' => [ 'type' => 'string' ],
134 ],
135 ],
136 ];
137 }
138
139 /**
140 * Execute ability.
141 *
142 * @param array<string, mixed> $input Ability input payload.
143 * @return array<string, mixed>|\WP_Error
144 */
145 public function execute( $input ) {
146 $plugin = isset( $input['plugin'] ) ? sanitize_key( (string) $input['plugin'] ) : '';
147
148 if ( ! in_array( $plugin, self::ALLOWED_PLUGINS, true ) ) {
149 return new \WP_Error(
150 'thinkrank_invalid_import_plugin',
151 __( 'A supported source plugin is required (yoast, rankmath, seopress, aioseo, squirrly, slimseo).', 'thinkrank' ),
152 [ 'status' => 400 ]
153 );
154 }
155
156 $exporter = $this->get_exporter( $plugin );
157 if ( null === $exporter ) {
158 return new \WP_Error(
159 'thinkrank_invalid_import_plugin',
160 __( 'Could not resolve an exporter for the selected plugin.', 'thinkrank' ),
161 [ 'status' => 400 ]
162 );
163 }
164
165 try {
166 $this->run_export( $exporter );
167 } catch ( \Throwable $e ) {
168 return new \WP_Error(
169 'thinkrank_import_preview_failed',
170 $e->getMessage(),
171 [ 'status' => 500 ]
172 );
173 }
174
175 return $this->run_preview( $plugin );
176 }
177
178 /**
179 * Export every data type into a fresh snapshot and finalize it (the
180 * non-destructive read phase). Writes nothing to ThinkRank.
181 *
182 * @param object $exporter Source-plugin exporter.
183 * @return void
184 */
185 private function run_export( $exporter ) {
186 foreach ( self::EXPORT_TYPES as $type ) {
187 $page = 1;
188 do {
189 $result = $exporter->export_chunk( $type, $page );
190 $has_more = is_array( $result ) && ! empty( $result['has_more'] );
191 ++$page;
192 } while ( $has_more && $page <= self::MAX_PAGES );
193 }
194
195 $exporter->finalize_export();
196 }
197
198 /**
199 * Dry-run the snapshot and aggregate the per-type preview counts.
200 *
201 * @param string $plugin Source plugin slug.
202 * @return array<string, mixed>
203 */
204 private function run_preview( $plugin ) {
205 $migrator = new Snapshot_Migrator();
206
207 $totals = [
208 'records' => 0,
209 'would_write' => 0,
210 'conflicts' => 0,
211 'unmatched' => 0,
212 'skipped' => 0,
213 ];
214 $by_type = [];
215 $samples = [ 'would_write' => [], 'conflicts' => [], 'unmatched' => [] ];
216 $errors = [];
217
218 foreach ( self::PREVIEW_TYPES as $type ) {
219 $type_totals = [
220 'records' => 0,
221 'would_write' => 0,
222 'conflicts' => 0,
223 'unmatched' => 0,
224 'skipped' => 0,
225 ];
226
227 $page = 1;
228 do {
229 $preview = $migrator->preview_chunk( $plugin, $type, $page );
230
231 if ( ! empty( $preview['error'] ) ) {
232 $errors[] = (string) $preview['error'];
233 break;
234 }
235
236 foreach ( array_keys( $type_totals ) as $key ) {
237 $type_totals[ $key ] += (int) ( $preview[ $key ] ?? 0 );
238 }
239
240 foreach ( [ 'would_write', 'conflicts', 'unmatched' ] as $bucket ) {
241 foreach ( (array) ( $preview['samples'][ $bucket ] ?? [] ) as $sample ) {
242 if ( count( $samples[ $bucket ] ) < 10 ) {
243 $samples[ $bucket ][] = array_merge( [ 'type' => $type ], $sample );
244 }
245 }
246 }
247
248 $has_more = ! empty( $preview['has_more'] );
249 ++$page;
250 } while ( $has_more && $page <= self::MAX_PAGES );
251
252 $by_type[ $type ] = $type_totals;
253 foreach ( array_keys( $totals ) as $key ) {
254 $totals[ $key ] += $type_totals[ $key ];
255 }
256 }
257
258 return array_merge(
259 [ 'plugin' => $plugin ],
260 $totals,
261 [
262 'by_type' => $by_type,
263 'samples' => $samples,
264 'errors' => $errors,
265 ]
266 );
267 }
268
269 /**
270 * Resolve the exporter for a source plugin.
271 *
272 * @param string $plugin Source plugin slug.
273 * @return object|null Exporter instance or null when unknown.
274 */
275 private function get_exporter( $plugin ) {
276 switch ( $plugin ) {
277 case 'yoast':
278 return new Yoast_Exporter();
279 case 'rankmath':
280 return new Rankmath_Exporter();
281 case 'seopress':
282 return new SEOPress_Exporter();
283 case 'aioseo':
284 return new AIOSEO_Exporter();
285 case 'squirrly':
286 return new Squirrly_Exporter();
287 case 'slimseo':
288 return new Slim_SEO_Exporter();
289 default:
290 return null;
291 }
292 }
293 }
294