首页 > 解决方案 > 带有 PhpWord 的隐蔽 HTML:错误 - DOMDocument::loadXML(): 实体中未定义 p 上的命名空间前缀 o

问题描述

我正在尝试隐藏使用 Php 字格式的 HTML。

我用summernote 创建了一个html 表单。Summernote 允许用户格式化文本。此文本使用 html 标记保存到数据库中。

接下来使用phpWord,我想将捕获的信息输出到一个word文档中。请看下面的代码:

$rational = DB::table('rationals')->where('qualificationheader_id',$qualId)->value('rational');

 $wordTest = new \PhpOffice\PhpWord\PhpWord();
        $newSection = $wordTest->addSection();
        $newSection->getStyle()->setPageNumberingStart(1);


    \PhpOffice\PhpWord\Shared\Html::addHtml($newSection,$rational);
    $footer = $newSection->addFooter();
    $footer->addText($curriculum->curriculum_code.'-'.$curriculum->curriculum_title);



    $objectWriter = \PhpOffice\PhpWord\IOFactory::createWriter($wordTest,'Word2007');
    try {
        $objectWriter->save(storage_path($curriculum->curriculum_code.'-'.$curriculum->curriculum_title.'.docx'));
    } catch (Exception $e) {
    }

    return response()->download(storage_path($curriculum->curriculum_code.'-'.$curriculum->curriculum_title.'.docx'));

保存在数据库中的文本如下所示:

<p class="MsoNormal"><span lang="EN-GB" style="background-image: initial; background-position: initial; background-size: initial; background-repeat: initial; background-attachment: initial; background-origin: initial; background-clip: initial;"><span style="font-family: Arial;">The want for this qualification originated from the energy crisis in
South Africa in 2008 together with the fact that no existing qualifications
currently focuses on energy efficiency as one of the primary solutions.  </span><span style="font-family: Arial;">The fact that energy supply remains under
severe pressure demands the development of skills sets that can deliver the
necessary solutions.</span><span style="font-family: Arial;">  </span><o:p></o:p></span></p><p class="MsoNormal"><span lang="EN-GB" style="background-image: initial; background-position: initial; background-size: initial; background-repeat: initial; background-attachment: initial; background-origin: initial; background-clip: initial; font-family: Arial;">This qualification addresses the need from Industry to acquire credible
and certified professionals with specialised skill sets in the energy
efficiency field. The need for this skill set has been confirmed as a global
requirement in few of the International commitment to the reduction of carbon

我收到以下错误:

ErrorException (E_WARNING) DOMDocument::loadXML(): Namespace prefix o on p is not defined in Entity, line: 1

标签: phplaravelsummernotephpword

解决方案


问题

解析器抱怨您的文本在元素标签中包含命名空间,更具体地说是标签上的前缀<o:p>(前缀在哪里o:)。它似乎是Word 的某种格式

重现问题

为了重现这个问题,我不得不挖掘一下,因为抛出异常的不是 PHPWord,而是DOMDocumentPHPWord 正在使用。下面的代码使用了与 PHPWord相同的解析方法,并且应该输出关于代码的所有警告和通知。

# Make sure to display all errors
ini_set("display_errors", "1");
error_reporting(E_ALL);

$html = '<o:p>Foo <o:b>Bar</o:b></o:p>';

# Set up and parse the code
$doc = new DOMDocument();
$doc->loadXML($html); # This is the line that's causing the warning.
# Print it back
echo $doc->saveXML();

分析

对于格式良好的 HTML 结构,可以在声明中包含命名空间,从而告诉解析器这些前缀实际上是什么。但由于它似乎只是要被解析的 HTML 代码的一部分,所以这是不可能的。

可以DOMXPath 使用 namespace来提供,以便PHPWord可以使用它。不幸的DOMXPath 是,API 中不公开,因此不可能。

相反,似乎最好的方法是从标签中去除前缀​​,并使警告消失。

编辑 2018-10-04:我已经发现了一种将前缀保留在标签中并且仍然使错误消失的方法,但是执行不是最佳的。如果有人可以提出更好的解决方案,请随时编辑我的帖子或发表评论。

解决方案

根据分析,解决方案是去掉前缀,反过来我们必须对代码进行预解析。由于 PHPWord 正在使用DOMDocument,我们也可以使用它,并确保我们不需要安装任何(额外的)依赖项。

PHPWord 正在用 解析 HTML loadXML,这是抱怨格式的函数。在这种方法中可以抑制错误消息,我们将在两种解决方案中都必须这样做。这是通过向and函数传递一个附加参数来完成的。loadXMLloadHTML

解决方案 1:预解析为 XML 并删除前缀

第一种方法将 html 代码解析为 XML 并递归地遍历树并删除标记名称上出现的任何前缀。

我创建了一个应该解决这个问题的类。

class TagPrefixFixer {

    /**
      * @desc Removes all prefixes from tags
      * @param string $xml The XML code to replace against.
      * @return string The XML code with no prefixes in the tags.
    */
    public static function Clean(string $xml) {
        $doc = new DOMDocument();
        /* Load the XML */
        $doc->loadXML($xml,
            LIBXML_HTML_NOIMPLIED | # Make sure no extra BODY
            LIBXML_HTML_NODEFDTD |  # or DOCTYPE is created
            LIBXML_NOERROR |        # Suppress any errors
            LIBXML_NOWARNING        # or warnings about prefixes.
        );
        /* Run the code */
        self::removeTagPrefixes($doc);
        /* Return only the XML */
        return $doc->saveXML();
    }

