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

714 lines 26.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(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 // Browsers accept bare "&" and HTML named entities in inline SVG, but strict XML parsing rejects them
107 $svg_content = preg_replace('/&(?!#?[a-zA-Z0-9]+;)/', '&amp;', $svg_content);
108 $svg_content = preg_replace_callback('/&([a-zA-Z][a-zA-Z0-9]*);/', function ($matches) {
109 $decoded = html_entity_decode($matches[0], ENT_QUOTES | ENT_HTML5, 'UTF-8');
110
111 if ($decoded === $matches[0]) {
112 return '';
113 }
114
115 return htmlspecialchars($decoded, ENT_QUOTES | ENT_XML1, 'UTF-8');
116 }, $svg_content);
117
118 // Load the SVG string into a DOMDocument and discard errors for malformed XML
119 $dom = new \DOMDocument();
120 libxml_use_internal_errors(true);
121 $loaded = $dom->loadXML($svg_content);
122 libxml_clear_errors();
123
124 if (!$loaded || !$dom->documentElement) {
125 return '';
126 }
127
128 // Sanitize by removing unwanted tags and attributes
129 self::sanitizeNode($dom->documentElement, $allowed_tags);
130
131 return $dom->saveXML($dom->documentElement);
132 }
133
134 private static function sanitizeNode(\DOMNode $node, array $allowed_tags)
135 {
136 if ($node->nodeType === XML_ELEMENT_NODE) {
137 /** @var \DOMElement $node */
138 if (!isset($allowed_tags[$node->nodeName])) {
139 $node->parentNode->removeChild($node);
140 return;
141 }
142
143 // Check attributes
144 $attributes = $node->attributes;
145 $length = $attributes->length;
146 for ($i = $length - 1; $i >= 0; $i--) {
147 $attr = $attributes->item($i);
148 $attr_name = $attr->nodeName;
149 if (!isset($allowed_tags[$node->nodeName][$attr_name])) {
150 $node->removeAttribute($attr_name);
151 } else {
152 // Sanitize attribute values
153 // FILTER_SANITIZE_STRING is deprecated in PHP 8.1
154 $sanitized_value = htmlspecialchars($attr->nodeValue, ENT_QUOTES, 'UTF-8');
155 $node->setAttribute($attr_name, $sanitized_value);
156 }
157 }
158 }
159
160 // Recursively sanitize child nodes
161 for ($i = $node->childNodes->length - 1; $i >= 0; $i--) {
162 self::sanitizeNode($node->childNodes->item($i), $allowed_tags);
163 }
164 }
165
166 public static function sanitizeEmoji($emoji, $single = true)
167 {
168 $emoji = (string)$emoji;
169 $emoji = trim($emoji);
170
171 if (!$emoji) {
172 return '';
173 }
174
175 if ($single && function_exists('\mb_substr')) {
176 $emoji = \mb_substr($emoji, 0, 4, 'UTF-8');
177 }
178
179 $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);
180
181 if ($isEmoji) {
182 return $emoji;
183 }
184 return '';
185 }
186
187 public static function sanitizeWelcomeBannerSettings($settings, $views = ['login', 'logout'])
188 {
189 $views = array_intersect($views, ['login', 'logout', 'enrolled', 'not_enrolled']);
190
191 $rules = [
192 'title' => 'sanitize_text_field',
193 'description' => 'wp_kses_post',
194 'mediaType' => 'sanitize_text_field',
195 'allowClose' => 'sanitize_text_field',
196 'enabled' => 'sanitize_text_field',
197 ];
198
199 $sanitizedSettings = [];
200 foreach ($views as $type) {
201 $typeSettings = Arr::get($settings, $type, []);
202 if (empty($typeSettings)) {
203 continue;
204 }
205
206 $bannerVideo = Arr::get($typeSettings, 'bannerVideo', []);
207 $bannerImage = Arr::get($typeSettings, 'bannerImage', '');
208 $ctaButtons = Arr::get($typeSettings, 'ctaButtons', []);
209
210 $sanitizedSettings[$type]['bannerVideo'] = self::sanitizeBannerVideo($bannerVideo);
211 $sanitizedSettings[$type]['bannerImage'] = self::sanitizeBannerImage($bannerImage);
212 $sanitizedSettings[$type]['ctaButtons'] = self::sanitizeCtaButtons($ctaButtons);
213
214 $description = Arr::get($typeSettings, 'description');
215 if (!empty($description)) {
216 $description = wp_kses_post(self::unslashMarkdown(wp_unslash($description)));
217 }
218 $sanitizedSettings[$type]['description'] = $description;
219
220 foreach ($typeSettings as $key => $value) {
221 if (isset($rules[$key]) && !in_array($key, ['bannerVideo', 'bannerImage', 'ctaButtons', 'description'])) {
222 $sanitizedSettings[$type][$key] = call_user_func($rules[$key], $value);
223 }
224 }
225 }
226
227 return $sanitizedSettings;
228 }
229
230 private static function sanitizeBannerVideo($video)
231 {
232 if (empty($video)) {
233 return [];
234 }
235
236 return array_filter([
237 'type' => sanitize_text_field(Arr::get($video, 'type', '')),
238 'url' => sanitize_url(Arr::get($video, 'url', '')),
239 'content_type' => sanitize_text_field(Arr::get($video, 'content_type', '')),
240 'provider' => sanitize_text_field(Arr::get($video, 'provider', '')),
241 'title' => sanitize_text_field(Arr::get($video, 'title', '')),
242 'author_name' => sanitize_text_field(Arr::get($video, 'author_name', '')),
243 'html' => self::sanitizeRichText(Arr::get($video, 'html', '')),
244 'image' => sanitize_url(Arr::get($video, 'image', '')),
245 ]);
246 }
247
248 private static function sanitizeBannerImage($imageUrl)
249 {
250 if (empty($imageUrl)) {
251 return '';
252 }
253
254 $media = Helper::getMediaFromUrl($imageUrl);
255 if ($media) {
256 $media->update([
257 'is_active' => true,
258 'user_id' => get_current_user_id(),
259 'object_source' => 'general'
260 ]);
261 return $media->public_url;
262 }
263
264 return sanitize_url($imageUrl);
265 }
266
267 private static function sanitizeCtaButtons($ctaButtons)
268 {
269 if (empty($ctaButtons)) {
270 return [];
271 }
272
273 $sanitizerMap = [
274 'label' => 'sanitize_text_field',
275 'link' => function ($url) {
276 return esc_url_raw($url, ['http', 'https', 'mailto']);
277 },
278 'type' => 'sanitize_text_field',
279 'newTab' => 'sanitize_text_field'
280 ];
281
282 foreach ($ctaButtons as $btnKey => $btnValue) {
283 foreach ($btnValue as $key => $value) {
284 if (isset($sanitizerMap[$key])) {
285 $ctaButtons[$btnKey][$key] = call_user_func($sanitizerMap[$key], $value);
286 }
287 }
288 }
289
290 return $ctaButtons;
291 }
292
293 public static function santizeLinkItem($item)
294 {
295 $validKeys = ['title', 'enabled', 'new_tab', 'emoji', 'icon_image', 'shape_svg', 'title', 'permalink', 'slug', 'privacy', 'membership_ids'];
296 $item = array_filter(Arr::only($item, $validKeys));
297
298 $yesNoItems = ['enabled', 'new_tab', 'is_locked', 'is_unavailable'];
299 foreach ($yesNoItems as $key) {
300 if (isset($item[$key])) {
301 $item[$key] = $item[$key] === 'yes' ? 'yes' : 'no';
302 }
303 }
304
305 $item['emoji'] = self::sanitizeEmoji(Arr::get($item, 'emoji'));
306
307 if (empty($item['slug'])) {
308 $item['slug'] = sanitize_title(Arr::get($item, 'title', ''));
309 } else {
310 $item['slug'] = sanitize_title($item['slug']);
311 }
312
313 $textTypes = ['title'];
314 foreach ($textTypes as $key) {
315 if (isset($item[$key])) {
316 $item[$key] = sanitize_text_field($item[$key]);
317 }
318 }
319 $item['permalink'] = sanitize_url(Arr::get($item, 'permalink', ''));
320
321
322 if (!empty($item['icon_image'])) {
323 $media = Helper::getMediaFromUrl($item['icon_image']);
324 if ($media) {
325 $media->update([
326 'is_active' => true,
327 'user_id' => get_current_user_id(),
328 'object_source' => 'general'
329 ]);
330 $item['icon_image'] = $media->public_url;
331 } else {
332 $item['icon_image'] = sanitize_text_field($item['icon_image']);
333 }
334 }
335
336 if (!empty($item['icon_svg'])) {
337 $item['icon_svg'] = self::sanitizeSvg($item['icon_svg']);
338 }
339
340 if (!empty($item['shape_svg'])) {
341 $item['shape_svg'] = self::sanitizeSvg($item['shape_svg']);
342 }
343
344 if (Arr::get($item, 'privacy') == 'members_only') {
345 $item['membership_ids'] = array_map('sanitize_text_field', (array)Arr::get($item, 'membership_ids', []));
346 }
347
348 return array_filter($item);
349 }
350
351 /**
352 * @param array $items
353 * @return array
354 */
355 public static function sanitizeSpaceMenuItems($items)
356 {
357 $sanitized = [];
358 $seen = [];
359
360 foreach ((array)$items as $item) {
361 $menuItem = self::sanitizeSpaceMenuItem($item);
362
363 if (!$menuItem || isset($seen[$menuItem['slug']])) {
364 continue;
365 }
366
367 $seen[$menuItem['slug']] = true;
368 $sanitized[] = $menuItem;
369 }
370
371 return $sanitized;
372 }
373
374 /**
375 * One row of a space's primary menu. Returns null for a row that cannot be rendered — no
376 * slug, a custom row with no label, or a destination that survived neither the protocol
377 * allowlist nor the page lookup.
378 *
379 * @param array $item
380 * @return array|null
381 */
382 public static function sanitizeSpaceMenuItem($item)
383 {
384 // `parent` is accepted and stored but nothing renders it yet. It holds a sibling row's
385 // slug for the one-level sub-menu, and keeping it on the write path now means that
386 // feature is additive rather than a migration of everyone's stored menu.
387 $validKeys = [
388 'slug', 'title', 'enabled', 'new_tab', 'emoji', 'icon_image', 'shape_svg',
389 'permalink', 'page_slug', 'link_type', 'privacy', 'membership_ids', 'is_custom', 'parent',
390 ];
391
392 $item = Arr::only((array)$item, $validKeys);
393
394 $isCustom = Arr::get($item, 'is_custom') === 'yes';
395
396 $slug = Utility::slugify(Arr::get($item, 'slug', ''));
397
398 if ($isCustom) {
399 // Force the prefix so a custom row's slug can never hijack a real tab's slug.
400 if (strpos($slug, 'fcom_custom_') !== 0) {
401 $slug = 'fcom_custom_' . ($slug ?: substr(md5(wp_generate_password(12, false)), 0, 10));
402 }
403 } elseif (!$slug) {
404 return null;
405 }
406
407 $sanitized = [
408 'slug' => $slug,
409 'title' => sanitize_text_field(Arr::get($item, 'title', '')),
410 'enabled' => Arr::get($item, 'enabled') === 'no' ? 'no' : 'yes',
411 'is_custom' => $isCustom ? 'yes' : 'no',
412 'parent' => sanitize_title(Arr::get($item, 'parent', '')),
413 ];
414
415 $emoji = self::sanitizeEmoji(Arr::get($item, 'emoji', ''));
416
417 if ($emoji) {
418 $sanitized['emoji'] = $emoji;
419 }
420
421 $shapeSvg = self::sanitizeSvg(Arr::get($item, 'shape_svg', ''));
422
423 if ($shapeSvg) {
424 $sanitized['shape_svg'] = $shapeSvg;
425 }
426
427 $iconImage = Arr::get($item, 'icon_image');
428
429 if ($iconImage) {
430 $media = Helper::getMediaFromUrl($iconImage);
431
432 if ($media) {
433 $media->update([
434 'is_active' => true,
435 'user_id' => get_current_user_id(),
436 'object_source' => 'general',
437 ]);
438 $sanitized['icon_image'] = $media->public_url;
439 } else {
440 $sanitized['icon_image'] = sanitize_url($iconImage);
441 }
442 }
443
444 $privacy = Arr::get($item, 'privacy');
445
446 if (!in_array($privacy, ['public', 'logged_in', 'logged_out_only', 'members_only'], true)) {
447 $privacy = 'public';
448 }
449
450 $sanitized['privacy'] = $privacy;
451
452 if ($privacy === 'members_only') {
453 $membershipIds = array_map('intval', (array)Arr::get($item, 'membership_ids', []));
454 $sanitized['membership_ids'] = array_values(array_filter($membershipIds));
455 }
456
457 if (!$isCustom) {
458 return $sanitized;
459 }
460
461 if (!$sanitized['title']) {
462 return null;
463 }
464
465 $linkType = Arr::get($item, 'link_type') === 'space_page' ? 'space_page' : 'url';
466 $sanitized['link_type'] = $linkType;
467
468 if ($linkType === 'space_page') {
469 $pageSlug = sanitize_title(Arr::get($item, 'page_slug', ''));
470
471 if (!$pageSlug) {
472 return null;
473 }
474
475 $sanitized['page_slug'] = $pageSlug;
476 $sanitized['new_tab'] = 'no';
477
478 return $sanitized;
479 }
480
481 // sanitize_url drops everything outside WordPress's protocol allowlist, so a
482 // javascript: destination comes back empty and the row is discarded.
483 $permalink = sanitize_url(Arr::get($item, 'permalink', ''));
484
485 if (!$permalink) {
486 return null;
487 }
488
489 $sanitized['permalink'] = $permalink;
490 $sanitized['new_tab'] = Arr::get($item, 'new_tab') === 'yes' ? 'yes' : 'no';
491
492 return $sanitized;
493 }
494
495 public static function sanitizeRichText($content, $print = false)
496 {
497 if ($print) {
498 echo self::sanitizeHtml($content); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
499 }
500
501 return self::sanitizeHtml($content);
502 }
503
504 public static function sanitizeHtml($html)
505 {
506 if (current_user_can('unfiltered_html')) {
507 return $html;
508 }
509
510 if (!$html) {
511 return $html;
512 }
513
514 // Return $html if it's just a plain text
515 if (!preg_match('/<[^>]*>/', $html)) {
516 return $html;
517 }
518
519 $tags = wp_kses_allowed_html('post');
520
521 // No <style> element: kses filters style="" attributes but never the text content
522 // of a <style> block, so allowing it would let any role that can author this markup
523 // persist CSS (@import, attribute-selector data exfiltration, UI redress) against
524 // every viewer. Embed/media HTML never needs a <style> element.
525
526 // iframe. Note there is deliberately no 'srcdoc' here: a srcdoc iframe without a
527 // sandbox attribute is same-origin with the portal, so allowing it would let any
528 // role that can author embed markup run script against every viewer. Real embed
529 // providers only ever use src.
530 $tags['iframe'] = [
531 'width' => [],
532 'height' => [],
533 'src' => [],
534 'title' => [],
535 'frameborder' => [],
536 'allow' => [],
537 'class' => [],
538 'id' => [],
539 'allowfullscreen' => [],
540 'referrerpolicy' => [],
541 ];
542
543 $tags = apply_filters('fluent_community/allowed_html_tags', $tags);
544
545 return wp_kses($html, $tags);
546 }
547
548 public static function santizeEmailSettings($settings)
549 {
550 $prevSettings = Utility::getEmailNotificationSettings();
551 $settings = Arr::only($settings, array_keys($prevSettings));
552
553 $yesNoFields = ['com_my_post_mail', 'reply_my_com_mail', 'mention_mail', 'digest_email_status', 'disable_powered_by'];
554 $textFields = ['send_from_name', 'reply_to_name'];
555 $emailFields = ['send_from_email', 'reply_to_email'];
556
557 foreach ($yesNoFields as $field) {
558 if (isset($settings[$field])) {
559 $settings[$field] = $settings[$field] === 'yes' ? 'yes' : 'no';
560 }
561 }
562
563 foreach ($textFields as $field) {
564 if (isset($settings[$field])) {
565 $settings[$field] = sanitize_text_field($settings[$field]);
566 }
567 }
568
569 foreach ($emailFields as $field) {
570 if (isset($settings[$field])) {
571 $settings[$field] = sanitize_email($settings[$field]);
572 }
573 }
574
575 $time = Arr::get($settings, 'daily_digest_time');
576
577 if ($time) {
578 $time = sanitize_text_field($time);
579
580 // check time is valid or not
581 if (!preg_match('/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/', $time)) {
582 $time = '09:00';
583 }
584 } else {
585 $time = '09:00';
586 }
587
588 $emailDay = Arr::get($settings, 'digest_mail_day');
589
590 if ($emailDay) {
591 $emailDay = sanitize_text_field($emailDay);
592 if (!in_array($emailDay, ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'])) {
593 $emailDay = 'tue';
594 }
595 } else {
596 $emailDay = 'tue';
597 }
598
599 $settings['digest_mail_day'] = $emailDay;
600 $settings['daily_digest_time'] = $time;
601 $settings['email_footer'] = wp_kses_post(self::unslashMarkdown(Arr::get($settings, 'email_footer')));
602 $settings['email_footer_rendered'] = FeedsHelper::mdToHtml($settings['email_footer']);
603
604 if (!empty($settings['logo'])) {
605 $settings['logo'] = sanitize_url($settings['logo']);
606 }
607
608 return $settings;
609 }
610
611 public static function sanitizeUserName($username)
612 {
613 $username = strtolower($username);
614 // check of @ symbol
615 if (strpos($username, '@') !== false) {
616 $username = explode('@', $username)[0];
617 }
618
619 $username = sanitize_user($username);
620 $username = preg_replace('/[^a-zA-Z0-9_]/', '', $username);
621 return $username;
622 }
623
624 public static function unslashMarkdown($markdown)
625 {
626 $replaceMaps = [
627 "\\\n" => PHP_EOL,
628 '\@' => '@',
629 '\\_' => '_',
630 '\\&' => '&',
631 '\\*' => '*',
632 '\\~' => '~',
633 '\\:' => ':',
634 '\\.' => '.'
635 ];
636
637 return str_replace(array_keys($replaceMaps), array_values($replaceMaps), $markdown);
638 }
639
640 public static function santizeSpaceSettings($settings = [], $privacy = 'public')
641 {
642 $yesNotFields = [
643 'restricted_post_only',
644 'verified_post_only',
645 'can_request_join',
646 'show_paywalls',
647 'show_sidebar',
648 'hide_members_count',
649 'document_library',
650 'media_gallery',
651 'disable_post_sort_by',
652 'disable_layout_style'
653 ];
654
655 $settings = Arr::only($settings, array_keys((new Space())->defaultSettings()));
656
657 foreach ($yesNotFields as $field) {
658 $settings[$field] = Arr::get($settings, $field) === 'yes' ? 'yes' : 'no';
659 }
660
661 $settings['shape_svg'] = self::sanitizeSvg(Arr::get($settings, 'shape_svg', ''));
662 if (empty($settings['shape_svg'])) {
663 $settings['emoji'] = self::sanitizeEmoji(Arr::get($settings, 'emoji', ''));
664 } else {
665 $settings['emoji'] = '';
666 }
667
668 $lockScreenType = Arr::get($settings, 'custom_lock_screen');
669 if (!in_array($lockScreenType, ['yes', 'no', 'redirect']) || $privacy !== 'private') {
670 $lockScreenType = 'no';
671 }
672 $settings['custom_lock_screen'] = $lockScreenType;
673
674
675 if ($lockScreenType === 'redirect') {
676 $redirectUrl = Arr::get($settings, 'onboard_redirect_url');
677 if (!$redirectUrl || !filter_var($redirectUrl, FILTER_VALIDATE_URL)) {
678 return new \WP_Error('invalid_redirect_url', __('Invalid redirect URL.', 'fluent-community'));
679 }
680 $settings['onboard_redirect_url'] = sanitize_url($redirectUrl);
681 }
682
683 $validOrderOptions = array_keys(Helper::getPostOrderOptions());
684 $defaultOrder = Arr::get($settings, 'default_post_sort_by', '');
685 $settings['default_post_sort_by'] = in_array($defaultOrder, $validOrderOptions) ? $defaultOrder : '';
686
687 $validCommentOrderOptions = array_keys(Helper::getCommentOrderOptions());
688 $defaultCommentOrder = Arr::get($settings, 'default_comment_sort_by', '');
689 $settings['default_comment_sort_by'] = in_array($defaultCommentOrder, $validCommentOrderOptions) ? $defaultCommentOrder : '';
690
691 $accessOptions = ['members_only', 'logged_in', 'everybody'];
692 $mediaAccess = Arr::get($settings, 'media_access');
693 $settings['media_access'] = in_array($mediaAccess, $accessOptions, true) ? $mediaAccess : 'members_only';
694
695 $documentAccess = Arr::get($settings, 'document_access');
696 $settings['document_access'] = in_array($documentAccess, $accessOptions, true) ? $documentAccess : 'members_only';
697
698 $documentUploadOptions = ['admin_only', 'members_only'];
699 $documentUpload = Arr::get($settings, 'document_upload');
700 $settings['document_upload'] = in_array($documentUpload, $documentUploadOptions, true) ? $documentUpload : 'admin_only';
701
702 return $settings;
703 }
704
705 public static function santizeEditorBody($body)
706 {
707 if (current_user_can('unfiltered_html')) {
708 return $body;
709 }
710
711 return wp_kses_post($body);
712 }
713 }
714