| 1 |
<?php |
| 2 |
|
| 3 |
/** |
| 4 |
* SEOPress Exporter |
| 5 |
* |
| 6 |
* Reads SEOPress data from postmeta/termmeta/options and normalizes |
| 7 |
* into the canonical snapshot format. |
| 8 |
* |
| 9 |
* CRITICAL: _seopress_robots_index = 'yes' means NOINDEX (inverted logic). |
| 10 |
* |
| 11 |
* @package ThinkRank\Admin\Importers |
| 12 |
* @since 2.0.0 |
| 13 |
*/ |
| 14 |
|
| 15 |
declare(strict_types=1); |
| 16 |
|
| 17 |
namespace ThinkRank\Admin\Importers; |
| 18 |
|
| 19 |
if (!defined('ABSPATH')) { |
| 20 |
exit; |
| 21 |
} |
| 22 |
|
| 23 |
/** |
| 24 |
* SEOPress Exporter Class |
| 25 |
* |
| 26 |
* @since 2.0.0 |
| 27 |
*/ |
| 28 |
class SEOPress_Exporter extends Abstract_Plugin_Exporter { |
| 29 |
|
| 30 |
/** |
| 31 |
* Constructor |
| 32 |
*/ |
| 33 |
public function __construct() { |
| 34 |
$this->plugin_slug = 'seopress'; |
| 35 |
$this->plugin_name = 'SEOPress'; |
| 36 |
$this->plugin_file = 'wp-seopress/seopress.php'; |
| 37 |
$this->meta_key_prefix = '_seopress_'; |
| 38 |
// SEOPress option names all carry the `_option_name` suffix. |
| 39 |
$this->option_keys = ['seopress_titles_option_name', 'seopress_social_option_name', 'seopress_advanced_option_name']; |
| 40 |
} |
| 41 |
|
| 42 |
/** |
| 43 |
* {@inheritDoc} |
| 44 |
*/ |
| 45 |
public function detect(): bool { |
| 46 |
global $wpdb; |
| 47 |
|
| 48 |
$count = (int) $wpdb->get_var( |
| 49 |
$wpdb->prepare( |
| 50 |
"SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key LIKE %s LIMIT 1", |
| 51 |
$wpdb->esc_like($this->meta_key_prefix) . '%' |
| 52 |
) |
| 53 |
); |
| 54 |
|
| 55 |
return $count > 0; |
| 56 |
} |
| 57 |
|
| 58 |
/** |
| 59 |
* {@inheritDoc} |
| 60 |
*/ |
| 61 |
public function get_available_types(): array { |
| 62 |
global $wpdb; |
| 63 |
|
| 64 |
$types = []; |
| 65 |
|
| 66 |
$post_count = (int) $wpdb->get_var( |
| 67 |
$wpdb->prepare( |
| 68 |
"SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key LIKE %s", |
| 69 |
$wpdb->esc_like($this->meta_key_prefix) . '%' |
| 70 |
) |
| 71 |
); |
| 72 |
if ($post_count > 0) { |
| 73 |
$types['postmeta'] = $post_count; |
| 74 |
} |
| 75 |
|
| 76 |
$term_count = (int) $wpdb->get_var( |
| 77 |
$wpdb->prepare( |
| 78 |
"SELECT COUNT(DISTINCT term_id) FROM {$wpdb->termmeta} WHERE meta_key LIKE %s", |
| 79 |
$wpdb->esc_like($this->meta_key_prefix) . '%' |
| 80 |
) |
| 81 |
); |
| 82 |
if ($term_count > 0) { |
| 83 |
$types['termmeta'] = $term_count; |
| 84 |
} |
| 85 |
|
| 86 |
// Check for redirections via seopress_404 CPT |
| 87 |
$redirect_count = (int) $wpdb->get_var( |
| 88 |
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'seopress_404'" |
| 89 |
); |
| 90 |
if ($redirect_count > 0) { |
| 91 |
$types['redirections'] = $redirect_count; |
| 92 |
} |
| 93 |
|
| 94 |
foreach (array_merge($this->option_keys, ['seopress_xml_sitemap_option_name', 'seopress_instant_indexing_option_name']) as $key) { |
| 95 |
if (get_option($key, null) !== null) { |
| 96 |
$types['settings'] = 1; |
| 97 |
break; |
| 98 |
} |
| 99 |
} |
| 100 |
|
| 101 |
return $types; |
| 102 |
} |
| 103 |
|
| 104 |
/** |
| 105 |
* {@inheritDoc} |
| 106 |
*/ |
| 107 |
protected function export_postmeta_page(int $page): array { |
| 108 |
$post_ids = $this->get_post_ids_with_meta($page); |
| 109 |
|
| 110 |
if (empty($post_ids)) { |
| 111 |
return []; |
| 112 |
} |
| 113 |
|
| 114 |
$records = []; |
| 115 |
foreach ($post_ids as $post_id) { |
| 116 |
$post_id = (int) $post_id; |
| 117 |
$meta = $this->get_all_plugin_meta($post_id); |
| 118 |
|
| 119 |
if (empty($meta)) { |
| 120 |
continue; |
| 121 |
} |
| 122 |
|
| 123 |
// CRITICAL: SEOPress inverted noindex logic |
| 124 |
// _seopress_robots_index = 'yes' means NOINDEX |
| 125 |
$noindex = ($meta['_seopress_robots_index'] ?? '') === 'yes' ? 1 : 0; |
| 126 |
$nofollow = ($meta['_seopress_robots_follow'] ?? '') === 'yes' ? 1 : 0; |
| 127 |
|
| 128 |
// Focus keywords are one comma-separated string. The full list |
| 129 |
// feeds ThinkRank's keyword array (data.focus_keywords); the first |
| 130 |
// entry stays the legacy single focus keyword. |
| 131 |
$focus_kw_raw = $meta['_seopress_analysis_target_kw'] ?? ''; |
| 132 |
$focus_keywords = array_values(array_filter( |
| 133 |
array_map('trim', explode(',', $focus_kw_raw)), |
| 134 |
static fn($keyword) => $keyword !== '' |
| 135 |
)); |
| 136 |
$primary_keyword = $focus_keywords[0] ?? ''; |
| 137 |
$additional_keywords = array_slice($focus_keywords, 1); |
| 138 |
|
| 139 |
$records[] = [ |
| 140 |
'object_id' => $post_id, |
| 141 |
'object_type' => 'post', |
| 142 |
'source_plugin' => $this->plugin_slug, |
| 143 |
'data' => [ |
| 144 |
'seo_title' => $this->convert_template_variables($meta['_seopress_titles_title'] ?? '', $post_id), |
| 145 |
'meta_description' => $this->convert_template_variables($meta['_seopress_titles_desc'] ?? '', $post_id), |
| 146 |
'focus_keyword' => $primary_keyword, |
| 147 |
'focus_keywords' => $focus_keywords, |
| 148 |
'canonical_url' => $meta['_seopress_robots_canonical'] ?? '', |
| 149 |
'noindex' => $noindex, |
| 150 |
'nofollow' => $nofollow, |
| 151 |
'noarchive' => ($meta['_seopress_robots_archive'] ?? '') === 'yes' ? 1 : 0, |
| 152 |
'noimageindex' => ($meta['_seopress_robots_imageindex'] ?? '') === 'yes' ? 1 : 0, |
| 153 |
'nosnippet' => ($meta['_seopress_robots_snippet'] ?? '') === 'yes' ? 1 : 0, |
| 154 |
'og_title' => $this->convert_template_variables($meta['_seopress_social_fb_title'] ?? '', $post_id), |
| 155 |
'og_description' => $this->convert_template_variables($meta['_seopress_social_fb_desc'] ?? '', $post_id), |
| 156 |
'og_image' => $meta['_seopress_social_fb_img'] ?? '', |
| 157 |
'twitter_title' => $this->convert_template_variables($meta['_seopress_social_twitter_title'] ?? '', $post_id), |
| 158 |
'twitter_description' => $this->convert_template_variables($meta['_seopress_social_twitter_desc'] ?? '', $post_id), |
| 159 |
'twitter_image' => $meta['_seopress_social_twitter_img'] ?? '', |
| 160 |
'primary_category' => (int) ($meta['_seopress_robots_primary_cat'] ?? 0), |
| 161 |
'schema_type' => '', |
| 162 |
], |
| 163 |
'extended' => [ |
| 164 |
'focus_keywords_additional' => $additional_keywords, |
| 165 |
'redirect_enabled' => !empty($meta['_seopress_redirections_enabled']), |
| 166 |
'redirect_url' => $meta['_seopress_redirections_value'] ?? '', |
| 167 |
'redirect_type' => $meta['_seopress_redirections_type'] ?? '301', |
| 168 |
'og_image_width' => $meta['_seopress_social_fb_img_width'] ?? '', |
| 169 |
'og_image_height' => $meta['_seopress_social_fb_img_height'] ?? '', |
| 170 |
'twitter_card_type' => $meta['_seopress_social_twitter_card_img_size'] ?? '', |
| 171 |
], |
| 172 |
]; |
| 173 |
} |
| 174 |
|
| 175 |
return $records; |
| 176 |
} |
| 177 |
|
| 178 |
/** |
| 179 |
* {@inheritDoc} |
| 180 |
*/ |
| 181 |
protected function export_termmeta_page(int $page): array { |
| 182 |
$term_ids = $this->get_term_ids_with_meta($page); |
| 183 |
|
| 184 |
if (empty($term_ids)) { |
| 185 |
return []; |
| 186 |
} |
| 187 |
|
| 188 |
$records = []; |
| 189 |
foreach ($term_ids as $term_id) { |
| 190 |
$term_id = (int) $term_id; |
| 191 |
$meta = $this->get_all_plugin_term_meta($term_id); |
| 192 |
|
| 193 |
if (empty($meta)) { |
| 194 |
continue; |
| 195 |
} |
| 196 |
|
| 197 |
$noindex = ($meta['_seopress_robots_index'] ?? '') === 'yes' ? 1 : 0; |
| 198 |
$nofollow = ($meta['_seopress_robots_follow'] ?? '') === 'yes' ? 1 : 0; |
| 199 |
|
| 200 |
$records[] = [ |
| 201 |
'object_id' => $term_id, |
| 202 |
'object_type' => 'term', |
| 203 |
'source_plugin' => $this->plugin_slug, |
| 204 |
'data' => [ |
| 205 |
'seo_title' => $this->convert_template_variables($meta['_seopress_titles_title'] ?? ''), |
| 206 |
'meta_description' => $this->convert_template_variables($meta['_seopress_titles_desc'] ?? ''), |
| 207 |
'canonical_url' => $meta['_seopress_robots_canonical'] ?? '', |
| 208 |
'noindex' => $noindex, |
| 209 |
'nofollow' => $nofollow, |
| 210 |
'og_title' => $this->convert_template_variables($meta['_seopress_social_fb_title'] ?? ''), |
| 211 |
'og_description' => $this->convert_template_variables($meta['_seopress_social_fb_desc'] ?? ''), |
| 212 |
], |
| 213 |
'extended' => [ |
| 214 |
'og_image' => $meta['_seopress_social_fb_img'] ?? '', |
| 215 |
'twitter_title' => $meta['_seopress_social_twitter_title'] ?? '', |
| 216 |
'twitter_description' => $meta['_seopress_social_twitter_desc'] ?? '', |
| 217 |
], |
| 218 |
]; |
| 219 |
} |
| 220 |
|
| 221 |
return $records; |
| 222 |
} |
| 223 |
|
| 224 |
/** |
| 225 |
* {@inheritDoc} |
| 226 |
*/ |
| 227 |
protected function export_usermeta_page(int $page): array { |
| 228 |
return []; |
| 229 |
} |
| 230 |
|
| 231 |
/** |
| 232 |
* {@inheritDoc} |
| 233 |
*/ |
| 234 |
protected function export_settings(): array { |
| 235 |
$titles = get_option('seopress_titles_option_name', []); |
| 236 |
$social = get_option('seopress_social_option_name', []); |
| 237 |
$advanced = get_option('seopress_advanced_option_name', []); |
| 238 |
$xml_sitemap = get_option('seopress_xml_sitemap_option_name', []); |
| 239 |
$instant = get_option('seopress_instant_indexing_option_name', []); |
| 240 |
$pro = get_option('seopress_pro_option_name', []); |
| 241 |
|
| 242 |
$titles = is_array($titles) ? $titles : []; |
| 243 |
$social = is_array($social) ? $social : []; |
| 244 |
$advanced = is_array($advanced) ? $advanced : []; |
| 245 |
$xml_sitemap = is_array($xml_sitemap) ? $xml_sitemap : []; |
| 246 |
$instant = is_array($instant) ? $instant : []; |
| 247 |
$pro = is_array($pro) ? $pro : []; |
| 248 |
|
| 249 |
return [ |
| 250 |
[ |
| 251 |
'type' => 'settings', |
| 252 |
'source_plugin' => $this->plugin_slug, |
| 253 |
'data' => [ |
| 254 |
'separator' => $titles['seopress_titles_sep'] ?? '-', |
| 255 |
'homepage_title' => $this->convert_template_variables($titles['seopress_titles_home_site_title'] ?? ''), |
| 256 |
'homepage_description' => $this->convert_template_variables($titles['seopress_titles_home_site_desc'] ?? ''), |
| 257 |
// Knowledge Graph lives in SEOPress's SOCIAL option |
| 258 |
// (knowledge_type 'Organization'|'Person', name, img). |
| 259 |
'organization_name' => $this->convert_template_variables($social['seopress_social_knowledge_name'] ?? ''), |
| 260 |
'organization_logo' => $social['seopress_social_knowledge_img'] ?? '', |
| 261 |
'knowledge_graph' => $this->extract_seopress_knowledge_graph($social), |
| 262 |
'social_profiles' => [ |
| 263 |
'facebook' => $social['seopress_social_accounts_facebook'] ?? '', |
| 264 |
'twitter' => $social['seopress_social_accounts_twitter'] ?? '', |
| 265 |
'instagram' => $social['seopress_social_accounts_instagram'] ?? '', |
| 266 |
'linkedin' => $social['seopress_social_accounts_linkedin'] ?? '', |
| 267 |
'youtube' => $social['seopress_social_accounts_youtube'] ?? '', |
| 268 |
'pinterest' => $social['seopress_social_accounts_pinterest'] ?? '', |
| 269 |
], |
| 270 |
// A disabled archive is stronger than noindex — ThinkRank |
| 271 |
// cannot remove date archives, so both map to noindex. |
| 272 |
'noindex_archives' => [ |
| 273 |
'date' => !empty($titles['seopress_titles_archives_date_noindex']) |
| 274 |
|| !empty($titles['seopress_titles_archives_date_disable']), |
| 275 |
'author' => !empty($titles['seopress_titles_archives_author_noindex']) |
| 276 |
|| !empty($titles['seopress_titles_archives_author_disable']), |
| 277 |
], |
| 278 |
'twitter_card_type' => (string) ($social['seopress_social_twitter_card_img_size'] ?? ''), |
| 279 |
'social_defaults' => [ |
| 280 |
'facebook_app_id' => (string) ($social['seopress_social_facebook_app_id'] ?? ''), |
| 281 |
'og_default_image' => (string) ($social['seopress_social_facebook_img'] ?? ''), |
| 282 |
], |
| 283 |
// SEOPress's "Bing API key" IS the IndexNow key (served at |
| 284 |
// /{key}.txt) — carrying it over skips re-verification. |
| 285 |
'instant_indexing' => [ |
| 286 |
'api_key' => (string) ($instant['seopress_instant_indexing_bing_api_key'] ?? ''), |
| 287 |
], |
| 288 |
], |
| 289 |
'extended' => [ |
| 290 |
// Pinterest is applied (ThinkRank renders it); the rest is |
| 291 |
// preserved and gates /import/cleanup. |
| 292 |
'webmaster_tools' => array_filter([ |
| 293 |
'google' => (string) ($advanced['seopress_advanced_advanced_google'] ?? ''), |
| 294 |
'bing' => (string) ($advanced['seopress_advanced_advanced_bing'] ?? ''), |
| 295 |
'yandex' => (string) ($advanced['seopress_advanced_advanced_yandex'] ?? ''), |
| 296 |
'baidu' => (string) ($advanced['seopress_advanced_advanced_baidu'] ?? ''), |
| 297 |
'pinterest' => (string) ($advanced['seopress_advanced_advanced_pinterest'] ?? ''), |
| 298 |
]), |
| 299 |
'title_formats' => $this->extract_seopress_title_formats($titles), |
| 300 |
'post_type_settings' => $this->extract_seopress_post_type_settings($titles), |
| 301 |
'author_archives' => $this->extract_seopress_author_archives($titles), |
| 302 |
// Auto image alt is a single toggle in SEOPress (no format). |
| 303 |
'image_seo' => !empty($advanced['seopress_advanced_advanced_image_auto_alt_txt']) |
| 304 |
? ['add_missing_alt' => true] |
| 305 |
: [], |
| 306 |
'breadcrumb_settings' => $this->extract_seopress_breadcrumbs($pro), |
| 307 |
'sitemap_settings' => $this->normalize_seopress_sitemap($xml_sitemap), |
| 308 |
], |
| 309 |
], |
| 310 |
]; |
| 311 |
} |
| 312 |
|
| 313 |
/** |
| 314 |
* Extract SEOPress's Knowledge Graph entity (social option: |
| 315 |
* knowledge_type 'Organization'|'Person' + knowledge_name). |
| 316 |
* |
| 317 |
* @param array $social seopress_social_option_name value |
| 318 |
* @return array{type: string, name: string} |
| 319 |
*/ |
| 320 |
private function extract_seopress_knowledge_graph(array $social): array { |
| 321 |
$type = strtolower((string) ($social['seopress_social_knowledge_type'] ?? '')); |
| 322 |
$name = $this->convert_template_variables((string) ($social['seopress_social_knowledge_name'] ?? '')); |
| 323 |
|
| 324 |
if (!in_array($type, ['organization', 'person'], true) || $name === '') { |
| 325 |
return ['type' => '', 'name' => '']; |
| 326 |
} |
| 327 |
|
| 328 |
return ['type' => $type, 'name' => $name]; |
| 329 |
} |
| 330 |
|
| 331 |
/** |
| 332 |
* Map SEOPress's per-context title templates onto ThinkRank's Site |
| 333 |
* Identity keys, converting %%vars%% to the identity renderer's %token% |
| 334 |
* vocabulary. |
| 335 |
* |
| 336 |
* @param array $titles seopress_titles_option_name value |
| 337 |
* @return array Map of ThinkRank title-format key => converted template |
| 338 |
*/ |
| 339 |
private function extract_seopress_title_formats(array $titles): array { |
| 340 |
$single = is_array($titles['seopress_titles_single_titles'] ?? null) ? $titles['seopress_titles_single_titles'] : []; |
| 341 |
$tax = is_array($titles['seopress_titles_tax_titles'] ?? null) ? $titles['seopress_titles_tax_titles'] : []; |
| 342 |
|
| 343 |
$sources = [ |
| 344 |
'homepage_title' => [$titles['seopress_titles_home_site_title'] ?? '', ''], |
| 345 |
'post_title' => [$single['post']['title'] ?? '', '%post_title%'], |
| 346 |
'page_title' => [$single['page']['title'] ?? '', '%page_title%'], |
| 347 |
'category_title' => [$tax['category']['title'] ?? '', '%category_title%'], |
| 348 |
'tag_title' => [$tax['post_tag']['title'] ?? '', '%tag_title%'], |
| 349 |
'search_title' => [$titles['seopress_titles_archives_search_title'] ?? '', '%search_term%'], |
| 350 |
'archive_title' => [$titles['seopress_titles_archives_date_title'] ?? '', '%date%'], |
| 351 |
'author_title' => [$titles['seopress_titles_archives_author_title'] ?? '', '%author_name%'], |
| 352 |
]; |
| 353 |
|
| 354 |
$formats = []; |
| 355 |
foreach ($sources as $tr_key => [$raw, $context_token]) { |
| 356 |
$converted = $this->convert_seopress_identity_template((string) $raw, $context_token); |
| 357 |
if ($converted !== '') { |
| 358 |
$formats[$tr_key] = $converted; |
| 359 |
} |
| 360 |
} |
| 361 |
|
| 362 |
return $formats; |
| 363 |
} |
| 364 |
|
| 365 |
/** |
| 366 |
* Convert a SEOPress title template into ThinkRank's Site Identity token |
| 367 |
* vocabulary, preserving structure. Unknown %%vars%% are stripped and a |
| 368 |
* dangling separator dropped. |
| 369 |
* |
| 370 |
* @param string $template Raw SEOPress template (%%var%% syntax) |
| 371 |
* @param string $context_token Token %%post_title%%/%%term_title%% stands for (may be '') |
| 372 |
* @return string ThinkRank Site Identity template |
| 373 |
*/ |
| 374 |
private function convert_seopress_identity_template(string $template, string $context_token): string { |
| 375 |
if ($template === '' || strpos($template, '%%') === false) { |
| 376 |
return trim($template); |
| 377 |
} |
| 378 |
|
| 379 |
$map = [ |
| 380 |
'%%sitetitle%%' => '%site_title%', |
| 381 |
'%%sitename%%' => '%site_title%', |
| 382 |
'%%tagline%%' => '%site_description%', |
| 383 |
'%%sitedesc%%' => '%site_description%', |
| 384 |
'%%sep%%' => '%sep%', |
| 385 |
'%%search_keywords%%' => '%search_term%', |
| 386 |
'%%archive_title%%' => '%archive_title%', |
| 387 |
'%%archive_date%%' => '%date%', |
| 388 |
'%%post_date%%' => '%date%', |
| 389 |
'%%post_author%%' => '%author_name%', |
| 390 |
]; |
| 391 |
if ($context_token !== '') { |
| 392 |
$map['%%post_title%%'] = $context_token; |
| 393 |
$map['%%title%%'] = $context_token; |
| 394 |
$map['%%term_title%%'] = $context_token; |
| 395 |
} |
| 396 |
$template = str_replace(array_keys($map), array_values($map), $template); |
| 397 |
|
| 398 |
// Drop remaining SEOPress vars ThinkRank cannot resolve (%%page%%, …). |
| 399 |
$template = (string) preg_replace('/%%[a-z0-9_-]+%%/i', '', $template); |
| 400 |
|
| 401 |
$template = (string) preg_replace('/\s{2,}/', ' ', $template); |
| 402 |
$template = trim($template); |
| 403 |
$template = (string) preg_replace('/^(?:%sep%)\s*/', '', $template); |
| 404 |
$template = (string) preg_replace('/\s*(?:%sep%)$/', '', $template); |
| 405 |
|
| 406 |
return trim($template); |
| 407 |
} |
| 408 |
|
| 409 |
/** |
| 410 |
* Extract SEOPress's per-post-type title/description templates and robots |
| 411 |
* in the shape migrate_post_type_settings() consumes (Global SEO dialect: |
| 412 |
* %title%/%sitename%/%excerpt%). |
| 413 |
* |
| 414 |
* @param array $titles seopress_titles_option_name value |
| 415 |
* @return array Map of post_type => {title_template, description_template, custom_robots, robots} |
| 416 |
*/ |
| 417 |
private function extract_seopress_post_type_settings(array $titles): array { |
| 418 |
$single = is_array($titles['seopress_titles_single_titles'] ?? null) ? $titles['seopress_titles_single_titles'] : []; |
| 419 |
$settings = []; |
| 420 |
|
| 421 |
foreach ($single as $post_type => $pt) { |
| 422 |
if (!is_array($pt)) { |
| 423 |
continue; |
| 424 |
} |
| 425 |
|
| 426 |
$pt_settings = []; |
| 427 |
|
| 428 |
$title = $this->convert_seopress_global_template((string) ($pt['title'] ?? '')); |
| 429 |
if ($title !== '') { |
| 430 |
$pt_settings['title_template'] = $title; |
| 431 |
} |
| 432 |
|
| 433 |
$description = $this->convert_seopress_global_template((string) ($pt['description'] ?? '')); |
| 434 |
if ($description !== '') { |
| 435 |
$pt_settings['description_template'] = $description; |
| 436 |
} |
| 437 |
|
| 438 |
$directives = []; |
| 439 |
foreach (['noindex', 'nofollow'] as $flag) { |
| 440 |
if (!empty($pt[$flag])) { |
| 441 |
$directives[] = $flag; |
| 442 |
} |
| 443 |
} |
| 444 |
if (!empty($directives)) { |
| 445 |
$pt_settings['custom_robots'] = true; |
| 446 |
$pt_settings['robots'] = $directives; |
| 447 |
} |
| 448 |
|
| 449 |
if (!empty($pt_settings)) { |
| 450 |
$settings[$post_type] = $pt_settings; |
| 451 |
} |
| 452 |
} |
| 453 |
|
| 454 |
// SEOPress keeps attachment noindex as its own flag outside |
| 455 |
// single_titles. |
| 456 |
if (!empty($titles['seopress_titles_attachments_noindex'])) { |
| 457 |
$settings['attachment']['custom_robots'] = true; |
| 458 |
$settings['attachment']['robots'] = array_values(array_unique(array_merge( |
| 459 |
$settings['attachment']['robots'] ?? [], |
| 460 |
['noindex'] |
| 461 |
))); |
| 462 |
} |
| 463 |
|
| 464 |
return $settings; |
| 465 |
} |
| 466 |
|
| 467 |
/** |
| 468 |
* Convert a SEOPress template into the Global SEO Pattern_Resolver |
| 469 |
* vocabulary (%title%/%sitename%/%sep%/%excerpt%). |
| 470 |
* |
| 471 |
* @param string $template Raw SEOPress template |
| 472 |
* @return string Converted template |
| 473 |
*/ |
| 474 |
private function convert_seopress_global_template(string $template): string { |
| 475 |
if ($template === '' || strpos($template, '%%') === false) { |
| 476 |
return trim($template); |
| 477 |
} |
| 478 |
|
| 479 |
$map = [ |
| 480 |
'%%post_title%%' => '%title%', |
| 481 |
'%%title%%' => '%title%', |
| 482 |
'%%sitetitle%%' => '%sitename%', |
| 483 |
'%%sitename%%' => '%sitename%', |
| 484 |
'%%sep%%' => '%sep%', |
| 485 |
'%%post_excerpt%%' => '%excerpt%', |
| 486 |
'%%excerpt%%' => '%excerpt%', |
| 487 |
'%%post_date%%' => '%date%', |
| 488 |
'%%post_author%%' => '%author%', |
| 489 |
'%%post_category%%' => '%category%', |
| 490 |
]; |
| 491 |
$template = str_replace(array_keys($map), array_values($map), $template); |
| 492 |
|
| 493 |
$template = (string) preg_replace('/%%[a-z0-9_-]+%%/i', '', $template); |
| 494 |
$template = (string) preg_replace('/\s+/', ' ', $template); |
| 495 |
|
| 496 |
return trim($template); |
| 497 |
} |
| 498 |
|
| 499 |
/** |
| 500 |
* Extract SEOPress's author-archive behaviour for ThinkRank's Author |
| 501 |
* Archives feature. The noindex flag travels in data.noindex_archives. |
| 502 |
* |
| 503 |
* @param array $titles seopress_titles_option_name value |
| 504 |
* @return array Author archive settings |
| 505 |
*/ |
| 506 |
private function extract_seopress_author_archives(array $titles): array { |
| 507 |
$has_any = isset($titles['seopress_titles_archives_author_disable']) |
| 508 |
|| !empty($titles['seopress_titles_archives_author_title']) |
| 509 |
|| !empty($titles['seopress_titles_archives_author_desc']); |
| 510 |
if (!$has_any) { |
| 511 |
return []; |
| 512 |
} |
| 513 |
|
| 514 |
return [ |
| 515 |
'enabled' => empty($titles['seopress_titles_archives_author_disable']), |
| 516 |
'title' => $this->convert_seopress_identity_template((string) ($titles['seopress_titles_archives_author_title'] ?? ''), '%author_name%'), |
| 517 |
'description' => $this->convert_seopress_identity_template((string) ($titles['seopress_titles_archives_author_desc'] ?? ''), '%author_name%'), |
| 518 |
]; |
| 519 |
} |
| 520 |
|
| 521 |
/** |
| 522 |
* Extract SEOPress Pro's breadcrumb settings in the canonical |
| 523 |
* breadcrumb_settings shape. No-op on SEOPress Free (option absent). |
| 524 |
* |
| 525 |
* @param array $pro seopress_pro_option_name value |
| 526 |
* @return array Canonical breadcrumb_settings payload |
| 527 |
*/ |
| 528 |
private function extract_seopress_breadcrumbs(array $pro): array { |
| 529 |
if (empty($pro)) { |
| 530 |
return []; |
| 531 |
} |
| 532 |
|
| 533 |
$has_any = isset($pro['seopress_breadcrumbs_enable']) |
| 534 |
|| !empty($pro['seopress_breadcrumbs_separator']) |
| 535 |
|| !empty($pro['seopress_breadcrumbs_i18n_home']); |
| 536 |
if (!$has_any) { |
| 537 |
return []; |
| 538 |
} |
| 539 |
|
| 540 |
return [ |
| 541 |
'enabled' => !empty($pro['seopress_breadcrumbs_enable']), |
| 542 |
'home_label' => (string) ($pro['seopress_breadcrumbs_i18n_home'] ?? ''), |
| 543 |
'separator' => trim(html_entity_decode((string) ($pro['seopress_breadcrumbs_separator'] ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8')), |
| 544 |
'prefix' => (string) ($pro['seopress_breadcrumbs_i18n_here'] ?? ''), |
| 545 |
]; |
| 546 |
} |
| 547 |
|
| 548 |
/** |
| 549 |
* Normalize SEOPress's `seopress_xml_sitemap_option` into the flat, canonical |
| 550 |
* `sitemap_settings` shape the migrator's migrate_sitemap() reads. |
| 551 |
* |
| 552 |
* SEOPress stores a global enable flag, an image-inclusion flag, and per-post- |
| 553 |
* type / per-taxonomy inclusion as { <slug>: { include: '1' } } maps. It |
| 554 |
* exposes no links-per-file, featured-image or ping toggle, so those keys are |
| 555 |
* omitted (the migrator skips absent keys). Returns ['has_data' => false] when |
| 556 |
* there is nothing to migrate. |
| 557 |
* |
| 558 |
* NOTE: SEOPress is not installed in this environment; the key paths below |
| 559 |
* come from SEOPress's documented options schema, not a live capture. |
| 560 |
* |
| 561 |
* @param array $sitemap seopress_xml_sitemap_option value |
| 562 |
* @return array Canonical sitemap_settings payload |
| 563 |
*/ |
| 564 |
private function normalize_seopress_sitemap(array $sitemap): array { |
| 565 |
if (empty($sitemap)) { |
| 566 |
return ['has_data' => false]; |
| 567 |
} |
| 568 |
|
| 569 |
$post_types = is_array($sitemap['seopress_xml_sitemap_post_types_list'] ?? null) |
| 570 |
? $sitemap['seopress_xml_sitemap_post_types_list'] : []; |
| 571 |
$taxonomies = is_array($sitemap['seopress_xml_sitemap_taxonomies_list'] ?? null) |
| 572 |
? $sitemap['seopress_xml_sitemap_taxonomies_list'] : []; |
| 573 |
|
| 574 |
return [ |
| 575 |
'enabled' => !empty($sitemap['seopress_xml_sitemap_general_enable']), |
| 576 |
'include_images' => !empty($sitemap['seopress_xml_sitemap_img_enable']), |
| 577 |
'include_posts' => !empty($post_types['post']['include']), |
| 578 |
'include_pages' => !empty($post_types['page']['include']), |
| 579 |
'include_categories' => !empty($taxonomies['category']['include']), |
| 580 |
'include_tags' => !empty($taxonomies['post_tag']['include']), |
| 581 |
'has_data' => true, |
| 582 |
]; |
| 583 |
} |
| 584 |
|
| 585 |
/** |
| 586 |
* {@inheritDoc} |
| 587 |
*/ |
| 588 |
protected function export_redirections_page(int $page): array { |
| 589 |
$offset = ($page - 1) * $this->chunk_size; |
| 590 |
|
| 591 |
$redirects = get_posts([ |
| 592 |
'post_type' => 'seopress_404', |
| 593 |
'posts_per_page' => $this->chunk_size, |
| 594 |
'offset' => $offset, |
| 595 |
'orderby' => 'ID', |
| 596 |
'order' => 'ASC', |
| 597 |
'post_status' => 'any', |
| 598 |
]); |
| 599 |
|
| 600 |
if (empty($redirects)) { |
| 601 |
return []; |
| 602 |
} |
| 603 |
|
| 604 |
$records = []; |
| 605 |
foreach ($redirects as $redirect) { |
| 606 |
$meta = get_post_meta($redirect->ID); |
| 607 |
|
| 608 |
$records[] = [ |
| 609 |
'object_type' => 'redirection', |
| 610 |
'source_plugin' => $this->plugin_slug, |
| 611 |
'data' => [], |
| 612 |
'extended' => [ |
| 613 |
'source_url' => $redirect->post_title ?? '', |
| 614 |
'target_url' => $meta['_seopress_redirections_value'][0] ?? '', |
| 615 |
'http_code' => (int) ($meta['_seopress_redirections_type'][0] ?? 301), |
| 616 |
'is_regex' => !empty($meta['_seopress_redirections_enabled_regex'][0]), |
| 617 |
'enabled' => !empty($meta['_seopress_redirections_enabled'][0]), |
| 618 |
], |
| 619 |
]; |
| 620 |
} |
| 621 |
|
| 622 |
return $records; |
| 623 |
} |
| 624 |
|
| 625 |
/** |
| 626 |
* {@inheritDoc} |
| 627 |
*/ |
| 628 |
protected function convert_template_variables($value, ?int $post_id = null): string { |
| 629 |
// Foreign data first: booleans/arrays in the source plugin's options |
| 630 |
// must degrade to '' here, not fatal the migration (see abstract). |
| 631 |
$value = $this->stringify_template_value($value); |
| 632 |
|
| 633 |
if (empty($value) || strpos($value, '%%') === false) { |
| 634 |
return $value; |
| 635 |
} |
| 636 |
|
| 637 |
// SEOPress uses %%variable%% syntax similar to Yoast |
| 638 |
$replacements = [ |
| 639 |
'%%sitetitle%%' => get_bloginfo('name'), |
| 640 |
'%%sitename%%' => get_bloginfo('name'), |
| 641 |
'%%tagline%%' => get_bloginfo('description'), |
| 642 |
'%%sitedesc%%' => get_bloginfo('description'), |
| 643 |
'%%sep%%' => '-', |
| 644 |
'%%currentyear%%' => gmdate('Y'), |
| 645 |
'%%currentmonth%%' => gmdate('F'), |
| 646 |
'%%currentday%%' => gmdate('j'), |
| 647 |
'%%currentdate%%' => gmdate('Y-m-d'), |
| 648 |
]; |
| 649 |
|
| 650 |
if ($post_id) { |
| 651 |
$post = get_post($post_id); |
| 652 |
if ($post) { |
| 653 |
$replacements['%%post_title%%'] = $post->post_title; |
| 654 |
$replacements['%%title%%'] = $post->post_title; |
| 655 |
$replacements['%%post_excerpt%%'] = \ThinkRank\Core\Seo_Text::trim_words( |
| 656 |
$post->post_excerpt ?: \ThinkRank\Core\Seo_Text::trim_words(wp_strip_all_tags($post->post_content), 55), |
| 657 |
55 |
| 658 |
); |
| 659 |
$replacements['%%post_date%%'] = get_the_date('', $post); |
| 660 |
$replacements['%%post_modified_date%%'] = get_the_modified_date('', $post); |
| 661 |
$replacements['%%post_author%%'] = get_the_author_meta('display_name', (int) $post->post_author); |
| 662 |
|
| 663 |
$post_type_obj = get_post_type_object($post->post_type); |
| 664 |
$replacements['%%post_type%%'] = $post_type_obj ? $post_type_obj->labels->singular_name : ''; |
| 665 |
|
| 666 |
$categories = get_the_category($post_id); |
| 667 |
$replacements['%%post_category%%'] = !empty($categories) ? $categories[0]->name : ''; |
| 668 |
|
| 669 |
$tags = get_the_tags($post_id); |
| 670 |
$replacements['%%post_tag%%'] = !empty($tags) ? $tags[0]->name : ''; |
| 671 |
} |
| 672 |
} |
| 673 |
|
| 674 |
$value = str_replace(array_keys($replacements), array_values($replacements), $value); |
| 675 |
|
| 676 |
// Strip remaining unknown variables |
| 677 |
$value = preg_replace('/%%[a-z0-9_-]+%%/i', '', $value); |
| 678 |
|
| 679 |
return trim($value); |
| 680 |
} |
| 681 |
} |
| 682 |
|