[BUGFIX] Reimplement previewing of date / usergroup restricted content 27/59927/24
authorSusanne Moog <susanne.moog@typo3.com>
Fri, 8 Mar 2019 17:04:44 +0000 (18:04 +0100)
committerSusanne Moog <look@susi.dev>
Thu, 16 Jan 2020 10:35:59 +0000 (11:35 +0100)
Preview functionality was only implemented in the Admin Panel. Previewing
itself (as in being able to preview pages with access or user restrictions)
should also work without having the admin panel installed and open.

The basic process is now like this:
- Backend generates preview URLs for pages with access restrictions
-- starttime, endtime, fe groups
--> parameters ADMCMD_simUser and ADMCMD_simTime are appended to the FE URL
- Frontend PreviewSimulator Middleware uses these parameters to modify
the current Context
- Adminpanel - if installed and open - takes given parameters as settings
for preview date/time/group - when user changes those, they are overwritten

Technical Changes:
- BackendUtility: Enable link generation for a specified context
- DateTimeAspect: Add new property to aspect to mirror SIM_ACCESS_TIME global
- PageRepository: Use new DateTimeAspect context property for enable fields
- AdminPanel: Set $_GET params in settings if given, remove $_GET vars if user
saves admin panel settings (to allow user to change date/time in AdminPanel)

Resolves: #86653
Releases: master, 9.5
Change-Id: I3a2302845461e9c18f9349438e10f1c059a85e48
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/59927
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Daniel Goerz <daniel.goerz@posteo.de>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Daniel Goerz <daniel.goerz@posteo.de>
Reviewed-by: Benni Mack <benni@typo3.org>
18 files changed:
Build/Sources/TypeScript/adminpanel/Resources/Public/TypeScript/AdminPanel.ts
Build/Sources/TypeScript/adminpanel/Resources/Public/TypeScript/Modules/Preview.ts
typo3/sysext/adminpanel/Classes/Modules/PreviewModule.php
typo3/sysext/adminpanel/Resources/Public/JavaScript/AdminPanel.js
typo3/sysext/adminpanel/Resources/Public/JavaScript/Modules/Preview.js
typo3/sysext/adminpanel/Tests/Unit/Modules/PreviewModuleTest.php
typo3/sysext/backend/Classes/Utility/BackendUtility.php
typo3/sysext/core/Classes/Context/DateTimeAspect.php
typo3/sysext/core/Classes/Domain/Repository/PageRepository.php
typo3/sysext/core/Classes/Routing/PageRouter.php
typo3/sysext/core/Classes/Routing/RouterInterface.php
typo3/sysext/core/Classes/Site/Entity/Site.php
typo3/sysext/core/Tests/Functional/Domain/Repository/PageRepositoryTest.php
typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Storage/Typo3DbQueryParserTest.php
typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
typo3/sysext/frontend/Classes/Middleware/PreviewSimulator.php [new file with mode: 0644]
typo3/sysext/frontend/Configuration/RequestMiddlewares.php
typo3/sysext/install/Configuration/ExtensionScanner/Php/InterfaceMethodChangedMatcher.php

