DOMDocumentFactory::fromString()   A
last analyzed

Complexity

Conditions 5
Paths 6

Size

Total Lines 44
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 25
nc 6
nop 2
dl 0
loc 44
rs 9.2088
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\XML;
6
7
use DOMDocument;
8
use SimpleSAML\XML\Assert\Assert;
9
use SimpleSAML\XML\Exception\IOException;
10
use SimpleSAML\XML\Exception\RuntimeException;
11
use SimpleSAML\XML\Exception\UnparseableXMLException;
12
13
use function file_get_contents;
14
use function func_num_args;
15
use function libxml_clear_errors;
16
use function libxml_set_external_entity_loader;
17
use function libxml_use_internal_errors;
18
use function sprintf;
19
20
/**
21
 * @package simplesamlphp/xml-common
22
 */
23
final class DOMDocumentFactory
24
{
25
    /**
26
     * @var non-negative-int
27
     * TODO: Add LIBXML_NO_XXE to the defaults when PHP 8.4.0 + libxml 2.13.0 become generally available
28
     */
29
    public const DEFAULT_OPTIONS = \LIBXML_COMPACT | \LIBXML_NONET | \LIBXML_NSCLEAN;
30
31
32
    /**
33
     * @param string $xml
34
     * @param non-negative-int $options
35
     *
36
     * @return \DOMDocument
37
     */
38
    public static function fromString(
39
        string $xml,
40
        int $options = self::DEFAULT_OPTIONS,
41
    ): DOMDocument {
42
        libxml_set_external_entity_loader(null);
43
        Assert::notWhitespaceOnly($xml);
44
        Assert::notRegex(
45
            $xml,
46
            '/<(\s*)!(\s*)DOCTYPE/',
47
            'Dangerous XML detected, DOCTYPE nodes are not allowed in the XML body',
48
            RuntimeException::class,
49
        );
50
51
        $internalErrors = libxml_use_internal_errors(true);
52
        libxml_clear_errors();
53
54
        // If LIBXML_NO_XXE is available and option not set
55
        if (func_num_args() === 1 && defined('LIBXML_NO_XXE')) {
56
            $options |= \LIBXML_NO_XXE;
57
        }
58
59
        $domDocument = self::create();
60
        $loaded = $domDocument->loadXML($xml, $options);
61
62
        libxml_use_internal_errors($internalErrors);
63
64
        if (!$loaded) {
65
            $error = libxml_get_last_error();
66
            libxml_clear_errors();
67
68
            throw new UnparseableXMLException($error);
69
        }
70
71
        libxml_clear_errors();
72
73
        foreach ($domDocument->childNodes as $child) {
74
            Assert::false(
75
                $child->nodeType === \XML_DOCUMENT_TYPE_NODE,
76
                'Dangerous XML detected, DOCTYPE nodes are not allowed in the XML body',
77
                RuntimeException::class,
78
            );
79
        }
80
81
        return $domDocument;
82
    }
83
84
85
    /**
86
     * @param string $file
87
     * @param non-negative-int $options
88
     *
89
     * @return \DOMDocument
90
     */
91
    public static function fromFile(
92
        string $file,
93
        int $options = self::DEFAULT_OPTIONS,
94
    ): DOMDocument {
95
        error_clear_last();
96
        $xml = @file_get_contents($file);
97
        if ($xml === false) {
98
            $e = error_get_last();
99
            $error = $e['message'] ?? "Check that the file exists and can be read.";
100
101
            throw new IOException("File '$file' was not loaded;  $error");
102
        }
103
104
        Assert::notWhitespaceOnly($xml, sprintf('File "%s" does not have content', $file), RuntimeException::class);
105
        return (func_num_args() < 2) ? static::fromString($xml) : static::fromString($xml, $options);
106
    }
107
108
109
    /**
110
     * @param string $version
111
     * @param string $encoding
112
     * @return \DOMDocument
113
     */
114
    public static function create(string $version = '1.0', string $encoding = 'UTF-8'): DOMDocument
115
    {
116
        return new DOMDocument($version, $encoding);
117
    }
118
}
119