PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.10.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 1.0.0 All 52 releases
← All changes | includes/abilities/import/class-run-seo-import.php +103 -12 1.30.0 → 2.10.0 View file →
@@ -13,8 +13,9 @@
13 13 use ThinkRank\Admin\Importers\AIOSEO_Exporter;
14 14 use ThinkRank\Admin\Importers\Rankmath_Exporter;
15 15 use ThinkRank\Admin\Importers\SEOPress_Exporter;
16 16 use ThinkRank\Admin\Importers\Snapshot_Migrator;
17 +use ThinkRank\Admin\Importers\Squirrly_Exporter;
17 18 use ThinkRank\Admin\Importers\Yoast_Exporter;
18 19
19 20 if ( ! defined( 'ABSPATH' ) ) {
20 21 exit; // Exit if accessed directly.
@@ -27,8 +28,12 @@
27 28 * endpoints: it exports the chosen source (Yoast/RankMath/SEOPress/AIOSEO) into
28 29 * a snapshot, then migrates that snapshot into ThinkRank's `_thinkrank_*`
29 30 * metadata. Existing ThinkRank values are never overwritten. Source-plugin data
30 31 * is left intact (no cleanup); reports aggregate counters, not per-item rows.
32 + *
33 + * One step is not metadata: `content_blocks` rewrites Rank Math FAQ / HowTo
34 + * blocks inside `post_content` into ThinkRank blocks (#777). The description
35 + * says so, and `types` lets a caller leave it out.
31 36 */
32 37 class Run_Seo_Import extends Ability_Base {
33 38 /**
34 39 * Source plugins that can be imported.
@@ -34,9 +39,9 @@
34 39 * Source plugins that can be imported.
35 40 *
36 41 * @var string[]
37 42 */
38 - private const ALLOWED_PLUGINS = [ 'yoast', 'rankmath', 'seopress', 'aioseo' ];
43 + private const ALLOWED_PLUGINS = [ 'yoast', 'rankmath', 'seopress', 'aioseo', 'squirrly' ];
39 44
40 45 /**
41 46 * Data types processed, in pipeline order.
42 47 *
@@ -41,9 +46,9 @@
41 46 * Data types processed, in pipeline order.
42 47 *
43 48 * @var string[]
44 49 */
45 - private const TYPES = [ 'postmeta', 'termmeta', 'usermeta', 'redirections', 'settings' ];
50 + private const TYPES = [ 'postmeta', 'termmeta', 'usermeta', 'redirections', 'content_blocks', 'settings' ];
46 51
47 52 /**
48 53 * Safety cap on chunk iterations per type (avoids runaway loops).
49 54 */
@@ -54,9 +59,9 @@
54 59 */
55 60 public function __construct() {
56 61 $this->id = 'thinkrank/run-seo-import';
57 62 $this->label = __( 'Run SEO Data Import', 'thinkrank' );
58 - $this->description = __( 'Import SEO metadata from another plugin (Yoast, RankMath, SEOPress, or AIOSEO) into ThinkRank. Exports the source to a snapshot then migrates it; existing ThinkRank values are never overwritten and source data is left intact. Returns aggregate export/migration counters.', 'thinkrank' );
63 + $this->description = __( 'Import SEO data from another plugin (Yoast, RankMath, SEOPress, AIOSEO, or Squirrly) into ThinkRank. Exports the source to a snapshot then migrates it; existing ThinkRank values are never overwritten and the source plugin\'s own meta and settings are left intact. The content_blocks type (RankMath only) edits post content: it rewrites RankMath FAQ and HowTo blocks into ThinkRank blocks. Each converted post gets a revision, or a restorable backup where revisions are disabled. Pass types to limit the run, for example to leave content_blocks out. Posts with malformed block markup are left unchanged and listed in errors. Returns aggregate export/migration counters. Run preview-seo-import first to see what would change; get-import-status reports on the snapshot afterwards.', 'thinkrank' );
59 64 }
60 65
61 66 /**
62 67 * {@inheritDoc}
@@ -65,8 +70,16 @@
65 70 */
66 71 public function get_annotations() {
67 72 return [
68 73 'readonly' => false,
74 + // Still false, deliberately, although content_blocks edits
75 + // post_content. `destructive` is for losses that cannot be undone
76 + // (#675), and this one can: every converted post gets a revision,
77 + // and where revisions are off Block_Converter keeps the original
78 + // in post meta for POST /import/content-blocks/restore. A post
79 + // whose markup cannot be converted safely is not written at all.
80 + // The description spells the content edit out instead, so the
81 + // caller can decide, and `types` can leave the step out.
69 82 'destructive' => false,
70 83 'idempotent' => false,
71 84 'priority' => 2.0,
72 85 'openWorldHint' => false,
@@ -87,8 +100,18 @@
87 100 'type' => 'string',
88 101 'description' => __( 'The source SEO plugin to import from.', 'thinkrank' ),
89 102 'enum' => self::ALLOWED_PLUGINS,
90 103 ],
104 + 'types' => [
105 + 'type' => 'array',
106 + 'description' => __( 'Data types to import. Defaults to all of them. Omit content_blocks to leave post content untouched.', 'thinkrank' ),
107 + 'items' => [
108 + 'type' => 'string',
109 + 'enum' => self::TYPES,
110 + ],
111 + 'minItems' => 1,
112 + 'uniqueItems' => true,
113 + ],
91 114 ],
92 115 'required' => [ 'plugin' ],
93 116 ];
94 117 }
@@ -107,8 +130,12 @@
107 130 'exported' => [
108 131 'type' => 'object',
109 132 'additionalProperties' => true,
110 133 ],
134 + 'types' => [
135 + 'type' => 'array',
136 + 'items' => [ 'type' => 'string' ],
137 + ],
111 138 'migrated' => [
112 139 'type' => 'object',
113 140 'additionalProperties' => true,
114 141 ],
@@ -131,13 +158,19 @@
131 158
132 159 if ( ! in_array( $plugin, self::ALLOWED_PLUGINS, true ) ) {
133 160 return new \WP_Error(
134 161 'thinkrank_invalid_import_plugin',
135 - __( 'A supported source plugin is required (yoast, rankmath, seopress, aioseo).', 'thinkrank' ),
162 + __( 'A supported source plugin is required (yoast, rankmath, seopress, aioseo, squirrly).', 'thinkrank' ),
136 163 [ 'status' => 400 ]
137 164 );
138 165 }
139 166
167 + $types = $this->resolve_types( $input['types'] ?? null );
168 +
169 + if ( is_wp_error( $types ) ) {
170 + return $types;
171 + }
172 +
140 173 $exporter = $this->get_exporter( $plugin );
141 174
142 175 if ( null === $exporter ) {
143 176 return new \WP_Error(
@@ -147,10 +180,10 @@
147 180 );
148 181 }
149 182
150 183 try {
151 - $exported = $this->run_export( $exporter );
152 - $migrated = $this->run_migration( $plugin );
184 + $exported = $this->run_export( $exporter, $types );
185 + $migrated = $this->run_migration( $plugin, $types );
153 186 } catch ( \Throwable $e ) {
154 187 return new \WP_Error(
155 188 'thinkrank_import_failed',
156 189 $e->getMessage(),
@@ -160,12 +193,14 @@
160 193
161 194 return [
162 195 'success' => empty( $migrated['errors'] ),
163 196 'plugin' => $plugin,
197 + 'types' => $types,
164 198 'exported' => $exported,
165 199 'migrated' => [
166 200 'processed' => $migrated['processed'],
167 201 'skipped' => $migrated['skipped'],
202 + 'failed' => $migrated['failed'],
168 203 'analyzed' => $migrated['analyzed'],
169 204 'keywords_seeded' => $migrated['keywords_seeded'],
170 205 ],
171 206 'errors' => $migrated['errors'],
@@ -172,8 +207,50 @@
172 207 ];
173 208 }
174 209
175 210 /**
211 + * The types to run, in pipeline order.
212 + *
213 + * Absent means every type, which is what the ability did before `types`
214 + * existed. Order always follows TYPES rather than the caller's list, since
215 + * the migrator relies on postmeta running before settings.
216 + *
217 + * @param mixed $requested The `types` input, if any.
218 + * @return string[]|\WP_Error
219 + */
220 + private function resolve_types( $requested ) {
221 + if ( null === $requested ) {
222 + return self::TYPES;
223 + }
224 +
225 + if ( ! is_array( $requested ) || empty( $requested ) ) {
226 + return new \WP_Error(
227 + 'thinkrank_invalid_import_types',
228 + __( 'types must be a non-empty list of data types.', 'thinkrank' ),
229 + [ 'status' => 400 ]
230 + );
231 + }
232 +
233 + $requested = array_map( 'sanitize_key', array_map( 'strval', $requested ) );
234 + $unknown = array_diff( $requested, self::TYPES );
235 +
236 + if ( ! empty( $unknown ) ) {
237 + return new \WP_Error(
238 + 'thinkrank_invalid_import_types',
239 + sprintf(
240 + /* translators: 1: unknown type slugs, 2: allowed type slugs. */
241 + __( 'Unknown import types: %1$s. Allowed: %2$s.', 'thinkrank' ),
242 + implode( ', ', $unknown ),
243 + implode( ', ', self::TYPES )
244 + ),
245 + [ 'status' => 400 ]
246 + );
247 + }
248 +
249 + return array_values( array_intersect( self::TYPES, $requested ) );
250 + }
251 +
252 + /**
176 253 * Resolve the exporter for a source plugin.
177 254 *
178 255 * @param string $plugin Source plugin slug.
179 256 * @return object|null Exporter instance or null when unknown.
@@ -187,8 +264,10 @@
187 264 case 'seopress':
188 265 return new SEOPress_Exporter();
189 266 case 'aioseo':
190 267 return new AIOSEO_Exporter();
268 + case 'squirrly':
269 + return new Squirrly_Exporter();
191 270 default:
192 271 return null;
193 272 }
194 273 }
@@ -195,15 +274,16 @@
195 274
196 275 /**
197 276 * Export every data type into a snapshot and finalize it.
198 277 *
199 - * @param object $exporter Source-plugin exporter.
278 + * @param object $exporter Source-plugin exporter.
279 + * @param string[] $types Types to export, in pipeline order.
200 280 * @return array<string, int> Per-type exported counts.
201 281 */
202 - private function run_export( $exporter ) {
282 + private function run_export( $exporter, array $types ) {
203 283 $exported = [];
204 284
205 - foreach ( self::TYPES as $type ) {
285 + foreach ( $types as $type ) {
206 286 $count = 0;
207 287 $page = 1;
208 288
209 289 do {
@@ -224,22 +304,24 @@
224 304
225 305 /**
226 306 * Migrate the snapshot into ThinkRank metadata.
227 307 *
228 - * @param string $plugin Source plugin slug.
308 + * @param string $plugin Source plugin slug.
309 + * @param string[] $types Types to migrate, in pipeline order.
229 310 * @return array<string, mixed> Aggregate counters and any errors.
230 311 */
231 - private function run_migration( $plugin ) {
312 + private function run_migration( $plugin, array $types ) {
232 313 $migrator = new Snapshot_Migrator();
233 314 $totals = [
234 315 'processed' => 0,
235 316 'skipped' => 0,
317 + 'failed' => 0,
236 318 'analyzed' => 0,
237 319 'keywords_seeded' => 0,
238 320 'errors' => [],
239 321 ];
240 322
241 - foreach ( self::TYPES as $type ) {
323 + foreach ( $types as $type ) {
242 324 $page = 1;
243 325
244 326 do {
245 327 $result = $migrator->migrate_chunk( $plugin, $type, $page );
@@ -256,8 +338,17 @@
256 338 $totals['processed'] += (int) ( $result['processed'] ?? 0 );
257 339 $totals['skipped'] += (int) ( $result['skipped'] ?? 0 );
258 340 $totals['analyzed'] += (int) ( $result['analyzed'] ?? 0 );
259 341 $totals['keywords_seeded'] += (int) ( $result['keywords_seeded'] ?? 0 );
342 + $totals['failed'] += (int) ( $result['failed'] ?? 0 );
343 +
344 + // Per-post failures (content_blocks: a post whose block markup
345 + // could not be converted safely and was left as it was). The
346 + // chunk itself succeeded, so they are not a chunk error, but
347 + // the caller needs the post ids to go and fix them.
348 + foreach ( (array) ( $result['failures'] ?? [] ) as $failure ) {
349 + $totals['errors'][] = (string) ( $failure['message'] ?? '' );
350 + }
260 351
261 352 $has_more = ! empty( $result['has_more'] );
262 353 ++$page;
263 354 } while ( $has_more && $page <= self::MAX_PAGES );