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

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