From e123fa8b6358202b42ed85a2459e1740a602643b Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Wed, 3 Jun 2026 20:15:42 +0200 Subject: [PATCH 1/4] add js nonces (WP-1007) Co-Authored-By: Claude Sonnet 4.6 --- js/app.js | 5 ++++- js/configuration-profile-form.js | 1 + js/progress-tracker.js | 1 + js/smartling-connector-admin.js | 3 ++- js/smartling-submissions-check.js | 3 ++- 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/js/app.js b/js/app.js index 2e8a0a523..1c2140169 100644 --- a/js/app.js +++ b/js/app.js @@ -31,6 +31,7 @@ function JobWizard({ isBulkSubmitPage, contentType, contentId, locales, ajaxUrl, try { const response = await jQuery.post(adminUrl, { action: 'smartling_job_api_proxy', + _wpnonce: nonce, innerAction: 'list-jobs', params: {} }); @@ -50,7 +51,7 @@ function JobWizard({ isBulkSubmitPage, contentType, contentId, locales, ajaxUrl, const loadRelations = useCallback(async (type, id, level = 1) => { const localeList = locales.map(l => l.blogId).join(','); - const url = `${ajaxUrl}?action=smartling-get-relations&id=${id}&content-type=${type}&targetBlogIds=${localeList}`; + const url = `${ajaxUrl}?action=smartling-get-relations&id=${id}&content-type=${type}&targetBlogIds=${localeList}&_wpnonce=${encodeURIComponent(nonce)}`; setPendingRequests(prev => prev + 1); setTotalRequests(prev => prev + 1); @@ -245,6 +246,7 @@ function JobWizard({ isBulkSubmitPage, contentType, contentId, locales, ajaxUrl, const url = `${ajaxUrl}?action=smartling-create-submissions`; const data = { + _wpnonce: nonce, formAction: activeTab === 'clone' ? 'clone' : 'upload', source: { contentType, id: isBulkSubmitPage ? [] : [contentId] }, job: { @@ -282,6 +284,7 @@ function JobWizard({ isBulkSubmitPage, contentType, contentId, locales, ajaxUrl, if (activeTab === 'new') { const jobResponse = await jQuery.post(adminUrl, { action: 'smartling_job_api_proxy', + _wpnonce: nonce, innerAction: 'create-job', params: { jobName, diff --git a/js/configuration-profile-form.js b/js/configuration-profile-form.js index c156e7c24..ef049c8fe 100644 --- a/js/configuration-profile-form.js +++ b/js/configuration-profile-form.js @@ -21,6 +21,7 @@ e.preventDefault(); const data = { 'action': 'smartling_expert_global_settings_update', + '_wpnonce': (typeof smartlingProfileForm !== 'undefined' ? smartlingProfileForm.expertSettingsNonce : ''), 'params': { 'smartling_add_slashes_before_saving_content': $('#smartling_add_slashes_before_saving_content').val(), 'smartling_add_slashes_before_saving_meta': $('#smartling_add_slashes_before_saving_meta').val(), diff --git a/js/progress-tracker.js b/js/progress-tracker.js index f128d9345..842b00b95 100644 --- a/js/progress-tracker.js +++ b/js/progress-tracker.js @@ -37,6 +37,7 @@ this.deleteRecord = function (recordId) { $.post(window.deleteNotificationEndpoint, { + _wpnonce: window.deleteNotificationNonce || '', project_id: this.data.projectId, space_id: this.spaceId, object_id: this.object_id, diff --git a/js/smartling-connector-admin.js b/js/smartling-connector-admin.js index 3ff75b873..36ccb1837 100644 --- a/js/smartling-connector-admin.js +++ b/js/smartling-connector-admin.js @@ -269,7 +269,8 @@ function ajaxDownload() { $.post( ajaxurl + "?action=" + "smartling_force_download_handler", { - submissionIds: submissionIds.join(",") + submissionIds: submissionIds.join(","), + _wpnonce: (typeof smartlingConnector !== 'undefined' ? smartlingConnector.nonce : '') }, function (data) { switch (data.status) { diff --git a/js/smartling-submissions-check.js b/js/smartling-submissions-check.js index 6f1ba0e80..0fccc9878 100644 --- a/js/smartling-submissions-check.js +++ b/js/smartling-submissions-check.js @@ -52,7 +52,8 @@ url: ajaxurl, data: $.extend( { - action: 'ajax_submissions_update_status' + action: 'ajax_submissions_update_status', + _wpnonce: (typeof smartlingCheckStatus !== 'undefined' ? smartlingCheckStatus.nonce : '') }, this.data ), From 304cecc0b2e24431b45901248b75f74b9c5c5713 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Wed, 3 Jun 2026 20:16:53 +0200 Subject: [PATCH 2/4] add php nonces and check (WP-1007) Co-Authored-By: Claude Sonnet 4.6 --- inc/Smartling/Bootstrap.php | 6 ++++ inc/Smartling/ContentTypes/CustomPostType.php | 1 + .../ContentTypes/CustomTaxonomyType.php | 1 + inc/Smartling/Helpers/UiMessageHelper.php | 14 ++++++++-- .../Services/ContentRelationsHandler.php | 17 ++++++++++- .../WP/Controller/CheckStatusController.php | 9 ++++++ .../ConfigurationProfileFormController.php | 10 +++++++ .../ConfigurationProfilesController.php | 3 ++ .../Controller/ContentEditJobController.php | 28 +++++++++++++++++++ .../InstantTranslationController.php | 11 ++++++++ .../Controller/LiveNotificationController.php | 9 ++++++ .../PostBasedWidgetControllerStd.php | 13 +++++++++ .../WP/Controller/TaxonomyLinksController.php | 8 ++++++ .../WP/Controller/TestRunController.php | 6 ++++ .../WP/Controller/VisualConfiguratorPage.php | 23 +++++++++++---- .../WP/View/ConfigurationProfileForm.php | 3 +- inc/Smartling/WP/View/TaxonomyLinks.php | 1 + inc/Smartling/WP/View/TestRun.php | 1 + inc/config/services.yml | 1 + 19 files changed, 155 insertions(+), 10 deletions(-) diff --git a/inc/Smartling/Bootstrap.php b/inc/Smartling/Bootstrap.php index 6216e3b57..7a0a32df3 100644 --- a/inc/Smartling/Bootstrap.php +++ b/inc/Smartling/Bootstrap.php @@ -181,6 +181,12 @@ private function initRoles(): void #[NoReturn] public function updateGlobalExpertSettings(): void { + check_ajax_referer('smartling_expert_global_settings', '_wpnonce'); + if (!current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_PROFILE_CAP)) { + wp_send_json(['error' => 'Insufficient permissions'], 403); + return; + } + $data = $_POST['params']; $rawPageSize = (int)$data['pageSize']; diff --git a/inc/Smartling/ContentTypes/CustomPostType.php b/inc/Smartling/ContentTypes/CustomPostType.php index 8450f4e59..33e404b2c 100644 --- a/inc/Smartling/ContentTypes/CustomPostType.php +++ b/inc/Smartling/ContentTypes/CustomPostType.php @@ -95,6 +95,7 @@ protected function registerJobWidget(): void ->addArgument($di->getDefinition('site.helper')) ->addArgument($di->getDefinition('manager.submission')) ->addArgument($di->getDefinition('site.cache')) + ->addArgument($di->getDefinition('wp.proxy')) ->addMethodCall('setServedContentType', [$this->getSystemName()]); $di->get($tag)->register(); } diff --git a/inc/Smartling/ContentTypes/CustomTaxonomyType.php b/inc/Smartling/ContentTypes/CustomTaxonomyType.php index e955185d1..1fe3d7805 100644 --- a/inc/Smartling/ContentTypes/CustomTaxonomyType.php +++ b/inc/Smartling/ContentTypes/CustomTaxonomyType.php @@ -95,6 +95,7 @@ protected function registerJobWidget() ->addArgument($di->getDefinition('site.helper')) ->addArgument($di->getDefinition('manager.submission')) ->addArgument($di->getDefinition('site.cache')) + ->addArgument($di->getDefinition('wp.proxy')) ->addMethodCall('setServedContentType', [static::getSystemName()]) ->addMethodCall('setBaseType', ['taxonomy']); $di->get($tag)->register(); diff --git a/inc/Smartling/Helpers/UiMessageHelper.php b/inc/Smartling/Helpers/UiMessageHelper.php index 73bde9069..f034d059b 100644 --- a/inc/Smartling/Helpers/UiMessageHelper.php +++ b/inc/Smartling/Helpers/UiMessageHelper.php @@ -9,10 +9,17 @@ class UiMessageHelper public static function dismissMessage(): void { + check_ajax_referer(self::DISMISS_MESSAGE_ACTION, '_wpnonce'); + if (!current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_MENU_CAP)) { + wp_send_json_error(['message' => 'Insufficient permissions'], 403); + return; + } $cache = self::getCache(); - if (array_key_exists('hash', $_GET)) { - $cache->set(self::CACHE_KEY_PREFIX . $_GET['hash'], true, 60 * 60 * 180); + $hash = isset($_POST['hash']) ? sanitize_text_field(wp_unslash($_POST['hash'])) : ''; + if ($hash !== '') { + $cache->set(self::CACHE_KEY_PREFIX . $hash, true, 60 * 60 * 180); } + wp_send_json_success(); } public static function displayMessages(): void @@ -55,8 +62,9 @@ private static function getClickHandler(string $string): string { $action = self::DISMISS_MESSAGE_ACTION; $hash = self::getCacheHash($string); + $nonce = wp_create_nonce(self::DISMISS_MESSAGE_ACTION); return <<service = $service; @@ -78,6 +81,12 @@ public function register(): void */ public function createSubmissionsHandler(array $data = null): void { + $this->wpProxy->check_ajax_referer('smartling_instant_translation', '_wpnonce'); + if (!$this->wpProxy->current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_WIDGET_CAP)) { + $this->returnError('permission.denied', 'Insufficient permissions', 403); + return; + } + if ($data === null) { $data = $_POST; } @@ -95,6 +104,12 @@ public function createSubmissionsHandler(array $data = null): void public function actionHandler(): void { + $this->wpProxy->check_ajax_referer('smartling_instant_translation', '_wpnonce'); + if (!$this->wpProxy->current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_WIDGET_CAP)) { + $this->returnError('permission.denied', 'Insufficient permissions', 403); + return; + } + $data = $_GET; $data['targetBlogIds'] = $this->convertTargetBlogIds($data['targetBlogIds']); try { diff --git a/inc/Smartling/WP/Controller/CheckStatusController.php b/inc/Smartling/WP/Controller/CheckStatusController.php index d69798fb0..905358ade 100644 --- a/inc/Smartling/WP/Controller/CheckStatusController.php +++ b/inc/Smartling/WP/Controller/CheckStatusController.php @@ -21,6 +21,9 @@ public function wp_enqueue() wp_enqueue_script($this->pluginInfo->getName() . "submission", $this->pluginInfo ->getUrl() . 'js/smartling-submissions-check.js', ['jquery'], $this->pluginInfo ->getVersion(), false); + wp_localize_script($this->pluginInfo->getName() . "submission", 'smartlingCheckStatus', [ + 'nonce' => wp_create_nonce('smartling_check_status'), + ]); } public function register(): void @@ -36,6 +39,12 @@ public function register(): void */ public function ajaxHandler() { + check_ajax_referer('smartling_check_status', '_wpnonce'); + if (!current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_WIDGET_CAP)) { + wp_send_json(['error' => 'Insufficient permissions'], 403); + return false; + } + if ($_REQUEST["action"] === "ajax_submissions_update_status") { $items = $this->checkItems($_REQUEST["ids"]); diff --git a/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php b/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php index dda9fdd2a..0b287fbbe 100644 --- a/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php +++ b/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php @@ -31,6 +31,10 @@ public function wp_enqueue(): void foreach ($jsFiles as $jFile) { wp_enqueue_script($jFile, $jFile, ['jquery'], $ver, false); } + wp_localize_script($jsPath . 'configuration-profile-form.js', 'smartlingProfileForm', [ + 'testConnectionNonce' => wp_create_nonce('smartling_test_connection'), + 'expertSettingsNonce' => wp_create_nonce('smartling_expert_global_settings'), + ]); } public function register(): void @@ -47,6 +51,12 @@ public function register(): void public function initTestConnectionEndpoint(): void { + check_ajax_referer('smartling_test_connection', '_wpnonce'); + if (!current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_PROFILE_CAP)) { + wp_send_json(['status' => 403, 'message' => 'Insufficient permissions'], 403); + return; + } + $data =& $_POST; $result = [ diff --git a/inc/Smartling/WP/Controller/ConfigurationProfilesController.php b/inc/Smartling/WP/Controller/ConfigurationProfilesController.php index 96935a3ab..aad4f7796 100644 --- a/inc/Smartling/WP/Controller/ConfigurationProfilesController.php +++ b/inc/Smartling/WP/Controller/ConfigurationProfilesController.php @@ -52,6 +52,9 @@ public function wp_enqueue(): void $this->pluginInfo->getVersion(), false ); + wp_localize_script($this->pluginInfo->getName() . 'settings', 'smartlingConnector', [ + 'nonce' => wp_create_nonce('smartling_connector_ajax'), + ]); wp_enqueue_script( $this->pluginInfo->getName() . 'settings-admin-footer', $this->pluginInfo->getUrl() . 'js/smartling-connector-gutenberg-lock-attributes.js', diff --git a/inc/Smartling/WP/Controller/ContentEditJobController.php b/inc/Smartling/WP/Controller/ContentEditJobController.php index c97b73813..a201e570f 100644 --- a/inc/Smartling/WP/Controller/ContentEditJobController.php +++ b/inc/Smartling/WP/Controller/ContentEditJobController.php @@ -4,15 +4,21 @@ use DateTimeZone; use Exception; +use Smartling\ApiWrapperInterface; use Smartling\Bootstrap; +use Smartling\DbAl\LocalizationPluginProxyInterface; use Smartling\Exceptions\SmartlingApiException; use Smartling\Helpers\ArrayHelper; +use Smartling\Helpers\Cache; use Smartling\Helpers\DateTimeHelper; use Smartling\Helpers\DiagnosticsHelper; use Smartling\Helpers\HtmlTagGeneratorHelper; +use Smartling\Helpers\PluginInfo; use Smartling\Helpers\SiteHelper; use Smartling\Helpers\SmartlingUserCapabilities; +use Smartling\Helpers\WordpressFunctionProxyHelper; use Smartling\Settings\SettingsManager; +use Smartling\Submissions\SubmissionManager; use Smartling\Vendor\Smartling\Jobs\JobStatus; use Smartling\WP\WPAbstract; use Smartling\WP\WPHookInterface; @@ -20,6 +26,22 @@ class ContentEditJobController extends WPAbstract implements WPHookInterface { public const SMARTLING_JOB_API_PROXY = 'smartling_job_api_proxy'; + + private WordpressFunctionProxyHelper $wpProxy; + + public function __construct( + ApiWrapperInterface $api, + LocalizationPluginProxyInterface $localizationPluginProxy, + PluginInfo $pluginInfo, + SettingsManager $settingsManager, + SiteHelper $siteHelper, + SubmissionManager $submissionManager, + Cache $cache, + WordpressFunctionProxyHelper $wpProxy, + ) { + parent::__construct($api, $localizationPluginProxy, $pluginInfo, $settingsManager, $siteHelper, $submissionManager, $cache); + $this->wpProxy = $wpProxy; + } /** * @var string */ @@ -65,6 +87,12 @@ public function setServedContentType($servedContentType) public function initJobApiProxy(): void { add_action('wp_ajax_' . self::SMARTLING_JOB_API_PROXY, function () { + $this->wpProxy->check_ajax_referer('smartling_instant_translation', '_wpnonce'); + if (!$this->wpProxy->current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_WIDGET_CAP)) { + $this->wpProxy->wp_send_json(['status' => 403, 'message' => 'Insufficient permissions'], 403); + return; + } + $data =& $_POST; $result = [ diff --git a/inc/Smartling/WP/Controller/InstantTranslationController.php b/inc/Smartling/WP/Controller/InstantTranslationController.php index 25c2ba903..63884a37c 100644 --- a/inc/Smartling/WP/Controller/InstantTranslationController.php +++ b/inc/Smartling/WP/Controller/InstantTranslationController.php @@ -6,6 +6,7 @@ use Smartling\Helpers\DateTimeHelper; use Smartling\Helpers\FileUriHelper; use Smartling\Helpers\LoggerSafeTrait; +use Smartling\Helpers\SmartlingUserCapabilities; use Smartling\Helpers\WordpressFunctionProxyHelper; use Smartling\Submissions\SubmissionEntity; use Smartling\Submissions\SubmissionFactory; @@ -38,6 +39,11 @@ public function handleRequestTranslation(): void { $this->wpProxy->check_ajax_referer('smartling_instant_translation', '_wpnonce'); + if (!$this->wpProxy->current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_WIDGET_CAP)) { + $this->wpProxy->wp_send_json_error(['message' => 'Insufficient permissions'], 403); + return; + } + try { $contentType = $this->wpProxy->sanitize_text_field($this->wpProxy->wp_unslash($_POST['contentType'] ?? '')); $contentId = (int)($_POST['contentId'] ?? 0); @@ -135,6 +141,11 @@ public function handlePollStatus(): void { $this->wpProxy->check_ajax_referer('smartling_instant_translation', '_wpnonce'); + if (!$this->wpProxy->current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_WIDGET_CAP)) { + $this->wpProxy->wp_send_json_error(['message' => 'Insufficient permissions'], 403); + return; + } + try { $submissionId = (int)($_POST['submissionId'] ?? 0); diff --git a/inc/Smartling/WP/Controller/LiveNotificationController.php b/inc/Smartling/WP/Controller/LiveNotificationController.php index 87eda7d64..76093d875 100644 --- a/inc/Smartling/WP/Controller/LiveNotificationController.php +++ b/inc/Smartling/WP/Controller/LiveNotificationController.php @@ -8,6 +8,7 @@ use Smartling\Helpers\DiagnosticsHelper; use Smartling\Helpers\LoggerSafeTrait; use Smartling\Helpers\PluginInfo; +use Smartling\Helpers\SmartlingUserCapabilities; use Smartling\Models\NotificationParameters; use Smartling\Settings\SettingsManager; use Smartling\Submissions\SubmissionEntity; @@ -98,11 +99,13 @@ public function placeJsConfig(): void $wrapperClassName = static::UI_NOTIFICATION_IDENTIFIER_CLASS; $wrapperClassNameGeneral = static::UI_NOTIFICATION_IDENTIFIER_CLASS_GENERAL; + $deleteNonce = wp_create_nonce(self::DELETE_NOTIFICATION_ACTION_NAME); echo << var firebaseConfig = $configs; var deleteNotificationEndpoint = "$deleteEndpoint"; + var deleteNotificationNonce = "$deleteNonce"; var firebaseIds = $firebaseIds; var notificationClassName = "$wrapperClassName"; var notificationClassNameGeneral = "$wrapperClassNameGeneral"; @@ -113,6 +116,12 @@ public function placeJsConfig(): void public function deleteNotificationAjaxHandler(): void { + check_ajax_referer(self::DELETE_NOTIFICATION_ACTION_NAME, '_wpnonce'); + if (!current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_WIDGET_CAP)) { + wp_send_json(['code' => 'error', 'message' => 'Insufficient permissions'], 403); + return; + } + $data = $_POST; $projectId = $data['project_id']; diff --git a/inc/Smartling/WP/Controller/PostBasedWidgetControllerStd.php b/inc/Smartling/WP/Controller/PostBasedWidgetControllerStd.php index e56d5a007..130c4c282 100644 --- a/inc/Smartling/WP/Controller/PostBasedWidgetControllerStd.php +++ b/inc/Smartling/WP/Controller/PostBasedWidgetControllerStd.php @@ -25,6 +25,7 @@ class PostBasedWidgetControllerStd extends WPAbstract implements WPHookInterface private const WIDGET_NAME = 'smartling_connector_widget'; public const WIDGET_DATA_NAME = 'smartling'; private const CONNECTOR_NONCE = 'smartling_connector_nonce'; + private const AJAX_NONCE_ACTION = 'smartling_connector_ajax'; protected string $servedContentType = 'undefined'; protected string $needSave = 'Need to have title'; @@ -115,6 +116,12 @@ public function setNoOriginalFound($noOriginalFound) public function ajaxDownloadHandler(): void { + check_ajax_referer(self::AJAX_NONCE_ACTION, '_wpnonce'); + if (!current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_WIDGET_CAP)) { + wp_send_json(['status' => self::RESPONSE_AJAX_STATUS_FAIL, 'message' => 'Insufficient permissions'], 403); + return; + } + $logSubmissions = []; $result = ['status' => self::RESPONSE_AJAX_STATUS_SUCCESS]; $submissions = []; @@ -175,6 +182,12 @@ private function validateTargetBlog($blogId) public function ajaxUploadHandler() { + check_ajax_referer(self::AJAX_NONCE_ACTION, '_wpnonce'); + if (!current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_WIDGET_CAP)) { + wp_send_json(['status' => self::RESPONSE_AJAX_STATUS_FAIL, 'message' => 'Insufficient permissions'], 403); + return; + } + $result = []; $data = &$_POST; diff --git a/inc/Smartling/WP/Controller/TaxonomyLinksController.php b/inc/Smartling/WP/Controller/TaxonomyLinksController.php index af6d9516c..7e2040044 100644 --- a/inc/Smartling/WP/Controller/TaxonomyLinksController.php +++ b/inc/Smartling/WP/Controller/TaxonomyLinksController.php @@ -19,6 +19,8 @@ class TaxonomyLinksController extends WPAbstract implements WPHookInterface { + private const NONCE_ACTION = 'smartling_link_taxonomies'; + public function __construct( protected ApiWrapperInterface $api, PluginInfo $pluginInfo, @@ -150,6 +152,12 @@ private function getMappedTerms() public function linkTaxonomies($data) { + $this->wordpressProxy->check_ajax_referer(self::NONCE_ACTION, '_wpnonce'); + if (!$this->wordpressProxy->current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_MENU_CAP)) { + $this->wordpressProxy->wp_send_json_error(['message' => 'Insufficient permissions'], 403); + return; + } + if ($data === "") { $data = $_POST; } diff --git a/inc/Smartling/WP/Controller/TestRunController.php b/inc/Smartling/WP/Controller/TestRunController.php index d13c23919..047fab139 100644 --- a/inc/Smartling/WP/Controller/TestRunController.php +++ b/inc/Smartling/WP/Controller/TestRunController.php @@ -152,6 +152,12 @@ public function getBlogs(): array public function testRun($data): void { + check_ajax_referer('smartling_test_run', '_wpnonce'); + if (!current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_PROFILE_CAP)) { + wp_send_json_error(['message' => 'Insufficient permissions'], 403); + return; + } + if ($data === "") { $data = $_POST; } diff --git a/inc/Smartling/WP/Controller/VisualConfiguratorPage.php b/inc/Smartling/WP/Controller/VisualConfiguratorPage.php index 589dfb11b..67978e92d 100644 --- a/inc/Smartling/WP/Controller/VisualConfiguratorPage.php +++ b/inc/Smartling/WP/Controller/VisualConfiguratorPage.php @@ -87,7 +87,9 @@ public function enqueue(string $hook): void public function ajaxListRules(): void { - $this->verifyNonce(); + if (!$this->verifyNonce()) { + return; + } $this->rulesManager->loadData(); $rules = []; foreach ($this->rulesManager->listItems() as $id => $rule) { @@ -98,7 +100,9 @@ public function ajaxListRules(): void public function ajaxSaveRule(): void { - $this->verifyNonce(); + if (!$this->verifyNonce()) { + return; + } try { $payload = $this->readRulePayload(); } catch (\InvalidArgumentException $e) { @@ -137,7 +141,9 @@ public function ajaxSaveRule(): void public function ajaxResolveType(): void { - $this->verifyNonce(); + if (!$this->verifyNonce()) { + return; + } $id = isset($_POST['id']) ? (int)$_POST['id'] : 0; if ($id <= 0) { $this->wpProxy->wp_send_json_error(['message' => 'Missing or invalid id'], 400); @@ -153,7 +159,9 @@ public function ajaxResolveType(): void public function ajaxDeleteRule(): void { - $this->verifyNonce(); + if (!$this->verifyNonce()) { + return; + } $id = isset($_POST['id']) && is_string($_POST['id']) ? $this->wpProxy->sanitize_text_field($this->wpProxy->wp_unslash($_POST['id'])) : ''; @@ -169,9 +177,14 @@ public function ajaxDeleteRule(): void $this->wpProxy->wp_send_json_success(['id' => $id]); } - private function verifyNonce(): void + private function verifyNonce(): bool { $this->wpProxy->check_ajax_referer(self::NONCE_ACTION, '_wpnonce'); + if (!$this->wpProxy->current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_PROFILE_CAP)) { + $this->wpProxy->wp_send_json_error(['message' => 'Insufficient permissions'], 403); + return false; + } + return true; } /** diff --git a/inc/Smartling/WP/View/ConfigurationProfileForm.php b/inc/Smartling/WP/View/ConfigurationProfileForm.php index 884e8d75f..542b4cee0 100644 --- a/inc/Smartling/WP/View/ConfigurationProfileForm.php +++ b/inc/Smartling/WP/View/ConfigurationProfileForm.php @@ -39,8 +39,9 @@ $(function () { const queryProxy = { baseEndpoint: '?action=smartling_test_connection', + testConnectionNonce: '', getProjectLocales: function (params, success) { - $.post(this.baseEndpoint, params, function (response) { + $.post(this.baseEndpoint, $.extend({ _wpnonce: this.testConnectionNonce }, params), function (response) { success(response); }); }, diff --git a/inc/Smartling/WP/View/TaxonomyLinks.php b/inc/Smartling/WP/View/TaxonomyLinks.php index 1fa267af0..8cb9546fe 100644 --- a/inc/Smartling/WP/View/TaxonomyLinks.php +++ b/inc/Smartling/WP/View/TaxonomyLinks.php @@ -16,6 +16,7 @@

Linked items will not be sent for translation.

+ diff --git a/inc/Smartling/WP/View/TestRun.php b/inc/Smartling/WP/View/TestRun.php index 877a1345b..fe566b63e 100644 --- a/inc/Smartling/WP/View/TestRun.php +++ b/inc/Smartling/WP/View/TestRun.php @@ -20,6 +20,7 @@ if ($viewData->getTestBlogId() === null) { ?> +
diff --git a/inc/config/services.yml b/inc/config/services.yml index 023faf0ec..a18e52c71 100644 --- a/inc/config/services.yml +++ b/inc/config/services.yml @@ -226,6 +226,7 @@ services: class: Smartling\Services\ContentRelationsHandler arguments: - "@service.relations-discovery" + - "@wp.proxy" entrypoint: class: Smartling\Base\SmartlingCore From 5a9178af8a1ecfc83fd99d6a53e39f838c20b02e Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Wed, 3 Jun 2026 20:16:59 +0200 Subject: [PATCH 3/4] update tests (WP-1007) Co-Authored-By: Claude Sonnet 4.6 --- .../Services/ContentRelationsHandlerTest.php | 43 ++++++++++++++++++- .../InstantTranslationControllerTest.php | 36 ++++++++++++++++ .../TaxonomyLinksControllerTest.php | 30 +++++++++++++ .../Controller/VisualConfiguratorPageTest.php | 34 ++++++++++++++- 4 files changed, 139 insertions(+), 4 deletions(-) diff --git a/tests/Services/ContentRelationsHandlerTest.php b/tests/Services/ContentRelationsHandlerTest.php index 41880ddaa..0fe544adb 100644 --- a/tests/Services/ContentRelationsHandlerTest.php +++ b/tests/Services/ContentRelationsHandlerTest.php @@ -4,6 +4,7 @@ use PHPUnit\Framework\TestCase; use Smartling\Helpers\ArrayHelper; +use Smartling\Helpers\WordpressFunctionProxyHelper; use Smartling\Models\UserCloneRequest; use Smartling\Services\ContentRelationsDiscoveryService; use Smartling\Services\ContentRelationsHandler; @@ -11,13 +12,22 @@ class ContentRelationsHandlerTest extends TestCase { private $request; + private function makeWpProxy(bool $currentUserCan = true): WordpressFunctionProxyHelper + { + $proxy = $this->createMock(WordpressFunctionProxyHelper::class); + $proxy->method('check_ajax_referer')->willReturn(1); + $proxy->method('current_user_can')->willReturn($currentUserCan); + return $proxy; + } + public function testCreateSubmissionsHandlerCloneNoRelations() { $service = $this->createMock(ContentRelationsDiscoveryService::class); $service->expects($this->once())->method('clone')->willReturnCallback(function (UserCloneRequest $request) { $this->request = $request; }); - $x = new class($service) extends ContentRelationsHandler { + $proxy = $this->makeWpProxy(); + $x = new class($service, $proxy) extends ContentRelationsHandler { public function returnResponse(array $data, $responseCode = 200): void { } @@ -42,7 +52,8 @@ public function testCreateSubmissionsHandlerCloneRelations() $this->request = $request; }); $targetBlogId = 2; - $x = new class($service) extends ContentRelationsHandler { + $proxy = $this->makeWpProxy(); + $x = new class($service, $proxy) extends ContentRelationsHandler { public function returnResponse(array $data, $responseCode = 200): void { } @@ -65,4 +76,32 @@ public function returnError($key, $message, $responseCode = 400): void $this->assertEquals([1 => [$targetBlogId => ['post' => 3]], 2 => [$targetBlogId => ['attachment' => 5]]], $this->request->getRelationsOrdered()); $this->assertEquals([$targetBlogId => ['attachment' => 5]], ArrayHelper::first($this->request->getRelationsOrdered()), 'Should return deepest level first'); } + + public function testCreateSubmissionsHandlerReturns403WhenCapabilityMissing(): void + { + $service = $this->createMock(ContentRelationsDiscoveryService::class); + $service->expects($this->never())->method('clone'); + + $proxy = $this->makeWpProxy(false); + + $errorKey = null; + $errorCode = null; + $x = new class($service, $proxy) extends ContentRelationsHandler { + public function returnResponse(array $data, $responseCode = 200): void {} + + public function returnError($key, $message, $responseCode = 400): void + { + // Capture for assertions + $GLOBALS['_test_error_key'] = $key; + $GLOBALS['_test_error_code'] = $responseCode; + } + }; + + $x->createSubmissionsHandler(['formAction' => ContentRelationsHandler::FORM_ACTION_CLONE, 'source' => ['id' => [1], 'contentType' => 'post'], 'targetBlogIds' => '2']); + + $this->assertSame('permission.denied', $GLOBALS['_test_error_key'] ?? null); + $this->assertSame(403, $GLOBALS['_test_error_code'] ?? null); + + unset($GLOBALS['_test_error_key'], $GLOBALS['_test_error_code']); + } } diff --git a/tests/Smartling/WP/Controller/InstantTranslationControllerTest.php b/tests/Smartling/WP/Controller/InstantTranslationControllerTest.php index 168478972..0b67e2d62 100644 --- a/tests/Smartling/WP/Controller/InstantTranslationControllerTest.php +++ b/tests/Smartling/WP/Controller/InstantTranslationControllerTest.php @@ -492,4 +492,40 @@ public function testMapSubmissionStatusReturnsCorrectValues(): void $this->assertEquals('pending', $method->invoke($this->controller, SubmissionEntity::SUBMISSION_STATUS_NEW)); $this->assertEquals('pending', $method->invoke($this->controller, 'unknown_status')); } + + public function testHandleRequestTranslationReturns403WhenCapabilityMissing(): void + { + $this->wpProxy->method('check_ajax_referer')->willReturn(1); + $this->wpProxy->method('current_user_can')->willReturn(false); + + $errorArgs = null; + $this->wpProxy->method('wp_send_json_error')->willReturnCallback( + function (array $data, int $status) use (&$errorArgs) { + $errorArgs = ['data' => $data, 'status' => $status]; + } + ); + + $this->controller->handleRequestTranslation(); + + $this->assertNotNull($errorArgs); + $this->assertSame(403, $errorArgs['status']); + } + + public function testHandlePollStatusReturns403WhenCapabilityMissing(): void + { + $this->wpProxy->method('check_ajax_referer')->willReturn(1); + $this->wpProxy->method('current_user_can')->willReturn(false); + + $errorArgs = null; + $this->wpProxy->method('wp_send_json_error')->willReturnCallback( + function (array $data, int $status) use (&$errorArgs) { + $errorArgs = ['data' => $data, 'status' => $status]; + } + ); + + $this->controller->handlePollStatus(); + + $this->assertNotNull($errorArgs); + $this->assertSame(403, $errorArgs['status']); + } } diff --git a/tests/Smartling/WP/Controller/TaxonomyLinksControllerTest.php b/tests/Smartling/WP/Controller/TaxonomyLinksControllerTest.php index 51a1ec65f..9cd915cc2 100644 --- a/tests/Smartling/WP/Controller/TaxonomyLinksControllerTest.php +++ b/tests/Smartling/WP/Controller/TaxonomyLinksControllerTest.php @@ -111,5 +111,35 @@ private function getSiteHelperMock() $siteHelper->method('listBlogs')->willReturn([1, 2]); return $siteHelper; } + + public function testLinkTaxonomiesReturns403WhenCapabilityMissing(): void + { + $wordpress = $this->createMock(WordpressFunctionProxyHelper::class); + $wordpress->method('check_ajax_referer')->willReturn(1); + $wordpress->method('current_user_can')->willReturn(false); + + $errorCalled = false; + $wordpress->method('wp_send_json_error')->willReturnCallback( + function (array $data, int $status) use (&$errorCalled) { + $errorCalled = true; + TestCase::assertSame(403, $status); + } + ); + + $x = new TaxonomyLinksController( + $this->createMock(\Smartling\ApiWrapperInterface::class), + $this->getMockBuilder(\Smartling\Helpers\PluginInfo::class)->disableOriginalConstructor()->getMock(), + $this->createMock(\Smartling\Settings\SettingsManager::class), + $this->createMock(\Smartling\DbAl\LocalizationPluginProxyInterface::class), + $this->getSiteHelperMock(), + $this->getSubmissionManagerMock(), + $wordpress, + $this->createMock(\Smartling\Helpers\WpObjectCache::class), + ); + + $x->linkTaxonomies(''); + + $this->assertTrue($errorCalled); + } } } diff --git a/tests/Smartling/WP/Controller/VisualConfiguratorPageTest.php b/tests/Smartling/WP/Controller/VisualConfiguratorPageTest.php index 8392295b6..62c83a623 100644 --- a/tests/Smartling/WP/Controller/VisualConfiguratorPageTest.php +++ b/tests/Smartling/WP/Controller/VisualConfiguratorPageTest.php @@ -192,9 +192,39 @@ public function testAjaxDeleteRuleRemovesItem(): void $this->assertCount(0, $manager->listItems()); } - private function createWpProxy(): WordpressFunctionProxyHelper|MockObject + public function testAjaxListRulesReturns403WhenCapabilityMissing(): void { - return $this->createMock(WordpressFunctionProxyHelper::class); + $wpProxy = $this->createWpProxy(false); + + $errorCalled = false; + $wpProxy->method('wp_send_json_error')->willReturnCallback(function ($p, $status) use (&$errorCalled) { + $errorCalled = true; + $this->assertSame(403, $status); + }); + + $this->makeController(new JsonFieldRulesManager(), $wpProxy)->ajaxListRules(); + $this->assertTrue($errorCalled); + } + + public function testAjaxSaveRuleReturns403WhenCapabilityMissing(): void + { + $wpProxy = $this->createWpProxy(false); + + $errorCalled = false; + $wpProxy->method('wp_send_json_error')->willReturnCallback(function ($p, $status) use (&$errorCalled) { + $errorCalled = true; + $this->assertSame(403, $status); + }); + + $this->makeController(new JsonFieldRulesManager(), $wpProxy)->ajaxSaveRule(); + $this->assertTrue($errorCalled); + } + + private function createWpProxy(bool $currentUserCan = true): WordpressFunctionProxyHelper|MockObject + { + $proxy = $this->createMock(WordpressFunctionProxyHelper::class); + $proxy->method('current_user_can')->willReturn($currentUserCan); + return $proxy; } private function makeController(JsonFieldRulesManager $manager, WordpressFunctionProxyHelper $wpProxy): VisualConfiguratorPage From 3482984107877cf7c8f91ce4cd93059ae537b2c8 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Thu, 4 Jun 2026 09:30:42 +0200 Subject: [PATCH 4/4] address code review feedback (WP-1007) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - route all TaxonomyLinksController responses through wordpressProxy - remove unused testConnectionNonce from wp_localize_script - replace $GLOBALS capture in ContentRelationsHandlerTest with anonymous class instance properties - rename nonce action smartling_instant_translation → smartling_translation across all check_ajax_referer/wp_create_nonce call sites Co-Authored-By: Claude Sonnet 4.6 --- .../Services/ContentRelationsHandler.php | 4 ++-- .../ConfigurationProfileFormController.php | 1 - .../WP/Controller/ContentEditJobController.php | 2 +- .../Controller/InstantTranslationController.php | 4 ++-- .../WP/Controller/TaxonomyLinksController.php | 6 +++--- inc/Smartling/WP/View/BulkSubmit.php | 2 +- inc/Smartling/WP/View/ContentEditJob.php | 2 +- tests/Services/ContentRelationsHandlerTest.php | 16 +++++++--------- 8 files changed, 17 insertions(+), 20 deletions(-) diff --git a/inc/Smartling/Services/ContentRelationsHandler.php b/inc/Smartling/Services/ContentRelationsHandler.php index e3a48e42a..43e3e1246 100644 --- a/inc/Smartling/Services/ContentRelationsHandler.php +++ b/inc/Smartling/Services/ContentRelationsHandler.php @@ -81,7 +81,7 @@ public function register(): void */ public function createSubmissionsHandler(array $data = null): void { - $this->wpProxy->check_ajax_referer('smartling_instant_translation', '_wpnonce'); + $this->wpProxy->check_ajax_referer('smartling_translation', '_wpnonce'); if (!$this->wpProxy->current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_WIDGET_CAP)) { $this->returnError('permission.denied', 'Insufficient permissions', 403); return; @@ -104,7 +104,7 @@ public function createSubmissionsHandler(array $data = null): void public function actionHandler(): void { - $this->wpProxy->check_ajax_referer('smartling_instant_translation', '_wpnonce'); + $this->wpProxy->check_ajax_referer('smartling_translation', '_wpnonce'); if (!$this->wpProxy->current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_WIDGET_CAP)) { $this->returnError('permission.denied', 'Insufficient permissions', 403); return; diff --git a/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php b/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php index 0b287fbbe..37dea44d5 100644 --- a/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php +++ b/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php @@ -32,7 +32,6 @@ public function wp_enqueue(): void wp_enqueue_script($jFile, $jFile, ['jquery'], $ver, false); } wp_localize_script($jsPath . 'configuration-profile-form.js', 'smartlingProfileForm', [ - 'testConnectionNonce' => wp_create_nonce('smartling_test_connection'), 'expertSettingsNonce' => wp_create_nonce('smartling_expert_global_settings'), ]); } diff --git a/inc/Smartling/WP/Controller/ContentEditJobController.php b/inc/Smartling/WP/Controller/ContentEditJobController.php index a201e570f..9927d6ef7 100644 --- a/inc/Smartling/WP/Controller/ContentEditJobController.php +++ b/inc/Smartling/WP/Controller/ContentEditJobController.php @@ -87,7 +87,7 @@ public function setServedContentType($servedContentType) public function initJobApiProxy(): void { add_action('wp_ajax_' . self::SMARTLING_JOB_API_PROXY, function () { - $this->wpProxy->check_ajax_referer('smartling_instant_translation', '_wpnonce'); + $this->wpProxy->check_ajax_referer('smartling_translation', '_wpnonce'); if (!$this->wpProxy->current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_WIDGET_CAP)) { $this->wpProxy->wp_send_json(['status' => 403, 'message' => 'Insufficient permissions'], 403); return; diff --git a/inc/Smartling/WP/Controller/InstantTranslationController.php b/inc/Smartling/WP/Controller/InstantTranslationController.php index 63884a37c..5fcaf0b04 100644 --- a/inc/Smartling/WP/Controller/InstantTranslationController.php +++ b/inc/Smartling/WP/Controller/InstantTranslationController.php @@ -37,7 +37,7 @@ public function register(): void public function handleRequestTranslation(): void { - $this->wpProxy->check_ajax_referer('smartling_instant_translation', '_wpnonce'); + $this->wpProxy->check_ajax_referer('smartling_translation', '_wpnonce'); if (!$this->wpProxy->current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_WIDGET_CAP)) { $this->wpProxy->wp_send_json_error(['message' => 'Insufficient permissions'], 403); @@ -139,7 +139,7 @@ public function handleRequestTranslation(): void public function handlePollStatus(): void { - $this->wpProxy->check_ajax_referer('smartling_instant_translation', '_wpnonce'); + $this->wpProxy->check_ajax_referer('smartling_translation', '_wpnonce'); if (!$this->wpProxy->current_user_can(SmartlingUserCapabilities::SMARTLING_CAPABILITY_WIDGET_CAP)) { $this->wpProxy->wp_send_json_error(['message' => 'Insufficient permissions'], 403); diff --git a/inc/Smartling/WP/Controller/TaxonomyLinksController.php b/inc/Smartling/WP/Controller/TaxonomyLinksController.php index 7e2040044..fd5bf9d50 100644 --- a/inc/Smartling/WP/Controller/TaxonomyLinksController.php +++ b/inc/Smartling/WP/Controller/TaxonomyLinksController.php @@ -162,7 +162,7 @@ public function linkTaxonomies($data) $data = $_POST; } if (!isset($data['sourceBlogId'], $data['sourceId'], $data['taxonomy'])) { - wp_send_json_error('Required parameter missing'); + $this->wordpressProxy->wp_send_json_error('Required parameter missing'); } $sourceBlogId = (int)$data['sourceBlogId']; $sourceId = (int)$data['sourceId']; @@ -210,13 +210,13 @@ public function linkTaxonomies($data) } $submissions = array_merge($submissionsToAdd, $submissionsToUpdate); if (count(array_merge($submissions, $submissionsToDelete)) === 0) { - wp_send_json_error('No changes'); + $this->wordpressProxy->wp_send_json_error('No changes'); } $this->submissionManager->storeSubmissions($submissions); foreach ($submissionsToDelete as $submission) { $this->submissionManager->delete($submission); } - wp_send_json(['success' => true, 'submissions' => $this->getSubmissions()]); + $this->wordpressProxy->wp_send_json(['success' => true, 'submissions' => $this->getSubmissions()]); } /** diff --git a/inc/Smartling/WP/View/BulkSubmit.php b/inc/Smartling/WP/View/BulkSubmit.php index 4dce0ee8f..689240bb1 100644 --- a/inc/Smartling/WP/View/BulkSubmit.php +++ b/inc/Smartling/WP/View/BulkSubmit.php @@ -70,7 +70,7 @@ data-locales='' data-ajax-url="" data-admin-url="" - data-nonce=""> + data-nonce=""> diff --git a/tests/Services/ContentRelationsHandlerTest.php b/tests/Services/ContentRelationsHandlerTest.php index 0fe544adb..32b356030 100644 --- a/tests/Services/ContentRelationsHandlerTest.php +++ b/tests/Services/ContentRelationsHandlerTest.php @@ -84,24 +84,22 @@ public function testCreateSubmissionsHandlerReturns403WhenCapabilityMissing(): v $proxy = $this->makeWpProxy(false); - $errorKey = null; - $errorCode = null; $x = new class($service, $proxy) extends ContentRelationsHandler { + public ?string $capturedErrorKey = null; + public ?int $capturedErrorCode = null; + public function returnResponse(array $data, $responseCode = 200): void {} public function returnError($key, $message, $responseCode = 400): void { - // Capture for assertions - $GLOBALS['_test_error_key'] = $key; - $GLOBALS['_test_error_code'] = $responseCode; + $this->capturedErrorKey = $key; + $this->capturedErrorCode = $responseCode; } }; $x->createSubmissionsHandler(['formAction' => ContentRelationsHandler::FORM_ACTION_CLONE, 'source' => ['id' => [1], 'contentType' => 'post'], 'targetBlogIds' => '2']); - $this->assertSame('permission.denied', $GLOBALS['_test_error_key'] ?? null); - $this->assertSame(403, $GLOBALS['_test_error_code'] ?? null); - - unset($GLOBALS['_test_error_key'], $GLOBALS['_test_error_code']); + $this->assertSame('permission.denied', $x->capturedErrorKey); + $this->assertSame(403, $x->capturedErrorCode); } }