PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.7.5
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.7.5
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.7.5, at app/Services/CustomSanitizer.php

548 lines 21.0 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(Arr::get($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 /** @var \DOMElement $node */
124 if (!isset($allowed_tags[$node->nodeName])) {
125 $node->parentNode->removeChild($node);
126 return;
127 }
128
129 // Check attributes
130 $attributes = $node->attributes;
131 $length = $attributes->length;
132 for ($i = $length - 1; $i >= 0; $i--) {
133 $attr = $attributes->item($i);
134 $attr_name = $attr->nodeName;
135 if (!isset($allowed_tags[$node->nodeName][$attr_name])) {
136 $node->removeAttribute($attr_name);
137 } else {
138 // Sanitize attribute values
139 // FILTER_SANITIZE_STRING is deprecated in PHP 8.1
140 $sanitized_value = htmlspecialchars($attr->nodeValue, ENT_QUOTES, 'UTF-8');
141 $node->setAttribute($attr_name, $sanitized_value);
142 }
143 }
144 }
145
146 // Recursively sanitize child nodes
147 for ($i = $node->childNodes->length - 1; $i >= 0; $i--) {
148 self::sanitizeNode($node->childNodes->item($i), $allowed_tags);
149 }
150 }
151
152 public static function sanitizeEmoji($emoji, $single = true)
153 {
154 $emoji = (string)$emoji;
155 $emoji = trim($emoji);
156
157 if (!$emoji) {
158 return '';
159 }
160
161 if ($single && function_exists('\mb_substr')) {
162 $emoji = \mb_substr($emoji, 0, 4, 'UTF-8');
163 }
164
165 $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);
166
167 if ($isEmoji) {
168 return $emoji;
169 }
170 return '';
171 }
172
173 public static function sanitizeWelcomeBannerSettings($settings, $views = ['login', 'logout'])
174 {
175 $views = array_intersect($views, ['login', 'logout', 'enrolled', 'not_enrolled']);
176
177 $rules = [
178 'title' => 'sanitize_text_field',
179 'description' => 'wp_kses_post',
180 'mediaType' => 'sanitize_text_field',
181 'allowClose' => 'sanitize_text_field',
182 'enabled' => 'sanitize_text_field',
183 ];
184
185 $sanitizedSettings = [];
186 foreach ($views as $type) {
187 $typeSettings = Arr::get($settings, $type, []);
188 if (empty($typeSettings)) {
189 continue;
190 }
191
192 $bannerVideo = Arr::get($typeSettings, 'bannerVideo', []);
193 $bannerImage = Arr::get($typeSettings, 'bannerImage', '');
194 $ctaButtons = Arr::get($typeSettings, 'ctaButtons', []);
195
196 $sanitizedSettings[$type]['bannerVideo'] = self::sanitizeBannerVideo($bannerVideo);
197 $sanitizedSettings[$type]['bannerImage'] = self::sanitizeBannerImage($bannerImage);
198 $sanitizedSettings[$type]['ctaButtons'] = self::sanitizeCtaButtons($ctaButtons);
199
200 $description = Arr::get($typeSettings, 'description');
201 if (!empty($description)) {
202 $description = wp_kses_post(self::unslashMarkdown(wp_unslash($description)));
203 }
204 $sanitizedSettings[$type]['description'] = $description;
205
206 foreach ($typeSettings as $key => $value) {
207 if (isset($rules[$key]) && !in_array($key, ['bannerVideo', 'bannerImage', 'ctaButtons', 'description'])) {
208 $sanitizedSettings[$type][$key] = call_user_func($rules[$key], $value);
209 }
210 }
211 }
212
213 return $sanitizedSettings;
214 }
215
216 private static function sanitizeBannerVideo($video)
217 {
218 if (empty($video)) {
219 return [];
220 }
221
222 return array_filter([
223 'type' => sanitize_text_field(Arr::get($video, 'type', '')),
224 'url' => sanitize_url(Arr::get($video, 'url', '')),
225 'content_type' => sanitize_text_field(Arr::get($video, 'content_type', '')),
226 'provider' => sanitize_text_field(Arr::get($video, 'provider', '')),
227 'title' => sanitize_text_field(Arr::get($video, 'title', '')),
228 'author_name' => sanitize_text_field(Arr::get($video, 'author_name', '')),
229 'html' => self::sanitizeRichText(Arr::get($video, 'html', '')),
230 'image' => sanitize_url(Arr::get($video, 'image', '')),
231 ]);
232 }
233
234 private static function sanitizeBannerImage($imageUrl)
235 {
236 if (empty($imageUrl)) {
237 return '';
238 }
239
240 $media = Helper::getMediaFromUrl($imageUrl);
241 if ($media) {
242 $media->update([
243 'is_active' => true,
244 'user_id' => get_current_user_id(),
245 'object_source' => 'general'
246 ]);
247 return $media->public_url;
248 }
249
250 return sanitize_url($imageUrl);
251 }
252
253 private static function sanitizeCtaButtons($ctaButtons)
254 {
255 if (empty($ctaButtons)) {
256 return [];
257 }
258
259 $sanitizerMap = [
260 'label' => 'sanitize_text_field',
261 'link' => function ($url) {
262 return esc_url_raw($url, ['http', 'https', 'mailto']);
263 },
264 'type' => 'sanitize_text_field',
265 'newTab' => 'sanitize_text_field'
266 ];
267
268 foreach ($ctaButtons as $btnKey => $btnValue) {
269 foreach ($btnValue as $key => $value) {
270 if (isset($sanitizerMap[$key])) {
271 $ctaButtons[$btnKey][$key] = call_user_func($sanitizerMap[$key], $value);
272 }
273 }
274 }
275
276 return $ctaButtons;
277 }
278
279 public static function santizeLinkItem($item)
280 {
281 $validKeys = ['title', 'enabled', 'new_tab', 'emoji', 'icon_image', 'shape_svg', 'title', 'permalink', 'slug', 'privacy', 'membership_ids'];
282 $item = array_filter(Arr::only($item, $validKeys));
283
284 $yesNoItems = ['enabled', 'new_tab', 'is_locked', 'is_unavailable'];
285 foreach ($yesNoItems as $key) {
286 if (isset($item[$key])) {
287 $item[$key] = $item[$key] === 'yes' ? 'yes' : 'no';
288 }
289 }
290
291 $item['emoji'] = self::sanitizeEmoji(Arr::get($item, 'emoji'));
292
293 if (empty($item['slug'])) {
294 $item['slug'] = sanitize_title(Arr::get($item, 'title', ''));
295 } else {
296 $item['slug'] = sanitize_title($item['slug']);
297 }
298
299 $textTypes = ['title'];
300 foreach ($textTypes as $key) {
301 if (isset($item[$key])) {
302 $item[$key] = sanitize_text_field($item[$key]);
303 }
304 }
305 $item['permalink'] = sanitize_url(Arr::get($item, 'permalink', ''));
306
307
308 if (!empty($item['icon_image'])) {
309 $media = Helper::getMediaFromUrl($item['icon_image']);
310 if ($media) {
311 $media->update([
312 'is_active' => true,
313 'user_id' => get_current_user_id(),
314 'object_source' => 'general'
315 ]);
316 $item['icon_image'] = $media->public_url;
317 } else {
318 $item['icon_image'] = sanitize_text_field($item['icon_image']);
319 }
320 }
321
322 if (!empty($item['icon_svg'])) {
323 $item['icon_svg'] = self::sanitizeSvg($item['icon_svg']);
324 }
325
326 if (Arr::get($item, 'privacy') == 'members_only') {
327 $item['membership_ids'] = array_map('sanitize_text_field', (array)Arr::get($item, 'membership_ids', []));
328 }
329
330 return array_filter($item);
331 }
332
333 public static function sanitizeRichText($content, $print = false)
334 {
335 if ($print) {
336 echo self::sanitizeHtml($content); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
337 }
338
339 return self::sanitizeHtml($content);
340 }
341
342 public static function sanitizeHtml($html)
343 {
344 if (current_user_can('unfiltered_html')) {
345 return $html;
346 }
347
348 if (!$html) {
349 return $html;
350 }
351
352 // Return $html if it's just a plain text
353 if (!preg_match('/<[^>]*>/', $html)) {
354 return $html;
355 }
356
357 $tags = wp_kses_allowed_html('post');
358 $tags['style'] = [
359 'types' => [],
360 ];
361
362 // iframe
363 $tags['iframe'] = [
364 'width' => [],
365 'height' => [],
366 'src' => [],
367 'srcdoc' => [],
368 'title' => [],
369 'frameborder' => [],
370 'allow' => [],
371 'class' => [],
372 'id' => [],
373 'allowfullscreen' => [],
374 'referrerpolicy' => [],
375 'style' => [],
376 ];
377
378 $tags = apply_filters('fluent_community/allowed_html_tags', $tags);
379
380 return wp_kses($html, $tags);
381 }
382
383 public static function santizeEmailSettings($settings)
384 {
385 $prevSettings = Utility::getEmailNotificationSettings();
386 $settings = Arr::only($settings, array_keys($prevSettings));
387
388 $yesNoFields = ['com_my_post_mail', 'reply_my_com_mail', 'mention_mail', 'digest_email_status', 'disable_powered_by'];
389 $textFields = ['send_from_name', 'reply_to_name'];
390 $emailFields = ['send_from_email', 'reply_to_email'];
391
392 foreach ($yesNoFields as $field) {
393 if (isset($settings[$field])) {
394 $settings[$field] = $settings[$field] === 'yes' ? 'yes' : 'no';
395 }
396 }
397
398 foreach ($textFields as $field) {
399 if (isset($settings[$field])) {
400 $settings[$field] = sanitize_text_field($settings[$field]);
401 }
402 }
403
404 foreach ($emailFields as $field) {
405 if (isset($settings[$field])) {
406 $settings[$field] = sanitize_email($settings[$field]);
407 }
408 }
409
410 $time = Arr::get($settings, 'daily_digest_time');
411
412 if ($time) {
413 $time = sanitize_text_field($time);
414
415 // check time is valid or not
416 if (!preg_match('/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/', $time)) {
417 $time = '09:00';
418 }
419 } else {
420 $time = '09:00';
421 }
422
423 $emailDay = Arr::get($settings, 'digest_mail_day');
424
425 if ($emailDay) {
426 $emailDay = sanitize_text_field($emailDay);
427 if (!in_array($emailDay, ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'])) {
428 $emailDay = 'tue';
429 }
430 } else {
431 $emailDay = 'tue';
432 }
433
434 $settings['digest_mail_day'] = $emailDay;
435 $settings['daily_digest_time'] = $time;
436 $settings['email_footer'] = wp_kses_post(self::unslashMarkdown(Arr::get($settings, 'email_footer')));
437 $settings['email_footer_rendered'] = FeedsHelper::mdToHtml($settings['email_footer']);
438
439 if (!empty($settings['logo'])) {
440 $settings['logo'] = sanitize_url($settings['logo']);
441 }
442
443 return $settings;
444 }
445
446 public static function sanitizeUserName($username)
447 {
448 $username = strtolower($username);
449 // check of @ symbol
450 if (strpos($username, '@') !== false) {
451 $username = explode('@', $username)[0];
452 }
453
454 $username = sanitize_user($username);
455 $username = preg_replace('/[^a-zA-Z0-9_]/', '', $username);
456 return $username;
457 }
458
459 public static function unslashMarkdown($markdown)
460 {
461 $replaceMaps = [
462 "\\\n" => PHP_EOL,
463 '\@' => '@',
464 '\\_' => '_',
465 '\\&' => '&',
466 '\\*' => '*',
467 '\\~' => '~',
468 '\\:' => ':',
469 '\\.' => '.'
470 ];
471
472 return str_replace(array_keys($replaceMaps), array_values($replaceMaps), $markdown);
473 }
474
475 public static function santizeSpaceSettings($settings = [], $privacy = 'public')
476 {
477 $yesNotFields = [
478 'restricted_post_only',
479 'can_request_join',
480 'show_paywalls',
481 'show_sidebar',
482 'hide_members_count',
483 'document_library',
484 'media_gallery',
485 'disable_post_sort_by',
486 'disable_layout_style'
487 ];
488
489 $settings = Arr::only($settings, array_keys((new Space())->defaultSettings()));
490
491 foreach ($yesNotFields as $field) {
492 $settings[$field] = Arr::get($settings, $field) === 'yes' ? 'yes' : 'no';
493 }
494
495 $settings['shape_svg'] = self::sanitizeSvg(Arr::get($settings, 'shape_svg', ''));
496 if (empty($settings['shape_svg'])) {
497 $settings['emoji'] = self::sanitizeEmoji(Arr::get($settings, 'emoji', ''));
498 } else {
499 $settings['emoji'] = '';
500 }
501
502 $lockScreenType = Arr::get($settings, 'custom_lock_screen');
503 if (!in_array($lockScreenType, ['yes', 'no', 'redirect']) || $privacy !== 'private') {
504 $lockScreenType = 'no';
505 }
506 $settings['custom_lock_screen'] = $lockScreenType;
507
508
509 if ($lockScreenType === 'redirect') {
510 $redirectUrl = Arr::get($settings, 'onboard_redirect_url');
511 if (!$redirectUrl || !filter_var($redirectUrl, FILTER_VALIDATE_URL)) {
512 return new \WP_Error('invalid_redirect_url', __('Invalid redirect URL.', 'fluent-community'));
513 }
514 $settings['onboard_redirect_url'] = sanitize_url($redirectUrl);
515 }
516
517 $validOrderOptions = array_keys(Helper::getPostOrderOptions());
518 $defaultOrder = Arr::get($settings, 'default_post_sort_by', '');
519 $settings['default_post_sort_by'] = in_array($defaultOrder, $validOrderOptions) ? $defaultOrder : '';
520
521 $validCommentOrderOptions = array_keys(Helper::getCommentOrderOptions());
522 $defaultCommentOrder = Arr::get($settings, 'default_comment_sort_by', '');
523 $settings['default_comment_sort_by'] = in_array($defaultCommentOrder, $validCommentOrderOptions) ? $defaultCommentOrder : '';
524
525 $accessOptions = ['members_only', 'logged_in', 'everybody'];
526 $mediaAccess = Arr::get($settings, 'media_access');
527 $settings['media_access'] = in_array($mediaAccess, $accessOptions, true) ? $mediaAccess : 'members_only';
528
529 $documentAccess = Arr::get($settings, 'document_access');
530 $settings['document_access'] = in_array($documentAccess, $accessOptions, true) ? $documentAccess : 'members_only';
531
532 $documentUploadOptions = ['admin_only', 'members_only'];
533 $documentUpload = Arr::get($settings, 'document_upload');
534 $settings['document_upload'] = in_array($documentUpload, $documentUploadOptions, true) ? $documentUpload : 'admin_only';
535
536 return $settings;
537 }
538
539 public static function santizeEditorBody($body)
540 {
541 if (current_user_can('unfiltered_html')) {
542 return $body;
543 }
544
545 return wp_kses_post($body);
546 }
547 }
548