openrat-cms

# OpenRat Content Management System
git clone http://git.code.weiherhei.de/openrat-cms.git
Log | Files | Refs

commit 4338bc7124e13db09a36212553359416efde3e8b
parent f39b4850f26efe0989b1d3ad14acbb9924de6718
Author: Jan Dankert <develop@jandankert.de>
Date:   Thu, 20 Feb 2020 23:51:34 +0100

Refactoring of the template compiler: The Templates are at first converted into a component tree and then into a element tree which is then rendered.

Diffstat:
modules/template-engine/Element.class.php | 57+++++++++++++++++++++++++++++----------------------------
modules/template-engine/EmptyElement.class.php | 23+++++++++++++++++++++++
modules/template-engine/HtmlElement.class.php | 18++++++++++++------
modules/template-engine/PHPBlockElement.class.php | 14++++++++------
modules/template-engine/TemplateCompiler.php | 3+++
modules/template-engine/components/html/Component.class.php | 52+++++++++++++++++++++++++++++++---------------------
modules/template-engine/components/html/NativeHtmlComponent.class.php | 21+++++++++++++++++++++
modules/template-engine/components/html/form/Form.class.php | 2+-
modules/template-engine/components/html/input/Input.class.php | 7++++++-
modules/template-engine/components/html/table/Table.class.php | 10++++++----
modules/template-engine/engine/TemplateEngine.class.php | 129++++++++++++++++++++++++++++++++++++-------------------------------------------
modules/template-engine/require.php | 1+
12 files changed, 200 insertions(+), 137 deletions(-)

