Merge remote-tracking branch 'origin/support/2.7' into develop

# Conflicts:
#	core/attributedef.class.inc.php
#	core/config.class.inc.php
#	core/htmlsanitizer.class.inc.php
#	sources/Renderer/RenderingOutput.php
#	test/core/sanitizer/HTMLDOMSanitizerTest.php
#	test/integration/DictionariesConsistencyTest.php
This commit is contained in:
Pierre Goiffon
2021-11-24 15:01:38 +01:00
14 changed files with 516 additions and 235 deletions

View File

@@ -8130,6 +8130,25 @@ class AttributeImage extends AttributeBlob
return "Image"; return "Image";
} }
/**
* {@inheritDoc}
* @see AttributeBlob::MakeRealValue()
*/
public function MakeRealValue($proposedValue, $oHostObj)
{
$oDoc = parent::MakeRealValue($proposedValue, $oHostObj);
if (($oDoc instanceof ormDocument)
&& (false === $oDoc->IsEmpty())
&& ($oDoc->GetMimeType() === 'image/svg+xml')) {
$sCleanSvg = HTMLSanitizer::Sanitize($oDoc->GetData(), 'svg_sanitizer');
$oDoc = new ormDocument($sCleanSvg, $oDoc->GetMimeType(), $oDoc->GetFileName());
}
// The validation of the MIME Type is done by CheckFormat below
return $oDoc;
}
/** /**
* Check that the supplied ormDocument actually contains an image * Check that the supplied ormDocument actually contains an image
* {@inheritDoc} * {@inheritDoc}

View File

@@ -1125,6 +1125,14 @@ class Config
'source_of_value' => '', 'source_of_value' => '',
'show_in_conf_sample' => false, 'show_in_conf_sample' => false,
], ],
'svg_sanitizer' => [
'type' => 'string',
'description' => 'The class to use for SVG sanitization : allow to provide a custom made sanitizer',
'default' => 'SvgDOMSanitizer',
'value' => '',
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'inline_image_max_display_width' => [ 'inline_image_max_display_width' => [
'type' => 'integer', 'type' => 'integer',
'description' => 'The maximum width (in pixels) when displaying images inside an HTML formatted attribute. Images will be displayed using this this maximum width.', 'description' => 'The maximum width (in pixels) when displaying images inside an HTML formatted attribute. Images will be displayed using this this maximum width.',

View File

@@ -97,64 +97,163 @@ class HTMLNullSanitizer extends HTMLSanitizer
{ {
return $sHTML; return $sHTML;
} }
} }
/** /**
* A standard-compliant HTMLSanitizer based on the HTMLPurifier library by Edward Z. Yang * Common implementation for sanitizer using DOM parsing
* Complete but quite slow
* http://htmlpurifier.org
*/ */
/* abstract class DOMSanitizer extends HTMLSanitizer
class HTMLPurifierSanitizer extends HTMLSanitizer
{ {
protected static $oPurifier = null; /** @var DOMDocument */
protected $oDoc;
public function __construct() abstract public function GetTagsWhiteList();
{
if (self::$oPurifier == null)
{
$sLibPath = APPROOT.'lib/htmlpurifier/HTMLPurifier.auto.php';
if (!file_exists($sLibPath))
{
throw new Exception("Missing library '$sLibPath', cannot use HTMLPurifierSanitizer.");
}
require_once($sLibPath);
$oPurifierConfig = HTMLPurifier_Config::createDefault(); abstract public function GetTagsBlackList();
$oPurifierConfig->set('Core.Encoding', 'UTF-8'); // defaults to 'UTF-8'
$oPurifierConfig->set('HTML.Doctype', 'XHTML 1.0 Strict'); // defaults to 'XHTML 1.0 Transitional' abstract public function GetAttrsWhiteList();
$oPurifierConfig->set('URI.AllowedSchemes', array (
'http' => true, abstract public function GetAttrsBlackList();
'https' => true,
'data' => true, // This one is not present by default abstract public function GetStylesWhiteList();
));
$sPurifierCache = APPROOT.'data/HTMLPurifier';
if (!is_dir($sPurifierCache))
{
mkdir($sPurifierCache);
}
if (!is_dir($sPurifierCache))
{
throw new Exception("Could not create the cache directory '$sPurifierCache'");
}
$oPurifierConfig->set('Cache.SerializerPath', $sPurifierCache); // no trailing slash
self::$oPurifier = new HTMLPurifier($oPurifierConfig);
}
}
public function DoSanitize($sHTML) public function DoSanitize($sHTML)
{ {
$sCleanHtml = self::$oPurifier->purify($sHTML); $this->oDoc = new DOMDocument();
$this->oDoc->preserveWhitespace = true;
// MS outlook implements empty lines by the mean of <p><o:p></o:p></p>
// We have to transform that into <p><br></p> (which is how Thunderbird implements empty lines)
// Unfortunately, DOMDocument::loadHTML does not take the tag namespaces into account (once loaded there is no way to know if the tag did have a namespace)
// therefore we have to do the transformation upfront
$sHTML = preg_replace('@<o:p>(\s|&nbsp;)*</o:p>@', '<br>', $sHTML);
$this->LoadDoc($sHTML);
$this->CleanNode($this->oDoc);
$sCleanHtml = $this->PrintDoc();
return $sCleanHtml; return $sCleanHtml;
} }
abstract public function LoadDoc($sHTML);
/**
* @return string cleaned source
* @uses \DOMSanitizer::oDoc
*/
abstract public function PrintDoc();
protected function CleanNode(DOMNode $oElement)
{
$aAttrToRemove = array();
// Gather the attributes to remove
if ($oElement->hasAttributes()) {
foreach ($oElement->attributes as $oAttr) {
$sAttr = strtolower($oAttr->name);
if ((false === empty($this->GetAttrsBlackList()))
&& (in_array($sAttr, $this->GetAttrsBlackList(), true))) {
$aAttrToRemove[] = $oAttr->name;
} else if ((false === empty($this->GetTagsWhiteList()))
&& (false === in_array($sAttr, $this->GetTagsWhiteList()[strtolower($oElement->tagName)]))) {
$aAttrToRemove[] = $oAttr->name;
} else if (!$this->IsValidAttributeContent($sAttr, $oAttr->value)) {
// Invalid content
$aAttrToRemove[] = $oAttr->name;
} else if ($sAttr == 'style') {
// Special processing for style tags
$sCleanStyle = $this->CleanStyle($oAttr->value);
if ($sCleanStyle == '') {
// Invalid content
$aAttrToRemove[] = $oAttr->name;
} else {
$oElement->setAttribute($oAttr->name, $sCleanStyle);
}
}
}
// Now remove them
foreach($aAttrToRemove as $sName)
{
$oElement->removeAttribute($sName);
}
}
if ($oElement->hasChildNodes())
{
$aChildElementsToRemove = array();
// Gather the child noes to remove
foreach($oElement->childNodes as $oNode) {
if ($oNode instanceof DOMElement) {
$sNodeTagName = strtolower($oNode->tagName);
}
if (($oNode instanceof DOMElement)
&& (false === empty($this->GetTagsBlackList()))
&& (in_array($sNodeTagName, $this->GetTagsBlackList(), true))) {
$aChildElementsToRemove[] = $oNode;
} else if (($oNode instanceof DOMElement)
&& (false === empty($this->GetTagsWhiteList()))
&& (false === array_key_exists($sNodeTagName, $this->GetTagsWhiteList()))) {
$aChildElementsToRemove[] = $oNode;
} else if ($oNode instanceof DOMComment) {
$aChildElementsToRemove[] = $oNode;
} else {
// Recurse
$this->CleanNode($oNode);
if (($oNode instanceof DOMElement) && (strtolower($oNode->tagName) == 'img')) {
InlineImage::ProcessImageTag($oNode);
}
}
}
// Now remove them
foreach($aChildElementsToRemove as $oDomElement)
{
$oElement->removeChild($oDomElement);
}
}
}
protected function IsValidAttributeContent($sAttributeName, $sValue)
{
if ((false === empty($this->GetAttrsBlackList()))
&& (in_array($sAttributeName, $this->GetAttrsBlackList(), true))) {
return true;
}
if (array_key_exists($sAttributeName, $this->GetAttrsWhiteList())) {
return preg_match($this->GetAttrsWhiteList()[$sAttributeName], $sValue);
}
return true;
}
protected function CleanStyle($sStyle)
{
if (empty($this->GetStylesWhiteList())) {
return $sStyle;
}
$aAllowedStyles = array();
$aItems = explode(';', $sStyle);
{
foreach ($aItems as $sItem) {
$aElements = explode(':', trim($sItem));
if (in_array(trim(strtolower($aElements[0])), $this->GetStylesWhiteList())) {
$aAllowedStyles[] = trim($sItem);
}
}
}
return implode(';', $aAllowedStyles);
}
} }
*/
class HTMLDOMSanitizer extends HTMLSanitizer
class HTMLDOMSanitizer extends DOMSanitizer
{ {
protected $oDoc;
/** /**
* @var array * @var array
* @see https://www.itophub.io/wiki/page?id=2_6_0%3Aadmin%3Arich_text_limitations * @see https://www.itophub.io/wiki/page?id=2_6_0%3Aadmin%3Arich_text_limitations
@@ -239,6 +338,31 @@ class HTMLDOMSanitizer extends HTMLSanitizer
'white-space', 'white-space',
); );
public function GetTagsWhiteList()
{
return static::$aTagsWhiteList;
}
public function GetTagsBlackList()
{
return [];
}
public function GetAttrsWhiteList()
{
return static::$aAttrsWhiteList;
}
public function GetAttrsBlackList()
{
return [];
}
public function GetStylesWhiteList()
{
return static::$aStylesWhiteList;
}
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
@@ -264,139 +388,152 @@ class HTMLDOMSanitizer extends HTMLSanitizer
} }
} }
public function DoSanitize($sHTML) public function LoadDoc($sHTML)
{ {
$this->oDoc = new DOMDocument();
$this->oDoc->preserveWhitespace = true;
// MS outlook implements empty lines by the mean of <p><o:p></o:p></p>
// We have to transform that into <p><br></p> (which is how Thunderbird implements empty lines)
// Unfortunately, DOMDocument::loadHTML does not take the tag namespaces into account (once loaded there is no way to know if the tag did have a namespace)
// therefore we have to do the transformation upfront
$sHTML = preg_replace('@<o:p>(\s|&nbsp;)*</o:p>@', '<br>', $sHTML);
// Replace badly encoded non breaking space
$sHTML = preg_replace('~\xc2\xa0~', ' ', $sHTML);
@$this->oDoc->loadHTML('<?xml encoding="UTF-8"?>'.$sHTML); // For loading HTML chunks where the character set is not specified @$this->oDoc->loadHTML('<?xml encoding="UTF-8"?>'.$sHTML); // For loading HTML chunks where the character set is not specified
$this->oDoc->preserveWhitespace = true;
}
$this->CleanNode($this->oDoc); public function PrintDoc()
{
$oXPath = new DOMXPath($this->oDoc); $oXPath = new DOMXPath($this->oDoc);
$sXPath = "//body"; $sXPath = "//body";
$oNodesList = $oXPath->query($sXPath); $oNodesList = $oXPath->query($sXPath);
if ($oNodesList->length == 0) if ($oNodesList->length == 0) {
{
// No body, save the whole document // No body, save the whole document
$sCleanHtml = $this->oDoc->saveHTML(); $sCleanHtml = $this->oDoc->saveHTML();
} } else {
else
{
// Export only the content of the body tag // Export only the content of the body tag
$sCleanHtml = $this->oDoc->saveHTML($oNodesList->item(0)); $sCleanHtml = $this->oDoc->saveHTML($oNodesList->item(0));
// remove the body tag itself // remove the body tag itself
$sCleanHtml = str_replace( array('<body>', '</body>'), '', $sCleanHtml); $sCleanHtml = str_replace(array('<body>', '</body>'), '', $sCleanHtml);
} }
return $sCleanHtml; return $sCleanHtml;
} }
}
protected function CleanNode(DOMNode $oElement)
/**
* @since 2.6.5 2.7.6 3.0.0 N°4360
*/
class SvgDOMSanitizer extends DOMSanitizer
{
public function GetTagsWhiteList()
{ {
$aAttrToRemove = array(); return [];
// Gather the attributes to remove
if ($oElement->hasAttributes())
{
foreach($oElement->attributes as $oAttr)
{
$sAttr = strtolower($oAttr->name);
if (!in_array($sAttr, self::$aTagsWhiteList[strtolower($oElement->tagName)]))
{
// Forbidden (or unknown) attribute
$aAttrToRemove[] = $oAttr->name;
}
else if (!$this->IsValidAttributeContent($sAttr, $oAttr->value))
{
// Invalid content
$aAttrToRemove[] = $oAttr->name;
}
else if ($sAttr == 'style')
{
// Special processing for style tags
$sCleanStyle = $this->CleanStyle($oAttr->value);
if ($sCleanStyle == '')
{
// Invalid content
$aAttrToRemove[] = $oAttr->name;
}
else
{
$oElement->setAttribute($oAttr->name, $sCleanStyle);
}
}
}
// Now remove them
foreach($aAttrToRemove as $sName)
{
$oElement->removeAttribute($sName);
}
}
if ($oElement->hasChildNodes())
{
$aChildElementsToRemove = array();
// Gather the child noes to remove
foreach($oElement->childNodes as $oNode)
{
if (($oNode instanceof DOMElement) && (!array_key_exists(strtolower($oNode->tagName), self::$aTagsWhiteList)))
{
$aChildElementsToRemove[] = $oNode;
}
else if ($oNode instanceof DOMComment)
{
$aChildElementsToRemove[] = $oNode;
}
else
{
// Recurse
$this->CleanNode($oNode);
if (($oNode instanceof DOMElement) && (strtolower($oNode->tagName) == 'img'))
{
InlineImage::ProcessImageTag($oNode);
}
}
}
// Now remove them
foreach($aChildElementsToRemove as $oDomElement)
{
$oElement->removeChild($oDomElement);
}
}
} }
protected function CleanStyle($sStyle) /**
* @return string[]
* @link https://developer.mozilla.org/en-US/docs/Web/SVG/Element/script
*/
public function GetTagsBlackList()
{ {
$aAllowedStyles = array(); return [
$aItems = explode(';', $sStyle); 'script',
{ ];
foreach($aItems as $sItem)
{
$aElements = explode(':', trim($sItem));
if (in_array(trim(strtolower($aElements[0])), static::$aStylesWhiteList))
{
$aAllowedStyles[] = trim($sItem);
}
}
}
return implode(';', $aAllowedStyles);
} }
protected function IsValidAttributeContent($sAttributeName, $sValue) public function GetAttrsWhiteList()
{ {
if (array_key_exists($sAttributeName, self::$aAttrsWhiteList)) return [];
{ }
return preg_match(self::$aAttrsWhiteList[$sAttributeName], $sValue);
} /**
return true; * @return string[]
* @link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/Events#document_event_attributes
*/
public function GetAttrsBlackList()
{
return [
'onbegin',
'onbegin',
'onrepeat',
'onabort',
'onerror',
'onerror',
'onscroll',
'onunload',
'oncopy',
'oncut',
'onpaste',
'oncancel',
'oncanplay',
'oncanplaythrough',
'onchange',
'onclick',
'onclose',
'oncuechange',
'ondblclick',
'ondrag',
'ondragend',
'ondragenter',
'ondragleave',
'ondragover',
'ondragstart',
'ondrop',
'ondurationchange',
'onemptied',
'onended',
'onerror',
'onfocus',
'oninput',
'oninvalid',
'onkeydown',
'onkeypress',
'onkeyup',
'onload',
'onloadeddata',
'onloadedmetadata',
'onloadstart',
'onmousedown',
'onmouseenter',
'onmouseleave',
'onmousemove',
'onmouseout',
'onmouseover',
'onmouseup',
'onmousewheel',
'onpause',
'onplay',
'onplaying',
'onprogress',
'onratechange',
'onreset',
'onresize',
'onscroll',
'onseeked',
'onseeking',
'onselect',
'onshow',
'onstalled',
'onsubmit',
'onsuspend',
'ontimeupdate',
'ontoggle',
'onvolumechange',
'onwaiting',
'onactivate',
'onfocusin',
'onfocusout',
];
}
public function GetStylesWhiteList()
{
return [];
}
public function LoadDoc($sHTML)
{
@$this->oDoc->loadXml($sHTML, LIBXML_NOBLANKS);
}
public function PrintDoc()
{
return $this->oDoc->saveXML();
} }
} }

View File

@@ -138,7 +138,7 @@ final class ormTagSet extends ormSet
} }
/** /**
* @return array of tags indexed by code * @return array index: code, value: corresponding {@see \TagSetFieldData}
*/ */
public function GetTags() public function GetTags()
{ {

View File

@@ -107,18 +107,36 @@ EOF
// ... in view mode // ... in view mode
else else
{ {
$aItems = $oOrmItemSet->GetTags(); if ($oOrmItemSet instanceof \ormTagSet) {
$aItems = $oOrmItemSet->GetTags();
$fExtractTagData = static function($oTag, &$sItemLabel, &$sItemDescription) {
$sItemLabel = $oTag->Get('label');
$sItemDescription = $oTag->Get('description');
};
} else {
$aItems = $oOrmItemSet->GetValues();
$oAttDef = MetaModel::GetAttributeDef($oOrmItemSet->GetClass(), $oOrmItemSet->GetAttCode());
$fExtractTagData = static function($sEnumSetValue, &$sItemLabel, &$sItemDescription) use ($oAttDef) {
$sItemLabel = $oAttDef->GetValueLabel($sEnumSetValue);
$sItemDescription = '';
};
}
$oOutput->AddHtml('<div class="form-control-static">') $oOutput->AddHtml('<div class="form-control-static">')
->AddHtml('<span class="label-group">'); ->AddHtml('<span class="label-group">');
foreach($aItems as $sItemCode => $oItem) foreach($aItems as $sItemCode => $value)
{ {
$sItemLabel = $oItem->Get('label'); $fExtractTagData($value, $sItemLabel, $sItemDescription);
$sItemDescription = $oItem->Get('description');
$sDescriptionAttr = (empty($sItemDescription))
? ''
: ' data-description="'.utils::HtmlEntities($sItemDescription).'"';
$oOutput->AddHtml('<span class="label label-default" data-code="'.$sItemCode.'" data-label="') $oOutput->AddHtml('<span class="label label-default" data-code="'.$sItemCode.'" data-label="')
->AddHtml($sItemLabel, true) ->AddHtml($sItemLabel, true)
->AddHtml('" data-description="') ->AddHtml('"')
->AddHtml($sItemDescription, true) ->AddHtml($sDescriptionAttr)
->AddHtml('">') ->AddHtml('>')
->AddHtml($sItemLabel, true) ->AddHtml($sItemLabel, true)
->AddHtml('</span>'); ->AddHtml('</span>');
} }

View File

@@ -20,6 +20,8 @@
namespace Combodo\iTop\Renderer; namespace Combodo\iTop\Renderer;
use utils;
/** /**
* Description of RenderingOutput * Description of RenderingOutput
* *
@@ -111,15 +113,15 @@ class RenderingOutput
/** /**
* *
* @param string $sHtml * @param ?string $sHtml
* @param bool $bEncodeHtmlEntities * @param bool $bEscapeHtmlEntities
* *
* @return \Combodo\iTop\Renderer\RenderingOutput * @return \Combodo\iTop\Renderer\RenderingOutput
*/ */
public function AddHtml(?string $sHtml, bool $bEncodeHtmlEntities = false) public function AddHtml(?string $sHtml, bool $bEscapeHtmlEntities = false)
{ {
if (!is_null($sHtml)) { if (!is_null($sHtml)) {
$this->sHtml .= ($bEncodeHtmlEntities) ? htmlentities($sHtml, ENT_QUOTES, 'UTF-8') : $sHtml; $this->sHtml .= ($bEscapeHtmlEntities) ? utils::Escapehtml($sHtml) : $sHtml;
} }
return $this; return $this;

View File

@@ -0,0 +1,75 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Core\Sanitizer;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
abstract class AbstractDOMSanitizerTest extends ItopTestCase
{
const INPUT_DIRECTORY = 'input';
const OUTPUT_DIRECTORY = 'output';
protected function setUp()
{
parent::setUp();
require_once(APPROOT.'application/utils.inc.php');
require_once(APPROOT.'core/htmlsanitizer.class.inc.php');
require_once(APPROOT.'test/core/sanitizer/InlineImageMock.php');
}
protected function ReadTestFile($sFileToTest, $sFolderName)
{
$sCurrentPath = __DIR__;
return file_get_contents($sCurrentPath.DIRECTORY_SEPARATOR
.$sFolderName.DIRECTORY_SEPARATOR
.$sFileToTest);
}
protected function RemoveNewLines($sText)
{
$sText = str_replace("\r\n", "\n", $sText);
$sText = str_replace("\r", "\n", $sText);
$sText = str_replace("\n", '', $sText);
return $sText;
}
/**
* Generates an appropriate value for the given attribute, or use the counter if needed.
* This is necessary as most of the attributes with empty or inappropriate values (like a numeric for a href) are removed by the parser
*
* @param string $sTagAttribute
* @param int $iAttributeCounter
*
* @return string attribute value
*/
protected function GetTagAttributeValue($sTagAttribute, $iAttributeCounter)
{
$sTagAttrValue = ' '.$sTagAttribute.'="';
if (in_array($sTagAttribute, array('href', 'src'))) {
return $sTagAttrValue.'http://www.combodo.com"';
}
if ($sTagAttribute === 'style') {
return $sTagAttrValue.'color: black"';
}
return $sTagAttrValue.$iAttributeCounter.'"';
}
protected function IsClosingTag($sTag)
{
if (in_array($sTag, array('br', 'img', 'hr'))) {
return false;
}
return true;
}
}

View File

@@ -1,20 +1,19 @@
<?php <?php
namespace Combodo\iTop\Test\UnitTest\Core\Sanitizer;
namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use HTMLDOMSanitizer; use HTMLDOMSanitizer;
require_once __DIR__.'/AbstractDOMSanitizerTest.php';
/** /**
* @runTestsInSeparateProcesses * @runTestsInSeparateProcesses
* @preserveGlobalState disabled * @preserveGlobalState disabled
* @backupGlobals disabled * @backupGlobals disabled
*/ */
class HTMLDOMSanitizerTest extends ItopTestCase class HTMLDOMSanitizerTest extends AbstractDOMSanitizerTest
{ {
const INPUT_DIRECTORY = 'sanitizer/input';
const OUTPUT_DIRECTORY = 'sanitizer/output';
/** /**
* @dataProvider DoSanitizeProvider * @dataProvider DoSanitizeProvider
* *
@@ -41,34 +40,15 @@ class HTMLDOMSanitizerTest extends ItopTestCase
$this->assertEquals($sOutputHtml, $sRes); $this->assertEquals($sOutputHtml, $sRes);
} }
private function ReadTestFile($sFileToTest, $sFolderName)
{
$sCurrentPath = __DIR__;
return file_get_contents($sCurrentPath.DIRECTORY_SEPARATOR
.$sFolderName.DIRECTORY_SEPARATOR
.$sFileToTest);
}
private function RemoveNewLines($sText)
{
$sText = str_replace("\r\n", "\n", $sText);
$sText = str_replace("\r", "\n", $sText);
$sText = str_replace("\n", '', $sText);
return $sText;
}
public function DoSanitizeProvider() public function DoSanitizeProvider()
{ {
return array( return array(
array( array(
'utf-8_wrong_character_email_truncated.txt', 'scripts.html',
), ),
); );
} }
/** /**
* @dataProvider WhiteListProvider * @dataProvider WhiteListProvider
* *
@@ -146,13 +126,11 @@ class HTMLDOMSanitizerTest extends ItopTestCase
$aTestCaseArray = array(); $aTestCaseArray = array();
$sInputText = $this->ReadTestFile('whitelist_test.html', self::INPUT_DIRECTORY); $sInputText = $this->ReadTestFile('whitelist_test.html', self::INPUT_DIRECTORY);
foreach ($aTagsWhiteList as $sTag => $aTagAttributes) foreach ($aTagsWhiteList as $sTag => $aTagAttributes) {
{
$sTestCaseText = $sInputText; $sTestCaseText = $sInputText;
$sStartTag = "<$sTag"; $sStartTag = "<$sTag";
$iAttrCounter = 0; $iAttrCounter = 0;
foreach ($aTagAttributes as $sTagAttribute) foreach ($aTagAttributes as $sTagAttribute) {
{
$sStartTag .= $this->GetTagAttributeValue($sTagAttribute, $iAttrCounter); $sStartTag .= $this->GetTagAttributeValue($sTagAttribute, $iAttrCounter);
$iAttrCounter++; $iAttrCounter++;
} }
@@ -168,41 +146,6 @@ class HTMLDOMSanitizerTest extends ItopTestCase
return $aTestCaseArray; return $aTestCaseArray;
} }
/**
* Generates an appropriate value for the given attribute, or use the counter if needed.
* This is necessary as most of the attributes with empty or inappropriate values (like a numeric for a href) are removed by the parser
*
* @param string $sTagAttribute
* @param int $iAttributeCounter
*
* @return string attribute value
*/
private function GetTagAttributeValue($sTagAttribute, $iAttributeCounter)
{
$sTagAttrValue = ' '.$sTagAttribute.'="';
if (in_array($sTagAttribute, array('href', 'src')))
{
return $sTagAttrValue.'http://www.combodo.com"';
}
if ($sTagAttribute === 'style')
{
return $sTagAttrValue.'color: black"';
}
return $sTagAttrValue.$iAttributeCounter.'"';
}
private function IsClosingTag($sTag)
{
if (in_array($sTag, array('br', 'img', 'hr')))
{
return false;
}
return true;
}
/** /**
* @dataProvider RemoveBlackListedTagContentProvider * @dataProvider RemoveBlackListedTagContentProvider
*/ */

View File

@@ -0,0 +1,53 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Core\Sanitizer;
use SvgDOMSanitizer;
require_once __DIR__.'/AbstractDOMSanitizerTest.php';
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class SvgDOMSanitizerTest extends AbstractDOMSanitizerTest
{
/**
* @dataProvider DoSanitizeProvider
*
* @param string $sFileToTest filename
*/
public function testDoSanitize($sFileToTest)
{
$sInputHtml = $this->ReadTestFile($sFileToTest, self::INPUT_DIRECTORY);
$sOutputHtml = $this->ReadTestFile($sFileToTest, self::OUTPUT_DIRECTORY);
$sOutputHtml = $this->RemoveNewLines($sOutputHtml);
$oSanitizer = new SvgDOMSanitizer();
$sRes = $oSanitizer->DoSanitize($sInputHtml);
// Removing newlines as the parser gives different results depending on the PHP version
// Didn't manage to get it right :
// - no php.ini difference
// - playing with the parser preserveWhitespace/formatOutput parser options didn't help
// So we're removing new lines on both sides :/
$sOutputHtml = $this->RemoveNewLines($sOutputHtml);
$sRes = $this->RemoveNewLines($sRes);
$this->debug($sRes);
$this->assertEquals($sOutputHtml, $sRes);
}
public function DoSanitizeProvider()
{
return array(
array(
'scripts.svg',
),
);
}
}

View File

@@ -0,0 +1,7 @@
<h1>Test with lots of JS scripts to filter !</h1>
<p><img src="http://toto.invalid/" onerror="alert('hello world !');"></p>
<script>
alert("hello world !");
</script>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" baseProfile="full" onload="alert('hello world !');">
<rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)"/>
<script type="text/javascript">
alert("XSS");
</script>
</svg>

After

Width:  |  Height:  |  Size: 418 B

View File

@@ -0,0 +1,3 @@
<h1>Test with lots of JS scripts to filter !</h1>
<p><img src="http://toto.invalid/"></p>

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" baseProfile="full"><rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)"/></svg>

After

Width:  |  Height:  |  Size: 305 B

View File

@@ -34,7 +34,10 @@ class DictionariesConsistencyTest extends ItopTestCase
'da' => array('DA DA', 'Danish', 'Dansk'), 'da' => array('DA DA', 'Danish', 'Dansk'),
'de' => array('DE DE', 'German', 'Deutsch'), 'de' => array('DE DE', 'German', 'Deutsch'),
'en' => array('EN US', 'English', 'English'), 'en' => array('EN US', 'English', 'English'),
'es_cr' => array('ES CR', 'Spanish', 'Español, Castellano'), 'es_cr' => array('ES CR', 'Spanish', array(
'Español, Castellaño', // old value
'Español, Castellano', // new value since N°3635
)),
'fr' => array('FR FR', 'French', 'Français'), 'fr' => array('FR FR', 'French', 'Français'),
'hu' => array('HU HU', 'Hungarian', 'Magyar'), 'hu' => array('HU HU', 'Hungarian', 'Magyar'),
'it' => array('IT IT', 'Italian', 'Italiano'), 'it' => array('IT IT', 'Italian', 'Italiano'),
@@ -57,7 +60,7 @@ class DictionariesConsistencyTest extends ItopTestCase
static::fail("Unknown prefix '$sLangPrefix' for dictionary file '$sDictFile'"); static::fail("Unknown prefix '$sLangPrefix' for dictionary file '$sDictFile'");
} }
[$sExpectedLanguageCode, $sExpectedEnglishLanguageDesc, $sExpectedLocalizedLanguageDesc] = $aPrefixToLanguageData[$sLangPrefix]; [$sExpectedLanguageCode, $sExpectedEnglishLanguageDesc, $aExpectedLocalizedLanguageDesc] = $aPrefixToLanguageData[$sLangPrefix];
$sDictPHP = file_get_contents($sDictFile); $sDictPHP = file_get_contents($sDictFile);
$iCount = preg_match_all("@Dict::Add\('(.*)'\s*,\s*'(.*)'\s*,\s*'(.*)'@", $sDictPHP, $aMatches); $iCount = preg_match_all("@Dict::Add\('(.*)'\s*,\s*'(.*)'\s*,\s*'(.*)'@", $sDictPHP, $aMatches);
@@ -76,8 +79,12 @@ class DictionariesConsistencyTest extends ItopTestCase
static::assertSame($sExpectedEnglishLanguageDesc, $sEnglishLanguageDesc, static::assertSame($sExpectedEnglishLanguageDesc, $sEnglishLanguageDesc,
"Unexpected language description (english) for Dict::Add in dictionary $sDictFile"); "Unexpected language description (english) for Dict::Add in dictionary $sDictFile");
} }
foreach ($aMatches[3] as $sLocalizedLanguageDesc) { foreach ($aMatches[3] as $sLocalizedLanguageDesc)
static::assertSame($sExpectedLocalizedLanguageDesc, $sLocalizedLanguageDesc, {
if (false === is_array($aExpectedLocalizedLanguageDesc)) {
$aExpectedLocalizedLanguageDesc = array($aExpectedLocalizedLanguageDesc);
}
static::assertContains($sLocalizedLanguageDesc,$aExpectedLocalizedLanguageDesc,
"Unexpected language description for Dict::Add in dictionary $sDictFile"); "Unexpected language description for Dict::Add in dictionary $sDictFile");
} }
} }