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 +106 -12 1.28.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' ];
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.', '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).', '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.
@@ -187,8 +265,12 @@
187 265 case 'seopress':
188 266 return new SEOPress_Exporter();
189 267 case 'aioseo':
190 268 return new AIOSEO_Exporter();
269 + case 'squirrly':
270 + return new Squirrly_Exporter();
271 + case 'slimseo':
272 + return new Slim_SEO_Exporter();
191 273 default:
192 274 return null;
193 275 }
194 276 }
@@ -195,15 +277,16 @@
195 277
196 278 /**
197 279 * Export every data type into a snapshot and finalize it.
198 280 *
199 - * @param object $exporter Source-plugin exporter.
281 + * @param object $exporter Source-plugin exporter.
282 + * @param string[] $types Types to export, in pipeline order.
200 283 * @return array<string, int> Per-type exported counts.
201 284 */
202 - private function run_export( $exporter ) {
285 + private function run_export( $exporter, array $types ) {
203 286 $exported = [];
204 287
205 - foreach ( self::TYPES as $type ) {
288 + foreach ( $types as $type ) {
206 289 $count = 0;
207 290 $page = 1;
208 291
209 292 do {
@@ -224,22 +307,24 @@
224 307
225 308 /**
226 309 * Migrate the snapshot into ThinkRank metadata.
227 310 *
228 - * @param string $plugin Source plugin slug.
311 + * @param string $plugin Source plugin slug.
312 + * @param string[] $types Types to migrate, in pipeline order.
229 313 * @return array<string, mixed> Aggregate counters and any errors.
230 314 */
231 - private function run_migration( $plugin ) {
315 + private function run_migration( $plugin, array $types ) {
232 316 $migrator = new Snapshot_Migrator();
233 317 $totals = [
234 318 'processed' => 0,
235 319 'skipped' => 0,
320 + 'failed' => 0,
236 321 'analyzed' => 0,
237 322 'keywords_seeded' => 0,
238 323 'errors' => [],
239 324 ];
240 325
241 - foreach ( self::TYPES as $type ) {
326 + foreach ( $types as $type ) {
242 327 $page = 1;
243 328
244 329 do {
245 330 $result = $migrator->migrate_chunk( $plugin, $type, $page );
@@ -256,8 +341,17 @@
256 341 $totals['processed'] += (int) ( $result['processed'] ?? 0 );
257 342 $totals['skipped'] += (int) ( $result['skipped'] ?? 0 );
258 343 $totals['analyzed'] += (int) ( $result['analyzed'] ?? 0 );
259 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 + }
260 354
261 355 $has_more = ! empty( $result['has_more'] );
262 356 ++$page;
263 357 } while ( $has_more && $page <= self::MAX_PAGES );