[FEATURE] Create sites on page creation 58/61658/20
authorAndreas Fernandez <a.fernandez@scripting-base.de>
Tue, 10 Sep 2019 13:18:25 +0000 (15:18 +0200)
committerAnja Leichsenring <aleichsenring@ab-softlab.de>
Tue, 24 Sep 2019 07:36:15 +0000 (09:36 +0200)
A new hook is introduced that creates a site configuration when a new
page on root level is created. This takes effect for pages of type
"default", "link" and "shortcut".

To reduce the likelihood for conflicts, a shortened MD5 hash of the page
id appended to the site identifier.

Resolves: #89142
Releases: master
Change-Id: Ibe2957e3789f2a165e36949ae5fb4fa2a1a572df
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/61658
Reviewed-by: Benni Mack <benni@typo3.org>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
typo3/sysext/core/Classes/Configuration/SiteConfiguration.php
typo3/sysext/core/Classes/Hooks/CreateSiteConfiguration.php [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-89142-CreateSiteConfigurationIfPageIsCreatedOnRootLevel.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Acceptance/Backend/Site/SiteModuleCest.php
typo3/sysext/core/ext_localconf.php
typo3/sysext/install/Classes/Controller/InstallerController.php

index a467aa1..6e6fcc8 100644 (file)
@@ -87,6 +87,39 @@ class SiteConfiguration implements SingletonInterface
     }
 
     /**
+     * Creates a site configuration with one language "English" which is the de-facto default language for TYPO3 in general.
+     *
+     * @param string $identifier
+     * @param int $rootPageId
+     * @param string $base
+     */
+    public function createNewBasicSite(string $identifier, int $rootPageId, string $base): void
+    {
+        // Create a default site configuration called "main" as best practice
+        $this->write($identifier, [
+            'rootPageId' => $rootPageId,
+            'base' => $base,
+            'languages' => [
+                0 => [
+                    'title' => 'English',
+                    'enabled' => true,
+                    'languageId' => 0,
+                    'base' => '/en/',
+                    'typo3Language' => 'default',
+                    'locale' => 'en_US.UTF-8',
+                    'iso-639-1' => 'en',
+                    'navigationTitle' => 'English',
+                    'hreflang' => 'en-us',
+                    'direction' => 'ltr',
+                    'flag' => 'us',
+                ],
+            ],
+            'errorHandling' => [],
+            'routes' => [],
+        ]);
+    }
+
+    /**
      * Resolve all site objects which have been found in the filesystem.
      *
      * @return Site[]
diff --git a/typo3/sysext/core/Classes/Hooks/CreateSiteConfiguration.php b/typo3/sysext/core/Classes/Hooks/CreateSiteConfiguration.php
new file mode 100644 (file)
index 0000000..8bb0d21
--- /dev/null
@@ -0,0 +1,143 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Hooks;
+
+/*
+ * 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 TYPO3\CMS\Core\Configuration\SiteConfiguration;
+use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\DataHandling\DataHandler;
+use TYPO3\CMS\Core\Domain\Repository\PageRepository;
+use TYPO3\CMS\Core\Exception\SiteNotFoundException;
+use TYPO3\CMS\Core\Http\NormalizedParams;
+use TYPO3\CMS\Core\Site\SiteFinder;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Hook for creating a basic site configuration for new pages on root level.
+ *
+ * @internal This class is a hook implementation and is not part of the TYPO3 Core API.
+ */
+class CreateSiteConfiguration
+{
+    protected $allowedPageTypes = [
+        PageRepository::DOKTYPE_DEFAULT,
+        PageRepository::DOKTYPE_LINK,
+        PageRepository::DOKTYPE_SHORTCUT
+    ];
+
+    public function processDatamap_afterDatabaseOperations(string $status, string $table, $id, array $fieldValues, DataHandler $dataHandler): void
+    {
+        /**
+         * Take action only on
+         *   - new records
+         *   - pages table
+         *   - live workspace
+         *   - resolved uids
+         *   - pages on root level
+         *   - non-versioned records
+         *   - allowed doktypes
+         *   - not bulk importing things via CLI
+         */
+        if ($status !== 'new'
+            || $table !== 'pages'
+            || $dataHandler->BE_USER->workspace > 0
+            || !isset($dataHandler->substNEWwithIDs[$id])
+            || (int)$fieldValues['pid'] !== 0
+            || (isset($fieldValues['t3ver_oid']) && (int)$fieldValues['t3ver_oid'] > 0)
+            || !in_array((int)$fieldValues['doktype'], $this->allowedPageTypes, true)
+            || $dataHandler->isImporting
+        ) {
+            return;
+        }
+
+        $uid = (int)$dataHandler->substNEWwithIDs[$id];
+        $this->generateSiteConfigurationForRootPage($uid);
+    }
+
+    protected function generateSiteConfigurationForRootPage(int $pageId): void
+    {
+        $entryPoint = 'autogenerated-' . $pageId;
+        $siteIdentifier = $entryPoint . '-' . GeneralUtility::shortMD5((string)$pageId);
+
+        if (!$this->siteExistsByRootPageId($pageId)) {
+            $siteConfiguration = GeneralUtility::makeInstance(
+                SiteConfiguration::class,
+                Environment::getConfigPath() . '/sites'
+            );
+            $siteConfiguration->createNewBasicSite(
+                $siteIdentifier,
+                $pageId,
+                $this->getNormalizedParams()->getSiteUrl() . $entryPoint
+            );
+            $this->updateSlugForPage($pageId);
+        }
+    }
+
+    protected function getNormalizedParams(): NormalizedParams
+    {
+        $normalizedParams = null;
+        $serverParams = $_SERVER;
+        if (isset($GLOBALS['TYPO3_REQUEST'])) {
+            $normalizedParams = $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams');
+            $serverParams = $GLOBALS['TYPO3_REQUEST']->getServerParams();
+        }
+
+        if (!($normalizedParams instanceof NormalizedParams)) {
+            $normalizedParams = new NormalizedParams(
+                $serverParams,
+                $GLOBALS['TYPO3_CONF_VARS']['SYS'],
+                Environment::getCurrentScript(),
+                Environment::getPublicPath()
+            );
+        }
+
+        return $normalizedParams;
+    }
+
+    /**
+     * Updates the slug of the given pageId by spinning up a new DataHandler instance.
+     *
+     * @param int $pageId
+     */
+    protected function updateSlugForPage(int $pageId): void
+    {
+        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
+        $dataMap = [
+            'pages' => [
+                $pageId => [
+                    'slug' => '',
+                ],
+            ],
+        ];
+        $dataHandler->start($dataMap, []);
+        $dataHandler->process_datamap();
+    }
+
+    /**
+     * Checks whether a site exists by its root page. Sets up a new SiteFinder instance
+     *
+     * @param int $rootPageId the page ID (default language)
+     * @return bool
+     */
+    protected function siteExistsByRootPageId(int $rootPageId): bool
+    {
+        try {
+            GeneralUtility::makeInstance(SiteFinder::class)->getSiteByRootPageId($rootPageId);
+        } catch (SiteNotFoundException $e) {
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-89142-CreateSiteConfigurationIfPageIsCreatedOnRootLevel.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-89142-CreateSiteConfigurationIfPageIsCreatedOnRootLevel.rst
new file mode 100644 (file)
index 0000000..a65d2b1
--- /dev/null
@@ -0,0 +1,36 @@
+.. include:: ../../Includes.txt
+
+============================================================================
+Feature: #89142 - Create site configuration if page is created on root level
+============================================================================
+
+See :issue:`89142`
+
+Description
+===========
+
+When creating a typical new page on the root level of a TYPO3 installation, a new site configuration is now
+automatically created as well. This makes it easier to work with multi-sites and get a basic configuration set up
+more quickly than before.
+
+Under the hood, a new DataHandler hook checks for new pages being one of the following doktypes:
+
+* Default pages
+* Links
+* Shortcuts
+
+The entry point consists of the current domain where the configuration has been created, plus a short identifier using
+the page uid and the prefix "site", e.g. `https://example.com/site-42`.
+
+The identifier of the site uses the entry point without the domain, and a MD5 hash of the page id to avoid potential
+conflicts for existing site configurations. An identifier may look like `site-42-a1d0c6e83f`.
+
+Impact
+======
+
+A new site configuration with a pre-defined identifier, entry point and a default language gets created automatically.
+
+Ideally, there are no scenarios anymore where a site needs to be created after a first page is created, avoiding
+any issues related to Slug handling for root pages, which are always set to `/` by default.
+
+.. index:: Backend, ext:core
index 90fc373..78b56c2 100644 (file)
@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Core\Tests\Acceptance\Backend\Redirect;
  */
 
 use TYPO3\CMS\Core\Tests\Acceptance\Support\BackendTester;
+use TYPO3\CMS\Core\Tests\Acceptance\Support\Helper\ModalDialog;
 
 /**
  * Tests concerning Sites Module
@@ -34,13 +35,50 @@ class SiteModuleCest
     /**
      * @param BackendTester $I
      */
-    public function createNewRecordIfNoneExist(BackendTester $I)
+    public function editExistingRecord(BackendTester $I)
     {
         $I->click('Sites');
         $I->switchToContentFrame();
         $I->canSee('Site Configuration', 'h1');
 
-        $I->amGoingTo('create a new site configuration when none are in the system, yet');
+        $I->amGoingTo('edit an automatically created site configuration');
+        $I->click('Edit');
+        $I->waitForElementNotVisible('#t3js-ui-block');
+        $I->canSee('Edit Site Configuration "autogenerated-1-c4ca4238a0" on root level');
+        $I->fillField('//input[contains(@data-formengine-input-name, "data[site]") and contains(@data-formengine-input-name, "[identifier]")]', 'autogenerated-1-c4ca4238a0');
+        $I->fillField('//input[contains(@data-formengine-input-name, "data[site]") and contains(@data-formengine-input-name, "[base]")]', 'http://web:8000/typo3temp/var/tests/acceptance/');
+        $I->click('Languages');
+        $I->click('#data-0-site-1-languages-site_language-0_div');
+        $I->wait(2);
+        $I->fillField('//input[contains(@data-formengine-input-name, "data[site_language]") and contains(@data-formengine-input-name, "[title]")]', 'English');
+        $I->fillField('//input[contains(@data-formengine-input-name, "data[site_language]") and contains(@data-formengine-input-name, "[base]")]', 'http://web:8000/typo3temp/var/tests/acceptance/');
+        $I->fillField('//input[contains(@data-formengine-input-name, "data[site_language]") and contains(@data-formengine-input-name, "[locale]")]', 'en_US.UTF-8');
+
+        $saveButtonLink = '//*/button[@name="_savedok"][1]';
+        $I->waitForElement($saveButtonLink, 30);
+        $I->click($saveButtonLink);
+        $I->waitForElementNotVisible('#t3js-ui-block');
+
+        $I->click('div.module-docheader .btn.t3js-editform-close');
+
+        $I->waitForElementVisible('table.table-striped');
+        $I->canSee('Site Configuration', 'h1');
+        $I->canSee('autogenerated-1-c4ca4238a0');
+    }
+
+    public function createSiteConfigIfNoneExists(BackendTester $I, ModalDialog $modalDialog)
+    {
+        $I->click('Sites');
+        $I->switchToContentFrame();
+        $I->canSee('Site Configuration', 'h1');
+
+        $I->amGoingTo('delete the auto generated config in order to create one manually');
+        $I->click('Delete site configuration');
+        $modalDialog->canSeeDialog();
+        $modalDialog->clickButtonInDialog('Delete');
+        $I->switchToContentFrame();
+
+        $I->amGoingTo('manually create a new site config for the existing root page');
         $I->click('Add new site configuration for this site');
         $I->waitForElementNotVisible('#t3js-ui-block');
         $I->canSee('Create new Site configuration');
@@ -50,14 +88,11 @@ class SiteModuleCest
         $I->fillField('//input[contains(@data-formengine-input-name, "data[site_language]") and contains(@data-formengine-input-name, "[title]")]', 'Homepage');
         $I->fillField('//input[contains(@data-formengine-input-name, "data[site_language]") and contains(@data-formengine-input-name, "[base]")]', 'http://web:8000/typo3temp/var/tests/acceptance/');
         $I->fillField('//input[contains(@data-formengine-input-name, "data[site_language]") and contains(@data-formengine-input-name, "[locale]")]', 'en_US.UTF-8');
-
         $saveButtonLink = '//*/button[@name="_savedok"][1]';
         $I->waitForElement($saveButtonLink, 30);
         $I->click($saveButtonLink);
         $I->waitForElementNotVisible('#t3js-ui-block');
-
         $I->click('div.module-docheader .btn.t3js-editform-close');
-
         $I->waitForElementVisible('table.table-striped');
         $I->canSee('Site Configuration', 'h1');
         $I->canSee('SitesTestIdentifier');
@@ -66,7 +101,7 @@ class SiteModuleCest
     /**
      * Add a default FE ts snipped to the existing site config and verify FE is rendered
      *
-     * @depends createNewRecordIfNoneExist
+     * @depends editExistingRecord
      *
      * @param BackendTester $I
      */
index 534196c..23511c9 100644 (file)
@@ -29,6 +29,7 @@ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php
 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'][] = \TYPO3\CMS\Core\Resource\Security\FileMetadataPermissionsAspect::class;
 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = \TYPO3\CMS\Core\Hooks\DestroySessionHook::class;
 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = \TYPO3\CMS\Core\Hooks\PagesTsConfigGuard::class;
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][\TYPO3\CMS\Core\Hooks\CreateSiteConfiguration::class] = \TYPO3\CMS\Core\Hooks\CreateSiteConfiguration::class;
 
 $signalSlotDispatcher->connect(
     \TYPO3\CMS\Core\Resource\ResourceStorage::class,
index 33575a3..33af40e 100644 (file)
@@ -23,7 +23,6 @@ use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
 use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
 use TYPO3\CMS\Core\Configuration\SiteConfiguration;
-use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash;
 use TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash;
@@ -1234,26 +1233,6 @@ For each website you need a TypoScript template on the main page of your website
             SiteConfiguration::class,
             Environment::getConfigPath() . '/sites'
         );
-        $siteConfiguration->write($identifier, [
-            'rootPageId' => $rootPageId,
-            'base' => $normalizedParams->getSiteUrl(),
-            'languages' => [
-                0 => [
-                    'title' => 'English',
-                    'enabled' => true,
-                    'languageId' => 0,
-                    'base' => '/en/',
-                    'typo3Language' => 'default',
-                    'locale' => 'en_US.UTF-8',
-                    'iso-639-1' => 'en',
-                    'navigationTitle' => 'English',
-                    'hreflang' => 'en-us',
-                    'direction' => 'ltr',
-                    'flag' => 'us',
-                ],
-            ],
-            'errorHandling' => [],
-            'routes' => [],
-        ]);
+        $siteConfiguration->createNewBasicSite($identifier, $rootPageId, $normalizedParams->getSiteUrl());
     }
 }