diff --git a/inc/Smartling/Bootstrap.php b/inc/Smartling/Bootstrap.php index 6216e3b5..7a0a32df 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 8450f4e5..33e404b2 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 e955185d..1fe3d780 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 73bde906..f034d059 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_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_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 d69798fb..905358ad 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 dda9fdd2..37dea44d 100644 --- a/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php +++ b/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php @@ -31,6 +31,9 @@ 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', [ + 'expertSettingsNonce' => wp_create_nonce('smartling_expert_global_settings'), + ]); } public function register(): void @@ -47,6 +50,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 96935a3a..aad4f779 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 c97b7381..9927d6ef 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_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 25c2ba90..5fcaf0b0 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; @@ -36,7 +37,12 @@ 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); + return; + } try { $contentType = $this->wpProxy->sanitize_text_field($this->wpProxy->wp_unslash($_POST['contentType'] ?? '')); @@ -133,7 +139,12 @@ 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); + return; + } try { $submissionId = (int)($_POST['submissionId'] ?? 0); diff --git a/inc/Smartling/WP/Controller/LiveNotificationController.php b/inc/Smartling/WP/Controller/LiveNotificationController.php index 87eda7d6..76093d87 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 e56d5a00..130c4c28 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 af6d9516..fd5bf9d5 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,11 +152,17 @@ 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; } 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']; @@ -202,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/Controller/TestRunController.php b/inc/Smartling/WP/Controller/TestRunController.php index d13c2391..047fab13 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 589dfb11..67978e92 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/BulkSubmit.php b/inc/Smartling/WP/View/BulkSubmit.php index 4dce0ee8..689240bb 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/inc/Smartling/WP/View/TaxonomyLinks.php b/inc/Smartling/WP/View/TaxonomyLinks.php index 1fa267af..8cb9546f 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 877a1345..fe566b63 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 023faf0e..a18e52c7 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 diff --git a/js/app.js b/js/app.js index 2e8a0a52..1c214016 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 c156e7c2..ef049c8f 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 f128d934..842b00b9 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 3ff75b87..36ccb183 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 6f1ba0e8..0fccc987 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 ), diff --git a/tests/Services/ContentRelationsHandlerTest.php b/tests/Services/ContentRelationsHandlerTest.php index 41880dda..32b35603 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,30 @@ 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); + + $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 + { + $this->capturedErrorKey = $key; + $this->capturedErrorCode = $responseCode; + } + }; + + $x->createSubmissionsHandler(['formAction' => ContentRelationsHandler::FORM_ACTION_CLONE, 'source' => ['id' => [1], 'contentType' => 'post'], 'targetBlogIds' => '2']); + + $this->assertSame('permission.denied', $x->capturedErrorKey); + $this->assertSame(403, $x->capturedErrorCode); + } } diff --git a/tests/Smartling/WP/Controller/InstantTranslationControllerTest.php b/tests/Smartling/WP/Controller/InstantTranslationControllerTest.php index 16847897..0b67e2d6 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 51a1ec65..9cd915cc 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 8392295b..62c83a62 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