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/Http/Controllers/SpaceController.php +453 -165 1.0.992.11.0 View file →
@@ -1,9 +1,8 @@
1 1 <?php
2 2
3 3 namespace FluentCommunity\App\Http\Controllers;
4 4
5 -use FluentCommunity\App\Functions\Utility;
6 5 use FluentCommunity\App\Models\Space;
7 6 use FluentCommunity\App\Models\Feed;
8 7 use FluentCommunity\App\Models\BaseSpace;
9 8 use FluentCommunity\App\Models\SpaceGroup;
@@ -8,11 +7,11 @@
8 7 use FluentCommunity\App\Models\BaseSpace;
9 8 use FluentCommunity\App\Models\SpaceGroup;
10 9 use FluentCommunity\App\Models\User;
11 10 use FluentCommunity\App\Services\CustomSanitizer;
12 -use FluentCommunity\App\Services\FeedsHelper;
13 11 use FluentCommunity\App\Services\Helper;
14 -use FluentCommunity\App\Services\LockscreenService;
12 +use FluentCommunity\App\Services\SpaceMenuService;
13 +use FluentCommunity\App\Functions\Utility;
15 14 use FluentCommunity\App\Services\ProfileHelper;
16 15 use FluentCommunity\Framework\Http\Request\Request;
17 16 use FluentCommunity\App\Models\Comment;
18 17 use FluentCommunity\App\Models\Reaction;
@@ -28,60 +27,85 @@
28 27 $q->where('user_id', get_current_user_id());
29 28 })
30 29 ->get();
31 30
32 - return [
31 + $data = [
33 32 'spaces' => $spaces
34 33 ];
34 +
35 + return apply_filters('fluent_community/spaces_api_response', $data, $this->request->all());
35 36 }
36 37
37 38 public function create(Request $request)
38 39 {
39 40 $currentUser = $this->getUser(true);
40 - $currentUser->verifyCommunityPermission('community_admin');
41 41
42 42 $data = $request->get('space', []);
43 43
44 - if (empty($data['slug'])) {
45 - $data['slug'] = sanitize_title($data['title'], '');
46 - } else {
47 - $data['slug'] = sanitize_title($data['slug'], '');
48 - }
44 + $slug = Arr::get($data, 'slug') ?: Arr::get($data, 'title');
49 45
50 - $data['title'] = sanitize_text_field($data['title']);
51 - $data['privacy'] = sanitize_text_field($data['privacy']);
46 + $slug = preg_replace('/[^a-zA-Z0-9-_]/', '', $slug);
52 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 +
53 52 $this->validate($data, [
54 - 'title' => 'required',
55 - 'slug' => 'required|unique:fcom_spaces,slug',
56 - 'privacy' => 'required|in:public,private,secret',
57 - 'parent_id' => 'required|exists:fcom_spaces,id'
58 - ], [
59 - 'parent_id.required' => __('Please select a menu group', 'fluent-community')
53 + 'title' => 'required',
54 + 'slug' => 'unique:fcom_spaces,slug',
55 + 'privacy' => 'required|in:public,private,secret'
60 56 ]);
61 57
62 - $spaceGroup = SpaceGroup::findOrFail($data['parent_id']);
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 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 +
64 80 $spaceData = apply_filters('fluent_community/space/create_data', [
65 81 'title' => sanitize_text_field($data['title']),
66 82 'slug' => $data['slug'],
67 83 'privacy' => $data['privacy'],
68 - 'description' => sanitize_textarea_field($data['description']),
69 - 'settings' => [
70 - 'restricted_post_only' => Arr::get($data, 'settings.restricted_post_only', 'no'),
71 - 'emoji' => CustomSanitizer::sanitizeEmoji(Arr::get($data, 'settings.emoji', '')),
72 - 'can_request_join' => Arr::get($data, 'settings.can_request_join', 'no'),
73 - 'custom_lock_screen' => Arr::get($data, 'settings.custom_lock_screen', 'no'),
74 - 'layout_style' => Arr::get($data, 'settings.layout_style', 'timeline'),
75 - 'show_sidebar' => Arr::get($data, 'settings.show_sidebar', 'yes'),
76 - 'shape_svg' => CustomSanitizer::sanitizeSvg(Arr::get($data, 'settings.shape_svg', '')),
77 - 'hide_members_count' => Arr::get($data, 'settings.hide_members_count', 'no') == 'yes' ? 'yes' : 'no',
78 - ],
79 - 'parent_id' => $spaceGroup->id,
80 - 'serial' => BaseSpace::where('parent_id', $spaceGroup->id)->max('serial') + 1
84 + 'description' => sanitize_textarea_field(Arr::get($data, 'description', '')),
85 + 'settings' => $settings,
86 + 'parent_id' => $spaceGroup ? $spaceGroup->id : null,
87 + 'serial' => $serial ?: 1
81 88 ]);
82 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 +
83 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 + }
84 108
85 109 $imageTypes = ['cover_photo', 'logo'];
86 110 $metaData = [];
87 111 foreach ($imageTypes as $type) {
@@ -107,8 +131,15 @@
107 131 $space->members()->attach(get_current_user_id(), [
108 132 'role' => 'admin'
109 133 ]);
110 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 +
111 142 $currentUser->cacheAccessSpaces();
112 143 do_action('fluent_community/space/created', $space, $data);
113 144
114 145 return [
@@ -118,64 +149,130 @@
118 149 }
119 150
120 151 public function discover(Request $request)
121 152 {
122 -
153 + $start = microtime(true);
123 154 $currentUser = $this->getUser();
155 + $type = $request->getSafe('type');
156 + $search = $request->getSafe('search');
157 + $sortBy = $request->getSafe('sort_by', 'sanitize_text_field', 'alphabetical');
124 158
125 - $spaces = Space::orderBy('title', 'ASC')
126 - ->with(['space_pivot' => function ($q) {
127 - $q->where('user_id', get_current_user_id());
128 - }])
159 + $spaces = Space::with(['space_pivot' => function ($q) {
160 + $q->where('user_id', get_current_user_id());
161 + }])
129 162 ->where(function ($q) {
130 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) {
131 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');
132 223 })
133 224 ->orWhereIn('privacy', ['public', 'private']);
134 - })
135 - ->get();
225 + });
226 + }
227 + $spaces = $spacesQuery->paginate();
136 228
229 + $memberCounts = $this->getActiveMemberCounts($spaces->pluck('id')->toArray());
230 +
137 231 foreach ($spaces as $space) {
138 - if(Arr::get($space->settings, 'hide_members_count') == 'yes' && (!$currentUser || !$space->verifyUserPermisson($currentUser, 'can_view_members', false))) {
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) {
139 236 $space->members_count = 0;
140 237 continue;
141 238 }
142 239
143 - $space->members_count = $space->members()->wherePivot('status', 'active')->count();
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]);
144 244 }
145 245
146 - return [
246 + return apply_filters('fluent_community/all_spaces_api_response', [
147 247 'spaces' => $spaces
148 - ];
248 + ], $request->all());
149 249 }
150 250
151 251 public function getBySlug(Request $request, $spaceSlug)
152 252 {
153 253 $user = $this->getUser();
154 - $space = Space::where('slug', $spaceSlug)
155 - ->firstOrFail();
254 + $space = Space::where('slug', $spaceSlug)->first();
156 255
157 - $space->permissions = $space->getUserPermissions($user);
158 - $space->description_rendered = FeedsHelper::mdToHtml($space->description);
159 - $space->membership = $space->getMembership(get_current_user_id());
160 - $space->topics = Utility::getTopicsBySpaceId($space->id);
256 + $userId = $user ? $user->ID : null;
161 257
162 - if (!Helper::isSiteAdmin()) {
163 - $space->lockscreen_config = LockscreenService::getLockscreenConfig($space, $space->membership);
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);
164 264 }
165 265
166 - if ($space->privacy == 'secret' && !$space->membership) {
167 - return $this->sendError([
168 - 'message' => __('You are not allowed to view this space', 'fluent-community'),
169 - 'error_type' => 'restricted'
170 - ]);
171 - }
266 + $space = $space->formatSpaceData($user);
172 267
173 268 do_action_ref_array('fluent_community/space', [&$space]);
174 269
175 - return [
270 + $data = [
176 271 'space' => $space
177 272 ];
273 +
274 + return apply_filters('fluent_community/space_api_response', $data, $request->all());
178 275 }
179 276
180 277 public function patchBySlug(Request $request, $slug)
181 278 {
@@ -183,42 +280,57 @@
183 280 ->first();
184 281
185 282 if (!$space) {
186 283 return $this->sendError([
187 - 'message' => 'Space not found'
284 + 'message' => __('Space not found', 'fluent-community')
188 285 ]);
189 286 }
190 287
191 - $space->verifyUserPermisson($this->getUser(), 'community_admin');
288 + $data = $request->get('data', []);
192 289
193 - $data = $request->get('data', []);
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 + }
194 295
195 - if (!empty($data['title'])) {
196 - $taken = Space::where('title', $data['title'])
296 + if (!empty($data['slug'])) {
297 + $taken = Space::where('slug', $data['slug'])
197 298 ->where('id', '!=', $space->id)
198 299 ->first();
300 +
199 301 if ($taken) {
200 302 return $this->sendError([
201 - 'message' => 'Space title is already taken. Please use a different title'
303 + 'message' => __('Slug is already taken. Please use a different slug', 'fluent-community')
202 304 ]);
203 305 }
204 306 }
205 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 +
206 324 $mediaTypes = ['cover_photo', 'logo'];
207 325 foreach ($mediaTypes as $type) {
208 326 if (!empty($data[$type])) {
209 327 $media = Helper::getMediaFromUrl($data[$type]);
210 - if (!$media) {
328 + if (!$media || $media->is_active) {
211 329 unset($data[$type]);
212 330 continue;
213 331 }
214 332
215 - if (!$media || $media->is_active) {
216 - return $this->sendError([
217 - 'message' => 'Invalid media image. Please upload a new one.'
218 - ]);
219 - }
220 -
221 333 $data[$type] = $media->public_url;
222 334
223 335 $media->update([
224 336 'is_active' => true,
@@ -232,22 +344,53 @@
232 344 }
233 345
234 346 $data = apply_filters('fluent_community/space/update_data', $data, $space);
235 347
236 - $space->updateCustomData($data, true);
348 + if (isset($data['parent_id']) && !$data['parent_id']) {
349 + $data['parent_id'] = '';
350 + }
237 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 +
238 360 if (Arr::has($data, 'topic_ids')) {
361 + $topicsConfig = Helper::getTopicsConfig();
239 362 $topicIds = (array)Arr::get($data, 'topic_ids', []);
363 + $topicIds = array_slice($topicIds, 0, $topicsConfig['max_topics_per_space']);
240 364 $space->syncTopics($topicIds);
241 365 }
242 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 +
243 377 return [
244 - 'message' => __('Space has been updated', 'fluent-community')
378 + 'message' => __('Settings have been updated', 'fluent-community'),
379 + 'redirect_url' => $slugUpdated ? $space->getPermalink() : ''
245 380 ];
246 381 }
247 382
383 + public function patchById(Request $request, $id)
384 + {
385 + $space = Space::findOrFail($id);
386 +
387 + return $this->patchBySlug($request, $space->slug);
388 + }
389 +
248 390 public function getMembers(Request $request, $slug)
249 391 {
392 + /** @var Space $space */
250 393 $space = Space::where('slug', $slug)
251 394 ->firstOrFail();
252 395
253 396 $user = $this->getUser();
@@ -257,8 +400,9 @@
257 400 'message' => __('You are not allowed to view members of this space', 'fluent-community'),
258 401 'permission_failed' => true
259 402 ]);
260 403 }
404 +
261 405 $search = $request->getSafe('search', 'sanitize_text_field');
262 406
263 407 $pendingCount = 0;
264 408 if ($user && $user->can('can_add_member', $space)) {
@@ -277,30 +421,48 @@
277 421 }])
278 422 ->where('status', 'pending')
279 423 ->paginate();
280 424
281 - return [
425 + return apply_filters('fluent_community/space_members_api_response', [
282 426 'members' => $pendingRequests,
283 427 'pending_count' => $pendingCount
284 - ];
428 + ], $pendingRequests, $request->all());
285 429 }
286 430 }
287 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 +
288 446 $spaceMembers = SpaceUserPivot::bySpace($space->id)
289 447 ->whereHas('xprofile', function ($q) use ($search) {
290 - return $q->searchBy($search)
291 - ->where('status', 'active');
448 + $q->searchBy($search)->where('status', 'active');
292 449 })
450 + ->where('fcom_space_user.status', 'active')
293 451 ->with(['xprofile' => function ($q) {
294 452 $q->select(ProfileHelper::getXProfilePublicFields());
295 453 }])
296 - ->where('status', 'active')
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)
297 459 ->paginate();
298 460
299 - return [
461 + return apply_filters('fluent_community/space_members_api_response', [
300 462 'members' => $spaceMembers,
301 463 'pending_count' => $pendingCount
302 - ];
464 + ], $spaceMembers, $request->all());
303 465 }
304 466
305 467 public function join(Request $request, $slug)
306 468 {
@@ -307,9 +469,9 @@
307 469 $space = Space::where('slug', $slug)->first();
308 470
309 471 if (!$space) {
310 472 return $this->sendError([
311 - 'message' => 'Space not found'
473 + 'message' => __('Space not found', 'fluent-community')
312 474 ]);
313 475 }
314 476
315 477 $user = $this->getUser();
@@ -317,22 +479,23 @@
317 479 $membership = $space->getMembership(get_current_user_id());
318 480
319 481 if ($membership) {
320 482 return $this->sendError([
321 - 'message' => 'You are already a member of this space. Please reload this page'
483 + 'message' => __('You are already a member of this space. Please reload this page', 'fluent-community')
322 484 ]);
323 485 }
324 486
325 487 $roles = $user->getCommunityRoles();
488 + $isPrivileged = !!array_intersect($roles, ['admin', 'moderator']);
326 489
327 - if (!$roles && $space->privacy == 'secret') {
490 + if (!$isPrivileged && $space->privacy == 'secret') {
328 491 return $this->sendError([
329 - 'message' => 'You are not allowed to join this space'
492 + 'message' => __('You are not allowed to join this space', 'fluent-community')
330 493 ]);
331 494 }
332 495
333 496 $status = 'active';
334 - if (!$roles) {
497 + if (!$isPrivileged) {
335 498 if ($space->privacy != 'public') {
336 499 $status = apply_filters('fluent_community/space/join_status_for_private', 'pending', $space, $user);
337 500
338 501 if (!in_array($status, ['pending', 'active'])) {
@@ -372,9 +535,9 @@
372 535 $space = Space::where('slug', $slug)->first();
373 536
374 537 if (!$space) {
375 538 return $this->sendError([
376 - 'message' => 'Space not found'
539 + 'message' => __('Space not found', 'fluent-community')
377 540 ]);
378 541 }
379 542
380 543 $membership = $space->getMembership($user->ID);
@@ -380,9 +543,9 @@
380 543 $membership = $space->getMembership($user->ID);
381 544
382 545 if (!$membership) {
383 546 return $this->sendError([
384 - 'message' => 'You are not a member of this community'
547 + 'message' => __('You are not a member of this community', 'fluent-community')
385 548 ]);
386 549 }
387 550
388 551 Helper::removeFromSpace($space, $user->ID, 'self');
@@ -391,24 +554,18 @@
391 554 'message' => __('You have left this space', 'fluent-community')
392 555 ];
393 556 }
394 557
395 - public function delete(Request $request, $slug)
558 + public function deleteBySlug(Request $request, $slug)
396 559 {
397 560 $space = Space::where('slug', $slug)->first();
398 561
399 562 if (!$space) {
400 563 return $this->sendError([
401 - 'message' => 'Space not found'
564 + 'message' => __('Space not found', 'fluent-community')
402 565 ]);
403 566 }
404 567
405 - if (!Helper::isSiteAdmin()) {
406 - return $this->sendError([
407 - 'message' => 'You are not allowed to delete this community'
408 - ]);
409 - }
410 -
411 568 do_action('fluent_community/space/before_delete', $space);
412 569
413 570 Comment::whereHas('post', function ($q) use ($space) {
414 571 $q->where('space_id', $space->id);
@@ -431,8 +588,15 @@
431 588 'message' => __('Space has been deleted successfully', 'fluent-community')
432 589 ];
433 590 }
434 591
592 + public function deleteById(Request $request, $id)
593 + {
594 + $space = Space::findOrFail($id);
595 +
596 + return $this->deleteBySlug($request, $space->slug);
597 + }
598 +
435 599 public function addMember(Request $request, $slug)
436 600 {
437 601 $space = Space::where('slug', $slug)->first();
438 602
@@ -437,9 +601,9 @@
437 601 $space = Space::where('slug', $slug)->first();
438 602
439 603 if (!$space) {
440 604 return $this->sendError([
441 - 'message' => 'Space not found'
605 + 'message' => __('Space not found', 'fluent-community')
442 606 ]);
443 607 }
444 608
445 609 $this->validate($request->all(), [
@@ -447,9 +611,10 @@
447 611 ]);
448 612
449 613 $userId = $request->get('user_id');
450 614 $targetUser = User::findOrFail($userId);
451 - $xprofile = $targetUser->syncXProfile();
615 + $targetUser->syncXProfile();
616 + $xprofile = $targetUser->xprofile;
452 617
453 618 if ($xprofile && $xprofile->status != 'active') {
454 619 return $this->sendError([
455 620 'message' => __('Selected user is not active', 'fluent-community')
@@ -455,16 +620,15 @@
455 620 'message' => __('Selected user is not active', 'fluent-community')
456 621 ]);
457 622 }
458 623
459 - $admin = User::find(get_current_user_id());
460 - $admin->verifySpacePermission('can_add_member', $space);
461 -
462 624 $pivot = SpaceUserPivot::bySpace($space->id)
463 625 ->byUser($userId)
464 626 ->first();
465 627
466 - $role = $request->get('role', 'member');
628 + $allowedRoles = ['member', 'moderator', 'admin'];
629 + $requestedRole = $request->get('role', 'member');
630 + $role = in_array($requestedRole, $allowedRoles, true) ? $requestedRole : 'member';
467 631
468 632 if ($pivot) {
469 633 if ($pivot->status == 'active') {
470 634 if ($role != $pivot->role) {
@@ -473,14 +637,14 @@
473 637
474 638 do_action('fluent_community/space/member/role_updated', $space, $pivot);
475 639
476 640 return [
477 - 'message' => 'Member role updated'
641 + 'message' => __('Member role updated', 'fluent-community')
478 642 ];
479 643 }
480 644
481 645 return $this->sendError([
482 - 'message' => 'Selected user is already a member of this community'
646 + 'message' => __('Selected user is already a member of this community', 'fluent-community')
483 647 ]);
484 648 }
485 649
486 650 $pivot->status = 'active';
@@ -491,9 +655,9 @@
491 655 do_action('fluent_community/space/member/role_updated', $space, $pivot);
492 656 }
493 657
494 658 return [
495 - 'message' => 'Member approved'
659 + 'message' => __('Member approved', 'fluent-community')
496 660 ];
497 661 }
498 662
499 663 $space->members()->attach($userId, [
@@ -505,9 +669,9 @@
505 669
506 670 do_action('fluent_community/space/joined', $space, $userId, 'by_admin');
507 671
508 672 return [
509 - 'message' => 'User has been added to this community'
673 + 'message' => __('User has been added to this Space', 'fluent-community')
510 674 ];
511 675 }
512 676
513 677 public function removeMember(Request $request, $slug)
@@ -515,17 +679,14 @@
515 679 $space = Space::where('slug', $slug)->first();
516 680
517 681 if (!$space) {
518 682 return $this->sendError([
519 - 'message' => 'Space not found'
683 + 'message' => __('Space not found', 'fluent-community')
520 684 ]);
521 685 }
522 686
523 687 $userId = $request->get('user_id');
524 688
525 - $admin = User::find(get_current_user_id());
526 - $admin->verifySpacePermission('can_remove_member', $space);
527 -
528 689 $pivot = SpaceUserPivot::bySpace($space->id)
529 690 ->byUser($userId)
530 691 ->first();
531 692
@@ -530,9 +691,9 @@
530 691 ->first();
531 692
532 693 if (!$pivot) {
533 694 return $this->sendError([
534 - 'message' => 'Selected user is not a member of this community'
695 + 'message' => __('Selected user is not a member of this community', 'fluent-community')
535 696 ]);
536 697 }
537 698
538 699 $pivot->delete();
@@ -560,25 +721,53 @@
560 721
561 722 $isMod = $currentUser->isCommunityModerator() && current_user_can('list_users');
562 723
563 724 $spaceId = $request->get('space_id');
725 + $space = Space::findOrFail($spaceId);
564 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 +
565 733 $selects = ['ID', 'display_name'];
566 -
567 734 if ($isMod) {
568 735 $selects[] = 'user_email';
569 736 }
570 737
571 - $users = User::whereDoesntHave('spaces', function ($q) use ($spaceId) {
572 - return $q->where('space_id', $spaceId);
573 - })
574 - ->select($selects)
575 - ->searchBy($request->get('search'))
576 - ->paginate();
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'));
577 744
578 - return [
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 = [
579 767 'users' => $users
580 768 ];
769 + return apply_filters('fluent_community/space_non_members_api_response', $data, $request->all());
581 770 }
582 771
583 772 public function updateLinks(Request $request, $slug)
584 773 {
@@ -585,14 +774,12 @@
585 774 $space = Space::where('slug', $slug)->first();
586 775
587 776 if (!$space) {
588 777 return $this->sendError([
589 - 'message' => 'Space not found'
778 + 'message' => __('Space not found', 'fluent-community')
590 779 ]);
591 780 }
592 781
593 - $space->verifyUserPermisson($this->getUser(), 'community_admin');
594 -
595 782 $links = $request->get('links', []);
596 783
597 784 $links = array_map(function ($link) {
598 785 return CustomSanitizer::santizeLinkItem($link);
@@ -608,41 +795,112 @@
608 795 'links' => $links
609 796 ];
610 797 }
611 798
612 - public function getSpaceGroups(Request $request)
799 + public function getPrimaryMenu(Request $request, $slug)
613 800 {
614 - $user = $this->getUser(true);
615 - if (!$user->isCommunityModerator()) {
801 + $space = Space::where('slug', $slug)->first();
802 +
803 + if (!$space) {
616 804 return $this->sendError([
617 - 'message' => 'You are not allowed to create space group'
805 + 'message' => __('Space not found', 'fluent-community'),
618 806 ]);
619 807 }
620 808
809 + // Lighter than formatSpaceData(), but listeners like fcom-chat need membership too.
621 810 $user = $this->getUser();
811 + $space->permissions = $space->getUserPermissions($user);
812 + $space->membership = $space->getMembership($user ? $user->ID : null);
622 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 +
623 866 $groups = Helper::getAllCommunityGroups($user, false);
624 867
625 868 foreach ($groups as $group) {
626 869 foreach ($group->spaces as $space) {
627 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 + }
628 876 }
629 877 }
630 878
631 - return [
632 - 'groups' => $groups
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
633 897 ];
898 + return apply_filters('fluent_community/space_groups_api_response', $data, $request->all());
634 899 }
635 900
636 901 public function createSpaceGroup(Request $request)
637 902 {
638 - $user = $this->getUser(true);
639 - if (!$user->isCommunityModerator()) {
640 - return $this->sendError([
641 - 'message' => 'You are not allowed to create space group'
642 - ]);
643 - }
644 -
645 903 $data = $request->all();
646 904
647 905 $this->validate($data, [
648 906 'title' => 'required|unique:fcom_spaces,title',
@@ -648,13 +906,28 @@
648 906 'title' => 'required|unique:fcom_spaces,title',
649 907 'slug' => 'required|unique:fcom_spaces,slug'
650 908 ]);
651 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', ''));
652 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 +
653 926 $formattedData = [
654 - 'title' => sanitize_text_field($data['title']),
655 - 'slug' => sanitize_title($data['slug']),
656 - 'description' => sanitize_textarea_field($data['description']),
927 + 'title' => $title,
928 + 'slug' => $slug,
929 + 'description' => $desc,
657 930 'status' => 'active',
658 931 'type' => 'space_group',
659 932 'settings' => [
660 933 'always_show_spaces' => Arr::get($data, 'settings.always_show_spaces', 'yes'),
@@ -671,15 +944,8 @@
671 944 }
672 945
673 946 public function updateSpaceGroup(Request $request, $groupId)
674 947 {
675 - $user = $this->getUser();
676 - if (!$user || !$user->isCommunityModerator()) {
677 - return $this->sendError([
678 - 'message' => 'You are not allowed to create space group'
679 - ]);
680 - }
681 -
682 948 $group = SpaceGroup::findOrFail($groupId);
683 949 $data = $request->all();
684 950
685 951 $this->validate($data, [
@@ -691,9 +957,9 @@
691 957 ->first();
692 958
693 959 if ($taken) {
694 960 return $this->sendError([
695 - 'message' => 'The title is already taken. Please use a different title'
961 + 'message' => __('The title is already taken. Please use a different title', 'fluent-community')
696 962 ]);
697 963 }
698 964
699 965 $formattedData = [
@@ -708,9 +974,9 @@
708 974
709 975 $group->fill($formattedData)->save();
710 976
711 977 return [
712 - 'message' => __('Space group has been created updated', 'fluent-community'),
978 + 'message' => __('Space group has been updated', 'fluent-community'),
713 979 'group' => $group
714 980 ];
715 981 }
716 982
@@ -715,21 +981,13 @@
715 981 }
716 982
717 983 public function deleteSpaceGroup(Request $request, $groupId)
718 984 {
719 -
720 - $user = $this->getUser();
721 - if (!$user || !$user->isCommunityModerator()) {
722 - return $this->sendError([
723 - 'message' => 'You are not allowed to create space group'
724 - ]);
725 - }
726 -
727 985 $group = SpaceGroup::findOrFail($groupId);
728 986
729 987 if (!$group->spaces->isEmpty()) {
730 988 return $this->sendError([
731 - 'message' => 'You can not delete this group. It has spaces'
989 + 'message' => __('You cannot delete this group. It has spaces', 'fluent-community')
732 990 ]);
733 991 }
734 992
735 993 $group->delete();
@@ -780,9 +1038,9 @@
780 1038 $space = BaseSpace::withoutGlobalScopes()->findOrFail($spaceId);
781 1039 $group = SpaceGroup::findOrFail($groupId);
782 1040
783 1041 $space->update([
784 - 'parent_id' => $groupId
1042 + 'parent_id' => $group->id
785 1043 ]);
786 1044
787 1045 return [
788 1046 'message' => __('Space has been moved successfully', 'fluent-community')
@@ -790,28 +1048,58 @@
790 1048 }
791 1049
792 1050 public function getLockScreenSettings(Request $request, $spaceSlug)
793 1051 {
794 - $space = Space::where('slug', $spaceSlug)->firstOrFail();
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 +
795 1063 $lockscreen = $space->getLockscreen();
796 1064
1065 + $lockscreen = apply_filters('fluent_community/get_lockscreen_settings', $lockscreen, $space);
1066 +
797 1067 return [
798 1068 'lockscreen' => $lockscreen
799 1069 ];
800 1070 }
801 1071
802 - public function updateLockscreenSettings(Request $request, $spaceSlug)
1072 + public function getMetaSettings(Request $request, $spaceSlug)
803 1073 {
804 1074 $space = Space::where('slug', $spaceSlug)->firstOrFail();
1075 + $metaSettings = apply_filters('fluent_community/space/meta_fields', [], $space);
805 1076
806 - $settingFields = $request->get('lockscreen', []);
1077 + if (!$metaSettings) {
1078 + return [
1079 + 'meta_settings' => null
1080 + ];
1081 + }
807 1082
808 - $formattedFields = LockscreenService::formatLockscreenFields($settingFields);
1083 + return [
1084 + 'meta_settings' => $metaSettings
1085 + ];
1086 + }
809 1087
810 - $space->setLockscreen($formattedFields);
1088 + private function getActiveMemberCounts($spaceIds)
1089 + {
1090 + if (!$spaceIds) {
1091 + return [];
1092 + }
811 1093
812 - return [
813 - 'message' => 'Lockscreen settings have been updated successfully.',
814 - 'community' => $space
815 - ];
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();
816 1104 }
817 1105 }