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 / Http / Controllers / SpaceController.php

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

1,106 lines 36.1 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\Http\Controllers;
4
5 use FluentCommunity\App\Models\Space;
6 use FluentCommunity\App\Models\Feed;
7 use FluentCommunity\App\Models\BaseSpace;
8 use FluentCommunity\App\Models\SpaceGroup;
9 use FluentCommunity\App\Models\User;
10 use FluentCommunity\App\Services\CustomSanitizer;
11 use FluentCommunity\App\Services\Helper;
12 use FluentCommunity\App\Services\SpaceMenuService;
13 use FluentCommunity\App\Functions\Utility;
14 use FluentCommunity\App\Services\ProfileHelper;
15 use FluentCommunity\Framework\Http\Request\Request;
16 use FluentCommunity\App\Models\Comment;
17 use FluentCommunity\App\Models\Reaction;
18 use FluentCommunity\App\Models\SpaceUserPivot;
19 use FluentCommunity\Framework\Support\Arr;
20
21 class SpaceController extends Controller
22 {
23 public function get()
24 {
25 $spaces = Space::orderBy('title', 'ASC')
26 ->whereHas('space_pivot', function ($q) {
27 $q->where('user_id', get_current_user_id());
28 })
29 ->get();
30
31 $data = [
32 'spaces' => $spaces
33 ];
34
35 return apply_filters('fluent_community/spaces_api_response', $data, $this->request->all());
36 }
37
38 public function create(Request $request)
39 {
40 $currentUser = $this->getUser(true);
41
42 $data = $request->get('space', []);
43
44 $slug = Arr::get($data, 'slug') ?: Arr::get($data, 'title');
45
46 $slug = preg_replace('/[^a-zA-Z0-9-_]/', '', $slug);
47
48 $data['slug'] = sanitize_title($slug, '');
49 $data['title'] = sanitize_text_field(Arr::get($data, 'title'));
50 $data['privacy'] = sanitize_text_field(Arr::get($data, 'privacy'));
51
52 $this->validate($data, [
53 'title' => 'required',
54 'slug' => 'unique:fcom_spaces,slug',
55 'privacy' => 'required|in:public,private,secret'
56 ]);
57
58 if (Arr::get($data, 'settings.topic_required') === 'yes' && !array_filter((array)Arr::get($data, 'topic_ids', []))) {
59 return $this->sendError([
60 'message' => __('Please select at least one topic when members are required to select a topic.', 'fluent-community')
61 ]);
62 }
63
64 $spaceGroup = null;
65 if (!empty($data['parent_id'])) {
66 $spaceGroup = SpaceGroup::findOrFail($data['parent_id']);
67 $serial = BaseSpace::query()->withoutGlobalScopes()->where('parent_id', $spaceGroup->id)->max('serial') + 1;
68 } else {
69 $serial = BaseSpace::query()->withoutGlobalScopes()->max('serial') + 1;
70 }
71
72 $settings = CustomSanitizer::santizeSpaceSettings(Arr::get($data, 'settings', []), $data['privacy']);
73
74 if (is_wp_error($settings)) {
75 return $this->sendError([
76 'message' => $settings->get_error_message()
77 ]);
78 }
79
80 $spaceData = apply_filters('fluent_community/space/create_data', [
81 'title' => sanitize_text_field($data['title']),
82 'slug' => $data['slug'],
83 'privacy' => $data['privacy'],
84 'description' => sanitize_textarea_field(Arr::get($data, 'description', '')),
85 'settings' => $settings,
86 'parent_id' => $spaceGroup ? $spaceGroup->id : null,
87 'serial' => $serial ?: 1
88 ]);
89
90 $ogImage = Arr::get($data, 'settings.og_image', '');
91 $ogMedia = null;
92 if ($ogImage) {
93 $ogMedia = Helper::getMediaFromUrl($ogImage);
94 if ($ogMedia && !$ogMedia->is_active) {
95 $spaceData['settings']['og_image'] = $ogMedia->public_url;
96 }
97 }
98
99 $space = Space::create($spaceData);
100 if ($ogMedia && !$ogMedia->is_active) {
101 $ogMedia->update([
102 'is_active' => true,
103 'user_id' => get_current_user_id(),
104 'sub_object_id' => $space->id,
105 'object_source' => 'space_og_image'
106 ]);
107 }
108
109 $imageTypes = ['cover_photo', 'logo'];
110 $metaData = [];
111 foreach ($imageTypes as $type) {
112 if (!empty($data[$type])) {
113 $media = Helper::getMediaFromUrl($data[$type]);
114 if (!$media || $media->is_active) {
115 continue;
116 }
117 $metaData[$type] = $media->public_url;
118 $media->update([
119 'is_active' => true,
120 'user_id' => get_current_user_id(),
121 'sub_object_id' => $space->id,
122 'object_source' => 'space_' . $type
123 ]);
124 }
125 }
126
127 if ($metaData) {
128 $space->updateCustomData($metaData, false);
129 }
130
131 $space->members()->attach(get_current_user_id(), [
132 'role' => 'admin'
133 ]);
134
135 if (Arr::has($data, 'topic_ids')) {
136 $topicsConfig = Helper::getTopicsConfig();
137 $topicIds = (array)Arr::get($data, 'topic_ids', []);
138 $topicIds = array_slice($topicIds, 0, $topicsConfig['max_topics_per_space']);
139 $space->syncTopics($topicIds);
140 }
141
142 $currentUser->cacheAccessSpaces();
143 do_action('fluent_community/space/created', $space, $data);
144
145 return [
146 'message' => __('Space has been created successfully', 'fluent-community'),
147 'space' => $space
148 ];
149 }
150
151 public function discover(Request $request)
152 {
153 $start = microtime(true);
154 $currentUser = $this->getUser();
155 $type = $request->getSafe('type');
156 $search = $request->getSafe('search');
157 $sortBy = $request->getSafe('sort_by', 'sanitize_text_field', 'alphabetical');
158
159 $spaces = Space::with(['space_pivot' => function ($q) {
160 $q->where('user_id', get_current_user_id());
161 }])
162 ->where(function ($q) {
163 $q->whereHas('space_pivot', function ($q) {
164 $q->where('user_id', get_current_user_id())
165 ->where('status', 'active');
166 })
167 ->orWhereIn('privacy', ['public', 'private']);
168 })
169 ->when($type == 'joined', function ($q) {
170 $q->whereHas('space_pivot', function ($q) {
171 $q->where('user_id', get_current_user_id());
172 });
173 })
174 ->when($search, function ($q) use ($search) {
175 $q->where('title', 'like', '%' . $search . '%');
176 })
177 ->when($sortBy == 'alphabetical', function ($q) {
178 $q->orderBy('title', 'ASC');
179 })
180 ->when($sortBy == 'oldest', function ($q) {
181 $q->orderBy('created_at', 'ASC');
182 })
183 ->when($sortBy == 'latest', function ($q) {
184 $q->orderBy('created_at', 'DESC');
185 })
186 ->paginate();
187
188 $memberCounts = $this->getActiveMemberCounts($spaces->pluck('id')->toArray());
189
190 foreach ($spaces as $space) {
191 $space->description_rendered = wpautop($space->description);
192
193 $shouldHideMembersCount = Arr::get($space->settings, 'hide_members_count') == 'yes';
194 $canViewMembers = $currentUser && $space->verifyUserPermisson($currentUser, 'can_view_members', false);
195
196 if ($shouldHideMembersCount && !$canViewMembers) {
197 $space->members_count = 0;
198 continue;
199 }
200
201 $space->members_count = (int)Arr::get($memberCounts, $space->id, 0);
202 }
203
204 $data = [
205 'spaces' => $spaces,
206 'execution_time' => microtime(true) - $start
207 ];
208
209 return apply_filters('fluent_community/spaces_api_response', $data, $request->all());
210 }
211
212 public function getAllSpaces(Request $request)
213 {
214 $currentUser = $this->getUser();
215
216 $spacesQuery = Space::query();
217
218 if (!($currentUser && $currentUser->isCommunityModerator())) {
219 $spacesQuery->where(function ($q) {
220 $q->whereHas('space_pivot', function ($q) {
221 $q->where('user_id', get_current_user_id())
222 ->where('status', 'active');
223 })
224 ->orWhereIn('privacy', ['public', 'private']);
225 });
226 }
227 $spaces = $spacesQuery->paginate();
228
229 $memberCounts = $this->getActiveMemberCounts($spaces->pluck('id')->toArray());
230
231 foreach ($spaces as $space) {
232 $shouldHideMembersCount = Arr::get($space->settings, 'hide_members_count') == 'yes';
233 $canViewMembers = $currentUser && $space->verifyUserPermisson($currentUser, 'can_view_members', false);
234
235 if ($shouldHideMembersCount && !$canViewMembers) {
236 $space->members_count = 0;
237 continue;
238 }
239
240 $space->members_count = (int)Arr::get($memberCounts, $space->id, 0);
241
242 $space->formatSpaceData($currentUser);
243 do_action_ref_array('fluent_community/space', [&$space]);
244 }
245
246 return apply_filters('fluent_community/all_spaces_api_response', [
247 'spaces' => $spaces
248 ], $request->all());
249 }
250
251 public function getBySlug(Request $request, $spaceSlug)
252 {
253 $user = $this->getUser();
254 $space = Space::where('slug', $spaceSlug)->first();
255
256 $userId = $user ? $user->ID : null;
257
258 // A hidden secret space must be indistinguishable from a non-existent one, so its
259 // existence cannot be enumerated by slug. Both return an identical 404.
260 if (!$space || ($space->privacy == 'secret' && !$space->getMembership($userId) && !$space->isAdmin($userId, true))) {
261 return $this->sendError([
262 'message' => __('Space not found', 'fluent-community')
263 ], 404);
264 }
265
266 $space = $space->formatSpaceData($user);
267
268 do_action_ref_array('fluent_community/space', [&$space]);
269
270 $data = [
271 'space' => $space
272 ];
273
274 return apply_filters('fluent_community/space_api_response', $data, $request->all());
275 }
276
277 public function patchBySlug(Request $request, $slug)
278 {
279 $space = Space::where('slug', $slug)
280 ->first();
281
282 if (!$space) {
283 return $this->sendError([
284 'message' => __('Space not found', 'fluent-community')
285 ]);
286 }
287
288 $data = $request->get('data', []);
289
290 if (Arr::has($data, 'title') && !trim(sanitize_text_field(Arr::get($data, 'title', '')))) {
291 return $this->sendError([
292 'message' => __('Space title is required.', 'fluent-community')
293 ]);
294 }
295
296 if (!empty($data['slug'])) {
297 $taken = Space::where('slug', $data['slug'])
298 ->where('id', '!=', $space->id)
299 ->first();
300
301 if ($taken) {
302 return $this->sendError([
303 'message' => __('Slug is already taken. Please use a different slug', 'fluent-community')
304 ]);
305 }
306 }
307
308 $topicRequired = Arr::has($data, 'settings.topic_required')
309 ? Arr::get($data, 'settings.topic_required')
310 : Arr::get($space->settings, 'topic_required');
311
312 if ($topicRequired === 'yes') {
313 $topicIds = Arr::has($data, 'topic_ids')
314 ? (array)Arr::get($data, 'topic_ids', [])
315 : array_column(Utility::getTopicsBySpaceId($space->id), 'id');
316
317 if (!array_filter($topicIds)) {
318 return $this->sendError([
319 'message' => __('Please select at least one topic when members are required to select a topic.', 'fluent-community')
320 ]);
321 }
322 }
323
324 $mediaTypes = ['cover_photo', 'logo'];
325 foreach ($mediaTypes as $type) {
326 if (!empty($data[$type])) {
327 $media = Helper::getMediaFromUrl($data[$type]);
328 if (!$media || $media->is_active) {
329 unset($data[$type]);
330 continue;
331 }
332
333 $data[$type] = $media->public_url;
334
335 $media->update([
336 'is_active' => true,
337 'user_id' => get_current_user_id(),
338 'sub_object_id' => $space->id,
339 'object_source' => 'space_' . $type
340 ]);
341 } else if (isset($data[$type])) {
342 $data[$type] = '';
343 }
344 }
345
346 $data = apply_filters('fluent_community/space/update_data', $data, $space);
347
348 if (isset($data['parent_id']) && !$data['parent_id']) {
349 $data['parent_id'] = '';
350 }
351
352 $space = $space->updateCustomData($data, true);
353
354 if (is_wp_error($space)) {
355 return $this->sendError([
356 'message' => $space->get_error_message()
357 ]);
358 }
359
360 if (Arr::has($data, 'topic_ids')) {
361 $topicsConfig = Helper::getTopicsConfig();
362 $topicIds = (array)Arr::get($data, 'topic_ids', []);
363 $topicIds = array_slice($topicIds, 0, $topicsConfig['max_topics_per_space']);
364 $space->syncTopics($topicIds);
365 }
366
367 do_action('fluent_community/space/updated', $space, $data);
368 $slugUpdated = $slug != $space->slug;
369
370 $metaSettings = $request->get('meta_settings', []);
371 if($metaSettings) {
372 foreach ($metaSettings as $metaProvider => $metaData) {
373 do_action('fluent_community/space/update_meta_settings_'.$metaProvider, $metaData, $space);
374 }
375 }
376
377 return [
378 'message' => __('Settings have been updated', 'fluent-community'),
379 'redirect_url' => $slugUpdated ? $space->getPermalink() : ''
380 ];
381 }
382
383 public function patchById(Request $request, $id)
384 {
385 $space = Space::findOrFail($id);
386
387 return $this->patchBySlug($request, $space->slug);
388 }
389
390 public function getMembers(Request $request, $slug)
391 {
392 /** @var Space $space */
393 $space = Space::where('slug', $slug)
394 ->firstOrFail();
395
396 $user = $this->getUser();
397
398 if (!$space->verifyUserPermisson($user, 'can_view_members', false)) {
399 return $this->sendError([
400 'message' => __('You are not allowed to view members of this space', 'fluent-community'),
401 'permission_failed' => true
402 ]);
403 }
404
405 $search = $request->getSafe('search', 'sanitize_text_field');
406
407 $pendingCount = 0;
408 if ($user && $user->can('can_add_member', $space)) {
409 $pendingCount = SpaceUserPivot::bySpace($space->id)
410 ->where('status', 'pending')
411 ->count();
412
413 if ($request->get('status') == 'pending') {
414 $pendingRequests = SpaceUserPivot::bySpace($space->id)
415 ->whereHas('xprofile', function ($q) use ($search) {
416 return $q->searchBy($search)
417 ->where('status', 'active');
418 })
419 ->with(['xprofile' => function ($q) {
420 $q->select(ProfileHelper::getXProfilePublicFields());
421 }])
422 ->where('status', 'pending')
423 ->paginate();
424
425 return apply_filters('fluent_community/space_members_api_response', [
426 'members' => $pendingRequests,
427 'pending_count' => $pendingCount
428 ], $pendingRequests, $request->all());
429 }
430 }
431
432 $defaultDirections = [
433 'last_activity' => 'DESC',
434 'display_name' => 'ASC',
435 'created_at' => 'DESC',
436 ];
437
438 $sortBy = $request->getSafe('sort_by', 'sanitize_text_field', 'created_at');
439 $sortColumn = in_array($sortBy, array_keys($defaultDirections), true) ? $sortBy : 'created_at';
440 $sortDir = strtoupper($request->getSafe('sort_dir', 'sanitize_text_field', ''));
441 $sortDirection = in_array($sortDir, ['ASC', 'DESC'], true) ? $sortDir : ($sortColumn === 'created_at' ? 'ASC' : $defaultDirections[$sortColumn]);
442
443 $profileSort = $sortColumn !== 'created_at';
444 $orderColumn = $profileSort ? 'fcom_xprofile.' . $sortColumn : 'fcom_space_user.created_at';
445
446 $spaceMembers = SpaceUserPivot::bySpace($space->id)
447 ->whereHas('xprofile', function ($q) use ($search) {
448 $q->searchBy($search)->where('status', 'active');
449 })
450 ->where('fcom_space_user.status', 'active')
451 ->with(['xprofile' => function ($q) {
452 $q->select(ProfileHelper::getXProfilePublicFields());
453 }])
454 ->when($profileSort, function ($q) {
455 $q->join('fcom_xprofile', 'fcom_xprofile.user_id', '=', 'fcom_space_user.user_id')
456 ->select('fcom_space_user.*');
457 })
458 ->orderBy($orderColumn, $sortDirection)
459 ->paginate();
460
461 return apply_filters('fluent_community/space_members_api_response', [
462 'members' => $spaceMembers,
463 'pending_count' => $pendingCount
464 ], $spaceMembers, $request->all());
465 }
466
467 public function join(Request $request, $slug)
468 {
469 $space = Space::where('slug', $slug)->first();
470
471 if (!$space) {
472 return $this->sendError([
473 'message' => __('Space not found', 'fluent-community')
474 ]);
475 }
476
477 $user = $this->getUser();
478
479 $membership = $space->getMembership(get_current_user_id());
480
481 if ($membership) {
482 return $this->sendError([
483 'message' => __('You are already a member of this space. Please reload this page', 'fluent-community')
484 ]);
485 }
486
487 $roles = $user->getCommunityRoles();
488 $isPrivileged = !!array_intersect($roles, ['admin', 'moderator']);
489
490 if (!$isPrivileged && $space->privacy == 'secret') {
491 return $this->sendError([
492 'message' => __('You are not allowed to join this space', 'fluent-community')
493 ]);
494 }
495
496 $status = 'active';
497 if (!$isPrivileged) {
498 if ($space->privacy != 'public') {
499 $status = apply_filters('fluent_community/space/join_status_for_private', 'pending', $space, $user);
500
501 if (!in_array($status, ['pending', 'active'])) {
502 $status = 'pending';
503 }
504 }
505 $role = 'member';
506 } else {
507 $role = $user->isCommunityAdmin() ? 'admin' : 'moderator';
508 }
509
510
511 $space->members()->attach(get_current_user_id(), [
512 'role' => $role,
513 'status' => $status
514 ]);
515
516 $space->membership = $space->getMembership(get_current_user_id());
517
518 if ($status == 'pending') {
519 do_action('fluent_community/space/join_requested', $space, $user->ID, 'self');
520 } else {
521 do_action('fluent_community/space/joined', $space, $user->ID, 'self');
522 }
523
524 $user->cacheAccessSpaces();
525
526 return [
527 'message' => ($status == 'active') ? __('You have joined this Space', 'fluent-community') : __('Your join request has been sent to the Space admin.', 'fluent-community'),
528 'membership' => $space->membership
529 ];
530 }
531
532 public function leave(Request $request, $slug)
533 {
534 $user = $this->getUser(true);
535 $space = Space::where('slug', $slug)->first();
536
537 if (!$space) {
538 return $this->sendError([
539 'message' => __('Space not found', 'fluent-community')
540 ]);
541 }
542
543 $membership = $space->getMembership($user->ID);
544
545 if (!$membership) {
546 return $this->sendError([
547 'message' => __('You are not a member of this community', 'fluent-community')
548 ]);
549 }
550
551 Helper::removeFromSpace($space, $user->ID, 'self');
552
553 return [
554 'message' => __('You have left this space', 'fluent-community')
555 ];
556 }
557
558 public function deleteBySlug(Request $request, $slug)
559 {
560 $space = Space::where('slug', $slug)->first();
561
562 if (!$space) {
563 return $this->sendError([
564 'message' => __('Space not found', 'fluent-community')
565 ]);
566 }
567
568 do_action('fluent_community/space/before_delete', $space);
569
570 Comment::whereHas('post', function ($q) use ($space) {
571 $q->where('space_id', $space->id);
572 })->delete();
573
574 Reaction::whereHas('feed', function ($q) use ($space) {
575 $q->where('space_id', $space->id);
576 })->delete();
577
578 Feed::where('space_id', $space->id)->delete();
579
580 SpaceUserPivot::where('space_id', $space->id)->delete();
581
582 $spaceId = $space->id;
583 $space->delete();
584
585 do_action('fluent_community/space/deleted', $spaceId);
586
587 return [
588 'message' => __('Space has been deleted successfully', 'fluent-community')
589 ];
590 }
591
592 public function deleteById(Request $request, $id)
593 {
594 $space = Space::findOrFail($id);
595
596 return $this->deleteBySlug($request, $space->slug);
597 }
598
599 public function addMember(Request $request, $slug)
600 {
601 $space = Space::where('slug', $slug)->first();
602
603 if (!$space) {
604 return $this->sendError([
605 'message' => __('Space not found', 'fluent-community')
606 ]);
607 }
608
609 $this->validate($request->all(), [
610 'user_id' => 'required|exists:users,ID'
611 ]);
612
613 $userId = $request->get('user_id');
614 $targetUser = User::findOrFail($userId);
615 $targetUser->syncXProfile();
616 $xprofile = $targetUser->xprofile;
617
618 if ($xprofile && $xprofile->status != 'active') {
619 return $this->sendError([
620 'message' => __('Selected user is not active', 'fluent-community')
621 ]);
622 }
623
624 $pivot = SpaceUserPivot::bySpace($space->id)
625 ->byUser($userId)
626 ->first();
627
628 $allowedRoles = ['member', 'moderator', 'admin'];
629 $requestedRole = $request->get('role', 'member');
630 $role = in_array($requestedRole, $allowedRoles, true) ? $requestedRole : 'member';
631
632 if ($pivot) {
633 if ($pivot->status == 'active') {
634 if ($role != $pivot->role) {
635 $pivot->role = $role;
636 $pivot->save();
637
638 do_action('fluent_community/space/member/role_updated', $space, $pivot);
639
640 return [
641 'message' => __('Member role updated', 'fluent-community')
642 ];
643 }
644
645 return $this->sendError([
646 'message' => __('Selected user is already a member of this community', 'fluent-community')
647 ]);
648 }
649
650 $pivot->status = 'active';
651 $pivot->save();
652 do_action('fluent_community/space/joined', $space, $userId, 'by_admin');
653
654 if ($role != 'member') {
655 do_action('fluent_community/space/member/role_updated', $space, $pivot);
656 }
657
658 return [
659 'message' => __('Member approved', 'fluent-community')
660 ];
661 }
662
663 $space->members()->attach($userId, [
664 'role' => $role,
665 'status' => 'active'
666 ]);
667
668 $targetUser->cacheAccessSpaces();
669
670 do_action('fluent_community/space/joined', $space, $userId, 'by_admin');
671
672 return [
673 'message' => __('User has been added to this Space', 'fluent-community')
674 ];
675 }
676
677 public function removeMember(Request $request, $slug)
678 {
679 $space = Space::where('slug', $slug)->first();
680
681 if (!$space) {
682 return $this->sendError([
683 'message' => __('Space not found', 'fluent-community')
684 ]);
685 }
686
687 $userId = $request->get('user_id');
688
689 $pivot = SpaceUserPivot::bySpace($space->id)
690 ->byUser($userId)
691 ->first();
692
693 if (!$pivot) {
694 return $this->sendError([
695 'message' => __('Selected user is not a member of this community', 'fluent-community')
696 ]);
697 }
698
699 $pivot->delete();
700
701 $targetUser = User::find($userId);
702
703 if ($targetUser) {
704 $targetUser->cacheAccessSpaces();
705 }
706
707 do_action('fluent_community/space/user_left', $space, $userId, 'by_admin');
708
709 return [
710 'message' => __('User has been removed from this community', 'fluent-community')
711 ];
712 }
713
714 public function getOtherUsers(Request $request)
715 {
716 $currentUser = $this->getUser(true);
717
718 $this->validate($request->all(), [
719 'space_id' => 'required|exists:fcom_spaces,id'
720 ]);
721
722 $isMod = $currentUser->isCommunityModerator() && current_user_can('list_users');
723
724 $spaceId = $request->get('space_id');
725 $space = Space::findOrFail($spaceId);
726
727 if (!$space->verifyUserPermisson($currentUser, 'can_add_member', false)) {
728 return $this->sendError([
729 'message' => __('You are not allowed to add members to this space', 'fluent-community')
730 ]);
731 }
732
733 $selects = ['ID', 'display_name'];
734 if ($isMod) {
735 $selects[] = 'user_email';
736 }
737
738 $userQuery = User::select(['ID'])
739 ->whereDoesntHave('space_pivot', function ($q) use ($space) {
740 $q->where('space_id', $space->id);
741 })
742 ->limit(100)
743 ->searchBy($request->getSafe('search'));
744
745 if (is_multisite()) {
746 global $wpdb;
747 $blogId = get_current_blog_id();
748 $blogPrefix = $wpdb->get_blog_prefix($blogId);
749 $userQuery->whereHas('usermeta', function ($q) use ($blogPrefix) {
750 $q->where('meta_key', $blogPrefix . 'capabilities');
751 });
752 }
753
754 $userIds = $userQuery->get()
755 ->pluck('ID')
756 ->toArray();
757
758 if ($userIds) {
759 update_meta_cache('user', $userIds);
760 }
761
762 $users = User::select($selects)
763 ->whereIn('ID', $userIds)
764 ->paginate(100);
765
766 $data = [
767 'users' => $users
768 ];
769 return apply_filters('fluent_community/space_non_members_api_response', $data, $request->all());
770 }
771
772 public function updateLinks(Request $request, $slug)
773 {
774 $space = Space::where('slug', $slug)->first();
775
776 if (!$space) {
777 return $this->sendError([
778 'message' => __('Space not found', 'fluent-community')
779 ]);
780 }
781
782 $links = $request->get('links', []);
783
784 $links = array_map(function ($link) {
785 return CustomSanitizer::santizeLinkItem($link);
786 }, $links);
787
788 $settings = $space->settings;
789 $settings['links'] = $links;
790 $space->settings = $settings;
791 $space->save();
792
793 return [
794 'message' => __('Links have been updated for the space', 'fluent-community'),
795 'links' => $links
796 ];
797 }
798
799 public function getPrimaryMenu(Request $request, $slug)
800 {
801 $space = Space::where('slug', $slug)->first();
802
803 if (!$space) {
804 return $this->sendError([
805 'message' => __('Space not found', 'fluent-community'),
806 ]);
807 }
808
809 // Lighter than formatSpaceData(), but listeners like fcom-chat need membership too.
810 $user = $this->getUser();
811 $space->permissions = $space->getUserPermissions($user);
812 $space->membership = $space->getMembership($user ? $user->ID : null);
813
814 return [
815 'menu_items' => SpaceMenuService::getManagerItems($space),
816 ];
817 }
818
819 public function updatePrimaryMenu(Request $request, $slug)
820 {
821 $space = Space::where('slug', $slug)->first();
822
823 if (!$space) {
824 return $this->sendError([
825 'message' => __('Space not found', 'fluent-community'),
826 ]);
827 }
828
829 $incoming = $request->get('menu_items', []);
830
831 if (!is_array($incoming)) {
832 return $this->sendError([
833 'message' => __('Menu items must be a list.', 'fluent-community'),
834 ], 422);
835 }
836
837 $menuItems = CustomSanitizer::sanitizeSpaceMenuItems($incoming);
838
839 SpaceMenuService::storeMenu($space, $menuItems);
840
841 // No re-fetch needed — unlike formatSpaceData(), this doesn't make $space unsaveable.
842 $user = $this->getUser();
843 $space->permissions = $space->getUserPermissions($user);
844 $space->membership = $space->getMembership($user ? $user->ID : null);
845
846 return [
847 'message' => __('Menu has been updated for the space', 'fluent-community'),
848 'menu_items' => SpaceMenuService::getManagerItems($space),
849 ];
850 }
851
852 public function getSpaceGroups(Request $request)
853 {
854 if ($request->get('options_only')) {
855 $groups = SpaceGroup::orderBy('serial', 'ASC')
856 ->select(['id', 'title'])
857 ->get();
858
859 return [
860 'groups' => $groups
861 ];
862 }
863
864 $user = $this->getUser();
865
866 $groups = Helper::getAllCommunityGroups($user, false);
867
868 foreach ($groups as $group) {
869 foreach ($group->spaces as $space) {
870 $space->permalink = $space->getPermalink();
871 if ($space->type === 'community') {
872 $space = $space->formatSpaceData($user);
873 } else {
874 $space->topics = Utility::getTopicsBySpaceId($space->id);
875 }
876 }
877 }
878
879 $orphanedSpaces = BaseSpace::withoutGlobalScopes()
880 ->whereNull('parent_id')
881 ->whereIn('type', ['community', 'course'])
882 ->orderBy('title', 'ASC')
883 ->get();
884
885 foreach ($orphanedSpaces as $space) {
886 $space->permalink = $space->getPermalink();
887 if ($space->type === 'community') {
888 $space = $space->formatSpaceData($user);
889 } else {
890 $space->topics = Utility::getTopicsBySpaceId($space->id);
891 }
892 }
893
894 $data = [
895 'groups' => $groups,
896 'orphaned_spaces' => $orphanedSpaces
897 ];
898 return apply_filters('fluent_community/space_groups_api_response', $data, $request->all());
899 }
900
901 public function createSpaceGroup(Request $request)
902 {
903 $data = $request->all();
904
905 $this->validate($data, [
906 'title' => 'required|unique:fcom_spaces,title',
907 'slug' => 'required|unique:fcom_spaces,slug'
908 ]);
909
910 $title = sanitize_text_field(Arr::get($data, 'title', ''));
911 $slug = sanitize_title(Arr::get($data, 'slug', ''));
912 $desc = sanitize_textarea_field(Arr::get($data, 'description', ''));
913
914 if (!$title) {
915 return $this->sendError([
916 'message' => __('Please enter a valid group title.', 'fluent-community')
917 ]);
918 }
919
920 if (!$slug) {
921 return $this->sendError([
922 'message' => __('Please enter a valid group slug.', 'fluent-community')
923 ]);
924 }
925
926 $formattedData = [
927 'title' => $title,
928 'slug' => $slug,
929 'description' => $desc,
930 'status' => 'active',
931 'type' => 'space_group',
932 'settings' => [
933 'always_show_spaces' => Arr::get($data, 'settings.always_show_spaces', 'yes'),
934 ],
935 'serial' => SpaceGroup::max('serial') + 1
936 ];
937
938 $group = SpaceGroup::create($formattedData);
939
940 return [
941 'message' => __('Space group has been created successfully', 'fluent-community'),
942 'group' => $group
943 ];
944 }
945
946 public function updateSpaceGroup(Request $request, $groupId)
947 {
948 $group = SpaceGroup::findOrFail($groupId);
949 $data = $request->all();
950
951 $this->validate($data, [
952 'title' => 'required'
953 ]);
954
955 $taken = BaseSpace::where('title', $data['title'])
956 ->where('id', '!=', $group->id)
957 ->first();
958
959 if ($taken) {
960 return $this->sendError([
961 'message' => __('The title is already taken. Please use a different title', 'fluent-community')
962 ]);
963 }
964
965 $formattedData = [
966 'title' => sanitize_text_field($data['title']),
967 'description' => sanitize_textarea_field($data['description']),
968 'status' => 'active',
969 'type' => 'space_group',
970 'settings' => [
971 'always_show_spaces' => Arr::get($data, 'settings.always_show_spaces', 'yes'),
972 ]
973 ];
974
975 $group->fill($formattedData)->save();
976
977 return [
978 'message' => __('Space group has been updated', 'fluent-community'),
979 'group' => $group
980 ];
981 }
982
983 public function deleteSpaceGroup(Request $request, $groupId)
984 {
985 $group = SpaceGroup::findOrFail($groupId);
986
987 if (!$group->spaces->isEmpty()) {
988 return $this->sendError([
989 'message' => __('You cannot delete this group. It has spaces', 'fluent-community')
990 ]);
991 }
992
993 $group->delete();
994
995 return [
996 'message' => __('Space group has been deleted successfully', 'fluent-community')
997 ];
998 }
999
1000 public function updateSpaceGroupIndexes(Request $request)
1001 {
1002 $indexes = $request->get('indexes', []);
1003
1004 foreach ($indexes as $groupId => $indexNumber) {
1005 $group = SpaceGroup::findOrFail($groupId);
1006 $group->update([
1007 'serial' => $indexNumber + 1
1008 ]);
1009 }
1010
1011 return [
1012 'message' => __('Space group indexes have been updated.', 'fluent-community')
1013 ];
1014
1015 }
1016
1017 public function updateSpaceIndexes(Request $request)
1018 {
1019 $indexes = $request->get('indexes', []);
1020
1021 foreach ($indexes as $index => $spaceId) {
1022 $space = BaseSpace::withoutGlobalScopes()->findOrFail($spaceId);
1023 $space->update([
1024 'serial' => $index + 1
1025 ]);
1026 }
1027
1028 return [
1029 'message' => __('Space indexes have been updated.', 'fluent-community')
1030 ];
1031 }
1032
1033 public function moveSpace(Request $request)
1034 {
1035 $spaceId = $request->getSafe('space_id', 'intval');
1036 $groupId = $request->getSafe('group_id', 'intval');
1037
1038 $space = BaseSpace::withoutGlobalScopes()->findOrFail($spaceId);
1039 $group = SpaceGroup::findOrFail($groupId);
1040
1041 $space->update([
1042 'parent_id' => $group->id
1043 ]);
1044
1045 return [
1046 'message' => __('Space has been moved successfully', 'fluent-community')
1047 ];
1048 }
1049
1050 public function getLockScreenSettings(Request $request, $spaceSlug)
1051 {
1052 /** @var Space $space */
1053 $space = Space::where('slug', $spaceSlug)->first();
1054
1055 $userId = $this->getUser() ? $this->getUser()->ID : null;
1056
1057 if (!$space || ($space->privacy == 'secret' && !$space->getMembership($userId) && !$space->isAdmin($userId, true))) {
1058 return $this->sendError([
1059 'message' => __('Space not found', 'fluent-community')
1060 ], 404);
1061 }
1062
1063 $lockscreen = $space->getLockscreen();
1064
1065 $lockscreen = apply_filters('fluent_community/get_lockscreen_settings', $lockscreen, $space);
1066
1067 return [
1068 'lockscreen' => $lockscreen
1069 ];
1070 }
1071
1072 public function getMetaSettings(Request $request, $spaceSlug)
1073 {
1074 $space = Space::where('slug', $spaceSlug)->firstOrFail();
1075 $metaSettings = apply_filters('fluent_community/space/meta_fields', [], $space);
1076
1077 if (!$metaSettings) {
1078 return [
1079 'meta_settings' => null
1080 ];
1081 }
1082
1083 return [
1084 'meta_settings' => $metaSettings
1085 ];
1086 }
1087
1088 private function getActiveMemberCounts($spaceIds)
1089 {
1090 if (!$spaceIds) {
1091 return [];
1092 }
1093
1094 return SpaceUserPivot::whereIn('space_id', $spaceIds)
1095 ->where('status', 'active')
1096 ->whereHas('xprofile', function ($q) {
1097 $q->where('status', 'active');
1098 })
1099 ->whereHas('user')
1100 ->selectRaw('space_id, COUNT(*) as total')
1101 ->groupBy('space_id')
1102 ->pluck('total', 'space_id')
1103 ->toArray();
1104 }
1105 }
1106