index 8dab859..3791bb4 100644 (file)
@@ -180,7 +180,7 @@ namespace TYPO3 {
       const request = new XMLHttpRequest();
       request.open('POST', this.adminPanel.dataset.typo3AjaxUrl);
       request.send(formData);
-      request.onload = () => location.reload();
+      request.onload = () => location.assign(this.getCleanReloadUrl());
     }
 
     private toggleAdminPanelState(): void {
@@ -190,6 +190,24 @@ namespace TYPO3 {
       request.onload = () => location.reload();
     }
 
+    /**
+     * When previewing access/time restricted packs from the backend, "ADMCMD_" parameters are attached to the URL
+     * - their settings will be saved in the admin panel. To make sure that the user is able to change those settings
+     * via the Admin Panel User Interface the $_GET parameters are removed from the URL after saving and the page is
+     * reloaded
+     */
+    private getCleanReloadUrl(): string {
+      let urlParams: string[] = [];
+      location.search.substr(1).split('&').forEach((item: string): void => {
+        if (item && !item.includes('ADMCMD_')) {
+          urlParams.push(item);
+        }
+      });
+
+      const queryString = urlParams ? '?' + urlParams.join('&') : '';
+      return location.origin + location.pathname + queryString;
+    }
+
     private addBackdropListener(): void {
       this.querySelectorAll('.' + AdminPanelClasses.backdrop)
         .forEach((elm: HTMLElement) =>  {
index 0b94eea..61510d1 100644 (file)
@@ -4,19 +4,22 @@ namespace TYPO3 {
     private readonly timeField: HTMLInputElement = null;
     private readonly targetField: HTMLInputElement = null;
 
+    /**
+     * Initialize date and time fields of preview
+     *
+     * PHP / backend side always uses UTC timestamps (for generating time based previews and access time checks)
+     * Date and Time fields are HTML5 input fields, combined they update the "targetfield" always containing a PHP
+     * compatible (seconds-based) timestamp
+     */
     constructor() {
       this.dateField = <HTMLInputElement>document.getElementById('preview_simulateDate-date-hr');
       this.timeField = <HTMLInputElement>document.getElementById('preview_simulateDate-time-hr');
       this.targetField = <HTMLInputElement>document.getElementById(this.dateField.dataset.target);
 
       if (this.targetField.value) {
-        const cd = new Date(this.targetField.value);
-        this.dateField.value =
-          cd.getFullYear() + '-' + ((cd.getMonth() + 1) < 10 ? '0' : '')
-          + (cd.getMonth() + 1) + '-' + (cd.getDate() < 10 ? '0' : '') + cd.getDate();
-        this.timeField.value =
-          (cd.getHours() < 10 ? '0' : '') + cd.getHours() + ':'
-          + (cd.getMinutes() < 10 ? '0' : '') + cd.getMinutes();
+        const initialDate = new Date(parseInt(this.targetField.value, 10) * 1000);
+        this.dateField.valueAsDate = initialDate;
+        this.timeField.valueAsDate = initialDate;
       }
 
       this.dateField.addEventListener('change', this.updateDateField);
@@ -40,7 +43,7 @@ namespace TYPO3 {
         const stringDate = dateVal + ' ' + timeVal;
         const date = new Date(stringDate);
 
-        this.targetField.value = date.toISOString();
+        this.targetField.value = (date.valueOf() / 1000).toString();
       }
     }
   }
index a41a8a1..1231784 100644 (file)
@@ -74,11 +74,14 @@ class PreviewModule extends AbstractModule implements RequestEnricherInterface,
      */
     public function enrich(ServerRequestInterface $request): ServerRequestInterface
     {
+        // Backend preview params (ADMCMD_) take precedence over configured admin panel values
+        $simulateGroupByRequest = (int)($request->getQueryParams()['ADMCMD_simUser'] ?? 0);
+        $simulateTimeByRequest = (int)($request->getQueryParams()['ADMCMD_simTime'] ?? 0);
         $this->config = [
             'showHiddenPages' => (bool)$this->getConfigOptionForModule('showHiddenPages'),
-            'simulateDate' => $this->getConfigOptionForModule('simulateDate'),
+            'simulateDate' => $simulateTimeByRequest ?: (int)$this->getConfigOptionForModule('simulateDate'),
             'showHiddenRecords' => (bool)$this->getConfigOptionForModule('showHiddenRecords'),
-            'simulateUserGroup' => (int)$this->getConfigOptionForModule('simulateUserGroup'),
+            'simulateUserGroup' => $simulateGroupByRequest ?: (int)$this->getConfigOptionForModule('simulateUserGroup'),
             'showFluidDebug' => (bool)$this->getConfigOptionForModule('showFluidDebug'),
         ];
         if ($this->config['showFluidDebug']) {
@@ -150,13 +153,14 @@ class PreviewModule extends AbstractModule implements RequestEnricherInterface,
      *
      * @param bool $showHiddenPages
      * @param bool $showHiddenRecords
-     * @param string $simulateDate
+     * @param int $simulateDate
      * @param int $simulateUserGroup UID of the fe_group to simulate
+     * @throws \Exception
      */
     protected function initializeFrontendPreview(
         bool $showHiddenPages,
         bool $showHiddenRecords,
-        string $simulateDate,
+        int $simulateDate,
         int $simulateUserGroup
     ): void {
         $context = GeneralUtility::makeInstance(Context::class);
@@ -212,29 +216,23 @@ class PreviewModule extends AbstractModule implements RequestEnricherInterface,
     }
 
     /**
-     * @param string $simulateDate
+     * The simulated date needs to be a timestring (UTC)
+     *
+     * Simulation date is either set via configuration of AdminPanel (Date and Time Fields) or via ADMCMD_ $_GET
+     * parameter from backend previews
+     *
+     * @param int $simulateDate
      * @return int
      */
-    protected function parseDate(string $simulateDate): ?int
+    protected function parseDate(int $simulateDate): ?int
     {
-        $simTime = null;
-        $date = false;
         try {
-            $date = new \DateTime($simulateDate);
-        } catch (\Exception $e) {
-            if (is_numeric($simulateDate)) {
-                try {
-                    $date = new \DateTime('@' . $simulateDate);
-                } catch (\Exception $e) {
-                    $date = false;
-                }
-            }
-        }
-        if ($date !== false) {
-            $simTime = $date->getTimestamp();
+            $simTime = (new \DateTime('@' . $simulateDate))->getTimestamp();
             $simTime = max($simTime, 60);
+        } catch (\Exception $e) {
+            $simTime = null;
         }
-        return $simTime ?? null;
+        return $simTime;
     }
 
     protected function clearPreviewSettings(Context $context): void
index d3887aa..f390ab1 100644 (file)
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-"use strict";var TYPO3;!function(e){e.AdminPanelSelectors={adminPanelRole:"form[data-typo3-role=typo3-adminPanel]",moduleTriggerRole:"[data-typo3-role=typo3-adminPanel-module-trigger]",moduleParentClass:".typo3-adminPanel-module",contentTabRole:"[data-typo3-role=typo3-adminPanel-content-tab]",saveButtonRole:"[data-typo3-role=typo3-adminPanel-saveButton]",triggerRole:"[data-typo3-role=typo3-adminPanel-trigger]",popupTriggerRole:"[data-typo3-role=typo3-adminPanel-popup-trigger]",panelTriggerRole:"[data-typo3-role=typo3-adminPanel-panel-trigger]",panelParentClass:".typo3-adminPanel-panel",contentSettingsTriggerRole:"[data-typo3-role=typo3-adminPanel-content-settings]",contentSettingsParentClass:".typo3-adminPanel-content-settings",contentParentClass:".typo3-adminPanel-content",zoomTarget:"[data-typo3-zoom-target]",zoomClose:"[data-typo3-zoom-close]",currentContentRole:"[data-typo3-role=typo3-adminPanel-content]",contentPaneRole:"[data-typo3-role=typo3-adminPanel-content-pane]"},e.AdminPanelClasses={active:"active",activeModule:"typo3-adminPanel-module-active",activeContentSetting:"typo3-adminPanel-content-settings-active",backdrop:"typo3-adminPanel-backdrop",activeTab:"typo3-adminPanel-content-header-item-active",activePane:"typo3-adminPanel-content-panes-item-active",noScroll:"typo3-adminPanel-noscroll",zoomShow:"typo3-adminPanel-zoom-show"};e.AdminPanel=class{constructor(){this.adminPanel=document.querySelector(e.AdminPanelSelectors.adminPanelRole),this.modules=this.querySelectorAll(e.AdminPanelSelectors.moduleTriggerRole).map(t=>{const n=t.closest(e.AdminPanelSelectors.moduleParentClass);return new s(this,n,t)}),this.popups=this.querySelectorAll(e.AdminPanelSelectors.popupTriggerRole).map(e=>new t(this,e)),this.panels=this.querySelectorAll(e.AdminPanelSelectors.panelTriggerRole).map(t=>{const a=t.closest(e.AdminPanelSelectors.panelParentClass);return new n(a,t)}),this.contentSettings=this.querySelectorAll(e.AdminPanelSelectors.contentSettingsTriggerRole).map(t=>{const n=t.closest(e.AdminPanelSelectors.contentParentClass).querySelector(e.AdminPanelSelectors.contentSettingsParentClass);return new a(n,t)}),this.trigger=document.querySelector(e.AdminPanelSelectors.triggerRole),this.initializeEvents(),this.addBackdropListener()}disableModules(){this.modules.forEach(e=>e.disable())}disablePopups(){this.popups.forEach(e=>e.disable())}renderBackdrop(){const t=document.querySelector("#TSFE_ADMIN_PANEL_FORM"),n=document.createElement("div");document.querySelector("body").classList.add(e.AdminPanelClasses.noScroll),n.classList.add(e.AdminPanelClasses.backdrop),t.appendChild(n),this.addBackdropListener()}removeBackdrop(){const t=document.querySelector("."+e.AdminPanelClasses.backdrop);document.querySelector("body").classList.remove(e.AdminPanelClasses.noScroll),null!==t&&t.remove()}querySelectorAll(e,t=null){return null===t?Array.from(document.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}initializeEvents(){this.querySelectorAll(e.AdminPanelSelectors.contentTabRole).forEach(e=>e.addEventListener("click",this.switchTab.bind(this))),this.querySelectorAll(e.AdminPanelSelectors.zoomTarget).forEach(e=>e.addEventListener("click",this.openZoom.bind(this))),this.querySelectorAll(e.AdminPanelSelectors.zoomClose).forEach(e=>e.addEventListener("click",this.closeZoom.bind(this))),this.querySelectorAll(e.AdminPanelSelectors.triggerRole).forEach(e=>e.addEventListener("click",this.toggleAdminPanelState.bind(this))),this.querySelectorAll(e.AdminPanelSelectors.saveButtonRole).forEach(e=>e.addEventListener("click",this.sendAdminPanelForm.bind(this))),this.querySelectorAll("[data-typo3-role=typo3-adminPanel-content-close]").forEach(e=>{e.addEventListener("click",()=>{this.disableModules(),this.removeBackdrop()})}),this.querySelectorAll(".typo3-adminPanel-table th, .typo3-adminPanel-table td").forEach(e=>{e.addEventListener("click",()=>{e.focus();try{document.execCommand("copy")}catch(e){}})})}switchTab(t){t.preventDefault();const n=e.AdminPanelClasses.activeTab,a=e.AdminPanelClasses.activePane,s=t.currentTarget,l=s.closest(e.AdminPanelSelectors.currentContentRole),i=this.querySelectorAll(e.AdminPanelSelectors.contentTabRole,l),o=this.querySelectorAll(e.AdminPanelSelectors.contentPaneRole,l);i.forEach(e=>e.classList.remove(n)),s.classList.add(n),o.forEach(e=>e.classList.remove(a)),document.querySelector("[data-typo3-tab-id="+s.dataset.typo3TabTarget+"]").classList.add(a)}openZoom(t){t.preventDefault();const n=t.currentTarget.getAttribute("data-typo3-zoom-target");document.querySelector("[data-typo3-zoom-id="+n+"]").classList.add(e.AdminPanelClasses.zoomShow)}closeZoom(t){t.preventDefault(),t.currentTarget.closest("[data-typo3-zoom-id]").classList.remove(e.AdminPanelClasses.zoomShow)}sendAdminPanelForm(e){e.preventDefault();const t=new FormData(this.adminPanel),n=new XMLHttpRequest;n.open("POST",this.adminPanel.dataset.typo3AjaxUrl),n.send(t),n.onload=()=>location.reload()}toggleAdminPanelState(){const e=new XMLHttpRequest;e.open("GET",this.trigger.dataset.typo3AjaxUrl),e.send(),e.onload=()=>location.reload()}addBackdropListener(){this.querySelectorAll("."+e.AdminPanelClasses.backdrop).forEach(t=>{t.addEventListener("click",()=>{this.removeBackdrop(),this.querySelectorAll(e.AdminPanelSelectors.moduleTriggerRole).forEach(t=>{t.closest(e.AdminPanelSelectors.moduleParentClass).classList.remove(e.AdminPanelClasses.activeModule)})})})}};class t{constructor(e,t){this.adminPanel=e,this.element=t,this.initializeEvents()}isActive(){return this.element.classList.contains(e.AdminPanelClasses.active)}enable(){this.element.classList.add(e.AdminPanelClasses.active)}disable(){this.element.classList.remove(e.AdminPanelClasses.active)}initializeEvents(){this.element.addEventListener("click",()=>{this.isActive()?this.disable():(this.adminPanel.disablePopups(),this.enable())})}}class n{constructor(e,t){this.element=e,this.trigger=t,this.initializeEvents()}isActive(){return this.element.classList.contains(e.AdminPanelClasses.active)}enable(){this.element.classList.add(e.AdminPanelClasses.active)}disable(){this.element.classList.remove(e.AdminPanelClasses.active)}initializeEvents(){this.trigger.addEventListener("click",()=>{this.isActive()?this.disable():this.enable()})}}class a{constructor(e,t){this.element=e,this.trigger=t,this.initializeEvents()}isActive(){return this.element.classList.contains(e.AdminPanelClasses.activeContentSetting)}enable(){this.element.classList.add(e.AdminPanelClasses.activeContentSetting)}disable(){this.element.classList.remove(e.AdminPanelClasses.activeContentSetting)}initializeEvents(){this.trigger.addEventListener("click",()=>{this.isActive()?this.disable():this.enable()})}}class s{constructor(e,t,n){this.adminPanel=e,this.element=t,this.trigger=n,this.initializeEvents()}isActive(){return this.element.classList.contains(e.AdminPanelClasses.activeModule)}enable(){this.element.classList.add(e.AdminPanelClasses.activeModule)}disable(){this.element.classList.remove(e.AdminPanelClasses.activeModule)}initializeEvents(){this.trigger.addEventListener("click",()=>{this.adminPanel.removeBackdrop(),this.isActive()?this.disable():(this.adminPanel.disableModules(),this.adminPanel.renderBackdrop(),this.enable())})}}}(TYPO3||(TYPO3={})),window.addEventListener("load",()=>new TYPO3.AdminPanel,!1);
\ No newline at end of file
+"use strict";var TYPO3;!function(e){e.AdminPanelSelectors={adminPanelRole:"form[data-typo3-role=typo3-adminPanel]",moduleTriggerRole:"[data-typo3-role=typo3-adminPanel-module-trigger]",moduleParentClass:".typo3-adminPanel-module",contentTabRole:"[data-typo3-role=typo3-adminPanel-content-tab]",saveButtonRole:"[data-typo3-role=typo3-adminPanel-saveButton]",triggerRole:"[data-typo3-role=typo3-adminPanel-trigger]",popupTriggerRole:"[data-typo3-role=typo3-adminPanel-popup-trigger]",panelTriggerRole:"[data-typo3-role=typo3-adminPanel-panel-trigger]",panelParentClass:".typo3-adminPanel-panel",contentSettingsTriggerRole:"[data-typo3-role=typo3-adminPanel-content-settings]",contentSettingsParentClass:".typo3-adminPanel-content-settings",contentParentClass:".typo3-adminPanel-content",zoomTarget:"[data-typo3-zoom-target]",zoomClose:"[data-typo3-zoom-close]",currentContentRole:"[data-typo3-role=typo3-adminPanel-content]",contentPaneRole:"[data-typo3-role=typo3-adminPanel-content-pane]"},e.AdminPanelClasses={active:"active",activeModule:"typo3-adminPanel-module-active",activeContentSetting:"typo3-adminPanel-content-settings-active",backdrop:"typo3-adminPanel-backdrop",activeTab:"typo3-adminPanel-content-header-item-active",activePane:"typo3-adminPanel-content-panes-item-active",noScroll:"typo3-adminPanel-noscroll",zoomShow:"typo3-adminPanel-zoom-show"};e.AdminPanel=class{constructor(){this.adminPanel=document.querySelector(e.AdminPanelSelectors.adminPanelRole),this.modules=this.querySelectorAll(e.AdminPanelSelectors.moduleTriggerRole).map(t=>{const n=t.closest(e.AdminPanelSelectors.moduleParentClass);return new s(this,n,t)}),this.popups=this.querySelectorAll(e.AdminPanelSelectors.popupTriggerRole).map(e=>new t(this,e)),this.panels=this.querySelectorAll(e.AdminPanelSelectors.panelTriggerRole).map(t=>{const a=t.closest(e.AdminPanelSelectors.panelParentClass);return new n(a,t)}),this.contentSettings=this.querySelectorAll(e.AdminPanelSelectors.contentSettingsTriggerRole).map(t=>{const n=t.closest(e.AdminPanelSelectors.contentParentClass).querySelector(e.AdminPanelSelectors.contentSettingsParentClass);return new a(n,t)}),this.trigger=document.querySelector(e.AdminPanelSelectors.triggerRole),this.initializeEvents(),this.addBackdropListener()}disableModules(){this.modules.forEach(e=>e.disable())}disablePopups(){this.popups.forEach(e=>e.disable())}renderBackdrop(){const t=document.querySelector("#TSFE_ADMIN_PANEL_FORM"),n=document.createElement("div");document.querySelector("body").classList.add(e.AdminPanelClasses.noScroll),n.classList.add(e.AdminPanelClasses.backdrop),t.appendChild(n),this.addBackdropListener()}removeBackdrop(){const t=document.querySelector("."+e.AdminPanelClasses.backdrop);document.querySelector("body").classList.remove(e.AdminPanelClasses.noScroll),null!==t&&t.remove()}querySelectorAll(e,t=null){return null===t?Array.from(document.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}initializeEvents(){this.querySelectorAll(e.AdminPanelSelectors.contentTabRole).forEach(e=>e.addEventListener("click",this.switchTab.bind(this))),this.querySelectorAll(e.AdminPanelSelectors.zoomTarget).forEach(e=>e.addEventListener("click",this.openZoom.bind(this))),this.querySelectorAll(e.AdminPanelSelectors.zoomClose).forEach(e=>e.addEventListener("click",this.closeZoom.bind(this))),this.querySelectorAll(e.AdminPanelSelectors.triggerRole).forEach(e=>e.addEventListener("click",this.toggleAdminPanelState.bind(this))),this.querySelectorAll(e.AdminPanelSelectors.saveButtonRole).forEach(e=>e.addEventListener("click",this.sendAdminPanelForm.bind(this))),this.querySelectorAll("[data-typo3-role=typo3-adminPanel-content-close]").forEach(e=>{e.addEventListener("click",()=>{this.disableModules(),this.removeBackdrop()})}),this.querySelectorAll(".typo3-adminPanel-table th, .typo3-adminPanel-table td").forEach(e=>{e.addEventListener("click",()=>{e.focus();try{document.execCommand("copy")}catch(e){}})})}switchTab(t){t.preventDefault();const n=e.AdminPanelClasses.activeTab,a=e.AdminPanelClasses.activePane,s=t.currentTarget,l=s.closest(e.AdminPanelSelectors.currentContentRole),i=this.querySelectorAll(e.AdminPanelSelectors.contentTabRole,l),o=this.querySelectorAll(e.AdminPanelSelectors.contentPaneRole,l);i.forEach(e=>e.classList.remove(n)),s.classList.add(n),o.forEach(e=>e.classList.remove(a)),document.querySelector("[data-typo3-tab-id="+s.dataset.typo3TabTarget+"]").classList.add(a)}openZoom(t){t.preventDefault();const n=t.currentTarget.getAttribute("data-typo3-zoom-target");document.querySelector("[data-typo3-zoom-id="+n+"]").classList.add(e.AdminPanelClasses.zoomShow)}closeZoom(t){t.preventDefault(),t.currentTarget.closest("[data-typo3-zoom-id]").classList.remove(e.AdminPanelClasses.zoomShow)}sendAdminPanelForm(e){e.preventDefault();const t=new FormData(this.adminPanel),n=new XMLHttpRequest;n.open("POST",this.adminPanel.dataset.typo3AjaxUrl),n.send(t),n.onload=()=>location.assign(this.getCleanReloadUrl())}toggleAdminPanelState(){const e=new XMLHttpRequest;e.open("GET",this.trigger.dataset.typo3AjaxUrl),e.send(),e.onload=()=>location.reload()}getCleanReloadUrl(){let e=[];location.search.substr(1).split("&").forEach(t=>{t&&!t.includes("ADMCMD_")&&e.push(t)});const t=e?"?"+e.join("&"):"";return location.origin+location.pathname+t}addBackdropListener(){this.querySelectorAll("."+e.AdminPanelClasses.backdrop).forEach(t=>{t.addEventListener("click",()=>{this.removeBackdrop(),this.querySelectorAll(e.AdminPanelSelectors.moduleTriggerRole).forEach(t=>{t.closest(e.AdminPanelSelectors.moduleParentClass).classList.remove(e.AdminPanelClasses.activeModule)})})})}};class t{constructor(e,t){this.adminPanel=e,this.element=t,this.initializeEvents()}isActive(){return this.element.classList.contains(e.AdminPanelClasses.active)}enable(){this.element.classList.add(e.AdminPanelClasses.active)}disable(){this.element.classList.remove(e.AdminPanelClasses.active)}initializeEvents(){this.element.addEventListener("click",()=>{this.isActive()?this.disable():(this.adminPanel.disablePopups(),this.enable())})}}class n{constructor(e,t){this.element=e,this.trigger=t,this.initializeEvents()}isActive(){return this.element.classList.contains(e.AdminPanelClasses.active)}enable(){this.element.classList.add(e.AdminPanelClasses.active)}disable(){this.element.classList.remove(e.AdminPanelClasses.active)}initializeEvents(){this.trigger.addEventListener("click",()=>{this.isActive()?this.disable():this.enable()})}}class a{constructor(e,t){this.element=e,this.trigger=t,this.initializeEvents()}isActive(){return this.element.classList.contains(e.AdminPanelClasses.activeContentSetting)}enable(){this.element.classList.add(e.AdminPanelClasses.activeContentSetting)}disable(){this.element.classList.remove(e.AdminPanelClasses.activeContentSetting)}initializeEvents(){this.trigger.addEventListener("click",()=>{this.isActive()?this.disable():this.enable()})}}class s{constructor(e,t,n){this.adminPanel=e,this.element=t,this.trigger=n,this.initializeEvents()}isActive(){return this.element.classList.contains(e.AdminPanelClasses.activeModule)}enable(){this.element.classList.add(e.AdminPanelClasses.activeModule)}disable(){this.element.classList.remove(e.AdminPanelClasses.activeModule)}initializeEvents(){this.trigger.addEventListener("click",()=>{this.adminPanel.removeBackdrop(),this.isActive()?this.disable():(this.adminPanel.disableModules(),this.adminPanel.renderBackdrop(),this.enable())})}}}(TYPO3||(TYPO3={})),window.addEventListener("load",()=>new TYPO3.AdminPanel,!1);
\ No newline at end of file
index e4e3840..1d7ef84 100644 (file)
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-"use strict";var TYPO3;!function(e){e.Preview=class{constructor(){if(this.dateField=null,this.timeField=null,this.targetField=null,this.updateDateField=()=>{let e=this.dateField.value,t=this.timeField.value;if(!e&&t){let t=new Date;e=t.getFullYear()+"-"+(t.getMonth()+1)+"-"+t.getDate()}if(e&&!t&&(t="00:00"),e||t){const i=new Date(e+" "+t);this.targetField.value=i.toISOString()}else this.targetField.value=""},this.dateField=document.getElementById("preview_simulateDate-date-hr"),this.timeField=document.getElementById("preview_simulateDate-time-hr"),this.targetField=document.getElementById(this.dateField.dataset.target),this.targetField.value){const e=new Date(this.targetField.value);this.dateField.value=e.getFullYear()+"-"+(e.getMonth()+1<10?"0":"")+(e.getMonth()+1)+"-"+(e.getDate()<10?"0":"")+e.getDate(),this.timeField.value=(e.getHours()<10?"0":"")+e.getHours()+":"+(e.getMinutes()<10?"0":"")+e.getMinutes()}this.dateField.addEventListener("change",this.updateDateField),this.timeField.addEventListener("change",this.updateDateField)}}}(TYPO3||(TYPO3={})),window.addEventListener("load",()=>new TYPO3.Preview,!1);
\ No newline at end of file
+"use strict";var TYPO3;!function(e){e.Preview=class{constructor(){if(this.dateField=null,this.timeField=null,this.targetField=null,this.updateDateField=()=>{let e=this.dateField.value,t=this.timeField.value;if(!e&&t){let t=new Date;e=t.getFullYear()+"-"+(t.getMonth()+1)+"-"+t.getDate()}if(e&&!t&&(t="00:00"),e||t){const i=new Date(e+" "+t);this.targetField.value=(i.valueOf()/1e3).toString()}else this.targetField.value=""},this.dateField=document.getElementById("preview_simulateDate-date-hr"),this.timeField=document.getElementById("preview_simulateDate-time-hr"),this.targetField=document.getElementById(this.dateField.dataset.target),this.targetField.value){const e=new Date(1e3*parseInt(this.targetField.value,10));this.dateField.valueAsDate=e,this.timeField.valueAsDate=e}this.dateField.addEventListener("change",this.updateDateField),this.timeField.addEventListener("change",this.updateDateField)}}}(TYPO3||(TYPO3={})),window.addEventListener("load",()=>new TYPO3.Preview,!1);
\ No newline at end of file
index 67db7af..40c0bdd 100644 (file)
@@ -13,14 +13,9 @@ use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 class PreviewModuleTest extends UnitTestCase
 {
-    public function simulateDateDataProvider()
+    public function simulateDateDataProvider(): array
     {
         return [
-            'datetime' => [
-                '2018-01-01T12:00:15Z',
-                (int)(new \DateTime('2018-01-01 12:00:15 UTC'))->getTimestamp(),
-                (new \DateTime('2018-01-01 12:00:00 UTC'))->getTimestamp(),
-            ],
             'timestamp' => [
                 (string)(new \DateTime('2018-01-01 12:00:15 UTC'))->getTimestamp(),
                 (int)(new \DateTime('2018-01-01 12:00:15 UTC'))->getTimestamp(),
index eb6ef0c..4bebbbe 100644 (file)
@@ -24,6 +24,8 @@ use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
 use TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent;
 use TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader;
 use TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Context\DateTimeAspect;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
@@ -2398,7 +2400,9 @@ class BackendUtility
         } else {
             $permissionClause = $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW);
             $pageInfo = self::readPageAccess($pageUid, $permissionClause);
-            $additionalGetVars .= self::ADMCMD_previewCmds($pageInfo);
+            // prepare custom context for link generation (to allow for example time based previews)
+            $context = clone GeneralUtility::makeInstance(Context::class);
+            $additionalGetVars .= self::ADMCMD_previewCmds($pageInfo, $context);
 
             // Build the URL with a site as prefix, if configured
             $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
@@ -2417,7 +2421,7 @@ class BackendUtility
                 unset($additionalQueryParams['L']);
             }
             try {
-                $previewUrl = (string)$site->getRouter()->generateUri(
+                $previewUrl = (string)$site->getRouter($context)->generateUri(
                     $pageUid,
                     $additionalQueryParams,
                     $anchorSection,
@@ -3841,10 +3845,11 @@ class BackendUtility
      * Creates ADMCMD parameters for the "viewpage" extension / frontend
      *
      * @param array $pageInfo Page record
+     * @param \TYPO3\CMS\Core\Context\Context $context
      * @return string Query-parameters
      * @internal
      */
-    public static function ADMCMD_previewCmds($pageInfo)
+    public static function ADMCMD_previewCmds($pageInfo, Context $context)
     {
         $simUser = '';
         $simTime = '';
@@ -3868,11 +3873,24 @@ class BackendUtility
                 $simUser = '&ADMCMD_simUser=' . $activeFeGroupRow['uid'];
             }
         }
-        if ($pageInfo['starttime'] > $GLOBALS['EXEC_TIME']) {
-            $simTime = '&ADMCMD_simTime=' . $pageInfo['starttime'];
-        }
-        if ($pageInfo['endtime'] < $GLOBALS['EXEC_TIME'] && $pageInfo['endtime'] != 0) {
-            $simTime = '&ADMCMD_simTime=' . ($pageInfo['endtime'] - 1);
+        $startTime = (int)$pageInfo['starttime'];
+        $endTime = (int)$pageInfo['endtime'];
+        if ($startTime > $GLOBALS['EXEC_TIME']) {
+            // simulate access time to ensure PageRepository will find the page and in turn PageRouter will generate
+            // an URL for it
+            $dateAspect = GeneralUtility::makeInstance(DateTimeAspect::class, new \DateTimeImmutable('@' . $startTime));
+            $context->setAspect('date', $dateAspect);
+            $simTime = '&ADMCMD_simTime=' . $startTime;
+        }
+        if ($endTime < $GLOBALS['EXEC_TIME'] && $endTime !== 0) {
+            // Set access time to page's endtime subtracted one second to ensure PageRepository will find the page and
+            // in turn PageRouter will generate an URL for it
+            $dateAspect = GeneralUtility::makeInstance(
+                DateTimeAspect::class,
+                new \DateTimeImmutable('@' . ($endTime - 1))
+            );
+            $context->setAspect('date', $dateAspect);
+            $simTime = '&ADMCMD_simTime=' . ($endTime - 1);
         }
         return $simUser . $simTime;
     }
index 8ba2e67..30d83d2 100644 (file)
@@ -62,6 +62,8 @@ class DateTimeAspect implements AspectInterface
                 return $this->dateTimeObject->format('e');
             case 'full':
                 return $this->dateTimeObject;
+            case 'accessTime':
+                return $this->dateTimeObject->format('U') - ($this->dateTimeObject->format('U') % 60);
         }
         throw new AspectPropertyNotFoundException('Property "' . $name . '" not found in Aspect "' . __CLASS__ . '".', 1527778767);
     }
index d065411..f6ba516 100644 (file)
@@ -1277,13 +1277,19 @@ class PageRepository implements LoggerAwareInterface
                     }
                     if (($ctrl['enablecolumns']['starttime'] ?? false) && !($ignore_array['starttime'] ?? false)) {
                         $field = $table . '.' . $ctrl['enablecolumns']['starttime'];
-                        $constraints[] = $expressionBuilder->lte($field, (int)$GLOBALS['SIM_ACCESS_TIME']);
+                        $constraints[] = $expressionBuilder->lte(
+                            $field,
+                            $this->context->getPropertyFromAspect('date', 'accessTime', 0)
+                        );
                     }
                     if (($ctrl['enablecolumns']['endtime'] ?? false) && !($ignore_array['endtime'] ?? false)) {
                         $field = $table . '.' . $ctrl['enablecolumns']['endtime'];
                         $constraints[] = $expressionBuilder->orX(
                             $expressionBuilder->eq($field, 0),
-                            $expressionBuilder->gt($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
+                            $expressionBuilder->gt(
+                                $field,
+                                $this->context->getPropertyFromAspect('date', 'accessTime', 0)
+                            )
                         );
                     }
                     if (($ctrl['enablecolumns']['fe_group'] ?? false) && !($ignore_array['fe_group'] ?? false)) {
index 2434d69..19e48bb 100644 (file)
@@ -89,15 +89,23 @@ class PageRouter implements RouterInterface
     protected $cacheHashCalculator;
 
     /**
+     * @var \TYPO3\CMS\Core\Context\Context|null
+     */
+    protected $context;
+
+    /**
      * A page router is always bound to a specific site.
+     *
      * @param Site $site
+     * @param \TYPO3\CMS\Core\Context\Context|null $context
      */
-    public function __construct(Site $site)
+    public function __construct(Site $site, Context $context = null)
     {
         $this->site = $site;
         $this->enhancerFactory = GeneralUtility::makeInstance(EnhancerFactory::class);
         $this->aspectFactory = GeneralUtility::makeInstance(AspectFactory::class);
         $this->cacheHashCalculator = GeneralUtility::makeInstance(CacheHashCalculator::class);
+        $this->context = $context ?? GeneralUtility::makeInstance(Context::class);
     }
 
     /**
@@ -114,7 +122,7 @@ class PageRouter implements RouterInterface
             throw new RouteNotFoundException('No previous result given. Cannot find a page for an empty route part', 1555303496);
         }
 
-        $candidateProvider = $this->getSlugCandidateProvider(GeneralUtility::makeInstance(Context::class));
+        $candidateProvider = $this->getSlugCandidateProvider($this->context);
 
         // Legacy URIs (?id=12345) takes precedence, no matter if a route is given
         $requestId = (int)($request->getQueryParams()['id'] ?? 0);
@@ -225,7 +233,7 @@ class PageRouter implements RouterInterface
             $pageId = (int)$route;
         }
 
-        $context = clone GeneralUtility::makeInstance(Context::class);
+        $context = clone $this->context;
         $context->setAspect('language', LanguageAspectFactory::createFromSiteLanguage($language));
         $pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context);
         $page = $pageRepository->getPage($pageId, true);
index 8a0e9a4..f523f35 100644 (file)
@@ -51,7 +51,6 @@ interface RouterInterface
      * @param string $fragment the section/fragment www.example.com/page/#fragment, WITHOUT the hash
      * @param string $type see the constants above.
      * @return UriInterface
-     * @throws InvalidRouteArgumentsException
      */
     public function generateUri($route, array $parameters = [], string $fragment = '', string $type = self::ABSOLUTE_URL): UriInterface;
 }
index abd499d..e44f0ae 100644 (file)
@@ -19,6 +19,7 @@ namespace TYPO3\CMS\Core\Site\Entity;
 use Psr\Http\Message\UriInterface;
 use Symfony\Component\ExpressionLanguage\SyntaxError;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Error\PageErrorHandler\FluidPageErrorHandler;
 use TYPO3\CMS\Core\Error\PageErrorHandler\InvalidPageErrorHandlerException;
 use TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler;
@@ -363,11 +364,12 @@ class Site implements SiteInterface
     /**
      * Returns the applicable router for this site. This might be configurable in the future.
      *
+     * @param  $context
      * @return RouterInterface
      */
-    public function getRouter(): RouterInterface
+    public function getRouter(Context $context = null): RouterInterface
     {
-        return GeneralUtility::makeInstance(PageRouter::class, $this);
+        return GeneralUtility::makeInstance(PageRouter::class, $this, $context);
     }
 
     /**
index ab1cd37..3bfc1cb 100644 (file)
@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Core\Tests\Functional\Domain\Repository;
 
 use Prophecy\Argument;
 use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Context\DateTimeAspect;
 use TYPO3\CMS\Core\Context\LanguageAspect;
 use TYPO3\CMS\Core\Context\WorkspaceAspect;
 use TYPO3\CMS\Core\Database\ConnectionPool;
@@ -352,15 +353,15 @@ class PageRepositoryTest extends \TYPO3\TestingFramework\Core\Functional\Functio
     /**
      * @test
      */
-    public function initSetsPublicPropertyCorrectlyForLive()
+    public function initSetsEnableFieldsCorrectlyForLive(): void
     {
-        $GLOBALS['SIM_ACCESS_TIME'] = 123;
-
-        $subject = new PageRepository(new Context());
+        $subject = new PageRepository(new Context([
+            'date' => new DateTimeAspect(new \DateTimeImmutable('@1451779200'))
+        ]));
 
         $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages');
         $expectedSQL = sprintf(
-            ' AND ((%s = 0) AND (%s <= 0) AND (%s <> -1) AND (%s = 0) AND (%s <= 123) AND ((%s = 0) OR (%s > 123))) AND (%s < 200)',
+            ' AND ((%s = 0) AND (%s <= 0) AND (%s <> -1) AND (%s = 0) AND (%s <= 1451779200) AND ((%s = 0) OR (%s > 1451779200))) AND (%s < 200)',
             $connection->quoteIdentifier('pages.deleted'),
             $connection->quoteIdentifier('pages.t3ver_state'),
             $connection->quoteIdentifier('pages.pid'),
index aa2c1e5..f004694 100644 (file)
@@ -15,6 +15,8 @@ namespace TYPO3\CMS\Extbase\Tests\Unit\Persistence\Generic\Storage;
  */
 
 use Prophecy\Argument;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Context\DateTimeAspect;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
@@ -595,12 +597,12 @@ class Typo3DbQueryParserTest extends UnitTestCase
         return [
             'in be: include all' => ['BE', true, [], true, ''],
             'in be: ignore enable fields but do not include deleted' => ['BE', true, [], false, 'tx_foo_table.deleted_column=0'],
-            'in be: respect enable fields but include deleted' => ['BE', false, [], true, '(tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 123456789)'],
-            'in be: respect enable fields and do not include deleted' => ['BE', false, [], false, '(tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 123456789) AND tx_foo_table.deleted_column=0'],
+            'in be: respect enable fields but include deleted' => ['BE', false, [], true, '(tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 1451779200)'],
+            'in be: respect enable fields and do not include deleted' => ['BE', false, [], false, '(tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 1451779200) AND tx_foo_table.deleted_column=0'],
             'in fe: include all' => ['FE', true, [], true, ''],
             'in fe: ignore enable fields but do not include deleted' => ['FE', true, [], false, 'tx_foo_table.deleted_column=0'],
             'in fe: ignore only starttime and do not include deleted' => ['FE', true, ['starttime'], false, '(tx_foo_table.deleted_column = 0) AND (tx_foo_table.disabled_column = 0)'],
-            'in fe: respect enable fields and do not include deleted' => ['FE', false, [], false, '(tx_foo_table.deleted_column = 0) AND (tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 123456789)']
+            'in fe: respect enable fields and do not include deleted' => ['FE', false, [], false, '(tx_foo_table.deleted_column = 0) AND (tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 1451779200)']
         ];
     }
 
@@ -618,7 +620,12 @@ class Typo3DbQueryParserTest extends UnitTestCase
             ],
             'delete' => 'deleted_column'
         ];
-        $GLOBALS['SIM_ACCESS_TIME'] = 123456789;
+        // simulate time for backend enable fields
+        $GLOBALS['SIM_ACCESS_TIME'] = 1451779200;
+        // simulate time for frontend (PageRepository) enable fields
+        $dateAspect = new DateTimeAspect(new \DateTimeImmutable('3.1.2016'));
+        $context = new Context(['date' => $dateAspect]);
+        GeneralUtility::setSingletonInstance(Context::class, $context);
 
         $connectionProphet = $this->prophesize(Connection::class);
         $connectionProphet->quoteIdentifier(Argument::cetera())->willReturnArgument(0);
@@ -663,9 +670,9 @@ class Typo3DbQueryParserTest extends UnitTestCase
     {
         return [
             'in be: respectEnableFields=false' => ['BE', false, ''],
-            'in be: respectEnableFields=true' => ['BE', true, '(tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 123456789) AND tx_foo_table.deleted_column=0'],
+            'in be: respectEnableFields=true' => ['BE', true, '(tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 1451779200) AND tx_foo_table.deleted_column=0'],
             'in FE: respectEnableFields=false' => ['FE', false, ''],
-            'in FE: respectEnableFields=true' => ['FE', true, '(tx_foo_table.deleted_column = 0) AND (tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 123456789)']
+            'in FE: respectEnableFields=true' => ['FE', true, '(tx_foo_table.deleted_column = 0) AND (tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 1451779200)']
         ];
     }
 
@@ -683,7 +690,12 @@ class Typo3DbQueryParserTest extends UnitTestCase
             ],
             'delete' => 'deleted_column'
         ];
-        $GLOBALS['SIM_ACCESS_TIME'] = 123456789;
+        // simulate time for backend enable fields
+        $GLOBALS['SIM_ACCESS_TIME'] = 1451779200;
+        // simulate time for frontend (PageRepository) enable fields
+        $dateAspect = new DateTimeAspect(new \DateTimeImmutable('3.1.2016'));
+        $context = new Context(['date' => $dateAspect]);
+        GeneralUtility::setSingletonInstance(Context::class, $context);
 
         $connectionProphet = $this->prophesize(Connection::class);
         $connectionProphet->quoteIdentifier(Argument::cetera())->willReturnArgument(0);
index 07138de..26ebbda 100644 (file)
@@ -1063,7 +1063,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         }
         $originalFrontendUserGroup = null;
         if ($this->fe_user->user) {
-            $originalFrontendUserGroup = $this->fe_user->user[$this->fe_user->usergroup_column];
+            $originalFrontendUserGroup = $this->context->getPropertyFromAspect('frontend.user', 'groupIds');
         }
 
         // The preview flag is set if the current page turns out to be hidden
@@ -1078,7 +1078,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         if ($this->whichWorkspace() > 0) {
             $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, true));
         }
-        return $this->simUserGroup ? $originalFrontendUserGroup : null;
+        return $this->context->getPropertyFromAspect('frontend.preview', 'preview', false) ? $originalFrontendUserGroup : null;
     }
 
     /**
diff --git a/typo3/sysext/frontend/Classes/Middleware/PreviewSimulator.php b/typo3/sysext/frontend/Classes/Middleware/PreviewSimulator.php
new file mode 100644 (file)
index 0000000..f375449
--- /dev/null
@@ -0,0 +1,135 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Frontend\Middleware;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Context\DateTimeAspect;
+use TYPO3\CMS\Core\Context\UserAspect;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\Aspect\PreviewAspect;
+
+/**
+ * Middleware for handling preview settings
+ * used when simulating / previewing pages or content through query params when
+ * previewing access or time restricted content via for example backend preview links
+ */
+class PreviewSimulator implements MiddlewareInterface
+{
+    /**
+     * @var \TYPO3\CMS\Core\Context\Context
+     */
+    private $context;
+
+    public function __construct(Context $context)
+    {
+        $this->context = $context;
+    }
+
+    /**
+     * Evaluates preview settings if a backend user is logged in
+     *
+     * @param ServerRequestInterface $request
+     * @param RequestHandlerInterface $handler
+     * @return ResponseInterface
+     * @throws \Exception
+     */
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        if ((bool)$this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false)) {
+            $simulatingDate = $this->simulateDate($request);
+            $simulatingGroup = $this->simulateUserGroup($request);
+            $isPreview = (int)($simulatingDate || $simulatingGroup);
+            $previewAspect = GeneralUtility::makeInstance(PreviewAspect::class, $isPreview);
+            $this->context->setAspect('frontend.preview', $previewAspect);
+        }
+
+        return $handler->handle($request);
+    }
+
+    /**
+     * Simulate dates for preview functionality
+     * When previewing a time restricted page from the backend, the parameter ADMCMD_simTime it added containing
+     * a timestamp with the time to preview. The globals 'SIM_EXEC_TIME' and 'SIM_ACCESS_TIME' and the 'DateTimeAspect'
+     * are used to simulate rendering at that point in time.
+     * Ideally the global access is removed in future versions.
+     * This functionality needs to be loaded after BackendAuthenticator as it is only relevant for
+     * logged in backend users and needs to be done before any page resolving starts.
+     *
+     * @param ServerRequestInterface $request
+     * @return bool
+     */
+    protected function simulateDate(ServerRequestInterface $request): bool
+    {
+        $simulatedDate = null;
+        $queryTime = $request->getQueryParams()['ADMCMD_simTime'] ?? false;
+        if (!$queryTime) {
+            return false;
+        }
+
+        $simulatedDate = new \DateTimeImmutable('@' . $queryTime);
+        if (!$simulatedDate) {
+            return false;
+        }
+
+        $GLOBALS['SIM_EXEC_TIME'] = $queryTime;
+        $GLOBALS['SIM_ACCESS_TIME'] = $queryTime - $queryTime % 60;
+        $this->context->setAspect(
+            'date',
+            GeneralUtility::makeInstance(
+                DateTimeAspect::class,
+                $simulatedDate
+            )
+        );
+        return true;
+    }
+
+    /**
+     * Simulate user group for preview functionality
+     * When previewing a page with a usergroup restriction, the parameter ADMCMD_simUser = <groupId> will be added
+     * to the preview url. Simulation happens.
+     * legacy: via TSFE member variables (->fe_user->user[<groupColumn>])
+     * new: via Context::UserAspect
+     * This functionality needs to be loaded after BackendAuthenticator as it is only relevant for
+     * logged in backend users and needs to be done before any page resolving starts.
+     *
+     * @param ServerRequestInterface $request
+     * @return bool
+     */
+    protected function simulateUserGroup(ServerRequestInterface $request): bool
+    {
+        $simulateUserGroup = (int)($request->getQueryParams()['ADMCMD_simUser'] ?? 0);
+        if (!$simulateUserGroup) {
+            return false;
+        }
+
+        $frontendUser = $request->getAttribute('frontend.user');
+        $frontendUser->user[$frontendUser->usergroup_column] = $simulateUserGroup;
+        $this->context->setAspect(
+            'frontend.user',
+            GeneralUtility::makeInstance(
+                UserAspect::class,
+                $frontendUser,
+                [$simulateUserGroup]
+            )
+        );
+        return true;
+    }
+}
index 41791e6..edf22ab 100644 (file)
@@ -43,6 +43,16 @@ return [
                 'typo3/cms-frontend/maintenance-mode'
             ]
         ],
+        'typo3/cms-frontend/preview-simulator' => [
+            'target' => \TYPO3\CMS\Frontend\Middleware\PreviewSimulator::class,
+            'after' => [
+                'typo3/cms-frontend/backend-user-authentication',
+                'typo3/cms-frontend/authentication',
+            ],
+            'before' => [
+                'typo3/cms-frontend/tsfe'
+            ]
+        ],
         'typo3/cms-frontend/site' => [
             'target' => \TYPO3\CMS\Frontend\Middleware\SiteResolver::class,
             'after' => [