diff --git a/modules/template-engine/Element.class.php b/modules/template-engine/Element.class.php @@ -14,28 +14,30 @@ class Element */ protected $attributes = []; protected $content = ''; - protected $selfClosing = true; + + protected $selfClosing = false; /** * @var array */ - private $parents = []; - - private $children = []; + protected $children = []; /** * @param $wrapperElement Element * @return $this + * @deprecated */ public function addWrapper($wrapperElement ) { - $wrapperElement->selfClosing(false); - $this->parents[] = $wrapperElement; return $this; } public function addChild($child ) { - $this->selfClosing(false ); - $this->children[] = $child; + + if ( is_array( $child ) ) + $this->children += $child; + else + $this->children[] = $child; + return $this; } @@ -54,39 +56,26 @@ class Element return $this; } - public function getBegin() { + public function render() { - $content = array_reduce( array_reverse($this->parents), function($carry,$item) { - //$content = ''; - //foreach( $item->children as $child) { - // $content .= $child->getBegin().$child->getEnd(); - //} - return $carry.$item->getBegin(); - }, '' ); + $this->selfClosing = $this->selfClosing && !$this->content && !$this->children; + + $content = ''; if ( $this->name ) $content .= '<'.$this->name. - array_reduce( array_keys($this->attributes),function($carry,$key){return $carry.' '.$this->getAttributeValue($key);},'').(($this->selfClosing && !$this->content && !$this->children)?' /':'').'>'; + array_reduce( array_keys($this->attributes),function($carry,$key){return $carry.' '.$this->getAttributeValue($key);},'').(($this->selfClosing ?' /':'').'>'); $content .= $this->getContent(); - return $content; - } - - - public function getEnd() { + $content .= $this->renderChildren(); - $content = ''; - if ( $this->selfClosing && !$this->content && !$this->children) + if ( $this->selfClosing ) ; else if ( $this->name ) $content .= '</'.$this->name.'>'; - $content .= array_reduce( $this->parents, function($carry,$item) { - return $carry.$item->getEnd(); - }, '' ); - return $content; } @@ -104,6 +93,18 @@ class Element { return $this->content; } + + protected function renderChildren() + { + $content = ''; + + /** @var Element $child */ + foreach($this->children as $child ) { + $content .= $child->render(); + } + + return $content; + } } diff --git a/modules/template-engine/EmptyElement.class.php b/modules/template-engine/EmptyElement.class.php @@ -0,0 +1,23 @@ +<?php + + +namespace modules\template_engine; + + +use cms\template_engine\SimpleAttribute; + + +/** + * An empty element. + * + * 'empty' means, the element has no tag and no text content. But it may (and should) contain child elements. + * + * @package modules\template_engine + */ +class EmptyElement extends Element +{ + public function __construct() + { + parent::__construct(null); + } +} diff --git a/modules/template-engine/HtmlElement.class.php b/modules/template-engine/HtmlElement.class.php @@ -33,16 +33,15 @@ class HtmlElement extends Element return parent::getAttributeValue(htmlspecialchars($name)); } - public function getBegin() - { - $this->addAttribute('class', implode(' ',$this->styleClasses) ); - return parent::getBegin(); - } - public function __construct( $name ) { parent::__construct( $name ); + + // Only "void elements" are self-closable, see + // https://html.spec.whatwg.org/multipage/syntax.html#void-elements + if ( in_array($name,['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr'])) + $this->selfClosing = true; } @@ -54,4 +53,11 @@ class HtmlElement extends Element } + public function render() + { + $this->addAttribute('class', implode(' ',$this->styleClasses) ); + + return parent::render(); + } + } \ No newline at end of file diff --git a/modules/template-engine/PHPBlockElement.class.php b/modules/template-engine/PHPBlockElement.class.php @@ -15,15 +15,17 @@ class PHPBlockElement extends HtmlElement } - public function getBegin() + public function render() { - return '<?php '.$this->beforeBlock.' { '.$this->inBlock.' ?>'; - } + $content = ''; + $content .= '<?php '.$this->beforeBlock.' { '.$this->inBlock.' ?>'; - public function getEnd() - { - return ' <?php } ?>'; + $content .= $this->renderChildren(); + + $content .= ' <?php } ?>'; + + return $content; } diff --git a/modules/template-engine/TemplateCompiler.php b/modules/template-engine/TemplateCompiler.php @@ -40,6 +40,9 @@ foreach(FileUtils::readDir( $dir ) as $action ) $outFile = $dir.'/'.$action.'/'.$method.'.php'; $engine = new TemplateEngine(); + + // We are creating a fake request, because the template compiler needs to know + // the action and subaction in which it will be executed. $fakeRequest = new \cms\action\RequestParams(); $fakeRequest->action = $action; $fakeRequest->method = $method; diff --git a/modules/template-engine/components/html/Component.class.php b/modules/template-engine/components/html/Component.class.php @@ -7,9 +7,17 @@ use modules\template_engine\Element; abstract class Component { - + private $childComponents = []; private $depth; + /** + * @param $component Component + */ + public function addChildComponent($component ) { + if ( $component ) + $this->childComponents[] = $component; + } + /** * @var RequestParams */ @@ -20,6 +28,11 @@ abstract class Component */ private $element; + /** + * @var Element + */ + protected $adoptiveElement; + public function __construct() { } @@ -38,33 +51,31 @@ abstract class Component return null; } - - /** - * Gets the beginning of this component. - * @return string - */ - public function getBegin() - { - return $this->element->getBegin(); - } - - public function getEnd() - { - return $this->element->getEnd(); - } - public function init() { $this->element = $this->createElement(); if ( $this->element) $this->element->selfClosing(false); + + // if there is no special adoptive element, lets use the root element. + if ( ! $this->adoptiveElement ) + $this->adoptiveElement = $this->element; } - -} - + /** + * Gets the element with all child elements from all child components. + * + * @return Element + */ + public function getElement() + { + /** @var Component $childComponent */ + foreach ($this->childComponents as $childComponent ) + $this->adoptiveElement->addChild( $childComponent->getElement() ); + return $this->element; + } -?>- \ No newline at end of file +} diff --git a/modules/template-engine/components/html/NativeHtmlComponent.class.php b/modules/template-engine/components/html/NativeHtmlComponent.class.php @@ -0,0 +1,21 @@ +<?php + +namespace template_engine\components; + +use modules\template_engine\HtmlElement; + +class NativeHtmlComponent extends HtmlComponent +{ + public $attributes; + public $tag; + + public function createElement() + { + $html = new HtmlElement( $this->tag ); + + foreach ($this->attributes as $attribute) + $html->addAttribute( $attribute->name,$attribute->value ); + + return $html; + } +} diff --git a/modules/template-engine/components/html/form/Form.class.php b/modules/template-engine/components/html/form/Form.class.php @@ -99,7 +99,7 @@ class FormComponent extends Component (new CMSElement('input')) ->addAttribute('type', 'hidden') ->addAttribute('name', REQ_PARAM_TOKEN) - ->addAttribute('value', '<?php token();') // TODO escaping + ->addAttribute('value', '<?php token();?>') // TODO escaping ); $form->addChild( diff --git a/modules/template-engine/components/html/input/Input.class.php b/modules/template-engine/components/html/input/Input.class.php @@ -22,6 +22,7 @@ class InputComponent extends FieldComponent public $size; + public $minlength = 0; public $maxlength = 256; public $onchange; @@ -50,7 +51,8 @@ class InputComponent extends FieldComponent } $input->addAttribute('name',$this->name); - $input->addAttribute('disabled',$this->readonly); + if ( $this->readonly ) + $input->addAttribute('disabled','disabled'); if ( $this->required ) $input->addAttribute( 'required','required'); @@ -65,6 +67,9 @@ class InputComponent extends FieldComponent $input->addAttribute('type',$this->type); $input->addAttribute('maxlength',$this->maxlength); + if ( $this->minlength ) + $input->addAttribute('minlength',$this->minlength); + if ( $this->class ) $input->addStyleClass($this->class); diff --git a/modules/template-engine/components/html/table/Table.class.php b/modules/template-engine/components/html/table/Table.class.php @@ -25,18 +25,20 @@ class TableComponent extends HtmlComponent $tableWrapper->addChild($filter); } - $tableContent = (new HtmlElement('div'))->addStyleClass('or-table-area'); + $tableContent = (new HtmlElement('div'))->addStyleClass('or-table-area'); $table = new CMSElement('table'); - if ( $this->class) + $tableWrapper->addChild( $tableContent->addChild( $table) ); + + if ( $this->class) $table->addStyleClass($this->class); if ( $this->width) $table->addAttribute('width',$this->width); - $table->addWrapper($tableContent)->addWrapper($tableWrapper); + $this->adoptiveElement = $table; - return $table; + return $tableWrapper; } } \ No newline at end of file diff --git a/modules/template-engine/engine/TemplateEngine.class.php b/modules/template-engine/engine/TemplateEngine.class.php @@ -7,19 +7,24 @@ use DOMDocument; use DOMElement; use Exception; use LogicException; +use modules\template_engine\EmptyElement; +use modules\template_engine\HtmlElement; +use modules\template_engine\PHPBlockElement; use SimpleXMLElement; use \template_engine\components\Component; +use template_engine\components\NativeHtmlComponent; /** * Wandelt eine Vorlage in ein PHP-Skript um. - * + * * Die Vorlage wird gesparst, Elemente werden geladen und in die Zieldatei kopiert. - * + * * @author Jan Dankert * @package openrat.services */ class TemplateEngine { + // For now we are only supporting HTML rendering. public $renderType = 'html'; public $config = array(); @@ -52,42 +57,46 @@ class TemplateEngine // Imports the base class of all component types. require_once (dirname(__FILE__).'/../components/'.$this->renderType.'/Component.class.' . PHP_EXT); require_once (dirname(__FILE__).'/../components/'.$this->renderType.'/HtmlComponent.class.' . PHP_EXT); + require_once (dirname(__FILE__).'/../components/'.$this->renderType.'/NativeHtmlComponent.class.' . PHP_EXT); require_once (dirname(__FILE__).'/../components/'.$this->renderType.'/FieldComponent.class.' . PHP_EXT); + // We are now building a complete DOM tree and this is the root element. + $rootElement = new PHPBlockElement(); + + // The generated template should only be executable in our CMS environment (for security reasons). + $rootElement->beforeBlock = 'if (!defined(\'OR_TITLE\')) die(\'Forbidden\');'; + try { $confCompiler = $this->config; - + if (is_file($srcXmlFilename)) $srcFilename = $srcXmlFilename; else // Wenn Vorlage (noch) nicht existiert throw new LogicException("Template not found: $srcXmlFilename"); - + + // Vorlage und Zieldatei oeffnen + $document = $this->loadDocument($srcFilename); + + // creating a tree of components + $rootComponent = $this->processElement( $document->documentElement ); + + // converting the component tree to a element tree + $rootElement->addChild( $rootComponent->getElement() ); + $filename = $tplOutName; - - // Wenn Vorlage gaendert wurde, dann Umwandlung erneut ausf�hren. - if (false && is_file($filename) && filemtime($srcFilename) <= filemtime($filename)) - return; - + if (is_file($filename) && ! is_writable($filename)) throw new LogicException("Template output file is read-only: $filename"); - - // Vorlage und Zieldatei oeffnen - $document = $this->loadDocument($srcFilename); - - $outFile = @fopen($filename, 'w'); - fwrite($outFile, '<?php if (!defined(\'OR_TITLE\')) die(\'Forbidden\'); ?>'); - if (! is_resource($outFile)) - throw new LogicException("Template '$srcXmlFilename': Unable to open file for writing: '$filename'"); - - $this->processElement( $document->documentElement, $outFile ); + $writtenBytes = file_put_contents( $filename, $rootElement->render() ); + + if ( $writtenBytes === FALSE ) + throw new LogicException("Unable writing to output file: '$filename'"); - fclose($outFile); - // CHMOD ausfuehren. - if (! empty($confCompiler['chmod'])) + if ( @$confCompiler['chmod'] ) if (! @chmod($filename, octdec($confCompiler['chmod']))) throw new \InvalidArgumentException("Template {$srcXmlFilename} failed to compile: CHMOD '{$confCompiler['chmod']}' failed on file {$filename}."); } @@ -101,29 +110,28 @@ class TemplateEngine /** * @param DOMElement $element - * @param resource $outFile * @param int $depth */ - private function processElement( $element, $outFile, $depth = 0 ) { + private function processElement( $element, $depth = 0 ) { // Only process DOM Elements (ignoring Text, Comments, ...) if ( $element->nodeType == XML_ELEMENT_NODE ) ; else - return; + return null; // The namespace decides what to do with this element: if ( $element->namespaceURI == 'http://www.openrat.de/template') - $this->processCMSElement( $element, $outFile, $depth ); + return $this->processCMSElement( $element, $depth ); elseif ( $element->namespaceURI == 'http://www.w3.org/1999/xhtml') - $this->processHTMLElement( $element, $outFile, $depth ); + return $this->processHTMLElement( $element, $depth ); else throw new LogicException("Unknown Element ".$element->tagName.' in NS '.$element->namespaceURI ); } - private function processCMSElement(DOMElement $element, $outFile, $depth) + private function processCMSElement(DOMElement $element, $depth) { $attributes = $element->attributes; $tag = $element->localName; @@ -166,52 +174,30 @@ class TemplateEngine $component->$attributeName = $attributeValue; } - $output = $component->init(); - $output = $component->getBegin(); - if($this->debug) $output = '<!-- Begin '.$tag.' -->'.$output; - - if ($output) { - $prepend = ($depth>=0?"\n":'').str_repeat("\t",$depth); - fwrite($outFile, $prepend.$output); - } + $component->init(); foreach( $element->childNodes as $child ) { - $this->processElement($child,$outFile,$depth+1); - } - - $output = $component->getEnd(); - if($this->debug) $output = $output.'<!-- End '.$tag.' -->'; - if ( $output ) { - $prepend = "\n".str_repeat("\t",$depth); - fwrite($outFile, $prepend.$output); + $component->addChildComponent( $this->processElement( $child,$depth ) ); } - + return $component; } - - private function processHTMLElement($element, $outFile, $depth) + /** + * Creates a new HTML element. + * @param $element DOMElement + * @param $depth + * @return NativeHtmlComponent + */ + private function processHTMLElement($element, $depth) { - $attributes = $element->attributes; - $tag = $element->localName; - - $out = '<'.$tag; - foreach ($attributes as $attribute) - $out .= ' '.$attribute->name.'="'.$attribute->value.'"'; + $component = new NativeHtmlComponent(); - $out .= '>'; - - $prepend = ($depth>=0?"\n":'').str_repeat("\t",$depth); - fwrite($outFile, $prepend.$out); - - foreach( $element->childNodes as $child ) { - $this->processElement($child,$outFile,$depth+1); - } - - $out = '</'.$tag.'>'; - $prepend = "\n".str_repeat("\t",$depth); - fwrite($outFile, $prepend.$out); + $component->tag = $element->localName; + $component->attributes = $element->attributes; + $component->init(); + return $component; } /** @@ -264,20 +250,23 @@ class TemplateEngine } catch (Exception $e) { throw new DomainException("Compilation failed for Template '$srcFile'.", 0, $e); } - #header("X-CMS-Template-File: " . $templateFile); + } - // Spätestens jetzt muss das Template vorhanden sein. + // Spätestens jetzt muss das Template vorhanden sein. if (!is_file($templateFile)) throw new LogicException("Template file '$templateFile' was not found."); - // Übertragen der Ausgabe-Variablen in den aktuellen Kontext + if ( DEVELOPMENT ) + // save a few bytes in production mode ;) + header("X-CMS-Template-File: " . $templateFile); + + // Übertragen der Ausgabe-Variablen in den aktuellen Kontext // extract($outputData); - // Einbinden des Templates + // Include the template require_once($templateFile); - } } diff --git a/modules/template-engine/require.php b/modules/template-engine/require.php @@ -5,6 +5,7 @@ include( dirname(__FILE__) . '/engine/TemplateEngine.class.php'); include( dirname(__FILE__) . '/Element.class.php'); include( dirname(__FILE__) . '/HtmlElement.class.php'); include( dirname(__FILE__) . '/CMSElement.class.php'); +include( dirname(__FILE__) . '/EmptyElement.class.php'); include( dirname(__FILE__) . '/PHPBlockElement.class.php'); include( dirname(__FILE__) . '/SimpleAttribute.php'); include( dirname(__FILE__) . '/ConditionalAttribute.php');