Passed
Push — master ( a3bed1...1f331d )
by Tim
01:52
created

DOMDocumentFactory::fromFile()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 8
c 2
b 0
f 0
nc 2
nop 1
dl 0
loc 17
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\XML;
6
7
use DOMDocument;
8
use InvalidArgumentException;
9
use RuntimeException;
10
use SimpleSAML\Assert\Assert;
11
use SimpleSAML\XML\Exception\IOException;
12
use SimpleSAML\XML\Exception\UnparseableXMLException;
13
14
use function defined;
15
use function file_get_contents;
16
use function is_file;
17
use function is_readable;
18
use function libxml_clear_errors;
19
use function libxml_disable_entity_loader;
20
use function libxml_get_last_error;
21
use function libxml_use_internal_errors;
22
use function sprintf;
23
24
/**
25
 * @package simplesamlphp/xml-common
26
 */
27
final class DOMDocumentFactory
28
{
29
    /**
30
     * @param non-empty-string $xml
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
31
     *
32
     * @return \DOMDocument
33
     */
34
    public static function fromString(string $xml): DOMDocument
35
    {
36
        Assert::notWhitespaceOnly($xml);
37
38
        $internalErrors = libxml_use_internal_errors(true);
39
        libxml_clear_errors();
40
41
        $domDocument = self::create();
42
        $options = LIBXML_DTDLOAD | LIBXML_DTDATTR | LIBXML_NONET | LIBXML_PARSEHUGE | LIBXML_NSCLEAN;
43
        if (defined('LIBXML_COMPACT')) {
44
            $options |= LIBXML_COMPACT;
45
        }
46
47
        $loaded = $domDocument->loadXML($xml, $options);
48
49
        libxml_use_internal_errors($internalErrors);
50
51
        if (!$loaded) {
52
            $error = libxml_get_last_error();
53
            libxml_clear_errors();
54
55
            throw new UnparseableXMLException($error);
56
        }
57
58
        libxml_clear_errors();
59
60
        foreach ($domDocument->childNodes as $child) {
61
            if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
62
                throw new RuntimeException(
63
                    'Dangerous XML detected, DOCTYPE nodes are not allowed in the XML body',
64
                );
65
            }
66
        }
67
68
        return $domDocument;
69
    }
70
71
72
    /**
73
     * @param string $file
74
     *
75
     * @return \DOMDocument
76
     */
77
    public static function fromFile(string $file): DOMDocument
78
    {
79
        // libxml_disable_entity_loader(true) disables \DOMDocument::load() method
80
        // so we need to read the content and use \DOMDocument::loadXML()
81
        error_clear_last();
82
        $xml = @file_get_contents($file);
83
        if ($xml === false) {
84
            /** @psalm-var array $e */
85
            $e = error_get_last();
86
            $error = $e['message'] ?: "Check that the file exists and can be read.";
87
88
            throw new IOException("File '$file' was not loaded;  $error");
89
        }
90
91
        Assert::notWhitespaceOnly($xml, sprintf('File "%s" does not have content', $file), RuntimeException::class);
92
        /** @psalm-var non-empty-string $xml */
93
        return static::fromString($xml);
94
    }
95
96
97
    /**
98
     * @param ?string $version
99
     * @param ?string $encoding
100
     * @return \DOMDocument
101
     */
102
    public static function create(?string $version = null, ?string $encoding = null): DOMDocument
103
    {
104
        return new DOMDocument($version ?? '1.0', $encoding ?? '');
105
    }
106
}
107