diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index 6491da16d33..56bbb11930e 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -12,3 +12,4 @@ ### System - MySQL mutex locks and PHP session names are now namespaced using the application ID combined with the environment name. ([#15313](https://github.com/craftcms/cms/issues/15313)) +- `x-craft-preview` and `x-craft-live-preview` params are now hashed, and `craft\web\Request::getIsPreview()` will only return `true` if the param validates. ([#15605](https://github.com/craftcms/cms/discussions/15605)) diff --git a/src/controllers/ElementsController.php b/src/controllers/ElementsController.php index ccd00a037f4..3bb4c835180 100644 --- a/src/controllers/ElementsController.php +++ b/src/controllers/ElementsController.php @@ -24,6 +24,7 @@ use craft\helpers\Db; use craft\helpers\ElementHelper; use craft\helpers\Html; +use craft\helpers\StringHelper; use craft\helpers\UrlHelper; use craft\models\ElementActivity; use craft\models\FieldLayoutForm; @@ -440,6 +441,7 @@ public function actionEdit(?ElementInterface $element, ?int $elementId = null): 'isUnpublishedDraft' => $isUnpublishedDraft, 'previewTargets' => $previewTargets, 'previewToken' => $previewTargets ? $security->generateRandomString() : null, + 'previewParamValue' => $previewTargets ? $security->hashData(StringHelper::randomString(10)) : null, 'revisionId' => $element->revisionId, 'siteId' => $element->siteId, 'siteStatuses' => $siteStatuses, @@ -1423,11 +1425,13 @@ public function actionSaveDraft(): ?Response if ($this->request->getIsCpRequest()) { [$docTitle, $title] = $this->_editElementTitles($element); + $previewTargets = $element->getPreviewTargets(); $data += $this->_fieldLayoutData($element); $data += [ 'docTitle' => $docTitle, 'title' => $title, - 'previewTargets' => $element->getPreviewTargets(), + 'previewTargets' => $previewTargets, + 'previewParamValue' => $previewTargets ? Craft::$app->getSecurity()->hashData(StringHelper::randomString(10)) : null, 'initialDeltaValues' => Craft::$app->getView()->getInitialDeltaValues(), 'updatedTimestamp' => $element->dateUpdated->getTimestamp(), 'canonicalUpdatedTimestamp' => $element->getCanonical()->dateUpdated->getTimestamp(), diff --git a/src/web/Request.php b/src/web/Request.php index 49d9c7286f8..1bcd3270f75 100644 --- a/src/web/Request.php +++ b/src/web/Request.php @@ -696,11 +696,16 @@ public function getActionSegments(): ?array */ public function getIsPreview(): bool { - return ( - ($this->getQueryParam('x-craft-preview') ?? $this->getQueryParam('x-craft-live-preview')) !== null && - // If there's a token but it expired, they're looking at the live site - (!$this->getHadToken() || $this->getToken() !== null) - ); + $previewParamValue = $this->getQueryParam('x-craft-preview') ?? $this->getQueryParam('x-craft-live-preview'); + if (!$previewParamValue) { + return false; + } + if (!Craft::$app->getSecurity()->validateData($previewParamValue)) { + return false; + } + + // If there's a token but it expired, they're looking at the live site + return !$this->getHadToken() || $this->getToken() !== null; } /** diff --git a/src/web/assets/cp/dist/cp.js b/src/web/assets/cp/dist/cp.js index a7b530406f2..35bb8314d8c 100644 --- a/src/web/assets/cp/dist/cp.js +++ b/src/web/assets/cp/dist/cp.js @@ -1,3 +1,3 @@ /*! For license information please see cp.js.LICENSE.txt */ -(function(){var __webpack_modules__={463:function(){Craft.Accordion=Garnish.Base.extend({$trigger:null,targetSelector:null,_$target:null,init:function(t){var e=this;this.$trigger=$(t),this.$trigger.data("accordion")&&(console.warn("Double-instantiating an accordion trigger on an element"),this.$trigger.data("accordion").destroy()),this.$trigger.data("accordion",this),this.targetSelector=this.$trigger.attr("aria-controls")?"#".concat(this.$trigger.attr("aria-controls")):null,this.targetSelector&&(this._$target=$(this.targetSelector)),this.addListener(this.$trigger,"click","onTriggerClick"),this.addListener(this.$trigger,"keypress",(function(t){var i=t.keyCode;i!==Garnish.SPACE_KEY&&i!==Garnish.RETURN_KEY||(t.preventDefault(),e.onTriggerClick())}))},onTriggerClick:function(){"true"===this.$trigger.attr("aria-expanded")?this.hideTarget(this._$target):this.showTarget(this._$target)},showTarget:function(t){var e=this;if(t&&t.length){this.showTarget._currentHeight=t.height(),t.removeClass("hidden"),this.$trigger.removeClass("collapsed").addClass("expanded").attr("aria-expanded","true");for(var i=0;i .address-card");for(var n=0;n=this.settings.maxItems)){var e=$(t).appendTo(this.$tbody),i=e.find(".delete");this.settings.sortable&&this.sorter.addItems(e),this.$deleteBtns=this.$deleteBtns.add(i),this.addListener(i,"click","handleDeleteBtnClick"),this.totalItems++,this.updateUI()}},reorderItems:function(){var t=this;if(this.settings.sortable){for(var e=[],i=0;i=this.settings.maxItems?$(this.settings.newItemBtnSelector).addClass("hidden"):$(this.settings.newItemBtnSelector).removeClass("hidden"))}},{defaults:{tableSelector:null,noItemsSelector:null,newItemBtnSelector:null,idAttribute:"data-id",nameAttribute:"data-name",sortable:!1,allowDeleteAll:!0,minItems:0,maxItems:null,reorderAction:null,deleteAction:null,reorderSuccessMessage:Craft.t("app","New order saved."),reorderFailMessage:Craft.t("app","Couldn’t save new order."),confirmDeleteMessage:Craft.t("app","Are you sure you want to delete “{name}”?"),deleteSuccessMessage:Craft.t("app","“{name}” deleted."),deleteFailMessage:Craft.t("app","Couldn’t delete “{name}”."),onReorderItems:$.noop,onDeleteItem:$.noop}})},6872:function(){Craft.AssetImageEditor=Garnish.Modal.extend({$body:null,$footer:null,$imageTools:null,$buttons:null,$cancelBtn:null,$replaceBtn:null,$saveBtn:null,$focalPointBtn:null,$editorContainer:null,$straighten:null,$croppingCanvas:null,$spinner:null,$constraintContainer:null,$constraintRadioInputs:null,$customConstraints:null,canvas:null,image:null,viewport:null,focalPoint:null,grid:null,croppingCanvas:null,clipper:null,croppingRectangle:null,cropperHandles:null,cropperGrid:null,croppingShade:null,imageStraightenAngle:0,viewportRotation:0,originalWidth:0,originalHeight:0,imageVerticeCoords:null,zoomRatio:1,animationInProgress:!1,currentView:"",assetId:null,cacheBust:null,draggingCropper:!1,scalingCropper:!1,draggingFocal:!1,previousMouseX:0,previousMouseY:0,shiftKeyHeld:!1,editorHeight:0,editorWidth:0,cropperState:!1,scaleFactor:1,flipData:{},focalPointState:!1,maxImageSize:null,lastLoadedDimensions:null,imageIsLoading:!1,mouseMoveEvent:null,croppingConstraint:!1,constraintOrientation:"landscape",showingCustomConstraint:!1,saving:!1,renderImage:null,renderCropper:null,_queue:null,init:function(t,e){var i=this;this._queue=new Craft.Queue,this.cacheBust=Date.now(),this.setSettings(e,Craft.AssetImageEditor.defaults),null===this.settings.allowDegreeFractions&&(this.settings.allowDegreeFractions=Craft.isImagick),Garnish.prefersReducedMotion()&&(this.settings.animationDuration=1),this.assetId=t,this.flipData={x:0,y:0},this.$container=$('').appendTo(Garnish.$bod),this.$body=$('
').appendTo(this.$container),this.$footer=$('