PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 18.1
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v18.1
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / src / actions / importing / aioseo-posts-importing-action.php

aioseo-posts-importing-action.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 18.1, at src/actions/importing/aioseo-posts-importing-action.php

439 lines 14.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yoast\WP\SEO\Actions\Importing;
4
5 use wpdb;
6 use Yoast\WP\SEO\Conditionals\AIOSEO_V4_Importer_Conditional;
7 use Yoast\WP\SEO\Helpers\Indexable_To_Postmeta_Helper;
8 use Yoast\WP\SEO\Helpers\Options_Helper;
9 use Yoast\WP\SEO\Helpers\Sanitization_Helper;
10 use Yoast\WP\SEO\Helpers\Wpdb_Helper;
11 use Yoast\WP\SEO\Models\Indexable;
12 use Yoast\WP\SEO\Repositories\Indexable_Repository;
13 use Yoast\WP\SEO\Services\Importing\Aioseo_Replacevar_Handler;
14 use Yoast\WP\SEO\Services\Importing\Aioseo_Robots_Provider_Service;
15 use Yoast\WP\SEO\Services\Importing\Aioseo_Robots_Transformer_Service;
16
17 /**
18 * Importing action for AIOSEO post data.
19 *
20 * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
21 */
22 class Aioseo_Posts_Importing_Action extends Abstract_Importing_Action {
23
24 use Import_Cursor_Manager_Trait;
25
26 /**
27 * The plugin of the action.
28 */
29 const PLUGIN = 'aioseo';
30
31 /**
32 * The type of the action.
33 */
34 const TYPE = 'posts';
35
36 /**
37 * The map of aioseo to yoast meta.
38 *
39 * @var array
40 */
41 protected $aioseo_to_yoast_map = [
42 'title' => [
43 'yoast_name' => 'title',
44 'transform_method' => 'simple_import',
45 ],
46 'description' => [
47 'yoast_name' => 'description',
48 'transform_method' => 'simple_import',
49 ],
50 'og_title' => [
51 'yoast_name' => 'open_graph_title',
52 'transform_method' => 'simple_import',
53 ],
54 'og_description' => [
55 'yoast_name' => 'open_graph_description',
56 'transform_method' => 'simple_import',
57 ],
58 'twitter_title' => [
59 'yoast_name' => 'twitter_title',
60 'transform_method' => 'simple_import',
61 ],
62 'twitter_description' => [
63 'yoast_name' => 'twitter_description',
64 'transform_method' => 'simple_import',
65 ],
66 'canonical_url' => [
67 'yoast_name' => 'canonical',
68 'transform_method' => 'url_import',
69 ],
70 'keyphrases' => [
71 'yoast_name' => 'primary_focus_keyword',
72 'transform_method' => 'keyphrase_import',
73 ],
74 'robots_noindex' => [
75 'yoast_name' => 'is_robots_noindex',
76 'transform_method' => 'post_robots_noindex_import',
77 'robots_import' => true,
78 ],
79 'robots_nofollow' => [
80 'yoast_name' => 'is_robots_nofollow',
81 'transform_method' => 'post_general_robots_import',
82 'robots_import' => true,
83 'robot_type' => 'nofollow',
84 ],
85 'robots_noarchive' => [
86 'yoast_name' => 'is_robots_noarchive',
87 'transform_method' => 'post_general_robots_import',
88 'robots_import' => true,
89 'robot_type' => 'noarchive',
90 ],
91 'robots_nosnippet' => [
92 'yoast_name' => 'is_robots_nosnippet',
93 'transform_method' => 'post_general_robots_import',
94 'robots_import' => true,
95 'robot_type' => 'nosnippet',
96 ],
97 'robots_noimageindex' => [
98 'yoast_name' => 'is_robots_noimageindex',
99 'transform_method' => 'post_general_robots_import',
100 'robots_import' => true,
101 'robot_type' => 'noimageindex',
102 ],
103 ];
104
105 /**
106 * Represents the indexables repository.
107 *
108 * @var Indexable_Repository
109 */
110 protected $indexable_repository;
111
112 /**
113 * The WordPress database instance.
114 *
115 * @var wpdb
116 */
117 protected $wpdb;
118
119 /**
120 * The indexable_to_postmeta helper.
121 *
122 * @var Indexable_To_Postmeta_Helper
123 */
124 protected $indexable_to_postmeta;
125
126 /**
127 * The wpdb helper.
128 *
129 * @var Wpdb_Helper
130 */
131 protected $wpdb_helper;
132
133 /**
134 * Class constructor.
135 *
136 * @param Indexable_Repository $indexable_repository The indexables repository.
137 * @param wpdb $wpdb The WordPress database instance.
138 * @param Indexable_To_Postmeta_Helper $indexable_to_postmeta The indexable_to_postmeta helper.
139 * @param Options_Helper $options The options helper.
140 * @param Sanitization_Helper $sanitization The sanitization helper.
141 * @param Wpdb_Helper $wpdb_helper The wpdb_helper helper.
142 * @param Aioseo_Replacevar_Handler $replacevar_handler The replacevar handler.
143 * @param Aioseo_Robots_Provider_Service $robots_provider The robots provider service.
144 * @param Aioseo_Robots_Transformer_Service $robots_transformer The robots transfomer service.
145 */
146 public function __construct(
147 Indexable_Repository $indexable_repository,
148 wpdb $wpdb,
149 Indexable_To_Postmeta_Helper $indexable_to_postmeta,
150 Options_Helper $options,
151 Sanitization_Helper $sanitization,
152 Wpdb_Helper $wpdb_helper,
153 Aioseo_Replacevar_Handler $replacevar_handler,
154 Aioseo_Robots_Provider_Service $robots_provider,
155 Aioseo_Robots_Transformer_Service $robots_transformer ) {
156 parent::__construct( $options, $sanitization, $replacevar_handler, $robots_provider, $robots_transformer );
157
158 $this->indexable_repository = $indexable_repository;
159 $this->wpdb = $wpdb;
160 $this->indexable_to_postmeta = $indexable_to_postmeta;
161 $this->wpdb_helper = $wpdb_helper;
162 }
163
164 /**
165 * Retrieves the AIOSEO table name along with the db prefix.
166 *
167 * @return string The AIOSEO table name along with the db prefix.
168 */
169 protected function get_table() {
170 return $this->wpdb->prefix . 'aioseo_posts';
171 }
172
173 /**
174 * Determines if the AIOSEO database table exists.
175 *
176 * @return bool True if the table is found.
177 */
178 protected function aioseo_exists() {
179 return $this->wpdb_helper->table_exists( $this->get_table() ) === true;
180 }
181
182 /**
183 * Returns whether the AISOEO post importing action is enabled.
184 *
185 * @return bool True if the AISOEO post importing action is enabled.
186 */
187 public function is_enabled() {
188 $aioseo_importer_conditional = \YoastSEO()->classes->get( AIOSEO_V4_Importer_Conditional::class );
189
190 return $aioseo_importer_conditional->is_met();
191 }
192
193 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: They are already prepared.
194
195 /**
196 * Returns the total number of unimported objects.
197 *
198 * @return int The total number of unimported objects.
199 */
200 public function get_total_unindexed() {
201 if ( ! $this->aioseo_exists() ) {
202 return 0;
203 }
204
205 $limit = false;
206 $just_detect = true;
207 $indexables_to_create = $this->wpdb->get_col( $this->query( $limit, $just_detect ) );
208
209 $number_of_indexables_to_create = \count( $indexables_to_create );
210 $completed = $number_of_indexables_to_create === 0;
211 $this->set_completed( $completed );
212
213 return $number_of_indexables_to_create;
214 }
215
216 /**
217 * Returns the limited number of unimported objects.
218 *
219 * @param int $limit The maximum number of unimported objects to be returned.
220 *
221 * @return int|false The limited number of unindexed posts. False if the query fails.
222 */
223 public function get_limited_unindexed_count( $limit ) {
224 if ( ! $this->aioseo_exists() ) {
225 return 0;
226 }
227
228 $just_detect = true;
229 $indexables_to_create = $this->wpdb->get_col( $this->query( $limit, $just_detect ) );
230
231 $number_of_indexables_to_create = \count( $indexables_to_create );
232 $completed = $number_of_indexables_to_create === 0;
233 $this->set_completed( $completed );
234
235 return $number_of_indexables_to_create;
236 }
237
238 /**
239 * Imports AIOSEO meta data and creates the respective Yoast indexables and postmeta.
240 *
241 * @todo: Replace the replace vars with Yoast ones.
242 *
243 * @return Indexable[]|false An array of created indexables or false if aioseo data was not found.
244 */
245 public function index() {
246 if ( ! $this->aioseo_exists() ) {
247 return false;
248 }
249
250 $limit = $this->get_limit();
251 $aioseo_indexables = $this->wpdb->get_results( $this->query( $limit ), ARRAY_A );
252 $created_indexables = [];
253
254 $completed = \count( $aioseo_indexables ) === 0;
255 $this->set_completed( $completed );
256
257 $last_indexed_aioseo_id = 0;
258 foreach ( $aioseo_indexables as $aioseo_indexable ) {
259 $last_indexed_aioseo_id = $aioseo_indexable['id'];
260
261 $indexable = $this->indexable_repository->find_by_id_and_type( $aioseo_indexable['post_id'], 'post' );
262
263 // Let's ensure that the current post id represents something that we want to index (eg. *not* shop_order).
264 if ( ! \is_a( $indexable, 'Yoast\WP\SEO\Models\Indexable' ) ) {
265 continue;
266 }
267
268 $indexable = $this->map( $indexable, $aioseo_indexable );
269 $indexable->save();
270
271 // To ensure that indexables can be rebuild after a reset, we have to store the data in the postmeta table too.
272 $this->indexable_to_postmeta->map_to_postmeta( $indexable );
273
274 $last_indexed_aioseo_id = $aioseo_indexable['id'];
275
276 $created_indexables[] = $indexable;
277 }
278
279 $cursor_id = $this->get_cursor_id();
280 $this->set_cursor( $this->options, $cursor_id, $last_indexed_aioseo_id );
281
282 return $created_indexables;
283 }
284
285 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
286
287 /**
288 * Maps AIOSEO meta data to Yoast meta data.
289 *
290 * @param Indexable $indexable The Yoast indexable.
291 * @param array $aioseo_indexable The AIOSEO indexable.
292 *
293 * @return Indexable The created indexables.
294 */
295 public function map( $indexable, $aioseo_indexable ) {
296 foreach ( $this->aioseo_to_yoast_map as $aioseo_key => $yoast_mapping ) {
297 // For robots import.
298 if ( isset( $yoast_mapping['robots_import'] ) && $yoast_mapping['robots_import'] ) {
299 $yoast_mapping['subtype'] = $indexable->object_sub_type;
300 $indexable->{$yoast_mapping['yoast_name']} = \call_user_func( [ $this, $yoast_mapping['transform_method'] ], $aioseo_indexable, $yoast_mapping, $aioseo_key );
301
302 continue;
303 }
304
305 // For import of everything else.
306 // Do not overwrite any existing values.
307 if ( ! empty( $indexable->{$yoast_mapping['yoast_name']} ) ) {
308 continue;
309 }
310
311 if ( ! empty( $aioseo_indexable[ $aioseo_key ] ) ) {
312 $indexable->{$yoast_mapping['yoast_name']} = \call_user_func( [ $this, $yoast_mapping['transform_method'] ], $aioseo_indexable[ $aioseo_key ], $yoast_mapping );
313 }
314 }
315
316 return $indexable;
317 }
318
319 /**
320 * Returns the number of objects that will be imported in a single importing pass.
321 *
322 * @return int The limit.
323 */
324 public function get_limit() {
325 /**
326 * Filter 'wpseo_aioseo_post_indexation_limit' - Allow filtering the number of posts indexed during each indexing pass.
327 *
328 * @api int The maximum number of posts indexed.
329 */
330 $limit = \apply_filters( 'wpseo_aioseo_post_indexation_limit', 25 );
331
332 if ( ! \is_int( $limit ) || $limit < 1 ) {
333 $limit = 25;
334 }
335
336 return $limit;
337 }
338
339 /**
340 * Creates a query for gathering AiOSEO data from the database.
341 *
342 * @param int $limit The maximum number of unimported objects to be returned.
343 * @param bool $just_detect Whether we want to just detect if there are unimported objects. If false, we want to actually import them too.
344 *
345 * @return string The query to use for importing or counting the number of items to import.
346 */
347 public function query( $limit = false, $just_detect = false ) {
348 $table = $this->get_table();
349
350 $select_statement = 'id';
351 if ( ! $just_detect ) {
352 // If we want to import too, we need the actual needed data from AIOSEO indexables.
353 $needed_data = \array_keys( $this->aioseo_to_yoast_map );
354 \array_push( $needed_data, 'id', 'post_id', 'robots_default' );
355
356 $select_statement = \implode( ', ', $needed_data );
357 }
358
359 $cursor_id = $this->get_cursor_id();
360 $cursor = $this->get_cursor( $this->options, $cursor_id );
361
362 /**
363 * Filter 'wpseo_aioseo_post_cursor' - Allow filtering the value of the aioseo post import cursor.
364 *
365 * @api int The value of the aioseo post import cursor.
366 */
367 $cursor = \apply_filters( 'wpseo_aioseo_post_import_cursor', $cursor );
368
369 $replacements = [ $cursor ];
370
371 $limit_statement = '';
372 if ( ! empty( $limit ) ) {
373 $replacements[] = $limit;
374 $limit_statement = ' LIMIT %d';
375 }
376
377 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
378 return $this->wpdb->prepare(
379 "SELECT {$select_statement} FROM {$table} WHERE id > %d ORDER BY id{$limit_statement}",
380 $replacements
381 );
382 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
383 }
384
385 /**
386 * Plucks the keyphrase to be imported from the AIOSEO array of keyphrase meta data.
387 *
388 * @param array $meta_data The keyphrase meta data to be imported.
389 *
390 * @return string|null The plucked keyphrase.
391 */
392 public function keyphrase_import( $meta_data ) {
393 $meta_data = \json_decode( $meta_data, true );
394 if ( ! isset( $meta_data['focus']['keyphrase'] ) ) {
395 return null;
396 }
397
398 return $this->sanitization->sanitize_text_field( $meta_data['focus']['keyphrase'] );
399 }
400
401 /**
402 * Imports the post's noindex setting.
403 *
404 * @param bool $aioseo_robots_settings AIOSEO's set of robot settings for the post.
405 *
406 * @return bool|null The value of Yoast's noindex setting for the post.
407 */
408 public function post_robots_noindex_import( $aioseo_robots_settings ) {
409 // If robot settings defer to default settings, we have null in the is_robots_noindex field.
410 if ( isset( $aioseo_robots_settings['robots_default'] ) && $aioseo_robots_settings['robots_default'] ) {
411 return null;
412 }
413
414 return $aioseo_robots_settings['robots_noindex'];
415 }
416
417 /**
418 * Imports the post's robots setting.
419 *
420 * @param bool $aioseo_robots_settings AIOSEO's set of robot settings for the post.
421 * @param array $mapping The mapping of the setting we're working with.
422 * @param string $aioseo_key The AIOSEO key that contains the robot setting we're working with.
423 *
424 * @return bool|null The value of Yoast's noindex setting for the post.
425 */
426 public function post_general_robots_import( $aioseo_robots_settings, $mapping, $aioseo_key ) {
427 $mapping['type'] = 'postTypes';
428 $mapping['option_name'] = 'aioseo_options_dynamic';
429
430 if ( isset( $aioseo_robots_settings['robots_default'] ) && $aioseo_robots_settings['robots_default'] ) {
431 // Let's first get the subtype's setting value and then transform it taking into consideration whether it defers to global defaults.
432 $subtype_setting = $this->robots_provider->get_subtype_robot_setting( $mapping );
433 return $this->robots_transformer->transform_robot_setting( $mapping['robot_type'], $subtype_setting, $mapping );
434 }
435
436 return $aioseo_robots_settings[ $aioseo_key ];
437 }
438 }
439