如何从用户输入中删除不需要的HTML标记,但使用DOMDocument

时间:2016-09-13 10:41:04

标签: php html dom domdocument

我在S3中有大约2百万个存储的HTML页面,其中包含各种HTML。我试图仅从那些存储的页面中提取内容,但我希望保留具有某些约束的HTML结构。此HTML是用户提供的所有输入,应被视为不安全。因此,出于显示目的,我希望仅保留一些HTML属性和属性值约束的HTML标记,但仍保留所有正确编码的文本内容,甚至是不允许的标记。

例如,我只想允许<p><h1><h2><h3><ul>,{{1}等特定代码},<ol>等。但我也希望在不允许的标签之间保留任何文本并保持其结构。我还希望能够限制每个标记中的属性,或强制将某些属性应用于特定标记。

例如,在以下HTML ...

<li>

我希望结果是......

<div id="content">
  Some text...
  <p class="someclass">Hello <span style="color: purple;">PHP</span>!</p>
</div>

因此,删除了不需要的 Some text... <p>Hello PHP!</p> <div>代码,所有代码的不需要的属性,并且仍将文字保留在<span><div>内。

简单地使用<span>将不起作用。所以我尝试使用DOMDocuemnt执行以下操作。

strip_tags()

哪种方法适用于没有嵌套标签的简单情况,但是当HTML很复杂时显然会失败。

我无法在每个节点的子节点上递归调用此函数,因为如果我删除该节点,则会丢失所有进一步嵌套的子节点。即使我将节点删除推迟到递归之后,文本插入的顺序也变得棘手。因为我试图深入并返回所有有效节点,然后开始将无效子节点的值连接在一起,结果非常混乱。

