PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.6.01
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.6.01
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 1.1.0 All 77 releases
fluent-community / app / Services / CustomSanitizer.php

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

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