1+ <?php
2+
3+ if (!function_exists ('validation_image_extensions ' )) {
4+ function validation_image_extensions (): array
5+ {
6+ return array_values (array_filter ((array ) config ('validation.image_extensions ' , ['jpeg ' , 'jpg ' , 'png ' , 'webp ' , 'gif ' , 'svg ' ])));
7+ }
8+ }
9+
10+ if (!function_exists ('validation_image_accept ' )) {
11+ function validation_image_accept (): string
12+ {
13+ return implode (', ' , array_map (static fn ($ extension ) => '. ' . $ extension , validation_image_extensions ()));
14+ }
15+ }
16+
17+ if (!function_exists ('validation_image_label ' )) {
18+ function validation_image_label (): string
19+ {
20+ return implode (', ' , array_map (static fn ($ extension ) => strtoupper ($ extension ), validation_image_extensions ()));
21+ }
22+ }
23+
24+ if (!function_exists ('validation_rich_text_allowed_tags ' )) {
25+ function validation_rich_text_allowed_tags (): array
26+ {
27+ return array_values (array_filter ((array ) config ('validation.rich_text.allowed_tags ' , ['a ' , 'p ' , 'strong ' , 'i ' , 'ul ' , 'ol ' , 'li ' , 'blockquote ' , 'h2 ' , 'h3 ' , 'h4 ' , 'br ' ])));
28+ }
29+ }
30+
31+ if (!function_exists ('validation_rich_text_allowed_protocols ' )) {
32+ function validation_rich_text_allowed_protocols (): array
33+ {
34+ return array_values (array_filter ((array ) config ('validation.rich_text.allowed_protocols ' , ['http ' , 'https ' , 'mailto ' , 'tel ' ])));
35+ }
36+ }
37+
38+ if (!function_exists ('sanitize_rich_text ' )) {
39+ function sanitize_rich_text ($ html ): string
40+ {
41+ $ html = (string ) $ html ;
42+
43+ if ($ html === '' ) {
44+ return '' ;
45+ }
46+
47+ $ allowedTags = validation_rich_text_allowed_tags ();
48+ $ allowedProtocols = validation_rich_text_allowed_protocols ();
49+
50+ if (!class_exists ('DOMDocument ' )) {
51+ return strip_tags ($ html , '< ' . implode ('>< ' , $ allowedTags ) . '> ' );
52+ }
53+
54+ $ dom = new \DOMDocument ('1.0 ' , 'UTF-8 ' );
55+ $ previousUseErrors = libxml_use_internal_errors (true );
56+ $ wrapperId = 'linkstack-rich-text-root ' ;
57+ $ dom ->loadHTML ('<?xml encoding="UTF-8"><div id=" ' . $ wrapperId . '"> ' . $ html . '</div> ' , LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
58+ libxml_clear_errors ();
59+ libxml_use_internal_errors ($ previousUseErrors );
60+
61+ $ wrapper = $ dom ->getElementById ($ wrapperId );
62+ if (!$ wrapper ) {
63+ return strip_tags ($ html , '< ' . implode ('>< ' , $ allowedTags ) . '> ' );
64+ }
65+
66+ return sanitize_rich_text_node ($ wrapper , $ allowedTags , $ allowedProtocols );
67+ }
68+ }
69+
70+ if (!function_exists ('sanitize_rich_text_node ' )) {
71+ function sanitize_rich_text_node (\DOMNode $ node , array $ allowedTags , array $ allowedProtocols ): string
72+ {
73+ $ output = '' ;
74+
75+ foreach ($ node ->childNodes as $ childNode ) {
76+ if ($ childNode ->nodeType === XML_TEXT_NODE ) {
77+ $ output .= htmlspecialchars ($ childNode ->nodeValue ?? '' , ENT_QUOTES | ENT_SUBSTITUTE , 'UTF-8 ' );
78+ continue ;
79+ }
80+
81+ if ($ childNode ->nodeType !== XML_ELEMENT_NODE ) {
82+ continue ;
83+ }
84+
85+ $ tagName = strtolower ($ childNode ->nodeName );
86+ if (!in_array ($ tagName , $ allowedTags , true )) {
87+ if (in_array ($ tagName , ['script ' , 'style ' ], true )) {
88+ continue ;
89+ }
90+
91+ $ output .= sanitize_rich_text_node ($ childNode , $ allowedTags , $ allowedProtocols );
92+ continue ;
93+ }
94+
95+ if ($ tagName === 'br ' ) {
96+ $ output .= '<br> ' ;
97+ continue ;
98+ }
99+
100+ $ attributes = '' ;
101+
102+ if ($ tagName === 'a ' ) {
103+ $ href = trim ((string ) $ childNode ->attributes ?->getNamedItem('href ' )?->nodeValue);
104+ $ isAllowedLink = false ;
105+
106+ if ($ href !== '' ) {
107+ $ scheme = strtolower ((string ) parse_url ($ href , PHP_URL_SCHEME ));
108+ if (in_array ($ scheme , $ allowedProtocols , true )) {
109+ $ isAllowedLink = true ;
110+ $ attributes .= ' href=" ' . htmlspecialchars ($ href , ENT_QUOTES | ENT_SUBSTITUTE , 'UTF-8 ' ) . '" ' ;
111+ }
112+ }
113+
114+ if (!$ isAllowedLink ) {
115+ $ output .= sanitize_rich_text_node ($ childNode , $ allowedTags , $ allowedProtocols );
116+ continue ;
117+ }
118+
119+ $ attributes .= ' rel="noopener noreferrer nofollow" ' ;
120+ }
121+
122+ $ output .= '< ' . $ tagName . $ attributes . '> ' ;
123+ $ output .= sanitize_rich_text_node ($ childNode , $ allowedTags , $ allowedProtocols );
124+ $ output .= '</ ' . $ tagName . '> ' ;
125+ }
126+
127+ return $ output ;
128+ }
129+ }
130+
131+ if (!function_exists ('strip_tags_except_allowed_protocols ' )) {
132+ function strip_tags_except_allowed_protocols ($ str )
133+ {
134+ return sanitize_rich_text ($ str );
135+ }
136+ }
137+
138+ if (!function_exists ('validation_image_extensions ' )) {
139+ function validation_image_extensions (): array
140+ {
141+ return array_values (array_filter ((array ) config ('validation.image_extensions ' , ['jpeg ' , 'jpg ' , 'png ' , 'webp ' , 'gif ' , 'svg ' ])));
142+ }
143+ }
144+
145+ if (!function_exists ('validation_image_accept ' )) {
146+ function validation_image_accept (): string
147+ {
148+ return implode (', ' , array_map (static fn ($ extension ) => '. ' . $ extension , validation_image_extensions ()));
149+ }
150+ }
151+
152+ if (!function_exists ('validation_image_label ' )) {
153+ function validation_image_label (): string
154+ {
155+ return implode (', ' , array_map (static fn ($ extension ) => strtoupper ($ extension ), validation_image_extensions ()));
156+ }
157+ }
158+
159+ if (!function_exists ('validation_rich_text_allowed_tags ' )) {
160+ function validation_rich_text_allowed_tags (): array
161+ {
162+ return array_values (array_filter ((array ) config ('validation.rich_text.allowed_tags ' , ['a ' , 'p ' , 'strong ' , 'i ' , 'ul ' , 'ol ' , 'li ' , 'blockquote ' , 'h2 ' , 'h3 ' , 'h4 ' , 'br ' ])));
163+ }
164+ }
165+
166+ if (!function_exists ('validation_rich_text_allowed_protocols ' )) {
167+ function validation_rich_text_allowed_protocols (): array
168+ {
169+ return array_values (array_filter ((array ) config ('validation.rich_text.allowed_protocols ' , ['http ' , 'https ' , 'mailto ' , 'tel ' ])));
170+ }
171+ }
172+
173+ if (!function_exists ('sanitize_rich_text ' )) {
174+ function sanitize_rich_text ($ html ): string
175+ {
176+ $ html = (string ) $ html ;
177+
178+ if ($ html === '' ) {
179+ return '' ;
180+ }
181+
182+ $ allowedTags = validation_rich_text_allowed_tags ();
183+ $ allowedProtocols = validation_rich_text_allowed_protocols ();
184+
185+ if (!class_exists ('DOMDocument ' )) {
186+ return strip_tags ($ html , '< ' . implode ('>< ' , $ allowedTags ) . '> ' );
187+ }
188+
189+ $ dom = new \DOMDocument ('1.0 ' , 'UTF-8 ' );
190+ $ previousUseErrors = libxml_use_internal_errors (true );
191+ $ wrapperId = 'linkstack-rich-text-root ' ;
192+ $ dom ->loadHTML ('<?xml encoding="UTF-8"><div id=" ' . $ wrapperId . '"> ' . $ html . '</div> ' , LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
193+ libxml_clear_errors ();
194+ libxml_use_internal_errors ($ previousUseErrors );
195+
196+ $ wrapper = $ dom ->getElementById ($ wrapperId );
197+ if (!$ wrapper ) {
198+ return strip_tags ($ html , '< ' . implode ('>< ' , $ allowedTags ) . '> ' );
199+ }
200+
201+ return sanitize_rich_text_node ($ wrapper , $ allowedTags , $ allowedProtocols );
202+ }
203+ }
204+
205+ if (!function_exists ('sanitize_rich_text_node ' )) {
206+ function sanitize_rich_text_node (\DOMNode $ node , array $ allowedTags , array $ allowedProtocols ): string
207+ {
208+ $ output = '' ;
209+
210+ foreach ($ node ->childNodes as $ childNode ) {
211+ if ($ childNode ->nodeType === XML_TEXT_NODE ) {
212+ $ output .= htmlspecialchars ($ childNode ->nodeValue ?? '' , ENT_QUOTES | ENT_SUBSTITUTE , 'UTF-8 ' );
213+ continue ;
214+ }
215+
216+ if ($ childNode ->nodeType !== XML_ELEMENT_NODE ) {
217+ continue ;
218+ }
219+
220+ $ tagName = strtolower ($ childNode ->nodeName );
221+ if (!in_array ($ tagName , $ allowedTags , true )) {
222+ if (in_array ($ tagName , ['script ' , 'style ' ], true )) {
223+ continue ;
224+ }
225+
226+ $ output .= sanitize_rich_text_node ($ childNode , $ allowedTags , $ allowedProtocols );
227+ continue ;
228+ }
229+
230+ if ($ tagName === 'br ' ) {
231+ $ output .= '<br> ' ;
232+ continue ;
233+ }
234+
235+ $ attributes = '' ;
236+
237+ if ($ tagName === 'a ' ) {
238+ $ href = trim ((string ) $ childNode ->attributes ?->getNamedItem('href ' )?->nodeValue);
239+ $ isAllowedLink = false ;
240+
241+ if ($ href !== '' ) {
242+ $ scheme = strtolower ((string ) parse_url ($ href , PHP_URL_SCHEME ));
243+ if (in_array ($ scheme , $ allowedProtocols , true )) {
244+ $ isAllowedLink = true ;
245+ $ attributes .= ' href=" ' . htmlspecialchars ($ href , ENT_QUOTES | ENT_SUBSTITUTE , 'UTF-8 ' ) . '" ' ;
246+ }
247+ }
248+
249+ if (!$ isAllowedLink ) {
250+ $ output .= sanitize_rich_text_node ($ childNode , $ allowedTags , $ allowedProtocols );
251+ continue ;
252+ }
253+
254+ $ attributes .= ' rel="noopener noreferrer nofollow" ' ;
255+ }
256+
257+ $ output .= '< ' . $ tagName . $ attributes . '> ' ;
258+ $ output .= sanitize_rich_text_node ($ childNode , $ allowedTags , $ allowedProtocols );
259+ $ output .= '</ ' . $ tagName . '> ' ;
260+ }
261+
262+ return $ output ;
263+ }
264+ }
265+
266+ if (!function_exists ('strip_tags_except_allowed_protocols ' )) {
267+ function strip_tags_except_allowed_protocols ($ str )
268+ {
269+ return sanitize_rich_text ($ str );
270+ }
271+ }
0 commit comments