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/Models/BaseSpace.php +250 -51 1.0.962.11.0 View file →
@@ -1,14 +1,39 @@
1 1 <?php
2 2
3 3 namespace FluentCommunity\App\Models;
4 4
5 +use FluentCommunity\App\App;
5 6 use FluentCommunity\App\Functions\Utility;
6 7 use FluentCommunity\App\Services\CustomSanitizer;
7 8 use FluentCommunity\App\Services\LockscreenService;
8 9 use FluentCommunity\App\Services\Helper;
10 +use FluentCommunity\App\Services\SpaceMenuService;
9 11 use FluentCommunity\Framework\Support\Arr;
10 12
13 +/**
14 + * @property int $id
15 + * @property int|null $created_by
16 + * @property int|null $parent_id
17 + * @property string $title
18 + * @property string $slug
19 + * @property string|null $description
20 + * @property string|null $logo
21 + * @property string|null $cover_photo
22 + * @property string|null $type
23 + * @property string|null $privacy
24 + * @property string|null $status
25 + * @property int|null $serial
26 + * @property array $settings
27 + * @property string|null $created_at
28 + * @property string|null $updated_at
29 + * @property array|null $permissions
30 + * @property string|null $description_rendered
31 + * @property mixed $membership
32 + * @property array|null $topics
33 + * @property array|null $header_links
34 + * @property array|null $lockscreen_config
35 + */
11 36 class BaseSpace extends Model
12 37 {
13 38 protected $table = 'fcom_spaces';
14 39
@@ -13,9 +38,9 @@
13 38 protected $table = 'fcom_spaces';
14 39
15 40 protected static $type = 'community';
16 41
17 - protected $guarded = ['id'];
42 + protected $guarded = [ 'id' ];
18 43
19 44 protected $fillable = [
20 45 'created_by',
21 46 'parent_id',
@@ -27,19 +52,23 @@
27 52 'type',
28 53 'privacy',
29 54 'status',
30 55 'serial',
31 - 'settings'
56 + 'settings',
32 57 ];
33 58
34 59 protected $searchable = [
35 60 'title',
36 - 'description'
61 + 'description',
37 62 ];
38 63
64 + public $_preloadedMembershipUserId = null;
65 + public $_preloadedMembership = null;
66 +
39 67 public static function boot()
40 68 {
41 69 parent::boot();
70 +
42 71 static::creating(function ($model) {
43 72 if (empty($model->created_by)) {
44 73 $model->created_by = get_current_user_id();
45 74 }
@@ -44,11 +73,11 @@
44 73 $model->created_by = get_current_user_id();
45 74 }
46 75
47 76 if (empty($model->slug)) {
48 - $slug = sanitize_title($model->tilte, time());
49 - $model->slug = $slug;
77 + $model->slug = self::generateNewSlug($model);
50 78 }
79 +
51 80 $model->type = static::$type;
52 81 });
53 82
54 83 static::addGlobalScope('type', function ($query) {
@@ -60,11 +89,11 @@
60 89 });
61 90
62 91 static::deleting(function ($space) {
63 92 Media::where('sub_object_id', $space->id)
64 - ->whereIn('object_source', ['space_logo', 'space_cover_photo'])
93 + ->whereIn('object_source', [ 'space_logo', 'space_cover_photo' ])
65 94 ->update([
66 - 'is_active' => 0
95 + 'is_active' => 0,
67 96 ]);
68 97 });
69 98 }
70 99
@@ -77,8 +106,17 @@
77 106 {
78 107 return $this->belongsTo(SpaceUserPivot::class, 'id', 'space_id');
79 108 }
80 109
110 + public function admins()
111 + {
112 + return $this->belongsToMany(User::class, 'fcom_space_user', 'space_id', 'user_id')
113 + ->where(function ($query) {
114 + $query->where('role', 'admin')
115 + ->orWhere('role', 'moderator');
116 + });
117 + }
118 +
81 119 public function scopeSearchBy($query, $search)
82 120 {
83 121 if ($search) {
84 122 $fields = $this->searchable;
@@ -92,8 +130,13 @@
92 130
93 131 return $query;
94 132 }
95 133
134 + public function scopeOnlyMain($query)
135 + {
136 + return $query->withoutGlobalScopes()->whereIn('type', [ 'community', 'course' ]);
137 + }
138 +
96 139 public function scopeFilterByUserId($query, $userId)
97 140 {
98 141 if (!$userId) {
99 142 return $query->where('privacy', 'public');
@@ -126,18 +169,23 @@
126 169 {
127 170 return $this->hasMany(Feed::class, 'space_id', 'id');
128 171 }
129 172
173 + public function comments()
174 + {
175 + return $this->hasManyThrough(Comment::class, Feed::class, 'space_id', 'post_id');
176 + }
177 +
130 178 public function members()
131 179 {
132 180 return $this->belongsToMany(User::class, 'fcom_space_user', 'space_id', 'user_id')
133 - ->withPivot(['role', 'created_at', 'status']);
181 + ->withPivot([ 'role', 'created_at', 'status' ]);
134 182 }
135 183
136 184 public function x_members()
137 185 {
138 186 return $this->belongsToMany(XProfile::class, 'fcom_space_user', 'space_id', 'user_id', 'id', 'user_id')
139 - ->withPivot(['role', 'created_at', 'status']);
187 + ->withPivot([ 'role', 'created_at', 'status' ]);
140 188 }
141 189
142 190 public function group()
143 191 {
@@ -149,11 +197,43 @@
149 197 if (!$userId) {
150 198 return null;
151 199 }
152 200
201 + if ($this->_preloadedMembershipUserId == $userId) {
202 + return $this->_preloadedMembership;
203 + }
204 +
153 205 return $this->members()->where('user_id', $userId)->first();
154 206 }
155 207
208 + public static function preloadMemberships($spaces, $userId)
209 + {
210 + if (!$userId || $spaces->isEmpty()) {
211 + return;
212 + }
213 +
214 + $spaceIds = $spaces->pluck('id')->toArray();
215 +
216 + $pivots = SpaceUserPivot::where('user_id', $userId)
217 + ->whereIn('space_id', $spaceIds)
218 + ->get()
219 + ->keyBy('space_id');
220 +
221 + foreach ($spaces as $space) {
222 + $space->_preloadedMembershipUserId = $userId;
223 + $pivot = $pivots->get($space->id);
224 + $space->_preloadedMembership = $pivot ? self::pivotToMembership($pivot) : null;
225 + }
226 + }
227 +
228 + private static function pivotToMembership($pivot)
229 + {
230 + $membership = new \stdClass();
231 + $membership->pivot = (object) $pivot->toArray();
232 + $membership->ID = $pivot->user_id;
233 + return $membership;
234 + }
235 +
156 236 public function isCourseSpace()
157 237 {
158 238 return $this->type == 'course';
159 239 }
@@ -163,19 +243,22 @@
163 243 if (Helper::isSiteAdmin($userId)) {
164 244 return true;
165 245 }
166 246
247 + $roles = [ 'admin' ];
248 +
249 + if ($checkModerator) {
250 + if (Helper::isModerator()) {
251 + return true;
252 + }
253 + $roles[] = 'moderator';
254 + }
255 +
167 256 $membership = $this->getMembership($userId);
168 257 if (!$membership) {
169 258 return false;
170 259 }
171 260
172 - $roles = ['admin'];
173 -
174 - if ($checkModerator) {
175 - $roles[] = 'moderator';
176 - }
177 -
178 261 return in_array($membership->pivot->role, $roles);
179 262 }
180 263
181 264 public function updateCustomData($data, $removeSrc = false)
@@ -203,32 +286,30 @@
203 286 $this->privacy = sanitize_text_field($data['privacy']);
204 287 }
205 288
206 289 if (isset($data['parent_id'])) {
207 - $group = SpaceGroup::find($data['parent_id']);
208 - if (!$group) {
209 - throw new \Exception('Invalid group id', 400);
290 + if (!empty($data['parent_id'])) {
291 + $group = SpaceGroup::find($data['parent_id']);
292 + if (!$group) {
293 + throw new \Exception(esc_html__('Invalid group id', 'fluent-community'), 400);
294 + }
295 + $this->parent_id = $group->id;
296 + } else {
297 + $this->parent_id = null;
210 298 }
211 -
212 - $this->parent_id = $group->id;
213 299 }
214 300
215 301 if (isset($data['settings'])) {
216 - $shapSvg = '';
217 - if (!empty($data['settings']['shape_svg'])) {
218 - $shapSvg = CustomSanitizer::sanitizeSvg($data['settings']['shape_svg']);
219 - }
220 302
221 - $exisitingSetting = $this->settings;
222 - $settings = Arr::only(fluentCommunitySanitizeArray($data['settings']), array_keys($this->defaultSettings()));
303 + $settings = CustomSanitizer::santizeSpaceSettings($data['settings'], $this->privacy);
223 304
224 - if ($shapSvg) {
225 - $settings['shape_svg'] = $shapSvg;
226 - } else if (!empty($settings['emoji'])) {
227 - $settings['emoji'] = CustomSanitizer::sanitizeEmoji($settings['emoji']);
305 + if (is_wp_error($settings)) {
306 + return $settings;
228 307 }
229 308
309 + $exisitingSetting = $this->settings;
230 310 $settings['links'] = Arr::get($exisitingSetting, 'links', []);
311 + $settings[SpaceMenuService::SETTINGS_KEY] = Arr::get($exisitingSetting, SpaceMenuService::SETTINGS_KEY, []);
231 312
232 313 if (isset($settings['og_image'])) {
233 314 $ogImageUrl = Arr::get($settings, 'og_image');
234 315 if ($ogImageUrl) {
@@ -234,14 +315,14 @@
234 315 if ($ogImageUrl) {
235 316 $ogImageMedia = Helper::getMediaFromUrl($ogImageUrl);
236 317 if ($ogImageMedia && $ogImageMedia->is_active) {
237 318 unset($settings['og_image']);
238 - } else if ($ogImageMedia) {
319 + } elseif ($ogImageMedia) {
239 320 $ogImageMedia->update([
240 321 'is_active' => true,
241 322 'user_id' => get_current_user_id(),
242 323 'sub_object_id' => $this->id,
243 - 'object_source' => 'space_og_media'
324 + 'object_source' => 'space_og_media',
244 325 ]);
245 326 $settings['og_image'] = $ogImageMedia->public_url;
246 327 } else {
247 328 $settings['og_image'] = sanitize_url($ogImageUrl);
@@ -254,8 +335,28 @@
254 335 $settings = wp_parse_args($settings, $exisitingSetting);
255 336 $this->settings = $settings;
256 337 }
257 338
339 + if (!empty($data['slug']) && $data['slug'] !== $this->slug) {
340 + $newSlug = preg_replace('/[^a-zA-Z0-9-_]/', '', $data['slug']);
341 +
342 + $newSlug = sanitize_title($newSlug);
343 +
344 + if (empty($newSlug)) {
345 + throw new \Exception('Invalid slug', 400);
346 + }
347 +
348 + $exist = App::getInstance('db')->table('fcom_spaces')->where('slug', $newSlug)
349 + ->where('id', '!=', $this->id)
350 + ->exists();
351 +
352 + if ($exist) {
353 + throw new \Exception(esc_html__('Slug already exists. Please use a different slug', 'fluent-community'), 400);
354 + }
355 +
356 + $this->slug = $newSlug;
357 + }
358 +
258 359 $deletePhotos = array_filter($deletePhotos);
259 360 if ($removeSrc && $deletePhotos) {
260 361 $deletePhotos = array_filter($deletePhotos);
261 362 do_action('fluent_community/remove_medias_by_url', $deletePhotos, [
@@ -274,17 +375,29 @@
274 375
275 376 return $this;
276 377 }
277 378
379 + public function isContentSpace()
380 + {
381 + return in_array($this->type, [ 'community', 'course' ]);
382 + }
383 +
278 384 public function getSettingsAttribute($value)
279 385 {
280 - $settings = maybe_unserialize($value);
386 + $settings = Utility::safeUnserialize($value);
281 387
282 388 if (!$settings) {
283 389 $settings = [];
284 390 }
285 391
286 - return wp_parse_args($settings, $this->defaultSettings());
392 + $settings = wp_parse_args($settings, $this->defaultSettings());
393 +
394 + if (!defined('FLUENT_COMMUNITY_PRO')) {
395 + $settings['document_library'] = 'no';
396 + $settings['media_gallery'] = 'no';
397 + }
398 +
399 + return $settings;
287 400 }
288 401
289 402 public function defaultSettings()
290 403 {
@@ -297,8 +410,9 @@
297 410 'layout_style' => 'timeline',
298 411 'show_sidebar' => 'yes',
299 412 'og_image' => '',
300 413 'links' => [],
414 + 'topic_required' => 'no',
301 415 'hide_members_count' => 'no', // yes / no
302 416 'members_page_status' => 'members_only', // members_only, everybody, logged_in, admin_only
303 417 ];
304 418 }
@@ -310,13 +424,19 @@
310 424
311 425 public function getPublicPermissions()
312 426 {
313 427 if ($this->privacy == 'public') {
428 +
429 + $hasDocuments = defined('FLUENT_COMMUNITY_PRO') && Arr::get($this->settings, 'document_library') == 'yes';
430 + $hasMediaGallery = defined('FLUENT_COMMUNITY_PRO') && Arr::get($this->settings, 'media_gallery') == 'yes';
431 +
314 432 return [
315 - 'can_view_info' => true,
316 - 'can_view_posts' => true,
317 - 'can_view_members' => $this->canViewMembers(null),
318 - 'can_create_post' => false
433 + 'can_view_info' => true,
434 + 'can_view_posts' => true,
435 + 'can_view_members' => $this->canViewMembers(null),
436 + 'can_create_post' => false,
437 + 'can_view_documents' => $hasDocuments && Arr::get($this->settings, 'document_access') == 'everybody',
438 + 'can_view_media' => $hasMediaGallery && Arr::get($this->settings, 'media_access') == 'everybody',
319 439 ];
320 440 }
321 441
322 442 return [
@@ -349,9 +469,10 @@
349 469 return true;
350 470 }
351 471
352 472 if ($viewStatus === 'members_only') {
353 - return Helper::isUserInSpace($user->ID, $this->id);
473 + $membership = $this->getMembership($user->ID);
474 + return $membership && isset($membership->pivot) && $membership->pivot->status === 'active';
354 475 }
355 476
356 477 return $this->isAdmin($user->ID, true);
357 478 }
@@ -362,9 +483,10 @@
362 483
363 484 $hasPermission = $permissions[$permission] ?? false;
364 485
365 486 if (!$hasPermission && $exception) {
366 - throw new \Exception('Sorry you do not have permission ' . esc_html($permission), 403);
487 + /* translators: %s is the permission name */
488 + throw new \Exception(esc_html(sprintf(__('Sorry you do not have permission %s', 'fluent-community'), $permission)), 403);
367 489 }
368 490
369 491 return $hasPermission;
370 492 }
@@ -373,19 +495,30 @@
373 495 {
374 496 return $this->updateCustomMeta('lockscreen_settings', $settingFields);
375 497 }
376 498
499 + /**
500 + * Sidebar links, emails, invitations, canonical URLs and API payloads all resolve
501 + * through here, so the filter moves every one of them together.
502 + *
503 + * @return string
504 + */
377 505 public function getPermalink()
378 506 {
507 + $permalink = Helper::baseUrl('/');
508 +
379 509 if ($this->type == 'community') {
380 - return Helper::baseUrl('space/' . $this->slug . '/home');
510 + $permalink = Helper::baseUrl('space/' . $this->slug . '/home');
511 + } elseif ($this->type == 'course') {
512 + $permalink = Helper::baseUrl('course/' . $this->slug . '/lessons');
513 + } elseif ($this->type == 'sidebar_link') {
514 + $customLink = Arr::get($this->settings, 'permalink');
515 + if ($customLink) {
516 + $permalink = $customLink;
517 + }
381 518 }
382 519
383 - if ($this->type == 'course') {
384 - return Helper::baseUrl('course/' . $this->slug . '/lessons');
385 - }
386 -
387 - return Helper::baseUrl('/');
520 + return apply_filters('fluent_community/space_permalink', $permalink, $this);
388 521 }
389 522
390 523 public function getLockscreen()
391 524 {
@@ -391,8 +524,13 @@
391 524 {
392 525 return LockscreenService::getLockscreenSettings($this);
393 526 }
394 527
528 + public function hasPaywallIntegration()
529 + {
530 + return !empty(Arr::get($this->settings, 'cart_product_ids', []));
531 + }
532 +
395 533 public function getCustomMeta($key, $default = null)
396 534 {
397 535 return Helper::getSpaceMeta($this->id, $key, $default);
398 536 }
@@ -408,16 +546,16 @@
408 546 return '';
409 547 }
410 548
411 549 if ($this->logo) {
412 - return '<img alt="" src="' . $this->logo . '"/>';
550 + return '<img alt="" src="' . esc_url($this->logo) . '"/>';
413 551 }
414 552
415 - if ($imoji = Arr::get($this->settings, 'emoji')) {
553 + if ($imoji = Arr::get($this->settings, 'emoji')) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
416 554 return '<span class="fcom_emoji">' . $imoji . '</span>';
417 555 }
418 556
419 - if ($svg = Arr::get($this->settings, 'shape_svg')) {
557 + if ($svg = Arr::get($this->settings, 'shape_svg')) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
420 558 return '<span class="fcom_shape"><i class="el-icon">' . $svg . '</i></span>';
421 559 }
422 560
423 561 return '';
@@ -442,10 +580,10 @@
442 580
443 581 // We have to create one
444 582 $new = Meta::create([
445 583 'object_id' => $topicId,
446 - 'meta_key' => $this->id,
447 - 'object_type' => 'term_space_relation'
584 + 'meta_key' => $this->id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
585 + 'object_type' => 'term_space_relation',
448 586 ]);
449 587
450 588 $existIds[] = $new->id;
451 589 }
@@ -460,5 +598,66 @@
460 598
461 599 return $this;
462 600 }
463 601
602 + protected static function generateNewSlug($newModel)
603 + {
604 + if ($newModel->title) {
605 + // Remove the emojis
606 + $title = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $newModel->title);
607 + // get the first 30 char from the title
608 + $title = mb_substr($title, 0, 30, 'UTF-8');
609 + } else {
610 + $title = static::$type . '-' . time();
611 + }
612 +
613 + $title = Helper::normalizeToAscii($title);
614 + $title = remove_accents($title);
615 +
616 + $title = strtolower($title);
617 +
618 + $title = trim(preg_replace('/[^a-z0-9-_]/', ' ', $title));
619 +
620 + $slugNum = self::withoutGlobalScopes()->where('type', static::$type)->count();
621 +
622 + $title = sanitize_title($title, static::$type . '-' . $slugNum);
623 +
624 + // check if the slug is already exists
625 + $slug = $title;
626 + $count = 1;
627 + while (self::withoutGlobalScopes()->where('slug', $slug)->exists()) {
628 + if ($count == 5) {
629 + $count = time();
630 + }
631 + $slug = $title . '-' . (++$slugNum);
632 + ++$count;
633 + }
634 +
635 + return $slug;
636 + }
637 +
638 + public function formatSpaceData($user)
639 + {
640 + $userId = $user ? $user->ID : null;
641 +
642 + $this->permissions = $this->getUserPermissions($user);
643 + $this->description_rendered = wpautop($this->description);
644 + $this->membership = $this->getMembership($userId);
645 + $this->topics = Utility::getTopicsBySpaceId($this->id);
646 +
647 + $this->header_links = SpaceMenuService::getMenuLinks($this, $user);
648 +
649 + if ($this->isAdmin($userId, true)) {
650 + return $this;
651 + }
652 +
653 + $this->lockscreen_config = LockscreenService::getLockscreenConfig($this, $this->membership, true);
654 +
655 + $spaceSettings = $this->settings;
656 +
657 + $spaceSettings['links'] = Helper::filterAccessibleLinks(Arr::get($spaceSettings, 'links', []), $user);
658 +
659 + $this->settings = $spaceSettings;
660 +
661 + return $this;
662 + }
464 663 }