169 lines
4.8 KiB
PHP
169 lines
4.8 KiB
PHP
|
<?php
|
||
|
namespace enshrined\svgSanitize\ElementReference;
|
||
|
|
||
|
use enshrined\svgSanitize\data\XPath;
|
||
|
use enshrined\svgSanitize\Exceptions\NestingException;
|
||
|
use enshrined\svgSanitize\Helper;
|
||
|
|
||
|
class Resolver
|
||
|
{
|
||
|
/**
|
||
|
* @var XPath
|
||
|
*/
|
||
|
protected $xPath;
|
||
|
|
||
|
/**
|
||
|
* @var Subject[]
|
||
|
*/
|
||
|
protected $subjects = [];
|
||
|
|
||
|
/**
|
||
|
* @var array DOMElement[]
|
||
|
*/
|
||
|
protected $elementsToRemove = [];
|
||
|
|
||
|
/**
|
||
|
* @var int
|
||
|
*/
|
||
|
protected $useNestingLimit;
|
||
|
|
||
|
public function __construct(XPath $xPath, $useNestingLimit)
|
||
|
{
|
||
|
$this->xPath = $xPath;
|
||
|
$this->useNestingLimit = $useNestingLimit;
|
||
|
}
|
||
|
|
||
|
public function collect()
|
||
|
{
|
||
|
$this->collectIdentifiedElements();
|
||
|
$this->processReferences();
|
||
|
$this->determineInvalidSubjects();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Resolves one subject by element.
|
||
|
*
|
||
|
* @param \DOMElement $element
|
||
|
* @param bool $considerChildren Whether to search in Subject's children as well
|
||
|
* @return Subject|null
|
||
|
*/
|
||
|
public function findByElement(\DOMElement $element, $considerChildren = false)
|
||
|
{
|
||
|
foreach ($this->subjects as $subject) {
|
||
|
if (
|
||
|
$element === $subject->getElement()
|
||
|
|| $considerChildren && Helper::isElementContainedIn($element, $subject->getElement())
|
||
|
) {
|
||
|
return $subject;
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Resolves subjects (plural!) by element id - in theory malformed
|
||
|
* DOM might have same ids assigned to different elements and leaving
|
||
|
* it to client/browser implementation which element to actually use.
|
||
|
*
|
||
|
* @param string $elementId
|
||
|
* @return Subject[]
|
||
|
*/
|
||
|
public function findByElementId($elementId)
|
||
|
{
|
||
|
return array_filter(
|
||
|
$this->subjects,
|
||
|
function (Subject $subject) use ($elementId) {
|
||
|
return $elementId === $subject->getElementId();
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Collects elements having `id` attribute (those that can be referenced).
|
||
|
*/
|
||
|
protected function collectIdentifiedElements()
|
||
|
{
|
||
|
/** @var \DOMNodeList|\DOMElement[] $elements */
|
||
|
$elements = $this->xPath->query('//*[@id]');
|
||
|
foreach ($elements as $element) {
|
||
|
$this->subjects[$element->getAttribute('id')] = new Subject($element, $this->useNestingLimit);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Processes references from and to elements having `id` attribute concerning
|
||
|
* their occurrence in `<use ... xlink:href="#identifier">` statements.
|
||
|
*/
|
||
|
protected function processReferences()
|
||
|
{
|
||
|
$useNodeName = $this->xPath->createNodeName('use');
|
||
|
foreach ($this->subjects as $subject) {
|
||
|
$useElements = $this->xPath->query(
|
||
|
$useNodeName . '[@href or @xlink:href]',
|
||
|
$subject->getElement()
|
||
|
);
|
||
|
|
||
|
/** @var \DOMElement $useElement */
|
||
|
foreach ($useElements as $useElement) {
|
||
|
$useId = Helper::extractIdReferenceFromHref(
|
||
|
Helper::getElementHref($useElement)
|
||
|
);
|
||
|
if ($useId === null || !isset($this->subjects[$useId])) {
|
||
|
continue;
|
||
|
}
|
||
|
$subject->addUse($this->subjects[$useId]);
|
||
|
$this->subjects[$useId]->addUsedIn($subject);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Determines and tags infinite loops.
|
||
|
*/
|
||
|
protected function determineInvalidSubjects()
|
||
|
{
|
||
|
foreach ($this->subjects as $subject) {
|
||
|
|
||
|
if (in_array($subject->getElement(), $this->elementsToRemove)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$useId = Helper::extractIdReferenceFromHref(
|
||
|
Helper::getElementHref($subject->getElement())
|
||
|
);
|
||
|
|
||
|
try {
|
||
|
if ($useId === $subject->getElementId()) {
|
||
|
$this->markSubjectAsInvalid($subject);
|
||
|
} elseif ($subject->hasInfiniteLoop()) {
|
||
|
$this->markSubjectAsInvalid($subject);
|
||
|
}
|
||
|
} catch (NestingException $e) {
|
||
|
$this->elementsToRemove[] = $e->getElement();
|
||
|
$this->markSubjectAsInvalid($subject);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get all the elements that caused a nesting exception.
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
public function getElementsToRemove() {
|
||
|
return $this->elementsToRemove;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* The Subject is invalid for some reason, therefore we should
|
||
|
* remove it and all it's child usages.
|
||
|
*
|
||
|
* @param Subject $subject
|
||
|
*/
|
||
|
protected function markSubjectAsInvalid(Subject $subject) {
|
||
|
$this->elementsToRemove = array_merge(
|
||
|
$this->elementsToRemove,
|
||
|
$subject->clearInternalAndGetAffectedElements()
|
||
|
);
|
||
|
}
|
||
|
}
|