例如,假设我想在以下HTML中允许$dom = new DOMDocument; $dom->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); foreach($dom->childNodes as $node) { if ($node->nodeName != "p") { // only allow paragraph tags $text = $node->nodeValue; $node->parentNode->nodeValue .= $text; $node->parentNode->removeChild($node); } } echo $dom->saveHTML(); <p>

<em>

但我不想允许<p>Hello <strong>there <em>PHP</em>!</strong></p> 。如果<strong>已嵌套<strong>,我的方法会让人感到困惑。因为我会得到类似......

<em>

这显然是错误的。我意识到整个<p>Hello there !<em>PHP</em></p> 是一个不好的方法。所以我开始挖掘其他方法,一次遍历整个树的一个节点。只是发现很难概括这个解决方案,以便每次都能很好地运作。

更新

使用nodeValuethe answer provided here的解决方案对我的用例没有帮助,因为前者不允许我控制属性,后者删除任何具有属性的标记。我不想删除任何具有属性的标记。我想明确允许某些标记,但仍然可以扩展控制HTML中可以保留/修改的属性。

2 个答案:

答案 0 :(得分:1)

为了概括解决方案,似乎需要将此问题分解为两个较小的步骤。

首先,走DOM树

为了找到一个可行的解决方案,我发现我需要有一种合理的方法来遍历DOM树中的每个节点并检查它,以确定它是应该按原样保存还是修改。

所以我使用了以下方法作为从DOMDocument扩展的简单生成器。

class HTMLFixer extends DOMDocument {
    public function walk(DOMNode $node, $skipParent = false) {
        if (!$skipParent) {
            yield $node;
        }
        if ($node->hasChildNodes()) {
            foreach ($node->childNodes as $n) {
                yield from $this->walk($n);
            }
        }
    }
}

这样做foreach($dom->walk($dom) as $node)之类的东西给了我一个简单的循环来遍历整个树。当然,由于yield from语法,这是一个仅限PHP 7的解决方案,但我对此没有问题。

其次,删除标签但保留文本

棘手的部分是弄清楚如何在循环内部进行修改时保留文本而不是标记。因此,在尝试了几种不同的方法后,我发现最简单的方法是构建一个要从循环内部删除的标记列表,然后使用DOMNode::insertBefore()删除它们以将文本节点附加到树中。这样,以后删除这些节点没有副作用。

所以我为stripTags的这个子类添加了另一个通用的DOMDocument方法。

public function stripTags(DOMNode $node) {
    $change = $remove = [];

    /* Walk the entire tree to build a list of things that need removed */
    foreach($this->walk($node) as $n) {
        if ($n instanceof DOMText || $n instanceof DOMDocument) {
            continue;
        }
        $this->stripAttributes($n); // strips all node attributes not allowed
        $this->forceAttributes($n); // forces any required attributes
        if (!in_array($n->nodeName, $this->allowedTags, true)) {
            // track the disallowed node for removal
            $remove[] = $n;
            // we take all of its child nodes for modification later
            foreach($n->childNodes as $child) {
                $change[] = [$child, $n];
            }
        }
    }

    /* Go through the list of changes first so we don't break the
       referential integrity of the tree */
    foreach($change as list($a, $b)) {
        $b->parentNode->insertBefore($a, $b);
    }

    /* Now we can safely remove the old nodes */
    foreach($remove as $a) {
        if ($a->parentNode) {
            $a->parentNode->removeChild($a);
        }
    }
}

这里的诀窍是因为我们在不允许的标签的子节点(即文本节点)上使用insertBefore将它们移动到父标签,我们可以轻松地破坏树(我们&#39;重新复制)。起初这让我很困惑,但是看看方法的工作方式,这是有道理的。推迟移动节点可确保当更深的节点是允许的节点时,我们不会中断parentNode引用,但其父节点不在允许的标签列表中。例如。 p>

完整解决方案

这是我提出的完整解决方案,以更一般地解决这个问题。我将在答案中加入,因为我在其他地方使用DOMDocument这样做时很难找到很多边缘情况。它允许您指定允许的标记,并删除所有其他标记。它还允许您指定允许哪些属性以及可以删除所有其他属性(甚至强制某些标记上的某些属性)。

class HTMLFixer extends DOMDocument {
    protected static $defaultAllowedTags = [
        'p',
        'h1',
        'h2',
        'h3',
        'h4',
        'h5',
        'h6',
        'pre',
        'code',
        'blockquote',
        'q',
        'strong',
        'em',
        'del',
        'img',
        'a',
        'table',
        'thead',
        'tbody',
        'tfoot',
        'tr',
        'th',
        'td',
        'ul',
        'ol',
        'li',
    ];
    protected static $defaultAllowedAttributes = [
        'a'   => ['href'],
        'img' => ['src'],
        'pre' => ['class'],
    ];
    protected static $defaultForceAttributes = [
        'a' => ['target' => '_blank'],
    ];

    protected $allowedTags       = [];
    protected $allowedAttributes = [];
    protected $forceAttributes   = [];

    public function __construct($version = null, $encoding = null, $allowedTags = [],
                                $allowedAttributes = [], $forceAttributes = []) {
        $this->setAllowedTags($allowedTags ?: static::$defaultAllowedTags);
        $this->setAllowedAttributes($allowedAttributes ?: static::$defaultAllowedAttributes);
        $this->setForceAttributes($forceAttributes ?: static::$defaultForceAttributes);
        parent::__construct($version, $encoding);
    }

    public function setAllowedTags(Array $tags) {
        $this->allowedTags = $tags;
    }

    public function setAllowedAttributes(Array $attributes) {
        $this->allowedAttributes = $attributes;
    }

    public function setForceAttributes(Array $attributes) {
        $this->forceAttributes = $attributes;
    }

    public function getAllowedTags() {
        return $this->allowedTags;
    }

    public function getAllowedAttributes() {
        return $this->allowedAttributes;
    }

    public function getForceAttributes() {
        return $this->forceAttributes;
    }

    public function saveHTML(DOMNode $node = null) {
        if (!$node) {
            $node = $this;
        }
        $this->stripTags($node);
        return parent::saveHTML($node);
    }

    protected function stripTags(DOMNode $node) {
        $change = $remove = [];
        foreach($this->walk($node) as $n) {
            if ($n instanceof DOMText || $n instanceof DOMDocument) {
                continue;
            }
            $this->stripAttributes($n);
            $this->forceAttributes($n);
            if (!in_array($n->nodeName, $this->allowedTags, true)) {
                $remove[] = $n;
                foreach($n->childNodes as $child) {
                    $change[] = [$child, $n];
                }
            }
        }
        foreach($change as list($a, $b)) {
            $b->parentNode->insertBefore($a, $b);
        }
        foreach($remove as $a) {
            if ($a->parentNode) {
                $a->parentNode->removeChild($a);
            }
        }
    }

    protected function stripAttributes(DOMNode $node) {
        $attributes = $node->attributes;
        $len = $attributes->length;
        for ($i = $len - 1; $i >= 0; $i--) {
            $attr = $attributes->item($i);
            if (!isset($this->allowedAttributes[$node->nodeName]) ||
                !in_array($attr->name, $this->allowedAttributes[$node->nodeName], true)) {
                $node->removeAttributeNode($attr);
            }
        }
    }

    protected function forceAttributes(DOMNode $node) {
        if (isset($this->forceAttributes[$node->nodeName])) {
            foreach ($this->forceAttributes[$node->nodeName] as $attribute => $value) {
                $node->setAttribute($attribute, $value);
            }
        }
    }

    protected function walk(DOMNode $node, $skipParent = false) {
        if (!$skipParent) {
            yield $node;
        }
        if ($node->hasChildNodes()) {
            foreach ($node->childNodes as $n) {
                yield from $this->walk($n);
            }
        }
    }
}

所以,如果我们有以下HTML

<div id="content">
  Some text...
  <p class="someclass">Hello <span style="color: purple;">P<em>H</em>P</span>!</p>
</div>

我们只想允许<p><em>

$html = <<<'HTML'
    <div id="content">
      Some text...
      <p class="someclass">Hello <span style="color: purple;">P<em>H</em>P</span>!</p>
    </div>
HTML;

$dom = new HTMLFixer(null, null, ['p', 'em']);
$dom->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);

echo $dom->saveHTML($dom);

我们得到这样的东西......

      Some text...
      <p>Hello P<em>H</em>P!</p>

由于您可以将此限制为DOM中的特定子树,因此解决方案可以进一步推广。

答案 1 :(得分:0)

您可以像这样使用strip_tags():

$html = '<div id="content">
  Some text...
  <p class="someclass">Hello <span style="color: purple;">PHP</span>!</p>
</div>';
$updatedHTML = strip_tags($text,"<p><h1><h2><h3><ul><ol><li>"); 
   //in second parameter we need to provide which html tag we need to retain.

您可以在此处获取更多信息:http://php.net/manual/en/function.strip-tags.php