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 +284 -48 1.0.922.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,28 +197,69 @@
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 }
160 240
161 - public function isAdmin($userId)
241 + public function isAdmin($userId, $checkModerator = false)
162 242 {
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 - return $membership->pivot->role == 'admin';
261 + return in_array($membership->pivot->role, $roles);
173 262 }
174 263
175 264 public function updateCustomData($data, $removeSrc = false)
176 265 {
@@ -197,32 +286,30 @@
197 286 $this->privacy = sanitize_text_field($data['privacy']);
198 287 }
199 288
200 289 if (isset($data['parent_id'])) {
201 - $group = SpaceGroup::find($data['parent_id']);
202 - if (!$group) {
203 - 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;
204 298 }
205 -
206 - $this->parent_id = $group->id;
207 299 }
208 300
209 301 if (isset($data['settings'])) {
210 - $shapSvg = '';
211 - if (!empty($data['settings']['shape_svg'])) {
212 - $shapSvg = CustomSanitizer::sanitizeSvg($data['settings']['shape_svg']);
213 - }
214 302
215 - $exisitingSetting = $this->settings;
216 - $settings = Arr::only(fluentCommunitySanitizeArray($data['settings']), array_keys($this->defaultSettings()));
303 + $settings = CustomSanitizer::santizeSpaceSettings($data['settings'], $this->privacy);
217 304
218 - if ($shapSvg) {
219 - $settings['shape_svg'] = $shapSvg;
220 - } else if (!empty($settings['emoji'])) {
221 - $settings['emoji'] = CustomSanitizer::sanitizeEmoji($settings['emoji']);
305 + if (is_wp_error($settings)) {
306 + return $settings;
222 307 }
223 308
309 + $exisitingSetting = $this->settings;
224 310 $settings['links'] = Arr::get($exisitingSetting, 'links', []);
311 + $settings[SpaceMenuService::SETTINGS_KEY] = Arr::get($exisitingSetting, SpaceMenuService::SETTINGS_KEY, []);
225 312
226 313 if (isset($settings['og_image'])) {
227 314 $ogImageUrl = Arr::get($settings, 'og_image');
228 315 if ($ogImageUrl) {
@@ -228,14 +315,14 @@
228 315 if ($ogImageUrl) {
229 316 $ogImageMedia = Helper::getMediaFromUrl($ogImageUrl);
230 317 if ($ogImageMedia && $ogImageMedia->is_active) {
231 318 unset($settings['og_image']);
232 - } else if ($ogImageMedia) {
319 + } elseif ($ogImageMedia) {
233 320 $ogImageMedia->update([
234 321 'is_active' => true,
235 322 'user_id' => get_current_user_id(),
236 323 'sub_object_id' => $this->id,
237 - 'object_source' => 'space_og_media'
324 + 'object_source' => 'space_og_media',
238 325 ]);
239 326 $settings['og_image'] = $ogImageMedia->public_url;
240 327 } else {
241 328 $settings['og_image'] = sanitize_url($ogImageUrl);
@@ -248,8 +335,28 @@
248 335 $settings = wp_parse_args($settings, $exisitingSetting);
249 336 $this->settings = $settings;
250 337 }
251 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 +
252 359 $deletePhotos = array_filter($deletePhotos);
253 360 if ($removeSrc && $deletePhotos) {
254 361 $deletePhotos = array_filter($deletePhotos);
255 362 do_action('fluent_community/remove_medias_by_url', $deletePhotos, [
@@ -256,22 +363,41 @@
256 363 'sub_object_id' => $this->id,
257 364 ]);
258 365 }
259 366
260 - $this->save();
367 + $dirty = $this->getDirty();
261 368
369 + if ($dirty) {
370 + $this->save();
371 + if ($this->type == 'community') {
372 + do_action('fluent_community/space/updated', $this, $dirty);
373 + }
374 + }
375 +
262 376 return $this;
263 377 }
264 378
379 + public function isContentSpace()
380 + {
381 + return in_array($this->type, [ 'community', 'course' ]);
382 + }
383 +
265 384 public function getSettingsAttribute($value)
266 385 {
267 - $settings = maybe_unserialize($value);
386 + $settings = Utility::safeUnserialize($value);
268 387
269 388 if (!$settings) {
270 389 $settings = [];
271 390 }
272 391
273 - 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;
274 400 }
275 401
276 402 public function defaultSettings()
277 403 {
@@ -283,9 +409,12 @@
283 409 'can_request_join' => 'no',
284 410 'layout_style' => 'timeline',
285 411 'show_sidebar' => 'yes',
286 412 'og_image' => '',
287 - 'links' => []
413 + 'links' => [],
414 + 'topic_required' => 'no',
415 + 'hide_members_count' => 'no', // yes / no
416 + 'members_page_status' => 'members_only', // members_only, everybody, logged_in, admin_only
288 417 ];
289 418 }
290 419
291 420 public function setSettingsAttribute($value)
@@ -295,13 +424,19 @@
295 424
296 425 public function getPublicPermissions()
297 426 {
298 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 +
299 432 return [
300 - 'can_view_info' => true,
301 - 'can_view_posts' => true,
302 - 'can_view_members' => true,
303 - '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',
304 439 ];
305 440 }
306 441
307 442 return [
@@ -318,8 +453,31 @@
318 453
319 454 return $user->getSpacePermissions($this);
320 455 }
321 456
457 + public function canViewMembers($user)
458 + {
459 + $viewStatus = Arr::get($this->settings, 'members_page_status');
460 + if ($viewStatus === 'everybody') {
461 + return true;
462 + }
463 +
464 + if (!$user) {
465 + return false;
466 + }
467 +
468 + if ($viewStatus === 'logged_in') {
469 + return true;
470 + }
471 +
472 + if ($viewStatus === 'members_only') {
473 + $membership = $this->getMembership($user->ID);
474 + return $membership && isset($membership->pivot) && $membership->pivot->status === 'active';
475 + }
476 +
477 + return $this->isAdmin($user->ID, true);
478 + }
479 +
322 480 public function verifyUserPermisson($user, $permission, $exception = true)
323 481 {
324 482 $permissions = $this->getUserPermissions($user);
325 483
@@ -325,9 +483,10 @@
325 483
326 484 $hasPermission = $permissions[$permission] ?? false;
327 485
328 486 if (!$hasPermission && $exception) {
329 - 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);
330 489 }
331 490
332 491 return $hasPermission;
333 492 }
@@ -336,19 +495,30 @@
336 495 {
337 496 return $this->updateCustomMeta('lockscreen_settings', $settingFields);
338 497 }
339 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 + */
340 505 public function getPermalink()
341 506 {
507 + $permalink = Helper::baseUrl('/');
508 +
342 509 if ($this->type == 'community') {
343 - 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 + }
344 518 }
345 519
346 - if ($this->type == 'course') {
347 - return Helper::baseUrl('course/' . $this->slug . '/lessons');
348 - }
349 -
350 - return Helper::baseUrl('/');
520 + return apply_filters('fluent_community/space_permalink', $permalink, $this);
351 521 }
352 522
353 523 public function getLockscreen()
354 524 {
@@ -354,8 +524,13 @@
354 524 {
355 525 return LockscreenService::getLockscreenSettings($this);
356 526 }
357 527
528 + public function hasPaywallIntegration()
529 + {
530 + return !empty(Arr::get($this->settings, 'cart_product_ids', []));
531 + }
532 +
358 533 public function getCustomMeta($key, $default = null)
359 534 {
360 535 return Helper::getSpaceMeta($this->id, $key, $default);
361 536 }
@@ -371,16 +546,16 @@
371 546 return '';
372 547 }
373 548
374 549 if ($this->logo) {
375 - return '<img alt="" src="' . $this->logo . '"/>';
550 + return '<img alt="" src="' . esc_url($this->logo) . '"/>';
376 551 }
377 552
378 - if ($imoji = Arr::get($this->settings, 'emoji')) {
553 + if ($imoji = Arr::get($this->settings, 'emoji')) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
379 554 return '<span class="fcom_emoji">' . $imoji . '</span>';
380 555 }
381 556
382 - if ($svg = Arr::get($this->settings, 'shape_svg')) {
557 + if ($svg = Arr::get($this->settings, 'shape_svg')) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
383 558 return '<span class="fcom_shape"><i class="el-icon">' . $svg . '</i></span>';
384 559 }
385 560
386 561 return '';
@@ -405,10 +580,10 @@
405 580
406 581 // We have to create one
407 582 $new = Meta::create([
408 583 'object_id' => $topicId,
409 - 'meta_key' => $this->id,
410 - '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',
411 586 ]);
412 587
413 588 $existIds[] = $new->id;
414 589 }
@@ -423,5 +598,66 @@
423 598
424 599 return $this;
425 600 }
426 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 + }
427 663 }