PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 1.1.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v1.1.0
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
fluent-community / app / Services / CustomSanitizer.php

CustomSanitizer.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 1.1.0, at app/Services/CustomSanitizer.php

448 lines 16.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\App\Services;
4
5 use FluentCommunity\App\Functions\Utility;
6 use FluentCommunity\Framework\Support\Arr;
7
8 class CustomSanitizer
9 {
10 public static function sanitizeMenuLink($item)
11 {
12 $validKeys = ['title', 'enabled', 'permalink', 'new_tab', 'link_classes', 'shape_svg', 'emoji', 'icon_image', 'is_custom', 'is_system', 'is_locked', 'is_unavailable', 'slug'];
13 $item = array_filter(Arr::only($item, $validKeys));
14
15 $yesNoItems = ['enabled', 'is_custom', 'is_system', 'is_locked', 'is_unavailable'];
16 foreach ($yesNoItems as $key) {
17 if (isset($item[$key])) {
18 $item[$key] = $item[$key] === 'yes' ? 'yes' : 'no';
19 }
20 }
21
22 $textTypes = ['title', 'new_tab', 'link_classes', 'slug'];
23 foreach ($textTypes as $key) {
24 if (isset($item[$key])) {
25 $item[$key] = sanitize_text_field($item[$key]);
26 }
27 }
28
29 $item['permalink'] = sanitize_url($item['permalink']);
30
31 if (!empty($item['shape_svg'])) {
32 $item['shape_svg'] = self::sanitizeSvg($item['shape_svg']);
33 }
34
35 if (!empty($item['emoji'])) {
36 $item['emoji'] = self::sanitizeEmoji($item['emoji']);
37 }
38
39 if (!empty($item['icon_image'])) {
40 $media = Helper::getMediaFromUrl($item['icon_image']);
41 if ($media) {
42 $item['icon_image'] = $media->public_url;
43 $media->update([
44 'is_active' => true,
45 'user_id' => get_current_user_id(),
46 'object_source' => 'general'
47 ]);
48 } else {
49 $item['icon_image'] = sanitize_url($item['icon_image']);
50 }
51 }
52
53 return $item;
54 }
55
56 public static function sanitizeSvg($svg_content)
57 {
58 if (!$svg_content) {
59 return '';
60 }
61
62 if (current_user_can('unfiltered_html')) {
63 return $svg_content;
64 }
65
66 // Remove any comments
67 $svg_content = preg_replace('/<!--(.|\s)*?-->/', '', $svg_content);
68
69 // Remove XML or DOCTYPE declarations
70 $svg_content = preg_replace('/<\?xml(.|\s)*?\?>/', '', $svg_content);
71 $svg_content = preg_replace('/<!DOCTYPE(.|\s)*?>/i', '', $svg_content);
72
73 // Remove embedded scripts, iframes, or event handlers
74 $svg_content = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $svg_content);
75 $svg_content = preg_replace('/<iframe\b[^>]*>(.*?)<\/iframe>/is', '', $svg_content);
76 $svg_content = preg_replace('/on\w+="[^"]*"/i', '', $svg_content);
77
78 $allowed_tags = [
79 'svg' => ['width' => true, 'height' => true, 'viewBox' => true, 'version' => true, 'xmlns' => true, 'xmlns:xlink' => true, 'xml:space' => true, 'style' => true, 'preserveAspectRatio' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'color' => true],
80 'g' => ['fill' => true, 'fill-rule' => true, 'stroke' => true, 'stroke-width' => true, 'clip-path' => true, 'transform' => true],
81 'path' => ['d' => true, 'opacity' => true, 'stroke-linecap' => true, 'fill' => true, 'fill-rule' => true, 'stroke' => true, 'stroke-width' => true, 'style' => true, 'transform' => true],
82 'rect' => ['width' => true, 'height' => true, 'x' => true, 'y' => true, 'rx' => true, 'ry' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'transform' => true, 'style' => true],
83 'circle' => ['cx' => true, 'cy' => true, 'r' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'transform' => true],
84 'ellipse' => ['cx' => true, 'cy' => true, 'rx' => true, 'ry' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'transform' => true],
85 'line' => ['x1' => true, 'x2' => true, 'y1' => true, 'y2' => true, 'stroke' => true, 'stroke-width' => true, 'transform' => true],
86 'polyline' => ['points' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'transform' => true],
87 'polygon' => ['points' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'transform' => true],
88 'text' => ['x' => true, 'y' => true, 'font-size' => true, 'font-family' => true, 'text-anchor' => true, 'fill' => true, 'transform' => true],
89 'tspan' => ['x' => true, 'y' => true, 'font-size' => true, 'font-family' => true, 'text-anchor' => true, 'fill' => true],
90 'defs' => [],
91 'clipPath' => ['id' => true, 'clipPathUnits' => true],
92 'stop' => ['offset' => true, 'stop-color' => true, 'stop-opacity' => true],
93 'linearGradient' => ['id' => true, 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, 'gradientUnits' => true, 'gradientTransform' => true],
94 'radialGradient' => ['id' => true, 'cx' => true, 'cy' => true, 'r' => true, 'fx' => true, 'fy' => true, 'gradientUnits' => true, 'gradientTransform' => true],
95 'mask' => ['id' => true, 'maskUnits' => true, 'maskContentUnits' => true, 'x' => true, 'y' => true, 'width' => true, 'height' => true],
96 'use' => ['xlink:href' => true, 'x' => true, 'y' => true, 'width' => true, 'height' => true],
97 'title' => [],
98 'desc' => [],
99 ];
100
101 // Load the SVG string into a DOMDocument and discard errors for malformed XML
102 $dom = new \DOMDocument();
103 libxml_use_internal_errors(true);
104 $dom->loadXML($svg_content);
105 libxml_clear_errors();
106
107 // Sanitize by removing unwanted tags and attributes
108 self::sanitizeNode($dom->documentElement, $allowed_tags);
109
110 return $dom->saveXML($dom->documentElement);
111 }
112
113 private static function sanitizeNode(\DOMNode $node, array $allowed_tags)
114 {
115 if ($node->nodeType === XML_ELEMENT_NODE) {
116 if (!isset($allowed_tags[$node->nodeName])) {
117 $node->parentNode->removeChild($node);
118 return;
119 }
120
121 // Check attributes
122 $attributes = $node->attributes;
123 $length = $attributes->length;
124 for ($i = $length - 1; $i >= 0; $i--) {
125 $attr = $attributes->item($i);
126 $attr_name = $attr->nodeName;
127 if (!isset($allowed_tags[$node->nodeName][$attr_name])) {
128 $node->removeAttribute($attr_name);
129 } else {
130 // Sanitize attribute values
131 // FILTER_SANITIZE_STRING is deprecated in PHP 8.1
132 $sanitized_value = htmlspecialchars($attr->nodeValue, ENT_QUOTES, 'UTF-8');
133 $node->setAttribute($attr_name, $sanitized_value);
134 }
135 }
136 }
137
138 // Recursively sanitize child nodes
139 for ($i = $node->childNodes->length - 1; $i >= 0; $i--) {
140 self::sanitizeNode($node->childNodes->item($i), $allowed_tags);
141 }
142 }
143
144 public static function sanitizeEmoji($emoji, $single = true)
145 {
146 $emoji = (string)$emoji;
147 $emoji = trim($emoji);
148
149 if (!$emoji) {
150 return '';
151 }
152
153 if ($single && function_exists('\mb_substr')) {
154 $emoji = \mb_substr($emoji, 0, 1, 'UTF-8');
155 }
156
157 $isEmoji = preg_match('/[\x{1F600}-\x{1F64F}\x{1F300}-\x{1F5FF}\x{1F680}-\x{1F6FF}\x{1F700}-\x{1F77F}\x{1F780}-\x{1F7FF}\x{1F800}-\x{1F8FF}\x{1F900}-\x{1F9FF}\x{1FA00}-\x{1FA6F}\x{1FA70}-\x{1FAFF}\x{2600}-\x{26FF}\x{2700}-\x{27BF}\x{2B50}\x{2B55}\x{2934}\x{2935}\x{3297}\x{3299}]/u', $emoji);
158
159 if ($isEmoji) {
160 return $emoji;
161 }
162 return '';
163 }
164
165 public static function sanitizeWelcomeBannerSettings($settings)
166 {
167 $rules = [
168 'title' => 'sanitize_text_field',
169 'description' => 'wp_kses_post',
170 'mediaType' => 'sanitize_text_field',
171 'allowClose' => 'sanitize_text_field',
172 'enabled' => 'sanitize_text_field',
173 ];
174
175 $sanitizedSettings = [];
176 foreach (['login', 'logout'] as $type) {
177 $typeSettings = Arr::get($settings, $type, []);
178 if (empty($typeSettings)) {
179 continue;
180 }
181
182 $bannerVideo = Arr::get($typeSettings, 'bannerVideo', []);
183 $bannerImage = Arr::get($typeSettings, 'bannerImage', '');
184 $ctaButtons = Arr::get($typeSettings, 'ctaButtons', []);
185
186 $sanitizedSettings[$type]['bannerVideo'] = self::sanitizeBannerVideo($bannerVideo);
187 $sanitizedSettings[$type]['bannerImage'] = self::sanitizeBannerImage($bannerImage);
188 $sanitizedSettings[$type]['ctaButtons'] = self::sanitizeCtaButtons($ctaButtons);
189 $sanitizedSettings[$type]['description'] = self::unslashMarkdown(Arr::get($typeSettings, 'description'));
190
191 foreach ($typeSettings as $key => $value) {
192 if (isset($rules[$key]) && !in_array($key, ['bannerVideo', 'bannerImage', 'ctaButtons'])) {
193 $sanitizedSettings[$type][$key] = call_user_func($rules[$key], $value);
194 }
195 }
196 }
197
198 return $sanitizedSettings;
199 }
200
201 private static function sanitizeBannerVideo($video)
202 {
203 if (empty($video)) {
204 return [];
205 }
206
207 return array_filter([
208 'type' => sanitize_text_field(Arr::get($video, 'type', '')),
209 'url' => sanitize_url(Arr::get($video, 'url', '')),
210 'content_type' => sanitize_text_field(Arr::get($video, 'content_type', '')),
211 'provider' => sanitize_url(Arr::get($video, 'provider', '')),
212 'title' => sanitize_text_field(Arr::get($video, 'title', '')),
213 'author_name' => sanitize_text_field(Arr::get($video, 'author_name', '')),
214 'html' => self::sanitizeRichText(Arr::get($video, 'html', '')),
215 ]);
216 }
217
218 private static function sanitizeBannerImage($imageUrl)
219 {
220 if (empty($imageUrl)) {
221 return '';
222 }
223
224 $media = Helper::getMediaFromUrl($imageUrl);
225 if ($media) {
226 $media->update([
227 'is_active' => true,
228 'user_id' => get_current_user_id(),
229 'object_source' => 'general'
230 ]);
231 return $media->public_url;
232 }
233
234 return sanitize_url($imageUrl);
235 }
236
237 private static function sanitizeCtaButtons($ctaButtons)
238 {
239 if (empty($ctaButtons)) {
240 return [];
241 }
242
243 $sanitizerMap = [
244 'label' => 'sanitize_text_field',
245 'link' => 'sanitize_url',
246 'type' => 'sanitize_text_field',
247 'newTab' => 'sanitize_text_field'
248 ];
249
250 foreach ($ctaButtons as $btnKey => $btnValue) {
251 foreach ($btnValue as $key => $value) {
252 if (isset($sanitizerMap[$key])) {
253 $ctaButtons[$btnKey][$key] = call_user_func($sanitizerMap[$key], $value);
254 }
255 }
256 }
257
258 return $ctaButtons;
259 }
260
261 public static function santizeLinkItem($item)
262 {
263 $validKeys = ['title', 'enabled', 'new_tab', 'emoji', 'icon_image', 'shape_svg', 'title', 'permalink', 'slug'];
264 $item = array_filter(Arr::only($item, $validKeys));
265
266 $yesNoItems = ['enabled', 'new_tab', 'is_locked', 'is_unavailable'];
267 foreach ($yesNoItems as $key) {
268 if (isset($item[$key])) {
269 $item[$key] = $item[$key] === 'yes' ? 'yes' : 'no';
270 }
271 }
272
273 $item['emoji'] = self::sanitizeEmoji(Arr::get($item, 'emoji'));
274
275 if (empty($item['slug'])) {
276 $item['slug'] = sanitize_title($item['title']);
277 } else {
278 $item['slug'] = sanitize_title($item['slug']);
279 }
280
281 $textTypes = ['title'];
282 foreach ($textTypes as $key) {
283 if (isset($item[$key])) {
284 $item[$key] = sanitize_text_field($item[$key]);
285 }
286 }
287 $item['permalink'] = sanitize_url($item['permalink']);
288
289
290 if (!empty($item['icon_image'])) {
291 $media = Helper::getMediaFromUrl($item['icon_image']);
292 if ($media) {
293 $media->update([
294 'is_active' => true,
295 'user_id' => get_current_user_id(),
296 'object_source' => 'general'
297 ]);
298 $item['icon_image'] = $media->public_url;
299 } else {
300 $item['icon_image'] = sanitize_text_field($item['icon_image']);
301 }
302 }
303
304 if (!empty($item['icon_svg'])) {
305 $item['icon_svg'] = self::sanitizeSvg($item['icon_svg']);
306 }
307
308 return array_filter($item);
309 }
310
311 public static function sanitizeRichText($content, $print = false)
312 {
313 if ($print) {
314 echo self::sanitizeHtml($content); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
315 }
316
317 return self::sanitizeHtml($content);
318 }
319
320 public static function sanitizeHtml($html)
321 {
322 if (current_user_can('unfiltered_html')) {
323 return $html;
324 }
325
326 if (!$html) {
327 return $html;
328 }
329
330 // Return $html if it's just a plain text
331 if (!preg_match('/<[^>]*>/', $html)) {
332 return $html;
333 }
334
335 $tags = wp_kses_allowed_html('post');
336 $tags['style'] = [
337 'types' => [],
338 ];
339
340 // iframe
341 $tags['iframe'] = [
342 'width' => [],
343 'height' => [],
344 'src' => [],
345 'srcdoc' => [],
346 'title' => [],
347 'frameborder' => [],
348 'allow' => [],
349 'class' => [],
350 'id' => [],
351 'allowfullscreen' => [],
352 'referrerpolicy' => [],
353 'style' => [],
354 ];
355
356 $tags = apply_filters('fluent_community/allowed_html_tags', $tags);
357
358 return wp_kses($html, $tags);
359 }
360
361 public static function santizeEmailSettings($settings)
362 {
363 $prevSettings = Utility::getEmailNotificationSettings();
364 $settings = Arr::only($settings, array_keys($prevSettings));
365
366 $yesNoFields = ['com_my_post_mail', 'reply_my_com_mail', 'mention_mail', 'digest_email_status', 'disable_powered_by'];
367 $textFields = ['send_from_name', 'reply_to_name'];
368 $emailFields = ['send_from_email', 'reply_to_email'];
369
370 foreach ($yesNoFields as $field) {
371 if (isset($settings[$field])) {
372 $settings[$field] = $settings[$field] === 'yes' ? 'yes' : 'no';
373 }
374 }
375
376 foreach ($textFields as $field) {
377 if (isset($settings[$field])) {
378 $settings[$field] = sanitize_text_field($settings[$field]);
379 }
380 }
381
382 foreach ($emailFields as $field) {
383 if (isset($settings[$field])) {
384 $settings[$field] = sanitize_email($settings[$field]);
385 }
386 }
387
388 $time = Arr::get($settings, 'daily_digest_time');
389
390 if ($time) {
391 $time = sanitize_text_field($time);
392
393 // check time is valid or not
394 if (!preg_match('/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/', $time)) {
395 $time = '09:00';
396 }
397 } else {
398 $time = '09:00';
399 }
400
401 $emailDay = Arr::get($settings, 'digest_mail_day');
402
403 if ($emailDay) {
404 $emailDay = sanitize_text_field($emailDay);
405 if (!in_array($emailDay, ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'])) {
406 $emailDay = 'tue';
407 }
408 } else {
409 $emailDay = 'tue';
410 }
411
412 $settings['digest_mail_day'] = $emailDay;
413 $settings['daily_digest_time'] = $time;
414 $settings['email_footer'] = wp_kses_post(self::unslashMarkdown(Arr::get($settings, 'email_footer')));
415 $settings['email_footer_rendered'] = FeedsHelper::mdToHtml($settings['email_footer']);
416
417 return $settings;
418 }
419
420 public static function sanitizeUserName($username)
421 {
422 $username = strtolower($username);
423 // check of @ symbol
424 if (strpos($username, '@') !== false) {
425 $username = explode('@', $username)[0];
426 }
427
428 $username = sanitize_user($username);
429 $username = preg_replace('/[^a-zA-Z0-9_]/', '', $username);
430 return $username;
431 }
432
433 public static function unslashMarkdown($markdown)
434 {
435 $replaceMaps = [
436 "\\\n" => PHP_EOL,
437 '\@' => '@',
438 '\\_' => '_',
439 '\\&' => '&',
440 '\\*' => '*',
441 '\\~' => '~',
442 '\\:' => ':',
443 ];
444
445 return str_replace(array_keys($replaceMaps), array_values($replaceMaps), $markdown);
446 }
447 }
448