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
fluent-community / app / Models / BaseSpace.php

BaseSpace.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.11.0, at app/Models/BaseSpace.php

664 lines 19.6 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\Models;
4
5 use FluentCommunity\App\App;
6 use FluentCommunity\App\Functions\Utility;
7 use FluentCommunity\App\Services\CustomSanitizer;
8 use FluentCommunity\App\Services\LockscreenService;
9 use FluentCommunity\App\Services\Helper;
10 use FluentCommunity\App\Services\SpaceMenuService;
11 use FluentCommunity\Framework\Support\Arr;
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 */
36 class BaseSpace extends Model
37 {
38 protected $table = 'fcom_spaces';
39
40 protected static $type = 'community';
41
42 protected $guarded = [ 'id' ];
43
44 protected $fillable = [
45 'created_by',
46 'parent_id',
47 'title',
48 'slug',
49 'description',
50 'logo',
51 'cover_photo',
52 'type',
53 'privacy',
54 'status',
55 'serial',
56 'settings',
57 ];
58
59 protected $searchable = [
60 'title',
61 'description',
62 ];
63
64 public $_preloadedMembershipUserId = null;
65 public $_preloadedMembership = null;
66
67 public static function boot()
68 {
69 parent::boot();
70
71 static::creating(function ($model) {
72 if (empty($model->created_by)) {
73 $model->created_by = get_current_user_id();
74 }
75
76 if (empty($model->slug)) {
77 $model->slug = self::generateNewSlug($model);
78 }
79
80 $model->type = static::$type;
81 });
82
83 static::addGlobalScope('type', function ($query) {
84 if (static::$type) {
85 $query->where('type', static::$type);
86 }
87
88 return $query;
89 });
90
91 static::deleting(function ($space) {
92 Media::where('sub_object_id', $space->id)
93 ->whereIn('object_source', [ 'space_logo', 'space_cover_photo' ])
94 ->update([
95 'is_active' => 0,
96 ]);
97 });
98 }
99
100 public function owner()
101 {
102 return $this->belongsTo(User::class, 'created_by', 'ID');
103 }
104
105 public function space_pivot()
106 {
107 return $this->belongsTo(SpaceUserPivot::class, 'id', 'space_id');
108 }
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
119 public function scopeSearchBy($query, $search)
120 {
121 if ($search) {
122 $fields = $this->searchable;
123 $query->where(function ($query) use ($fields, $search) {
124 $query->where(array_shift($fields), 'LIKE', "%$search%");
125 foreach ($fields as $field) {
126 $query->orWhere($field, 'LIKE', "$search%");
127 }
128 });
129 }
130
131 return $query;
132 }
133
134 public function scopeOnlyMain($query)
135 {
136 return $query->withoutGlobalScopes()->whereIn('type', [ 'community', 'course' ]);
137 }
138
139 public function scopeFilterByUserId($query, $userId)
140 {
141 if (!$userId) {
142 return $query->where('privacy', 'public');
143 }
144
145 $ids = get_user_meta($userId, '_fcom_space_ids', true);
146
147 if (!$ids) {
148 return $query->where('privacy', 'public');
149 }
150
151 return $query->whereIn('id', $ids);
152 }
153
154 public function scopeByUserAccess($query, $userId)
155 {
156 if (!$userId) {
157 return $query->where('privacy', 'public');
158 }
159
160 return $this->where(function ($query) use ($userId) {
161 return $query->where('privacy', 'public')
162 ->orWhereHas('members', function ($query) use ($userId) {
163 return $query->where('user_id', $userId);
164 });
165 });
166 }
167
168 public function posts()
169 {
170 return $this->hasMany(Feed::class, 'space_id', 'id');
171 }
172
173 public function comments()
174 {
175 return $this->hasManyThrough(Comment::class, Feed::class, 'space_id', 'post_id');
176 }
177
178 public function members()
179 {
180 return $this->belongsToMany(User::class, 'fcom_space_user', 'space_id', 'user_id')
181 ->withPivot([ 'role', 'created_at', 'status' ]);
182 }
183
184 public function x_members()
185 {
186 return $this->belongsToMany(XProfile::class, 'fcom_space_user', 'space_id', 'user_id', 'id', 'user_id')
187 ->withPivot([ 'role', 'created_at', 'status' ]);
188 }
189
190 public function group()
191 {
192 return $this->belongsTo(SpaceGroup::class, 'parent_id', 'id');
193 }
194
195 public function getMembership($userId)
196 {
197 if (!$userId) {
198 return null;
199 }
200
201 if ($this->_preloadedMembershipUserId == $userId) {
202 return $this->_preloadedMembership;
203 }
204
205 return $this->members()->where('user_id', $userId)->first();
206 }
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
236 public function isCourseSpace()
237 {
238 return $this->type == 'course';
239 }
240
241 public function isAdmin($userId, $checkModerator = false)
242 {
243 if (Helper::isSiteAdmin($userId)) {
244 return true;
245 }
246
247 $roles = [ 'admin' ];
248
249 if ($checkModerator) {
250 if (Helper::isModerator()) {
251 return true;
252 }
253 $roles[] = 'moderator';
254 }
255
256 $membership = $this->getMembership($userId);
257 if (!$membership) {
258 return false;
259 }
260
261 return in_array($membership->pivot->role, $roles);
262 }
263
264 public function updateCustomData($data, $removeSrc = false)
265 {
266 $deletePhotos = [];
267 if (isset($data['logo'])) {
268 $deletePhotos[] = $this->logo;
269 $this->logo = sanitize_url($data['logo']);
270 }
271
272 if (isset($data['cover_photo'])) {
273 $deletePhotos[] = $this->cover_photo;
274 $this->cover_photo = sanitize_url($data['cover_photo']);
275 }
276
277 if (isset($data['description'])) {
278 $this->description = wp_kses_post($data['description']);
279 }
280
281 if (isset($data['title'])) {
282 $this->title = sanitize_text_field($data['title']);
283 }
284
285 if (isset($data['privacy'])) {
286 $this->privacy = sanitize_text_field($data['privacy']);
287 }
288
289 if (isset($data['parent_id'])) {
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;
298 }
299 }
300
301 if (isset($data['settings'])) {
302
303 $settings = CustomSanitizer::santizeSpaceSettings($data['settings'], $this->privacy);
304
305 if (is_wp_error($settings)) {
306 return $settings;
307 }
308
309 $exisitingSetting = $this->settings;
310 $settings['links'] = Arr::get($exisitingSetting, 'links', []);
311 $settings[SpaceMenuService::SETTINGS_KEY] = Arr::get($exisitingSetting, SpaceMenuService::SETTINGS_KEY, []);
312
313 if (isset($settings['og_image'])) {
314 $ogImageUrl = Arr::get($settings, 'og_image');
315 if ($ogImageUrl) {
316 $ogImageMedia = Helper::getMediaFromUrl($ogImageUrl);
317 if ($ogImageMedia && $ogImageMedia->is_active) {
318 unset($settings['og_image']);
319 } elseif ($ogImageMedia) {
320 $ogImageMedia->update([
321 'is_active' => true,
322 'user_id' => get_current_user_id(),
323 'sub_object_id' => $this->id,
324 'object_source' => 'space_og_media',
325 ]);
326 $settings['og_image'] = $ogImageMedia->public_url;
327 } else {
328 $settings['og_image'] = sanitize_url($ogImageUrl);
329 }
330 }
331 } else {
332 $deletePhotos[] = Arr::get($exisitingSetting, 'og_image');
333 }
334
335 $settings = wp_parse_args($settings, $exisitingSetting);
336 $this->settings = $settings;
337 }
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
359 $deletePhotos = array_filter($deletePhotos);
360 if ($removeSrc && $deletePhotos) {
361 $deletePhotos = array_filter($deletePhotos);
362 do_action('fluent_community/remove_medias_by_url', $deletePhotos, [
363 'sub_object_id' => $this->id,
364 ]);
365 }
366
367 $dirty = $this->getDirty();
368
369 if ($dirty) {
370 $this->save();
371 if ($this->type == 'community') {
372 do_action('fluent_community/space/updated', $this, $dirty);
373 }
374 }
375
376 return $this;
377 }
378
379 public function isContentSpace()
380 {
381 return in_array($this->type, [ 'community', 'course' ]);
382 }
383
384 public function getSettingsAttribute($value)
385 {
386 $settings = Utility::safeUnserialize($value);
387
388 if (!$settings) {
389 $settings = [];
390 }
391
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;
400 }
401
402 public function defaultSettings()
403 {
404 return [
405 'restricted_post_only' => 'no',
406 'emoji' => '',
407 'shape_svg' => '',
408 'custom_lock_screen' => 'no',
409 'can_request_join' => 'no',
410 'layout_style' => 'timeline',
411 'show_sidebar' => 'yes',
412 'og_image' => '',
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
417 ];
418 }
419
420 public function setSettingsAttribute($value)
421 {
422 $this->attributes['settings'] = maybe_serialize($value);
423 }
424
425 public function getPublicPermissions()
426 {
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
432 return [
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',
439 ];
440 }
441
442 return [
443 'can_view_info' => false,
444 'can_view_posts' => false,
445 ];
446 }
447
448 public function getUserPermissions($user = null)
449 {
450 if (!$user) {
451 return $this->getPublicPermissions();
452 }
453
454 return $user->getSpacePermissions($this);
455 }
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
480 public function verifyUserPermisson($user, $permission, $exception = true)
481 {
482 $permissions = $this->getUserPermissions($user);
483
484 $hasPermission = $permissions[$permission] ?? false;
485
486 if (!$hasPermission && $exception) {
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);
489 }
490
491 return $hasPermission;
492 }
493
494 public function setLockscreen($settingFields)
495 {
496 return $this->updateCustomMeta('lockscreen_settings', $settingFields);
497 }
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 */
505 public function getPermalink()
506 {
507 $permalink = Helper::baseUrl('/');
508
509 if ($this->type == 'community') {
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 }
518 }
519
520 return apply_filters('fluent_community/space_permalink', $permalink, $this);
521 }
522
523 public function getLockscreen()
524 {
525 return LockscreenService::getLockscreenSettings($this);
526 }
527
528 public function hasPaywallIntegration()
529 {
530 return !empty(Arr::get($this->settings, 'cart_product_ids', []));
531 }
532
533 public function getCustomMeta($key, $default = null)
534 {
535 return Helper::getSpaceMeta($this->id, $key, $default);
536 }
537
538 public function updateCustomMeta($key, $value)
539 {
540 return Helper::updateSpaceMeta($this->id, $key, $value);
541 }
542
543 public function getIconMark($isHtml = true)
544 {
545 if (!$isHtml) {
546 return '';
547 }
548
549 if ($this->logo) {
550 return '<img alt="" src="' . esc_url($this->logo) . '"/>';
551 }
552
553 if ($imoji = Arr::get($this->settings, 'emoji')) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
554 return '<span class="fcom_emoji">' . $imoji . '</span>';
555 }
556
557 if ($svg = Arr::get($this->settings, 'shape_svg')) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
558 return '<span class="fcom_shape"><i class="el-icon">' . $svg . '</i></span>';
559 }
560
561 return '';
562 }
563
564 public function syncTopics($topicIds)
565 {
566 $topics = Term::where('taxonomy_name', 'post_topic')->whereIn('id', $topicIds)->get();
567 $topicIds = $topics->pluck('id')->toArray();
568 $existIds = [];
569
570 foreach ($topicIds as $topicId) {
571 $relation = Meta::where('object_id', $topicId)
572 ->where('object_type', 'term_space_relation')
573 ->where('meta_key', $this->id)
574 ->first();
575
576 if ($relation) {
577 $existIds[] = $relation->id;
578 continue;
579 }
580
581 // We have to create one
582 $new = Meta::create([
583 'object_id' => $topicId,
584 'meta_key' => $this->id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
585 'object_type' => 'term_space_relation',
586 ]);
587
588 $existIds[] = $new->id;
589 }
590
591 // Remove other relations
592 Meta::whereNotIn('id', $existIds)
593 ->where('object_type', 'term_space_relation')
594 ->where('meta_key', $this->id)
595 ->delete();
596
597 Utility::forgetCache('fluent_community_post_topics');
598
599 return $this;
600 }
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 }
663 }
664