Simplify the domain and landing page management
authorAlexander Ebert <ebert@woltlab.com>
Sun, 2 May 2021 10:00:12 +0000 (12:00 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Sun, 2 May 2021 10:00:12 +0000 (12:00 +0200)
wcfsetup/install/files/acp/templates/applicationManagement.tpl
wcfsetup/install/files/lib/acp/form/ApplicationEditForm.class.php
wcfsetup/install/files/lib/acp/form/ApplicationManagementForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/page/ApplicationManagementPage.class.php [deleted file]
wcfsetup/install/files/lib/system/application/ApplicationHandler.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index 7bac0ef6eef750712e87135d0123febb4f0c2b08..87e4e41a7ebbce5a778de016be38aacd67c6046b 100644 (file)
        {/hascontent}
 </header>
 
-<div class="section tabularBox">
-       <table class="table">
-               <thead>
-                       <tr>
-                               <th class="columnID columnPackageID" colspan="2">{lang}wcf.global.objectID{/lang}</th>
-                               <th class="columnText columnPackageName">{lang}wcf.acp.package.name{/lang}</th>
-                               <th class="columnText columnDomainName">{lang}wcf.acp.application.domainName{/lang}</th>
-                               <th class="columnText columnDomainPath">{lang}wcf.acp.application.domainPath{/lang}</th>
-                               <th class="columnText columnLandingPageID">{lang}wcf.acp.application.landingPage{/lang}</th>
+{if $isMultiDomainSetup}
+       <div class="warning">{lang}wcf.acp.application.multiDomain{/lang}</div>
+{/if}
+
+<form method="post" action="{link controller='ApplicationManagement'}{/link}">
+       {if !$isMultiDomainSetup}
+               <section class="section">
+                       <h2 class="sectionTitle">{lang}wcf.acp.application.management.domain{/lang}</h2>
+
+                       <dl{if $errorField == 'domainName'} class="formError"{/if}>
+                               <dt><label for="domainName">{lang}wcf.acp.application.management.domainName{/lang}</label></dt>
+                               <dd>
+                                       <div class="inputAddon">
+                                               <span class="inputPrefix">http(s)://</span>
+                                               <input type="text" name="domainName" id="domainName" value="{$domainName}" class="long">
+                                       </div>
+                                       {if $errorField == 'domainName'}
+                                               <small class="innerError">
+                                                       {if $errorType == 'empty'}
+                                                               {lang}wcf.global.form.error.empty{/lang}
+                                                       {else}
+                                                               {lang}wcf.acp.application.management.domainName.error.{$errorType}{/lang}
+                                                       {/if}
+                                               </small>
+                                       {/if}
+                                       <small>{lang}wcf.acp.application.management.domainName.description{/lang}</small>
+                               </dd>
+                       </dl>
+
+                       <dl{if $errorField == 'cookieDomain'} class="formError"{/if}>
+                               <dt><label for="cookieDomain">{lang}wcf.acp.application.management.cookieDomain{/lang}</label></dt>
+                               <dd>
+                                       <input type="text" name="cookieDomain" id="cookieDomain" value="{$cookieDomain}" class="long">
+                                       {if $errorField == 'cookieDomain'}
+                                               <small class="innerError">
+                                                       {if $errorType == 'empty'}
+                                                               {lang}wcf.global.form.error.empty{/lang}
+                                                       {else}
+                                                               {lang}wcf.acp.application.management.cookieDomain.error.{$errorType}{/lang}
+                                                       {/if}
+                                               </small>
+                                       {/if}
+                                       <small>{lang}wcf.acp.application.management.cookieDomain.description{/lang}</small>
+                               </dd>
+                       </dl>
+               </section>
+
+               <script>
+                       (() => {
+                               const domainName = document.getElementById("domainName");
+                               const cookieDomain = document.getElementById("cookieDomain");
+
+                               // Keep the cookie domain in sync if it was previously identical.
+                               if (domainName.value === cookieDomain.value) {
+                                       domainName.addEventListener("input", () => {
+                                               cookieDomain.value = domainName.value;
+                                       });
+                               }
+                       })();
+               </script>
+       {/if}
+
+       <section class="section">
+               <h2 class="sectionTitle">{lang}wcf.acp.application.landingPage{/lang}</h2>
+
+               <div class="tabularBox">
+                       <table class="table">
+                               <thead>
+                                       <tr>
+                                               <th class="columnID columnPackageID">{lang}wcf.global.objectID{/lang}</th>
+                                               <th class="columnText columnPackageName">{lang}wcf.acp.package.name{/lang}</th>
+                                               <th class="columnText columnLandingPageID">{lang}wcf.acp.application.landingPage{/lang}</th>
+                                               
+                                               {event name='columnHeads'}
+                                       </tr>
+                               </thead>
                                
-                               {event name='columnHeads'}
-                       </tr>
-               </thead>
-               
-               <tbody>
-                       {foreach from=$applicationList item=application}
-                               <tr>
-                                       <td class="columnIcon"><a href="{link controller='ApplicationEdit' id=$application->packageID}{/link}" class="jsTooltip" title="{lang}wcf.global.button.edit{/lang}"><span class="icon icon16 fa-pencil"></span></a></td>
-                                       <td class="columnID columnPackageID">{#$application->packageID}</td>
-                                       <td class="columnTitle columnPackageName"><a href="{link controller='ApplicationEdit' id=$application->packageID}{/link}">{$application->getPackage()}</a></td>
-                                       <td class="columnText columnDomainName">{$application->domainName}</td>
-                                       <td class="columnText columnDomainPath">{$application->domainPath}</td>
-                                       <td class="columnText columnLandingPageID">
-                                               {if $application->landingPageID && $pageList[$application->landingPageID]|isset}
-                                                       {$pageList[$application->landingPageID]}
-                                               {else}
-                                                       {lang}wcf.global.noSelection{/lang}
-                                               {/if}
-                                       </td>
-                                       
-                                       {event name='columns'}
-                               </tr>
-                       {/foreach}
-               </tbody>
-       </table>
-</div>
+                               <tbody>
+                                       {foreach from=$applicationList item=application}
+                                               <tr>
+                                                       <td class="columnID columnPackageID">{#$application->packageID}</td>
+                                                       <td class="columnTitle columnPackageName">
+                                                               <p><strong>{$application->getPackage()}</strong></p>
+                                                               <small>{$application->getPageURL()}</small>
+                                                       </td>
+                                                       <td class="columnText columnLandingPageID">
+                                                               <select name="landingPageID[{$application->packageID}]">
+                                                                       <option value="0">{lang}wcf.acp.application.landingPage.default{/lang}</option>
+                                                                       
+                                                                       {foreach from=$pageNodeList item=pageNode}
+                                                                               {if !$pageNode->isDisabled && !$pageNode->requireObjectID && !$pageNode->excludeFromLandingPage}
+                                                                                       <option value="{@$pageNode->pageID}"{if $pageNode->pageID == $application->landingPageID} selected{/if} data-identifier="{@$pageNode->identifier}">{if $pageNode->getDepth() > 1}{@"&nbsp;&nbsp;&nbsp;&nbsp;"|str_repeat:($pageNode->getDepth() - 1)}{/if}{$pageNode->name}</option>
+                                                                               {/if}
+                                                                       {/foreach}
+                                                               </select>
+                                                       </td>
+                                                       
+                                                       {event name='columns'}
+                                               </tr>
+                                       {/foreach}
+                               </tbody>
+                       </table>
+               </div>
+       </section>
+
+       <div class="formSubmit">
+               <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
+               {csrfToken}
+       </div>
+</form>
 
 <footer class="contentFooter">
        {hascontent}
index dcb1a55114e43a7129090a127cd04fe1984296cd..2fce080e5ca46ed538c54f35376d9f5d7d16453f 100644 (file)
@@ -17,6 +17,7 @@ use wcf\system\exception\UserInputException;
 use wcf\system\Regex;
 use wcf\system\WCF;
 use wcf\util\FileUtil;
+use wcf\util\HeaderUtil;
 use wcf\util\StringUtil;
 
 /**
@@ -93,6 +94,10 @@ class ApplicationEditForm extends AbstractForm
     {
         parent::readParameters();
 
+        if (!ApplicationHandler::getInstance()->isMultiDomainSetup()) {
+            throw new IllegalLinkException();
+        }
+
         if (isset($_REQUEST['id'])) {
             $this->packageID = \intval($_REQUEST['id']);
         }
diff --git a/wcfsetup/install/files/lib/acp/form/ApplicationManagementForm.class.php b/wcfsetup/install/files/lib/acp/form/ApplicationManagementForm.class.php
new file mode 100644 (file)
index 0000000..8ffac0b
--- /dev/null
@@ -0,0 +1,231 @@
+<?php
+
+namespace wcf\acp\form;
+
+use Stripe\Exception\PermissionException;
+use wcf\data\application\ViewableApplicationList;
+use wcf\data\page\Page;
+use wcf\data\page\PageList;
+use wcf\data\page\PageNodeTree;
+use wcf\form\AbstractForm;
+use wcf\system\application\ApplicationHandler;
+use wcf\system\cache\builder\ApplicationCacheBuilder;
+use wcf\system\cache\builder\RoutingCacheBuilder;
+use wcf\system\exception\UserInputException;
+use wcf\system\Regex;
+use wcf\system\WCF;
+use wcf\util\ArrayUtil;
+use wcf\util\FileUtil;
+use wcf\util\StringUtil;
+
+/**
+ * Shows the application management form.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2021 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Form
+ */
+class ApplicationManagementForm extends AbstractForm
+{
+    /**
+     * @inheritDoc
+     */
+    public $activeMenuItem = 'wcf.acp.menu.link.application.management';
+
+    /**
+     * list of applications
+     * @var ViewableApplicationList
+     */
+    public $applicationList;
+
+    /**
+     * @var string
+     */
+    public $cookieDomain = '';
+
+    /**
+     * @var string
+     */
+    public $domainName = '';
+
+    /**
+     * @var int[]
+     */
+    public $landingPageID = [];
+
+    /**
+     * @inheritDoc
+     */
+    public $neededPermissions = ['admin.configuration.canManageApplication'];
+
+    /**
+     * nested list of page nodes
+     * @var \RecursiveIteratorIterator
+     */
+    public $pageNodeList;
+
+    /**
+     * @inheritDoc
+     */
+    public function readParameters()
+    {
+        parent::readParameters();
+
+        $this->pageNodeList = (new PageNodeTree())->getNodeList();
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function readFormParameters()
+    {
+        parent::readFormParameters();
+
+        if (isset($_POST['cookieDomain'])) $this->cookieDomain = StringUtil::trim($_POST['cookieDomain']);
+        if (isset($_POST['domainName'])) $this->domainName = StringUtil::trim($_POST['domainName']);
+        if (isset($_POST['landingPageID']) && \is_array($_POST['landingPageID'])) $this->landingPageID = ArrayUtil::toIntegerArray($_POST['landingPageID']);
+    }
+
+    public function validate()
+    {
+        parent::validate();
+
+        if (ApplicationHandler::getInstance()->isMultiDomainSetup()) {
+            // Changes to the domain for all apps are only possible for setups using the same domain.
+            if (!empty($this->cookieDomain) || !empty($this->domainName)) {
+                throw new PermissionException();
+            }
+        }
+
+        if (empty($this->domainName)) {
+            throw new UserInputException('domainName');
+        } else {
+            $regex = new Regex('^https?\://');
+            $this->domainName = FileUtil::removeTrailingSlash($regex->replace($this->domainName, ''));
+            $this->cookieDomain = FileUtil::removeTrailingSlash($regex->replace($this->cookieDomain, ''));
+
+            // domain may not contain path components
+            $regex = new Regex('[/#\?&]');
+            if ($regex->match($this->domainName)) {
+                throw new UserInputException('domainName', 'containsPath');
+            } elseif ($regex->match($this->cookieDomain)) {
+                throw new UserInputException('cookieDomain', 'containsPath');
+            }
+
+            // strip port from cookie domain
+            $regex = new Regex(':[0-9]+$');
+            $this->cookieDomain = $regex->replace($this->cookieDomain, '');
+
+            // check if cookie domain shares the same domain (may exclude subdomains)
+            if (!StringUtil::endsWith($regex->replace($this->domainName, ''), $this->cookieDomain)) {
+                throw new UserInputException('cookieDomain', 'invalid');
+            }
+        }
+
+        foreach ($this->landingPageID as $packageID => $landingPageID) {
+            if (!$landingPageID) {
+                continue;
+            }
+
+            $page = new Page($landingPageID);
+            if (!$page->pageID) {
+                throw new UserInputException('landingPageID');
+            } elseif ($page->requireObjectID || $page->excludeFromLandingPage || $page->isDisabled) {
+                throw new UserInputException('landingPageID', 'invalid');
+            }
+        }
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function readData()
+    {
+        parent::readData();
+
+        $this->applicationList = new ViewableApplicationList();
+        $this->applicationList->readObjects();
+
+        if (!ApplicationHandler::getInstance()->isMultiDomainSetup()) {
+            $core = ApplicationHandler::getInstance()->getApplicationByID(1);
+            $this->domainName = $core->domainName;
+            $this->cookieDomain = $core->cookieDomain;
+        }
+    }
+
+    public function save()
+    {
+        parent::save();
+
+        if (!ApplicationHandler::getInstance()->isMultiDomainSetup()) {
+            $sql = "UPDATE  wcf" . WCF_N . "_application
+                    SET     domainName = ?,
+                            cookieDomain = ?";
+            $statement = WCF::getDB()->prepareStatement($sql);
+            $statement->execute([
+                $this->domainName,
+                $this->cookieDomain,
+            ]);
+        }
+
+        $sql = "UPDATE  wcf" . WCF_N . "_application
+                SET     landingPageID = ?
+                WHERE   packageID = ?";
+        $statement = WCF::getDB()->prepareStatement($sql);
+        foreach ($this->landingPageID as $packageID => $landingPageID) {
+            $statement->execute([
+                $landingPageID ?: null,
+                $packageID,
+            ]);
+        }
+
+        $this->saved();
+
+        if (!empty($this->landingPageID[1])) {
+            (new Page($this->landingPageID[1]))->setAsLandingPage();
+        } else {
+            $sql = "UPDATE  wcf" . WCF_N . "_page
+                        SET     isLandingPage = ?
+                        WHERE   isLandingPage = ?";
+            $statement = WCF::getDB()->prepareStatement($sql);
+            $statement->execute([
+                0,
+                1,
+            ]);
+        }
+
+        ApplicationHandler::rebuild();
+
+        // Reset caches to reflect the new landing pages.
+        ApplicationCacheBuilder::getInstance()->reset();
+        RoutingCacheBuilder::getInstance()->reset();
+
+        // Reload the applications to update the selected landing page id.
+        $this->applicationList = new ViewableApplicationList();
+        $this->applicationList->readObjects();
+
+        // show success message
+        WCF::getTPL()->assign('success', true);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function assignVariables()
+    {
+        parent::assignVariables();
+
+        $pageList = new PageList();
+        $pageList->readObjects();
+
+        WCF::getTPL()->assign([
+            'applicationList' => $this->applicationList,
+            'cookieDomain' => $this->cookieDomain,
+            'domainName' => $this->domainName,
+            'isMultiDomainSetup' => ApplicationHandler::getInstance()->isMultiDomainSetup(),
+            'pageNodeList' => $this->pageNodeList,
+            'pageList' => $pageList->getObjects(),
+        ]);
+    }
+}
diff --git a/wcfsetup/install/files/lib/acp/page/ApplicationManagementPage.class.php b/wcfsetup/install/files/lib/acp/page/ApplicationManagementPage.class.php
deleted file mode 100644 (file)
index 7a88f6b..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-
-namespace wcf\acp\page;
-
-use wcf\data\application\ViewableApplicationList;
-use wcf\data\page\PageList;
-use wcf\page\AbstractPage;
-use wcf\system\WCF;
-
-/**
- * Shows the application management page.
- *
- * @author  Alexander Ebert
- * @copyright   2001-2019 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package WoltLabSuite\Core\Acp\Page
- */
-class ApplicationManagementPage extends AbstractPage
-{
-    /**
-     * @inheritDoc
-     */
-    public $activeMenuItem = 'wcf.acp.menu.link.application.management';
-
-    /**
-     * list of applications
-     * @var ViewableApplicationList
-     */
-    public $applicationList;
-
-    /**
-     * @inheritDoc
-     */
-    public $neededPermissions = ['admin.configuration.canManageApplication'];
-
-    /**
-     * @inheritDoc
-     */
-    public function readData()
-    {
-        parent::readData();
-
-        $this->applicationList = new ViewableApplicationList();
-        $this->applicationList->readObjects();
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function assignVariables()
-    {
-        parent::assignVariables();
-
-        $pageList = new PageList();
-        $pageList->readObjects();
-
-        WCF::getTPL()->assign([
-            'applicationList' => $this->applicationList,
-            'pageList' => $pageList->getObjects(),
-        ]);
-    }
-}
index 9edd53986c5e9ba4e3a7a32a8f0928bebbee180d..303fa51fcb60007f05286d294128e839f4a78773 100644 (file)
@@ -235,6 +235,7 @@ class ApplicationHandler extends SingletonFactory
      *
      * @return      bool
      * @since       3.1
+     * @deprecated  5.4
      */
     public function isMultiDomainSetup()
     {
index 66c9c5afd208f3f18e1b3492538b82a9495f29a0..2215a10ef8da2cfb7da9ce8d16d1c55b420afffe 100644 (file)
                <item name="wcf.acp.application.edit"><![CDATA[App bearbeiten]]></item>
                <item name="wcf.acp.application.edit.title"><![CDATA[App bearbeiten: „<a href="{link controller='Package' id=$application->packageID}{/link}">{$application->getPackage()->getName()}</a>“]]></item>
                <item name="wcf.acp.application.landingPage"><![CDATA[Einstiegsseite]]></item>
+               <item name="wcf.acp.application.landingPage.default"><![CDATA[(Standard)]]></item>
                <item name="wcf.acp.application.landingPage.description"><![CDATA[Optional: {if LANGUAGE_USE_INFORMAL_VARIANT}Gib{else}Geben Sie{/if} die Seite an, die angezeigt wird, wenn diese App direkt aufgerufen wird.]]></item>
                <item name="wcf.acp.application.list"><![CDATA[Installierte Apps]]></item>
+               <item name="wcf.acp.application.multiDomain"><![CDATA[<p style="font-size: 18px">Die Unterstützung für Installationen mit mehreren Domains wird eingestellt.</p>
+<p><br></p>
+<p>Es wird dringend empfohlen die Apps auf eine gemeinsame Domain zu verschieben, bitte passen Sie dazu die Domain- und Pfadkonfiguration der installierten Apps an:</p>
+<ul class="nativeList">
+       {foreach from=$applicationList item=application}
+               <li><a href="{link controller='ApplicationEdit' id=$application->packageID}{/link}">{$application->getPackage()->getName()}</a></li>
+       {/foreach}
+</ul>]]></item>
+               <item name="wcf.acp.application.management.domain"><![CDATA[Domain-Konfiguration]]></item>
+               <item name="wcf.acp.application.management.domainName"><![CDATA[Domain]]></item>
+               <item name="wcf.acp.application.management.domainName.description"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Gib{else}Geben Sie{/if} den Domain-Namen ohne Protokoll und Pfad ein, beispielsweise „www.example.com“.]]></item>
+               <item name="wcf.acp.application.management.cookieDomain"><![CDATA[Cookie-Domain]]></item>
+               <item name="wcf.acp.application.management.cookieDomain.description"><![CDATA[Die Cookie-Domain muss zur eingegebenen Domain passen, die Eingabe erfolgt ohne Protokoll und Pfad. Sollten {if LANGUAGE_USE_INFORMAL_VARIANT}du dir{else}Sie sich{/if} unsicher sein, {if LANGUAGE_USE_INFORMAL_VARIANT}trage{else}tragen Sie{/if} hier bitte den identischen Wert wie für „Domain“ ein.]]></item>
        </category>
        <category name="wcf.acp.article">
                <item name="wcf.acp.article.add"><![CDATA[Artikel hinzufügen]]></item>
index cc671d9f2b1d9004eabd62073a1f763852f5db7f..3c70f94f22d40fd4f546325247f62d33d221d532 100644 (file)
                <item name="wcf.acp.application.edit"><![CDATA[Edit Application]]></item>
                <item name="wcf.acp.application.edit.title"><![CDATA[Edit Application: “<a href="{link controller='Package' id=$application->packageID}{/link}">{$application->getPackage()->getName()}</a>”]]></item>
                <item name="wcf.acp.application.landingPage"><![CDATA[Entry Page]]></item>
+               <item name="wcf.acp.application.landingPage.default"><![CDATA[(Default)]]></item>
                <item name="wcf.acp.application.landingPage.description"><![CDATA[Optional: This page will be shown when a user is navigating to this app.]]></item>
                <item name="wcf.acp.application.list"><![CDATA[Installed Applications]]></item>
+               <item name="wcf.acp.application.multiDomain"><![CDATA[<p style="font-size: 18px">The support for setups with multiple domains is discontinued.</p>
+<p><br></p>
+<p>It is strongly recommended to consolidate all apps on a shared domain, please adjust the domain and path settings for these apps:</p>
+<ul class="nativeList">
+       {foreach from=$applicationList item=application}
+               <li><a href="{link controller='ApplicationEdit' id=$application->packageID}{/link}">{$application->getPackage()->getName()}</a></li>
+       {/foreach}
+</ul>]]></item>
+               <item name="wcf.acp.application.management.domain"><![CDATA[Domain Configuration]]></item>
+               <item name="wcf.acp.application.management.domainName"><![CDATA[Domain]]></item>
+               <item name="wcf.acp.application.management.domainName.description"><![CDATA[Provide the domain name excluding any protocol or path, for example “www.example.com”.]]></item>
+               <item name="wcf.acp.application.management.cookieDomain"><![CDATA[Cookie Domain]]></item>
+               <item name="wcf.acp.application.management.cookieDomain.description"><![CDATA[The cookie domain must match the provided domain. When in doubt simply provide the same value that you have entered for “Domain”.]]></item>
        </category>
        <category name="wcf.acp.article">
                <item name="wcf.acp.article.add"><![CDATA[Add Article]]></item>