PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.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 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / abilities / import / class-run-seo-import.php

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

271 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Run SEO import 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\Snapshot_Migrator;
17 use ThinkRank\Admin\Importers\Yoast_Exporter;
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit; // Exit if accessed directly.
21 }
22
23 /**
24 * Runs a full SEO data import from another plugin into ThinkRank.
25 *
26 * Composes the two-phase pipeline the REST controller exposes as separate
27 * endpoints: it exports the chosen source (Yoast/RankMath/SEOPress/AIOSEO) into
28 * a snapshot, then migrates that snapshot into ThinkRank's `_thinkrank_*`
29 * metadata. Existing ThinkRank values are never overwritten. Source-plugin data
30 * is left intact (no cleanup); reports aggregate counters, not per-item rows.
31 */
32 class Run_Seo_Import extends Ability_Base {
33 /**
34 * Source plugins that can be imported.
35 *
36 * @var string[]
37 */
38 private const ALLOWED_PLUGINS = [ 'yoast', 'rankmath', 'seopress', 'aioseo' ];
39
40 /**
41 * Data types processed, in pipeline order.
42 *
43 * @var string[]
44 */
45 private const TYPES = [ 'postmeta', 'termmeta', 'usermeta', 'redirections', 'settings' ];
46
47 /**
48 * Safety cap on chunk iterations per type (avoids runaway loops).
49 */
50 private const MAX_PAGES = 1000;
51
52 /**
53 * Constructor.
54 */
55 public function __construct() {
56 $this->id = 'thinkrank/run-seo-import';
57 $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' );
59 }
60
61 /**
62 * {@inheritDoc}
63 *
64 * @return array<string, bool|float|string>
65 */
66 public function get_annotations() {
67 return [
68 'readonly' => false,
69 'destructive' => false,
70 'idempotent' => false,
71 'priority' => 2.0,
72 'openWorldHint' => false,
73 ];
74 }
75
76 /**
77 * {@inheritDoc}
78 *
79 * @return array<string, mixed>
80 */
81 public function get_input_schema() {
82 return [
83 'type' => 'object',
84 'additionalProperties' => false,
85 'properties' => [
86 'plugin' => [
87 'type' => 'string',
88 'description' => __( 'The source SEO plugin to import from.', 'thinkrank' ),
89 'enum' => self::ALLOWED_PLUGINS,
90 ],
91 ],
92 'required' => [ 'plugin' ],
93 ];
94 }
95
96 /**
97 * {@inheritDoc}
98 *
99 * @return array<string, mixed>
100 */
101 public function get_output_schema() {
102 return [
103 'type' => 'object',
104 'properties' => [
105 'success' => [ 'type' => 'boolean' ],
106 'plugin' => [ 'type' => 'string' ],
107 'exported' => [
108 'type' => 'object',
109 'additionalProperties' => true,
110 ],
111 'migrated' => [
112 'type' => 'object',
113 'additionalProperties' => true,
114 ],
115 'errors' => [
116 'type' => 'array',
117 'items' => [ 'type' => 'string' ],
118 ],
119 ],
120 ];
121 }
122
123 /**
124 * Execute ability.
125 *
126 * @param array<string, mixed> $input Ability input payload.
127 * @return array<string, mixed>|\WP_Error
128 */
129 public function execute( $input ) {
130 $plugin = isset( $input['plugin'] ) ? sanitize_key( (string) $input['plugin'] ) : '';
131
132 if ( ! in_array( $plugin, self::ALLOWED_PLUGINS, true ) ) {
133 return new \WP_Error(
134 'thinkrank_invalid_import_plugin',
135 __( 'A supported source plugin is required (yoast, rankmath, seopress, aioseo).', 'thinkrank' ),
136 [ 'status' => 400 ]
137 );
138 }
139
140 $exporter = $this->get_exporter( $plugin );
141
142 if ( null === $exporter ) {
143 return new \WP_Error(
144 'thinkrank_invalid_import_plugin',
145 __( 'Could not resolve an exporter for the selected plugin.', 'thinkrank' ),
146 [ 'status' => 400 ]
147 );
148 }
149
150 try {
151 $exported = $this->run_export( $exporter );
152 $migrated = $this->run_migration( $plugin );
153 } catch ( \Throwable $e ) {
154 return new \WP_Error(
155 'thinkrank_import_failed',
156 $e->getMessage(),
157 [ 'status' => 500 ]
158 );
159 }
160
161 return [
162 'success' => empty( $migrated['errors'] ),
163 'plugin' => $plugin,
164 'exported' => $exported,
165 'migrated' => [
166 'processed' => $migrated['processed'],
167 'skipped' => $migrated['skipped'],
168 'analyzed' => $migrated['analyzed'],
169 'keywords_seeded' => $migrated['keywords_seeded'],
170 ],
171 'errors' => $migrated['errors'],
172 ];
173 }
174
175 /**
176 * Resolve the exporter for a source plugin.
177 *
178 * @param string $plugin Source plugin slug.
179 * @return object|null Exporter instance or null when unknown.
180 */
181 private function get_exporter( $plugin ) {
182 switch ( $plugin ) {
183 case 'yoast':
184 return new Yoast_Exporter();
185 case 'rankmath':
186 return new Rankmath_Exporter();
187 case 'seopress':
188 return new SEOPress_Exporter();
189 case 'aioseo':
190 return new AIOSEO_Exporter();
191 default:
192 return null;
193 }
194 }
195
196 /**
197 * Export every data type into a snapshot and finalize it.
198 *
199 * @param object $exporter Source-plugin exporter.
200 * @return array<string, int> Per-type exported counts.
201 */
202 private function run_export( $exporter ) {
203 $exported = [];
204
205 foreach ( self::TYPES as $type ) {
206 $count = 0;
207 $page = 1;
208
209 do {
210 $result = $exporter->export_chunk( $type, $page );
211 $count += is_array( $result ) ? (int) ( $result['exported'] ?? 0 ) : 0;
212 $has_more = is_array( $result ) && ! empty( $result['has_more'] );
213 ++$page;
214 } while ( $has_more && $page <= self::MAX_PAGES );
215
216 $exported[ $type ] = $count;
217 }
218
219 // Flip the manifest status to "complete" so migration is allowed to run.
220 $exporter->finalize_export();
221
222 return $exported;
223 }
224
225 /**
226 * Migrate the snapshot into ThinkRank metadata.
227 *
228 * @param string $plugin Source plugin slug.
229 * @return array<string, mixed> Aggregate counters and any errors.
230 */
231 private function run_migration( $plugin ) {
232 $migrator = new Snapshot_Migrator();
233 $totals = [
234 'processed' => 0,
235 'skipped' => 0,
236 'analyzed' => 0,
237 'keywords_seeded' => 0,
238 'errors' => [],
239 ];
240
241 foreach ( self::TYPES as $type ) {
242 $page = 1;
243
244 do {
245 $result = $migrator->migrate_chunk( $plugin, $type, $page );
246
247 if ( ! is_array( $result ) ) {
248 break;
249 }
250
251 if ( 'error' === ( $result['status'] ?? '' ) ) {
252 $totals['errors'][] = (string) ( $result['message'] ?? 'Unknown migration error.' );
253 break;
254 }
255
256 $totals['processed'] += (int) ( $result['processed'] ?? 0 );
257 $totals['skipped'] += (int) ( $result['skipped'] ?? 0 );
258 $totals['analyzed'] += (int) ( $result['analyzed'] ?? 0 );
259 $totals['keywords_seeded'] += (int) ( $result['keywords_seeded'] ?? 0 );
260
261 $has_more = ! empty( $result['has_more'] );
262 ++$page;
263 } while ( $has_more && $page <= self::MAX_PAGES );
264 }
265
266 $migrator->update_manifest_migration_info( $plugin );
267
268 return $totals;
269 }
270 }
271