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

438 lines 16.7 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\Framework\Support\Arr;
7
8 class CustomSanitizer
9 {
10 public static function sanitizeMenuLink($item)
11 {
12 $validKeys = ['title', 'enabled', 'permalink', 'new_tab', 'link_classes', 'shape_svg', 'emoji', 'icon_image', 'is_custom', 'is_system', 'is_locked', 'is_unavailable', 'slug'];
13 $item = array_filter(Arr::only($item, $validKeys));
14
15 $yesNoItems = ['enabled', 'is_custom', 'is_system', 'is_locked', 'is_unavailable'];
16 foreach ($yesNoItems as $key) {
17 if (isset($item[$key])) {
18 $item[$key] = $item[$key] === 'yes' ? 'yes' : 'no';
19 }
20 }
21
22 $textTypes = ['title', 'new_tab', 'link_classes', 'slug'];
23 foreach ($textTypes as $key) {
24 if (isset($item[$key])) {
25 $item[$key] = sanitize_text_field($item[$key]);
26 }
27 }
28 $item['permalink'] = sanitize_url($item['permalink']);
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 }
34
35 if (!empty($item['emoji'])) {
36 $item['emoji'] = self::sanitizeEmoji($item['emoji']);
37 }
38
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'] = '';
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, 'style' => 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, '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],
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' => ['xlink:href' => true, '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 // Sanitize by removing unwanted tags and attributes
113 self::sanitizeNode($dom->documentElement, $allowed_tags);
114
115 return $dom->saveXML($dom->documentElement);
116 }
117
118 private static function sanitizeNode(\DOMNode $node, array $allowed_tags)
119 {
120 if ($node->nodeType === XML_ELEMENT_NODE) {
121 if (!isset($allowed_tags[$node->nodeName])) {
122 $node->parentNode->removeChild($node);
123 return;
124 }
125
126 // Check attributes
127 $attributes = $node->attributes;
128 $length = $attributes->length;
129 for ($i = $length - 1; $i >= 0; $i--) {
130 $attr = $attributes->item($i);
131 $attr_name = $attr->nodeName;
132 if (!isset($allowed_tags[$node->nodeName][$attr_name])) {
133 $node->removeAttribute($attr_name);
134 } else {
135 // Sanitize attribute values
136 // FILTER_SANITIZE_STRING is deprecated in PHP 8.1
137 $sanitized_value = htmlspecialchars($attr->nodeValue, ENT_QUOTES, 'UTF-8');
138 $node->setAttribute($attr_name, $sanitized_value);
139 }
140 }
141 }
142
143 // Recursively sanitize child nodes
144 for ($i = $node->childNodes->length - 1; $i >= 0; $i--) {
145 self::sanitizeNode($node->childNodes->item($i), $allowed_tags);
146 }
147 }
148
149 public static function sanitizeEmoji($emoji, $single = true)
150 {
151 $emoji = (string)$emoji;
152 $emoji = trim($emoji);
153
154 if (!$emoji) {
155 return '';
156 }
157
158 if ($single && function_exists('\mb_substr')) {
159 $emoji = \mb_substr($emoji, 0, 1, 'UTF-8');
160 }
161
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);
163
164 if ($isEmoji) {
165 return $emoji;
166 }
167 return '';
168 }
169
170 public static function sanitizeWelcomeBannerSettings($settings)
171 {
172 $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',
178 ];
179
180 $sanitizedSettings = [];
181 foreach (['login', 'logout'] as $type) {
182 $typeSettings = Arr::get($settings, $type, []);
183 if (empty($typeSettings)) {
184 continue;
185 }
186
187 $bannerVideo = Arr::get($typeSettings, 'bannerVideo', []);
188 $bannerImage = Arr::get($typeSettings, 'bannerImage', '');
189 $ctaButtons = Arr::get($typeSettings, 'ctaButtons', []);
190
191 $sanitizedSettings[$type]['bannerVideo'] = self::sanitizeBannerVideo($bannerVideo);
192 $sanitizedSettings[$type]['bannerImage'] = self::sanitizeBannerImage($bannerImage);
193 $sanitizedSettings[$type]['ctaButtons'] = self::sanitizeCtaButtons($ctaButtons);
194 $sanitizedSettings[$type]['description'] = self::unslashMarkdown(Arr::get($typeSettings, 'description'));
195
196 foreach ($typeSettings as $key => $value) {
197 if (isset($rules[$key]) && !in_array($key, ['bannerVideo', 'bannerImage', 'ctaButtons'])) {
198 $sanitizedSettings[$type][$key] = call_user_func($rules[$key], $value);
199 }
200 }
201 }
202
203 return $sanitizedSettings;
204 }
205
206 private static function sanitizeBannerVideo($video)
207 {
208 if (empty($video)) {
209 return [];
210 }
211
212 return array_filter([
213 'type' => sanitize_text_field(Arr::get($video, 'type', '')),
214 'url' => sanitize_url(Arr::get($video, 'url', '')),
215 'content_type' => sanitize_text_field(Arr::get($video, 'content_type', '')),
216 'provider' => sanitize_url(Arr::get($video, 'provider', '')),
217 'title' => sanitize_text_field(Arr::get($video, 'title', '')),
218 'author_name' => sanitize_text_field(Arr::get($video, 'author_name', '')),
219 'html' => self::sanitizeRichText(Arr::get($video, 'html', '')),
220 ]);
221 }
222
223 private static function sanitizeBannerImage($imageUrl)
224 {
225 if (empty($imageUrl)) {
226 return '';
227 }
228
229 $media = Helper::getMediaFromUrl($imageUrl);
230 if ($media) {
231 $media->update([
232 'is_active' => true,
233 'user_id' => get_current_user_id(),
234 'object_source' => 'general'
235 ]);
236 return $media->public_url;
237 }
238
239 return sanitize_url($imageUrl);
240 }
241
242 private static function sanitizeCtaButtons($ctaButtons)
243 {
244 if (empty($ctaButtons)) {
245 return [];
246 }
247
248 $sanitizerMap = [
249 'label' => 'sanitize_text_field',
250 'link' => 'sanitize_url',
251 'type' => 'sanitize_text_field',
252 'newTab' => 'sanitize_text_field'
253 ];
254
255 foreach ($ctaButtons as $btnKey => $btnValue) {
256 foreach ($btnValue as $key => $value) {
257 if (isset($sanitizerMap[$key])) {
258 $ctaButtons[$btnKey][$key] = call_user_func($sanitizerMap[$key], $value);
259 }
260 }
261 }
262
263 return $ctaButtons;
264 }
265
266 public static function santizeLinkItem($item)
267 {
268 $validKeys = ['title', 'enabled', 'new_tab', 'emoji', 'icon_image', 'shape_svg', 'title', 'permalink', 'slug'];
269 $item = array_filter(Arr::only($item, $validKeys));
270
271 $yesNoItems = ['enabled', 'new_tab', 'is_locked', 'is_unavailable'];
272 foreach ($yesNoItems as $key) {
273 if (isset($item[$key])) {
274 $item[$key] = $item[$key] === 'yes' ? 'yes' : 'no';
275 }
276 }
277
278 $item['emoji'] = self::sanitizeEmoji(Arr::get($item, 'emoji'));
279
280 if (empty($item['slug'])) {
281 $item['slug'] = sanitize_title($item['title']);
282 } else {
283 $item['slug'] = sanitize_title($item['slug']);
284 }
285
286 $textTypes = ['title'];
287 foreach ($textTypes as $key) {
288 if (isset($item[$key])) {
289 $item[$key] = sanitize_text_field($item[$key]);
290 }
291 }
292 $item['permalink'] = sanitize_url($item['permalink']);
293
294
295 if (!empty($item['icon_image'])) {
296 $media = Helper::getMediaFromUrl($item['icon_image']);
297 if ($media) {
298 $media->update([
299 'is_active' => true,
300 'user_id' => get_current_user_id(),
301 'object_source' => 'general'
302 ]);
303 $item['icon_image'] = $media->public_url;
304 } else {
305 $item['icon_image'] = sanitize_text_field($item['icon_image']);
306 }
307 }
308
309 if (!empty($item['icon_svg'])) {
310 $item['icon_svg'] = self::sanitizeSvg($item['icon_svg']);
311 }
312
313 return array_filter($item);
314 }
315
316 public static function sanitizeRichText($content, $print = false)
317 {
318 if ($print) {
319 echo self::sanitizeHtml($content); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
320 }
321
322 return self::sanitizeHtml($content);
323 }
324
325 public static function sanitizeHtml($html)
326 {
327 if (current_user_can('unfiltered_html')) {
328 return $html;
329 }
330
331 if (!$html) {
332 return $html;
333 }
334
335 // Return $html if it's just a plain text
336 if (!preg_match('/<[^>]*>/', $html)) {
337 return $html;
338 }
339
340 $tags = wp_kses_allowed_html('post');
341 $tags['style'] = [
342 'types' => [],
343 ];
344
345 // iframe
346 $tags['iframe'] = [
347 'width' => [],
348 'height' => [],
349 'src' => [],
350 'srcdoc' => [],
351 'title' => [],
352 'frameborder' => [],
353 'allow' => [],
354 'class' => [],
355 'id' => [],
356 'allowfullscreen' => [],
357 'referrerpolicy' => [],
358 'style' => [],
359 ];
360
361 $tags = apply_filters('fluent_community/allowed_html_tags', $tags);
362
363 return wp_kses($html, $tags);
364 }
365
366 public static function santizeEmailSettings($settings)
367 {
368 $prevSettings = Utility::getEmailNotificationSettings();
369 $settings = Arr::only($settings, array_keys($prevSettings));
370
371 $yesNoFields = ['com_my_post_mail', 'reply_my_com_mail', 'mention_mail', 'digest_email_status', 'disable_powered_by'];
372 $textFields = ['send_from_name', 'reply_to_name'];
373 $emailFields = ['send_from_email', 'reply_to_email'];
374
375
376 foreach ($yesNoFields as $field) {
377 if (isset($settings[$field])) {
378 $settings[$field] = $settings[$field] === 'yes' ? 'yes' : 'no';
379 }
380 }
381
382 foreach ($textFields as $field) {
383 if (isset($settings[$field])) {
384 $settings[$field] = sanitize_text_field($settings[$field]);
385 }
386 }
387
388 foreach ($emailFields as $field) {
389 if (isset($settings[$field])) {
390 $settings[$field] = sanitize_email($settings[$field]);
391 }
392 }
393
394 $time = Arr::get($settings, 'daily_digest_time');
395
396 if ($time) {
397 $time = sanitize_text_field($time);
398
399 // check time is valid or not
400 if (!preg_match('/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/', $time)) {
401 $time = '09:00';
402 }
403 } else {
404 $time = '09:00';
405 }
406
407 $emailDay = Arr::get($settings, 'digest_mail_day');
408
409 if ($emailDay) {
410 $emailDay = sanitize_text_field($emailDay);
411 if (!in_array($emailDay, ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'])) {
412 $emailDay = 'tue';
413 }
414 } else {
415 $emailDay = 'tue';
416 }
417
418 $settings['digest_mail_day'] = $emailDay;
419 $settings['daily_digest_time'] = $time;
420 $settings['email_footer'] = wp_kses_post(self::unslashMarkdown(Arr::get($settings, 'email_footer')));
421 $settings['email_footer_rendered'] = FeedsHelper::mdToHtml($settings['email_footer']);
422
423 return $settings;
424 }
425
426 public static function sanitizeUserName($username)
427 {
428 $username = sanitize_user($username);
429 $username = preg_replace('/[^a-zA-Z0-9_]/', '', $username);
430 return $username;
431 }
432
433 public static function unslashMarkdown($markdown)
434 {
435 return str_replace(["\\\n", '\@', '\\_', '\\&', '\\*', '\\~'], ["\n", '@', '_', '&', '*', '~'], $markdown);
436 }
437 }
438