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 | Modules/Course/Http/Controllers/CourseAdminController.php +108 -18 2.7.52.11.0 View file →
@@ -85,9 +85,9 @@
85 85 $courseData = [
86 86 'parent_id' => $request->get('parent_id') ?: null, // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
87 87 'title' => $request->getSafe('title', 'sanitize_text_field'),
88 88 'privacy' => $request->get('privacy'),
89 - 'description' => wp_kses_post($request->get('description')),
89 + 'description' => wp_kses_post((string) $request->get('description', '')),
90 90 'status' => $request->get('status', 'draft'),
91 91 'settings' => [
92 92 'course_type' => $request->get('course_type'),
93 93 'emoji' => CustomSanitizer::sanitizeEmoji($request->get('settings.emoji', '')),
@@ -94,9 +94,9 @@
94 94 'shape_svg' => CustomSanitizer::sanitizeSvg($request->get('settings.shape_svg', '')),
95 95 'disable_comments' => $request->get('settings.disable_comments') === 'yes' ? 'yes' : 'no',
96 96 'hide_members_count' => $request->get('settings.hide_members_count') === 'yes' ? 'yes' : 'no',
97 97 'course_layout' => $request->get('settings.course_layout') === 'modern' ? 'modern' : 'classic',
98 - 'course_details' => CustomSanitizer::unslashMarkdown(trim($request->get('settings.course_details'))),
98 + 'course_details' => CustomSanitizer::unslashMarkdown(trim((string) $request->get('settings.course_details', ''))),
99 99 'hide_instructor_view' => $request->get('settings.hide_instructor_view') === 'yes' ? 'yes' : 'no',
100 100 'show_instructor_students_count' => $request->get('settings.show_instructor_students_count') === 'yes' ? 'yes' : 'no',
101 101 'sequential_lesson_order' => $request->get('settings.sequential_lesson_order') === 'yes' ? 'yes' : 'no',
102 102 ],
@@ -229,9 +229,9 @@
229 229
230 230 $courseData = [
231 231 'title' => $request->getSafe('title', 'sanitize_text_field'),
232 232 'privacy' => $request->get('privacy'),
233 - 'description' => wp_kses_post($request->get('description')),
233 + 'description' => wp_kses_post((string) $request->get('description', '')),
234 234 'status' => $request->get('status'),
235 235 'cover_photo' => $request->getSafe('cover_photo', 'sanitize_url'),
236 236 'parent_id' => $request->get('parent_id') ?: null, // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
237 237 ];
@@ -303,9 +303,9 @@
303 303 $existingSettings['show_instructor_students_count'] = $request->get('settings.show_instructor_students_count') === 'yes' ? 'yes' : 'no';
304 304 $existingSettings['show_paywalls'] = $request->get('settings.show_paywalls') === 'yes' ? 'yes' : 'no';
305 305 $existingSettings['show_welcome_banner'] = $request->get('settings.show_welcome_banner') === 'yes' ? 'yes' : 'no';
306 306 $existingSettings['course_layout'] = $request->get('settings.course_layout') === 'modern' ? 'modern' : 'classic';
307 - $existingSettings['course_details'] = CustomSanitizer::unslashMarkdown(trim($request->get('settings.course_details')));
307 + $existingSettings['course_details'] = CustomSanitizer::unslashMarkdown(trim((string) $request->get('settings.course_details', '')));
308 308 $existingSettings['sequential_lesson_order'] = $request->get('settings.sequential_lesson_order') === 'yes' ? 'yes' : 'no';
309 309
310 310 if ($request->get('privacy') == 'public' && $existingSettings['course_type'] == 'self_paced') {
311 311 $existingSettings['public_lesson_view'] = $request->get('settings.public_lesson_view') == 'yes' ? 'yes' : 'no';
@@ -383,8 +383,10 @@
383 383 $newTopic = $topic->replicate();
384 384 $newTopic->space_id = $newCourse->id;
385 385 $newTopic->save();
386 386
387 + do_action('fluent_community/section/created', $newTopic, $newCourse);
388 +
387 389 foreach ($topic->lessons as $lesson) {
388 390 /** @var CourseLesson $lesson */
389 391 $newLesson = $lesson->replicate();
390 392 $newLesson->space_id = $newCourse->id;
@@ -390,8 +392,10 @@
390 392 $newLesson->space_id = $newCourse->id;
391 393 $newLesson->parent_id = $newTopic->id;
392 394 $newLesson->save();
393 395 CourseHelper::copyLessonDocuments($lesson, $newLesson);
396 +
397 + do_action('fluent_community/lesson/created', $newLesson, $newTopic);
394 398 }
395 399 }
396 400
397 401 do_action('fluent_community/course/created', $newCourse);
@@ -487,13 +491,27 @@
487 491 Course::findOrFail($courseId);
488 492
489 493 $search = $request->getSafe('search');
490 494
491 - $students = XProfile::whereHas('space_pivot', function ($q) use ($courseId) {
492 - return $q->where('space_id', $courseId)
493 - ->where('role', 'student');
494 - })
495 - ->searchBy($search)
495 + $defaultDirections = [
496 + 'display_name' => 'ASC',
497 + 'created_at' => 'DESC',
498 + 'last_activity' => 'DESC',
499 + ];
500 +
501 + $sortBy = $request->getSafe('sort_by', 'sanitize_text_field', 'created_at');
502 + $sortColumn = in_array($sortBy, array_keys($defaultDirections), true) ? $sortBy : 'created_at';
503 + $sortDir = strtoupper($request->getSafe('sort_dir', 'sanitize_text_field', ''));
504 + $sortDirection = in_array($sortDir, ['ASC', 'DESC'], true) ? $sortDir : $defaultDirections[$sortColumn];
505 + $orderColumn = $sortColumn === 'created_at'
506 + ? 'fcom_space_user.created_at'
507 + : 'fcom_xprofile.' . $sortColumn;
508 +
509 + $publicFields = array_map(function ($field) {
510 + return 'fcom_xprofile.' . $field;
511 + }, ProfileHelper::getXProfilePublicFields());
512 +
513 + $students = XProfile::searchBy($search)
496 514 ->whereHas('user')
497 515 ->with([
498 516 'space_pivot' => function ($q) use ($courseId) {
499 517 return $q->where('space_id', $courseId);
@@ -498,9 +516,15 @@
498 516 'space_pivot' => function ($q) use ($courseId) {
499 517 return $q->where('space_id', $courseId);
500 518 },
501 519 ])
502 - ->select(ProfileHelper::getXProfilePublicFields())
520 + ->join('fcom_space_user', function ($join) use ($courseId) {
521 + $join->on('fcom_space_user.user_id', '=', 'fcom_xprofile.user_id')
522 + ->where('fcom_space_user.space_id', $courseId)
523 + ->where('fcom_space_user.role', 'student');
524 + })
525 + ->select($publicFields)
526 + ->orderBy($orderColumn, $sortDirection)
503 527 ->paginate();
504 528
505 529 $studentUserIds = $students->pluck('user_id')->toArray();
506 530
@@ -583,9 +607,13 @@
583 607 'message' => __('This student is not enrolled in this course.', 'fluent-community'),
584 608 ]);
585 609 }
586 610
587 - CourseHelper::resetCourseProgress($courseId, (int) $studentId);
611 + if (!CourseHelper::resetCourseProgress($courseId, (int) $studentId)) {
612 + return $this->sendError([
613 + 'message' => __("Student's progress could not be reset. Please try again.", 'fluent-community'),
614 + ]);
615 + }
588 616
589 617 return [
590 618 'message' => __("Student's progress has been reset.", 'fluent-community'),
591 619 ];
@@ -691,14 +719,13 @@
691 719 $lessonId = $request->getSafe('lesson_id', 'intval');
692 720 $sectionId = $request->getSafe('section_id', 'intval');
693 721
694 722 Course::findOrFail($courseId);
695 - CourseTopic::findOrFail($sectionId);
723 + $section = CourseTopic::where('space_id', $courseId)->findOrFail($sectionId);
724 + $lesson = CourseLesson::where('space_id', $courseId)->findOrFail($lessonId);
696 725
697 - $lesson = CourseLesson::findOrFail($lessonId);
698 -
699 726 $lesson->update([
700 - 'parent_id' => $sectionId,
727 + 'parent_id' => $section->id,
701 728 ]);
702 729
703 730 return [
704 731 'message' => __('Lesson has been moved successfully', 'fluent-community'),
@@ -716,9 +743,9 @@
716 743 'space_id' => $courseId,
717 744 'status' => 'published',
718 745 ];
719 746
720 - Course::findOrFail($courseId);
747 + $course = Course::findOrFail($courseId);
721 748
722 749 $latestPriority = CourseTopic::where('type', 'course_section')->where('space_id', $courseId)->max('priority');
723 750
724 751 $sectionData['priority'] = (int) $latestPriority + 1;
@@ -726,8 +753,10 @@
726 753 $section = CourseTopic::create($sectionData);
727 754
728 755 $section->load('lessons');
729 756
757 + do_action('fluent_community/section/created', $section, $course);
758 +
730 759 return [
731 760 'message' => __('Section has been created successfully.', 'fluent-community'),
732 761 'section' => $section,
733 762 ];
@@ -775,10 +804,21 @@
775 804 } elseif ($course->getCourseType() == 'structured') {
776 805 $acceptedFields[] = 'reactions_count';
777 806 }
778 807
808 + // Request::only() does not sanitize. Section titles currently render as escaped
809 + // text, so this is hygiene rather than a live sink, but keep the stored value
810 + // clean so a future v-html render cannot turn it into one.
779 811 $topicData = $request->only($acceptedFields);
780 812
813 + if (isset($topicData['title'])) {
814 + $topicData['title'] = sanitize_text_field($topicData['title']);
815 + }
816 +
817 + if (isset($topicData['status'])) {
818 + $topicData['status'] = sanitize_text_field($topicData['status']);
819 + }
820 +
781 821 if (!empty($topicData['scheduled_at'])) {
782 822 $topic->reactions_count = 0;
783 823 } elseif (isset($topicData['reactions_count'])) {
784 824 $topic->scheduled_at = null;
@@ -838,8 +878,10 @@
838 878 $newSection->space_id = $toCourse->id;
839 879 $newSection->priority = (int) $latestPriority + 1;
840 880 $newSection->save();
841 881
882 + do_action('fluent_community/section/created', $newSection, $toCourse);
883 +
842 884 $originalLessons = CourseLesson::where('parent_id', $originalSection->id)->get();
843 885 foreach ($originalLessons as $lesson) {
844 886 /** @var CourseLesson $lesson */
845 887 $newLesson = $lesson->replicate();
@@ -846,8 +888,10 @@
846 888 $newLesson->space_id = $toCourse->id;
847 889 $newLesson->parent_id = $newSection->id;
848 890 $newLesson->save();
849 891 CourseHelper::copyLessonDocuments($lesson, $newLesson);
892 +
893 + do_action('fluent_community/lesson/created', $newLesson, $newSection);
850 894 }
851 895
852 896 $newSection->load('lessons');
853 897
@@ -921,8 +965,11 @@
921 965
922 966 return apply_filters('fluent_community/admin_course_lesson_api_response', $data, $request->all());
923 967 }
924 968
969 + /**
970 + * Expects `title` and `section_id` at the top level of the request.
971 + */
925 972 public function createLesson(Request $request, $courseId)
926 973 {
927 974 $this->validate($request->all(), [
928 975 'title' => 'required',
@@ -956,8 +1003,10 @@
956 1003 $lesson = CourseLesson::create($lessonData);
957 1004
958 1005 $lesson = CourseLesson::findOrFail($lesson->id);
959 1006
1007 + do_action('fluent_community/lesson/created', $lesson, $topic);
1008 +
960 1009 return [
961 1010 'message' => __('Lesson has been created successfully.', 'fluent-community'),
962 1011 'lesson' => $lesson,
963 1012 ];
@@ -962,13 +1011,16 @@
962 1011 'lesson' => $lesson,
963 1012 ];
964 1013 }
965 1014
1015 + /**
1016 + * Expects the fields nested under `lesson`, referencing the section as `parent_id`.
1017 + */
966 1018 public function updateLesson(Request $request, $courseId, $lessionId)
967 1019 {
968 1020 Course::findOrFail($courseId);
969 1021
970 - $lessonData = $request->get('lesson');
1022 + $lessonData = (array)$request->get('lesson');
971 1023
972 1024 $this->validate($lessonData, [
973 1025 'title' => 'required',
974 1026 'parent_id' => 'required',
@@ -1032,8 +1084,12 @@
1032 1084 if ($dirtyFields) {
1033 1085 $lesson->save();
1034 1086 $isNewlyPublished = $lesson->status === 'published' && $previousStatus !== 'published';
1035 1087 do_action('fluent_community/lesson/updated', $lesson, $dirtyFields, $isNewlyPublished);
1088 +
1089 + if ($isNewlyPublished) {
1090 + do_action('fluent_community/lesson/published', $lesson);
1091 + }
1036 1092 }
1037 1093
1038 1094 do_action('fluent_community/lesson/additional_media_updated', $request->all(), $lesson, $updateData);
1039 1095
@@ -1050,15 +1106,39 @@
1050 1106 })
1051 1107 ->where('id', $lessionId)
1052 1108 ->firstOrFail();
1053 1109
1110 + $previousStatus = $lesson->status;
1111 +
1054 1112 $acceptedFields = [ 'title', 'status', 'slug' ];
1055 1113
1056 1114 // empty title/slug/status must not overwrite, but a literal "0" is a valid value
1115 + // Request::only() is a plain array pick with no sanitization, so each field is
1116 + // sanitized here the same way createLesson/updateLesson do it. The title is
1117 + // rendered with v-html in the course views, so it must not carry markup.
1057 1118 $lessonData = array_filter($request->only($acceptedFields), function ($value) {
1058 1119 return $value !== null && $value !== '';
1059 1120 });
1060 1121
1122 + if (isset($lessonData['title'])) {
1123 + $lessonData['title'] = sanitize_text_field($lessonData['title']);
1124 + }
1125 +
1126 + if (isset($lessonData['slug'])) {
1127 + // sanitize_title() alone let an author set a slug already taken by a
1128 + // sibling lesson, which getLessonBySlug() then resolves arbitrarily.
1129 + $lessonData['slug'] = CourseLesson::uniqueSlug(
1130 + $lessonData['slug'],
1131 + $lesson->space_id,
1132 + $lesson->id,
1133 + Arr::get($lessonData, 'title', $lesson->title)
1134 + );
1135 + }
1136 +
1137 + if (isset($lessonData['status']) && !in_array($lessonData['status'], ['draft', 'published', 'archived'], true)) {
1138 + unset($lessonData['status']);
1139 + }
1140 +
1061 1141 if (Arr::get($lessonData, 'status') === 'published' && $lesson->status !== 'published') {
1062 1142 if (empty($lesson->scheduled_at)) {
1063 1143 $lessonData['scheduled_at'] = current_time('mysql');
1064 1144 }
@@ -1070,10 +1150,19 @@
1070 1150 }
1071 1151
1072 1152 if (!empty($lessonData)) {
1073 1153 $lesson->fill($lessonData);
1074 - if ($lesson->isDirty()) {
1154 + $dirtyFields = $lesson->getDirty();
1155 +
1156 + if ($dirtyFields) {
1075 1157 $lesson->save();
1158 +
1159 + $isNewlyPublished = $lesson->status === 'published' && $previousStatus !== 'published';
1160 + do_action('fluent_community/lesson/updated', $lesson, $dirtyFields, $isNewlyPublished);
1161 +
1162 + if ($isNewlyPublished) {
1163 + do_action('fluent_community/lesson/published', $lesson);
1164 + }
1076 1165 }
1077 1166 }
1078 1167
1079 1168 return [
@@ -1148,8 +1237,9 @@
1148 1237 $newLesson = CourseLesson::findOrFail($newLesson->id);
1149 1238
1150 1239 CourseHelper::copyLessonDocuments($lesson, $newLesson);
1151 1240
1241 + do_action('fluent_community/lesson/created', $newLesson, $newLesson->topic);
1152 1242 do_action('fluent_community/lesson/duplicated', $newLesson, $lesson);
1153 1243
1154 1244 return [
1155 1245 'message' => __('Lesson has been duplicated successfully.', 'fluent-community'),