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