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
← All changes | app/Services/CustomSanitizer.php +333 -57 1.0.90 → 2.11.0 View file →
@@ -2,8 +2,9 @@
2 2
3 3 namespace FluentCommunity\App\Services;
4 4
5 5 use FluentCommunity\App\Functions\Utility;
6 +use FluentCommunity\App\Models\Space;
6 7 use FluentCommunity\Framework\Support\Arr;
7 8
8 9 class CustomSanitizer
9 10 {
@@ -8,9 +9,9 @@
8 9 class CustomSanitizer
9 10 {
10 11 public static function sanitizeMenuLink($item)
11 12 {
12 - $validKeys = ['title', 'enabled', 'permalink', 'new_tab', 'link_classes', 'shape_svg', 'emoji', 'icon_image', 'is_custom', 'is_system', 'is_locked', 'is_unavailable', 'slug'];
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'];
13 14 $item = array_filter(Arr::only($item, $validKeys));
14 15
15 16 $yesNoItems = ['enabled', 'is_custom', 'is_system', 'is_locked', 'is_unavailable'];
16 17 foreach ($yesNoItems as $key) {
@@ -18,42 +19,41 @@
18 19 $item[$key] = $item[$key] === 'yes' ? 'yes' : 'no';
19 20 }
20 21 }
21 22
22 - $textTypes = ['title', 'new_tab', 'link_classes', 'slug'];
23 + $textTypes = ['title', 'new_tab', 'link_classes', 'slug', 'privacy'];
23 24 foreach ($textTypes as $key) {
24 25 if (isset($item[$key])) {
25 26 $item[$key] = sanitize_text_field($item[$key]);
26 27 }
27 28 }
28 - $item['permalink'] = sanitize_url($item['permalink']);
29 29
30 - if (Arr::get($item, 'is_system') !== 'yes') {
31 - if (!empty($item['shape_svg'])) {
32 - $item['shape_svg'] = self::sanitizeSvg($item['shape_svg']);
33 - }
30 + $item['permalink'] = sanitize_url(Arr::get($item, 'permalink', ''));
34 31
35 - if (!empty($item['emoji'])) {
36 - $item['emoji'] = self::sanitizeEmoji($item['emoji']);
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']);
37 51 }
52 + }
38 53
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 - } else {
53 - $item['shape_svg'] = '';
54 - $item['emoji'] = '';
55 - $item['icon_image'] = '';
54 + if (Arr::get($item, 'privacy') == 'members_only') {
55 + $item['membership_ids'] = array_map('sanitize_text_field', (array)Arr::get($item, 'membership_ids', []));
56 56 }
57 57
58 58 return $item;
59 59 }
@@ -80,12 +80,12 @@
80 80 $svg_content = preg_replace('/<iframe\b[^>]*>(.*?)<\/iframe>/is', '', $svg_content);
81 81 $svg_content = preg_replace('/on\w+="[^"]*"/i', '', $svg_content);
82 82
83 83 $allowed_tags = [
84 - '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],
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 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, 'style' => 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, 'style' => 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 88 'circle' => ['cx' => true, 'cy' => true, 'r' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'transform' => true],
89 89 'ellipse' => ['cx' => true, 'cy' => true, 'rx' => true, 'ry' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'transform' => true],
90 90 'line' => ['x1' => true, 'x2' => true, 'y1' => true, 'y2' => true, 'stroke' => true, 'stroke-width' => true, 'transform' => true],
91 91 'polyline' => ['points' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'transform' => true],
@@ -97,19 +97,35 @@
97 97 'stop' => ['offset' => true, 'stop-color' => true, 'stop-opacity' => true],
98 98 'linearGradient' => ['id' => true, 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, 'gradientUnits' => true, 'gradientTransform' => true],
99 99 'radialGradient' => ['id' => true, 'cx' => true, 'cy' => true, 'r' => true, 'fx' => true, 'fy' => true, 'gradientUnits' => true, 'gradientTransform' => true],
100 100 'mask' => ['id' => true, 'maskUnits' => true, 'maskContentUnits' => true, 'x' => true, 'y' => true, 'width' => true, 'height' => true],
101 - 'use' => ['xlink:href' => true, 'x' => true, 'y' => true, 'width' => true, 'height' => true],
101 + 'use' => ['x' => true, 'y' => true, 'width' => true, 'height' => true],
102 102 'title' => [],
103 103 'desc' => [],
104 104 ];
105 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 +
106 118 // Load the SVG string into a DOMDocument and discard errors for malformed XML
107 119 $dom = new \DOMDocument();
108 120 libxml_use_internal_errors(true);
109 - $dom->loadXML($svg_content);
121 + $loaded = $dom->loadXML($svg_content);
110 122 libxml_clear_errors();
111 123
124 + if (!$loaded || !$dom->documentElement) {
125 + return '';
126 + }
127 +
112 128 // Sanitize by removing unwanted tags and attributes
113 129 self::sanitizeNode($dom->documentElement, $allowed_tags);
114 130
115 131 return $dom->saveXML($dom->documentElement);
@@ -117,8 +133,9 @@
117 133
118 134 private static function sanitizeNode(\DOMNode $node, array $allowed_tags)
119 135 {
120 136 if ($node->nodeType === XML_ELEMENT_NODE) {
137 + /** @var \DOMElement $node */
121 138 if (!isset($allowed_tags[$node->nodeName])) {
122 139 $node->parentNode->removeChild($node);
123 140 return;
124 141 }
@@ -155,12 +172,12 @@
155 172 return '';
156 173 }
157 174
158 175 if ($single && function_exists('\mb_substr')) {
159 - $emoji = \mb_substr($emoji, 0, 1, 'UTF-8');
176 + $emoji = \mb_substr($emoji, 0, 4, 'UTF-8');
160 177 }
161 178
162 - $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);
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);
163 180
164 181 if ($isEmoji) {
165 182 return $emoji;
166 183 }
@@ -166,20 +183,22 @@
166 183 }
167 184 return '';
168 185 }
169 186
170 - public static function sanitizeWelcomeBannerSettings($settings)
187 + public static function sanitizeWelcomeBannerSettings($settings, $views = ['login', 'logout'])
171 188 {
189 + $views = array_intersect($views, ['login', 'logout', 'enrolled', 'not_enrolled']);
190 +
172 191 $rules = [
173 - 'title' => 'sanitize_text_field',
174 - 'description' => 'wp_kses_post',
175 - 'mediaType' => 'sanitize_text_field',
176 - 'allowClose' => 'sanitize_text_field',
177 - 'enabled' => 'sanitize_text_field',
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',
178 197 ];
179 198
180 199 $sanitizedSettings = [];
181 - foreach (['login', 'logout'] as $type) {
200 + foreach ($views as $type) {
182 201 $typeSettings = Arr::get($settings, $type, []);
183 202 if (empty($typeSettings)) {
184 203 continue;
185 204 }
@@ -190,12 +209,17 @@
190 209
191 210 $sanitizedSettings[$type]['bannerVideo'] = self::sanitizeBannerVideo($bannerVideo);
192 211 $sanitizedSettings[$type]['bannerImage'] = self::sanitizeBannerImage($bannerImage);
193 212 $sanitizedSettings[$type]['ctaButtons'] = self::sanitizeCtaButtons($ctaButtons);
194 - $sanitizedSettings[$type]['description'] = self::unslashMarkdown(Arr::get($typeSettings, 'description'));
195 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 +
196 220 foreach ($typeSettings as $key => $value) {
197 - if (isset($rules[$key]) && !in_array($key, ['bannerVideo', 'bannerImage', 'ctaButtons'])) {
221 + if (isset($rules[$key]) && !in_array($key, ['bannerVideo', 'bannerImage', 'ctaButtons', 'description'])) {
198 222 $sanitizedSettings[$type][$key] = call_user_func($rules[$key], $value);
199 223 }
200 224 }
201 225 }
@@ -212,12 +236,13 @@
212 236 return array_filter([
213 237 'type' => sanitize_text_field(Arr::get($video, 'type', '')),
214 238 'url' => sanitize_url(Arr::get($video, 'url', '')),
215 239 'content_type' => sanitize_text_field(Arr::get($video, 'content_type', '')),
216 - 'provider' => sanitize_url(Arr::get($video, 'provider', '')),
240 + 'provider' => sanitize_text_field(Arr::get($video, 'provider', '')),
217 241 'title' => sanitize_text_field(Arr::get($video, 'title', '')),
218 242 'author_name' => sanitize_text_field(Arr::get($video, 'author_name', '')),
219 243 'html' => self::sanitizeRichText(Arr::get($video, 'html', '')),
244 + 'image' => sanitize_url(Arr::get($video, 'image', '')),
220 245 ]);
221 246 }
222 247
223 248 private static function sanitizeBannerImage($imageUrl)
@@ -228,10 +253,10 @@
228 253
229 254 $media = Helper::getMediaFromUrl($imageUrl);
230 255 if ($media) {
231 256 $media->update([
232 - 'is_active' => true,
233 - 'user_id' => get_current_user_id(),
257 + 'is_active' => true,
258 + 'user_id' => get_current_user_id(),
234 259 'object_source' => 'general'
235 260 ]);
236 261 return $media->public_url;
237 262 }
@@ -246,9 +271,11 @@
246 271 }
247 272
248 273 $sanitizerMap = [
249 274 'label' => 'sanitize_text_field',
250 - 'link' => 'sanitize_url',
275 + 'link' => function ($url) {
276 + return esc_url_raw($url, ['http', 'https', 'mailto']);
277 + },
251 278 'type' => 'sanitize_text_field',
252 279 'newTab' => 'sanitize_text_field'
253 280 ];
254 281
@@ -264,9 +291,9 @@
264 291 }
265 292
266 293 public static function santizeLinkItem($item)
267 294 {
268 - $validKeys = ['title', 'enabled', 'new_tab', 'emoji', 'icon_image', 'shape_svg', 'title', 'permalink', 'slug'];
295 + $validKeys = ['title', 'enabled', 'new_tab', 'emoji', 'icon_image', 'shape_svg', 'title', 'permalink', 'slug', 'privacy', 'membership_ids'];
269 296 $item = array_filter(Arr::only($item, $validKeys));
270 297
271 298 $yesNoItems = ['enabled', 'new_tab', 'is_locked', 'is_unavailable'];
272 299 foreach ($yesNoItems as $key) {
@@ -277,9 +304,9 @@
277 304
278 305 $item['emoji'] = self::sanitizeEmoji(Arr::get($item, 'emoji'));
279 306
280 307 if (empty($item['slug'])) {
281 - $item['slug'] = sanitize_title($item['title']);
308 + $item['slug'] = sanitize_title(Arr::get($item, 'title', ''));
282 309 } else {
283 310 $item['slug'] = sanitize_title($item['slug']);
284 311 }
285 312
@@ -288,9 +315,9 @@
288 315 if (isset($item[$key])) {
289 316 $item[$key] = sanitize_text_field($item[$key]);
290 317 }
291 318 }
292 - $item['permalink'] = sanitize_url($item['permalink']);
319 + $item['permalink'] = sanitize_url(Arr::get($item, 'permalink', ''));
293 320
294 321
295 322 if (!empty($item['icon_image'])) {
296 323 $media = Helper::getMediaFromUrl($item['icon_image']);
@@ -309,11 +336,163 @@
309 336 if (!empty($item['icon_svg'])) {
310 337 $item['icon_svg'] = self::sanitizeSvg($item['icon_svg']);
311 338 }
312 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 +
313 348 return array_filter($item);
314 349 }
315 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 +
316 495 public static function sanitizeRichText($content, $print = false)
317 496 {
318 497 if ($print) {
319 498 echo self::sanitizeHtml($content); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
@@ -337,18 +516,22 @@
337 516 return $html;
338 517 }
339 518
340 519 $tags = wp_kses_allowed_html('post');
341 - $tags['style'] = [
342 - 'types' => [],
343 - ];
344 520
345 - // iframe
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.
346 530 $tags['iframe'] = [
347 531 'width' => [],
348 532 'height' => [],
349 533 'src' => [],
350 - 'srcdoc' => [],
351 534 'title' => [],
352 535 'frameborder' => [],
353 536 'allow' => [],
354 537 'class' => [],
@@ -354,9 +537,8 @@
354 537 'class' => [],
355 538 'id' => [],
356 539 'allowfullscreen' => [],
357 540 'referrerpolicy' => [],
358 - 'style' => [],
359 541 ];
360 542
361 543 $tags = apply_filters('fluent_community/allowed_html_tags', $tags);
362 544
@@ -371,9 +553,8 @@
371 553 $yesNoFields = ['com_my_post_mail', 'reply_my_com_mail', 'mention_mail', 'digest_email_status', 'disable_powered_by'];
372 554 $textFields = ['send_from_name', 'reply_to_name'];
373 555 $emailFields = ['send_from_email', 'reply_to_email'];
374 556
375 -
376 557 foreach ($yesNoFields as $field) {
377 558 if (isset($settings[$field])) {
378 559 $settings[$field] = $settings[$field] === 'yes' ? 'yes' : 'no';
379 560 }
@@ -419,13 +600,23 @@
419 600 $settings['daily_digest_time'] = $time;
420 601 $settings['email_footer'] = wp_kses_post(self::unslashMarkdown(Arr::get($settings, 'email_footer')));
421 602 $settings['email_footer_rendered'] = FeedsHelper::mdToHtml($settings['email_footer']);
422 603
604 + if (!empty($settings['logo'])) {
605 + $settings['logo'] = sanitize_url($settings['logo']);
606 + }
607 +
423 608 return $settings;
424 609 }
425 610
426 611 public static function sanitizeUserName($username)
427 612 {
613 + $username = strtolower($username);
614 + // check of @ symbol
615 + if (strpos($username, '@') !== false) {
616 + $username = explode('@', $username)[0];
617 + }
618 +
428 619 $username = sanitize_user($username);
429 620 $username = preg_replace('/[^a-zA-Z0-9_]/', '', $username);
430 621 return $username;
431 622 }
@@ -431,7 +622,92 @@
431 622 }
432 623
433 624 public static function unslashMarkdown($markdown)
434 625 {
435 - return str_replace(["\\\n", '\@', '\\_', '\\&', '\\*', '\\~'], ["\n", '@', '_', '&', '*', '~'], $markdown);
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);
436 712 }
437 713 }