Passed
Push — master ( 0aaddc...edae55 )
by Tim
01:50
created

DOMDocumentFactory::fromString()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 44
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 11
Bugs 0 Features 1
Metric Value
eloc 25
c 11
b 0
f 1
dl 0
loc 44
rs 9.2088
cc 5
nc 6
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\XML;
6
7
use DOMDocument;
8
use SimpleSAML\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
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-negative-int at position 0 could not be parsed: Unknown type name 'non-negative-int' at position 0 in non-negative-int.
Loading history...
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;
0 ignored issues
show
Bug introduced by
The constant SimpleSAML\XML\LIBXML_NO_XXE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
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
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-negative-int at position 0 could not be parsed: Unknown type name 'non-negative-int' at position 0 in non-negative-int.
Loading history...
88
     *
89
     * @return \DOMDocument
90
     */
91
    public static function fromFile(
92
        string $file,
93
        ?string $schemaFile = null,
0 ignored issues
show
Unused Code introduced by
The parameter $schemaFile is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

93
        /** @scrutinizer ignore-unused */ ?string $schemaFile = null,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
94
        int $options = self::DEFAULT_OPTIONS,
95
    ): DOMDocument {
96
        error_clear_last();
97
        $xml = @file_get_contents($file);
98
        if ($xml === false) {
99
            $e = error_get_last();
100
            $error = $e['message'] ?? "Check that the file exists and can be read.";
101
102
            throw new IOException("File '$file' was not loaded;  $error");
103
        }
104
105
        Assert::notWhitespaceOnly($xml, sprintf('File "%s" does not have content', $file), RuntimeException::class);
106
        return (func_num_args() < 2) ? static::fromString($xml) : static::fromString($xml, $options);
107
    }
108
109
110
    /**
111
     * @param string $version
112
     * @param string $encoding
113
     * @return \DOMDocument
114
     */
115
    public static function create(string $version = '1.0', string $encoding = 'UTF-8'): DOMDocument
116
    {
117
        return new DOMDocument($version, $encoding);
118
    }
119
}
120