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()); } }