    private static function removeTagPrefixes(DOMNode $domNode) {
        /* Iterate over each child */
        foreach ($domNode->childNodes as $node) {
            /* Make sure the element is renameable and has children */
            if ($node->nodeType === 1) {
                /* Iterate recursively over the children.
                 * This is done before the renaming on purpose.
                 * If we rename this element, then the children, the element
                 * would need to be moved a lot more times due to how 
                 * renameNode works. */
                if($node->hasChildNodes()) {
                    self::removeTagPrefixes($node);
                }
                /* Check if the tag contains a ':' */
                if (strpos($node->tagName, ':') !== false) {
                    print $node->tagName;
                    /* Get the last part of the tag name */
                    $parts = explode(':', $node->tagName);
                    $newTagName = end($parts);
                    /* Change the name of the tag */
                    self::renameNode($node, $newTagName);
                }
            }
        }
    }

    private static function renameNode($node, $newName) {
        /* Create a new node with the new name */
        $newNode = $node->ownerDocument->createElement($newName);
        /* Copy over every attribute from the old node to the new one */
        foreach ($node->attributes as $attribute) {
            $newNode->setAttribute($attribute->nodeName, $attribute->nodeValue);
        }
        /* Copy over every child node to the new node */
        while ($node->firstChild) {
            $newNode->appendChild($node->firstChild);
        }
        /* Replace the old node with the new one */
        $node->parentNode->replaceChild($newNode, $node);
    }
}

要使用代码,只需调用TagPrefixFixer::Clean函数。

$xml = '<o:p>Foo <o:b>Bar</o:b></o:p>';
print TagPrefixFixer::Clean($xml);

输出

<?xml version="1.0"?>
<p>Foo <b>Bar</b></p>

解决方案 2:预解析为 HTML

我注意到,如果您使用loadHTML而不是loadXML使用PHPWord,它将在将 HTML 加载到类中时自行删除前缀。

这段代码明显更短。

function cleanHTML($html) {
    $doc = new DOMDocument();
    /* Load the HTML */
    $doc->loadHTML($html,
            LIBXML_HTML_NOIMPLIED | # Make sure no extra BODY
            LIBXML_HTML_NODEFDTD |  # or DOCTYPE is created
            LIBXML_NOERROR |        # Suppress any errors
            LIBXML_NOWARNING        # or warnings about prefixes.
    );
    /* Immediately save the HTML and return it. */
    return $doc->saveHTML();
}

要使用此代码,只需调用该cleanHTML函数

$html = '<o:p>Foo <o:b>Bar</o:b></o:p>';
print cleanHTML($html);

输出

<p>Foo <b>Bar</b></p>

解决方案 3:保留前缀并添加命名空间

在将数据输入解析器之前,我尝试使用给定的Microsoft Office 命名空间包装代码,这也将解决问题。具有讽刺意味的是,我还没有找到一种在DOMDocument不实际引发原始警告的情况下使用解析器添加命名空间的方法。所以 - 这个解决方案的执行有点笨拙,我不建议使用它,而是自己构建。但你明白了:

function addNamespaces($xml) {
    $root = '<w:wordDocument
        xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml"
        xmlns:wx="http://schemas.microsoft.com/office/word/2003/auxHint"
        xmlns:o="urn:schemas-microsoft-com:office:office">';
    $root .= $xml;
    $root .= '</w:wordDocument>';
    return $root;
}

要使用此代码,只需调用该addNamespaces函数

$xml = '<o:p>Foo <o:b>Bar</o:b></o:p>';
print addNamespaces($xml);

输出

<w:wordDocument
    xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml"
    xmlns:wx="http://schemas.microsoft.com/office/word/2003/auxHint"
    xmlns:o="urn:schemas-microsoft-com:office:office">
    <o:p>Foo <o:b>Bar</o:b></o:p>
</w:wordDocument>

然后可以将此代码提供给 PHPWord 函数addHtml,而不会引起任何警告。

可选解决方案(已弃用)

在之前的回复中,这些是作为(可选)解决方案提出的,但为了解决问题,我将让它们出现在下面。请记住,这些都不推荐,应谨慎使用。

关闭警告

由于它“只是”一个警告而不是致命的停止异常,因此您可以关闭警告。您可以通过在脚本顶部包含此代码来执行此操作。然而,这仍然会减慢您的应用程序,最好的方法始终是确保没有警告或错误。

// Show the default reporting except from warnings
error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED & ~E_WARNING);

这些设置源自默认报告级别

使用正则表达式

(可能)可以在将文本保存到数据库之前或在获取用于此函数的文本之后使用正则表达式删除(大多数)名称空间。由于它已经存储在数据库中,最好在从数据库中获取它之后使用下面的代码。正则表达式可能会错过一些事件,或者在最坏的情况下会弄乱 HTML。

正则表达式

$text_after = preg_replace('/[a-zA-Z]+:([a-zA-Z]+[=>])/', '$1', $text_before);

示例

$text = '<o:p>Foo <o:b>Bar</o:b></o:p>';
$text = preg_replace('/[a-zA-Z]+:([a-zA-Z]+[=>])/', '$1', $text);
echo $text; // Outputs '<p>Foo <b>Bar</b></p>'

推荐阅读