Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.79% covered (success)
94.79%
91 / 96
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
Parser
94.79% covered (success)
94.79%
91 / 96
66.67% covered (warning)
66.67%
2 / 3
71.71
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parse
83.87% covered (warning)
83.87%
26 / 31
0.00% covered (danger)
0.00%
0 / 1
6.15
 makeElementForName
100.00% covered (success)
100.00%
64 / 64
100.00% covered (success)
100.00%
1 / 1
64
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Svg;
6
7use Phpdftk\Svg\Exception\InvalidSvgException;
8use Phpdftk\Svg\Shape\Circle;
9use Phpdftk\Svg\Shape\Ellipse;
10use Phpdftk\Svg\Shape\Line;
11use Phpdftk\Svg\Shape\Polygon;
12use Phpdftk\Svg\Shape\Polyline;
13use Phpdftk\Svg\Shape\Rect;
14use Phpdftk\Xml\Exception\InvalidXmlException;
15use Phpdftk\Xml\HardenedLoader;
16use Phpdftk\Xml\TreeWalker;
17
18/**
19 * Secure SVG-to-typed-tree parser. Consumes a string of SVG XML and
20 * returns an `SvgDocument`.
21 *
22 * Routing through {@see HardenedLoader} for libxml + {@see TreeWalker}
23 * for the DOM walk means this parser's only format-specific code is
24 * the root validation (must be `<svg>` in `SVG_NS`) and the
25 * `makeElementForName` typed-element factory. The security boundary
26 * (no entity substitution, no network fetches, no XInclude) is owned
27 * by `HardenedLoader` so SVG and MathML cannot drift.
28 *
29 * Unknown elements outside the implemented v1 subset are preserved as
30 * generic `Element` instances so sanitiser-style callers can inspect
31 * them.
32 */
33final class Parser
34{
35    /** SVG namespace URI per spec. */
36    public const string SVG_NS = 'http://www.w3.org/2000/svg';
37
38    public function __construct(
39        private readonly HardenedLoader $loader = new HardenedLoader(),
40        private readonly TreeWalker $walker = new TreeWalker(),
41    ) {}
42
43    public function parse(string $xml): SvgDocument
44    {
45        try {
46            $dom = $this->loader->load($xml);
47        } catch (InvalidXmlException $e) {
48            // Re-throw as the format-specific exception so consumers
49            // can keep a single catch block per parser. The original
50            // libxml message is preserved via the previous exception.
51            // The error text is reflavoured to mention "SVG" so the
52            // consumer's logs make the format clear.
53            $message = str_replace(
54                'parse XML',
55                'parse SVG XML',
56                $e->getMessage(),
57            );
58            throw new InvalidSvgException($message, 0, $e);
59        }
60
61        $root = $dom->documentElement;
62        if ($root === null) {
63            throw new InvalidSvgException('SVG document has no root element.');
64        }
65        if ($root->localName !== 'svg') {
66            throw new InvalidSvgException(sprintf(
67                'Expected <svg> root element, got <%s>.',
68                $root->localName,
69            ));
70        }
71        if ($root->namespaceURI !== null && $root->namespaceURI !== self::SVG_NS) {
72            throw new InvalidSvgException(sprintf(
73                'Root <svg> declared in unexpected namespace %s.',
74                $root->namespaceURI,
75            ));
76        }
77
78        $doc = new SvgDocument();
79        $this->walker->walk(
80            $root,
81            $doc,
82            createElement: fn(string $localName) => $this->makeElementForName($localName),
83            createText: fn(string $data) => new Text($data),
84            setAttribute: static fn(Element $el, string $name, string $value) => $el->setAttribute($name, $value),
85            appendChild: static fn(Element $parent, Node $child) => $parent->appendChild($child),
86        );
87        return $doc;
88    }
89
90    private function makeElementForName(string $localName): Element
91    {
92        return match ($localName) {
93            'rect' => new Rect(),
94            'circle' => new Circle(),
95            'ellipse' => new Ellipse(),
96            'line' => new Line(),
97            'polyline' => new Polyline(),
98            'polygon' => new Polygon(),
99            'g' => new Group(),
100            // SVG 2 §7.5 — a nested `<svg>` establishes a new viewport /
101            // user space. (The root `<svg>` is built as SvgDocument above.)
102            'svg' => new NestedSvg(),
103            'path' => new Path(),
104            'text' => new Text\TextElement(),
105            'tspan' => new Text\Tspan(),
106            'defs' => new Defs(),
107            'symbol' => new Symbol(),
108            'use' => new Use_(),
109            'clipPath' => new ClipPath(),
110            'mask' => new Mask(),
111            'image' => new Image(),
112            'linearGradient' => new Gradient\LinearGradient(),
113            'radialGradient' => new Gradient\RadialGradient(),
114            'stop' => new Gradient\Stop(),
115            'style' => new StyleElement(),
116            // SVG 2 §12.1.1 — anchor container; PDF maps to a link
117            // annotation when an href is present.
118            'a' => new A_(),
119            // SVG 2 §15.3 — accessibility metadata; never paints.
120            'title' => new Title(),
121            'desc' => new Desc(),
122            // SVG 2 §5.7 — conditional rendering container.
123            'switch' => new Switch_(),
124            // SVG 2 §11.6 — foreign-content placeholder.
125            'foreignObject' => new ForeignObject(),
126            // SVG 2 §11.6 — vertex-marker definition (arrowheads etc.)
127            'marker' => new Marker(),
128            // SVG 2 §13.3 — tiled fill pattern definition.
129            'pattern' => new Pattern(),
130            // SVG 2 Filter Effects §6.1 — filter graph definition.
131            'filter' => new Filter(),
132            // SVG 2 Filter Effects §15 — filter primitives that
133            // live inside `<filter>`. Each lifts to its own typed
134            // class for accessor convenience.
135            'feGaussianBlur' => new Filter\FeGaussianBlur(),
136            'feOffset' => new Filter\FeOffset(),
137            'feFlood' => new Filter\FeFlood(),
138            'feBlend' => new Filter\FeBlend(),
139            'feComposite' => new Filter\FeComposite(),
140            'feMorphology' => new Filter\FeMorphology(),
141            'feMerge' => new Filter\FeMerge(),
142            'feMergeNode' => new Filter\FeMergeNode(),
143            'feColorMatrix' => new Filter\FeColorMatrix(),
144            'feDropShadow' => new Filter\FeDropShadow(),
145            'feTurbulence' => new Filter\FeTurbulence(),
146            'feImage' => new Filter\FeImage(),
147            'feTile' => new Filter\FeTile(),
148            'feDisplacementMap' => new Filter\FeDisplacementMap(),
149            'feConvolveMatrix' => new Filter\FeConvolveMatrix(),
150            'feComponentTransfer' => new Filter\FeComponentTransfer(),
151            'feFuncR' => new Filter\FeFuncR(),
152            'feFuncG' => new Filter\FeFuncG(),
153            'feFuncB' => new Filter\FeFuncB(),
154            'feFuncA' => new Filter\FeFuncA(),
155            'feDiffuseLighting' => new Filter\FeDiffuseLighting(),
156            'feSpecularLighting' => new Filter\FeSpecularLighting(),
157            'feDistantLight' => new Filter\FeDistantLight(),
158            'fePointLight' => new Filter\FePointLight(),
159            'feSpotLight' => new Filter\FeSpotLight(),
160            // SVG 2 §6.3 — declarative named viewport.
161            'view' => new View(),
162            // SVG 2 §15.2 — out-of-scope script content; the typed
163            // class lets the Translator skip it explicitly rather
164            // than recursing into any nested `<text>` etc. children
165            // a malicious document might smuggle in.
166            'script' => new Script(),
167            // SVG 2 §19 — animation elements. Out of scope for the
168            // static print medium; typed for explicit Translator
169            // skip and external-tooling recognition.
170            'animate' => new Animate(),
171            'animateTransform' => new AnimateTransform(),
172            'animateMotion' => new AnimateMotion(),
173            'set' => new SetElement(),
174            'mpath' => new MPath(),
175            // SVG 2 §6.4 — RDF/metadata, never renders.
176            'metadata' => new Metadata(),
177            default => new GenericElement($localName),
178        };
179    }
180}