Added {page}-PIP and CMS routing support
authorAlexander Ebert <ebert@woltlab.com>
Wed, 2 Dec 2015 17:41:39 +0000 (18:41 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 2 Dec 2015 17:41:48 +0000 (18:41 +0100)
com.woltlab.wcf/templates/cms.tpl [new file with mode: 0644]
wcfsetup/install/files/lib/data/page/Page.class.php
wcfsetup/install/files/lib/page/CmsPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/cache/builder/RoutingCacheBuilder.class.php
wcfsetup/install/files/lib/system/request/CmsLinkHandler.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/request/ControllerMap.class.php
wcfsetup/install/files/lib/system/request/LinkHandler.class.php
wcfsetup/install/files/lib/system/request/RequestHandler.class.php
wcfsetup/install/files/lib/system/template/plugin/PageBlockTemplatePlugin.class.php [new file with mode: 0644]
wcfsetup/install/files/style/layout/global.scss

diff --git a/com.woltlab.wcf/templates/cms.tpl b/com.woltlab.wcf/templates/cms.tpl
new file mode 100644 (file)
index 0000000..d92be9d
--- /dev/null
@@ -0,0 +1,59 @@
+{include file='documentHeader'}
+
+<head>
+       <title>{if !$page->isLandingPage}{$content[title]} - {/if}{PAGE_TITLE|language}</title>
+       
+       {include file='headInclude'}
+       
+       <link rel="canonical" href="{$canonicalURL}">
+</head>
+
+<body id="tpl{$templateName|ucfirst}" data-template="{$templateName}" data-application="{$templateNameApplication}">
+
+{include file='header'}
+
+{if $page->isLandingPage}
+       <header class="boxHeadline">
+               <h1>{PAGE_TITLE|language}</h1>
+               {hascontent}<p>{content}{PAGE_DESCRIPTION|language}{/content}</p>{/hascontent}
+       </header>
+{else}
+       <header class="boxHeadline">
+               <h1>{$content[title]}</h1>
+       </header>
+{/if}
+
+{include file='userNotice'}
+
+<div class="contentNavigation">
+       {hascontent}
+               <nav>
+                       <ul>
+                               {content}
+                                       {event name='contentNavigationButtonsTop'}
+                               {/content}
+                       </ul>
+               </nav>
+       {/hascontent}
+</div>
+
+<section class="cmsContent htmlContent">
+       {@$content[content]}
+</section>
+
+<div class="contentNavigation">
+       {hascontent}
+               <nav>
+                       <ul>
+                               {content}
+                                       {event name='contentNavigationButtonsBottom'}
+                               {/content}
+                       </ul>
+               </nav>
+       {/hascontent}
+</div>
+
+{include file='footer'}
+
+</body>
+</html>
index d0f8a834914a276684798db9368b9ae0ddc6e9ae..5f4cf10a82d3dbe3c62cd15ae1d5b8d5d4763845 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 namespace wcf\data\page;
 use wcf\data\DatabaseObject;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
 use wcf\system\WCF;
 
 /**
@@ -53,28 +54,53 @@ class Page extends DatabaseObject {
        /**
         * Returns the page content.
         * 
-        * @return array
+        * @return      array           content data
         */
        public function getPageContent() {
-               $content = array();
+               $content = [];
+               
                $sql = "SELECT  *
                        FROM    wcf".WCF_N."_page_content
-                       WHERE   pageID = ?";
+                       WHERE   pageID = ?";
                $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute(array($this->pageID));
+               $statement->execute([$this->pageID]);
                while ($row = $statement->fetchArray()) {
-                       $content[($row['languageID'] ?: 0)] = array(
+                       $content[($row['languageID'] ?: 0)] = [
                                'title' => $row['title'],
                                'content' => $row['content'],
                                'metaDescription' => $row['metaDescription'],
                                'metaKeywords' => $row['metaKeywords'],
                                'customURL' => $row['customURL']
-                       );
+                       ];
                }
                
                return $content;
        }
        
+       /**
+        * Returns content for a single language, passing `null` for `$languageID` is undefined
+        * for multilingual pages.
+        * 
+        * @param       integer         $languageID     language id or `null` if there are no localized versions
+        * @return      string[]        page content data
+        * @throws      \wcf\system\database\DatabaseException
+        */
+       public function getPageContentByLanguage($languageID = null) {
+               $conditions = new PreparedStatementConditionBuilder();
+               $conditions->add("pageID = ?", [$this->pageID]);
+               if ($this->isMultilingual) $conditions->add("languageID = ?", [$languageID]);
+               else $conditions->add("languageID IS NULL");
+               
+               $sql = "SELECT  *
+                       FROM    wcf".WCF_N."_page_content
+                       ".$conditions;
+               $statement = WCF::getDB()->prepareStatement($sql, 1);
+               $statement->execute($conditions->getParameters());
+               $row = $statement->fetchSingleRow();
+               
+               return ($row !== false) ? $row : [];
+       }
+       
        /**
         * Returns the page URL.
         * 
@@ -84,6 +110,22 @@ class Page extends DatabaseObject {
                // @todo
        }
        
+       /**
+        * Returns the page with the given identifier.
+        * 
+        * @param       string          $identifier     unique page identifier
+        * @return      Page
+        */
+       public static function getPageByIdentifier($identifier) {
+               $sql = "SELECT  *
+                       FROM    wcf".WCF_N."_page
+                       WHERE   identifier = ?";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute([$identifier]);
+               
+               return $statement->fetchObject(Page::class);
+       }
+       
        /**
         * Returns the page with the given name.
         * 
@@ -95,10 +137,8 @@ class Page extends DatabaseObject {
                        FROM    wcf".WCF_N."_page
                        WHERE   name = ?";
                $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute(array($name));
-               $row = $statement->fetchArray();
-               if ($row !== false) return new Page(null, $row);
+               $statement->execute([$name]);
                
-               return null;
+               return $statement->fetchObject(Page::class);
        }
 }
diff --git a/wcfsetup/install/files/lib/page/CmsPage.class.php b/wcfsetup/install/files/lib/page/CmsPage.class.php
new file mode 100644 (file)
index 0000000..df64878
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+namespace wcf\page;
+use wcf\data\page\Page;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+
+/**
+ * Generic controller to display cms content.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage page
+ * @category   Community Framework
+ */
+class CmsPage extends AbstractPage {
+       /**
+        * @var string[]
+        */
+       public $content;
+       
+       /**
+        * @var integer
+        */
+       public $languageID;
+       
+       /**
+        * @var Page
+        */
+       public $page;
+       
+       /**
+        * @var integer
+        */
+       public $pageID;
+       
+       /**
+        * @inheritDoc
+        * @throws      IllegalLinkException
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if (isset($_GET['languageID'])) $this->languageID = intval($_GET['languageID']);
+               if (isset($_GET['pageID'])) $this->pageID = intval($_GET['pageID']);
+               
+               if ($this->pageID) {
+                       $this->page = new Page($this->pageID);
+               }
+               
+               if ($this->page === null) {
+                       throw new IllegalLinkException();
+               }
+               
+               $this->content = $this->page->getPageContentByLanguage($this->languageID);
+               if (empty($this->content)) {
+                       throw new IllegalLinkException();
+               }
+               
+               $this->canonicalURL = LinkHandler::getInstance()->getCmsLink($this->pageID, $this->languageID);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign([
+                       'canonicalURL' => $this->canonicalURL,
+                       'content' => $this->content,
+                       'contentLanguageID' => $this->languageID,
+                       'page' => $this->page,
+                       'pageID' => $this->pageID
+               ]);
+       }
+}
index 01f30355e8700a614131dcb7258ce33999945df3..6dd4523bfbf95ab27a4ca6215101f34e6103ae40 100644 (file)
@@ -143,7 +143,7 @@ class RoutingCacheBuilder extends AbstractCacheBuilder {
                                $data['reverse'][$abbreviations[$packageID]][preg_replace('~^.*?([A-Za-z0-9]+)(?:Action|Form|Page)~', '$1', $row['controller'])] = $customUrl;
                        }
                        else {
-                               $cmsIdentifier = '__WCF_CMS__' . $row['pageID'] . '-' . $row['languageID'];
+                               $cmsIdentifier = '__WCF_CMS__' . $row['pageID'] . '-' . ($row['languageID'] ?: 0);
                                $data['lookup'][$abbreviations[$packageID]][$customUrl] = $cmsIdentifier;
                                $data['reverse'][$abbreviations[$packageID]][$cmsIdentifier] = $customUrl;
                        }
diff --git a/wcfsetup/install/files/lib/system/request/CmsLinkHandler.class.php b/wcfsetup/install/files/lib/system/request/CmsLinkHandler.class.php
new file mode 100644 (file)
index 0000000..08c95dd
--- /dev/null
@@ -0,0 +1,11 @@
+<?php
+namespace wcf\system\request;
+use wcf\system\SingletonFactory;
+
+class CmsLinkHandler extends SingletonFactory {
+       public function getLink($pageID, $languageID = -1) {
+               return LinkHandler::getInstance()->getLink('Cms', [
+                       'application' => ''
+               ]);
+       }
+}
index 3a7a662c1fc518339c8088b317d92d032577f8bf..3dcb0149bdd61763f8bf61eba92ebba709b383d1 100644 (file)
@@ -1,9 +1,9 @@
 <?php
 namespace wcf\system\request;
+use wcf\page\CmsPage;
 use wcf\system\cache\builder\RoutingCacheBuilder;
 use wcf\system\exception\SystemException;
 use wcf\system\SingletonFactory;
-use wcf\util\StringUtil;
 
 /**
  * Resolves incoming requests and performs lookups for controller to url mappings.
@@ -16,8 +16,14 @@ use wcf\util\StringUtil;
  * @category   Community Framework
  */
 class ControllerMap extends SingletonFactory {
+       /**
+        * @var string[][]
+        */
        protected $ciControllers;
        
+       /**
+        * @var string[][]
+        */
        protected $customUrls;
        
        /**
@@ -26,6 +32,10 @@ class ControllerMap extends SingletonFactory {
         */
        protected $lookupCache = [];
        
+       /**
+        * @inheritDoc
+        * @throws      SystemException
+        */
        protected function init() {
                $this->ciControllers = RoutingCacheBuilder::getInstance()->getData([], 'ciControllers');
                $this->customUrls = RoutingCacheBuilder::getInstance()->getData([], 'customUrls');
@@ -92,9 +102,10 @@ class ControllerMap extends SingletonFactory {
                if (isset($this->customUrls['lookup'][$application]) && isset($this->customUrls['lookup'][$application][$controller])) {
                        $data = $this->customUrls['lookup'][$application][$controller];
                        if (preg_match('~^__WCF_CMS__(?P<pageID>\d+)-(?P<languageID>\d+)$~', $data, $matches)) {
-                               // TODO: this does not work, it should match the returned array below
                                return [
-                                       'controller' => '\\wcf\\page\\CmsPage',
+                                       'className' => CmsPage::class,
+                                       'controller' => 'cms',
+                                       'pageType' => 'page',
                                        'languageID' => $matches['languageID'],
                                        'pageID' => $matches['pageID']
                                ];
@@ -144,6 +155,28 @@ class ControllerMap extends SingletonFactory {
                return $urlController;
        }
        
+       /**
+        * Looks up a cms page URL, returns an array containing the application identifier
+        * and url controller name or null if there was no match.
+        * 
+        * @param       integer         $pageID         page id
+        * @param       integer         $languageID     content language id
+        * @return      string[]|null
+        */
+       public function lookupCmsPage($pageID, $languageID) {
+               $key = '__WCF_CMS__' . $pageID . '-' . ($languageID ?: 0);
+               foreach ($this->customUrls['reverse'] as $application => $reverseURLs) {
+                       if (isset($reverseURLs[$key])) {
+                               return [
+                                       'application' => $application,
+                                       'controller' => $reverseURLs[$key]
+                               ];
+                       }
+               }
+               
+               return null;
+       }
+       
        /**
         * Returns the class data for the active request or null if for the given
         * configuration no proper class exist.
index 0b53908a162f0753467738b14189b5f78d1888ac..5f7306fc451f44a47859c843b3999f12d6bbc5e7 100644 (file)
@@ -2,6 +2,7 @@
 namespace wcf\system\request;
 use wcf\data\DatabaseObjectDecorator;
 use wcf\system\application\ApplicationHandler;
+use wcf\system\language\LanguageFactory;
 use wcf\system\menu\page\PageMenu;
 use wcf\system\Regex;
 use wcf\system\SingletonFactory;
@@ -214,4 +215,54 @@ class LinkHandler extends SingletonFactory {
                
                return $url;
        }
+       
+       /**
+        * Returns the full URL to a CMS page. The `$languageID` parameter is optional and if not
+        * present (or the integer value `-1` is given) will cause the handler to pick the correct
+        * language version based upon the user's language.
+        *
+        * Passing in an illegal page id will cause this method to fail silently, returning an
+        * empty string.
+        * 
+        * @param       integer         $pageID         page id
+        * @param       integer         $languageID     language id, optional
+        * @return      string          full URL of empty string if `$pageID` is invalid
+        * @throws      \wcf\system\exception\SystemException
+        */
+       public function getCmsLink($pageID, $languageID = -1) {
+               // use current language
+               if ($languageID === -1) {
+                       $data = ControllerMap::getInstance()->lookupCmsPage($pageID, WCF::getLanguage()->languageID);
+                       
+                       // no result
+                       if ($data === null) {
+                               // attempt to use the default language instead
+                               if (LanguageFactory::getInstance()->getDefaultLanguageID() != WCF::getLanguage()->languageID) {
+                                       $data = ControllerMap::getInstance()->lookupCmsPage($pageID, LanguageFactory::getInstance()->getDefaultLanguageID());
+                               }
+                               
+                               // no result, possibly this is a non-multilingual page
+                               if ($data === null) {
+                                       $data = ControllerMap::getInstance()->lookupCmsPage($pageID, null);
+                               }
+                               
+                               // still no result, page does not exist at all
+                               if ($data === null) {
+                                       return '';
+                               }
+                       }
+               }
+               else {
+                       $data = ControllerMap::getInstance()->lookupCmsPage($pageID, $languageID);
+                       
+                       // no result, page does not exist or at least not in the given language
+                       if ($data === null) {
+                               return '';
+                       }
+               }
+               
+               return $this->getLink($data['controller'], [
+                       'application' => $data['application']
+               ]);
+       }
 }
index c30085a1f52f82ed802599b0701d146562d70e8c..7256ed8b5eb06c39a86137295139035db49227a9 100644 (file)
@@ -173,6 +173,7 @@ class RequestHandler extends SingletonFactory {
                        $this->activeRequest = new Request($classData['className'], $classData['controller'], $classData['pageType']);
                }
                catch (SystemException $e) {
+                       die("<pre>".$e->getMessage());
                        throw new IllegalLinkException();
                }
        }
@@ -243,7 +244,7 @@ class RequestHandler extends SingletonFactory {
                // set default controller
                $applicationObj = WCF::getApplicationObject(ApplicationHandler::getInstance()->getApplication($application));
                $routeData['controller'] = preg_replace('~^.*?\\\([^\\\]+)(?:Action|Form|Page)$~', '\\1', $applicationObj->getPrimaryController());
-               $routeData['controller'] = ControllerMap::getInstance()->lookup($routeData['controller']);
+               $routeData['controller'] = ControllerMap::getInstance()->lookup($application, $routeData['controller']);
        }
        
        /**
diff --git a/wcfsetup/install/files/lib/system/template/plugin/PageBlockTemplatePlugin.class.php b/wcfsetup/install/files/lib/system/template/plugin/PageBlockTemplatePlugin.class.php
new file mode 100644 (file)
index 0000000..b67c831
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+namespace wcf\system\template\plugin;
+use wcf\data\page\Page;
+use wcf\system\exception\SystemException;
+use wcf\system\language\LanguageFactory;
+use wcf\system\request\LinkHandler;
+use wcf\system\template\TemplateEngine;
+
+/**
+ * Template block plugin which generates a link to a CMS page.
+ *
+ * The unique identifier and the `pageID` attribute as well as the `language` and
+ * `languageID` attribute are mutually exclusive. Combining these values will not
+ * cause this plugin to fail, but instead `pageID` and `languageID` will be considered
+ * to be of higher specificity and the unique identifier / `language` attribute will
+ * be ignored.
+ * 
+ * Usage, language is automatically resolved:
+ *      {page}com.woltlab.wcf.CookiePolicy{/page}
+ * 
+ * `pageID` attribute instead of unique identifier:
+ *      {page pageID=1}{/page}
+ * 
+ * `language` attribute to force a localized version:
+ *      {page language='de'}com.woltlab.wcf.CookiePolicy{/page}
+ * 
+ * `languageID` attribute similar to the `language` attribute:
+ *      {page languageID=2}com.woltlab.wcf.CookiePolicy{/page}
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage system.template.plugin
+ * @category   Community Framework
+ */
+class PageBlockTemplatePlugin implements IBlockTemplatePlugin {
+       /**
+        * internal loop counter
+        * @var integer
+        */
+       protected $counter = 0;
+       
+       /**
+        * @inheritDoc
+        */
+       public function execute($tagArgs, $blockContent, TemplateEngine $tplObj) {
+               $pageID = null;
+               if (!empty($tagArgs['pageID'])) {
+                       $pageID = intval($tagArgs['pageID']);
+               }
+               else if (!empty($blockContent)) {
+                       $page = Page::getPageByIdentifier($blockContent);
+                       $pageID = ($page) ? $page->pageID : 0;
+               }
+               
+               if ($pageID === null) {
+                       throw new SystemException("Missing 'pageID' attribute or unique identifier.");
+               }
+               
+               $languageID = -1;
+               if (!empty($tagArgs['languageID'])) {
+                       $languageID = intval($tagArgs['languageID']);
+               }
+               else if (!empty($tagArgs['language'])) {
+                       $language = LanguageFactory::getInstance()->getLanguageByCode($tagArgs['language']);
+                       if ($language !== null) $languageID = $language->languageID;
+               }
+               
+               return LinkHandler::getInstance()->getCmsLink($pageID, $languageID);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function init($tagArgs, TemplateEngine $tplObj) {
+               $this->counter = 0;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function next(TemplateEngine $tplObj) {
+               if ($this->counter == 0) {
+                       $this->counter++;
+                       return true;
+               }
+               
+               return false;
+       }
+}
index 8925b8e12438dfc91a5a792c9a3943987c5fa710..0634df45eea7e848c8a418078151731cbff9ce78 100644 (file)
@@ -121,3 +121,56 @@ strong {
                }
        }
 }
+
+.nativeList {
+       margin: 1em 0 1em 40px;
+       
+       ul,
+       ol {
+               margin-bottom: 0;
+               margin-top: 0;
+       }
+       
+       li {
+               margin: 5px 0;
+       }
+}
+
+ul.nativeList {
+       list-style-type: disc;
+}
+ol.nativeList {
+       list-style-type: decimal;
+}
+
+/* simulate native HTML styles for certain elements */
+.htmlContent {
+       p {
+               margin: 1em 0;
+       }
+       
+       h1 {
+               @extend .wcfFontTitle;
+       }
+       
+       h2 {
+               @extend .wcfFontHeadline;
+       }
+       
+       h1, h2, h3, h4, h5, h6 {
+               font-weight: bold;
+               margin: 0.5em 0;
+       }
+       
+       ul, ol {
+               @extend .nativeList;
+       }
+       
+       ul {
+               list-style-type: disc;
+       }
+       
+       ol {
+               list-style-type: decimal;
+       }
+}