带有PhpWord的隐式HTML:错误-DOMDocument :: loadXML():在Entity中未定义p上的命名空间前缀o

kya*_*kya 5 php laravel phpword summernote

我正在尝试隐秘用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'));
Run Code Online (Sandbox Code Playgroud)

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

<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
Run Code Online (Sandbox Code Playgroud)

我收到以下错误:

ErrorException(E_WARNING)DOMDocument :: loadXML():在实体上未定义p上的命名空间前缀o,第1行:

Joh*_*han 8

问题

解析器抱怨您的文本在element标签中包含名称空间,更具体地说是标签<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();
Run Code Online (Sandbox Code Playgroud)

分析

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

可能会DOMXPath 使用名称空间来提供,以便PHPWord可以利用它。不幸的DOMXPath ,API在API中不是公开的,因此是不可能的。

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

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

根据分析,解决方案是删除前缀,然后我们必须预先解析代码。由于PHPWord正在使用DOMDocument,因此我们也可以使用它,并确保我们不需要安装任何(额外)依赖项。

PHPWord正在使用解析HTML loadXML,这是抱怨格式的函数。这种方法有可能抑制错误消息,这在两种解决方案中都必须这样做。这是通过将附加参数传递loadXMLand loadHTML函数来完成的。

解决方案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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

$xml = '<o:p>Foo <o:b>Bar</o:b></o:p>';
print TagPrefixFixer::Clean($xml);
Run Code Online (Sandbox Code Playgroud)

输出量

$xml = '<o:p>Foo <o:b>Bar</o:b></o:p>';
print TagPrefixFixer::Clean($xml);
Run Code Online (Sandbox Code Playgroud)

解决方案2:预解析为HTML

我注意到,如果你使用loadHTML的不是loadXMLPHPWord使用它会在加载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();
}
Run Code Online (Sandbox Code Playgroud)

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

$html = '<o:p>Foo <o:b>Bar</o:b></o:p>';
print cleanHTML($html);
Run Code Online (Sandbox Code Playgroud)

输出量

<?xml version="1.0"?>
<p>Foo <b>Bar</b></p>
Run Code Online (Sandbox Code Playgroud)

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

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

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();
}
Run Code Online (Sandbox Code Playgroud)

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

$html = '<o:p>Foo <o:b>Bar</o:b></o:p>';
print cleanHTML($html);
Run Code Online (Sandbox Code Playgroud)

输出量

<p>Foo <b>Bar</b></p>
Run Code Online (Sandbox Code Playgroud)

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

可选解决方案(不建议使用)

在先前的答复中,这些作为(可选)解决方案提出,但是为了解决问题,我将在下面将它们放在此处。请记住,不建议使用这些方法,应谨慎使用。

关闭警告

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

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;
}
Run Code Online (Sandbox Code Playgroud)

设置来自默认报告级别

使用正则表达式

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

正则表达式

$xml = '<o:p>Foo <o:b>Bar</o:b></o:p>';
print addNamespaces($xml);
Run Code Online (Sandbox Code Playgroud)

范例

<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>
Run Code Online (Sandbox Code Playgroud)