1313
1414use Symfony \Component \HtmlSanitizer \HtmlSanitizerAction ;
1515use Symfony \Component \HtmlSanitizer \HtmlSanitizerConfig ;
16+ use Symfony \Component \HtmlSanitizer \Reference \W3CReference ;
1617use Symfony \Component \HtmlSanitizer \TextSanitizer \StringSanitizer ;
1718use Symfony \Component \HtmlSanitizer \Visitor \AttributeSanitizer \AttributeSanitizerInterface ;
1819use Symfony \Component \HtmlSanitizer \Visitor \Model \Cursor ;
@@ -51,6 +52,13 @@ final class DomVisitor
5152 */
5253 private array $ attributeSanitizers = [];
5354
55+ /**
56+ * Registry of elements configuration for each sanitization context used in the document.
57+ *
58+ * @var array<string, array<string, HtmlSanitizerAction|array<string, bool>>> $elementsConfigByContext
59+ */
60+ private array $ elementsConfigByContext = [];
61+
5462 /**
5563 * @param array<string, HtmlSanitizerAction|array<string, bool>> $elementsConfig Registry of allowed/blocked elements:
5664 * * If an element is present as a key and contains an array, the element should be allowed
@@ -75,9 +83,9 @@ public function __construct(
7583 $ this ->defaultAction = $ config ->getDefaultAction ();
7684 }
7785
78- public function visit (\DOMDocumentFragment $ domNode ): ?NodeInterface
86+ public function visit (? string $ context , \DOMDocumentFragment $ domNode ): ?NodeInterface
7987 {
80- $ cursor = new Cursor (new DocumentNode ());
88+ $ cursor = new Cursor ([ $ context ], new DocumentNode ());
8189 $ this ->visitChildren ($ domNode , $ cursor );
8290
8391 return $ cursor ->node ;
@@ -87,24 +95,35 @@ private function visitNode(\DOMNode $domNode, Cursor $cursor): void
8795 {
8896 $ nodeName = StringSanitizer::htmlLower ($ domNode ->nodeName );
8997
98+ if (array_key_exists ($ nodeName , W3CReference::CONTEXTS_MAP )) {
99+ $ cursor ->contextsPath [] = $ nodeName ;
100+ }
101+
90102 // Visit recursively if the node was not dropped
91103 if ($ this ->enterNode ($ nodeName , $ domNode , $ cursor )) {
92104 $ this ->visitChildren ($ domNode , $ cursor );
93105 $ cursor ->node = $ cursor ->node ->getParent ();
94106 }
107+
108+ if (array_key_exists ($ nodeName , W3CReference::CONTEXTS_MAP )) {
109+ array_pop ($ cursor ->contextsPath );
110+ }
95111 }
96112
97113 private function enterNode (string $ domNodeName , \DOMNode $ domNode , Cursor $ cursor ): bool
98114 {
99- if (!\array_key_exists ($ domNodeName , $ this ->elementsConfig )) {
115+ $ context = array_reverse ($ cursor ->contextsPath )[0 ] ?? 'body ' ;
116+ $ this ->elementsConfigByContext [$ context ] ??= $ this ->createContextElementsConfig ($ context );
117+
118+ if (!\array_key_exists ($ domNodeName , $ this ->elementsConfigByContext [$ context ])) {
100119 $ action = $ this ->defaultAction ;
101120 $ allowedAttributes = [];
102121 } else {
103- if (\is_array ($ this ->elementsConfig [$ domNodeName ])) {
122+ if (\is_array ($ this ->elementsConfigByContext [ $ context ] [$ domNodeName ])) {
104123 $ action = HtmlSanitizerAction::Allow;
105- $ allowedAttributes = $ this ->elementsConfig [$ domNodeName ];
124+ $ allowedAttributes = $ this ->elementsConfigByContext [ $ context ] [$ domNodeName ];
106125 } else {
107- $ action = $ this ->elementsConfig [$ domNodeName ];
126+ $ action = $ this ->elementsConfigByContext [ $ context ] [$ domNodeName ];
108127 $ allowedAttributes = [];
109128 }
110129 }
@@ -185,4 +204,53 @@ private function setAttributes(string $domNodeName, \DOMNode $domNode, Node $nod
185204 }
186205 }
187206 }
207+
208+ private function createContextElementsConfig (string $ context ): array
209+ {
210+ $ elementsConfig = [];
211+
212+ // Head: only a few elements are allowed
213+ if (W3CReference::CONTEXT_HEAD === $ context ) {
214+ foreach ($ this ->config ->getAllowedElements () as $ allowedElement => $ allowedAttributes ) {
215+ if (\array_key_exists ($ allowedElement , W3CReference::HEAD_ELEMENTS )) {
216+ $ elementsConfig [$ allowedElement ] = $ allowedAttributes ;
217+ }
218+ }
219+
220+ foreach ($ this ->config ->getBlockedElements () as $ blockedElement => $ v ) {
221+ if (\array_key_exists ($ blockedElement , W3CReference::HEAD_ELEMENTS )) {
222+ $ elementsConfig [$ blockedElement ] = HtmlSanitizerAction::Block;
223+ }
224+ }
225+
226+ foreach ($ this ->config ->getDroppedElements () as $ droppedElement => $ v ) {
227+ if (\array_key_exists ($ droppedElement , W3CReference::HEAD_ELEMENTS )) {
228+ $ elementsConfig [$ droppedElement ] = HtmlSanitizerAction::Drop;
229+ }
230+ }
231+
232+ return $ elementsConfig ;
233+ }
234+
235+ // Body: allow any configured element that isn't in <head>
236+ foreach ($ this ->config ->getAllowedElements () as $ allowedElement => $ allowedAttributes ) {
237+ if (!\array_key_exists ($ allowedElement , W3CReference::HEAD_ELEMENTS )) {
238+ $ elementsConfig [$ allowedElement ] = $ allowedAttributes ;
239+ }
240+ }
241+
242+ foreach ($ this ->config ->getBlockedElements () as $ blockedElement => $ v ) {
243+ if (!\array_key_exists ($ blockedElement , W3CReference::HEAD_ELEMENTS )) {
244+ $ elementsConfig [$ blockedElement ] = HtmlSanitizerAction::Block;
245+ }
246+ }
247+
248+ foreach ($ this ->config ->getDroppedElements () as $ droppedElement => $ v ) {
249+ if (!\array_key_exists ($ droppedElement , W3CReference::HEAD_ELEMENTS )) {
250+ $ elementsConfig [$ droppedElement ] = HtmlSanitizerAction::Drop;
251+ }
252+ }
253+
254+ return $ elementsConfig ;
255+ }
188256}
0 commit comments