From 71c1055ce46958bc95baa85e08fc18340798b2a0 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Tue, 5 Jul 2016 17:21:09 +0200 Subject: [PATCH] Added proper support for embedded content w/ tpl --- com.woltlab.wcf/objectType.xml | 5 + com.woltlab.wcf/templates/cms.tpl | 4 +- .../files/lib/data/page/Page.class.php | 12 +- .../files/lib/data/page/PageAction.class.php | 7 + .../data/page/content/PageContent.class.php | 32 +++ .../install/files/lib/page/CmsPage.class.php | 1 + .../html/simple/HtmlSimpleParser.class.php | 182 ++++++++++++++++++ ...mpleMessageEmbeddedObjectHandler.class.php | 22 +++ ...mpleMessageEmbeddedObjectHandler.class.php | 34 ++++ .../MessageEmbeddedObjectManager.class.php | 62 +++++- ...PageMessageEmbeddedObjectHandler.class.php | 62 ++++++ .../system/template/TemplateEngine.class.php | 9 + ...mbeddedObjectBlockTemplatePlugin.class.php | 49 +++++ ...dedObjectPrefilterTemplatePlugin.class.php | 23 +++ 14 files changed, 498 insertions(+), 6 deletions(-) create mode 100644 wcfsetup/install/files/lib/system/html/simple/HtmlSimpleParser.class.php create mode 100644 wcfsetup/install/files/lib/system/message/embedded/object/AbstractSimpleMessageEmbeddedObjectHandler.class.php create mode 100644 wcfsetup/install/files/lib/system/message/embedded/object/ISimpleMessageEmbeddedObjectHandler.class.php create mode 100644 wcfsetup/install/files/lib/system/message/embedded/object/PageMessageEmbeddedObjectHandler.class.php create mode 100644 wcfsetup/install/files/lib/system/template/plugin/EmbeddedObjectBlockTemplatePlugin.class.php create mode 100644 wcfsetup/install/files/lib/system/template/plugin/SimpleEmbeddedObjectPrefilterTemplatePlugin.class.php diff --git a/com.woltlab.wcf/objectType.xml b/com.woltlab.wcf/objectType.xml index 31b9dd79b5..57f1adfc65 100644 --- a/com.woltlab.wcf/objectType.xml +++ b/com.woltlab.wcf/objectType.xml @@ -752,6 +752,11 @@ com.woltlab.wcf.message.embeddedObject + + com.woltlab.wcf.page + com.woltlab.wcf.message.embeddedObject + + diff --git a/com.woltlab.wcf/templates/cms.tpl b/com.woltlab.wcf/templates/cms.tpl index f52d981580..d46c03a8c5 100644 --- a/com.woltlab.wcf/templates/cms.tpl +++ b/com.woltlab.wcf/templates/cms.tpl @@ -46,9 +46,9 @@ {@$content->getFormattedContent()} {elseif $page->pageType == 'html'} - {@$content->content} + {@$content->getParsedContent()} {elseif $page->pageType == 'tpl'} - {include file=$page->getTplName($contentLanguageID)} + {@$page->getParsedTemplate($content)} {/if} {/if} diff --git a/wcfsetup/install/files/lib/data/page/Page.class.php b/wcfsetup/install/files/lib/data/page/Page.class.php index 6d383d1f44..97e9586fcb 100644 --- a/wcfsetup/install/files/lib/data/page/Page.class.php +++ b/wcfsetup/install/files/lib/data/page/Page.class.php @@ -155,7 +155,7 @@ class Page extends DatabaseObject implements ILinkableObject, ITitledObject { */ public function getLink() { if ($this->controller) { - // todo + // todo: use a unified method for this $controllerParts = explode('\\', $this->controller); $controllerName = $controllerParts[count($controllerParts) - 1]; $controllerName = preg_replace('/(page|action|form)$/i', '', $controllerName); @@ -291,6 +291,16 @@ class Page extends DatabaseObject implements ILinkableObject, ITitledObject { return $this->boxIDs; } + /** + * Returns the parsed template. + * + * @param PageContent $pageContent page content + * @return string parsed template + */ + public function getParsedTemplate(PageContent $pageContent) { + return $pageContent->getParsedTemplate($this->getTplName($pageContent->languageID)); + } + /** * Returns the template name of this page. * diff --git a/wcfsetup/install/files/lib/data/page/PageAction.class.php b/wcfsetup/install/files/lib/data/page/PageAction.class.php index d3f8569db3..689ed24754 100644 --- a/wcfsetup/install/files/lib/data/page/PageAction.class.php +++ b/wcfsetup/install/files/lib/data/page/PageAction.class.php @@ -7,6 +7,7 @@ use wcf\data\ISearchAction; use wcf\data\IToggleAction; use wcf\system\exception\PermissionDeniedException; use wcf\system\exception\UserInputException; +use wcf\system\html\simple\HtmlSimpleParser; use wcf\system\message\embedded\object\MessageEmbeddedObjectManager; use wcf\system\page\handler\ILookupPageHandler; use wcf\system\WCF; @@ -90,6 +91,9 @@ class PageAction extends AbstractDatabaseObjectAction implements ISearchAction, $pageContentEditor->update(['hasEmbeddedObjects' => 1]); } } + else if ($page->pageType == 'html' || $page->pageType == 'tpl') { + HtmlSimpleParser::getInstance()->parse('com.woltlab.wcf.page.content', $pageContent->pageContentID, $pageContent->content); + } } } @@ -171,6 +175,9 @@ class PageAction extends AbstractDatabaseObjectAction implements ISearchAction, $pageContentEditor->update(['hasEmbeddedObjects' => ($pageContent->hasEmbeddedObjects ? 0 : 1)]); } } + else if ($page->pageType == 'html' || $page->pageType == 'tpl') { + HtmlSimpleParser::getInstance()->parse('com.woltlab.wcf.page.content', $pageContent->pageContentID, $pageContent->content); + } } // save template diff --git a/wcfsetup/install/files/lib/data/page/content/PageContent.class.php b/wcfsetup/install/files/lib/data/page/content/PageContent.class.php index 9c30258822..ef773d54da 100644 --- a/wcfsetup/install/files/lib/data/page/content/PageContent.class.php +++ b/wcfsetup/install/files/lib/data/page/content/PageContent.class.php @@ -2,7 +2,9 @@ namespace wcf\data\page\content; use wcf\data\DatabaseObject; use wcf\system\html\output\HtmlOutputProcessor; +use wcf\system\html\simple\HtmlSimpleParser; use wcf\system\message\embedded\object\MessageEmbeddedObjectManager; +use wcf\system\template\plugin\SimpledEmbeddedObjectPrefilterTemplatePlugin; use wcf\system\WCF; /** @@ -50,6 +52,36 @@ class PageContent extends DatabaseObject { return $processor->getHtml(); } + /** + * Parses simple placeholders embedded in raw html. + * + * @return string parsed content + */ + public function getParsedContent() { + MessageEmbeddedObjectManager::getInstance()->loadObjects('com.woltlab.wcf.page.content', [$this->pageContentID]); + + return HtmlSimpleParser::getInstance()->replaceTags('com.woltlab.wcf.page.content', $this->pageContentID, $this->content); + } + + /** + * Parses simple placeholders embedded in HTML with template scripting. + * + * @param string $templateName content template name + * @return string parsed template + */ + public function getParsedTemplate($templateName) { + MessageEmbeddedObjectManager::getInstance()->loadObjects('com.woltlab.wcf.page.content', [$this->pageContentID]); + HtmlSimpleParser::getInstance()->setContext('com.woltlab.wcf.page.content', $this->pageContentID); + + WCF::getTPL()->registerPrefilter(['simpleEmbeddedObject']); + + $returnValue = WCF::getTPL()->fetch($templateName); + + WCF::getTPL()->removePrefilter('simpleEmbeddedObject'); + + return $returnValue; + } + /** * Returns a certain page content. * diff --git a/wcfsetup/install/files/lib/page/CmsPage.class.php b/wcfsetup/install/files/lib/page/CmsPage.class.php index 2645e74a5b..ea533f15f8 100644 --- a/wcfsetup/install/files/lib/page/CmsPage.class.php +++ b/wcfsetup/install/files/lib/page/CmsPage.class.php @@ -3,6 +3,7 @@ namespace wcf\page; use wcf\data\page\content\PageContent; use wcf\data\page\Page; use wcf\system\exception\IllegalLinkException; +use wcf\system\html\simple\HtmlSimpleParser; use wcf\system\language\LanguageFactory; use wcf\system\request\LinkHandler; use wcf\system\request\RequestHandler; diff --git a/wcfsetup/install/files/lib/system/html/simple/HtmlSimpleParser.class.php b/wcfsetup/install/files/lib/system/html/simple/HtmlSimpleParser.class.php new file mode 100644 index 0000000000..d3daa46964 --- /dev/null +++ b/wcfsetup/install/files/lib/system/html/simple/HtmlSimpleParser.class.php @@ -0,0 +1,182 @@ + + * @package WoltLabSuite\Core\System\Message\Embedded\Object + * @since 3.0 + */ +class HtmlSimpleParser extends SingletonFactory { + /** + * embedded object context + * @var array + */ + protected $context = [ + 'objectType' => '', + 'objectID' => 0 + ]; + /** + * @var ISimpleMessageEmbeddedObjectHandler[] + */ + protected $handlers = []; + + /** + * regex for simple placeholders + * @var string + */ + protected $regexHandlers = '~{{\ ((?:[a-z][a-zA-Z]+="(?:\\"|[^"])+?"\ ?)*)\ }}~'; + + /** + * @inheritDoc + */ + protected function init() { + $this->handlers = MessageEmbeddedObjectManager::getInstance()->getSimpleMessageEmbeddedObjectHandlers(); + } + + /** + * Sets the embedded object context. + * + * @param string $objectType object type identifier + * @param integer $objectID object id + */ + public function setContext($objectType, $objectID) { + MessageEmbeddedObjectManager::getInstance()->setActiveMessage($objectType, $objectID); + + $this->context = [ + 'objectType' => $objectType, + 'objectID' => $objectID + ]; + } + + /** + * Parses a message to identify any embedded content using simple placeholders. + * + * @param string $objectType object type identifier + * @param integer $objectID object id + * @param string $message message content + * @return boolean true if there is at least one embedded content found + */ + public function parse($objectType, $objectID, $message) { + preg_match_all($this->regexHandlers, $message, $matches); + + $data = []; + foreach ($matches[1] as $attributesString) { + $attributes = $this->parseAttributes($attributesString); + $handler = $attributes['handler']; + + if (!isset($this->handlers[$handler])) { + // unknown handler, ignore + continue; + } + + if (!isset($data[$handler])) { + $data[$handler] = []; + } + + $data[$handler][] = $attributes['value']; + } + + $embeddedContent = []; + foreach ($data as $handler => $values) { + $values = $this->handlers[$handler]->validateValues($objectType, $objectID, $values); + if (!empty($values)) { + $embeddedContent[$this->handlers[$handler]->objectTypeID] = $values; + } + } + + return MessageEmbeddedObjectManager::getInstance()->registerSimpleObjects($objectType, $objectID, $embeddedContent); + } + + /** + * Replaces simple placeholders with embedded content data. + * + * @param string $objectType object type identifier + * @param integer $objectID object id + * @param string $message message content + * @return string parsed and replaced string + */ + public function replaceTags($objectType, $objectID, $message) { + MessageEmbeddedObjectManager::getInstance()->setActiveMessage($objectType, $objectID); + $this->setContext($objectType, $objectID); + + return preg_replace_callback($this->regexHandlers, function ($matches) use ($objectType, $objectID) { + $data = $this->parseAttributes($matches[1]); + + return $this->replaceTag($data); + }, $message); + } + + /** + * Replaces a placeholder. + * + * @param array $data placeholder data + * @return string placeholder replacement + */ + public function replaceTag(array $data) { + $handler = $data['handler']; + + if (!isset($this->handlers[$handler])) { + // unknown handler, return raw value + return $data['raw']; + } + + $value = $this->handlers[$handler]->replaceSimple($this->context['objectType'], $this->context['objectID'], $data['value'], $data['attributes']); + if ($value === null) { + // invalid value + return $data['raw']; + } + + return $value; + } + + /** + * Parses the template by replacing the simple embedded object syntax + * with a custom template plugin. This step ensures proper replacement + * without causing conflicts with existing syntax. + * + * @param string $template template content + * @return string template content with custom template plugin + */ + public function parseTemplate($template) { + return preg_replace_callback($this->regexHandlers, function ($matches) { + $data = $this->parseAttributes($matches[1]); + $handler = $data['handler']; + + if (!isset($this->handlers[$handler])) { + // unknown handler, return raw value + return $matches[0]; + } + + return '{embeddedObject}' . base64_encode(serialize($data)) . '{/embeddedObject}'; + }, $template); + } + + /** + * Parses the attribute string and return individual components. + * + * @param string $attributesString attributes string, e.g. `foo="1" bar="baz"` + * @return array list of individual components + */ + protected function parseAttributes($attributesString) { + preg_match_all('~([a-z][a-zA-Z]+)="((?:\\\\"|[^"])+?)"~', $attributesString, $attributes); + + $additionalAttributes = []; + for ($i = 1, $length = count($attributes[0]); $i < $length; $i++) { + $additionalAttributes[$attributes[1][$i]] = $attributes[2][$i]; + } + + return [ + 'attributes' => $additionalAttributes, + 'handler' => $attributes[1][0], + 'raw' => '{{ ' . $attributesString . ' }}', + 'value' => $attributes[2][0] + ]; + } +} diff --git a/wcfsetup/install/files/lib/system/message/embedded/object/AbstractSimpleMessageEmbeddedObjectHandler.class.php b/wcfsetup/install/files/lib/system/message/embedded/object/AbstractSimpleMessageEmbeddedObjectHandler.class.php new file mode 100644 index 0000000000..d27c624416 --- /dev/null +++ b/wcfsetup/install/files/lib/system/message/embedded/object/AbstractSimpleMessageEmbeddedObjectHandler.class.php @@ -0,0 +1,22 @@ + + * @package WoltLabSuite\Core\System\Message\Embedded\Object + */ +abstract class AbstractSimpleMessageEmbeddedObjectHandler extends AbstractMessageEmbeddedObjectHandler implements ISimpleMessageEmbeddedObjectHandler { + /** + * @inheritDoc + */ + public function parse(HtmlInputProcessor $htmlInputProcessor, array $embeddedData) { + // this default implementation allows for embedded object handlers that + // only handle the simplified syntax + return []; + } +} diff --git a/wcfsetup/install/files/lib/system/message/embedded/object/ISimpleMessageEmbeddedObjectHandler.class.php b/wcfsetup/install/files/lib/system/message/embedded/object/ISimpleMessageEmbeddedObjectHandler.class.php new file mode 100644 index 0000000000..f9af9858c8 --- /dev/null +++ b/wcfsetup/install/files/lib/system/message/embedded/object/ISimpleMessageEmbeddedObjectHandler.class.php @@ -0,0 +1,34 @@ + + * @package WoltLabSuite\Core\System\Message\Embedded\Object + */ +interface ISimpleMessageEmbeddedObjectHandler extends IMessageEmbeddedObjectHandler { + /** + * Validates the provided values for existence and returns the filtered list. + * + * @param string $objectType object type identifier + * @param integer $objectID object id + * @param integer[] $values list of value ids + * @return integer[] filtered list + */ + public function validateValues($objectType, $objectID, array $values); + + /** + * Returns replacement string for simple placeholders. Must return `null` + * if no replacement should be performed due to invalid or missing arguments. + * + * @param string $objectType object type identifier + * @param integer $objectID object id + * @param integer $value value id + * @param array $attributes list of additional attributes + * @return string|null replacement string or null if value id is unknown + */ + public function replaceSimple($objectType, $objectID, $value, array $attributes); +} diff --git a/wcfsetup/install/files/lib/system/message/embedded/object/MessageEmbeddedObjectManager.class.php b/wcfsetup/install/files/lib/system/message/embedded/object/MessageEmbeddedObjectManager.class.php index 965be1835d..80587ba53e 100644 --- a/wcfsetup/install/files/lib/system/message/embedded/object/MessageEmbeddedObjectManager.class.php +++ b/wcfsetup/install/files/lib/system/message/embedded/object/MessageEmbeddedObjectManager.class.php @@ -91,6 +91,42 @@ class MessageEmbeddedObjectManager extends SingletonFactory { return $returnValue; } + /** + * Registers the embedded objects found in a message using the simplified syntax. + * + * @param string $messageObjectType object type identifier + * @param integer $messageID object id + * @param integer[][] $embeddedContent list of object ids for embedded objects by object type id + * @return boolean true if at least one embedded object was found + */ + public function registerSimpleObjects($messageObjectType, $messageID, array $embeddedContent) { + $messageObjectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.message', $messageObjectType); + + // delete existing assignments + $this->removeObjects($messageObjectType, [$messageID]); + + if (empty($embeddedContent)) { + return false; + } + + // prepare statement + $sql = "INSERT INTO wcf".WCF_N."_message_embedded_object + (messageObjectTypeID, messageID, embeddedObjectTypeID, embeddedObjectID) + VALUES (?, ?, ?, ?)"; + $statement = WCF::getDB()->prepareStatement($sql); + + // call embedded object handlers + WCF::getDB()->beginTransaction(); + foreach ($embeddedContent as $objectTypeID => $objectIDs) { + foreach ($objectIDs as $objectID) { + $statement->execute([$messageObjectTypeID, $messageID, $objectTypeID, $objectID]); + } + } + WCF::getDB()->commitTransaction(); + + return true; + } + /** * Removes embedded object assigments for given messages. * @@ -115,8 +151,13 @@ class MessageEmbeddedObjectManager extends SingletonFactory { * @param integer[] $messageIDs */ public function loadObjects($messageObjectType, array $messageIDs) { + $messageObjectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.message', $messageObjectType); + if ($messageObjectTypeID === null) { + throw new \UnexpectedValueException("Expected a valid object type for definition 'com.woltlab.wcf.message'."); + } + $conditionBuilder = new PreparedStatementConditionBuilder(); - $conditionBuilder->add('messageObjectTypeID = ?', [ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.message', $messageObjectType)]); + $conditionBuilder->add('messageObjectTypeID = ?', [$messageObjectTypeID]); $conditionBuilder->add('messageID IN (?)', [$messageIDs]); // get object ids @@ -231,10 +272,25 @@ class MessageEmbeddedObjectManager extends SingletonFactory { } } + /** + * @return ISimpleMessageEmbeddedObjectHandler[]; + */ + public function getSimpleMessageEmbeddedObjectHandlers() { + $handlers = []; + foreach ($this->getEmbeddedObjectHandlers() as $handler) { + if ($handler instanceof ISimpleMessageEmbeddedObjectHandler) { + $name = lcfirst(preg_replace('~^.*\\\\([A-Z][a-zA-Z]+)MessageEmbeddedObjectHandler$~', '$1', get_class($handler))); + $handlers[$name] = $handler; + } + } + + return $handlers; + } + /** * Returns all embedded object handlers. * - * @return array + * @return IMessageEmbeddedObjectHandler[] */ protected function getEmbeddedObjectHandlers() { if ($this->embeddedObjectHandlers === null) { @@ -251,7 +307,7 @@ class MessageEmbeddedObjectManager extends SingletonFactory { * Returns a specific embedded object handler. * * @param integer $objectTypeID - * @return object + * @return IMessageEmbeddedObjectHandler */ protected function getEmbeddedObjectHandler($objectTypeID) { $this->getEmbeddedObjectHandlers(); diff --git a/wcfsetup/install/files/lib/system/message/embedded/object/PageMessageEmbeddedObjectHandler.class.php b/wcfsetup/install/files/lib/system/message/embedded/object/PageMessageEmbeddedObjectHandler.class.php new file mode 100644 index 0000000000..83b33384a8 --- /dev/null +++ b/wcfsetup/install/files/lib/system/message/embedded/object/PageMessageEmbeddedObjectHandler.class.php @@ -0,0 +1,62 @@ + + * @package WoltLabSuite\Core\System\Message\Embedded\Object + */ +class PageMessageEmbeddedObjectHandler extends AbstractSimpleMessageEmbeddedObjectHandler { + /** + * @inheritDoc + */ + public function loadObjects(array $objectIDs) { + $pages = []; + + foreach ($objectIDs as $objectID) { + $page = PageCache::getInstance()->getPage($objectID); + if ($page !== null) { + $pages[$objectID] = $page; + } + } + + return $pages; + } + + /** + * @inheritDoc + */ + public function validateValues($objectType, $objectID, array $values) { + return array_filter($values, function($value) { + return (PageCache::getInstance()->getPage($value) !== null); + }); + } + + /** + * @inheritDoc + */ + public function replaceSimple($objectType, $objectID, $value, array $attributes) { + /** @var Page $page */ + $page = MessageEmbeddedObjectManager::getInstance()->getObject('com.woltlab.wcf.page', $value); + if ($page === null) { + return null; + } + + $return = (!empty($attributes['return'])) ? $attributes['return'] : 'link'; + switch ($return) { + case 'title': + return $page->getTitle(); + break; + + case 'link': + default: + return $page->getLink(); + break; + } + } +} diff --git a/wcfsetup/install/files/lib/system/template/TemplateEngine.class.php b/wcfsetup/install/files/lib/system/template/TemplateEngine.class.php index bfd3201f98..e92e7f614e 100755 --- a/wcfsetup/install/files/lib/system/template/TemplateEngine.class.php +++ b/wcfsetup/install/files/lib/system/template/TemplateEngine.class.php @@ -649,6 +649,15 @@ class TemplateEngine extends SingletonFactory { } } + /** + * Removes a prefilter by its internal name. + * + * @param string $name internal prefilter identifier + */ + public function removePrefilter($name) { + unset($this->prefilters[$name]); + } + /** * Sets the dir for the compiled templates. * diff --git a/wcfsetup/install/files/lib/system/template/plugin/EmbeddedObjectBlockTemplatePlugin.class.php b/wcfsetup/install/files/lib/system/template/plugin/EmbeddedObjectBlockTemplatePlugin.class.php new file mode 100644 index 0000000000..9a1178e564 --- /dev/null +++ b/wcfsetup/install/files/lib/system/template/plugin/EmbeddedObjectBlockTemplatePlugin.class.php @@ -0,0 +1,49 @@ + + * @package WoltLabSuite\Core\System\Template\Plugin + * @since 3.0 + */ +class EmbeddedObjectBlockTemplatePlugin implements IBlockTemplatePlugin { + /** + * internal loop counter + * @var integer + */ + protected $counter = 0; + + /** + * @inheritDoc + */ + public function execute($tagArgs, $blockContent, TemplateEngine $tplObj) { + $data = unserialize(base64_decode($blockContent)); + + return HtmlSimpleParser::getInstance()->replaceTag($data); + } + + /** + * @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; + } +} diff --git a/wcfsetup/install/files/lib/system/template/plugin/SimpleEmbeddedObjectPrefilterTemplatePlugin.class.php b/wcfsetup/install/files/lib/system/template/plugin/SimpleEmbeddedObjectPrefilterTemplatePlugin.class.php new file mode 100644 index 0000000000..5e6108be27 --- /dev/null +++ b/wcfsetup/install/files/lib/system/template/plugin/SimpleEmbeddedObjectPrefilterTemplatePlugin.class.php @@ -0,0 +1,23 @@ + + * @package WoltLabSuite\Core\System\Template\Plugin + * @since 3.0 + */ +class SimpleEmbeddedObjectPrefilterTemplatePlugin implements IPrefilterTemplatePlugin { + /** + * @inheritDoc + */ + public function execute($templateName, $sourceContent, TemplateScriptingCompiler $compiler) { + return HtmlSimpleParser::getInstance()->parseTemplate($sourceContent); + } +} -- 2.20.1