From 2b63c6d21b285a3ea467b927336c2f1e99a8c267 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sat, 17 Nov 2018 20:48:05 +0100 Subject: [PATCH] Application overrides for pages installed by packages Closes #2773 --- .../install/files/acp/templates/pageAdd.tpl | 24 ++++++- .../files/lib/acp/form/PageEditForm.class.php | 37 ++++++++++- .../files/lib/data/page/Page.class.php | 17 +++-- .../builder/RoutingCacheBuilder.class.php | 62 ++++++++++++++++++- .../system/request/ControllerMap.class.php | 29 ++++++++- .../lib/system/request/LinkHandler.class.php | 4 +- wcfsetup/setup/db/install.sql | 4 +- 7 files changed, 166 insertions(+), 11 deletions(-) diff --git a/wcfsetup/install/files/acp/templates/pageAdd.tpl b/wcfsetup/install/files/acp/templates/pageAdd.tpl index 3350cea03d..824b61d55d 100644 --- a/wcfsetup/install/files/acp/templates/pageAdd.tpl +++ b/wcfsetup/install/files/acp/templates/pageAdd.tpl @@ -139,7 +139,7 @@ - + originIsSystem} style="display: none"{/if}>
+ {foreach from=$availableApplications item=availableApplication} + + {/foreach} + + {if $errorField == 'overrideApplicationPackageID'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}wcf.acp.page.application.error.{@$errorType}{/lang} + {/if} + + {/if} +
+ + {/if} + {if !$isMultilingual}
diff --git a/wcfsetup/install/files/lib/acp/form/PageEditForm.class.php b/wcfsetup/install/files/lib/acp/form/PageEditForm.class.php index 82a7c547dc..8f5d7787a4 100644 --- a/wcfsetup/install/files/lib/acp/form/PageEditForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/PageEditForm.class.php @@ -5,6 +5,7 @@ use wcf\data\page\PageAction; use wcf\data\page\PageCache; use wcf\form\AbstractForm; use wcf\system\acl\simple\SimpleAclHandler; +use wcf\system\application\ApplicationHandler; use wcf\system\exception\IllegalLinkException; use wcf\system\exception\UserInputException; use wcf\system\language\LanguageFactory; @@ -26,6 +27,11 @@ class PageEditForm extends PageAddForm { */ public $activeMenuItem = 'wcf.acp.menu.link.cms.page.list'; + /** + * @var int + */ + public $overrideApplicationPackageID; + /** * page id * @var integer @@ -67,6 +73,8 @@ class PageEditForm extends PageAddForm { public function readFormParameters() { parent::readFormParameters(); + if (isset($_POST['overrideApplicationPackageID'])) $this->overrideApplicationPackageID = intval($_POST['overrideApplicationPackageID']); + $this->pageType = $this->page->pageType; if ($this->page->originIsSystem) { $this->applicationPackageID = $this->page->applicationPackageID; @@ -75,6 +83,9 @@ class PageEditForm extends PageAddForm { $this->parentPageID = $this->page->parentPageID; } } + else { + $this->overrideApplicationPackageID = null; + } if ($this->page->requireObjectID || $this->page->excludeFromLandingPage) { // pages that require an object id can never be set as landing page @@ -82,6 +93,27 @@ class PageEditForm extends PageAddForm { } } + /** + * @inheritDoc + */ + public function validate() { + parent::validate(); + + $this->validateOverrideApplicationPackageID(); + } + + protected function validateOverrideApplicationPackageID() { + if ($this->overrideApplicationPackageID) { + if ($this->overrideApplicationPackageID == $this->applicationPackageID) { + // Picking the same app would have the same result, but also creates some overhead in the internal routing. + $this->overrideApplicationPackageID = null; + } + else if (ApplicationHandler::getInstance()->getApplicationByID($this->overrideApplicationPackageID) === null) { + throw new UserInputException('overrideApplicationPackageID', 'invalid'); + } + } + } + /** * @inheritDoc */ @@ -155,6 +187,7 @@ class PageEditForm extends PageAddForm { 'lastUpdateTime' => TIME_NOW, 'parentPageID' => $this->parentPageID ?: null, 'applicationPackageID' => $this->applicationPackageID, + 'overrideApplicationPackageID' => $this->overrideApplicationPackageID, 'availableDuringOfflineMode' => $this->availableDuringOfflineMode, 'allowSpidersToIndex' => $this->allowSpidersToIndex, 'enableShareButtons' => $this->enableShareButtons @@ -240,6 +273,7 @@ class PageEditForm extends PageAddForm { $this->parentPageID = $this->page->parentPageID; $this->pageType = $this->page->pageType; $this->applicationPackageID = $this->page->applicationPackageID; + $this->overrideApplicationPackageID = $this->page->overrideApplicationPackageID; $this->cssClassName = $this->page->cssClassName; if ($this->page->controllerCustomURL) $this->customURL[0] = $this->page->controllerCustomURL; if ($this->page->isLandingPage) $this->isLandingPage = 1; @@ -285,7 +319,8 @@ class PageEditForm extends PageAddForm { 'action' => 'edit', 'pageID' => $this->pageID, 'page' => $this->page, - 'lastVersion' => VersionTracker::getInstance()->getLastVersion('com.woltlab.wcf.page', $this->pageID) + 'lastVersion' => VersionTracker::getInstance()->getLastVersion('com.woltlab.wcf.page', $this->pageID), + 'overrideApplicationPackageID' => $this->overrideApplicationPackageID, ]); } } diff --git a/wcfsetup/install/files/lib/data/page/Page.class.php b/wcfsetup/install/files/lib/data/page/Page.class.php index c141e667d9..0e06560a3d 100644 --- a/wcfsetup/install/files/lib/data/page/Page.class.php +++ b/wcfsetup/install/files/lib/data/page/Page.class.php @@ -1,5 +1,6 @@ overrideApplicationPackageID) { + $application = ApplicationHandler::getInstance()->getApplicationByID($this->overrideApplicationPackageID)->getAbbreviation(); + } + return LinkHandler::getInstance()->getLink($controllerName, [ - 'application' => $controllerParts[0], + 'application' => $application, 'forceFrontend' => true ]); } - else if ($this->applicationPackageID === null) { + else if ($this->applicationPackageID === null && $this->overrideApplicationPackageID === null) { // we cannot reliably generate a link for an orphaned page return ''; - } + } return LinkHandler::getInstance()->getCmsLink($this->pageID); } @@ -191,10 +198,10 @@ class Page extends DatabaseObject implements ILinkableObject, ITitledObject { /** * Returns the application of this page. * - * @return \wcf\data\application\Application + * @return Application */ public function getApplication() { - return ApplicationHandler::getInstance()->getApplicationByID($this->applicationPackageID); + return ApplicationHandler::getInstance()->getApplicationByID($this->overrideApplicationPackageID ?: $this->applicationPackageID); } /** diff --git a/wcfsetup/install/files/lib/system/cache/builder/RoutingCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/RoutingCacheBuilder.class.php index 5cf5f629f1..3e9bca437f 100644 --- a/wcfsetup/install/files/lib/system/cache/builder/RoutingCacheBuilder.class.php +++ b/wcfsetup/install/files/lib/system/cache/builder/RoutingCacheBuilder.class.php @@ -5,6 +5,7 @@ use wcf\data\application\Application; use wcf\data\page\PageCache; use wcf\page\CmsPage; use wcf\system\application\ApplicationHandler; +use wcf\system\language\LanguageFactory; use wcf\system\request\ControllerMap; use wcf\system\WCF; use wcf\util\FileUtil; @@ -35,10 +36,69 @@ class RoutingCacheBuilder extends AbstractCacheBuilder { protected function rebuild(array $parameters) { $data = [ 'ciControllers' => $this->getCaseInsensitiveControllers(), - 'landingPages' => $this->getLandingPages() + 'landingPages' => $this->getLandingPages(), ]; $data['customUrls'] = $this->getCustomUrls($data['landingPages']); + $data['applicationOverrides'] = $this->getApplicationOverrides($data['customUrls']); + + return $data; + } + + /** + * Pages that belong to an installed package have an immutable application assigned to + * them which controls both the controller resolution and the base link. The override + * declares a different application that will be used instead without actually migrating + * the page to a different application. + * + * @param array $customUrls + * @return array + */ + protected function getApplicationOverrides(array &$customUrls) { + $data = [ + // URL -> Controller + 'lookup' => [], + // Controller -> URL + 'reverse' => [], + ]; + + $abbreviations = []; + foreach (ApplicationHandler::getInstance()->getApplications() as $application) { + $abbreviations[$application->packageID] = $application->getAbbreviation(); + } + + $languageIDs = [0]; + foreach (LanguageFactory::getInstance()->getLanguages() as $language) { + $languageIDs[] = $language->languageID; + } + + $sql = "SELECT pageID, pageType, controller, applicationPackageID, overrideApplicationPackageID + FROM wcf".WCF_N."_page + WHERE overrideApplicationPackageID IS NOT NULL"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(); + while ($row = $statement->fetchArray()) { + $application = $abbreviations[$row['applicationPackageID']]; + $overrideApplication = $abbreviations[$row['overrideApplicationPackageID']]; + + if ($row['pageType'] === 'system') { + $tmp = explode('\\', $row['controller']); + $controller = preg_replace('/(?:Action|Form|Page)$/', '', array_pop($tmp)); + $data['lookup'][$overrideApplication][$controller] = $application; + $data['reverse'][$application][$controller] = $overrideApplication; + } + else { + foreach ($languageIDs as $languageID) { + $key = "__WCF_CMS__{$row['pageID']}-{$languageID}"; + if (isset($customUrls['reverse'][$application][$key])) { + $controller = $customUrls['reverse'][$application][$key]; + $data['lookup'][$overrideApplication][$controller] = $application; + $data['reverse'][$application][$controller] = $overrideApplication; + } + } + } + + } return $data; } diff --git a/wcfsetup/install/files/lib/system/request/ControllerMap.class.php b/wcfsetup/install/files/lib/system/request/ControllerMap.class.php index ecb1fe3e91..61cd90e41c 100644 --- a/wcfsetup/install/files/lib/system/request/ControllerMap.class.php +++ b/wcfsetup/install/files/lib/system/request/ControllerMap.class.php @@ -18,6 +18,12 @@ use wcf\system\WCFACP; * @since 3.0 */ class ControllerMap extends SingletonFactory { + /** + * @var array + * @since 3.2 + */ + protected $applicationOverrides; + /** * @var array */ @@ -44,6 +50,7 @@ class ControllerMap extends SingletonFactory { * @throws SystemException */ protected function init() { + $this->applicationOverrides = RoutingCacheBuilder::getInstance()->getData([], 'applicationOverrides'); $this->ciControllers = RoutingCacheBuilder::getInstance()->getData([], 'ciControllers'); $this->customUrls = RoutingCacheBuilder::getInstance()->getData([], 'customUrls'); $this->landingPages = RoutingCacheBuilder::getInstance()->getData([], 'landingPages'); @@ -78,9 +85,14 @@ class ControllerMap extends SingletonFactory { else if ($controller === 'AjaxUpload') $controller = 'AJAXUpload'; else if ($controller === 'AjaxInvoke') $controller = 'AJAXInvoke'; - // work-around for package installation during upgrade 2.1 -> 3.0 + // work-around for package installation during the upgrade 2.1 -> 3.0 if ($isAcpRequest && $controller === 'InstallPackage') $application = 'wcf'; + // Map virtual controllers to their true application + if (isset($this->applicationOverrides['lookup'][$application][$controller])) { + $application = $this->applicationOverrides['lookup'][$application][$controller]; + } + $classData = $this->getClassData($application, $controller, $isAcpRequest, 'page'); if ($classData === null) $classData = $this->getClassData($application, $controller, $isAcpRequest, 'form'); if ($classData === null) $classData = $this->getClassData($application, $controller, $isAcpRequest, 'action'); @@ -315,6 +327,21 @@ class ControllerMap extends SingletonFactory { return true; } + /** + * Returns the virtual application abbreviation for the provided controller. + * + * @param string $application + * @param string $controller + * @return string + */ + public function getApplicationOverride($application, $controller) { + if (isset($this->applicationOverrides['reverse'][$application][$controller])) { + return $this->applicationOverrides['reverse'][$application][$controller]; + } + + return $application; + } + /** * Lookups the list of legacy controller names that violate the name * schema, e.g. are named 'BBCodeList' instead of `BbCodeList`. diff --git a/wcfsetup/install/files/lib/system/request/LinkHandler.class.php b/wcfsetup/install/files/lib/system/request/LinkHandler.class.php index 9ceb5a8268..57853f5f54 100644 --- a/wcfsetup/install/files/lib/system/request/LinkHandler.class.php +++ b/wcfsetup/install/files/lib/system/request/LinkHandler.class.php @@ -186,6 +186,8 @@ class LinkHandler extends SingletonFactory { $url = $routeURL . $url; + $abbreviation = ControllerMap::getInstance()->getApplicationOverride($abbreviation, $controller); + // handle applications if (!PACKAGE_ID) { $url = RouteHandler::getHost() . RouteHandler::getPath(['acp']) . ($isACP ? 'acp/' : '') . $url; @@ -258,7 +260,7 @@ class LinkHandler extends SingletonFactory { } return $this->getLink($data['controller'], [ - 'application' => $data['application'], + 'application' => $application, 'forceFrontend' => true ]); } diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index bd2ff80867..7b44e8f490 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -1049,6 +1049,7 @@ CREATE TABLE wcf1_page ( originIsSystem TINYINT(1) NOT NULL DEFAULT 0, packageID INT(10) NOT NULL, applicationPackageID INT(10), + overrideApplicationPackageID INT(10), controller VARCHAR(255) NOT NULL DEFAULT '', handler VARCHAR(255) NOT NULL DEFAULT '', controllerCustomURL VARCHAR(255) NOT NULL DEFAULT '', @@ -1995,6 +1996,7 @@ ALTER TABLE wcf1_paid_subscription_transaction_log ADD FOREIGN KEY (paymentMetho ALTER TABLE wcf1_page ADD FOREIGN KEY (parentPageID) REFERENCES wcf1_page (pageID) ON DELETE SET NULL; ALTER TABLE wcf1_page ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE; ALTER TABLE wcf1_page ADD FOREIGN KEY (applicationPackageID) REFERENCES wcf1_package (packageID) ON DELETE SET NULL; +ALTER TABLE wcf1_page ADD FOREIGN KEY (overrideApplicationPackageID) REFERENCES wcf1_package (packageID) ON DELETE SET NULL; ALTER TABLE wcf1_page_box_order ADD FOREIGN KEY (pageID) REFERENCES wcf1_page (pageID) ON DELETE CASCADE; ALTER TABLE wcf1_page_box_order ADD FOREIGN KEY (boxID) REFERENCES wcf1_box (boxID) ON DELETE CASCADE; @@ -2392,4 +2394,4 @@ INSERT INTO wcf1_reaction_type (title, type, showOrder, iconFile) VALUES ('wcf.r INSERT INTO wcf1_reaction_type (title, type, showOrder, iconFile) VALUES ('wcf.reactionType.title2', 1, 2, 'haha.svg'); INSERT INTO wcf1_reaction_type (title, type, showOrder, iconFile) VALUES ('wcf.reactionType.title3', -1, 3, 'sad.svg'); INSERT INTO wcf1_reaction_type (title, type, showOrder, iconFile) VALUES ('wcf.reactionType.title4', 0, 4, 'confused.svg'); -INSERT INTO wcf1_reaction_type (title, type, showOrder, iconFile) VALUES ('wcf.reactionType.title5', 1, 5, 'thanks.svg'); \ No newline at end of file +INSERT INTO wcf1_reaction_type (title, type, showOrder, iconFile) VALUES ('wcf.reactionType.title5', 1, 5, 'thanks.svg'); -- 2.20.1