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 +251 -51 1.0.952.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,10 @@
297 410 'layout_style' => 'timeline',
298 411 'show_sidebar' => 'yes',
299 412 'og_image' => '',
300 413 'links' => [],
414 + 'topic_required' => 'no',
415 + 'hide_members_count' => 'no', // yes / no
301 416 'members_page_status' => 'members_only', // members_only, everybody, logged_in, admin_only
302 417 ];
303 418 }
304 419
@@ -309,13 +424,19 @@
309 424
310 425 public function getPublicPermissions()
311 426 {
312 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 +
313 432 return [
314 - 'can_view_info' => true,
315 - 'can_view_posts' => true,
316 - 'can_view_members' => $this->canViewMembers(null),
317 - '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',
318 439 ];
319 440 }
320 441
321 442 return [
@@ -348,9 +469,10 @@
348 469 return true;
349 470 }
350 471
351 472 if ($viewStatus === 'members_only') {
352 - return Helper::isUserInSpace($user->ID, $this->id);
473 + $membership = $this->getMembership($user->ID);
474 + return $membership && isset($membership->pivot) && $membership->pivot->status === 'active';
353 475 }
354 476
355 477 return $this->isAdmin($user->ID, true);
356 478 }
@@ -361,9 +483,10 @@
361 483
362 484 $hasPermission = $permissions[$permission] ?? false;
363 485
364 486 if (!$hasPermission && $exception) {
365 - 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);
366 489 }
367 490
368 491 return $hasPermission;
369 492 }
@@ -372,19 +495,30 @@
372 495 {
373 496 return $this->updateCustomMeta('lockscreen_settings', $settingFields);
374 497 }
375 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 + */
376 505 public function getPermalink()
377 506 {
507 + $permalink = Helper::baseUrl('/');
508 +
378 509 if ($this->type == 'community') {
379 - 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 + }
380 518 }
381 519
382 - if ($this->type == 'course') {
383 - return Helper::baseUrl('course/' . $this->slug . '/lessons');
384 - }
385 -
386 - return Helper::baseUrl('/');
520 + return apply_filters('fluent_community/space_permalink', $permalink, $this);
387 521 }
388 522
389 523 public function getLockscreen()
390 524 {
@@ -390,8 +524,13 @@
390 524 {
391 525 return LockscreenService::getLockscreenSettings($this);
392 526 }
393 527
528 + public function hasPaywallIntegration()
529 + {
530 + return !empty(Arr::get($this->settings, 'cart_product_ids', []));
531 + }
532 +
394 533 public function getCustomMeta($key, $default = null)
395 534 {
396 535 return Helper::getSpaceMeta($this->id, $key, $default);
397 536 }
@@ -407,16 +546,16 @@
407 546 return '';
408 547 }
409 548
410 549 if ($this->logo) {
411 - return '<img alt="" src="' . $this->logo . '"/>';
550 + return '<img alt="" src="' . esc_url($this->logo) . '"/>';
412 551 }
413 552
414 - if ($imoji = Arr::get($this->settings, 'emoji')) {
553 + if ($imoji = Arr::get($this->settings, 'emoji')) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
415 554 return '<span class="fcom_emoji">' . $imoji . '</span>';
416 555 }
417 556
418 - if ($svg = Arr::get($this->settings, 'shape_svg')) {
557 + if ($svg = Arr::get($this->settings, 'shape_svg')) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
419 558 return '<span class="fcom_shape"><i class="el-icon">' . $svg . '</i></span>';
420 559 }
421 560
422 561 return '';
@@ -441,10 +580,10 @@
441 580
442 581 // We have to create one
443 582 $new = Meta::create([
444 583 'object_id' => $topicId,
445 - 'meta_key' => $this->id,
446 - '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',
447 586 ]);
448 587
449 588 $existIds[] = $new->id;
450 589 }
@@ -459,5 +598,66 @@
459 598
460 599 return $this;
461 600 }
462 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 + }
463 663 }