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