diff --git a/assets/modules/store/js/store.js b/assets/modules/store/js/store.js index 327f37e901..eada6f561d 100755 --- a/assets/modules/store/js/store.js +++ b/assets/modules/store/js/store.js @@ -15,6 +15,27 @@ function link(){ return mass[0]+'?id='+_GET['id']+'&a='+_GET['a']; } +// This module renders its own standalone document (see template/main.html), so it never +// gets the CSRF wiring that manager/media/script/main.js sets up for the rest of the +// Manager. Attach the token here instead, or every install/console_catalog call 403s. +function csrfToken(){ + var tokenMeta = document.querySelector('meta[name="csrf-token"]'); + return tokenMeta ? tokenMeta.getAttribute('content') : ''; +} +if (csrfToken()) { + // store.query() posts to the remote extras.evo.im catalog API, not the local Manager - + // only attach the token to same-origin calls, or the header trips a CORS preflight there. + $.ajaxSetup({ + beforeSend: function (xhr, settings) { + var a = document.createElement('a'); + a.href = settings.url; + if (a.protocol === window.location.protocol && a.host === window.location.host) { + xhr.setRequestHeader('X-CSRF-TOKEN', csrfToken()); + } + } + }); +} + function store_search(val){ $('.item_list .catalog_item').each(function(){ var search_name = $(this).find('h3').text() || ''; @@ -291,7 +312,8 @@ store = { }); if ($(elm).attr('data-method') == "package"){ - var install_url = link() + "&action=install&cid="+$(elm).attr('data-id')+"&name="+$(elm).attr('data-name')+"&dependencies="+$(elm).attr('data-dependencies')+"&file="+file; + // A plain iframe navigation can't carry a header, so the token has to ride in the URL here. + var install_url = link() + "&action=install&cid="+$(elm).attr('data-id')+"&name="+$(elm).attr('data-name')+"&dependencies="+$(elm).attr('data-dependencies')+"&file="+file+"&_token="+encodeURIComponent(csrfToken()); $.fancybox.open({ href : install_url, type: 'iframe', diff --git a/assets/modules/store/template/main.html b/assets/modules/store/template/main.html index df0f0d9598..2657b57c7b 100644 --- a/assets/modules/store/template/main.html +++ b/assets/modules/store/template/main.html @@ -1,6 +1,9 @@ + + Evolution CMS Store diff --git a/core/src/Services/Store/ModuleViewService.php b/core/src/Services/Store/ModuleViewService.php index 717e3b1dd5..0bfad9e3ab 100644 --- a/core/src/Services/Store/ModuleViewService.php +++ b/core/src/Services/Store/ModuleViewService.php @@ -23,6 +23,7 @@ public function render($store, $version) ? rtrim(EVO_CORE_PATH, '/\\') : rtrim(EVO_BASE_PATH, '/\\') . '/core'; $store->lang['system_task_ui_flags'] = json_encode($store->getSystemTaskUiFlags(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $store->lang['csrf_token'] = csrf_token(); if ((int) ($modx->config['manager_theme_mode'] ?? 0) === 4) { $store->lang['body_class_name'] = 'darkness'; diff --git a/manager/media/script/main.js b/manager/media/script/main.js index caa9dfaa58..68cff85d7b 100644 --- a/manager/media/script/main.js +++ b/manager/media/script/main.js @@ -1,3 +1,45 @@ +// Attaches the CSRF token (see the meta tag in header.blade.php) to every same-origin +// XMLHttpRequest so VerifyCsrfToken doesn't reject Manager actions that never post through +// a
- the package Store's install/console_catalog polling, evo.js popups, etc. +// Patching XMLHttpRequest itself (rather than jQuery.ajaxSetup) covers both raw XHR calls +// and jQuery's $.ajax in one place, since jQuery's transport is built on the same native XHR. +(function () { + 'use strict'; + var tokenMeta = document.querySelector('meta[name="csrf-token"]'); + if (!tokenMeta || typeof XMLHttpRequest === 'undefined') { + return; + } + + function isSameOrigin(url) { + try { + var a = document.createElement('a'); + a.href = url; + return a.protocol === window.location.protocol && a.host === window.location.host; + } catch (error) { + return false; + } + } + + var nativeOpen = XMLHttpRequest.prototype.open; + XMLHttpRequest.prototype.open = function (method, url) { + this.__evoCsrfEligible = isSameOrigin(url); + return nativeOpen.apply(this, arguments); + }; + + var nativeSend = XMLHttpRequest.prototype.send; + XMLHttpRequest.prototype.send = function () { + if (this.__evoCsrfEligible) { + try { + this.setRequestHeader('X-CSRF-TOKEN', tokenMeta.getAttribute('content')); + } catch (error) { + // send() was reached without a matching open() (or in an otherwise invalid + // state) - let the underlying XHR surface that failure itself. + } + } + return nativeSend.apply(this, arguments); + }; +})(); + // evoTooltips evo.tooltips = function (a) { 'use strict';