# fluent-community/trunk/Modules/Course/Http/Controllers/CourseAdminController.php

FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS &amp; Online Courses, version trunk. 1,351 lines.

- Page: https://pluginprobe.com/plugins/fluent-community/trunk/code/Modules/Course/Http/Controllers/CourseAdminController.php
- Raw: https://pluginprobe.com/plugins/fluent-community/trunk/raw/Modules/Course/Http/Controllers/CourseAdminController.php
- Modified: 2026-09-14T14:31:46+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/fluent-community/trunk/code/Modules/Course/Http/Controllers/CourseAdminController.php#L10-L20`.

```php
<?php

namespace FluentCommunity\Modules\Course\Http\Controllers;

use FluentCommunity\App\App;
use FluentCommunity\App\Functions\Utility;
use FluentCommunity\App\Http\Controllers\Controller;
use FluentCommunity\App\Models\BaseSpace;
use FluentCommunity\App\Models\Comment;
use FluentCommunity\App\Models\Reaction;
use FluentCommunity\App\Models\User;
use FluentCommunity\App\Models\XProfile;
use FluentCommunity\App\Services\CustomSanitizer;
use FluentCommunity\App\Services\Helper;
use FluentCommunity\App\Services\LockscreenService;
use FluentCommunity\App\Services\ProfileHelper;
use FluentCommunity\App\Services\FeedsHelper;
use FluentCommunity\Framework\Http\Request\Request;
use FluentCommunity\Framework\Support\Arr;
use FluentCommunity\App\Models\SpaceUserPivot;
use FluentCommunity\Modules\Course\Model\Course;
use FluentCommunity\Modules\Course\Model\CourseLesson;
use FluentCommunity\Modules\Course\Model\CourseTopic;
use FluentCommunity\Modules\Course\Services\CourseHelper;

class CourseAdminController extends Controller
{
    public function getCourses(Request $request)
    {
        $user = $this->getUser();

        $status    = $request->getSafe('status');
        $sortBy    = $request->getSafe('sort_by', 'sanitize_text_field', 'latest');
        $topicSlug = $request->getSafe('topic_slug');

        $query = Course::searchBy($request->getSafe('search'))
            ->byAdminAccess($user->ID)
            ->byPostTopic($topicSlug);

        if ($status && in_array($status, [ 'published', 'draft' ])) {
            $query->where('status', $status);
        }

        if ($sortBy === 'alphabetical') {
            $query->orderBy('title', 'ASC');
        } else {
            $query->orderBy('created_at', 'DESC');
        }

        $courses = $query->with([ 'owner' ])->paginate();

        foreach ($courses as $course) {
            $course->students_count = $course->students()->count();
            if (!$course->cover_photo) {
                $course->cover_photo = FLUENT_COMMUNITY_PLUGIN_URL . 'assets/images/course-placeholder.jpg';
            }
            $course->sectionsCount = CourseTopic::where('space_id', $course->id)->count();
            $course->lessonsCount = CourseLesson::where('space_id', $course->id)->count();
        }

        $data = [
            'courses'          => $courses,
            'course_categories' => $request->get('with_categories') ? CourseHelper::getCourseCategories() : [],
        ];

        return apply_filters('fluent_community/admin_courses_api_response', $data, $request->all());
    }

    public function createCourse(Request $request)
    {
        $this->validate($request->all(), [
            'title'       => 'required',
            'description' => 'required',
            'privacy'     => 'required|in:public,private,secret',
            'course_type' => 'required|in:self_paced,structured,scheduled',
        ]);

        $parentId = $request->get('parent_id');
        if ($parentId) {
            $serial = BaseSpace::where('parent_id', $parentId)->max('serial') + 1;
        } else {
            $serial = BaseSpace::max('serial') + 1;
        }

        $courseData = [
            'parent_id'   => $request->get('parent_id') ?: null, // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
            'title'       => $request->getSafe('title', 'sanitize_text_field'),
            'privacy'     => $request->get('privacy'),
            'description' => wp_kses_post((string) $request->get('description', '')),
            'status'      => $request->get('status', 'draft'),
            'settings'    => [
                'course_type'                    => $request->get('course_type'),
                'emoji'                          => CustomSanitizer::sanitizeEmoji($request->get('settings.emoji', '')),
                'shape_svg'                      => CustomSanitizer::sanitizeSvg($request->get('settings.shape_svg', '')),
                'disable_comments'               => $request->get('settings.disable_comments') === 'yes' ? 'yes' : 'no',
                'hide_members_count'             => $request->get('settings.hide_members_count') === 'yes' ? 'yes' : 'no',
                'course_layout'                  => $request->get('settings.course_layout') === 'modern' ? 'modern' : 'classic',
                'course_details'                 => CustomSanitizer::unslashMarkdown(trim((string) $request->get('settings.course_details', ''))),
                'hide_instructor_view'           => $request->get('settings.hide_instructor_view') === 'yes' ? 'yes' : 'no',
                'show_instructor_students_count' => $request->get('settings.show_instructor_students_count') === 'yes' ? 'yes' : 'no',
                'sequential_lesson_order'        => $request->get('settings.sequential_lesson_order') === 'yes' ? 'yes' : 'no',
            ],
            'serial'      => $serial,
        ];

        $lockScreenType = $request->get('settings.custom_lock_screen');
        if (!in_array($lockScreenType, [ 'yes', 'no', 'redirect' ]) || $request->get('privacy') != 'private') {
            $lockScreenType = 'no';
        }

        $courseData['settings']['custom_lock_screen'] = $lockScreenType;

        if ($lockScreenType === 'redirect') {
            $redirectUrl = $request->get('settings.onboard_redirect_url');
            if (!$redirectUrl || !filter_var($redirectUrl, FILTER_VALIDATE_URL)) {
                return $this->sendError([
                    'message' => __('Course Redirect URL is not valid', 'fluent-community'),
                ]);
            }
            $courseData['settings']['onboard_redirect_url'] = sanitize_url($redirectUrl);
        }

        if ($request->get('privacy') == 'public' && $request->get('course_type') == 'self_paced') {
            $courseData['settings']['public_lesson_view'] = $request->get('settings.public_lesson_view') == 'yes' ? 'yes' : 'no';
        }

        $slug = $request->get('slug');

        $slug = $slug ? $slug : $courseData['title'];

        $slug = preg_replace('/[^a-zA-Z0-9-_]/', '', $slug);

        $slug = sanitize_title($slug, '');

        if ($slug) {
            $slug = Utility::slugify($slug);
            $exist = Course::where('slug', $slug)
                ->exists();

            if ($exist) {
                $slug = $slug . '-' . time();
            }

            $courseData['slug'] = $slug;
        }

        do_action('fluent_community/course/before_create', $courseData);

        $course = Course::create($courseData);

        $imageTypes = [ 'cover_photo', 'logo' ];

        $metaData = [];
        foreach ($imageTypes as $type) {
            if (!empty($request->get($type))) {
                $media = Helper::getMediaFromUrl($request->get($type));
                if (!$media || $media->is_active) {
                    continue;
                }
                $metaData[$type] = $media->public_url;
                $media->update([
                    'is_active'     => true,
                    'user_id'       => get_current_user_id(),
                    'sub_object_id' => $course->id,
                    'object_source' => 'space_' . $type,
                ]);
            }
        }

        if ($metaData) {
            $course->fill($metaData);
            $course->save();
        }

        if ($request->get('category_ids', [])) {
            $topicsConfig = Helper::getTopicsConfig();
            $categoryIds = (array) $request->get('category_ids', []);
            $categoryIds = array_slice($categoryIds, 0, $topicsConfig['max_topics_per_space']);
            $course->syncCategories($categoryIds);
        }

        do_action('fluent_community/course/created', $course);

        return [
            'message' => __('Course has been created successfully', 'fluent-community'),
            'course' => $course,
        ];
    }

    public function findCourse(Request $request, $courseId)
    {
        /** @var Course $course */
        $course = Course::where('id', $courseId)
            ->with([ 'owner' ])
            ->firstOrFail();

        $course->students_count = $course->students()->count();
        $course->course_type = $course->settings['course_type'];
        $course->lockscreen = $course->getLockscreen();

        $course->category_ids = $course->categories->pluck('id')->toArray();

        if ($course->students_count) {
            $course->completed_students = $course->getCompletedStrundesCount();
            $course->overAllProgress = CourseHelper::overallCourseProgressAverage($course);
        }

        unset($course->categories);

        $course = apply_filters('fluent_community/course_info', $course, $request->all());

        return [
            'course' => $course,
        ];
    }

    public function updateCourse(Request $request, $courseId)
    {
        $this->validate($request->all(), [
            'title'       => 'required',
            'description' => 'required',
            'privacy'     => 'required|in:public,private,secret',
            'status'      => 'required|in:draft,published,archived',
            'course_type' => 'required|in:self_paced,structured,scheduled',
            'created_by'  => 'exists:users,ID',
        ]);

        $course = Course::findOrFail($courseId);

        $courseData = [
            'title'       => $request->getSafe('title', 'sanitize_text_field'),
            'privacy'     => $request->get('privacy'),
            'description' => wp_kses_post((string) $request->get('description', '')),
            'status'      => $request->get('status'),
            'cover_photo' => $request->getSafe('cover_photo', 'sanitize_url'),
            'parent_id'   => $request->get('parent_id') ?: null, // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
        ];

        $slug = $request->get('slug');
        if ($slug && $course->slug != $slug) {
            $slug = Utility::slugify($slug);

            $exist = App::getInstance('db')->table('fcom_spaces')->where('slug', $slug)
                ->where('id', '!=', $course->id)
                ->exists();

            if ($exist || !$slug) {
                return $this->sendError([
                    'message' => __('Slug is already taken. Please use a different slug', 'fluent-community'),
                ]);
            }

            $courseData['slug'] = $slug;
        }

        if ($request->get('created_by') && Helper::isSiteAdmin()) {
            $courseData['created_by'] = (int)$request->get('created_by');
        }

        $imageTypes = [ 'cover_photo', 'logo' ];

        foreach ($imageTypes as $type) {
            if (!empty($request->get($type))) {
                $media = Helper::getMediaFromUrl($request->get($type));
                if (!$media || $media->is_active) {
                    continue;
                }
                $courseData[$type] = $media->public_url;
                $media->update([
                    'is_active'     => true,
                    'user_id'       => get_current_user_id(),
                    'sub_object_id' => $course->id,
                    'object_source' => 'space_' . $type,
                ]);
            } else {
                $courseData[$type] = null;
            }
        }

        $existingSettings = $course->settings;
        $existingSettings['course_type'] = $request->get('course_type');

        $lockScreenType = $request->get('settings.custom_lock_screen');
        if (!in_array($lockScreenType, [ 'yes', 'no', 'redirect' ]) || $request->get('privacy') != 'private') {
            $lockScreenType = 'no';
        }
        if ($lockScreenType == 'redirect') {
            $redirectUrl = $request->get('settings.onboard_redirect_url');
            if (!$redirectUrl || !filter_var($redirectUrl, FILTER_VALIDATE_URL)) {
                return $this->sendError([
                    'message' => __('Course Redirect URL is not valid', 'fluent-community'),
                ]);
            }
            $existingSettings['onboard_redirect_url'] = sanitize_url($redirectUrl);
        }

        $existingSettings['custom_lock_screen'] = $lockScreenType;
        $existingSettings['emoji'] = CustomSanitizer::sanitizeEmoji($request->get('settings.emoji', ''));
        $existingSettings['shape_svg'] = CustomSanitizer::sanitizeSvg($request->get('settings.shape_svg', ''));
        $existingSettings['disable_comments'] = $request->get('settings.disable_comments') === 'yes' ? 'yes' : 'no';
        $existingSettings['hide_members_count'] = $request->get('settings.hide_members_count') === 'yes' ? 'yes' : 'no';
        $existingSettings['hide_instructor_view'] = $request->get('settings.hide_instructor_view') === 'yes' ? 'yes' : 'no';
        $existingSettings['show_instructor_students_count'] = $request->get('settings.show_instructor_students_count') === 'yes' ? 'yes' : 'no';
        $existingSettings['show_paywalls'] = $request->get('settings.show_paywalls') === 'yes' ? 'yes' : 'no';
        $existingSettings['show_welcome_banner'] = $request->get('settings.show_welcome_banner') === 'yes' ? 'yes' : 'no';
        $existingSettings['course_layout'] = $request->get('settings.course_layout') === 'modern' ? 'modern' : 'classic';
        $existingSettings['course_details'] = CustomSanitizer::unslashMarkdown(trim((string) $request->get('settings.course_details', '')));
        $existingSettings['sequential_lesson_order'] = $request->get('settings.sequential_lesson_order') === 'yes' ? 'yes' : 'no';

        if ($request->get('privacy') == 'public' && $existingSettings['course_type'] == 'self_paced') {
            $existingSettings['public_lesson_view'] = $request->get('settings.public_lesson_view') == 'yes' ? 'yes' : 'no';
        } else {
            unset($existingSettings['public_lesson_view']);
        }

        $courseData['settings'] = $existingSettings;

        $previousStatus = $course->status;

        $prevCourse = clone $course;

        $course->fill($courseData);
        $dirtyFields = $course->getDirty();

        if ($dirtyFields) {
            $course->save();
            do_action('fluent_community/course/updated', $course, $dirtyFields, $prevCourse);
            if ($previousStatus != 'published' && $course->status == 'published') {
                do_action('fluent_community/course/published', $course);
            }
        }

        if ($request->get('category_ids', [])) {
            $topicsConfig = Helper::getTopicsConfig();
            $categoryIds = (array) $request->get('category_ids', []);
            $categoryIds = array_slice($categoryIds, 0, $topicsConfig['max_topics_per_space']);
            $course->syncCategories($categoryIds);
        }

        $metaSettings = $request->get('meta_settings', []);
        if($metaSettings) {
            foreach ($metaSettings as $metaProvider => $metaData) {
                do_action('fluent_community/course/update_meta_settings_'.$metaProvider, $metaData, $course);
            }
        }

        return [
            'message' => __('Course has been updated successfully.', 'fluent-community'),
            'course'  => $course,
        ];
    }

    public function duplicateCourse(Request $request, $courseId)
    {
        $original = Course::findOrFail($courseId);

        $courseData = $original->toArray();
        $courseData['title'] = $original->title . ' (Copy)';
        $courseData['slug'] = Utility::slugify($original->slug . '-' . time());
        $courseData['status'] = 'draft';
        $courseData['created_by'] = get_current_user_id();

        do_action('fluent_community/course/before_create', $courseData);

        /** @var Course $newCourse */
        $newCourse = $original->replicate();
        $newCourse->title = $courseData['title'];
        $newCourse->slug = $courseData['slug'];
        $newCourse->status = $courseData['status'];
        $newCourse->created_by = $courseData['created_by'];
        $newCourse->save();

        if ($original->categories) {
            $newCourse->syncCategories($original->categories->pluck('id')->toArray());
        }

        $topics = CourseTopic::with('lessons')
            ->where('space_id', $original->id)
            ->get();

        foreach ($topics as $topic) {
            /** @var CourseTopic $topic */
            $newTopic = $topic->replicate();
            $newTopic->space_id = $newCourse->id;
            $newTopic->save();

            do_action('fluent_community/section/created', $newTopic, $newCourse);

            foreach ($topic->lessons as $lesson) {
                /** @var CourseLesson $lesson */
                $newLesson = $lesson->replicate();
                $newLesson->space_id = $newCourse->id;
                $newLesson->parent_id = $newTopic->id;
                $newLesson->save();
                CourseHelper::copyLessonDocuments($lesson, $newLesson);

                do_action('fluent_community/lesson/created', $newLesson, $newTopic);
            }
        }

        do_action('fluent_community/course/created', $newCourse);

        return [
            'message' => __('Course duplicated successfully.', 'fluent-community'),
            'course'  => $newCourse,
        ];
    }

    public function deleteCourse(Request $request, $courseId)
    {
        $course = Course::findOrFail($courseId);

        do_action('fluent_community/course/before_delete', $course);

        // Let's remove the reactions
        Reaction::query()->whereHas('feed', function ($q) use ($course) {
            $q->where('space_id', $course->id);
        })->delete();

        Comment::whereHas('post', function ($q) use ($course) {
            $q->where('space_id', $course->id);
        })->delete();

        $courseTopics = CourseTopic::with('lessons')
            ->where('space_id', $course->id)
            ->get();

        foreach ($courseTopics as $courseTopic) {
            do_action('fluent_community/section/before_deleted', $courseTopic);

            foreach ($courseTopic->lessons as $courseLesson) {
                do_action('fluent_community/lesson/before_deleted', $courseLesson);
                $courseLesson->delete();
            }

            $courseTopic->delete();
        }

        // Let's delete the student enrollments
        SpaceUserPivot::where('space_id', $course->id)
            ->delete();

        $courseId = $course->id;
        $course->delete();

        do_action('fluent_community/course/deleted', $courseId);

        return [
            'message' => __('Course has been deleted successfully along with all the associated data', 'fluent-community'),
        ];
    }

    public function getCourseComments(Request $request, $courseId)
    {
        $course = Course::findOrFail($courseId);

        $comments = Comment::byContentModerationAccessStatus($this->getUser(), $course)
            ->whereHas('post', function ($q) use ($courseId) {
                return $q->where('space_id', $courseId);
            })
            ->orderBy('id', 'DESC')
            ->with([
                'post'     => function ($q) {
                    return $q->select([ 'id', 'title', 'slug' ]);
                },
                'xprofile' => function ($q) {
                    $q->select(ProfileHelper::getXProfilePublicFields());
                },
            ])
            ->paginate();

        foreach ($comments as $comment) {
            if ($comment->user) {
                $comment->user->makeHidden([ 'user_email' ]);
            }
            $likedIds = FeedsHelper::getLikedIdsByUserFeedId($comment->post_id, get_current_user_id());
            if ($likedIds && in_array($comment->id, $likedIds)) {
                $comment->liked = 1;
            }
        }

        $data = [
            'comments' => $comments,
        ];

        return apply_filters('fluent_community/admin_course_comments_api_response', $data, $request->all());
    }

    public function getCourseStudents(Request $request, $courseId)
    {
        Course::findOrFail($courseId);

        $search = $request->getSafe('search');

        $defaultDirections = [
            'display_name'  => 'ASC',
            'created_at'    => 'DESC',
            'last_activity' => 'DESC',
        ];

        $sortBy = $request->getSafe('sort_by', 'sanitize_text_field', 'created_at');
        $sortColumn = in_array($sortBy, array_keys($defaultDirections), true) ? $sortBy : 'created_at';
        $sortDir = strtoupper($request->getSafe('sort_dir', 'sanitize_text_field', ''));
        $sortDirection = in_array($sortDir, ['ASC', 'DESC'], true) ? $sortDir : $defaultDirections[$sortColumn];
        $orderColumn = $sortColumn === 'created_at'
            ? 'fcom_space_user.created_at'
            : 'fcom_xprofile.' . $sortColumn;

        $publicFields = array_map(function ($field) {
            return 'fcom_xprofile.' . $field;
        }, ProfileHelper::getXProfilePublicFields());

        $students = XProfile::searchBy($search)
            ->whereHas('user')
            ->with([
                'space_pivot' => function ($q) use ($courseId) {
                    return $q->where('space_id', $courseId);
                },
            ])
            ->join('fcom_space_user', function ($join) use ($courseId) {
                $join->on('fcom_space_user.user_id', '=', 'fcom_xprofile.user_id')
                    ->where('fcom_space_user.space_id', $courseId)
                    ->where('fcom_space_user.role', 'student');
            })
            ->select($publicFields)
            ->orderBy($orderColumn, $sortDirection)
            ->paginate();

        $studentUserIds = $students->pluck('user_id')->toArray();

        $progressMap = CourseHelper::getBulkCourseProgress($courseId, $studentUserIds);

        foreach ($students as $student) {
            $student->progress = $progressMap[$student->user_id] ?? 0;
        }

        $data = [
            'students' => $students,
        ];
        
        return apply_filters('fluent_community/admin_course_students_api_response', $data, $request->all());
    }

    public function addStudent(Request $request, $courseId)
    {
        $course = Course::findOrFail($courseId);

        $this->validate($request->all(), [
            'user_id' => 'required|exists:users,ID',
        ]);

        $userId = (int)$request->get('user_id');
        $targetUser = User::findOrFail($userId);
        $xprofile = $targetUser->syncXProfile();

        if ($xprofile && $xprofile->status != 'active') {
            return $this->sendError([
                'message' => __('Selected user is not active', 'fluent-community'),
            ]);
        }

        $enrolled = CourseHelper::enrollCourse($course, $userId, 'by_admin');

        if (!$enrolled) {
            return $this->sendError([
                'message' => __('User is already added to this course.', 'fluent-community'),
            ]);
        }

        return [
            'message' => __('User has been added to this course', 'fluent-community'),
        ];
    }

    public function removeStudent(Request $request, $courseId, $studentId)
    {
        $course = Course::findOrFail($courseId);

        $student = SpaceUserPivot::bySpace($course->id)
            ->byUser($studentId)
            ->first();

        if (!$student) {
            return $this->sendError([
                'message' => __('Selected user is not a student of this course', 'fluent-community'),
            ]);
        }

        Helper::removeFromSpace($course, $studentId, 'by_admin');

        return [
            'message' => __('Student has been removed from this course', 'fluent-community'),
        ];
    }

    public function resetStudentProgress(Request $request, $courseId, $studentId)
    {
        Course::findOrFail($courseId);

        $pivot = SpaceUserPivot::where('space_id', $courseId)
            ->where('user_id', $studentId)
            ->where('role', 'student')
            ->first();

        if (!$pivot) {
            return $this->sendError([
                'message' => __('This student is not enrolled in this course.', 'fluent-community'),
            ]);
        }

        CourseHelper::resetCourseProgress($courseId, (int) $studentId);

        return [
            'message' => __("Student's progress has been reset.", 'fluent-community'),
        ];
    }

    public function getSections(Request $request, $courseId)
    {
        $course = Course::findOrFail($courseId);
        /** @var Course $course */

        $sectionsQuery = CourseTopic::where('space_id', $courseId)
            ->orderBy('priority', 'ASC')
            ->orderBy('id', 'ASC');

        if (in_array('only_published', $request->get('conditions', []))) {
            $sectionsQuery->where('status', 'published')
                ->with([
            'lessons' => function ($q) {
                    $q->where('status', 'published');
            },
            ]);
        }

        if (empty($request->get('conditions', []))) {
            $sectionsQuery->with([ 'lessons' ]);
        }

        $sections = $sectionsQuery->get();

        $data = [
            'sections' => $sections,
        ];

        if ($request->get('with_lock_screen')) {
            $data['lockscreen'] = LockscreenService::getLockscreenSettings($course);
        }

        return apply_filters('fluent_community/admin_course_sections_api_response', $data, $request->all());
    }

    public function getSection(Request $request, $courseId, $topicId)
    {
        $topic = CourseTopic::where('space_id', $courseId)
            ->whereHas('course', function ($query) use ($courseId) {
                $query->where('id', $courseId);
            })
            ->where('id', $topicId)
            ->with([ 'lessons' ])
            ->firstOrFail();

        $data = [
            'topic' => $topic,
        ];

        return apply_filters('fluent_community/admin_course_section_api_response', $data, $request->all());
    }

    public function resetSectionIndexes(Request $request, $courseId)
    {
        $indexes = $request->get('indexes', []);

        $sections = CourseTopic::where('space_id', $courseId)
            ->whereIn('id', array_keys($indexes))
            ->get();

        foreach ($sections as $section) {
            if (!isset($indexes[$section->id])) { continue;
            }
            $section->priority = $indexes[$section->id];
            $section->save();
        }

        return [
            'sections' => $sections,
            'message'  => __('Section indexes have been updated successfully.', 'fluent-community'),
        ];
    }

    public function resetLessonIndexes(Request $request, $courseId, $sectionId)
    {
        $indexes = $request->get('indexes', []);

        $lessons = CourseLesson::where('space_id', $courseId)
            ->where('parent_id', $sectionId)
            ->whereIn('id', array_keys($indexes))
            ->get();

        foreach ($lessons as $lesson) {
            if (!isset($indexes[$lesson->id])) { continue;
            }
            $lesson->priority = $indexes[$lesson->id];
            $lesson->save();
        }

        return [
            'lessons' => $lessons,
            'message' => __('Lesson indexes have been updated successfully.', 'fluent-community'),
        ];
    }

    public function moveLesson(Request $request, $courseId)
    {
        $lessonId = $request->getSafe('lesson_id', 'intval');
        $sectionId = $request->getSafe('section_id', 'intval');

        Course::findOrFail($courseId);
        $section = CourseTopic::where('space_id', $courseId)->findOrFail($sectionId);
        $lesson = CourseLesson::where('space_id', $courseId)->findOrFail($lessonId);

        $lesson->update([
            'parent_id' => $section->id,
        ]);

        return [
            'message' => __('Lesson has been moved successfully', 'fluent-community'),
        ];
    }

    public function createSection(Request $request, $courseId)
    {
        $this->validate($request->all(), [
            'title' => 'required',
        ]);

        $sectionData = [
            'title'    => $request->getSafe('title'),
            'space_id' => $courseId,
            'status'   => 'published',
        ];

        $course = Course::findOrFail($courseId);

        $latestPriority = CourseTopic::where('type', 'course_section')->where('space_id', $courseId)->max('priority');

        $sectionData['priority'] = (int) $latestPriority + 1;

        $section = CourseTopic::create($sectionData);

        $section->load('lessons');

        do_action('fluent_community/section/created', $section, $course);

        return [
            'message' => __('Section has been created successfully.', 'fluent-community'),
            'section' => $section,
        ];
    }

    public function updateSection(Request $request, $courseId, $tipicId)
    {
        $this->validate($request->all(), [
            'title'  => 'required',
            'status' => 'required|in:draft,published,archived',
        ]);

        Course::findOrFail($courseId);

        $topic = CourseTopic::where('space_id', $courseId)
            ->where('id', $tipicId)
            ->firstOrFail();


        $topicData = [
            'title'  => $request->getSafe('title'),
            'status' => $request->get('status'),
        ];

        $topic->update($topicData);

        return [
            'message' => __('Topic has been updated successfully.', 'fluent-community'),
            'topic'   => $topic,
        ];
    }

    public function patchSection(Request $request, $courseId, $tipicId)
    {
        $course = Course::findOrFail($courseId);

        $topic = CourseTopic::where('space_id', $courseId)
            ->where('id', $tipicId)
            ->firstOrFail();

        $acceptedFields = [ 'title', 'status' ];

        if ($course->getCourseType() == 'scheduled') {
            $acceptedFields[] = 'scheduled_at';
        } elseif ($course->getCourseType() == 'structured') {
            $acceptedFields[] = 'reactions_count';
        }

        // Request::only() does not sanitize. Section titles currently render as escaped
        // text, so this is hygiene rather than a live sink, but keep the stored value
        // clean so a future v-html render cannot turn it into one.
        $topicData = $request->only($acceptedFields);

        if (isset($topicData['title'])) {
            $topicData['title'] = sanitize_text_field($topicData['title']);
        }

        if (isset($topicData['status'])) {
            $topicData['status'] = sanitize_text_field($topicData['status']);
        }

        if (!empty($topicData['scheduled_at'])) {
            $topic->reactions_count = 0;
        } elseif (isset($topicData['reactions_count'])) {
            $topic->scheduled_at = null;
            $topic->reactions_count = $topicData['reactions_count'];
        }

        $topicData = apply_filters('fluent_community/section/update_data', $topicData, $course, $topic, $request->all());

        if (is_wp_error($topicData)) {
            return $this->sendError($topicData->get_error_messages());
        }

        $topic->fill($topicData);

        $scheduledAtDirty = $topic->isDirty('scheduled_at');
        $reactionsCountDirty = $topic->isDirty('reactions_count');

        $topic->save();

        if ($scheduledAtDirty) {
            do_action('fluent_community/section/scheduled_at_updated', $course, $topic);
        }
        if ($reactionsCountDirty) {
            do_action('fluent_community/section/reactions_count_updated', $course, $topic);
        }

        return [
            'message' => __('Topic has been updated successfully.', 'fluent-community'),
            'topic'   => $topic,
        ];
    }

    public function copySection(Request $request, $toCourseId)
    {
        $sectionId = $request->getSafe('section_id', 'intval');
        $fromCourseId = $request->getSafe('from_course_id', 'intval');

        $fromCourse = Course::findOrFail($fromCourseId);

        if (!$fromCourse->isCourseAdmin()) {
            return $this->sendError([
                'message' => __('You do not have permission to access this course', 'fluent-community'),
            ]);
        }

        /** @var CourseTopic $originalSection */
        $originalSection = CourseTopic::where('id', $sectionId)
            ->where('space_id', $fromCourseId)
            ->firstOrFail();

        $toCourse = Course::findOrFail($toCourseId);

        $latestPriority = CourseTopic::where('type', 'course_section')->where('space_id', $toCourse->id)->max('priority');

        /** @var CourseTopic $newSection */
        $newSection = $originalSection->replicate();
        $newSection->space_id = $toCourse->id;
        $newSection->priority = (int) $latestPriority + 1;
        $newSection->save();

        do_action('fluent_community/section/created', $newSection, $toCourse);

        $originalLessons = CourseLesson::where('parent_id', $originalSection->id)->get();
        foreach ($originalLessons as $lesson) {
            /** @var CourseLesson $lesson */
            $newLesson = $lesson->replicate();
            $newLesson->space_id = $toCourse->id;
            $newLesson->parent_id = $newSection->id;
            $newLesson->save();
            CourseHelper::copyLessonDocuments($lesson, $newLesson);

            do_action('fluent_community/lesson/created', $newLesson, $newSection);
        }

        $newSection->load('lessons');

        return [
            'message' => __('Section has been copied to the selected course', 'fluent-community'),
            'section' => $newSection,
        ];
    }

    public function deleteSection(Request $request, $courseId, $sectionId)
    {
        $topic = CourseTopic::where([
            'id'       => $sectionId,
            'space_id' => $courseId,
        ])->firstOrFail();

        do_action('fluent_community/section/before_deleted', $topic);

        $topic->delete();

        $lessons = CourseLesson::where([
            'parent_id' => $sectionId,
            'space_id'  => $courseId,
        ])->get();

        foreach ($lessons as $lesson) {
            do_action('fluent_community/lesson/before_deleted', $lesson);
            $lesson->delete();
        }

        return [
            'message' => __('Section has been deleted successfully.', 'fluent-community'),
        ];
    }

    public function getLessons(Request $request, $courseId)
    {
        Course::findOrFail($courseId);

        $lessons = CourseLesson::where('space_id', $courseId)
            ->orderBy('priority', 'ASC')
            ->orderBy('id', 'ASC');

        $topicId = (int)$request->get('topic_id');

        if ($topicId) {
            $lessons = $lessons->where('parent_id', $topicId);
        }

        $lessons = $lessons->get();

        $data = [
            'lessons' => $lessons,
        ];

        return apply_filters('fluent_community/admin_course_lessons_api_response', $data, $request->all());
    }

    public function getLesson(Request $request, $courseId, $lessonId)
    {
        $lesson = CourseLesson::whereHas('course', function ($query) use ($courseId) {
            $query->where('id', $courseId);
        })
            ->where('id', $lessonId)
            ->with([ 'topic', 'course' ])
            ->firstOrFail();

        $data = [
            'lesson' => $lesson,
        ];

        return apply_filters('fluent_community/admin_course_lesson_api_response', $data, $request->all());
    }

    /**
     * Expects `title` and `section_id` at the top level of the request.
     */
    public function createLesson(Request $request, $courseId)
    {
        $this->validate($request->all(), [
            'title'      => 'required',
            'section_id' => 'required',
        ]);

        $sectionId = (int)$request->get('section_id');

        $topic = CourseTopic::whereHas('course', function ($query) use ($courseId) {
            $query->where('id', $courseId);
        })
            ->where('id', $sectionId)
            ->firstOrFail();

        $lessonData = [
            'title'     => $request->getSafe('title'),
            'parent_id' => $topic->id,
            'space_id'  => $courseId,
            'status'    => 'draft',
        ];

        $latestPriority = CourseLesson::where('type', 'course_lesson')
            ->where('parent_id', $sectionId)
            ->where('space_id', $courseId)
            ->max('priority');
            
        $lessonData['priority'] = (int) $latestPriority + 1;

        $lessonData = apply_filters('fluent_community/lesson/create_data', $lessonData, $request);

        $lesson = CourseLesson::create($lessonData);

        $lesson = CourseLesson::findOrFail($lesson->id);

        do_action('fluent_community/lesson/created', $lesson, $topic);

        return [
            'message' => __('Lesson has been created successfully.', 'fluent-community'),
            'lesson'  => $lesson,
        ];
    }

    /**
     * Expects the fields nested under `lesson`, referencing the section as `parent_id`.
     */
    public function updateLesson(Request $request, $courseId, $lessionId)
    {
        Course::findOrFail($courseId);

        $lessonData = (array)$request->get('lesson');

        $this->validate($lessonData, [
            'title'     => 'required',
            'parent_id' => 'required',
            'status'    => 'required|in:draft,published,archived',
        ]);

        CourseTopic::whereHas('course', function ($query) use ($courseId) {
            $query->where('id', $courseId);
        })
            ->where('id', $lessonData['parent_id'])
            ->firstOrFail();

        /** @var CourseLesson $lesson */
        $lesson = CourseLesson::where('id', $lessionId)
            ->where('space_id', $courseId)
            ->firstOrFail();

        $previousStatus = $lesson->status;

        $updatedMeta = CourseHelper::sanitizeLessonMeta(Arr::get($lessonData, 'meta', []), $lesson);
        $updatedMeta['document_ids'] = Arr::get($lesson->meta, 'document_ids', []);

        if ($mediaId = Arr::get($updatedMeta, 'featured_image_id')) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
            if (!$lesson->isQuizType()) {
                $media = wp_get_attachment_image_url($mediaId);
                $lesson->featured_image = $media ? $media : null;
            } else {
                $media = Helper::getMediaFromUrl(sanitize_url($mediaId));
                if ($media && !$media->is_active) {
                    Helper::removeMediaByUrl($lesson->featured_image, $lesson->id);
                    $lesson->featured_image = $media->public_url;
                    $media->update([
                        'is_active'     => true,
                        'user_id'       => get_current_user_id(),
                        'sub_object_id' => $lesson->id,
                        'object_source' => 'quiz_thumbnail_' . $lesson->id,
                    ]);
                }
            }
        } else {
            Helper::removeMediaByUrl($lesson->featured_image, $lesson->id);
            $lesson->featured_image = null;
        }

        $updateData = array_filter([
            'title'  => sanitize_text_field(Arr::get($lessonData, 'title')),
            'status' => Arr::get($lessonData, 'status'),
            'meta'   => wp_parse_args($updatedMeta, $lesson->meta),
        ]);

        // message bypasses array_filter so an emptied lesson body still saves
        if (array_key_exists('message', $lessonData)) {
            $updateData['message'] = CourseHelper::santizeLessonBody((string) Arr::get($lessonData, 'message'));
        }

        $updateData = apply_filters('fluent_community/lesson/update_data', $updateData, $lesson);

        $lesson->fill($updateData);
        $dirtyFields = $lesson->getDirty();

        if ($dirtyFields) {
            $lesson->save();
            $isNewlyPublished = $lesson->status === 'published' && $previousStatus !== 'published';
            do_action('fluent_community/lesson/updated', $lesson, $dirtyFields, $isNewlyPublished);

            if ($isNewlyPublished) {
                do_action('fluent_community/lesson/published', $lesson);
            }
        }

        do_action('fluent_community/lesson/additional_media_updated', $request->all(), $lesson, $updateData);

        return [
            'message' => __('Lesson has been updated successfully.', 'fluent-community'),
            'lesson'  => $lesson,
        ];
    }

    public function patchLesson(Request $request, $courseId, $lessionId)
    {
        $lesson = CourseLesson::whereHas('course', function ($query) use ($courseId) {
            $query->where('id', $courseId);
        })
            ->where('id', $lessionId)
            ->firstOrFail();

        $previousStatus = $lesson->status;

        $acceptedFields = [ 'title', 'status', 'slug' ];

        // empty title/slug/status must not overwrite, but a literal "0" is a valid value
        // Request::only() is a plain array pick with no sanitization, so each field is
        // sanitized here the same way createLesson/updateLesson do it. The title is
        // rendered with v-html in the course views, so it must not carry markup.
        $lessonData = array_filter($request->only($acceptedFields), function ($value) {
            return $value !== null && $value !== '';
        });

        if (isset($lessonData['title'])) {
            $lessonData['title'] = sanitize_text_field($lessonData['title']);
        }

        if (isset($lessonData['slug'])) {
            // sanitize_title() alone let an author set a slug already taken by a
            // sibling lesson, which getLessonBySlug() then resolves arbitrarily.
            $lessonData['slug'] = CourseLesson::uniqueSlug(
                $lessonData['slug'],
                $lesson->space_id,
                $lesson->id,
                Arr::get($lessonData, 'title', $lesson->title)
            );
        }

        if (isset($lessonData['status']) && !in_array($lessonData['status'], ['draft', 'published', 'archived'], true)) {
            unset($lessonData['status']);
        }

        if (Arr::get($lessonData, 'status') === 'published' && $lesson->status !== 'published') {
            if (empty($lesson->scheduled_at)) {
                $lessonData['scheduled_at'] = current_time('mysql');
            }
        }

        // message bypasses the empty-value filter above so an emptied lesson body still saves
        if ($request->exists('message')) {
            $lessonData['message'] = CourseHelper::santizeLessonBody((string) $request->get('message'));
        }

        if (!empty($lessonData)) {
            $lesson->fill($lessonData);
            $dirtyFields = $lesson->getDirty();

            if ($dirtyFields) {
                $lesson->save();

                $isNewlyPublished = $lesson->status === 'published' && $previousStatus !== 'published';
                do_action('fluent_community/lesson/updated', $lesson, $dirtyFields, $isNewlyPublished);

                if ($isNewlyPublished) {
                    do_action('fluent_community/lesson/published', $lesson);
                }
            }
        }

        return [
            'message' => __('Lesson has been updated successfully.', 'fluent-community'),
            'lesson'  => $lesson,
        ];
    }

    public function deleteLesson(Request $request, $courseId, $lessionId)
    {
        $lesson = CourseLesson::whereHas('course', function ($query) use ($courseId) {
            $query->where('id', $courseId);
        })
            ->where('id', $lessionId)
            ->firstOrFail();

        do_action('fluent_community/lesson/before_deleted', $lesson);

        $lesson->delete();

        return [
            'message' => __('Lesson has been deleted successfully.', 'fluent-community'),
        ];
    }

    public function duplicateLesson(Request $request, $courseId, $lessonId)
    {
        Course::findOrFail($courseId);

        $lesson = CourseLesson::where('id', $lessonId)
            ->where('space_id', $courseId)
            ->firstOrFail();

        $siblings = CourseLesson::where('parent_id', $lesson->parent_id)
            ->where('space_id', $courseId)
            ->orderBy('priority', 'ASC')
            ->orderBy('id', 'ASC')
            ->get();

        $sourceIndex = $siblings->search(function ($sibling) use ($lesson) {
            return (int)$sibling->id === (int)$lesson->id;
        });

        if ($sourceIndex === false) {
            $sourceIndex = $siblings->count() - 1;
        }

        $existingTitles = $siblings->pluck('title')->toArray();
        $duplicateTitle = $lesson->title . ' (Copy)';
        if (in_array($duplicateTitle, $existingTitles, true)) {
            $counter = 2;
            while (in_array($lesson->title . ' (Copy ' . $counter . ')', $existingTitles, true)) {
                ++$counter;
            }
            $duplicateTitle = $lesson->title . ' (Copy ' . $counter . ')';
        }

        /** @var CourseLesson $newLesson */
        $newLesson = $lesson->replicate();
        $newLesson->title = $duplicateTitle;
        $newLesson->slug = null;
        $newLesson->priority = $sourceIndex + 1;
        $newLesson->save();

        $orderedIds = $siblings->pluck('id')->toArray();
        array_splice($orderedIds, $sourceIndex + 1, 0, [ $newLesson->id ]);

        foreach ($orderedIds as $index => $siblingId) {
            CourseLesson::where('id', $siblingId)->update([ 'priority' => $index ]);
        }

        $newLesson = CourseLesson::findOrFail($newLesson->id);

        CourseHelper::copyLessonDocuments($lesson, $newLesson);

        do_action('fluent_community/lesson/created', $newLesson, $newLesson->topic);
        do_action('fluent_community/lesson/duplicated', $newLesson, $lesson);

        return [
            'message' => __('Lesson has been duplicated successfully.', 'fluent-community'),
            'lesson'  => $newLesson,
        ];
    }

    public function getOtherUsers(Request $request, $courseId)
    {
        $selects = [
            'ID',
            'display_name',
        ];

        if (current_user_can('list_users')) {
            $selects[] = 'user_email';
        }

        $userQuery = User::select([ 'ID' ])
            ->whereDoesntHave('space_pivot', function ($q) use ($courseId) {
                $q->where('space_id', $courseId);
            })
            ->limit(100)
            ->searchBy($request->getSafe('search'));

        if (is_multisite()) {
            global $wpdb;
            $blogId = get_current_blog_id();
            $blogPrefix = $wpdb->get_blog_prefix($blogId);
            $userQuery->whereHas('usermeta', function ($q) use ($blogPrefix) {
                $q->where('meta_key', $blogPrefix . 'capabilities');
            });
        }

        $userIds = $userQuery->get()
            ->pluck('ID')
            ->toArray();

        $users = User::select($selects)
            ->whereIn('ID', $userIds)
            ->paginate(100);

        $data = [
            'users' => $users,
        ];

        return apply_filters('fluent_community/admin_course_non_members_api_response', $data, $request->all());
    }

    public function updateLinks(Request $request, $id)
    {
        $course = Course::findOrFail($id);
        $links = $request->get('links', []);

        $links = array_map(function ($link) {
            return CustomSanitizer::santizeLinkItem($link);
        }, $links);

        $settings = $course->settings;
        $settings['links'] = $links;
        $course->settings = $settings;
        $course->save();

        return [
            'message' => __('Links have been updated for the course', 'fluent-community'),
            'links'   => $links,
        ];
    }

    public function getMetaSettings(Request $request, $id)
    {
        $course = Course::findOrFail($id);
        $metaSettings = apply_filters('fluent_community/course/meta_fields', [], $course, $request->all());

        if (!$metaSettings) {
            return [
                'meta_settings' => null,
            ];
        }

        return [
            'meta_settings' => $metaSettings,
        ];
    }

    public function getOtherInstructors(Request $request, $courseId)
    {
        $search = $request->getSafe('search');

        Course::findOrFail($courseId);

        $selects = [
            'ID',
            'display_name',
        ];

        if (current_user_can('list_users')) {
            $selects[] = 'user_email';
        }

        $instructors = User::select($selects)
            ->limit(100)
            ->searchBy($search)
            ->get();

        $data = [
            'instructors' => $instructors,
        ];

        return apply_filters('fluent_community/admin_course_other_instructors_api_response', $data, $request->all());
    }
}

```
