Changeset 3657955
- Timestamp:
- 08/21/2026 12:20:58 AM (2 months ago)
- Location:
- prorank-seo/trunk
- Files:
-
- 32 added
- 17 deleted
- 22 edited
-
build-info.json (modified) (1 diff)
-
build/App-BzWPS4Dt.css (deleted)
-
build/App-ConVbF5i.css (added)
-
build/App-Dd3OPBZ_.js (deleted)
-
build/App-DktQ3r8n.js (added)
-
build/CompetitorImportStep-B42Pd5fP.js (added)
-
build/CompetitorImportStep-zdIPNvmD.js (deleted)
-
build/ModulesStep-9l74MK7B.js (deleted)
-
build/ModulesStep-DISyQc2j.js (added)
-
build/OptimizationStep-Co76P-Yd.js (deleted)
-
build/OptimizationStep-hUXCMlfx.js (added)
-
build/OverviewTab-Cge4lX_u.js (added)
-
build/OverviewTab-x8UJXvIF.js (deleted)
-
build/ReadyStep-CxBDyjZm.js (added)
-
build/ReadyStep-DpYwwttD.js (deleted)
-
build/WelcomeStep-ByTBOSo2.js (added)
-
build/WelcomeStep-Cv1PDtMh.js (deleted)
-
build/admin.asset.php (modified) (1 diff)
-
build/admin.js (modified) (1 diff)
-
build/content-visibility-learning.asset.php (added)
-
build/content-visibility-learning.js (added)
-
build/index-BCNX0d2j.js (deleted)
-
build/index-BMVrl8nb.js (deleted)
-
build/index-BiBBcfit.js (added)
-
build/index-BiskG-Dh.js (deleted)
-
build/index-BkRERGms.js (deleted)
-
build/index-C0SSOFPG.js (deleted)
-
build/index-CTssvNCD.js (added)
-
build/index-CU1RdToR.js (added)
-
build/index-Cc9E22V3.js (added)
-
build/index-DqieOZeU.js (added)
-
build/index-Ds34fav7.js (added)
-
build/index-Dsvvn0QB.js (deleted)
-
build/index-JKvGsgrN.js (added)
-
build/index-KZ_0GV60.js (deleted)
-
build/index-P8uNM8IO.js (added)
-
build/index-VnG1MpO_.js (deleted)
-
build/index-lawMXFif.js (added)
-
build/index-zOCfL-MX.js (deleted)
-
build/manifest.json (modified) (35 diffs)
-
includes/Admin/AdminBarMenu.php (modified) (1 diff)
-
includes/Admin/MenuHandler.php (modified) (2 diffs)
-
includes/Core/Abilities (added)
-
includes/Core/Abilities/AbilitiesIntegration.php (added)
-
includes/Core/Compatibility/CompatibilityProfileCatalog.php (added)
-
includes/Core/Compatibility/CompetitorDetector.php (modified) (12 diffs)
-
includes/Core/Importers/AIOSEOImporter.php (modified) (7 diffs)
-
includes/Core/Importers/AioRichSnippetsImporter.php (added)
-
includes/Core/Importers/BaseImporter.php (modified) (15 diffs)
-
includes/Core/Importers/ImportTransaction.php (added)
-
includes/Core/Importers/LegacySchemaMapper.php (added)
-
includes/Core/Importers/RankMathImporter.php (modified) (9 diffs)
-
includes/Core/Importers/RedirectionImporter.php (added)
-
includes/Core/Importers/SEOPressImporter.php (modified) (5 diffs)
-
includes/Core/Importers/SchemaProImporter.php (added)
-
includes/Core/Importers/YoastImporter.php (modified) (8 diffs)
-
includes/Core/Lifecycle.php (modified) (1 diff)
-
includes/Core/RestApi/ImportExportController.php (modified) (14 diffs)
-
includes/Core/RestApi/PerformanceSettingsController.php (modified) (4 diffs)
-
includes/Core/RestApi/SystemController.php (modified) (3 diffs)
-
includes/Core/RestApi/VideoSitemapController.php (modified) (3 diffs)
-
includes/Modules/Indexing/Sitemaps/VideoDiscoveryService.php (added)
-
includes/Modules/Indexing/Sitemaps/VideoMetadataEnricher.php (added)
-
includes/Modules/Indexing/Sitemaps/VideoSitemapProvider.php (modified) (11 diffs)
-
includes/Modules/Indexing/Sitemaps/YouTubeVideoMetadataClient.php (added)
-
includes/Modules/Performance/ContentVisibilityLearningPolicy.php (added)
-
includes/Modules/Performance/ContentVisibilityLearningService.php (added)
-
includes/Modules/Performance/ContentVisibilityModule.php (modified) (5 diffs)
-
includes/Plugin.php (modified) (1 diff)
-
prorank-seo.php (modified) (2 diffs)
-
readme.txt (modified) (7 diffs)
Legend:
- Unmodified
- Added
- Removed
-
prorank-seo/trunk/build-info.json
r3647918 r3657955 1 1 { 2 "fingerprint": " f27b0796adecbda3",3 "timestamp": 178 6736956,2 "fingerprint": "373a79812d323dad", 3 "timestamp": 1787270324, 4 4 "commit": "local-package", 5 5 "source": "package" -
prorank-seo/trunk/build/admin.asset.php
r3647918 r3657955 1 1 <?php return array( 2 2 'dependencies' => array('wp-element', 'wp-i18n', 'wp-api-fetch', 'wp-data', 'wp-hooks', 'wp-plugins', 'wp-editor', 'wp-components', 'wp-block-editor', 'wp-blocks', 'wp-dom-ready', 'react', 'react-dom'), 3 'version' => ' 5d6e6b0a'3 'version' => '3d9dfd32' 4 4 ); -
prorank-seo/trunk/build/admin.js
r3647918 r3657955 1 import"./index- KZ_0GV60.js";import"./shims/wp-dom-ready.js";import"./react-runtime-BTtUcEhG.js";import"./shims/wp-api-fetch.js";import"./preload-helper-xg4QBr_M.js";import"./shims/wp-components.js";import"./shims/wp-i18n.js";1 import"./index-CTssvNCD.js";import"./shims/wp-dom-ready.js";import"./react-runtime-BTtUcEhG.js";import"./shims/wp-api-fetch.js";import"./preload-helper-xg4QBr_M.js";import"./shims/wp-components.js";import"./shims/wp-i18n.js"; -
prorank-seo/trunk/build/manifest.json
r3647918 r3657955 7 7 ] 8 8 }, 9 "_App- BzWPS4Dt.css": {10 "file": "App- BzWPS4Dt.css",11 "src": "_App- BzWPS4Dt.css"12 }, 13 "_App-D d3OPBZ_.js": {14 "file": "App-D d3OPBZ_.js",9 "_App-ConVbF5i.css": { 10 "file": "App-ConVbF5i.css", 11 "src": "_App-ConVbF5i.css" 12 }, 13 "_App-DktQ3r8n.js": { 14 "file": "App-DktQ3r8n.js", 15 15 "name": "App", 16 16 "isDynamicEntry": true, … … 20 20 "src/shims/wordpress/i18n.js", 21 21 "src/shims/wordpress/components.js", 22 "_index- KZ_0GV60.js",22 "_index-CTssvNCD.js", 23 23 "_NotificationContext-D9ZXsh12.js", 24 24 "src/shims/wordpress/api-fetch.js", … … 27 27 ], 28 28 "dynamicImports": [ 29 "_index- VnG1MpO_.js",29 "_index-DqieOZeU.js", 30 30 "src/admin/pages/OnPageSEO/index.jsx", 31 31 "src/admin/pages/Performance/index.tsx", … … 39 39 ], 40 40 "css": [ 41 "App- BzWPS4Dt.css"41 "App-ConVbF5i.css" 42 42 ] 43 43 }, … … 303 303 ] 304 304 }, 305 "_index- KZ_0GV60.js": {306 "file": "index- KZ_0GV60.js",305 "_index-CTssvNCD.js": { 306 "file": "index-CTssvNCD.js", 307 307 "name": "index", 308 308 "imports": [ … … 315 315 ], 316 316 "dynamicImports": [ 317 "_App-D d3OPBZ_.js"318 ] 319 }, 320 "_index- VnG1MpO_.js": {321 "file": "index- VnG1MpO_.js",317 "_App-DktQ3r8n.js" 318 ] 319 }, 320 "_index-DqieOZeU.js": { 321 "file": "index-DqieOZeU.js", 322 322 "name": "index", 323 323 "isDynamicEntry": true, … … 326 326 "_react-runtime-BTtUcEhG.js", 327 327 "src/shims/wordpress/i18n.js", 328 "_App-D d3OPBZ_.js",328 "_App-DktQ3r8n.js", 329 329 "src/shims/wordpress/api-fetch.js", 330 330 "src/shims/wordpress/components.js", … … 389 389 "isEntry": true, 390 390 "imports": [ 391 "_index- KZ_0GV60.js",391 "_index-CTssvNCD.js", 392 392 "src/shims/wordpress/dom-ready.js", 393 393 "_react-runtime-BTtUcEhG.js", … … 416 416 }, 417 417 "src/admin/pages/AgenticSEO/index.jsx": { 418 "file": "index- C0SSOFPG.js",418 "file": "index-Ds34fav7.js", 419 419 "name": "index", 420 420 "src": "src/admin/pages/AgenticSEO/index.jsx", … … 427 427 "_ProrankCard-Cy8PtoUx.js", 428 428 "_ModuleNav-BzP32gru.js", 429 "_index- KZ_0GV60.js",429 "_index-CTssvNCD.js", 430 430 "src/shims/wordpress/components.js", 431 431 "src/shims/wordpress/api-fetch.js", … … 452 452 }, 453 453 "src/admin/pages/Dashboard/OverviewTab.jsx": { 454 "file": "OverviewTab- x8UJXvIF.js",454 "file": "OverviewTab-Cge4lX_u.js", 455 455 "name": "OverviewTab", 456 456 "src": "src/admin/pages/Dashboard/OverviewTab.jsx", … … 463 463 "_SparklesIcon-BFJM747C.js", 464 464 "_GlobeAltIcon-BTNnoa4Q.js", 465 "_index- VnG1MpO_.js",465 "_index-DqieOZeU.js", 466 466 "_ArrowPathIcon-2QpMU9ev.js", 467 467 "_Alert-ErpVCn56.js", 468 468 "_CpuChipIcon-y9h_g1cN.js", 469 469 "_preload-helper-xg4QBr_M.js", 470 "_App-D d3OPBZ_.js",471 "src/shims/wordpress/components.js", 472 "_index- KZ_0GV60.js",470 "_App-DktQ3r8n.js", 471 "src/shims/wordpress/components.js", 472 "_index-CTssvNCD.js", 473 473 "src/shims/wordpress/api-fetch.js", 474 474 "src/shims/wordpress/dom-ready.js", … … 486 486 }, 487 487 "src/admin/pages/Dashboard/wizard/steps/CompetitorImportStep.jsx": { 488 "file": "CompetitorImportStep- zdIPNvmD.js",488 "file": "CompetitorImportStep-B42Pd5fP.js", 489 489 "name": "CompetitorImportStep", 490 490 "src": "src/admin/pages/Dashboard/wizard/steps/CompetitorImportStep.jsx", … … 495 495 "src/shims/wordpress/api-fetch.js", 496 496 "_Alert-ErpVCn56.js", 497 "_index-DqieOZeU.js", 497 498 "_UserGroupIcon-5Uw1hvDU.js", 498 499 "_LinkIcon-xYrk300N.js", 499 500 "_ArchiveBoxIcon-CkLKgbHA.js", 500 "_index-VnG1MpO_.js", 501 "_preload-helper-xg4QBr_M.js", 502 "_App-Dd3OPBZ_.js", 503 "src/shims/wordpress/components.js", 504 "_index-KZ_0GV60.js", 501 "_preload-helper-xg4QBr_M.js", 502 "_App-DktQ3r8n.js", 503 "src/shims/wordpress/components.js", 504 "_index-CTssvNCD.js", 505 505 "src/shims/wordpress/dom-ready.js", 506 506 "_NotificationContext-D9ZXsh12.js", … … 518 518 }, 519 519 "src/admin/pages/Dashboard/wizard/steps/ModulesStep.jsx": { 520 "file": "ModulesStep- 9l74MK7B.js",520 "file": "ModulesStep-DISyQc2j.js", 521 521 "name": "ModulesStep", 522 522 "src": "src/admin/pages/Dashboard/wizard/steps/ModulesStep.jsx", … … 528 528 "_Alert-ErpVCn56.js", 529 529 "_LoadingState-OlYlGWjY.js", 530 "_App-D d3OPBZ_.js",531 "_index- VnG1MpO_.js",530 "_App-DktQ3r8n.js", 531 "_index-DqieOZeU.js", 532 532 "_UserGroupIcon-5Uw1hvDU.js", 533 533 "_GlobeAltIcon-BTNnoa4Q.js", … … 537 537 "_preload-helper-xg4QBr_M.js", 538 538 "src/shims/wordpress/components.js", 539 "_index- KZ_0GV60.js",539 "_index-CTssvNCD.js", 540 540 "src/shims/wordpress/dom-ready.js", 541 541 "_NotificationContext-D9ZXsh12.js", … … 551 551 }, 552 552 "src/admin/pages/Dashboard/wizard/steps/OptimizationStep.jsx": { 553 "file": "OptimizationStep- Co76P-Yd.js",553 "file": "OptimizationStep-hUXCMlfx.js", 554 554 "name": "OptimizationStep", 555 555 "src": "src/admin/pages/Dashboard/wizard/steps/OptimizationStep.jsx", … … 560 560 "src/shims/wordpress/api-fetch.js", 561 561 "_Alert-ErpVCn56.js", 562 "_index- VnG1MpO_.js",562 "_index-DqieOZeU.js", 563 563 "_PhotoIcon-CpC2WInW.js", 564 564 "_ArchiveBoxIcon-CkLKgbHA.js", … … 568 568 "_ArrowPathIcon-2QpMU9ev.js", 569 569 "_preload-helper-xg4QBr_M.js", 570 "_App-D d3OPBZ_.js",571 "src/shims/wordpress/components.js", 572 "_index- KZ_0GV60.js",570 "_App-DktQ3r8n.js", 571 "src/shims/wordpress/components.js", 572 "_index-CTssvNCD.js", 573 573 "src/shims/wordpress/dom-ready.js", 574 574 "_NotificationContext-D9ZXsh12.js", … … 586 586 }, 587 587 "src/admin/pages/Dashboard/wizard/steps/ReadyStep.jsx": { 588 "file": "ReadyStep- DpYwwttD.js",588 "file": "ReadyStep-CxBDyjZm.js", 589 589 "name": "ReadyStep", 590 590 "src": "src/admin/pages/Dashboard/wizard/steps/ReadyStep.jsx", … … 600 600 "_SparklesIcon-BFJM747C.js", 601 601 "_QuestionMarkCircleIcon-DsPzk0r9.js", 602 "_index- VnG1MpO_.js",603 "_preload-helper-xg4QBr_M.js", 604 "_App-D d3OPBZ_.js",605 "src/shims/wordpress/components.js", 606 "_index- KZ_0GV60.js",602 "_index-DqieOZeU.js", 603 "_preload-helper-xg4QBr_M.js", 604 "_App-DktQ3r8n.js", 605 "src/shims/wordpress/components.js", 606 "_index-CTssvNCD.js", 607 607 "src/shims/wordpress/api-fetch.js", 608 608 "src/shims/wordpress/dom-ready.js", … … 633 633 }, 634 634 "src/admin/pages/Dashboard/wizard/steps/WelcomeStep.jsx": { 635 "file": "WelcomeStep- Cv1PDtMh.js",635 "file": "WelcomeStep-ByTBOSo2.js", 636 636 "name": "WelcomeStep", 637 637 "src": "src/admin/pages/Dashboard/wizard/steps/WelcomeStep.jsx", … … 648 648 "_GlobeAltIcon-BTNnoa4Q.js", 649 649 "_CheckCircleIcon-DXMJugIQ.js", 650 "_index- VnG1MpO_.js",651 "_preload-helper-xg4QBr_M.js", 652 "_App-D d3OPBZ_.js",653 "src/shims/wordpress/components.js", 654 "_index- KZ_0GV60.js",650 "_index-DqieOZeU.js", 651 "_preload-helper-xg4QBr_M.js", 652 "_App-DktQ3r8n.js", 653 "src/shims/wordpress/components.js", 654 "_index-CTssvNCD.js", 655 655 "src/shims/wordpress/dom-ready.js", 656 656 "_NotificationContext-D9ZXsh12.js", … … 666 666 }, 667 667 "src/admin/pages/ImportExport/index.jsx": { 668 "file": "index- BkRERGms.js",668 "file": "index-lawMXFif.js", 669 669 "name": "index", 670 670 "src": "src/admin/pages/ImportExport/index.jsx", … … 680 680 }, 681 681 "src/admin/pages/InternalLinking/index.jsx": { 682 "file": "index- zOCfL-MX.js",682 "file": "index-JKvGsgrN.js", 683 683 "name": "index", 684 684 "src": "src/admin/pages/InternalLinking/index.jsx", … … 738 738 }, 739 739 "src/admin/pages/Performance/index.tsx": { 740 "file": "index- Dsvvn0QB.js",740 "file": "index-P8uNM8IO.js", 741 741 "name": "index", 742 742 "src": "src/admin/pages/Performance/index.tsx", … … 757 757 }, 758 758 "src/admin/pages/SiteAudit/index.jsx": { 759 "file": "index- BMVrl8nb.js",759 "file": "index-Cc9E22V3.js", 760 760 "name": "index", 761 761 "src": "src/admin/pages/SiteAudit/index.jsx", … … 789 789 }, 790 790 "src/admin/pages/Support/index.jsx": { 791 "file": "index-Bi skG-Dh.js",791 "file": "index-BiBBcfit.js", 792 792 "name": "index", 793 793 "src": "src/admin/pages/Support/index.jsx", … … 803 803 "_Alert-ErpVCn56.js", 804 804 "_headerBadges-C-mhMdk2.js", 805 "_App-D d3OPBZ_.js",805 "_App-DktQ3r8n.js", 806 806 "_DocumentTextIcon-BMi-Bfn7.js", 807 807 "_QuestionMarkCircleIcon-DsPzk0r9.js", … … 812 812 "src/shims/wordpress/icons.js", 813 813 "_preload-helper-xg4QBr_M.js", 814 "_index- KZ_0GV60.js",814 "_index-CTssvNCD.js", 815 815 "src/shims/wordpress/dom-ready.js", 816 816 "src/shims/wordpress/compose.js" … … 821 821 }, 822 822 "src/admin/pages/TechnicalSEO/index.jsx": { 823 "file": "index- BCNX0d2j.js",823 "file": "index-CU1RdToR.js", 824 824 "name": "index", 825 825 "src": "src/admin/pages/TechnicalSEO/index.jsx", … … 837 837 "_LoadingState-OlYlGWjY.js", 838 838 "_Spinner-D8opFLQ3.js", 839 "_App-D d3OPBZ_.js",839 "_App-DktQ3r8n.js", 840 840 "_ArrowPathIcon-2QpMU9ev.js", 841 841 "_ChartBarIcon-PJPP-myE.js", … … 856 856 "_preload-helper-xg4QBr_M.js", 857 857 "src/shims/wordpress/components.js", 858 "_index- KZ_0GV60.js",858 "_index-CTssvNCD.js", 859 859 "src/shims/wordpress/dom-ready.js", 860 860 "src/shims/wordpress/icons.js", … … 1043 1043 ] 1044 1044 }, 1045 "src/frontend/content-visibility-learning.js": { 1046 "file": "content-visibility-learning.js", 1047 "name": "content-visibility-learning", 1048 "src": "src/frontend/content-visibility-learning.js", 1049 "isEntry": true 1050 }, 1045 1051 "src/shims/jsx-runtime-shim.js": { 1046 1052 "file": "shims/jsx-runtime.js", -
prorank-seo/trunk/includes/Admin/AdminBarMenu.php
r3602273 r3657955 203 203 'parent' => 'prorank-quick-actions', 204 204 'id' => 'prorank-internal-linking', 205 'title' => $this->menu_icon('dashicons-admin-links') . ' ' . __(' Internal Linking', 'prorank-seo'),205 'title' => $this->menu_icon('dashicons-admin-links') . ' ' . __('Link Operations', 'prorank-seo'), 206 206 'href' => admin_url('admin.php?page=prorank-internal-linking'), 207 207 ]); -
prorank-seo/trunk/includes/Admin/MenuHandler.php
r3586375 r3657955 213 213 'prorank-seo', 214 214 'prorank-internal-linking', 215 ' Internal Linking',216 ' Internal Linking',215 'Link Operations', 216 'Link Operations', 217 217 'manage_options', 218 218 [$this, 'render_internal_linking_page'], … … 440 440 public function render_internal_linking_page(): void { 441 441 echo '<div id="prorank-internal-linking" class="wrap prorank-seo-admin-page prorank-internal-linking-page">'; 442 echo '<h1>' . esc_html ('Internal Linking') . '</h1>';442 echo '<h1>' . esc_html__('Link Operations', 'prorank-seo') . '</h1>'; 443 443 echo '<div class="prorank-admin-container" id="prorank-admin-root">'; 444 444 echo '<!-- React component will mount here -->'; -
prorank-seo/trunk/includes/Core/Compatibility/CompetitorDetector.php
r3509876 r3657955 413 413 $active = []; 414 414 foreach (self::$internal_linking_competitor_plugins as $plugin_file => $plugin_name) { 415 if ( is_plugin_active($plugin_file)) {415 if ($this->is_plugin_file_active($plugin_file)) { 416 416 $active[] = $plugin_name; 417 417 } … … 435 435 436 436 foreach (self::$competitor_plugins as $plugin_file => $plugin_name) { 437 if ( is_plugin_active($plugin_file)) {437 if ($this->is_plugin_file_active($plugin_file)) { 438 438 $active_competitors[] = $plugin_name; 439 439 } … … 456 456 $active = []; 457 457 foreach (self::$seo_competitor_plugins as $plugin_file => $plugin_name) { 458 if ( is_plugin_active($plugin_file)) {458 if ($this->is_plugin_file_active($plugin_file)) { 459 459 $active[] = $plugin_name; 460 460 } … … 477 477 $active = []; 478 478 foreach (self::$performance_competitor_plugins as $plugin_file => $plugin_name) { 479 if ( is_plugin_active($plugin_file)) {479 if ($this->is_plugin_file_active($plugin_file)) { 480 480 $active[] = $plugin_name; 481 481 } … … 497 497 $active = []; 498 498 foreach (self::$cache_asset_competitor_plugins as $plugin_file => $plugin_name) { 499 if ( is_plugin_active($plugin_file)) {499 if ($this->is_plugin_file_active($plugin_file)) { 500 500 $active[] = $plugin_name; 501 501 } … … 517 517 $active = []; 518 518 foreach (self::$image_optimization_competitor_plugins as $plugin_file => $plugin_name) { 519 if ( is_plugin_active($plugin_file)) {519 if ($this->is_plugin_file_active($plugin_file)) { 520 520 $active[] = $plugin_name; 521 521 } … … 539 539 540 540 foreach (self::$competitor_plugins as $plugin_file => $plugin_name) { 541 if ( is_plugin_active($plugin_file)) {541 if ($this->is_plugin_file_active($plugin_file)) { 542 542 $active_competitors[] = [ 543 543 'name' => $plugin_name, … … 567 567 $active = []; 568 568 foreach (self::$seo_competitor_plugins as $plugin_file => $plugin_name) { 569 if ( is_plugin_active($plugin_file)) {569 if ($this->is_plugin_file_active($plugin_file)) { 570 570 $active[] = [ 571 571 'name' => $plugin_name, … … 595 595 $active = []; 596 596 foreach (self::$performance_competitor_plugins as $plugin_file => $plugin_name) { 597 if ( is_plugin_active($plugin_file)) {597 if ($this->is_plugin_file_active($plugin_file)) { 598 598 $active[] = [ 599 599 'name' => $plugin_name, … … 680 680 } 681 681 682 if ($this->has_active_unprofiled_cache_asset_competitors()) { 683 $suppressed = array_merge($suppressed, $this->get_default_cache_asset_suppressions()); 684 } 682 $suppressed = array_merge($suppressed, $this->get_catalog_safe_mode_suppressions()); 685 683 686 684 if ($this->has_active_image_optimization_competitors()) { … … 692 690 693 691 /** 694 * Broad suppressions for cache/asset plugins we do not profile individually. 692 * Capability-level suppressions for known performance plugins. Unlike the 693 * old all-or-nothing fallback, a cache-only plugin does not disable 694 * ProRank's unrelated CSS, JavaScript, image, or database features. 695 695 * 696 696 * @return array<int,string> 697 697 */ 698 private function get_default_cache_asset_suppressions(): array { 699 return [ 700 'cache', 701 'modern_cache', 702 'browser_cache', 703 'cache_preload', 704 'resource_hints', 705 'speculation_rules', 706 'css_opt', 707 'js_opt', 708 'lazyload', 709 'critical_css', 710 'unused_css', 711 'js_minify', 712 'html_minify', 713 'partytown', 714 'inp_optimizer', 715 'lcp_beacon', 716 'lcp_optimizer', 717 'font_optimization', 718 'font_subsetting', 719 'heartbeat_control', 720 'database_optimization', 721 ]; 722 } 723 724 /** 725 * Whether any active cache/asset competitor lacks a dedicated feature map. 726 * 727 * @return bool 728 */ 729 private function has_active_unprofiled_cache_asset_competitors(): bool { 730 if (!function_exists('is_plugin_active')) { 731 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 732 } 733 734 foreach (self::$cache_asset_competitor_plugins as $plugin_file => $plugin_name) { 735 unset($plugin_name); 736 if (!is_plugin_active($plugin_file)) { 737 continue; 738 } 739 740 if (in_array($plugin_file, ['wp-rocket/wp-rocket.php', 'autoptimize/autoptimize.php', 'perfmatters/perfmatters.php', 'wp-meteor/wp-meteor.php'], true)) { 741 continue; 742 } 743 744 return true; 745 } 746 747 return false; 698 private function get_catalog_safe_mode_suppressions(): array { 699 $suppressed = []; 700 foreach (CompatibilityProfileCatalog::profiles() as $plugin_file => $modules) { 701 if ($this->is_plugin_file_active($plugin_file)) { 702 $suppressed = array_merge($suppressed, $modules); 703 } 704 } 705 706 return array_values(array_unique($suppressed)); 748 707 } 749 708 … … 942 901 } 943 902 944 return is_plugin_active($plugin_file); 903 if (is_plugin_active($plugin_file)) { 904 return true; 905 } 906 907 return function_exists('is_plugin_active_for_network') 908 && is_plugin_active_for_network($plugin_file); 945 909 } 946 910 -
prorank-seo/trunk/includes/Core/Importers/AIOSEOImporter.php
r3488385 r3657955 151 151 152 152 if (!empty($post->title)) { 153 update_post_meta($post_id, '_prorank_seo_title', sanitize_text_field($post->title));153 $this->write_post_meta($post_id, '_prorank_seo_title', sanitize_text_field($post->title)); 154 154 } 155 155 if (!empty($post->description)) { 156 update_post_meta($post_id, '_prorank_seo_description', sanitize_textarea_field($post->description));156 $this->write_post_meta($post_id, '_prorank_seo_description', sanitize_textarea_field($post->description)); 157 157 } 158 158 if (!empty($post->keyphrases)) { … … 174 174 } 175 175 if (!empty($post->canonical_url)) { 176 update_post_meta($post_id, '_prorank_seo_canonical_url', esc_url_raw($post->canonical_url));176 $this->write_post_meta($post_id, '_prorank_seo_canonical_url', esc_url_raw($post->canonical_url)); 177 177 } 178 178 if (!empty($post->og_title)) { 179 update_post_meta($post_id, '_prorank_og_title', sanitize_text_field($post->og_title));179 $this->write_post_meta($post_id, '_prorank_og_title', sanitize_text_field($post->og_title)); 180 180 } 181 181 if (!empty($post->og_description)) { 182 update_post_meta($post_id, '_prorank_og_description', sanitize_textarea_field($post->og_description));182 $this->write_post_meta($post_id, '_prorank_og_description', sanitize_textarea_field($post->og_description)); 183 183 } 184 184 if (!empty($post->twitter_title)) { 185 update_post_meta($post_id, '_prorank_twitter_title', sanitize_text_field($post->twitter_title));185 $this->write_post_meta($post_id, '_prorank_twitter_title', sanitize_text_field($post->twitter_title)); 186 186 } 187 187 if (!empty($post->twitter_description)) { 188 update_post_meta($post_id, '_prorank_twitter_description', sanitize_textarea_field($post->twitter_description));188 $this->write_post_meta($post_id, '_prorank_twitter_description', sanitize_textarea_field($post->twitter_description)); 189 189 } 190 190 … … 199 199 } 200 200 if (!empty($robots)) { 201 update_post_meta($post_id, '_prorank_meta_robots', $robots);201 $this->write_post_meta($post_id, '_prorank_meta_robots', $robots); 202 202 } 203 203 } … … 226 226 227 227 if (!empty($value)) { 228 update_post_meta($post_id, $prorank_key, $this->sanitize_meta_value($value));228 $this->write_post_meta($post_id, $prorank_key, $this->sanitize_meta_value($value)); 229 229 } 230 230 } … … 241 241 } 242 242 if (!empty($robots)) { 243 update_post_meta($post_id, '_prorank_meta_robots', $robots);243 $this->write_post_meta($post_id, '_prorank_meta_robots', $robots); 244 244 } 245 245 … … 249 249 250 250 if (!empty($value)) { 251 update_post_meta($post_id, $prorank_key, $this->sanitize_meta_value($value));251 $this->write_post_meta($post_id, $prorank_key, $this->sanitize_meta_value($value)); 252 252 } 253 253 } … … 279 279 280 280 if (!empty($term_data->title)) { 281 update_term_meta($term_id, '_prorank_seo_title', sanitize_text_field($term_data->title));281 $this->write_term_meta($term_id, '_prorank_seo_title', sanitize_text_field($term_data->title)); 282 282 } 283 283 if (!empty($term_data->description)) { 284 update_term_meta($term_id, '_prorank_seo_description', sanitize_textarea_field($term_data->description));284 $this->write_term_meta($term_id, '_prorank_seo_description', sanitize_textarea_field($term_data->description)); 285 285 } 286 286 if (!empty($term_data->canonical_url)) { 287 update_term_meta($term_id, '_prorank_seo_canonical', esc_url_raw($term_data->canonical_url));287 $this->write_term_meta($term_id, '_prorank_seo_canonical', esc_url_raw($term_data->canonical_url)); 288 288 } 289 289 if (!empty($term_data->og_title)) { 290 update_term_meta($term_id, '_prorank_seo_og_title', sanitize_text_field($term_data->og_title));290 $this->write_term_meta($term_id, '_prorank_seo_og_title', sanitize_text_field($term_data->og_title)); 291 291 } 292 292 if (!empty($term_data->og_description)) { 293 update_term_meta($term_id, '_prorank_seo_og_description', sanitize_textarea_field($term_data->og_description));293 $this->write_term_meta($term_id, '_prorank_seo_og_description', sanitize_textarea_field($term_data->og_description)); 294 294 } 295 295 } -
prorank-seo/trunk/includes/Core/Importers/BaseImporter.php
r3571125 r3657955 14 14 if ( ! defined( 'ABSPATH' ) ) { 15 15 exit; 16 } 17 18 if (!class_exists(ImportTransaction::class)) { 19 require_once __DIR__ . '/ImportTransaction.php'; 16 20 } 17 21 … … 40 44 'schemas' => 0, 41 45 'schemas_skipped' => 0, 46 'skipped' => 0, 42 47 'errors' => [], 43 48 ]; … … 49 54 */ 50 55 protected array $import_types = ['meta', 'settings', 'redirects', 'schema', 'locations']; 56 57 protected bool $dry_run = false; 58 protected ?ImportTransaction $transaction = null; 59 private int $planned_changes = 0; 60 /** @var array<int,array<string,mixed>> */ 61 private array $preview_changes = []; 51 62 52 63 /** … … 55 66 * @param array $types Import types to process 56 67 */ 57 public function __construct(array $types = [] ) {68 public function __construct(array $types = [], bool $dry_run = false, ?ImportTransaction $transaction = null) { 58 69 if (!empty($types)) { 59 70 $this->import_types = $types; 60 71 } 72 $this->dry_run = $dry_run; 73 $this->transaction = $transaction; 61 74 } 62 75 … … 88 101 89 102 foreach ($post_types as $post_type) { 90 $posts = get_posts([ 91 'post_type' => $post_type, 92 'posts_per_page' => -1, 93 'post_status' => 'any', 94 ]); 95 96 foreach ($posts as $post) { 97 $this->import_single_post_meta($post->ID); 98 if ($post->post_type === 'page') { 99 $this->stats['pages']++; 100 } else { 101 $this->stats['posts']++; 103 $page = 1; 104 do { 105 $posts = get_posts([ 106 'post_type' => $post_type, 107 'posts_per_page' => 500, 108 'paged' => $page, 109 'post_status' => 'any', 110 'orderby' => 'ID', 111 'order' => 'ASC', 112 'no_found_rows' => true, 113 ]); 114 foreach ($posts as $post) { 115 $this->import_single_post_meta($post->ID); 116 if ($post->post_type === 'page') { 117 $this->stats['pages']++; 118 } else { 119 $this->stats['posts']++; 120 } 102 121 } 103 } 122 $page++; 123 } while (count($posts) === 500); 104 124 } 105 125 } … … 126 146 127 147 foreach ($taxonomies as $taxonomy) { 128 $terms = get_terms([ 129 'taxonomy' => $taxonomy, 130 'hide_empty' => false, 131 ]); 132 133 if (is_wp_error($terms)) { 134 continue; 135 } 136 137 foreach ($terms as $term) { 138 $this->import_single_term_meta($term->term_id); 139 $this->stats['terms']++; 140 } 148 $offset = 0; 149 do { 150 $terms = get_terms([ 151 'taxonomy' => $taxonomy, 152 'hide_empty' => false, 153 'number' => 500, 154 'offset' => $offset, 155 'orderby' => 'term_id', 156 'order' => 'ASC', 157 ]); 158 if (is_wp_error($terms)) { 159 break; 160 } 161 foreach ($terms as $term) { 162 $this->import_single_term_meta($term->term_id); 163 $this->stats['terms']++; 164 } 165 $offset += count($terms); 166 } while (count($terms) === 500); 141 167 } 142 168 } … … 217 243 public function get_stats(): array { 218 244 return $this->stats; 245 } 246 247 /** @return array{dry_run:bool,total_changes:int,sample:array<int,array<string,mixed>>} */ 248 public function get_execution_summary(): array { 249 return [ 250 'dry_run' => $this->dry_run, 251 'total_changes' => $this->planned_changes, 252 'sample' => $this->preview_changes, 253 ]; 219 254 } 220 255 … … 254 289 } 255 290 291 /** @param mixed $value */ 292 protected function write_post_meta(int $post_id, string $key, $value): bool { 293 $current = get_post_meta($post_id, $key, true); 294 if ($current === $value) { 295 return true; 296 } 297 $this->plan_change('post_meta', $key, $post_id); 298 if ($this->dry_run) { 299 return true; 300 } 301 if ($this->transaction) { 302 $this->transaction->record_post_meta($post_id, $key); 303 } 304 $updated = update_post_meta($post_id, $key, $value); 305 return $updated !== false || get_post_meta($post_id, $key, true) === $value; 306 } 307 308 /** @param mixed $value */ 309 protected function write_term_meta(int $term_id, string $key, $value): bool { 310 $current = get_term_meta($term_id, $key, true); 311 if ($current === $value) { 312 return true; 313 } 314 $this->plan_change('term_meta', $key, $term_id); 315 if ($this->dry_run) { 316 return true; 317 } 318 if ($this->transaction) { 319 $this->transaction->record_term_meta($term_id, $key); 320 } 321 $updated = update_term_meta($term_id, $key, $value); 322 return $updated !== false || get_term_meta($term_id, $key, true) === $value; 323 } 324 325 /** @param mixed $value */ 326 protected function write_option(string $key, $value, bool $count_change = true): bool { 327 $sentinel = new \stdClass(); 328 $current = get_option($key, $sentinel); 329 if ($current !== $sentinel && $current === $value) { 330 return true; 331 } 332 if ($count_change) { 333 $this->plan_change('option', $key, null); 334 } 335 if ($this->dry_run) { 336 return true; 337 } 338 if ($this->transaction) { 339 $this->transaction->record_option($key); 340 } 341 $updated = update_option($key, $value); 342 return $updated !== false || get_option($key, $sentinel) === $value; 343 } 344 345 private function plan_change(string $kind, string $key, ?int $object_id): void { 346 $this->planned_changes++; 347 if (count($this->preview_changes) >= 100) { 348 return; 349 } 350 $change = ['kind' => $kind, 'key' => $key]; 351 if ($object_id !== null) { 352 $change['object_id'] = $object_id; 353 } 354 $this->preview_changes[] = $change; 355 } 356 357 /** @param array<string,mixed> $schema */ 358 protected function save_legacy_schema(int $post_id, array $schema): bool { 359 $existing = get_post_meta($post_id, '_prorank_schema_data', true); 360 if (is_array($existing) && !empty($existing['type'])) { 361 return false; 362 } 363 $encoded = wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 364 if (!is_string($encoded) || $encoded === '') { 365 return false; 366 } 367 return $this->write_post_meta($post_id, '_prorank_schema_data', [ 368 'type' => 'Custom', 369 'properties' => [], 370 'custom_json' => $encoded, 371 ]); 372 } 373 256 374 /** 257 375 * Merge array data into an option without removing existing keys. … … 271 389 } 272 390 273 update_option($option_key, array_merge($existing, $incoming));391 $this->write_option($option_key, array_merge($existing, $incoming)); 274 392 } 275 393 … … 329 447 } 330 448 331 update_option('prorank_seo_site_basics', array_merge($existing, $normalized));449 $this->write_option('prorank_seo_site_basics', array_merge($existing, $normalized)); 332 450 } 333 451 … … 382 500 if ($source === '' || $target === '') { 383 501 return false; 502 } 503 504 $this->plan_change('redirect', $source, null); 505 if ($this->dry_run) { 506 return true; 384 507 } 385 508 … … 394 517 ) 395 518 ); 519 if ($this->transaction) { 520 $this->transaction->record_redirect($table, $source_candidates, $existing_id ? (int) $existing_id : null); 521 } 396 522 397 523 $data = [ … … 450 576 'notes' => $notes, 451 577 ]; 452 update_option('prorank_seo_redirects', $redirects); 453 454 return true; 578 return $this->write_option('prorank_seo_redirects', $redirects, false); 455 579 } 456 580 … … 627 751 'updated_at' => current_time('mysql'), 628 752 ]); 629 update_option('prorank_local_seo_locations', $locations);753 $this->write_option('prorank_local_seo_locations', $locations); 630 754 631 755 return true; … … 642 766 643 767 $locations[$location_id] = $sanitized; 644 update_option('prorank_local_seo_locations', $locations);768 $this->write_option('prorank_local_seo_locations', $locations); 645 769 646 770 return true; -
prorank-seo/trunk/includes/Core/Importers/RankMathImporter.php
r3571125 r3657955 177 177 } 178 178 179 update_post_meta($post_id, $prorank_key, $this->sanitize_meta_value(179 $this->write_post_meta($post_id, $prorank_key, $this->sanitize_meta_value( 180 180 $this->normalize_rankmath_post_meta_value($value, $rm_key, $post_id) 181 181 )); … … 185 185 $robots_directives = array_values(array_unique(array_filter($robots_directives))); 186 186 if (!empty($robots_directives)) { 187 update_post_meta($post_id, '_prorank_meta_robots', $robots_directives);187 $this->write_post_meta($post_id, '_prorank_meta_robots', $robots_directives); 188 188 } 189 189 … … 204 204 $seo_score = get_post_meta($post_id, 'rank_math_seo_score', true); 205 205 if ($seo_score) { 206 update_post_meta($post_id, '_prorank_seo_score', intval($seo_score));206 $this->write_post_meta($post_id, '_prorank_seo_score', intval($seo_score)); 207 207 } 208 208 … … 213 213 } 214 214 if (!empty($schema_type)) { 215 update_post_meta($post_id, '_prorank_seo_schema_type', $this->sanitize_meta_value($schema_type));215 $this->write_post_meta($post_id, '_prorank_seo_schema_type', $this->sanitize_meta_value($schema_type)); 216 216 } 217 217 218 218 $schema_data = get_post_meta($post_id, 'rank_math_schema', true); 219 219 if (!empty($schema_data)) { 220 update_post_meta($post_id, '_prorank_seo_schema', $this->sanitize_meta_value($schema_data));220 $this->write_post_meta($post_id, '_prorank_seo_schema', $this->sanitize_meta_value($schema_data)); 221 221 } 222 222 … … 225 225 // keep their structured data instead of falling back to defaults. 226 226 $this->import_rankmath_schema_nodes($post_id); 227 $this->import_rankmath_video_fields($post_id); 228 } 229 230 /** 231 * Preserve VideoObject fields used by ProRank's video sitemap. 232 * 233 * Existing ProRank fields always win, so rerunning a migration cannot 234 * overwrite edits made after the original import. 235 */ 236 private function import_rankmath_video_fields(int $post_id): void { 237 foreach ($this->get_rankmath_schema_nodes($post_id) as $node) { 238 $payload = $node['payload']; 239 $raw_type = $payload['@type'] ?? $node['key_type']; 240 if (is_array($raw_type)) { 241 $raw_type = reset($raw_type); 242 } 243 if (!is_string($raw_type) || !in_array(strtolower($raw_type), ['video', 'videoobject'], true)) { 244 continue; 245 } 246 247 $content_url = $this->extract_rankmath_schema_text($payload, ['contentUrl']); 248 $embed_url = $this->extract_rankmath_schema_text($payload, ['embedUrl']); 249 $video_url = $content_url !== '' ? $content_url : $embed_url; 250 $thumbnail = $this->extract_rankmath_entity_value($payload['thumbnailUrl'] ?? null, ['url', 'contentUrl']); 251 $duration = $this->rankmath_video_duration_seconds($payload['duration'] ?? null); 252 $upload_date = $this->normalize_rankmath_video_date($payload['uploadDate'] ?? null); 253 254 $this->set_video_meta_if_empty($post_id, '_prorank_seo_video_url', esc_url_raw($video_url)); 255 $this->set_video_meta_if_empty( 256 $post_id, 257 '_prorank_seo_video_title', 258 sanitize_text_field($this->extract_rankmath_schema_text($payload, ['name', 'headline'])) 259 ); 260 $this->set_video_meta_if_empty( 261 $post_id, 262 '_prorank_seo_video_description', 263 sanitize_textarea_field($this->extract_rankmath_schema_text($payload, ['description'])) 264 ); 265 $this->set_video_meta_if_empty($post_id, '_prorank_seo_video_thumbnail', esc_url_raw($thumbnail)); 266 if ($duration > 0) { 267 $this->set_video_meta_if_empty($post_id, '_prorank_seo_video_duration', $duration); 268 } 269 $this->set_video_meta_if_empty($post_id, '_prorank_seo_video_upload_date', $upload_date); 270 if (strpos($video_url, '/shorts/') !== false) { 271 $this->set_video_meta_if_empty($post_id, '_prorank_seo_video_is_short', 'yes'); 272 } 273 274 return; 275 } 276 } 277 278 /** 279 * Set imported video metadata only when ProRank has no meaningful value. 280 * 281 * @param mixed $value Sanitized value. 282 */ 283 private function set_video_meta_if_empty(int $post_id, string $key, $value): void { 284 if ($value === '' || $value === null || get_post_meta($post_id, $key, true) !== '') { 285 return; 286 } 287 288 $this->write_post_meta($post_id, $key, $value); 289 } 290 291 /** 292 * Convert ISO-8601 or numeric video durations to sitemap seconds. 293 * 294 * @param mixed $duration Rank Math duration. 295 */ 296 private function rankmath_video_duration_seconds($duration): int { 297 if (is_numeric($duration)) { 298 return max(0, min(28800, (int) $duration)); 299 } 300 if (!is_string($duration) || $duration === '') { 301 return 0; 302 } 303 304 try { 305 $interval = new \DateInterval(strtoupper(trim($duration))); 306 } catch (\Throwable $exception) { 307 return 0; 308 } 309 310 return max(0, min(28800, ($interval->d * 86400) + ($interval->h * 3600) + ($interval->i * 60) + $interval->s)); 311 } 312 313 /** 314 * Convert an imported date to WordPress's stored datetime shape. 315 * 316 * @param mixed $date Rank Math date value. 317 */ 318 private function normalize_rankmath_video_date($date): string { 319 if (!is_string($date) || trim($date) === '') { 320 return ''; 321 } 322 323 $timestamp = strtotime($date); 324 return $timestamp === false ? '' : gmdate('Y-m-d H:i:s', $timestamp); 325 } 326 327 /** 328 * Keep only public post types that exist on the destination site. 329 * 330 * @param mixed $post_types Rank Math setting value. 331 * @return array<int,string> 332 */ 333 private function normalize_rankmath_video_post_types($post_types): array { 334 if (!is_array($post_types) || !function_exists('get_post_types')) { 335 return []; 336 } 337 338 $available = get_post_types(['public' => true], 'names'); 339 $normalize = static function (array $values): array { 340 $normalized = []; 341 foreach ($values as $value) { 342 if (is_scalar($value)) { 343 $slug = sanitize_key((string) $value); 344 if ($slug !== '') { 345 $normalized[] = $slug; 346 } 347 } 348 } 349 return array_values(array_unique($normalized)); 350 }; 351 $available = $normalize(is_array($available) ? $available : []); 352 $requested = $normalize($post_types); 353 354 return array_values(array_intersect($requested, $available)); 227 355 } 228 356 … … 276 404 } 277 405 278 update_post_meta($post_id, '_prorank_schema_data', $mapped);406 $this->write_post_meta($post_id, '_prorank_schema_data', $mapped); 279 407 $this->stats['schemas']++; 280 408 } … … 685 813 686 814 if (!empty($value)) { 687 update_term_meta($term_id, $prorank_key, $this->sanitize_meta_value($value));815 $this->write_term_meta($term_id, $prorank_key, $this->sanitize_meta_value($value)); 688 816 } 689 817 } … … 698 826 699 827 if (!empty($robots_directives)) { 700 update_term_meta($term_id, '_prorank_seo_robots', $robots_directives);828 $this->write_term_meta($term_id, '_prorank_seo_robots', $robots_directives); 701 829 if (in_array('noindex', $robots_directives, true)) { 702 update_term_meta($term_id, '_prorank_seo_noindex', 'noindex');703 update_term_meta($term_id, '_prorank_seo_robots_noindex', '1');830 $this->write_term_meta($term_id, '_prorank_seo_noindex', 'noindex'); 831 $this->write_term_meta($term_id, '_prorank_seo_robots_noindex', '1'); 704 832 } 705 833 if (in_array('nofollow', $robots_directives, true)) { 706 update_term_meta($term_id, '_prorank_seo_nofollow', '1');834 $this->write_term_meta($term_id, '_prorank_seo_nofollow', '1'); 707 835 } 708 836 } … … 889 1017 $this->stats['settings']++; 890 1018 } 1019 1020 $video_post_types = $this->normalize_rankmath_video_post_types($sitemap['video_sitemap_post_type'] ?? []); 1021 $rankmath_modules = get_option('rank_math_modules', []); 1022 $video_settings = [ 1023 'enabled' => is_array($rankmath_modules) && in_array('video-sitemap', $rankmath_modules, true), 1024 'hide_sitemap' => !empty($sitemap['hide_video_sitemap']), 1025 'auto_detect' => true, 1026 ]; 1027 if (!empty($video_post_types)) { 1028 $video_settings['post_types'] = $video_post_types; 1029 } 1030 if (isset($sitemap['video_sitemap_custom_fields']) && is_string($sitemap['video_sitemap_custom_fields'])) { 1031 $video_settings['custom_fields'] = sanitize_textarea_field($sitemap['video_sitemap_custom_fields']); 1032 } 1033 if (isset($sitemap['youtube_api_key']) && is_string($sitemap['youtube_api_key'])) { 1034 $video_settings['youtube_api_key'] = sanitize_text_field($sitemap['youtube_api_key']); 1035 } 1036 $this->merge_option_array('prorank_seo_video_sitemap_settings', $video_settings); 891 1037 } 892 1038 -
prorank-seo/trunk/includes/Core/Importers/SEOPressImporter.php
r3488385 r3657955 134 134 135 135 if (!empty($value)) { 136 update_post_meta($post_id, $prorank_key, $this->sanitize_meta_value($value));136 $this->write_post_meta($post_id, $prorank_key, $this->sanitize_meta_value($value)); 137 137 } 138 138 } … … 163 163 } 164 164 if (!empty($robots)) { 165 update_post_meta($post_id, '_prorank_meta_robots', $robots);165 $this->write_post_meta($post_id, '_prorank_meta_robots', $robots); 166 166 } 167 167 … … 188 188 $score = get_post_meta($post_id, '_seopress_analysis_score', true); 189 189 if ($score) { 190 update_post_meta($post_id, '_prorank_seo_score', intval($score));190 $this->write_post_meta($post_id, '_prorank_seo_score', intval($score)); 191 191 } 192 192 } … … 212 212 213 213 if (!empty($value)) { 214 update_term_meta($term_id, $prorank_key, $this->sanitize_meta_value($value));214 $this->write_term_meta($term_id, $prorank_key, $this->sanitize_meta_value($value)); 215 215 } 216 216 } … … 219 219 $robots_noindex = get_term_meta($term_id, '_seopress_robots_index', true); 220 220 if ($robots_noindex === 'yes') { 221 update_term_meta($term_id, '_prorank_seo_noindex', 'noindex');221 $this->write_term_meta($term_id, '_prorank_seo_noindex', 'noindex'); 222 222 } 223 223 } -
prorank-seo/trunk/includes/Core/Importers/YoastImporter.php
r3488385 r3657955 134 134 135 135 if (!empty($value)) { 136 update_post_meta($post_id, $prorank_key, $this->sanitize_meta_value($value));136 $this->write_post_meta($post_id, $prorank_key, $this->sanitize_meta_value($value)); 137 137 } 138 138 } … … 149 149 } 150 150 if (!empty($robots)) { 151 update_post_meta($post_id, '_prorank_meta_robots', $robots);151 $this->write_post_meta($post_id, '_prorank_meta_robots', $robots); 152 152 } 153 153 … … 189 189 $primary_category = get_post_meta($post_id, '_yoast_wpseo_primary_category', true); 190 190 if ($primary_category) { 191 update_post_meta($post_id, '_prorank_seo_primary_category', $primary_category);191 $this->write_post_meta($post_id, '_prorank_seo_primary_category', $primary_category); 192 192 } 193 193 … … 195 195 $breadcrumb_title = get_post_meta($post_id, '_yoast_wpseo_bctitle', true); 196 196 if ($breadcrumb_title) { 197 update_post_meta($post_id, '_prorank_seo_breadcrumb_title', $breadcrumb_title);197 $this->write_post_meta($post_id, '_prorank_seo_breadcrumb_title', $breadcrumb_title); 198 198 } 199 199 … … 201 201 $content_score = get_post_meta($post_id, '_yoast_wpseo_content_score', true); 202 202 if ($content_score !== '') { 203 update_post_meta($post_id, '_prorank_seo_score', intval($content_score));203 $this->write_post_meta($post_id, '_prorank_seo_score', intval($content_score)); 204 204 } 205 205 … … 207 207 $schema_page_type = get_post_meta($post_id, '_yoast_wpseo_schema_page_type', true); 208 208 if (!empty($schema_page_type)) { 209 update_post_meta($post_id, '_prorank_seo_schema_type', sanitize_text_field((string) $schema_page_type));209 $this->write_post_meta($post_id, '_prorank_seo_schema_type', sanitize_text_field((string) $schema_page_type)); 210 210 } 211 211 } … … 228 228 // Map Yoast term meta to ProRank 229 229 if (isset($meta['wpseo_title'])) { 230 update_term_meta($term_id, '_prorank_seo_title', $meta['wpseo_title']);230 $this->write_term_meta($term_id, '_prorank_seo_title', $meta['wpseo_title']); 231 231 } 232 232 if (isset($meta['wpseo_desc'])) { 233 update_term_meta($term_id, '_prorank_seo_description', $meta['wpseo_desc']);233 $this->write_term_meta($term_id, '_prorank_seo_description', $meta['wpseo_desc']); 234 234 } 235 235 if (isset($meta['wpseo_canonical'])) { 236 update_term_meta($term_id, '_prorank_seo_canonical', $meta['wpseo_canonical']);236 $this->write_term_meta($term_id, '_prorank_seo_canonical', $meta['wpseo_canonical']); 237 237 } 238 238 if (isset($meta['wpseo_noindex'])) { 239 239 $noindex = $meta['wpseo_noindex'] === 'noindex' ? 'noindex' : 'index'; 240 update_term_meta($term_id, '_prorank_seo_noindex', $noindex);240 $this->write_term_meta($term_id, '_prorank_seo_noindex', $noindex); 241 241 } 242 242 } … … 247 247 $title = get_term_meta($term_id, 'wpseo_title', true); 248 248 if ($title) { 249 update_term_meta($term_id, '_prorank_seo_title', $title);249 $this->write_term_meta($term_id, '_prorank_seo_title', $title); 250 250 } 251 251 252 252 $desc = get_term_meta($term_id, 'wpseo_desc', true); 253 253 if ($desc) { 254 update_term_meta($term_id, '_prorank_seo_description', $desc);254 $this->write_term_meta($term_id, '_prorank_seo_description', $desc); 255 255 } 256 256 } -
prorank-seo/trunk/includes/Core/Lifecycle.php
r3647918 r3657955 69 69 'prorank_cleanup_export', 70 70 'prorank_complete_audit', 71 'prorank_content_visibility_evaluate', 71 72 'prorank_critical_css_regenerate', 72 73 'prorank_css_cache_cleanup', -
prorank-seo/trunk/includes/Core/RestApi/ImportExportController.php
r3571125 r3657955 15 15 use ProRank\SEO\Core\ModuleManager; 16 16 use ProRank\SEO\Core\LicenseManager; 17 use ProRank\SEO\Core\Importers\ImportTransaction; 17 18 use WP_REST_Controller; 18 19 use WP_REST_Server; … … 154 155 'description' => __('Competitor plugin to import from', 'prorank-seo'), 155 156 'type' => 'string', 156 'enum' => ['yoast', 'rankmath', 'aioseo', 'seopress' ],157 'enum' => ['yoast', 'rankmath', 'aioseo', 'seopress', 'redirection', 'schema-pro', 'aio-rich-snippets'], 157 158 'required' => true, 158 159 ], … … 170 171 ]); 171 172 173 register_rest_route($this->namespace, '/' . $this->rest_base . '/competitor/preview', [ 174 [ 175 'methods' => WP_REST_Server::CREATABLE, 176 'callback' => [$this, 'preview_competitor_import'], 177 'permission_callback' => [$this, 'import_permissions_check'], 178 'args' => [ 179 'source' => [ 180 'type' => 'string', 181 'enum' => ['yoast', 'rankmath', 'aioseo', 'seopress', 'redirection', 'schema-pro', 'aio-rich-snippets'], 182 'required' => true, 183 ], 184 'types' => [ 185 'type' => 'array', 186 'items' => ['type' => 'string', 'enum' => ['meta', 'settings', 'redirects', 'schema', 'locations']], 187 'default' => ['meta', 'settings', 'redirects', 'schema', 'locations'], 188 ], 189 ], 190 ], 191 ]); 192 193 register_rest_route($this->namespace, '/' . $this->rest_base . '/competitor/rollback', [ 194 [ 195 'methods' => WP_REST_Server::DELETABLE, 196 'callback' => [$this, 'rollback_competitor_import'], 197 'permission_callback' => [$this, 'import_permissions_check'], 198 'args' => [ 199 'rollback_id' => [ 200 'type' => 'string', 201 'required' => true, 202 'minLength' => 16, 203 'maxLength' => 64, 204 'pattern' => '^[a-fA-F0-9-]+$', 205 ], 206 ], 207 ], 208 ]); 209 172 210 // Backward compatibility route used by older onboarding builds. 173 211 register_rest_route($this->namespace, '/import/competitor', [ … … 180 218 'description' => __('Competitor plugin to import from', 'prorank-seo'), 181 219 'type' => 'string', 182 'enum' => ['yoast', 'rankmath', 'aioseo', 'seopress' ],220 'enum' => ['yoast', 'rankmath', 'aioseo', 'seopress', 'redirection', 'schema-pro', 'aio-rich-snippets'], 183 221 'required' => true, 184 222 ], … … 235 273 'types' => ['meta', 'settings', 'redirects'], 236 274 ], 275 'redirection' => [ 276 'name' => 'Redirection', 277 'slug' => 'redirection', 278 'icon' => 'redirection', 279 'types' => ['redirects'], 280 ], 281 'schema-pro' => [ 282 'name' => 'Schema Pro', 283 'slug' => 'schema-pro', 284 'icon' => 'schema', 285 'types' => ['schema'], 286 ], 287 'aio-rich-snippets' => [ 288 'name' => 'All In One Schema Rich Snippets', 289 'slug' => 'aio-rich-snippets', 290 'icon' => 'schema', 291 'types' => ['schema'], 292 ], 237 293 ]; 238 294 … … 270 326 global $wpdb; 271 327 328 if ($source === 'redirection') { 329 $table = $wpdb->prefix . 'redirection_items'; 330 if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($table))) !== $table) { 331 return 0; 332 } 333 return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); 334 } 335 336 if ($source === 'schema-pro') { 337 return (int) $wpdb->get_var( 338 "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'aiosrs-schema' AND post_status = 'publish'" 339 ); 340 } 341 272 342 $patterns = [ 273 343 'yoast' => '_yoast_wpseo_title', … … 275 345 'rankmath' => 'rank_math_title', 276 346 'seopress' => '_seopress_titles_title', 347 'aio-rich-snippets' => '_bsf_post_type', 277 348 ]; 278 349 … … 1040 1111 $types = array_map('sanitize_key', (array) $request->get_param('types')); 1041 1112 1113 $transaction = null; 1042 1114 try { 1043 1115 // Get the appropriate importer … … 1063 1135 } 1064 1136 1137 $transaction = ImportTransaction::begin($source); 1138 $importer = $this->get_competitor_importer($source, $types, false, $transaction); 1139 if (!$importer) { 1140 throw new \RuntimeException(__('Importer could not be initialized.', 'prorank-seo')); 1141 } 1142 1065 1143 // Run the import 1066 1144 $result = $importer->import(); 1067 1145 1068 1146 if ($result['success']) { 1147 $rollback = $transaction->complete(); 1069 1148 return new WP_REST_Response([ 1070 1149 'success' => true, 1071 1150 'message' => $result['message'], 1072 1151 'stats' => $result['stats'], 1152 'rollback' => $rollback, 1073 1153 ], 200); 1074 1154 } else { 1155 $transaction->rollback_current(); 1075 1156 return new WP_Error( 1076 1157 'import_failed', … … 1081 1162 1082 1163 } catch (\Throwable $e) { 1164 if ($transaction instanceof ImportTransaction) { 1165 $transaction->rollback_current(); 1166 } 1083 1167 prorank_log('ProRank SEO Competitor Import Error: ' . $e->getMessage()); 1084 1168 return new WP_Error( … … 1091 1175 } 1092 1176 1177 /** Preview a competitor import without writing options, metadata or tables. */ 1178 public function preview_competitor_import($request) { 1179 $source = sanitize_key($request->get_param('source')); 1180 $types = array_map('sanitize_key', (array) $request->get_param('types')); 1181 try { 1182 $importer = $this->get_competitor_importer($source, $types, true, null); 1183 if (!$importer) { 1184 return new WP_Error('invalid_source', __('Invalid import source.', 'prorank-seo'), ['status' => 400]); 1185 } 1186 if (!$importer->is_available()) { 1187 return new WP_Error('source_not_available', __('No importable data was found.', 'prorank-seo'), ['status' => 404]); 1188 } 1189 $result = $importer->import(); 1190 return new WP_REST_Response([ 1191 'success' => !empty($result['success']), 1192 'message' => $result['message'] ?? __('Preview completed.', 'prorank-seo'), 1193 'stats' => $result['stats'] ?? [], 1194 'preview' => $importer->get_execution_summary(), 1195 ], !empty($result['success']) ? 200 : 500); 1196 } catch (\Throwable $e) { 1197 return new WP_Error('preview_failed', __('Import preview could not be completed.', 'prorank-seo'), ['status' => 500]); 1198 } 1199 } 1200 1201 /** Restore the values journalled immediately before a competitor import. */ 1202 public function rollback_competitor_import($request) { 1203 $this->load_import_transaction(); 1204 $result = ImportTransaction::rollback(sanitize_text_field((string) $request->get_param('rollback_id'))); 1205 return new WP_REST_Response($result, !empty($result['success']) ? 200 : 409); 1206 } 1207 1093 1208 /** 1094 1209 * Get competitor importer instance … … 1098 1213 * @return object|null Importer instance or null 1099 1214 */ 1100 private function get_competitor_importer(string $source, array $types ) {1215 private function get_competitor_importer(string $source, array $types, bool $dry_run = false, ?ImportTransaction $transaction = null) { 1101 1216 $importers = [ 1102 1217 'yoast' => 'YoastImporter', … … 1104 1219 'aioseo' => 'AIOSEOImporter', 1105 1220 'seopress' => 'SEOPressImporter', 1221 'redirection' => 'RedirectionImporter', 1222 'schema-pro' => 'SchemaProImporter', 1223 'aio-rich-snippets' => 'AioRichSnippetsImporter', 1106 1224 ]; 1107 1225 … … 1149 1267 } 1150 1268 1151 return new $class($types); 1269 return new $class($types, $dry_run, $transaction); 1270 } 1271 1272 private function load_import_transaction(): void { 1273 if (class_exists(ImportTransaction::class)) { 1274 return; 1275 } 1276 $plugin_dir = defined('PRORANK_SEO_PLUGIN_DIR') ? PRORANK_SEO_PLUGIN_DIR : (defined('PRORANK_PLUGIN_DIR') ? PRORANK_PLUGIN_DIR : ''); 1277 if ($plugin_dir !== '') { 1278 require_once $plugin_dir . 'includes/Core/Importers/ImportTransaction.php'; 1279 } 1152 1280 } 1153 1281 -
prorank-seo/trunk/includes/Core/RestApi/PerformanceSettingsController.php
r3647918 r3657955 1670 1670 'optm_skip_roles' => [], 1671 1671 'content_visibility_enabled' => false, 1672 'content_visibility_auto_learn' => false, 1672 1673 'content_visibility_allow_elementor' => false, 1673 1674 'content_visibility_selectors' => '', … … 1708 1709 'optm_skip_roles' => [], 1709 1710 'content_visibility_enabled' => false, 1711 'content_visibility_auto_learn' => false, 1710 1712 'content_visibility_allow_elementor' => false, 1711 1713 'content_visibility_selectors' => '', … … 1763 1765 'font_display_swap', 1764 1766 'content_visibility_enabled', 1767 'content_visibility_auto_learn', 1765 1768 ] as $bool_key) { 1766 1769 $canonical[$bool_key] = $this->normalize_boolean_setting($canonical[$bool_key] ?? false); … … 1775 1778 if (!$canonical['content_visibility_enabled']) { 1776 1779 $canonical['content_visibility_allow_elementor'] = false; 1780 $canonical['content_visibility_auto_learn'] = false; 1777 1781 } 1778 1782 -
prorank-seo/trunk/includes/Core/RestApi/SystemController.php
r3627350 r3657955 854 854 ], 855 855 [ 856 'slug' => 'redirection', 857 'name' => 'Redirection', 858 'file' => 'redirection/redirection.php', 859 'can_import' => true, 860 ], 861 [ 862 'slug' => 'schema-pro', 863 'name' => 'Schema Pro', 864 'file' => 'wp-schema-pro/wp-schema-pro.php', 865 'can_import' => true, 866 'meta_prefix' => 'bsf-aiosrs', 867 ], 868 [ 869 'slug' => 'aio-rich-snippets', 870 'name' => 'All In One Schema Rich Snippets', 871 'file' => 'all-in-one-schemaorg-rich-snippets/index.php', 872 'can_import' => true, 873 'meta_prefix' => '_bsf_', 874 ], 875 [ 856 876 'slug' => 'squirrly', 857 877 'name' => 'Squirrly SEO', … … 951 971 foreach ($competitors as $plugin) { 952 972 // Only check importable plugins that weren't already detected as active 953 if (empty($plugin['can_import']) || empty($plugin['meta_prefix'])) {973 if (empty($plugin['can_import'])) { 954 974 continue; 955 975 } … … 965 985 } 966 986 967 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 968 $has_data = (int) $wpdb->get_var( 969 $wpdb->prepare( 970 "SELECT 1 FROM {$wpdb->postmeta} WHERE meta_key LIKE %s LIMIT 1", 971 $wpdb->esc_like($plugin['meta_prefix']) . '%' 972 ) 973 ); 987 $has_data = 0; 988 if (!empty($plugin['meta_prefix'])) { 989 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 990 $has_data = (int) $wpdb->get_var( 991 $wpdb->prepare( 992 "SELECT 1 FROM {$wpdb->postmeta} WHERE meta_key LIKE %s LIMIT 1", 993 $wpdb->esc_like($plugin['meta_prefix']) . '%' 994 ) 995 ); 996 } elseif ($plugin['slug'] === 'redirection') { 997 $redirection_table = $wpdb->prefix . 'redirection_items'; 998 $has_data = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($redirection_table))) === $redirection_table 999 ? (int) $wpdb->get_var("SELECT 1 FROM {$redirection_table} LIMIT 1") 1000 : 0; 1001 } 974 1002 975 1003 if ($has_data) { -
prorank-seo/trunk/includes/Core/RestApi/VideoSitemapController.php
r3568558 r3657955 102 102 'custom_fields' => $settings['custom_fields'] ?? '', 103 103 'auto_detect' => $settings['auto_detect'] ?? true, 104 'supportedFormats' => $settings['supportedFormats'] ?? ['youtube', 'vimeo', ' mp4'],104 'supportedFormats' => $settings['supportedFormats'] ?? ['youtube', 'vimeo', 'dailymotion', 'ted', 'videopress', 'mp4', 'webm'], 105 105 'detect_shorts' => $settings['detect_shorts'] ?? true, 106 106 'enable_api_caching' => $settings['enable_api_caching'] ?? true, … … 145 145 146 146 if (isset($params['supportedFormats']) && is_array($params['supportedFormats'])) { 147 $settings['supportedFormats'] = array_values(array_filter(array_map('sanitize_key', $params['supportedFormats']))); 147 $settings['supportedFormats'] = array_values(array_intersect( 148 ['youtube', 'vimeo', 'dailymotion', 'ted', 'videopress', 'mp4', 'webm'], 149 array_filter(array_map('sanitize_key', $params['supportedFormats'])) 150 )); 148 151 } 149 152 … … 277 280 'description' => __('Supported video formats', 'prorank-seo'), 278 281 'type' => 'array', 279 'items' => ['type' => 'string'], 282 'items' => [ 283 'type' => 'string', 284 'enum' => ['youtube', 'vimeo', 'dailymotion', 'ted', 'videopress', 'mp4', 'webm'], 285 ], 280 286 ], 281 287 ]; -
prorank-seo/trunk/includes/Modules/Indexing/Sitemaps/VideoSitemapProvider.php
r3614385 r3657955 16 16 if ( ! defined( 'ABSPATH' ) ) { 17 17 exit; 18 } 19 20 if ( ! class_exists( VideoMetadataEnricher::class ) ) { 21 require_once __DIR__ . '/VideoMetadataEnricher.php'; 18 22 } 19 23 … … 137 141 'custom_fields' => '', 138 142 'auto_detect' => true, 139 'supportedFormats' => ['youtube', 'vimeo', ' mp4'],143 'supportedFormats' => ['youtube', 'vimeo', 'dailymotion', 'ted', 'videopress', 'mp4', 'webm'], 140 144 'detect_shorts' => true, 141 145 'enable_api_caching' => true, … … 215 219 216 220 // Add 2025 YouTube Shorts support 217 if ($is_youtube_short === 'yes' || $this->is_youtube_short($video_url)) {221 if ($is_youtube_short === 'yes' || (!empty($settings['detect_shorts']) && $this->is_youtube_short($video_url))) { 218 222 $video['is_short'] = 'yes'; 219 223 // Shorts don't require minimum watch time as of March 31, 2025 … … 244 248 // Also parse content for embedded videos if auto-detect is enabled 245 249 if ($settings['auto_detect']) { 246 $content_videos = $this->parse_content_videos( get_post_field('post_content',$post_id));250 $content_videos = $this->parse_content_videos($this->get_discovery_content($post_id)); 247 251 if (!empty($content_videos)) { 248 252 $videos = array_merge($videos, $content_videos); … … 350 354 if (!empty($field_videos)) { 351 355 $videos = array_merge($videos, $field_videos); 352 } elseif (filter_var($value, FILTER_VALIDATE_URL)) {353 // It's a direct URL354 $videos[] = [355 'content_loc' => esc_url($value),356 'title' => get_the_title($post_id),357 'description' => wp_trim_words(get_the_excerpt($post_id), 50),358 ];359 356 } 360 357 } … … 362 359 363 360 return $videos; 361 } 362 363 /** 364 * Collect public post content and known page-builder content fields. 365 * 366 * Only an explicit allow-list is read. Arbitrary post meta can contain 367 * credentials or large private payloads and must never be scanned. 368 */ 369 private function get_discovery_content(int $post_id): string { 370 $parts = [(string) get_post_field('post_content', $post_id)]; 371 $builder_fields = [ 372 '_elementor_data', 373 '_fl_builder_data', 374 '_bricks_page_content_2', 375 'ct_builder_shortcodes', 376 '_fusion_builder_content', 377 ]; 378 379 foreach ($builder_fields as $field) { 380 $value = get_post_meta($post_id, $field, true); 381 if (is_array($value) || is_object($value)) { 382 $value = wp_json_encode($value); 383 } 384 if (!is_string($value) || $value === '') { 385 continue; 386 } 387 388 // Bound each builder contribution before the discovery service's 389 // aggregate cap to avoid inflating memory on malformed metadata. 390 $parts[] = substr($value, 0, 524288); 391 } 392 393 return implode("\n", $parts); 364 394 } 365 395 … … 371 401 */ 372 402 private function is_youtube_short(string $video_url): bool { 373 // YouTube Shorts URLs contain /shorts/ in the path 374 if (strpos($video_url, '/shorts/') !== false) { 375 return true; 376 } 377 378 // Check if it's a regular YouTube video that might be a Short 379 // Shorts are typically vertical videos under 60 seconds 380 $video_id = $this->extract_youtube_id($video_url); 381 if ($video_id) { 382 $video_data = $this->fetch_youtube_video_data($video_id); 383 if ($video_data && isset($video_data['duration'])) { 384 // Shorts are 60 seconds or less 385 return $video_data['duration'] <= 60; 386 } 387 } 388 389 return false; 403 // Duration alone cannot identify a Short: Shorts can be up to three 404 // minutes and the API response does not expose orientation. Only an 405 // explicit Shorts URL or imported marker is authoritative. 406 return strpos($video_url, '/shorts/') !== false; 390 407 } 391 408 … … 409 426 private function fetch_youtube_video_data(string $video_id): ?array { 410 427 $settings = $this->get_video_settings(); 411 $api_key = $settings['youtube_api_key'] ?? ''; 412 413 if (empty($api_key)) { 414 return null; 415 } 416 417 // Cache key for API response (15 minute cache to optimize quota) 418 $cache_key = 'prorank_youtube_data_' . $video_id; 419 $cached_data = get_transient($cache_key); 420 421 if ($cached_data !== false) { 422 return $cached_data; 423 } 424 425 // Fetch from YouTube API 426 $api_url = sprintf( 427 'https://www.googleapis.com/youtube/v3/videos?id=%s&key=%s&part=snippet,contentDetails,statistics', 428 $client = new YouTubeVideoMetadataClient(); 429 return $client->fetch( 428 430 $video_id, 429 $api_key 431 (string) ($settings['youtube_api_key'] ?? ''), 432 (int) ($settings['cache_duration'] ?? 15), 433 !empty($settings['enable_api_caching']) 430 434 ); 431 432 $response = wp_remote_get($api_url);433 434 if (is_wp_error($response)) {435 return null;436 }437 438 $body = wp_remote_retrieve_body($response);439 $data = json_decode($body, true);440 441 if (!isset($data['items'][0])) {442 return null;443 }444 445 $item = $data['items'][0];446 $video_data = [447 'title' => $item['snippet']['title'] ?? '',448 'description' => $item['snippet']['description'] ?? '',449 'thumbnail' => $item['snippet']['thumbnails']['high']['url'] ?? '',450 'duration' => $this->parse_iso8601_duration($item['contentDetails']['duration'] ?? ''),451 'view_count' => intval($item['statistics']['viewCount'] ?? 0),452 'published_at' => $item['snippet']['publishedAt'] ?? '',453 'is_short' => $this->check_if_short_from_api($item),454 ];455 456 // Cache for 15 minutes to optimize API quota usage457 set_transient($cache_key, $video_data, 15 * MINUTE_IN_SECONDS);458 459 return $video_data;460 }461 462 /**463 * Check if video is a Short from API data464 *465 * @param array $item YouTube API item data466 * @return bool467 */468 private function check_if_short_from_api(array $item): bool {469 // Check duration (60 seconds or less)470 $duration = $this->parse_iso8601_duration($item['contentDetails']['duration'] ?? '');471 if ($duration <= 60) {472 // Check aspect ratio if available (Shorts are vertical)473 // This would require additional API calls, so we rely on duration for now474 return true;475 }476 return false;477 }478 479 /**480 * Parse ISO 8601 duration to seconds481 *482 * @param string $duration ISO 8601 duration483 * @return int Duration in seconds484 */485 private function parse_iso8601_duration(string $duration): int {486 if (empty($duration)) {487 return 0;488 }489 490 try {491 $interval = new \DateInterval($duration);492 } catch (\Exception $exception) {493 return 0;494 }495 496 return ($interval->h * 3600) + ($interval->i * 60) + $interval->s;497 435 } 498 436 … … 504 442 */ 505 443 private function parse_content_videos(string $content): array { 506 $videos = [];507 444 $settings = $this->get_video_settings(); 508 509 // YouTube embeds (including Shorts) 510 preg_match_all('/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/|youtube\.com\/shorts\/)([^"&?\/\s]{11})/i', $content, $youtube_matches); 511 512 foreach ($youtube_matches[1] as $youtube_id) { 513 $video_url = 'https://www.youtube.com/watch?v=' . $youtube_id; 514 $video_entry = [ 515 'content_loc' => $video_url, 516 'player_loc' => 'https://www.youtube.com/embed/' . $youtube_id, 517 'thumbnail_loc' => 'https://i.ytimg.com/vi/' . $youtube_id . '/hqdefault.jpg', 518 'allow_embed' => 'yes', 519 ]; 520 521 // Check if it's a YouTube Short and add metadata 522 if ($this->is_youtube_short($video_url)) { 523 $video_entry['is_short'] = 'yes'; 524 $video_entry['view_count_type'] = 'starts_and_replays'; 525 } 526 527 // Try to fetch additional data from YouTube API if available 528 if (!empty($settings['youtube_api_key'])) { 445 $supported_formats = is_array($settings['supportedFormats'] ?? null) 446 ? $settings['supportedFormats'] 447 : []; 448 $videos = VideoDiscoveryService::discover($content, home_url('/'), $supported_formats); 449 450 if (empty($settings['detect_shorts'])) { 451 foreach ($videos as &$video) { 452 unset($video['is_short']); 453 } 454 unset($video); 455 } 456 457 if (!empty($settings['youtube_api_key'])) { 458 foreach ($videos as &$video) { 459 $youtube_id = $this->extract_youtube_id((string) ($video['content_loc'] ?? $video['player_loc'] ?? '')); 460 if ($youtube_id === null) { 461 continue; 462 } 463 529 464 $api_data = $this->fetch_youtube_video_data($youtube_id); 530 if ($api_data) { 531 $video_entry['title'] = $api_data['title']; 532 $video_entry['description'] = $api_data['description']; 533 $video_entry['thumbnail_loc'] = $api_data['thumbnail']; 534 $video_entry['duration'] = $api_data['duration']; 535 $video_entry['view_count'] = $api_data['view_count']; 536 } 537 } 538 539 $videos[] = $video_entry; 540 } 541 542 // Vimeo embeds 543 preg_match_all('/vimeo\.com\/(?:video\/)?(\d+)/i', $content, $vimeo_matches); 544 545 foreach ($vimeo_matches[1] as $vimeo_id) { 546 $videos[] = [ 547 'content_loc' => 'https://vimeo.com/' . $vimeo_id, 548 'player_loc' => 'https://player.vimeo.com/video/' . $vimeo_id, 549 'allow_embed' => 'yes', 550 ]; 465 if (!$api_data) { 466 continue; 467 } 468 469 foreach (['title', 'description'] as $field) { 470 if (isset($api_data[$field]) && trim((string) $api_data[$field]) !== '') { 471 $video[$field] = $api_data[$field]; 472 } 473 } 474 if (!empty($api_data['thumbnail'])) { 475 $video['thumbnail_loc'] = $api_data['thumbnail']; 476 } 477 if (!empty($api_data['duration'])) { 478 $video['duration'] = $api_data['duration']; 479 } 480 if (isset($api_data['view_count']) && is_numeric($api_data['view_count'])) { 481 $video['view_count'] = max(0, (int) $api_data['view_count']); 482 } 483 if (!empty($api_data['is_short'])) { 484 $video['is_short'] = 'yes'; 485 } 486 } 487 unset($video); 488 } 489 490 $enricher = new VideoMetadataEnricher(); 491 foreach ($videos as $index => $video) { 492 $videos[$index] = $enricher->enrich($video, home_url('/')); 551 493 } 552 494 … … 579 521 580 522 if (isset($video['player_loc'])) { 581 echo "\t\t\t<video:player_loc>" . esc_url($video['player_loc']) . "</video:player_loc>\n"; 523 $allow_embed = ($video['allow_embed'] ?? '') === 'yes' ? ' allow_embed="yes"' : ''; 524 echo "\t\t\t<video:player_loc{$allow_embed}>" . esc_url($video['player_loc']) . "</video:player_loc>\n"; 582 525 } 583 526 … … 609 552 if (isset($video['category'])) { 610 553 echo "\t\t\t<video:category>" . esc_xml($video['category']) . "</video:category>\n"; 611 }612 613 if (isset($video['allow_embed'])) {614 echo "\t\t\t<video:requires_subscription>no</video:requires_subscription>\n";615 554 } 616 555 -
prorank-seo/trunk/includes/Modules/Performance/ContentVisibilityModule.php
r3647918 r3657955 28 28 * Reads from prorank_asset_optimization_settings: 29 29 * content_visibility_enabled (bool) – master toggle 30 * content_visibility_auto_learn (bool) – evidence-based selector learning 30 31 * content_visibility_allow_elementor (bool) – explicit, visually-tested Elementor opt-in 31 32 * content_visibility_selectors (string) – newline-delimited CSS selectors … … 36 37 class ContentVisibilityModule extends BaseModule { 37 38 39 private ?ContentVisibilityLearningService $learning_service = null; 40 38 41 /** 39 42 * Required tier for this module … … 73 76 } 74 77 75 if ($this->is_elementor_built_page()) {76 $allow_elementor = $this->is_explicit_elementor_opt_in_enabled()77 || (bool) apply_filters('prorank_content_visibility_allow_elementor', false, (int) get_queried_object_id());78 if (!$allow_elementor) {79 return;80 }81 }82 83 78 add_action('wp_head', [$this, 'inject_content_visibility_css'], 1); 79 80 // Module hooks are registered during plugins_loaded, before the main 81 // query exists. Defer singular-context ownership to the callback. 82 if ($this->is_learning_enabled()) { 83 add_action('wp_enqueue_scripts', [$this, 'enqueue_learning_script']); 84 } 84 85 } 85 86 … … 149 150 */ 150 151 public function inject_content_visibility_css(): void { 152 if (!$this->current_page_is_eligible()) { 153 return; 154 } 155 151 156 $selectors = $this->normalize_selectors($this->get_setting('content_visibility_selectors', '')); 152 if (empty($selectors)) { 153 return; 154 } 155 156 $size_hint = trim((string) $this->get_setting('content_visibility_size_hint', 'auto 800px')); 157 if ($size_hint === '' || !$this->is_safe_size_hint($size_hint)) { 158 $size_hint = 'auto 800px'; 159 } 160 161 $selector_list = implode(', ', array_map([$this, 'escape_selector'], $selectors)); 162 if ($selector_list === '') { 163 return; 164 } 165 166 $css = sprintf( 167 '%s { content-visibility: auto; contain-intrinsic-size: %s; }', 168 $selector_list, 169 $size_hint 170 ); 157 $rules = []; 158 159 if (!empty($selectors)) { 160 $size_hint = trim((string) $this->get_setting('content_visibility_size_hint', 'auto 800px')); 161 if ($size_hint === '' || !$this->is_safe_size_hint($size_hint)) { 162 $size_hint = 'auto 800px'; 163 } 164 165 $selector_list = implode(', ', array_map([$this, 'escape_selector'], $selectors)); 166 if ($selector_list !== '') { 167 $rules[] = sprintf( 168 '%s { content-visibility: auto; contain-intrinsic-size: %s; }', 169 $selector_list, 170 $size_hint 171 ); 172 } 173 } 174 175 if ($this->is_learning_enabled() && is_singular()) { 176 $learned_css = $this->learning_service()->profile_css((int) get_queried_object_id()); 177 if ($learned_css !== '') { 178 $rules[] = $learned_css; 179 } 180 } 181 182 if ($rules === []) { 183 return; 184 } 171 185 172 186 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Selector and intrinsic-size values pass strict CSS allowlists above. 173 echo "<style id=\"prorank-content-visibility\">" . $css . "</style>\n"; 187 echo "<style id=\"prorank-content-visibility\">" . implode('', $rules) . "</style>\n"; 188 } 189 190 public function enqueue_learning_script(): void { 191 if (!is_singular() || !$this->current_page_is_eligible()) { 192 return; 193 } 194 195 $this->learning_service()->enqueue_for_post((int) get_queried_object_id()); 174 196 } 175 197 … … 187 209 188 210 return (bool) $value; 211 } 212 213 private function is_learning_enabled(): bool { 214 $value = $this->get_setting('content_visibility_auto_learn', false); 215 216 if (is_string($value)) { 217 return in_array(strtolower(trim($value)), ['true', '1', 'yes', 'on'], true); 218 } 219 220 return (bool) $value; 221 } 222 223 /** 224 * Evaluate builder ownership after WordPress has resolved the main query. 225 * init_hooks() runs during plugins_loaded, where is_singular() is not yet 226 * reliable and would otherwise let Elementor pages bypass the opt-in. 227 */ 228 private function current_page_is_eligible(): bool { 229 if (\ProRank\SEO\Plugin::is_elementor_runtime_request()) { 230 return false; 231 } 232 233 if (!$this->is_elementor_built_page()) { 234 return true; 235 } 236 237 return $this->is_explicit_elementor_opt_in_enabled() 238 || (bool) apply_filters('prorank_content_visibility_allow_elementor', false, (int) get_queried_object_id()); 239 } 240 241 private function learning_service(): ContentVisibilityLearningService { 242 if (!$this->learning_service instanceof ContentVisibilityLearningService) { 243 $this->learning_service = new ContentVisibilityLearningService(); 244 } 245 246 return $this->learning_service; 189 247 } 190 248 -
prorank-seo/trunk/includes/Plugin.php
r3647918 r3657955 885 885 // REST API initialization 886 886 add_action( 'rest_api_init', [ $this, 'init_rest_api' ] ); 887 ( new \ProRank\SEO\Core\Abilities\AbilitiesIntegration() )->register_hooks(); 888 ( new \ProRank\SEO\Modules\Performance\ContentVisibilityLearningService() )->register_background_hooks(); 887 889 add_filter( 'auto_update_plugin', [ $this, 'filter_plugin_auto_update' ], 10, 2 ); 888 890 -
prorank-seo/trunk/prorank-seo.php
r3647918 r3657955 4 4 * Plugin URI: https://prorank.io 5 5 * Description: All-in-one WordPress SEO and performance toolkit with on-page SEO, technical SEO, caching, and local image optimisation. 6 * Version: 1.4. 76 * Version: 1.4.8 7 7 * Author: ProRank 8 8 * Author URI: https://prorank.io/about … … 96 96 // Define plugin constants 97 97 if ( ! defined( 'PRORANK_VERSION' ) ) { 98 define( 'PRORANK_VERSION', '1.4. 7' );98 define( 'PRORANK_VERSION', '1.4.8' ); 99 99 } 100 100 if ( ! defined( 'PRORANK_SEO_VERSION' ) ) { -
prorank-seo/trunk/readme.txt
r3647918 r3657955 6 6 Tested up to: 7.1 7 7 Requires PHP: 8.2 8 Stable tag: 1.4. 78 Stable tag: 1.4.8 9 9 License: GPL-2.0-or-later 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 25 25 * AI discovery support through llms.txt, schema-map, /ask, and /mcp 26 26 27 = What changed in 1.4.7 = 28 29 * Cache warming and stale-page refreshes now run in small background batches instead of delaying admin requests. 30 * Page caching is safer and more consistent during traffic spikes and for signed-in, session, WooCommerce, mobile, and recently updated pages. 31 * Image savings, modern-format delivery, renamed files, and cloud-to-local fallbacks are reported and handled more accurately. 32 * LCP learning, local fonts, consent tools, module activation, and scheduled cleanup are more reliable. 33 * Tested with the WordPress 7.1 editor and media pipeline, with axe-core 4.13 for accessibility checks. 27 = What changed in 1.4.8 = 28 29 * Preview supported competitor data before writing and undo a completed import for seven days. 30 * Import supported data from Redirection, Schema Pro, and All in One Schema Rich Snippets alongside the existing SEO importers. 31 * Fill missing video-sitemap metadata for Vimeo, Dailymotion, VideoPress, and TED videos. 32 * Use 11 permission-aware WordPress Abilities for SEO, schema, content, analytics, caching, images, links, and audits. 33 * Optionally learn stable desktop and mobile dimensions for faster below-the-fold rendering. 34 * Benefit from broader compatibility safeguards and a consolidated Link Operations workspace. 34 35 35 36 The free plugin covers on-page SEO, technical SEO, SWR page caching, self-healing JavaScript learning, local image compression, Agentic SEO readiness, H1 and title-length repair workflows, AI metadata cleanup, object-cache health controls, and one-click migration from Yoast, AIOSEO, Rank Math, or SEOPress. [ProRank Premium](https://prorank.io/pricing?utm_source=wporg&utm_campaign=readme) adds Keyword Research (Beta) powered by Ahrefs for Business and Agency plans, cloud image optimisation (WebP, AVIF, JXL), critical CSS extraction, unused CSS removal, AI-assisted content tools, richer AI Discovery modes, advanced analytics, local SEO, WooCommerce SEO, and agency features. … … 140 141 * Builder support for Gutenberg, Elementor, Divi Builder, Beaver Builder, SeedProd, and WPBakery 141 142 * Export and import settings 142 * Import feedback with post counts and import type badges143 * Import preview, post counts, import type badges, and one-click rollback 143 144 * Settings snapshots and backups 144 145 * Module manager … … 186 187 * **ProRank API** — Admin notifications, changelog checks, optional feedback submissions, license/update checks where configured, optional cache diagnostics, security alerts, extension metadata, and cloud image optimization where explicitly enabled/available. Sends site URL, plugin version, license key or tier where configured, feedback fields when you submit feedback, image files when cloud optimization is enabled, and diagnostic/error context for security or cache checks. [Terms](https://prorank.io/terms) | [Privacy](https://prorank.io/privacy-policy) 187 188 * **Google PageSpeed Insights API** — Core Web Vitals and performance audits. Sends the analysed URL and optional API key. [Terms](https://policies.google.com/terms) | [Privacy](https://policies.google.com/privacy) 189 * **YouTube Data API** — Optional video-sitemap metadata enrichment when you provide a YouTube API key. Sends the detected YouTube video IDs and your configured API key. [Terms](https://www.youtube.com/t/terms) | [Privacy](https://policies.google.com/privacy) 190 * **Hosted video metadata** — Video sitemaps can request public titles, descriptions, thumbnails, dates, and durations for detected Vimeo, Dailymotion, VideoPress, and TED videos. Sends only the detected public video URL; VideoPress also receives the requesting site host required by its oEmbed API. Requests use fixed provider endpoints, are cached, and run only while generating a sitemap containing those videos. [Vimeo](https://vimeo.com/terms) | [Dailymotion](https://legal.dailymotion.com/en/terms-of-use/) | [WordPress.com](https://wordpress.com/tos/) | [TED](https://www.ted.com/about/our-organization/our-policies-terms/ted-com-terms-of-use) 188 191 * **Schema.org Validator** — Schema validation. Sends the validated page URL. [Terms](https://schema.org/docs/terms.html) 189 192 * **IndexNow** — Instant indexing notifications. Sends host, API key, and URLs. [Protocol](https://www.indexnow.org/) … … 213 216 No. The free plugin works entirely locally — no account, no API key, no cloud dependency. Its AI Discovery surface is public list mode only (`/schema-map`, `/ask`, and `/mcp`). ProRank Premium requires a license key for cloud features, and its richer AI Discovery modes (`summarize` and `generate`) use your own AI Tools provider key plus WordPress application-password auth. 214 217 215 = Can I import from Yoast, Rank Math, All in One SEO, or SEOPress? =216 217 Yes. ProRank SEO includes import tools for Yoast SEO, AIOSEO, Rank Math, and SEOPress so you can switch without losing your existing SEO settings.218 = Which plugins can I import from? = 219 220 ProRank can preview and import supported data from Yoast SEO, AIOSEO, Rank Math, SEOPress, Redirection, Schema Pro, and All in One Schema Rich Snippets. A completed import includes a temporary rollback option. 218 221 219 222 = What is ProRank Premium? = … … 247 250 == Upgrade Notice == 248 251 252 = 1.4.8 = 253 Adds previewable, reversible competitor migration, broader video metadata, permission-aware WordPress Abilities, optional content-visibility learning, and wider plugin compatibility. 254 249 255 = 1.4.7 = 250 256 Moves cache warming into bounded background work, strengthens page delivery under load, corrects image optimisation reporting and delivery, and adds WordPress 7.1 compatibility. … … 365 371 366 372 == Changelog == 373 374 = 1.4.8 = 375 * Preview supported competitor data before writing and undo a completed import for seven days. 376 * Import supported data from Redirection, Schema Pro, and All in One Schema Rich Snippets alongside the existing SEO importers. 377 * Fill missing video-sitemap metadata for Vimeo, Dailymotion, VideoPress, and TED videos. 378 * Use 11 permission-aware WordPress Abilities for SEO, schema, content, analytics, caching, images, links, and audits. 379 * Optionally learn stable desktop and mobile dimensions for faster below-the-fold rendering. 380 * Benefit from broader compatibility safeguards and a consolidated Link Operations workspace. 367 381 368 382 = 1.4.7 =
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)