public function SanitizeHtml($html)
{
if(empty($this->arHtmlTags))
$this->SetLevel(self::SECURE_LEVEL_HIGH);
$openTagsStack = array();
$isCode = false;
$seg = $this->splitHtml($html);
//process segments
$segCount = count($seg);
for($i=0; $i<$segCount; $i++)
{
if($seg[$i]['segType'] == 'text')
{
if (trim($seg[$i]['value']) && ($tp = array_search('table', $openTagsStack)) !== false)
{
$cellTags = array_intersect(array('td', 'th'), array_keys($this->arHtmlTags));
if ($cellTags && !array_intersect($cellTags, array_slice($openTagsStack, $tp+1)))
{
array_splice($seg, $i, 0, array(array('segType' => 'tag', 'value' => sprintf('<%s>', reset($cellTags)))));
$i--; $segCount++;
continue;
}
}
if ($this->bHtmlSpecChars)
{
$openTagsStackSize = count($openTagsStack);
$entQuotes = ($openTagsStackSize && $openTagsStack[$openTagsStackSize-1] === 'style' ? ENT_NOQUOTES : ENT_QUOTES);
$seg[$i]['value'] = htmlspecialchars(
$seg[$i]['value'],
$entQuotes,
LANG_CHARSET,
$this->bDoubleEncode
);
}
}
elseif(
$seg[$i]['segType'] == 'tag'
&& (
preg_match('/^$/', $seg[$i]['value'])
)
)
{
//Keep ms html comments https://stackoverflow.design/email/base/mso/
$seg[$i]['segType'] = 'text';
}
elseif($seg[$i]['segType'] == 'tag')
{
//find tag type (open/close), tag name, attributies
preg_match('#^$#si'.BX_UTF_PCRE_MODIFIER, $seg[$i]['value'], $matches);
$seg[$i]['tagType'] = !empty($matches[1]) ? 'close' : 'open';
$seg[$i]['tagName'] = mb_strtolower($matches[2] ?? '');
if(($seg[$i]['tagName']=='code') && ($seg[$i]['tagType']=='close'))
$isCode = false;
//if tag founded inside it is simple text
if($isCode)
{
$seg[$i]['segType'] = 'text';
$i--;
continue;
}
if($seg[$i]['tagType'] == 'open')
{
// if tag unallowed screen it, or erase
if(!array_key_exists($seg[$i]['tagName'], $this->arHtmlTags))
{
if($this->bDelSanitizedTags)
{
$seg[$i]['action'] = self::ACTION_DEL;
}
else
{
$seg[$i]['segType'] = 'text';
$i--;
continue;
}
}
//if allowed
else
{
if (in_array('table', $openTagsStack))
{
if ($openTagsStack[count($openTagsStack)-1] == 'table')
{
if (array_key_exists('tr', $this->arHtmlTags) && !in_array($seg[$i]['tagName'], array('thead', 'tfoot', 'tbody', 'tr')))
{
array_splice($seg, $i, 0, array(array('segType' => 'tag', 'tagType' => 'open', 'tagName' => 'tr', 'action' => self::ACTION_ADD)));
$i++; $segCount++;
$openTagsStack[] = 'tr';
}
}
if (in_array($openTagsStack[count($openTagsStack)-1], array('thead', 'tfoot', 'tbody')))
{
if (array_key_exists('tr', $this->arHtmlTags) && $seg[$i]['tagName'] != 'tr')
{
array_splice($seg, $i, 0, array(array('segType' => 'tag', 'tagType' => 'open', 'tagName' => 'tr', 'action' => self::ACTION_ADD)));
$i++; $segCount++;
$openTagsStack[] = 'tr';
}
}
if ($seg[$i]['tagName'] == 'tr')
{
for ($j = count($openTagsStack)-1; $j >= 0; $j--)
{
if (in_array($openTagsStack[$j], array('table', 'thead', 'tfoot', 'tbody')))
break;
array_splice($seg, $i, 0, array(array('segType' => 'tag', 'tagType' => 'close', 'tagName' => $openTagsStack[$j], 'action' => self::ACTION_ADD)));
$i++; $segCount++;
array_splice($openTagsStack, $j, 1);
}
}
if ($openTagsStack[count($openTagsStack)-1] == 'tr')
{
$cellTags = array_intersect(array('td', 'th'), array_keys($this->arHtmlTags));
if ($cellTags && !in_array($seg[$i]['tagName'], $cellTags))
{
array_splice($seg, $i, 0, array(array('segType' => 'tag', 'tagType' => 'open', 'tagName' => reset($cellTags), 'action' => self::ACTION_ADD)));
$i++; $segCount++;
$openTagsStack[] = 'td';
}
}
if (in_array($seg[$i]['tagName'], array('td', 'th')))
{
for ($j = count($openTagsStack)-1; $j >= 0; $j--)
{
if ($openTagsStack[$j] == 'tr')
break;
array_splice($seg, $i, 0, array(array('segType' => 'tag', 'tagType' => 'close', 'tagName' => $openTagsStack[$j], 'action' => self::ACTION_ADD)));
$i++; $segCount++;
array_splice($openTagsStack, $j, 1);
}
}
}
//Processing valid tables
//if find 'tr','td', etc...
if(array_key_exists($seg[$i]['tagName'], $this->arTableTags))
{
$this->CleanTable($seg, $openTagsStack, $i, false);
if(isset($seg[$i]['action']) && $seg[$i]['action'] == self::ACTION_DEL)
continue;
}
$seg[$i]['attr'] = $this->processAttributes(
(string)$matches[3], //attributes string
(string)$seg[$i]['tagName']
);
if($seg[$i]['tagName'] === 'code')
{
$isCode = true;
}
//if tag need close tag add it to stack opened tags
if(!in_array($seg[$i]['tagName'], $this->arNoClose)) //!count($this->arHtmlTags[$seg[$i]['tagName']]) || fix:
{
$openTagsStack[] = $seg[$i]['tagName'];
$seg[$i]['closeIndex'] = count($openTagsStack)-1;
}
}
}
//if closing tag
else
{ //if tag allowed
if(array_key_exists($seg[$i]['tagName'], $this->arHtmlTags) && (!count($this->arHtmlTags[$seg[$i]['tagName']]) || ($this->arHtmlTags[$seg[$i]['tagName']][count($this->arHtmlTags[$seg[$i]['tagName']])-1] != false)))
{
if($seg[$i]['tagName'] == 'code')
{
$isCode = false;
}
//if open tags stack is empty, or not include it's name lets screen/erase it
if((empty($openTagsStack)) || (!in_array($seg[$i]['tagName'], $openTagsStack)))
{
if($this->bDelSanitizedTags || $this->arNoClose)
{
$seg[$i]['action'] = self::ACTION_DEL;
}
else
{
$seg[$i]['segType'] = 'text';
$i--;
continue;
}
}
else
{
//if this tag don't match last from open tags stack , adding right close tag
$tagName = array_pop($openTagsStack);
if($seg[$i]['tagName'] != $tagName)
{
array_splice($seg, $i, 0, array(array('segType'=>'tag', 'tagType'=>'close', 'tagName'=>$tagName, 'action'=>self::ACTION_ADD)));
$segCount++;
}
}
}
//if tag unallowed erase it
else
{
if($this->bDelSanitizedTags)
{
$seg[$i]['action'] = self::ACTION_DEL;
}
else
{
$seg[$i]['segType'] = 'text';
$i--;
continue;
}
}
}
}
}
//close tags stayed in stack
foreach(array_reverse($openTagsStack) as $val)
array_push($seg, array('segType'=>'tag', 'tagType'=>'close', 'tagName'=>$val, 'action'=>self::ACTION_ADD));
//build filtered code and return it
$filteredHTML = '';
$flagDeleteContent = false;
foreach($seg as $segt)
{
if(($segt['action'] ?? '') != self::ACTION_DEL && !$flagDeleteContent)
{
if($segt['segType'] == 'text')
{
$filteredHTML .= $segt['value'];
}
elseif($segt['segType'] == 'tag')
{
if($segt['tagType'] == 'open')
{
$filteredHTML .= '<'.$segt['tagName'];
if(isset($segt['attr']) && is_array($segt['attr']))
foreach($segt['attr'] as $attr_key => $attr_val)
$filteredHTML .= ' '.$attr_key.'="'.$attr_val.'"';
if (count($this->arHtmlTags[$segt['tagName']]) && ($this->arHtmlTags[$segt['tagName']][count($this->arHtmlTags[$segt['tagName']])-1] == false))
$filteredHTML .= " /";
$filteredHTML .= '>';
}
elseif($segt['tagType'] == 'close')
$filteredHTML .= ''.$segt['tagName'].'>';
}
}
else
{
if(in_array($segt['tagName'], $this->delTagsWithContent))
{
$flagDeleteContent = $segt['tagType'] == 'open';
}
}
}
if(!$this->bHtmlSpecChars && $html != $filteredHTML)
{
$filteredHTML = $this->SanitizeHtml($filteredHTML);
}
return $filteredHTML;
}