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 +479 -171 1.0.932.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,57 +27,86 @@
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 - $space = Space::create([
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', [
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 - ],
77 - 'parent_id' => $spaceGroup->id,
78 - '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
79 88 ]);
80 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 +
81 109 $imageTypes = ['cover_photo', 'logo'];
82 110 $metaData = [];
83 111 foreach ($imageTypes as $type) {
84 112 if (!empty($data[$type])) {
@@ -103,10 +131,16 @@
103 131 $space->members()->attach(get_current_user_id(), [
104 132 'role' => 'admin'
105 133 ]);
106 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 +
107 142 $currentUser->cacheAccessSpaces();
108 -
109 143 do_action('fluent_community/space/created', $space, $data);
110 144
111 145 return [
112 146 'message' => __('Space has been created successfully', 'fluent-community'),
@@ -115,56 +149,130 @@
115 149 }
116 150
117 151 public function discover(Request $request)
118 152 {
119 - $spaces = Space::orderBy('title', 'ASC')
120 - ->with(['space_pivot' => function ($q) {
121 - $q->where('user_id', get_current_user_id());
122 - }])
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 + }])
123 162 ->where(function ($q) {
124 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) {
125 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');
126 223 })
127 224 ->orWhereIn('privacy', ['public', 'private']);
128 - })
129 - ->get();
225 + });
226 + }
227 + $spaces = $spacesQuery->paginate();
130 228
229 + $memberCounts = $this->getActiveMemberCounts($spaces->pluck('id')->toArray());
230 +
131 231 foreach ($spaces as $space) {
132 - $space->members_count = $space->members()->wherePivot('status', 'active')->count();
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]);
133 244 }
134 245
135 - return [
246 + return apply_filters('fluent_community/all_spaces_api_response', [
136 247 'spaces' => $spaces
137 - ];
248 + ], $request->all());
138 249 }
139 250
140 251 public function getBySlug(Request $request, $spaceSlug)
141 252 {
142 253 $user = $this->getUser();
143 - $space = Space::where('slug', $spaceSlug)
144 - ->firstOrFail();
254 + $space = Space::where('slug', $spaceSlug)->first();
145 255
146 - $space->permissions = $space->getUserPermissions($user);
147 - $space->description_rendered = FeedsHelper::mdToHtml($space->description);
148 - $space->membership = $space->getMembership(get_current_user_id());
149 - $space->topics = Utility::getTopicsBySpaceId($space->id);
256 + $userId = $user ? $user->ID : null;
150 257
151 - if (!Helper::isSiteAdmin()) {
152 - $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);
153 264 }
154 265
155 - if ($space->privacy == 'secret' && !$space->membership) {
156 - return $this->sendError([
157 - 'message' => __('You are not allowed to view this space', 'fluent-community'),
158 - 'error_type' => 'restricted'
159 - ]);
160 - }
266 + $space = $space->formatSpaceData($user);
161 267
162 268 do_action_ref_array('fluent_community/space', [&$space]);
163 269
164 - return [
270 + $data = [
165 271 'space' => $space
166 272 ];
273 +
274 + return apply_filters('fluent_community/space_api_response', $data, $request->all());
167 275 }
168 276
169 277 public function patchBySlug(Request $request, $slug)
170 278 {
@@ -172,42 +280,57 @@
172 280 ->first();
173 281
174 282 if (!$space) {
175 283 return $this->sendError([
176 - 'message' => 'Space not found'
284 + 'message' => __('Space not found', 'fluent-community')
177 285 ]);
178 286 }
179 287
180 - $space->verifyUserPermisson($this->getUser(), 'community_admin');
288 + $data = $request->get('data', []);
181 289
182 - $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 + }
183 295
184 - if (!empty($data['title'])) {
185 - $taken = Space::where('title', $data['title'])
296 + if (!empty($data['slug'])) {
297 + $taken = Space::where('slug', $data['slug'])
186 298 ->where('id', '!=', $space->id)
187 299 ->first();
300 +
188 301 if ($taken) {
189 302 return $this->sendError([
190 - 'message' => 'Space title is already taken. Please use a different title'
303 + 'message' => __('Slug is already taken. Please use a different slug', 'fluent-community')
191 304 ]);
192 305 }
193 306 }
194 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 +
195 324 $mediaTypes = ['cover_photo', 'logo'];
196 325 foreach ($mediaTypes as $type) {
197 326 if (!empty($data[$type])) {
198 327 $media = Helper::getMediaFromUrl($data[$type]);
199 - if (!$media) {
328 + if (!$media || $media->is_active) {
200 329 unset($data[$type]);
201 330 continue;
202 331 }
203 332
204 - if (!$media || $media->is_active) {
205 - return $this->sendError([
206 - 'message' => 'Invalid media image. Please upload a new one.'
207 - ]);
208 - }
209 -
210 333 $data[$type] = $media->public_url;
211 334
212 335 $media->update([
213 336 'is_active' => true,
@@ -219,31 +342,67 @@
219 342 $data[$type] = '';
220 343 }
221 344 }
222 345
223 - $space->updateCustomData($data, true);
346 + $data = apply_filters('fluent_community/space/update_data', $data, $space);
224 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 +
225 360 if (Arr::has($data, 'topic_ids')) {
226 - $topicIds = Arr::get($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']);
227 364 $space->syncTopics($topicIds);
228 365 }
229 366
230 367 do_action('fluent_community/space/updated', $space, $data);
368 + $slugUpdated = $slug != $space->slug;
231 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 + }
232 376
233 377 return [
234 - 'message' => __('Space has been updated', 'fluent-community')
378 + 'message' => __('Settings have been updated', 'fluent-community'),
379 + 'redirect_url' => $slugUpdated ? $space->getPermalink() : ''
235 380 ];
236 381 }
237 382
383 + public function patchById(Request $request, $id)
384 + {
385 + $space = Space::findOrFail($id);
386 +
387 + return $this->patchBySlug($request, $space->slug);
388 + }
389 +
238 390 public function getMembers(Request $request, $slug)
239 391 {
392 + /** @var Space $space */
240 393 $space = Space::where('slug', $slug)
241 394 ->firstOrFail();
242 395
243 396 $user = $this->getUser();
244 - $space->verifyUserPermisson($user, 'can_view_members');
245 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 +
246 405 $search = $request->getSafe('search', 'sanitize_text_field');
247 406
248 407 $pendingCount = 0;
249 408 if ($user && $user->can('can_add_member', $space)) {
@@ -262,30 +421,48 @@
262 421 }])
263 422 ->where('status', 'pending')
264 423 ->paginate();
265 424
266 - return [
425 + return apply_filters('fluent_community/space_members_api_response', [
267 426 'members' => $pendingRequests,
268 427 'pending_count' => $pendingCount
269 - ];
428 + ], $pendingRequests, $request->all());
270 429 }
271 430 }
272 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 +
273 446 $spaceMembers = SpaceUserPivot::bySpace($space->id)
274 447 ->whereHas('xprofile', function ($q) use ($search) {
275 - return $q->searchBy($search)
276 - ->where('status', 'active');
448 + $q->searchBy($search)->where('status', 'active');
277 449 })
450 + ->where('fcom_space_user.status', 'active')
278 451 ->with(['xprofile' => function ($q) {
279 452 $q->select(ProfileHelper::getXProfilePublicFields());
280 453 }])
281 - ->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)
282 459 ->paginate();
283 460
284 - return [
461 + return apply_filters('fluent_community/space_members_api_response', [
285 462 'members' => $spaceMembers,
286 463 'pending_count' => $pendingCount
287 - ];
464 + ], $spaceMembers, $request->all());
288 465 }
289 466
290 467 public function join(Request $request, $slug)
291 468 {
@@ -292,9 +469,9 @@
292 469 $space = Space::where('slug', $slug)->first();
293 470
294 471 if (!$space) {
295 472 return $this->sendError([
296 - 'message' => 'Space not found'
473 + 'message' => __('Space not found', 'fluent-community')
297 474 ]);
298 475 }
299 476
300 477 $user = $this->getUser();
@@ -302,24 +479,29 @@
302 479 $membership = $space->getMembership(get_current_user_id());
303 480
304 481 if ($membership) {
305 482 return $this->sendError([
306 - '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')
307 484 ]);
308 485 }
309 486
310 487 $roles = $user->getCommunityRoles();
488 + $isPrivileged = !!array_intersect($roles, ['admin', 'moderator']);
311 489
312 - if (!$roles && $space->privacy == 'secret') {
490 + if (!$isPrivileged && $space->privacy == 'secret') {
313 491 return $this->sendError([
314 - 'message' => 'You are not allowed to join this space'
492 + 'message' => __('You are not allowed to join this space', 'fluent-community')
315 493 ]);
316 494 }
317 495
318 496 $status = 'active';
319 - if (!$roles) {
497 + if (!$isPrivileged) {
320 498 if ($space->privacy != 'public') {
321 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 + }
322 504 }
323 505 $role = 'member';
324 506 } else {
325 507 $role = $user->isCommunityAdmin() ? 'admin' : 'moderator';
@@ -324,8 +506,9 @@
324 506 } else {
325 507 $role = $user->isCommunityAdmin() ? 'admin' : 'moderator';
326 508 }
327 509
510 +
328 511 $space->members()->attach(get_current_user_id(), [
329 512 'role' => $role,
330 513 'status' => $status
331 514 ]);
@@ -340,9 +523,9 @@
340 523
341 524 $user->cacheAccessSpaces();
342 525
343 526 return [
344 - 'message' => ($status == 'active') ? __('You have joined this Space', 'fluent-community') : __('Your join request has been sent to the Space admin', 'fluent-community'),
527 + 'message' => ($status == 'active') ? __('You have joined this Space', 'fluent-community') : __('Your join request has been sent to the Space admin.', 'fluent-community'),
345 528 'membership' => $space->membership
346 529 ];
347 530 }
348 531
@@ -352,9 +535,9 @@
352 535 $space = Space::where('slug', $slug)->first();
353 536
354 537 if (!$space) {
355 538 return $this->sendError([
356 - 'message' => 'Space not found'
539 + 'message' => __('Space not found', 'fluent-community')
357 540 ]);
358 541 }
359 542
360 543 $membership = $space->getMembership($user->ID);
@@ -360,9 +543,9 @@
360 543 $membership = $space->getMembership($user->ID);
361 544
362 545 if (!$membership) {
363 546 return $this->sendError([
364 - 'message' => 'You are not a member of this community'
547 + 'message' => __('You are not a member of this community', 'fluent-community')
365 548 ]);
366 549 }
367 550
368 551 Helper::removeFromSpace($space, $user->ID, 'self');
@@ -371,24 +554,18 @@
371 554 'message' => __('You have left this space', 'fluent-community')
372 555 ];
373 556 }
374 557
375 - public function delete(Request $request, $slug)
558 + public function deleteBySlug(Request $request, $slug)
376 559 {
377 560 $space = Space::where('slug', $slug)->first();
378 561
379 562 if (!$space) {
380 563 return $this->sendError([
381 - 'message' => 'Space not found'
564 + 'message' => __('Space not found', 'fluent-community')
382 565 ]);
383 566 }
384 567
385 - if (!Helper::isSiteAdmin()) {
386 - return $this->sendError([
387 - 'message' => 'You are not allowed to delete this community'
388 - ]);
389 - }
390 -
391 568 do_action('fluent_community/space/before_delete', $space);
392 569
393 570 Comment::whereHas('post', function ($q) use ($space) {
394 571 $q->where('space_id', $space->id);
@@ -411,8 +588,15 @@
411 588 'message' => __('Space has been deleted successfully', 'fluent-community')
412 589 ];
413 590 }
414 591
592 + public function deleteById(Request $request, $id)
593 + {
594 + $space = Space::findOrFail($id);
595 +
596 + return $this->deleteBySlug($request, $space->slug);
597 + }
598 +
415 599 public function addMember(Request $request, $slug)
416 600 {
417 601 $space = Space::where('slug', $slug)->first();
418 602
@@ -417,9 +601,9 @@
417 601 $space = Space::where('slug', $slug)->first();
418 602
419 603 if (!$space) {
420 604 return $this->sendError([
421 - 'message' => 'Space not found'
605 + 'message' => __('Space not found', 'fluent-community')
422 606 ]);
423 607 }
424 608
425 609 $this->validate($request->all(), [
@@ -427,9 +611,10 @@
427 611 ]);
428 612
429 613 $userId = $request->get('user_id');
430 614 $targetUser = User::findOrFail($userId);
431 - $xprofile = $targetUser->syncXProfile();
615 + $targetUser->syncXProfile();
616 + $xprofile = $targetUser->xprofile;
432 617
433 618 if ($xprofile && $xprofile->status != 'active') {
434 619 return $this->sendError([
435 620 'message' => __('Selected user is not active', 'fluent-community')
@@ -435,16 +620,15 @@
435 620 'message' => __('Selected user is not active', 'fluent-community')
436 621 ]);
437 622 }
438 623
439 - $admin = User::find(get_current_user_id());
440 - $admin->verifySpacePermission('can_add_member', $space);
441 -
442 624 $pivot = SpaceUserPivot::bySpace($space->id)
443 625 ->byUser($userId)
444 626 ->first();
445 627
446 - $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';
447 631
448 632 if ($pivot) {
449 633 if ($pivot->status == 'active') {
450 634 if ($role != $pivot->role) {
@@ -453,20 +637,20 @@
453 637
454 638 do_action('fluent_community/space/member/role_updated', $space, $pivot);
455 639
456 640 return [
457 - 'message' => 'Member role updated'
641 + 'message' => __('Member role updated', 'fluent-community')
458 642 ];
459 643 }
460 644
461 645 return $this->sendError([
462 - 'message' => 'Selected user is already a member of this community'
646 + 'message' => __('Selected user is already a member of this community', 'fluent-community')
463 647 ]);
464 648 }
465 649
466 650 $pivot->status = 'active';
467 651 $pivot->save();
468 - do_action('fluent_community/space/joined', $space, $userId, 'approved');
652 + do_action('fluent_community/space/joined', $space, $userId, 'by_admin');
469 653
470 654 if ($role != 'member') {
471 655 do_action('fluent_community/space/member/role_updated', $space, $pivot);
472 656 }
@@ -471,9 +655,9 @@
471 655 do_action('fluent_community/space/member/role_updated', $space, $pivot);
472 656 }
473 657
474 658 return [
475 - 'message' => 'Member approved'
659 + 'message' => __('Member approved', 'fluent-community')
476 660 ];
477 661 }
478 662
479 663 $space->members()->attach($userId, [
@@ -485,9 +669,9 @@
485 669
486 670 do_action('fluent_community/space/joined', $space, $userId, 'by_admin');
487 671
488 672 return [
489 - 'message' => 'User has been added to this community'
673 + 'message' => __('User has been added to this Space', 'fluent-community')
490 674 ];
491 675 }
492 676
493 677 public function removeMember(Request $request, $slug)
@@ -495,17 +679,14 @@
495 679 $space = Space::where('slug', $slug)->first();
496 680
497 681 if (!$space) {
498 682 return $this->sendError([
499 - 'message' => 'Space not found'
683 + 'message' => __('Space not found', 'fluent-community')
500 684 ]);
501 685 }
502 686
503 687 $userId = $request->get('user_id');
504 688
505 - $admin = User::find(get_current_user_id());
506 - $admin->verifySpacePermission('can_remove_member', $space);
507 -
508 689 $pivot = SpaceUserPivot::bySpace($space->id)
509 690 ->byUser($userId)
510 691 ->first();
511 692
@@ -510,9 +691,9 @@
510 691 ->first();
511 692
512 693 if (!$pivot) {
513 694 return $this->sendError([
514 - 'message' => 'Selected user is not a member of this community'
695 + 'message' => __('Selected user is not a member of this community', 'fluent-community')
515 696 ]);
516 697 }
517 698
518 699 $pivot->delete();
@@ -525,9 +706,9 @@
525 706
526 707 do_action('fluent_community/space/user_left', $space, $userId, 'by_admin');
527 708
528 709 return [
529 - 'message' => 'User has been removed from this community'
710 + 'message' => __('User has been removed from this community', 'fluent-community')
530 711 ];
531 712 }
532 713
533 714 public function getOtherUsers(Request $request)
@@ -540,25 +721,53 @@
540 721
541 722 $isMod = $currentUser->isCommunityModerator() && current_user_can('list_users');
542 723
543 724 $spaceId = $request->get('space_id');
725 + $space = Space::findOrFail($spaceId);
544 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 +
545 733 $selects = ['ID', 'display_name'];
546 -
547 734 if ($isMod) {
548 735 $selects[] = 'user_email';
549 736 }
550 737
551 - $users = User::whereDoesntHave('spaces', function ($q) use ($spaceId) {
552 - return $q->where('space_id', $spaceId);
553 - })
554 - ->select($selects)
555 - ->searchBy($request->get('search'))
556 - ->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'));
557 744
558 - 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 = [
559 767 'users' => $users
560 768 ];
769 + return apply_filters('fluent_community/space_non_members_api_response', $data, $request->all());
561 770 }
562 771
563 772 public function updateLinks(Request $request, $slug)
564 773 {
@@ -565,14 +774,12 @@
565 774 $space = Space::where('slug', $slug)->first();
566 775
567 776 if (!$space) {
568 777 return $this->sendError([
569 - 'message' => 'Space not found'
778 + 'message' => __('Space not found', 'fluent-community')
570 779 ]);
571 780 }
572 781
573 - $space->verifyUserPermisson($this->getUser(), 'community_admin');
574 -
575 782 $links = $request->get('links', []);
576 783
577 784 $links = array_map(function ($link) {
578 785 return CustomSanitizer::santizeLinkItem($link);
@@ -583,46 +790,117 @@
583 790 $space->settings = $settings;
584 791 $space->save();
585 792
586 793 return [
587 - 'message' => __('Links has been updated for the space', 'fluent-community'),
794 + 'message' => __('Links have been updated for the space', 'fluent-community'),
588 795 'links' => $links
589 796 ];
590 797 }
591 798
592 - public function getSpaceGroups(Request $request)
799 + public function getPrimaryMenu(Request $request, $slug)
593 800 {
594 - $user = $this->getUser(true);
595 - if (!$user->isCommunityModerator()) {
801 + $space = Space::where('slug', $slug)->first();
802 +
803 + if (!$space) {
596 804 return $this->sendError([
597 - 'message' => 'You are not allowed to create space group'
805 + 'message' => __('Space not found', 'fluent-community'),
598 806 ]);
599 807 }
600 808
809 + // Lighter than formatSpaceData(), but listeners like fcom-chat need membership too.
601 810 $user = $this->getUser();
811 + $space->permissions = $space->getUserPermissions($user);
812 + $space->membership = $space->getMembership($user ? $user->ID : null);
602 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 +
603 866 $groups = Helper::getAllCommunityGroups($user, false);
604 867
605 868 foreach ($groups as $group) {
606 869 foreach ($group->spaces as $space) {
607 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 + }
608 876 }
609 877 }
610 878
611 - return [
612 - '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
613 897 ];
898 + return apply_filters('fluent_community/space_groups_api_response', $data, $request->all());
614 899 }
615 900
616 901 public function createSpaceGroup(Request $request)
617 902 {
618 - $user = $this->getUser(true);
619 - if (!$user->isCommunityModerator()) {
620 - return $this->sendError([
621 - 'message' => 'You are not allowed to create space group'
622 - ]);
623 - }
624 -
625 903 $data = $request->all();
626 904
627 905 $this->validate($data, [
628 906 'title' => 'required|unique:fcom_spaces,title',
@@ -628,13 +906,28 @@
628 906 'title' => 'required|unique:fcom_spaces,title',
629 907 'slug' => 'required|unique:fcom_spaces,slug'
630 908 ]);
631 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', ''));
632 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 +
633 926 $formattedData = [
634 - 'title' => sanitize_text_field($data['title']),
635 - 'slug' => sanitize_title($data['slug']),
636 - 'description' => sanitize_textarea_field($data['description']),
927 + 'title' => $title,
928 + 'slug' => $slug,
929 + 'description' => $desc,
637 930 'status' => 'active',
638 931 'type' => 'space_group',
639 932 'settings' => [
640 933 'always_show_spaces' => Arr::get($data, 'settings.always_show_spaces', 'yes'),
@@ -651,15 +944,8 @@
651 944 }
652 945
653 946 public function updateSpaceGroup(Request $request, $groupId)
654 947 {
655 - $user = $this->getUser();
656 - if (!$user || !$user->isCommunityModerator()) {
657 - return $this->sendError([
658 - 'message' => 'You are not allowed to create space group'
659 - ]);
660 - }
661 -
662 948 $group = SpaceGroup::findOrFail($groupId);
663 949 $data = $request->all();
664 950
665 951 $this->validate($data, [
@@ -671,9 +957,9 @@
671 957 ->first();
672 958
673 959 if ($taken) {
674 960 return $this->sendError([
675 - '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')
676 962 ]);
677 963 }
678 964
679 965 $formattedData = [
@@ -688,9 +974,9 @@
688 974
689 975 $group->fill($formattedData)->save();
690 976
691 977 return [
692 - 'message' => __('Space group has been created updated', 'fluent-community'),
978 + 'message' => __('Space group has been updated', 'fluent-community'),
693 979 'group' => $group
694 980 ];
695 981 }
696 982
@@ -695,21 +981,13 @@
695 981 }
696 982
697 983 public function deleteSpaceGroup(Request $request, $groupId)
698 984 {
699 -
700 - $user = $this->getUser();
701 - if (!$user || !$user->isCommunityModerator()) {
702 - return $this->sendError([
703 - 'message' => 'You are not allowed to create space group'
704 - ]);
705 - }
706 -
707 985 $group = SpaceGroup::findOrFail($groupId);
708 986
709 987 if (!$group->spaces->isEmpty()) {
710 988 return $this->sendError([
711 - 'message' => 'You can not delete this group. It has spaces'
989 + 'message' => __('You cannot delete this group. It has spaces', 'fluent-community')
712 990 ]);
713 991 }
714 992
715 993 $group->delete();
@@ -730,9 +1008,9 @@
730 1008 ]);
731 1009 }
732 1010
733 1011 return [
734 - 'message' => __('Space group indexes has been updated', 'fluent-community')
1012 + 'message' => __('Space group indexes have been updated.', 'fluent-community')
735 1013 ];
736 1014
737 1015 }
738 1016
@@ -747,9 +1025,9 @@
747 1025 ]);
748 1026 }
749 1027
750 1028 return [
751 - 'message' => __('Space indexes has been updated', 'fluent-community')
1029 + 'message' => __('Space indexes have been updated.', 'fluent-community')
752 1030 ];
753 1031 }
754 1032
755 1033 public function moveSpace(Request $request)
@@ -760,9 +1038,9 @@
760 1038 $space = BaseSpace::withoutGlobalScopes()->findOrFail($spaceId);
761 1039 $group = SpaceGroup::findOrFail($groupId);
762 1040
763 1041 $space->update([
764 - 'parent_id' => $groupId
1042 + 'parent_id' => $group->id
765 1043 ]);
766 1044
767 1045 return [
768 1046 'message' => __('Space has been moved successfully', 'fluent-community')
@@ -770,28 +1048,58 @@
770 1048 }
771 1049
772 1050 public function getLockScreenSettings(Request $request, $spaceSlug)
773 1051 {
774 - $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 +
775 1063 $lockscreen = $space->getLockscreen();
776 1064
1065 + $lockscreen = apply_filters('fluent_community/get_lockscreen_settings', $lockscreen, $space);
1066 +
777 1067 return [
778 1068 'lockscreen' => $lockscreen
779 1069 ];
780 1070 }
781 1071
782 - public function updateLockscreenSettings(Request $request, $spaceSlug)
1072 + public function getMetaSettings(Request $request, $spaceSlug)
783 1073 {
784 1074 $space = Space::where('slug', $spaceSlug)->firstOrFail();
1075 + $metaSettings = apply_filters('fluent_community/space/meta_fields', [], $space);
785 1076
786 - $settingFields = $request->get('lockscreen', []);
1077 + if (!$metaSettings) {
1078 + return [
1079 + 'meta_settings' => null
1080 + ];
1081 + }
787 1082
788 - $formattedFields = LockscreenService::formatLockscreenFields($settingFields);
1083 + return [
1084 + 'meta_settings' => $metaSettings
1085 + ];
1086 + }
789 1087
790 - $space->setLockscreen($formattedFields);
1088 + private function getActiveMemberCounts($spaceIds)
1089 + {
1090 + if (!$spaceIds) {
1091 + return [];
1092 + }
791 1093
792 - return [
793 - 'message' => 'Lockscreen settings has been updated successfully.',
794 - 'community' => $space
795 - ];
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();
796 1104 }
797 1105 }