mikuli.cz

:)
git clone https://git.sr.ht/~ashymad/mikuli.cz
Log | Files | Refs

commit 0479a5394f13fe8b54362ccb794618fba5cf806a
parent c0583002aa21b44b506be2b7b85fe20a61506d67
Author: markseu <mark2011@mayberg.se>
Date:   Thu,  5 Mar 2020 22:40:33 +0100

Replaced safe mode with HTML whitelist

Diffstat:
Msystem/extensions/core.php | 233+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msystem/extensions/edit.js | 13++++++++++---
Msystem/extensions/edit.php | 65+++++++++++++++++++++++++++++++++++++----------------------------
Msystem/extensions/image.php | 63++-------------------------------------------------------------
Msystem/extensions/install-languages.zip | 0
Msystem/extensions/markdown.php | 16+++++++---------
Msystem/extensions/update.php | 7+++----
Msystem/settings/system.ini | 1-
Myellow.php | 3+--
9 files changed, 230 insertions(+), 171 deletions(-)

diff --git a/system/extensions/core.php b/system/extensions/core.php @@ -43,7 +43,6 @@ class YellowCore { $this->system->setDefault("coreTrashDir", "system/trash/"); $this->system->setDefault("coreServerUrl", "auto"); $this->system->setDefault("coreServerTimezone", "UTC"); - $this->system->setDefault("coreSafeMode", "0"); $this->system->setDefault("coreMultiLanguageMode", "0"); $this->system->setDefault("coreMediaLocation", "/media/"); $this->system->setDefault("coreDownloadLocation", "/media/downloads/"); @@ -85,7 +84,7 @@ class YellowCore { extension_loaded("mbstring") || die("Datenstrom Yellow requires PHP mbstring extension! $troubleshooting\n"); extension_loaded("zip") || die("Datenstrom Yellow requires PHP zip extension! $troubleshooting\n"); } - + // Handle initialisation public function load() { if (defined("DEBUG") && DEBUG>=3) { @@ -187,8 +186,8 @@ class YellowCore { $rawData = $this->toolbox->readFile($fileNameError); } else { $language = $this->lookup->findLanguageFromFile($fileName, $this->system->get("language")); - $rawData = "---\nTitle:".$this->text->getText("coreError${statusCode}Title", $language)."\n"; - $rawData .= "Layout:error\n---\n".$this->text->getText("coreError${statusCode}Text", $language); + $rawData = "---\nTitle: ".$this->text->getText("coreError${statusCode}Title", $language)."\n"; + $rawData .= "Layout: error\n---\n".$this->text->getText("coreError${statusCode}Text", $language); } $cacheable = false; } else { @@ -292,7 +291,7 @@ class YellowCore { $handler = $this->getCommandHandler(); echo "YellowCore::command status:$statusCode handler:$handler time:$time ms<br/>\n"; } - return $statusCode; + return $statusCode<400 ? 0 : 1; } // Handle startup @@ -401,7 +400,6 @@ class YellowPage { public $outputData; //response output public $parser; //content parser public $parserData; //content data of page - public $safeMode; //page is parsed in safe mode? (boolean) public $available; //page is available? (boolean) public $visible; //page is visible location? (boolean) public $active; //page is active location? (boolean) @@ -431,7 +429,6 @@ class YellowPage { $this->rawData = $rawData; $this->parser = null; $this->parserData = ""; - $this->safeMode = intval($this->yellow->system->get("coreSafeMode")); $this->available = $this->yellow->lookup->isAvailableLocation($this->location, $this->fileName); $this->visible = true; $this->active = $this->yellow->lookup->isActiveLocation($this->location, $this->yellow->page->location); @@ -519,6 +516,7 @@ class YellowPage { $this->parserData = preg_replace("/@pageRead/i", $this->get("pageRead"), $this->parserData); $this->parserData = preg_replace("/@pageEdit/i", $this->get("pageEdit"), $this->parserData); $this->parserData = $this->parser->onParseContentRaw($this, $this->parserData); + $this->parserData = $this->yellow->toolbox->normaliseData($this->parserData, "html"); foreach ($this->yellow->extensions->extensions as $key=>$value) { if (method_exists($value["obj"], "onParseContentText")) { $output = $value["obj"]->onParseContentText($this, $this->parserData); @@ -564,7 +562,6 @@ class YellowPage { fclose($fileHandle); } $output = strreplaceu("\n", "<br />\n", htmlspecialchars($dataBuffer)); - if ($this->safeMode || empty($output)) $output = "No log file available."; } } } @@ -615,11 +612,9 @@ class YellowPage { // Parse page layout public function parsePageLayout($name) { $this->outputData = null; - if (!$this->isError()) { - foreach ($this->yellow->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onParsePageLayout")) { - $value["obj"]->onParsePageLayout($this, $name); - } + foreach ($this->yellow->extensions->extensions as $key=>$value) { + if (method_exists($value["obj"], "onParsePageLayout")) { + $value["obj"]->onParsePageLayout($this, $name); } } if (is_null($this->outputData)) { @@ -1257,7 +1252,7 @@ class YellowContent { if ($match[1]==2) { $page = new YellowPage($this->yellow); $page->setRequestInformation($scheme, $address, $base, $one->location."#".$match[2], $one->fileName); - $page->parseData("---\nTitle:$match[3]\n---\n", false, 0); + $page->parseData("---\nTitle: $match[3]\n---\n", false, 0); $pages->append($page); } } @@ -2078,12 +2073,12 @@ class YellowLookup { if (!preg_match("/^\w+:/", trim(html_entity_decode($location, ENT_QUOTES, "UTF-8")))) { $pageBase = $this->yellow->page->base; $mediaBase = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreMediaLocation"); - if (preg_match("/^\#/", $location)) { - $location = $pageBase.$pageLocation.$location; - } elseif (!preg_match("/^\//", $location)) { - $location = $this->getDirectoryLocation($pageBase.$pageLocation).$location; - } elseif (!preg_match("#^($pageBase|$mediaBase)#", $location)) { - $location = $pageBase.$location; + if (!preg_match("/^\#/", $location)) { + if (!preg_match("/^\//", $location)) { + $location = $this->getDirectoryLocation($pageBase.$pageLocation).$location; + } elseif (!preg_match("#^($pageBase|$mediaBase)#", $location)) { + $location = $pageBase.$location; + } } $location = strreplaceu("/./", "/", $location); $location = strreplaceu(":", $this->yellow->toolbox->getLocationArgsSeparator(), $location); @@ -2386,6 +2381,70 @@ class YellowToolbox { return strreplaceu(array("%2F","%3A","%3D"), array("/",":","="), rawurlencode($text)); } + // Normalise elements and attributes in html/svg data + public function normaliseData($text, $type = "html", $filterStrict = true) { + $elementsHtml = array( + "a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "iframe", "image", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meta", "meter", "nav", "nobr", "ol", "optgroup", "option", "output", "p", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "shadow", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"); + $elementsSvg = array( + "svg", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "feblend", "fecolormatrix", "fecomponenttransfer", "fecomposite", "feconvolvematrix", "fediffuselighting", "fedisplacementmap", "fedistantlight", "feflood", "fefunca", "fefuncb", "fefuncg", "fefuncr", "fegaussianblur", "femerge", "femergenode", "femorphology", "feoffset", "fepointlight", "fespecularlighting", "fespotlight", "fetile", "feturbulence", "filter", "font", "g", "glyph", "glyphref", "hkern", "image", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "use", "view", "vkern"); + $attributesHtml = array( + "accept", "action", "align", "allowfullscreen", "alt", "autocomplete", "background", "bgcolor", "border", "cellpadding", "cellspacing", "charset", "checked", "cite", "class", "clear", "color", "cols", "colspan", "content", "coords", "crossorigin", "datetime", "default", "dir", "disabled", "download", "enctype", "face", "for", "frameborder", "headers", "height", "hidden", "high", "href", "hreflang", "id", "integrity", "ismap", "label", "lang", "list", "loop", "low", "max", "maxlength", "media", "method", "min", "multiple", "name", "noshade", "novalidate", "nowrap", "open", "optimum", "pattern", "placeholder", "poster", "prefix", "preload", "property", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "spellcheck", "scope", "selected", "shape", "size", "sizes", "span", "srclang", "start", "src", "srcset", "step", "style", "summary", "tabindex", "title", "type", "usemap", "valign", "value", "width", "xmlns"); + $attributesSvg = array( + "accent-height", "accumulate", "additivive", "alignment-baseline", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "datenstrom", "dx", "dy", "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "fill", "fill-opacity", "fill-rule", "filter", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href", "id", "image-rendering", "in", "in2", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "media", "method", "mode", "min", "name", "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "points", "preservealpha", "preserveaspectratio", "r", "rx", "ry", "radius", "refx", "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering", "specularconstant", "specularexponent", "spreadmethod", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style", "surfacescale", "tabindex", "targetx", "targety", "transform", "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode", "values", "viewbox", "visibility", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width", "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2", "xlink:href", "xml:id", "xml:space", "xmlns", "y", "y1", "y2", "z", "zoomandpan"); + $elementsSafe = $elementsHtml; + $attributesSafe = $attributesHtml; + if ($type=="svg") { + $elementsSafe = array_merge($elementsHtml, $elementsSvg); + $attributesSafe = array_merge($attributesHtml, $attributesSvg); + } + while (true) { + $elementFound = preg_match("/<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); + $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); + $elementStart = $matches[1][0]; + $elementName = $matches[2][0]; + $elementMiddle = $matches[3][0]; + $elementEnd = $matches[4][0]; + $output .= $elementBefore; + if ($elementName[0]=="!") { + $output .= "<$elementName$elementMiddle>"; + } elseif (in_array(strtolower($elementName), $elementsSafe)) { + $elementAttributes = $this->getTextAttributes($elementMiddle); + foreach ($elementAttributes as $key=>$value) { + if (!in_array(strtolower($key), $attributesSafe) && !preg_match("/^(aria|data)-/i", $key)) { + unset($elementAttributes[$key]); + } + } + if ($filterStrict) { + $href = isset($elementAttributes["href"]) ? $elementAttributes["href"] : ""; + if (preg_match("/^\w+:/", $href) && !preg_match("/^(http|https|ftp|mailto):/", $href)) { + $elementAttributes["href"] = "error-xss-filter"; + } + $href = isset($elementAttributes["xlink:href"]) ? $elementAttributes["xlink:href"] : ""; + if (preg_match("/^\w+:/", $href) && !preg_match("/^(http|https|ftp|mailto):/", $href)) { + $elementAttributes["xlink:href"] = "error-xss-filter"; + } + } + $output .= "<$elementStart$elementName"; + foreach ($elementAttributes as $key=>$value) $output .= " $key=\"$value\""; + if (!empty($elementEnd)) $output .= " "; + $output .= "$elementEnd>"; + } + if (!$elementFound) break; + $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); + } + return $output; + } + + // Normalise text lines, convert line endings + public function normaliseLines($text, $endOfLine = "lf") { + if ($endOfLine=="lf") { + $text = preg_replace("/\R/u", "\n", $text); + } else { + $text = preg_replace("/\R/u", "\r\n", $text); + } + return $text; + } + // Normalise text into UTF-8 NFC public function normaliseUnicode($text) { if (PHP_OS=="Darwin" && !mb_check_encoding($text, "ASCII")) { @@ -2656,6 +2715,49 @@ class YellowToolbox { return $lines; } + // Return attributes from text string + public function getTextAttributes($text) { + $tokens = array(); + $posStart = $posQuote = 0; + for ($pos=0; $pos<strlenb($text); ++$pos) { + if ($text[$pos]==" " && !$posQuote) { + if ($pos>$posStart) array_push($tokens, substrb($text, $posStart, $pos-$posStart)); + $posStart = $pos+1; + } + if ($text[$pos]=="=" && !$posQuote) { + if ($pos>$posStart) array_push($tokens, substrb($text, $posStart, $pos-$posStart)); + array_push($tokens, "="); + $posStart = $pos+1; + } + if ($text[$pos]=="\"") { + if ($posQuote) { + if ($pos>$posQuote) array_push($tokens, substrb($text, $posQuote+1, $pos-$posQuote-1)); + $posQuote = 0; + $posStart = $pos+1; + } else { + if ($pos==$posStart) $posQuote = $pos; + } + } + } + if ($pos>$posStart && !$posQuote) { + array_push($tokens, substrb($text, $posStart, $pos-$posStart)); + } + $attributes = array(); + for ($i=0; $i<count($tokens); ++$i) { + if ($tokens[$i+1]=="=") { + $key = $tokens[$i]; + $value = $tokens[$i+2]; + $i += 2; + } else { + $key = $value = $tokens[$i]; + } + if (!strempty($key) && !strempty($value)) { + $attributes[$key] = $value; + } + } + return $attributes; + } + // Return arguments from text string, space separated public function getTextArgs($text, $optional = "-") { $text = preg_replace("/\s+/s", " ", trim($text)); @@ -2673,78 +2775,83 @@ class YellowToolbox { return str_word_count($text); } + // Return text truncated at word boundary + public function getTextTruncated($text, $lengthMax) { + if (strlenu($text)>$lengthMax-1) { + $text = substru($text, 0, $lengthMax); + $pos = strrposu($text, " "); + $text = substru($text, 0, $pos ? $pos : $lengthMax-1)."…"; + } + return $text; + } + // Create description from text string public function createTextDescription($text, $lengthMax = 0, $removeHtml = true, $endMarker = "", $endMarkerText = "") { - if (preg_match("/^<h1>.*?<\/h1>(.*)$/si", $text, $matches)) $text = $matches[1]; + $elementsBlock = array("blockquote", "br", "div", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "li", "ol", "p", "pre", "ul"); + $elementsVoid = array("area", "br", "col", "embed", "hr", "img", "input", "param", "source", "wbr"); if ($lengthMax==0) $lengthMax = strlenu($text); if ($removeHtml) { while (true) { - $elementFound = preg_match("/<\s*?([\/!]?\w*)(.*?)\s*?\>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); - $element = $matches[0][0]; - $elementName = $matches[1][0]; - $elementText = $matches[2][0]; - $elementOffsetBytes = $elementFound ? $matches[0][1] : strlenb($text); - $string = html_entity_decode(substrb($text, $offsetBytes, $elementOffsetBytes - $offsetBytes), ENT_QUOTES, "UTF-8"); - if (preg_match("/^(blockquote|br|div|h\d|hr|li|ol|p|pre|ul)/i", $elementName)) $string .= " "; - if (preg_match("/^\/(code|pre)/i", $elementName)) $string = preg_replace("/^(\d+\n){2,}$/", "", $string); - $string = preg_replace("/\s+/s", " ", $string); - if (substru($string, 0, 1)==" " && (empty($output) || substru($output, -1)==" ")) $string = substru($string, 1); - $length = strlenu($string); - $output .= substru($string, 0, $length<$lengthMax ? $length : $lengthMax-1); - $lengthMax -= $length; - if (!empty($element) && $element==$endMarker) { + $elementFound = preg_match("/<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); + $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); + $elementRawData = $matches[0][0]; + $elementStart = $matches[1][0]; + $elementName = $matches[2][0]; + if(!strempty($elementBefore)) { + $rawText = preg_replace("/\s+/s", " ", html_entity_decode($elementBefore, ENT_QUOTES, "UTF-8")); + if (empty($elementStart) && in_array(strtolower($elementName), $elementsBlock)) $rawText = rtrim($rawText)." "; + if (substru($rawText, 0, 1)==" " && (empty($output) || substru($output, -1)==" ")) $rawText = ltrim($rawText); + $output .= $this->getTextTruncated($rawText, $lengthMax); + $lengthMax -= strlenu($rawText); + } + if (!empty($elementRawData) && $elementRawData==$endMarker) { + $output .= $endMarkerText; $lengthMax = 0; - $endMarkerFound = true; } if ($lengthMax<=0 || !$elementFound) break; - $offsetBytes = $elementOffsetBytes + strlenb($element); + $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); } - $output = rtrim($output); - if ($lengthMax<=0) $output .= $endMarkerFound ? $endMarkerText : "…"; + $output = preg_replace("/\s+\…$/s", "…", $output); } else { $elementsOpen = array(); while (true) { - $elementFound = preg_match("/&.*?\;|<\s*?([\/!]?\w*)(.*?)\s*?\>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); - $element = $matches[0][0]; - $elementName = $matches[1][0]; - $elementText = $matches[2][0]; - $elementOffsetBytes = $elementFound ? $matches[0][1] : strlenb($text); - $string = substrb($text, $offsetBytes, $elementOffsetBytes - $offsetBytes); - $length = strlenu($string); - $output .= substru($string, 0, $length<$lengthMax ? $length : $lengthMax-1); - $lengthMax -= $length + ($element[0]=="&" ? 1 : 0); - if (!empty($element) && $element==$endMarker) { + $elementFound = preg_match("/&.*?\;|<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); + $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); + $elementRawData = $matches[0][0]; + $elementStart = $matches[1][0]; + $elementName = $matches[2][0]; + $elementEnd = $matches[4][0]; + if(!strempty($elementBefore)) { + $output .= $this->getTextTruncated($elementBefore, $lengthMax); + $lengthMax -= strlenu($elementBefore); + } + if (!empty($elementRawData) && $elementRawData==$endMarker) { + $output .= $endMarkerText; $lengthMax = 0; - $endMarkerFound = true; } if ($lengthMax<=0 || !$elementFound) break; - if (!empty($elementName) && substru($elementText, -1)!="/" && - !preg_match("/^(area|br|col|hr|img|input|col|param|!)/i", $elementName)) { - if ($elementName[0]!="/") { + if (!empty($elementName) && empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) { + if (empty($elementStart)) { array_push($elementsOpen, $elementName); } else { array_pop($elementsOpen); } } - $output .= $element; - $offsetBytes = $elementOffsetBytes + strlenb($element); + $output .= $elementRawData; + if ($elementRawData[0]=="&") --$lengthMax; + $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); } - $output = rtrim($output); + $output = preg_replace("/\s+\…$/s", "…", $output); for ($i=count($elementsOpen)-1; $i>=0; --$i) { - if (!preg_match("/^(dl|ol|ul|table|tbody|thead|tfoot|tr)/i", $elementsOpen[$i])) break; - $output .= "</".$elementsOpen[$i].">"; - } - if ($lengthMax<=0) $output .= $endMarkerFound ? $endMarkerText : "…"; - for (; $i>=0; --$i) { $output .= "</".$elementsOpen[$i].">"; } } - return $output; + return trim($output); } // Create title from text string public function createTextTitle($text) { - if (preg_match("/^.*\/([\w\-]+)/", $text, $matches)) $text = strreplaceu("-", " ", ucfirst($matches[1])); + if (preg_match("/^.*\/([\pL\d\-\_]+)/u", $text, $matches)) $text = strreplaceu("-", " ", ucfirst($matches[1])); return $text; } diff --git a/system/extensions/edit.js b/system/extensions/edit.js @@ -3,7 +3,7 @@ // This file may be used and distributed under the terms of the public license. var yellow = { - onLoad: function() { yellow.edit.load(); }, + onLoad: function(e) { yellow.edit.load(e); }, onKeydown: function(e) { yellow.edit.keydown(e); }, onDrag: function(e) { yellow.edit.drag(e); }, onDrop: function(e) { yellow.edit.drop(e); }, @@ -22,13 +22,17 @@ yellow.edit = { intervalId: 0, //timer interval ID // Handle initialisation - load: function() { + load: function(e) { var body = document.getElementsByTagName("body")[0]; if (body && body.firstChild && !document.getElementById("yellow-bar")) { this.createBar("yellow-bar"); this.processAction(yellow.page.action, yellow.page.status); clearInterval(this.intervalId); } + if (e.type=="DOMContentLoaded") { + var page = document.getElementsByClassName("page")[0]; + if (page) this.bindActions(page); + } }, // Handle keyboard @@ -862,6 +866,8 @@ yellow.edit = { bindActions: function(element) { var elements = element.getElementsByTagName("a"); for (var i=0, l=elements.length; i<l; i++) { + var href = elements[i].getAttribute("href"); + if (href.substring(0, 13)=="#data-action-") elements[i].setAttribute("data-action", href.substring(13)); if (elements[i].getAttribute("data-action")) elements[i].onclick = yellow.onClickAction; if (elements[i].getAttribute("data-action")=="toolbar") elements[i].onmousedown = function(e) { e.preventDefault(); }; } @@ -1475,4 +1481,5 @@ yellow.toolbox = { } }; -yellow.edit.intervalId = setInterval("yellow.onLoad()", 1); +yellow.edit.intervalId = setInterval("yellow.onLoad(new Event('DOMContentLoading'))", 1); +window.addEventListener("DOMContentLoaded", yellow.onLoad, false); diff --git a/system/extensions/edit.php b/system/extensions/edit.php @@ -4,7 +4,7 @@ // This file may be used and distributed under the terms of the public license. class YellowEdit { - const VERSION = "0.8.21"; + const VERSION = "0.8.22"; const TYPE = "feature"; public $yellow; //access to API public $response; //web response @@ -61,11 +61,17 @@ class YellowEdit { return $output; } + // Handle page layout + public function onParsePageLayout($page, $name) { + if ($this->response->isActive()) { + $this->response->processPageData($page); + } + } + // Handle page extra data public function onParsePageExtra($page, $name) { $output = null; if ($name=="header" && $this->response->isActive()) { - $this->response->processPageData($page); $extensionLocation = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreExtensionLocation"); $output = "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" data-bundle=\"none\" href=\"{$extensionLocation}edit.css\" />\n"; $output .= "<script type=\"text/javascript\" data-bundle=\"none\" src=\"{$extensionLocation}edit.js\"></script>\n"; @@ -303,7 +309,7 @@ class YellowEdit { $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); $statusCode = $this->yellow->sendStatus(301, $location); } else { - $this->yellow->page->error($this->response->isUserAccess("edit", $location) ? 434 : 404); + $this->yellow->page->error($this->response->isUserAccess("create", $location) ? 434 : 404); $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); } } @@ -694,7 +700,7 @@ class YellowEdit { $this->response->rawDataOutput = ""; } } else { - $this->response->status = $this->yellow->command("update", $extension, $option)==200 ? "done" : "error"; + $this->response->status = $this->yellow->command("update", $extension, $option)==0 ? "done" : "error"; } if ($this->response->status=="done") { $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); @@ -1009,11 +1015,14 @@ class YellowEditResponse { if (empty($this->rawDataSource)) $this->rawDataSource = $page->rawData; if (empty($this->rawDataEdit)) $this->rawDataEdit = $page->rawData; if (empty($this->rawDataEndOfLine)) $this->rawDataEndOfLine = $this->getEndOfLine($page->rawData); - if ($page->statusCode==434) $this->rawDataEdit = $this->getRawDataNew($page, true); - if ($this->yellow->toolbox->isLocationArgs()) { + if ($page->statusCode==404 || $this->yellow->toolbox->isLocationArgs()) { $this->rawDataEdit = $this->getRawDataGenerated($page); $this->rawDataReadonly = true; } + if ($page->statusCode==434) { + $this->rawDataEdit = $this->getRawDataNew($page, true); + $this->rawDataReadonly = false; + } } if (empty($this->language)) $this->language = $page->get("language"); if (empty($this->action)) $this->action = $this->isUser() ? "none" : "login"; @@ -1023,16 +1032,17 @@ class YellowEditResponse { // Return new page public function getPageNew($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { + $rawData = $this->yellow->toolbox->normaliseLines($rawData, $endOfLine); $page = new YellowPage($this->yellow); $page->setRequestInformation($scheme, $address, $base, $location, $fileName); - $page->parseData($this->normaliseLines($rawData, $endOfLine), false, 0); + $page->parseData($rawData, false, 0); $this->editContentFile($page, "create"); if ($this->yellow->content->find($page->location)) { $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation")); $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); while ($this->yellow->content->find($page->location) || empty($page->fileName)) { - $rawData = $this->yellow->toolbox->setMetaData($page->rawData, "title", $this->getTitleNext($page->rawData)); - $page->rawData = $this->normaliseLines($rawData, $endOfLine); + $page->rawData = $this->yellow->toolbox->setMetaData($page->rawData, "title", $this->getTitleNext($page->rawData)); + $page->rawData = $this->yellow->toolbox->normaliseLines($page->rawData, $endOfLine); $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation")); $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); if (++$pageCounter>999) break; @@ -1051,16 +1061,16 @@ class YellowEditResponse { // Return modified page public function getPageEdit($scheme, $address, $base, $location, $fileName, $rawDataSource, $rawDataEdit, $rawDataFile, $endOfLine) { + $rawDataSource = $this->yellow->toolbox->normaliseLines($rawDataSource, $endOfLine); + $rawDataEdit = $this->yellow->toolbox->normaliseLines($rawDataEdit, $endOfLine); + $rawDataFile = $this->yellow->toolbox->normaliseLines($rawDataFile, $endOfLine); + $rawData = $this->extension->merge->merge($rawDataSource, $rawDataEdit, $rawDataFile); $page = new YellowPage($this->yellow); $page->setRequestInformation($scheme, $address, $base, $location, $fileName); - $rawData = $this->extension->merge->merge( - $this->normaliseLines($rawDataSource, $endOfLine), - $this->normaliseLines($rawDataEdit, $endOfLine), - $this->normaliseLines($rawDataFile, $endOfLine)); - $page->parseData($this->normaliseLines($rawData, $endOfLine), false, 0); + $page->parseData($rawData, false, 0); $pageSource = new YellowPage($this->yellow); $pageSource->setRequestInformation($scheme, $address, $base, $location, $fileName); - $pageSource->parseData($this->normaliseLines($rawDataSource, $endOfLine), false, 0); + $pageSource->parseData(($rawDataSource), false, 0); $this->editContentFile($page, "edit"); if ($this->isMetaModified($pageSource, $page) && $page->location!=$this->yellow->content->getHomeLocation($page->location)) { $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation"), true); @@ -1079,9 +1089,10 @@ class YellowEditResponse { // Return deleted page public function getPageDelete($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { + $rawData = $this->yellow->toolbox->normaliseLines($rawData, $endOfLine); $page = new YellowPage($this->yellow); $page->setRequestInformation($scheme, $address, $base, $location, $fileName); - $page->parseData($this->normaliseLines($rawData, $endOfLine), false, 0); + $page->parseData($rawData, false, 0); $this->editContentFile($page, "delete"); if (!$this->isUserAccess("delete", $page->location)) { $page->error(500, "Page '".$page->get("title")."' is restricted!"); @@ -1091,9 +1102,10 @@ class YellowEditResponse { // Return preview page public function getPagePreview($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { + $rawData = $this->yellow->toolbox->normaliseLines($rawData, $endOfLine); $page = new YellowPage($this->yellow); $page->setRequestInformation($scheme, $address, $base, $location, $fileName); - $page->parseData($this->normaliseLines($rawData, $endOfLine), false, 200); + $page->parseData($rawData, false, 200); $this->yellow->text->setLanguage($page->get("language")); $class = "page-preview layout-".$page->get("layout"); $output = "<div class=\"".htmlspecialchars($class)."\"><div class=\"content\"><div class=\"main\">"; @@ -1110,6 +1122,14 @@ class YellowEditResponse { $file->setRequestInformation($scheme, $address, $base, "/".$fileNameTemp, $fileNameTemp); $file->parseData(null, false, 0); $file->set("fileNameShort", $fileNameShort); + $file->set("type", $this->yellow->toolbox->getFileType($fileNameShort)); + if ($file->get("type")=="html" || $file->get("type")=="svg") { + $fileData = $this->yellow->toolbox->readFile($fileNameTemp); + $fileData = $this->yellow->toolbox->normaliseData($fileData, $file->get("type")); + if (empty($fileData) || !$this->yellow->toolbox->createFile($fileNameTemp, $fileData)) { + $file->error(500, "Can't write file '$fileNameTemp'!"); + } + } $this->editMediaFile($file, "upload"); $file->location = $this->getFileNewLocation($fileNameShort, $pageLocation, $file->get("fileNewLocation")); $file->fileName = substru($file->location, 1); @@ -1148,7 +1168,6 @@ class YellowEditResponse { $data["address"] = $this->yellow->page->address; $data["base"] = $this->yellow->page->base; $data["location"] = $this->yellow->page->location; - $data["safeMode"] = $this->yellow->page->safeMode; } if ($this->action!="none") $data = array_merge($data, $this->getRequestData()); $data["action"] = $this->action; @@ -1554,16 +1573,6 @@ class YellowEditResponse { } } - // Normalise text lines, convert line endings - public function normaliseLines($text, $endOfLine = "lf") { - if ($endOfLine=="lf") { - $text = preg_replace("/\R/u", "\n", $text); - } else { - $text = preg_replace("/\R/u", "\r\n", $text); - } - return $text; - } - // Check if meta data has been modified public function isMetaModified($pageSource, $pageOther) { return substrb($pageSource->rawData, 0, $pageSource->metaDataOffsetBytes) != diff --git a/system/extensions/image.php b/system/extensions/image.php @@ -4,7 +4,7 @@ // This file may be used and distributed under the terms of the public license. class YellowImage { - const VERSION = "0.8.6"; + const VERSION = "0.8.7"; const TYPE = "feature"; public $yellow; //access to API @@ -48,8 +48,7 @@ class YellowImage { public function onEditMediaFile($file, $action) { if ($action=="upload") { $fileName = $file->fileName; - $fileType = $this->yellow->toolbox->getFileType($file->get("fileNameShort")); - list($widthInput, $heightInput, $type) = $this->yellow->toolbox->detectImageInformation($fileName, $fileType); + list($widthInput, $heightInput, $type) = $this->yellow->toolbox->detectImageInformation($fileName, $file->get("type")); $widthMax = $this->yellow->system->get("imageUploadWidthMax"); $heightMax = $this->yellow->system->get("imageUploadHeightMax"); if (($widthInput>$widthMax || $heightInput>$heightMax) && ($type=="gif" || $type=="jpg" || $type=="png")) { @@ -61,12 +60,6 @@ class YellowImage { $file->error(500, "Can't write file '$fileName'!"); } } - if ($this->yellow->system->get("coreSafeMode") && $fileType=="svg") { - $output = $this->sanitiseXmlData($this->yellow->toolbox->readFile($fileName)); - if (empty($output) || !$this->yellow->toolbox->createFile($fileName, $output)) { - $file->error(500, "Can't write file '$fileName'!"); - } - } } } @@ -210,58 +203,6 @@ class YellowImage { } return intval($value); } - - // Return sanitised XML data - public function sanitiseXmlData($rawData) { - $output = ""; - $elementsHtml = array( - "a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "image", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meter", "nav", "nobr", "ol", "optgroup", "option", "output", "p", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "shadow", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"); - $elementsSvg = array( - "svg", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "feblend", "fecolormatrix", "fecomponenttransfer", "fecomposite", "feconvolvematrix", "fediffuselighting", "fedisplacementmap", "fedistantlight", "feflood", "fefunca", "fefuncb", "fefuncg", "fefuncr", "fegaussianblur", "femerge", "femergenode", "femorphology", "feoffset", "fepointlight", "fespecularlighting", "fespotlight", "fetile", "feturbulence", "filter", "font", "g", "glyph", "glyphref", "hkern", "image", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "use", "view", "vkern"); - $attributesHtml = array( - "accept", "action", "align", "alt", "autocomplete", "background", "bgcolor", "border", "cellpadding", "cellspacing", "checked", "cite", "class", "clear", "color", "cols", "colspan", "coords", "crossorigin", "datetime", "default", "dir", "disabled", "download", "enctype", "face", "for", "headers", "height", "hidden", "high", "href", "hreflang", "id", "integrity", "ismap", "label", "lang", "list", "loop", "low", "max", "maxlength", "media", "method", "min", "multiple", "name", "noshade", "novalidate", "nowrap", "open", "optimum", "pattern", "placeholder", "poster", "preload", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "spellcheck", "scope", "selected", "shape", "size", "sizes", "span", "srclang", "start", "src", "srcset", "step", "style", "summary", "tabindex", "title", "type", "usemap", "valign", "value", "width", "xmlns"); - $attributesSvg = array( - "accent-height", "accumulate", "additivive", "alignment-baseline", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "dx", "dy", "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "fill", "fill-opacity", "fill-rule", "filter", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href", "id", "image-rendering", "in", "in2", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "media", "method", "mode", "min", "name", "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "points", "preservealpha", "preserveaspectratio", "r", "rx", "ry", "radius", "refx", "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering", "specularconstant", "specularexponent", "spreadmethod", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style", "surfacescale", "tabindex", "targetx", "targety", "transform", "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode", "values", "viewbox", "visibility", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width", "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2", "xmlns", "y", "y1", "y2", "z", "zoomandpan"); - $attributesXml = array( - "xlink:href", "xml:id", "xml:space"); - if (!empty($rawData)) { - $entityLoader = libxml_disable_entity_loader(true); - $internalErrors = libxml_use_internal_errors(true); - $document = new DOMDocument(); - $document->recover = true; - if ($document->loadXML($rawData)) { - $elementsSafe = array_merge($elementsHtml, $elementsSvg); - $attributesSafe = array_merge($attributesHtml, $attributesSvg, $attributesXml); - $elements = $document->getElementsByTagName("*"); - for ($i=$elements->length-1; $i>=0; --$i) { - $element = $elements->item($i); - if (!in_array(strtolower($element->tagName), $elementsSafe)) { - $element->parentNode->removeChild($element); - continue; - } - for ($j=$element->attributes->length-1; $j>=0; --$j) { - $attribute = $element->attributes->item($j); - if (!in_array(strtolower($attribute->name), $attributesSafe) && !preg_match("/^(aria|data)-/i", $attribute->name)) { - $element->removeAttribute($attribute->name); - } - } - $href = $element->getAttribute("href"); - if (preg_match("/^\w+:/", $href) && !preg_match("/^(http|https|ftp|mailto):/", $href)) { - $element->setAttribute("href", "error-xss-filter"); - } - $href = $element->getAttribute("xlink:href"); - if (preg_match("/^\w+:/", $href) && !preg_match("/^(http|https|ftp|mailto):/", $href)) { - $element->setAttribute("xlink:href", "error-xss-filter"); - } - } - $output = $document->saveXML(); - if (!preg_match("/^<\?xml /", $rawData) && preg_match("/^<\?xml (.*?)>\s*(.*)$/s", $output, $matches)) $output = $matches[2]; - } - libxml_disable_entity_loader($entityLoader); - libxml_use_internal_errors($internalErrors); - } - return $output; - } // Check if file needs to be updated public function isFileNotUpdated($fileNameInput, $fileNameOutput) { diff --git a/system/extensions/install-languages.zip b/system/extensions/install-languages.zip Binary files differ. diff --git a/system/extensions/markdown.php b/system/extensions/markdown.php @@ -1,10 +1,10 @@ <?php // Markdown extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/markdown -// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se +// Copyright (c) 2013-2020 Datenstrom, https://datenstrom.se // This file may be used and distributed under the terms of the public license. class YellowMarkdown { - const VERSION = "0.8.11"; + const VERSION = "0.8.12"; const TYPE = "feature"; public $yellow; //access to API @@ -15,7 +15,7 @@ class YellowMarkdown { // Handle page content in raw format public function onParseContentRaw($page, $text) { - $markdown = new YellowMarkdownExtraParser($this->yellow, $page); + $markdown = new YellowMarkdownParser($this->yellow, $page); return $markdown->transform($text); } } @@ -3831,10 +3831,10 @@ class MarkdownExtraParser extends MarkdownParser { } } -// Yellow Markdown extra extension -// Copyright (c) 2013-2019 Datenstrom +// Datenstrom Yellow Markdown parser +// Copyright (c) 2013-2020 Datenstrom -class YellowMarkdownExtraParser extends MarkdownExtraParser { +class YellowMarkdownParser extends MarkdownExtraParser { public $yellow; //access to API public $page; //access to page public $idAttributes; //id attributes @@ -3845,10 +3845,8 @@ class YellowMarkdownExtraParser extends MarkdownExtraParser { $this->page = $page; $this->idAttributes = array(); $this->noticeLevel = 0; - $this->no_markup = $page->safeMode; $this->url_filter_func = function($url) use ($yellow, $page) { - return $yellow->lookup->normaliseLocation($url, $page->location, - $page->safeMode && $page->statusCode==200); + return $yellow->lookup->normaliseLocation($url, $page->location); }; $this->span_gamut += array("doStrikethrough" => 55); $this->block_gamut += array("doNoticeBlocks" => 65); diff --git a/system/extensions/update.php b/system/extensions/update.php @@ -4,7 +4,7 @@ // This file may be used and distributed under the terms of the public license. class YellowUpdate { - const VERSION = "0.8.16"; + const VERSION = "0.8.17"; const TYPE = "feature"; const PRIORITY = "2"; public $yellow; //access to API @@ -68,11 +68,10 @@ class YellowUpdate { } } if ($action=="update") { //TODO: remove later, converts old content settings - if ($this->yellow->system->isExisting("safeMode")) { - $coreSafeMode = $this->yellow->system->get("safeMode"); + if ($this->yellow->system->isExisting("multiLanguageMode")) { $coreMultiLanguageMode = $this->yellow->system->get("multiLanguageMode"); $fileName = $this->yellow->system->get("coreSettingDir").$this->yellow->system->get("coreSystemFile"); - $this->yellow->system->save($fileName, array("coreSafeMode" => $coreSafeMode, "coreMultiLanguageMode" => $coreMultiLanguageMode)); + $this->yellow->system->save($fileName, array("coreMultiLanguageMode" => $coreMultiLanguageMode)); $path = $this->yellow->system->get("coreContentDir"); foreach ($this->yellow->toolbox->getDirectoryEntriesRecursive($path, "/^.*\.md$/", true, false) as $entry) { $fileData = $fileDataNew = $this->yellow->toolbox->readFile($entry); diff --git a/system/settings/system.ini b/system/settings/system.ini @@ -17,7 +17,6 @@ CoreCacheDir: cache/ CoreTrashDir: system/trash/ CoreServerUrl: auto CoreServerTimezone: UTC -CoreSafeMode: 0 CoreMultiLanguageMode: 0 CoreMediaLocation: /media/ CoreDownloadLocation: /media/downloads/ diff --git a/yellow.php b/yellow.php @@ -12,6 +12,5 @@ if (PHP_SAPI!="cli") { } else { $yellow = new YellowCore(); $yellow->load(); - $statusCode = $yellow->command($argv[1], $argv[2], $argv[3], $argv[4], $argv[5], $argv[6], $argv[7]); - exit($statusCode<400 ? 0 : 1); + exit($yellow->command($argv[1], $argv[2], $argv[3], $argv[4], $argv[5], $argv[6], $argv[7])); }