diff --git a/Components/CleanupInventory.php b/Components/CleanupInventory.php index 6329447..5c5343a 100644 --- a/Components/CleanupInventory.php +++ b/Components/CleanupInventory.php @@ -55,6 +55,7 @@ class CleanupInventory { public const INFRASTRUCTURE_OPTIONS = [ 'translatewp_version', Migrations::TRANSLATED_MARKS_OPTION, + Migrations::LEXICON_PRIVATE_CACHE_PURGED_OPTION, \TranslateWP\Functionality\KeepWarm::PENDING_LANGUAGES_OPTION, QuotaHandoff::OPTION, 'translatewp_alt_text_notice', diff --git a/Components/Migrations.php b/Components/Migrations.php index 58d9c59..70c6cd5 100644 --- a/Components/Migrations.php +++ b/Components/Migrations.php @@ -16,6 +16,7 @@ class Migrations { public const VERSION_OPTION = 'translatewp_version'; public const TRANSLATED_MARKS_OPTION = 'translatewp_translated_marks_backfilled'; + public const LEXICON_PRIVATE_CACHE_PURGED_OPTION = 'translatewp_lexicon_private_cache_purged'; private const MIGRATIONS = [ '0.9.0-beta.1' => 'migrate_switcher_highlight_to_brand', @@ -33,6 +34,11 @@ public static function maybe_run(): void { $stored = (string) get_option( self::VERSION_OPTION, '' ); self::backfill_translated_marks(); + if ( '' === $stored ) { + update_option( self::LEXICON_PRIVATE_CACHE_PURGED_OPTION, time(), true ); + } elseif ( version_compare( $stored, TRANSLATEWP_VERSION, '<=' ) ) { + self::purge_lexicon_private_cache_once(); + } if ( '' !== $stored && version_compare( $stored, TRANSLATEWP_VERSION, '>=' ) ) { return; @@ -80,7 +86,19 @@ private static function migrate_1_0_0(): void { ( new IndexQueue() )->delete_unindexable_rows(); } + private static function purge_lexicon_private_cache_once(): void { + if ( get_option( self::LEXICON_PRIVATE_CACHE_PURGED_OPTION ) ) { + return; + } + // Mark before external cache hooks so a failing hook cannot repeat the purge on every init. + update_option( self::LEXICON_PRIVATE_CACHE_PURGED_OPTION, time(), true ); + PageCachePurge::purge_all( 'lexicon_private' ); + } + public static function seed_for_fresh_install(): void { + if ( '' === (string) get_option( self::VERSION_OPTION, '' ) ) { + update_option( self::LEXICON_PRIVATE_CACHE_PURGED_OPTION, time(), true ); + } update_option( self::VERSION_OPTION, TRANSLATEWP_VERSION ); } diff --git a/Components/TranslationAiSettings.php b/Components/TranslationAiSettings.php index 130775c..fbfff8c 100644 --- a/Components/TranslationAiSettings.php +++ b/Components/TranslationAiSettings.php @@ -27,10 +27,9 @@ private static function allowed_preset( string $value, array $free, string $fall /** * The translation settings sent with every job. * - * @param string $language The target language the settings are for; empty for - * the browser-bound copy that carries every term with its scope. + * @param string $language The target language; empty includes every term when requested. */ - public static function build( string $language = '' ): array { + public static function build( string $language = '', bool $include_lexicon = true ): array { $translation_settings = array_merge( [ 'auto_translate' => true, @@ -69,7 +68,7 @@ public static function build( string $language = '' ): array { 'tone' => self::allowed_preset( (string) $style_settings['tone'], self::FREE_TONES, 'friendly' ), 'formality' => (int) $style_settings['formality'], 'creativity' => (int) $style_settings['creativity'], - 'lexicon_terms' => '' === $language ? Lexicon::get_all() : Lexicon::terms_for_language( $language ), + 'lexicon_terms' => $include_lexicon ? ( '' === $language ? Lexicon::get_all() : Lexicon::terms_for_language( $language ) ) : [], ]; return $settings + self::advanced_controls( $style_settings ); diff --git a/Entities/CPTs/Lexicon.php b/Entities/CPTs/Lexicon.php index 0882701..5241b83 100644 --- a/Entities/CPTs/Lexicon.php +++ b/Entities/CPTs/Lexicon.php @@ -45,8 +45,7 @@ public static function languages_of( int $post_id ): array { /** * The terms the translator receives for one target language: every term * scoped to that language or to no language, reduced to the two fields the - * translation service reads. The same reduction runs in the browser so the - * two sides fingerprint the same settings. + * translation service reads. * * @return array */ @@ -112,11 +111,14 @@ public function register(): void { 'all_items' => __( 'Lexicon', 'translate' ), ], 'public' => false, + 'publicly_queryable' => false, + 'exclude_from_search' => true, 'show_ui' => false, 'show_in_menu' => false, - 'show_in_rest' => true, + 'show_in_rest' => false, 'has_archive' => false, - 'rewrite' => [ 'slug' => 'lexicon' ], + 'rewrite' => false, + 'query_var' => false, 'supports' => [ 'title' ], 'menu_icon' => 'dashicons-book-alt', 'taxonomies' => [ self::CATEGORY_TAXONOMY ], @@ -144,9 +146,10 @@ public function register(): void { 'show_admin_column' => false, 'show_tagcloud' => false, 'show_in_nav_menus' => false, - 'show_in_rest' => true, + 'show_in_rest' => false, 'hierarchical' => false, 'rewrite' => false, + 'query_var' => false, ] ); } diff --git a/Entities/Scripts/Admin.php b/Entities/Scripts/Admin.php index d687740..fb8b7ff 100644 --- a/Entities/Scripts/Admin.php +++ b/Entities/Scripts/Admin.php @@ -3,6 +3,7 @@ namespace TranslateWP\Entities\Scripts; use TranslateWP\Components\ProEligibility; +use TranslateWP\Components\Access; use TranslateWP\Components\RemoteApi; use TranslateWP\Components\SiteConnection; use TranslateWP\Components\Sites; @@ -46,7 +47,7 @@ public function context_data(): string { 'settings_scope' => (object) \TranslateWP\Components\SettingsScope::subsite_scope(), ], 'can' => \TranslateWP\Entities\Scripts\TranslateWP::manager_flags()['can'], - 'lexicon_terms' => Lexicon::get_all(), + 'lexicon_terms' => Access::can( Access::LEXICON ) || Access::can( Access::EDIT ) ? Lexicon::get_all() : [], ], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ) . ' );'; diff --git a/Entities/Scripts/TranslateWP.php b/Entities/Scripts/TranslateWP.php index e5f00ab..1bee1f1 100644 --- a/Entities/Scripts/TranslateWP.php +++ b/Entities/Scripts/TranslateWP.php @@ -327,7 +327,7 @@ public function get_frontend_options(): array { 'includeAttributes' => ! (bool) $translation_settings['preserve_html_attributes'], 'excludedAreas' => PageAreas::excluded( $translation_settings ), ], - 'ai_settings' => TranslationAiSettings::build(), + 'ai_settings' => TranslationAiSettings::build( '', false ), ]; } } diff --git a/Functionality/Endpoints/Admin.php b/Functionality/Endpoints/Admin.php index ad7ee7a..18cadf8 100644 --- a/Functionality/Endpoints/Admin.php +++ b/Functionality/Endpoints/Admin.php @@ -130,6 +130,14 @@ public function define_endpoints() { ); $get_lexicon_terms->useMiddleware( $cors ); + $get_lexicon_categories = new GetEndpoint( + $this->namespace, + 'get-lexicon-categories', + [ $this, 'get_lexicon_categories' ], + [ $this, 'lexicon_or_edit_permissions_check' ] + ); + $get_lexicon_categories->useMiddleware( $cors ); + $clear_cache = new PostEndpoint( $this->namespace, 'clear-cache', @@ -179,6 +187,7 @@ public function define_endpoints() { $update_lexicon_term, $delete_lexicon_term, $get_lexicon_terms, + $get_lexicon_categories, $clear_cache, $dashboard_metrics, $settings_revision, @@ -839,6 +848,28 @@ static function ( $term ) { ]; } + public function get_lexicon_categories( $request ) { + unset( $request ); + $terms = get_terms( + [ + 'taxonomy' => Lexicon::CATEGORY_TAXONOMY, + 'hide_empty' => false, + ] + ); + if ( is_wp_error( $terms ) ) { + return $terms; + } + + return array_map( + static fn( $term ): array => [ + 'id' => (int) $term->term_id, + 'name' => (string) $term->name, + 'slug' => (string) $term->slug, + ], + $terms + ); + } + private function is_admin_data_path_allowed( string $path ): bool { return in_array( $path, self::ADMIN_DATA_PATHS, true ); } diff --git a/composer.json b/composer.json index 6fe092e..c5144a7 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,6 @@ } }, "scripts": { - "test:php": "php tests/TranslatedNotWarmTest.php && php tests/QuotaHandoffTest.php && php tests/BrandingTest.php && php tests/SideTabTest.php && php tests/RoleCapabilitiesTest.php && php tests/AdminBarTest.php && php tests/BrandMenuIconTest.php && php tests/PluginRowTest.php && php tests/StyleConflictsTest.php && php tests/CacheScriptExclusionsTest.php && php tests/PageAreasTest.php && php tests/VisibleRolesTest.php && php tests/AdvancedStyleTest.php && php tests/TranslationReadinessTest.php && php tests/TranslationReadinessWiringTest.php && php tests/TranslationReadinessPacingTest.php && php tests/ReadinessSelfHealTest.php && php tests/SelfHealRepushTest.php && php tests/ReadinessSweepColdTest.php && php tests/ReadinessCursorTest.php && php tests/FirstPassPacingTest.php && php tests/SiteConnectionRouteTest.php && php tests/ConnectReturnPermalinkTest.php && php tests/RetryFailedTest.php && php tests/MissPayloadTest.php && php tests/OnboardingReturnTest.php && php tests/OnboardingResumeTest.php && php tests/ConnectNoticesTest.php && php tests/SiteConnectReturnTest.php && php tests/SiteLanguageSyncTest.php && php tests/SiteDisconnectTest.php && php tests/SiteAccountLabelTest.php && php tests/LyfecycleTest.php && php tests/RestErrorUnwrapTest.php && php tests/TranslationFingerprintGoldenTest.php && php tests/TranslationWhitespaceParityTest.php && php tests/TranslationProxyStorageTest.php && php tests/TranslationProxyClientIpTest.php && php tests/TranslationJobsProxyTest.php && php tests/IndexerTest.php && php tests/IndexerStoreBackfillTest.php && php tests/IndexQueueChangedPagesTest.php && php tests/IndexRereadChangedTest.php && php tests/TranslationProviderFailureTest.php && php tests/TranslationStoreTest.php && php tests/CronFallbackTest.php && php tests/MissProxyTest.php && php tests/TranslationUrlTest.php && php tests/LanguageUrlTest.php && php tests/LanguageRoutingTest.php && php tests/PageTextTranslatorTest.php && php tests/CrawlerTranslationTest.php && php tests/CrawlerResponseTest.php && php tests/StoreFromMissTest.php && php tests/StoreReadPathTest.php && php tests/StoreEvictionTest.php && php tests/StoreAnnouncesTranslationTest.php && php tests/NetworkConnectTest.php && php tests/NetworkConnectOutcomeTest.php && php tests/NetworkGrantRetirementTest.php && php tests/SwitcherCustomCssRetirementTest.php && php tests/MultisiteSettingsPageTest.php && php tests/MultisiteSettingsTest.php && php tests/SettingsScopeTest.php && php tests/SettingsScopeStateTest.php && php tests/SettingsLockedWriteTest.php && php tests/NetworkScopeTest.php && php tests/SubsiteScopeTest.php && php tests/KeepWarmRewarmTest.php && php tests/KeepWarmRereadTest.php && php tests/SEOCacheTest.php && php tests/SEOTest.php && php tests/SEOCoreCanonicalTest.php && php tests/SEOCanonicalPluginTest.php && php tests/SEOProVersionTest.php && php tests/SEOHtmlAttributesTest.php && php tests/SeoSettingsMergeTest.php && php tests/CrawlerCustomBotTest.php && php tests/LexiconOrderTest.php && php tests/LexiconEntitiesTest.php && php tests/ScriptConditionsTest.php && php tests/PageExclusionTest.php && php tests/PageExclusionMetaBoxTest.php && php tests/ConnectionInvalidationTest.php && php tests/AsyncScanTest.php && php tests/ContextScanSamplesTest.php && php tests/ClearCacheTest.php && php tests/OnboardingGateTest.php && php tests/CronKickTest.php && php tests/SetupPrefillTest.php && php tests/SaveStepResumeTest.php && php tests/SettingsResetTest.php && php tests/SwitcherSurfacesTest.php && php tests/SwitcherPlacementsContractTest.php && php tests/GutenbergSwitcherTest.php && php tests/SwitcherDefaultsTest.php && php tests/MigrationsTest.php && php tests/MigrationsHookTest.php && php tests/SharedStyleConditionsTest.php && php tests/EmDashSweepTest.php && php tests/UsageWordingSweepTest.php && php tests/ProEligibilityTest.php && php tests/ProInstallerMessageTest.php && php tests/TaglineTest.php && php tests/UpgradeTimelineTest.php && php tests/ProMenuFreshnessTest.php && php tests/ProUpdateTransientTest.php && php tests/AiProvidersSlugTest.php && php tests/TranslationContextTest.php && php tests/ForcedRetranslationTest.php && php tests/AdminScriptContextTest.php && php tests/IndexPagesTableTest.php && php tests/StoreBackfillResilienceTest.php && php tests/StoreConcurrentWorkTest.php && php tests/DashboardMetricsTest.php && php tests/DashboardMetricsSourceTest.php && php tests/LateLocaleTitleTest.php && php tests/MenusAiLabelTest.php && php tests/ConnectionResilienceTest.php && php tests/AddressReferenceTest.php && php tests/BrowserNotranslateTest.php && php tests/SetupRedirectNoticeTest.php && php tests/UrlSchemeTest.php && php tests/RebaseToHomeTest.php && php tests/DefaultLanguageMoveTest.php && php tests/LanguageSlugTest.php && php tests/SettingsTextTrimTest.php && php tests/IndexQueuePayloadTest.php && php tests/LexiconScopeTest.php && php tests/SwitcherSanitizeTest.php && php tests/SettingsRevisionTest.php && php tests/SettingsGroupsTest.php && php tests/ConnectChallengeTest.php && php tests/RetranslationEstimateTest.php && php tests/NoCreditsWordTest.php && php tests/SpineFirstTest.php && php tests/ScanProgressPayloadTest.php && php tests/RestNoCacheTest.php && php tests/PageCachePurgeTest.php && php tests/LexiconCandidatesEndpointTest.php && php tests/StyleVersionTest.php && php tests/SwitcherPositionEndpointTest.php && php tests/PluginCoexistenceTest.php && php tests/AdminNoticesTest.php && php tests/SwitcherPlacementTest.php && php tests/DecisionReferenceSweepTest.php && php tests/VersionConsistencyTest.php && php tests/PlatformRequirementsTest.php && php tests/VersionConsistencyPrereleaseTest.php && php tests/JobEventsPassThroughTest.php && php tests/UninstallBatchTest.php && php tests/UninstallEntryTest.php && php tests/UninstallWithProTest.php && php tests/TranslationSwitchRetirementTest.php && php tests/ProMenuActionTest.php && php tests/CachePluginNoticeTest.php && php tests/ProWelcomeTest.php && php tests/BundleContentsTest.php && php tests/AltTextGateTest.php && php tests/TextTranslationTest.php && php tests/EmailTemplateTest.php && php tests/TranslatedEmailsTest.php && php tests/SiteExportTest.php && php tests/ReadmeWordingTest.php && php tests/AltTextUpdateNoticeTest.php && php tests/UninstallDisconnectTest.php && php tests/UninstallCleanupTest.php && php tests/UninstallNetworkTest.php && php tests/DeactivationTest.php && php tests/AccessTest.php && php tests/AccessSurfacesTest.php && php tests/SupportReportTest.php && php tests/PlainTextTest.php && php tests/NotFoundTitleReadTest.php && php tests/AnchorMissesTest.php && php tests/AnswerEnginesDefaultTest.php && php tests/ServedTelemetryTest.php" + "test:php": "php tests/TranslatedNotWarmTest.php && php tests/QuotaHandoffTest.php && php tests/BrandingTest.php && php tests/SideTabTest.php && php tests/RoleCapabilitiesTest.php && php tests/AdminBarTest.php && php tests/BrandMenuIconTest.php && php tests/PluginRowTest.php && php tests/StyleConflictsTest.php && php tests/CacheScriptExclusionsTest.php && php tests/PageAreasTest.php && php tests/VisibleRolesTest.php && php tests/AdvancedStyleTest.php && php tests/TranslationReadinessTest.php && php tests/TranslationReadinessWiringTest.php && php tests/TranslationReadinessPacingTest.php && php tests/ReadinessSelfHealTest.php && php tests/SelfHealRepushTest.php && php tests/ReadinessSweepColdTest.php && php tests/ReadinessCursorTest.php && php tests/FirstPassPacingTest.php && php tests/SiteConnectionRouteTest.php && php tests/ConnectReturnPermalinkTest.php && php tests/RetryFailedTest.php && php tests/MissPayloadTest.php && php tests/OnboardingReturnTest.php && php tests/OnboardingResumeTest.php && php tests/ConnectNoticesTest.php && php tests/SiteConnectReturnTest.php && php tests/SiteLanguageSyncTest.php && php tests/SiteDisconnectTest.php && php tests/SiteAccountLabelTest.php && php tests/LyfecycleTest.php && php tests/RestErrorUnwrapTest.php && php tests/TranslationFingerprintGoldenTest.php && php tests/TranslationWhitespaceParityTest.php && php tests/TranslationProxyStorageTest.php && php tests/TranslationProxyClientIpTest.php && php tests/TranslationJobsProxyTest.php && php tests/IndexerTest.php && php tests/IndexerStoreBackfillTest.php && php tests/IndexQueueChangedPagesTest.php && php tests/IndexRereadChangedTest.php && php tests/TranslationProviderFailureTest.php && php tests/TranslationStoreTest.php && php tests/CronFallbackTest.php && php tests/MissProxyTest.php && php tests/TranslationUrlTest.php && php tests/LanguageUrlTest.php && php tests/LanguageRoutingTest.php && php tests/PageTextTranslatorTest.php && php tests/CrawlerTranslationTest.php && php tests/CrawlerResponseTest.php && php tests/StoreFromMissTest.php && php tests/StoreReadPathTest.php && php tests/StoreEvictionTest.php && php tests/StoreAnnouncesTranslationTest.php && php tests/NetworkConnectTest.php && php tests/NetworkConnectOutcomeTest.php && php tests/NetworkGrantRetirementTest.php && php tests/SwitcherCustomCssRetirementTest.php && php tests/MultisiteSettingsPageTest.php && php tests/MultisiteSettingsTest.php && php tests/SettingsScopeTest.php && php tests/SettingsScopeStateTest.php && php tests/SettingsLockedWriteTest.php && php tests/NetworkScopeTest.php && php tests/SubsiteScopeTest.php && php tests/KeepWarmRewarmTest.php && php tests/KeepWarmRereadTest.php && php tests/SEOCacheTest.php && php tests/SEOTest.php && php tests/SEOCoreCanonicalTest.php && php tests/SEOCanonicalPluginTest.php && php tests/SEOProVersionTest.php && php tests/SEOHtmlAttributesTest.php && php tests/SeoSettingsMergeTest.php && php tests/CrawlerCustomBotTest.php && php tests/LexiconOrderTest.php && php tests/LexiconEntitiesTest.php && php tests/ScriptConditionsTest.php && php tests/PageExclusionTest.php && php tests/PageExclusionMetaBoxTest.php && php tests/ConnectionInvalidationTest.php && php tests/AsyncScanTest.php && php tests/ContextScanSamplesTest.php && php tests/ClearCacheTest.php && php tests/OnboardingGateTest.php && php tests/CronKickTest.php && php tests/SetupPrefillTest.php && php tests/SaveStepResumeTest.php && php tests/SettingsResetTest.php && php tests/SwitcherSurfacesTest.php && php tests/SwitcherPlacementsContractTest.php && php tests/GutenbergSwitcherTest.php && php tests/SwitcherDefaultsTest.php && php tests/MigrationsTest.php && php tests/MigrationsHookTest.php && php tests/SharedStyleConditionsTest.php && php tests/EmDashSweepTest.php && php tests/UsageWordingSweepTest.php && php tests/ProEligibilityTest.php && php tests/ProInstallerMessageTest.php && php tests/TaglineTest.php && php tests/UpgradeTimelineTest.php && php tests/ProMenuFreshnessTest.php && php tests/ProUpdateTransientTest.php && php tests/AiProvidersSlugTest.php && php tests/TranslationContextTest.php && php tests/ForcedRetranslationTest.php && php tests/AdminScriptContextTest.php && php tests/IndexPagesTableTest.php && php tests/StoreBackfillResilienceTest.php && php tests/StoreConcurrentWorkTest.php && php tests/DashboardMetricsTest.php && php tests/DashboardMetricsSourceTest.php && php tests/LateLocaleTitleTest.php && php tests/MenusAiLabelTest.php && php tests/ConnectionResilienceTest.php && php tests/AddressReferenceTest.php && php tests/BrowserNotranslateTest.php && php tests/SetupRedirectNoticeTest.php && php tests/UrlSchemeTest.php && php tests/RebaseToHomeTest.php && php tests/DefaultLanguageMoveTest.php && php tests/LanguageSlugTest.php && php tests/SettingsTextTrimTest.php && php tests/IndexQueuePayloadTest.php && php tests/LexiconScopeTest.php && php tests/SwitcherSanitizeTest.php && php tests/SettingsRevisionTest.php && php tests/SettingsGroupsTest.php && php tests/ConnectChallengeTest.php && php tests/RetranslationEstimateTest.php && php tests/NoCreditsWordTest.php && php tests/SpineFirstTest.php && php tests/ScanProgressPayloadTest.php && php tests/RestNoCacheTest.php && php tests/PageCachePurgeTest.php && php tests/LexiconCandidatesEndpointTest.php && php tests/StyleVersionTest.php && php tests/SwitcherPositionEndpointTest.php && php tests/PluginCoexistenceTest.php && php tests/AdminNoticesTest.php && php tests/SwitcherPlacementTest.php && php tests/DecisionReferenceSweepTest.php && php tests/VersionConsistencyTest.php && php tests/PlatformRequirementsTest.php && php tests/VersionConsistencyPrereleaseTest.php && php tests/JobEventsPassThroughTest.php && php tests/UninstallBatchTest.php && php tests/UninstallEntryTest.php && php tests/UninstallWithProTest.php && php tests/TranslationSwitchRetirementTest.php && php tests/ProMenuActionTest.php && php tests/CachePluginNoticeTest.php && php tests/ProWelcomeTest.php && php tests/BundleContentsTest.php && php tests/AltTextGateTest.php && php tests/TextTranslationTest.php && php tests/EmailTemplateTest.php && php tests/TranslatedEmailsTest.php && php tests/SiteExportTest.php && php tests/ReadmeWordingTest.php && php tests/AltTextUpdateNoticeTest.php && php tests/UninstallDisconnectTest.php && php tests/UninstallCleanupTest.php && php tests/UninstallNetworkTest.php && php tests/DeactivationTest.php && php tests/AccessTest.php && php tests/AccessSurfacesTest.php && php tests/LexiconPrivacyTest.php && php tests/SupportReportTest.php && php tests/PlainTextTest.php && php tests/NotFoundTitleReadTest.php && php tests/AnchorMissesTest.php && php tests/AnswerEnginesDefaultTest.php && php tests/ServedTelemetryTest.php" } } diff --git a/src/shared/api/wordpress.ts b/src/shared/api/wordpress.ts index 9850673..59ca72c 100644 --- a/src/shared/api/wordpress.ts +++ b/src/shared/api/wordpress.ts @@ -60,7 +60,7 @@ export const getWordPressCategories = async () => export const getWordPressLexiconCategories = async () => { return await apiFetch({ - path: '/wp/v2/lexicon_category?per_page=100&hide_empty=false', + path: '/translatewp/v1/admin/get-lexicon-categories', method: 'GET' }); }; diff --git a/tests/AccessSurfacesTest.php b/tests/AccessSurfacesTest.php index 00e4f99..1f8b28e 100644 --- a/tests/AccessSurfacesTest.php +++ b/tests/AccessSurfacesTest.php @@ -104,6 +104,7 @@ function reachable_routes(): array { 'Admin POST update-lexicon-term', 'Admin POST delete-lexicon-term', 'Admin GET get-lexicon-terms', + 'Admin GET get-lexicon-categories', 'Admin GET settings-revision', 'Admin POST switcher-position', 'LexiconCandidates POST lexicon-candidates/scan', diff --git a/tests/LexiconPrivacyTest.php b/tests/LexiconPrivacyTest.php new file mode 100644 index 0000000..8634e9e --- /dev/null +++ b/tests/LexiconPrivacyTest.php @@ -0,0 +1,168 @@ + 19, 'name' => 'Brand', 'slug' => 'brand' ] ]; +( new Lexicon() )->register(); + +$post_type = $GLOBALS['wp_test_registered_post_types'][ Lexicon::POST_TYPE ] ?? []; +$taxonomy = $GLOBALS['lexicon_test_registered_taxonomies'][ Lexicon::CATEGORY_TAXONOMY ] ?? []; +foreach ( [ 'public', 'publicly_queryable', 'exclude_from_search', 'show_in_rest', 'has_archive', 'rewrite', 'query_var' ] as $flag ) { + $expected = 'exclude_from_search' === $flag; + assert_true( $expected === ( $post_type[ $flag ] ?? null ), "Lexicon post type {$flag} prevents a public read" ); +} +foreach ( [ 'public', 'show_in_rest', 'rewrite', 'query_var' ] as $flag ) { + assert_true( false === ( $taxonomy[ $flag ] ?? null ), "Lexicon category {$flag} prevents a public read" ); +} + +$admin = new Admin( 'translatewp', TRANSLATEWP_VERSION ); +$category_route = null; +foreach ( $admin->define_endpoints() as $route ) { + if ( 'get-lexicon-categories' === $route->getPath() ) { + $category_route = $route; + break; + } +} +assert_true( null !== $category_route, 'a private route supplies the Lexicon category picker' ); + +if ( null !== $category_route ) { + $allowed = $category_route->getPermissionCallback(); + $GLOBALS['wp_test_current_user'] = 0; + assert_true( false === $allowed(), 'anonymous visitors cannot read Lexicon categories' ); + + $GLOBALS['wp_test_current_user'] = 1; + $GLOBALS['wp_test_users'][1] = (object) [ 'ID' => 1, 'roles' => [ 'administrator' ] ]; + $GLOBALS['wp_test_user_caps'][1] = [ 'manage_options', 'edit_pages' ]; + $GLOBALS['wp_test_caps'] = $GLOBALS['wp_test_user_caps'][1]; + assert_true( true === $allowed(), 'an administrator can read Lexicon categories' ); + assert_true( + [ [ 'id' => 19, 'name' => 'Brand', 'slug' => 'brand' ] ] === $admin->get_lexicon_categories( new WP_REST_Request( 'GET', '' ) ), + 'the authorized category picker receives its categories' + ); +} + +wp_test_reset( [ + 'site' => [ 'id' => '00000000-0000-4000-8000-000000000001', 'token' => 'site-token' ], + 'languages' => [ 'list' => [ 'de' => [ 'locale' => 'de', 'active' => true, 'default' => false ] ] ], +] ); +$GLOBALS['wp_test_lexicon_posts'] = [ 77 => [ 'title' => 'Private Brand', 'content' => 'Private usage note' ] ]; +$GLOBALS['wp_test_lexicon_meta'] = [ 77 => [ Lexicon::LANGUAGES_META_KEY => [ 'de' ] ] ]; +$visitor = ( new TranslateWP( 'translatewp', TRANSLATEWP_VERSION ) )->frontend_data(); +assert_true( false !== strpos( $visitor, '"lexicon_terms":[]' ), 'visitor settings keep a valid empty Lexicon field' ); +assert_true( false === strpos( $visitor, 'Private Brand' ) && false === strpos( $visitor, 'Private usage note' ), 'visitor markup never carries the term or description' ); +$server = TranslationProxySettings::ai_settings( 'de' ); +assert_true( [ [ 'term' => 'Private Brand', 'description' => 'Private usage note' ] ] === $server['lexicon_terms'], 'server translation still receives the scoped term' ); + +$GLOBALS['wp_test_current_user'] = 7; +$GLOBALS['wp_test_current_user_id'] = 7; +$GLOBALS['wp_test_current_user_roles'] = [ 'editor' ]; +$GLOBALS['wp_test_users'] = [ (object) [ 'ID' => 7, 'roles' => [ 'editor' ] ] ]; +$GLOBALS['wp_test_user_caps'][7] = [ 'edit_pages' ]; +$GLOBALS['wp_test_caps'] = [ 'edit_pages' ]; +Access::set_role_map( [ 'manage_settings' => [ 'editor' => true ] ] ); +assert_true( Access::can( Access::SETTINGS ) && ! Access::can( Access::LEXICON ) && ! Access::can( Access::EDIT ), 'the fixture holds only Manage settings' ); +$settings_data = ( new AdminScript( 'translatewp', TRANSLATEWP_VERSION ) )->context_data(); +assert_true( false !== strpos( $settings_data, '"lexicon_terms":[]' ), 'a settings-only manager receives no Lexicon terms' ); +assert_true( false === strpos( $settings_data, 'Private Brand' ) && false === strpos( $settings_data, 'Private usage note' ), 'a settings-only manager cannot read Lexicon text in admin markup' ); +Access::set_role_map( [ 'manage_settings' => [ 'editor' => true ], 'manage_lexicon' => [ 'editor' => true ] ] ); +assert_true( Access::can( Access::LEXICON ), 'the fixture now holds Manage the Lexicon' ); +$lexicon_data = ( new AdminScript( 'translatewp', TRANSLATEWP_VERSION ) )->context_data(); +assert_true( false !== strpos( $lexicon_data, 'Private Brand' ) && false !== strpos( $lexicon_data, 'Private usage note' ), 'a Lexicon manager retains terms for authorized editing' ); + +$purges = 0; +wp_test_reset( [] ); +update_option( Migrations::VERSION_OPTION, '1.0.2' ); +wp_test_set_filter( PageCachePurge::ACTION, static function () use ( &$purges ): void { ++$purges; } ); +Migrations::maybe_run(); +Migrations::maybe_run(); +assert_true( 1 === $purges, 'an existing site purges page caches only once' ); +assert_true( (bool) get_option( Migrations::LEXICON_PRIVATE_CACHE_PURGED_OPTION ), 'the one-off marker is stored' ); + +wp_test_reset( [] ); +$purges = 0; +wp_test_set_filter( PageCachePurge::ACTION, static function () use ( &$purges ): void { ++$purges; } ); +Migrations::seed_for_fresh_install(); +Migrations::maybe_run(); +assert_true( 0 === $purges, 'a fresh installation does not purge existing page caches' ); + +wp_test_reset( [] ); +$purges = 0; +update_option( Migrations::VERSION_OPTION, '1.0.2' ); +wp_test_set_filter( PageCachePurge::ACTION, static function () use ( &$purges ): void { ++$purges; } ); +Migrations::seed_for_fresh_install(); +Migrations::maybe_run(); +assert_true( 1 === $purges, 'reactivating an existing site still purges its older public cache' ); + +wp_test_reset( [] ); +$purges = 0; +wp_test_set_filter( PageCachePurge::ACTION, static function () use ( &$purges ): void { ++$purges; } ); +Migrations::maybe_run(); +Migrations::maybe_run(); +assert_true( 0 === $purges, 'a subsite with no stored plugin version seeds the marker without purging' ); + +wp_test_reset( [] ); +$purges = 0; +update_option( Migrations::VERSION_OPTION, '999.0.0' ); +wp_test_set_filter( PageCachePurge::ACTION, static function () use ( &$purges ): void { ++$purges; } ); +Migrations::maybe_run(); +assert_true( 0 === $purges, 'running older plugin code after a newer version does not purge' ); + +wp_test_reset( [] ); +$purges = 0; +update_option( Migrations::VERSION_OPTION, '1.0.2' ); +wp_test_set_filter( PageCachePurge::ACTION, static function () use ( &$purges ): void { + ++$purges; + throw new RuntimeException( 'cache hook failed' ); +} ); +try { + Migrations::maybe_run(); +} catch ( RuntimeException $exception ) { + assert_true( 'cache hook failed' === $exception->getMessage(), 'the cache hook failure is visible' ); +} +Migrations::maybe_run(); +assert_true( 1 === $purges, 'a failing cache hook cannot trigger repeated whole-site purges' ); +assert_true( in_array( Migrations::LEXICON_PRIVATE_CACHE_PURGED_OPTION, CleanupInventory::INFRASTRUCTURE_OPTIONS, true ), 'uninstall removes the one-off marker' ); + +if ( $failures > 0 ) { + fwrite( STDERR, "LexiconPrivacyTest: {$failures} assertion(s) failed\n" ); + exit( 1 ); +} + +echo "LexiconPrivacyTest: all assertions passed\n"; diff --git a/tests/RoleCapabilitiesTest.php b/tests/RoleCapabilitiesTest.php index a1ec91d..1cfdfcc 100644 --- a/tests/RoleCapabilitiesTest.php +++ b/tests/RoleCapabilitiesTest.php @@ -135,10 +135,10 @@ function routes( array ...$sets ): array { return $all; } -const EDIT_ROUTES = [ 'Admin POST create-lexicon-term', 'Admin GET get-lexicon-terms' ]; +const EDIT_ROUTES = [ 'Admin POST create-lexicon-term', 'Admin GET get-lexicon-terms', 'Admin GET get-lexicon-categories' ]; const RETRANSLATE_ROUTES = [ 'Admin GET retranslation-estimate', 'Indexing POST retranslate-page', 'Indexing POST retranslate-all', 'KeepWarmEndpoints POST apply-lexicon-now', 'KeepWarmEndpoints POST apply-localizer-now' ]; const SETTINGS_ROUTES = [ 'Admin GET get-post-types', 'Admin GET get-roles', 'Admin GET get-data', 'Admin POST update-data', 'Admin GET settings-revision', 'Admin POST switcher-position', 'KeepWarmEndpoints GET stale-translations' ]; -const LEXICON_ROUTES = [ 'Admin POST create-lexicon-term', 'Admin POST update-lexicon-term', 'Admin POST delete-lexicon-term', 'Admin GET get-lexicon-terms', 'Admin GET get-data', 'KeepWarmEndpoints GET stale-translations', 'LexiconCandidates POST lexicon-candidates/scan', 'LexiconCandidates GET lexicon-candidates/scan', 'LexiconCandidates GET lexicon-candidates', 'LexiconCandidates POST lexicon-candidates/action' ]; +const LEXICON_ROUTES = [ 'Admin POST create-lexicon-term', 'Admin POST update-lexicon-term', 'Admin POST delete-lexicon-term', 'Admin GET get-lexicon-terms', 'Admin GET get-lexicon-categories', 'Admin GET get-data', 'KeepWarmEndpoints GET stale-translations', 'LexiconCandidates POST lexicon-candidates/scan', 'LexiconCandidates GET lexicon-candidates/scan', 'LexiconCandidates GET lexicon-candidates', 'LexiconCandidates POST lexicon-candidates/action' ]; /** * Whether the current user may read a settings path through get-data. @@ -195,7 +195,7 @@ function page_flags(): array { // --- Edit translations only --- editor_holds( [ 'edit_translations' ] ); -assert_true( routes( EDIT_ROUTES ) === reachable(), 'Edit only reaches Add to Lexicon and the term list, got ' . wp_json_encode( reachable() ) ); +assert_true( routes( EDIT_ROUTES ) === reachable(), 'Edit only reaches Add to Lexicon and the term and category lists, got ' . wp_json_encode( reachable() ) ); assert_true( [] === menus(), 'Edit only has no admin menu' ); assert_true( [ [ 'edit' => true, 'retranslate' => false, 'settings' => false, 'lexicon' => false ], false ] === page_flags(), 'Edit only: the edit flag, and canManage stays false' ); assert_true( ! in_array( 'Admin GET get-data', reachable(), true ) && ! in_array( 'Admin POST update-data', reachable(), true ), 'Edit only reaches no settings data' ); diff --git a/tests/wp-test-stubs.php b/tests/wp-test-stubs.php index 9ed2049..1010968 100644 --- a/tests/wp-test-stubs.php +++ b/tests/wp-test-stubs.php @@ -411,6 +411,7 @@ function wp_test_reset( array $options ): void { $GLOBALS['wp_test_queried_object_id'] = 0; $GLOBALS['wp_test_is_singular'] = false; $GLOBALS['wp_test_registered_post_meta'] = []; + $GLOBALS['wp_test_registered_post_types'] = []; $GLOBALS['wp_test_front_page_html'] = null; $GLOBALS['wp_test_front_page_fetches'] = 0; $GLOBALS['wp_test_remote_get_response'] = null; @@ -898,7 +899,7 @@ static function ( int $id ) use ( $post_id ): bool { } function register_post_type( $post_type, $args = [] ) { - unset( $post_type, $args ); + $GLOBALS['wp_test_registered_post_types'][ $post_type ] = $args; return true; }