PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.10.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.10.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 1.1.0 All 77 releases
← All changes | Modules/Course/Http/Controllers/CourseAdminController.php +103 -17 2.7.52.10.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
@@ -691,14 +715,13 @@
691 715 $lessonId = $request->getSafe('lesson_id', 'intval');
692 716 $sectionId = $request->getSafe('section_id', 'intval');
693 717
694 718 Course::findOrFail($courseId);
695 - CourseTopic::findOrFail($sectionId);
719 + $section = CourseTopic::where('space_id', $courseId)->findOrFail($sectionId);
720 + $lesson = CourseLesson::where('space_id', $courseId)->findOrFail($lessonId);
696 721
697 - $lesson = CourseLesson::findOrFail($lessonId);
698 -
699 722 $lesson->update([
700 - 'parent_id' => $sectionId,
723 + 'parent_id' => $section->id,
701 724 ]);
702 725
703 726 return [
704 727 'message' => __('Lesson has been moved successfully', 'fluent-community'),
@@ -716,9 +739,9 @@
716 739 'space_id' => $courseId,
717 740 'status' => 'published',
718 741 ];
719 742
720 - Course::findOrFail($courseId);
743 + $course = Course::findOrFail($courseId);
721 744
722 745 $latestPriority = CourseTopic::where('type', 'course_section')->where('space_id', $courseId)->max('priority');
723 746
724 747 $sectionData['priority'] = (int) $latestPriority + 1;
@@ -726,8 +749,10 @@
726 749 $section = CourseTopic::create($sectionData);
727 750
728 751 $section->load('lessons');
729 752
753 + do_action('fluent_community/section/created', $section, $course);
754 +
730 755 return [
731 756 'message' => __('Section has been created successfully.', 'fluent-community'),
732 757 'section' => $section,
733 758 ];
@@ -775,10 +800,21 @@
775 800 } elseif ($course->getCourseType() == 'structured') {
776 801 $acceptedFields[] = 'reactions_count';
777 802 }
778 803
804 + // Request::only() does not sanitize. Section titles currently render as escaped
805 + // text, so this is hygiene rather than a live sink, but keep the stored value
806 + // clean so a future v-html render cannot turn it into one.
779 807 $topicData = $request->only($acceptedFields);
780 808
809 + if (isset($topicData['title'])) {
810 + $topicData['title'] = sanitize_text_field($topicData['title']);
811 + }
812 +
813 + if (isset($topicData['status'])) {
814 + $topicData['status'] = sanitize_text_field($topicData['status']);
815 + }
816 +
781 817 if (!empty($topicData['scheduled_at'])) {
782 818 $topic->reactions_count = 0;
783 819 } elseif (isset($topicData['reactions_count'])) {
784 820 $topic->scheduled_at = null;
@@ -838,8 +874,10 @@
838 874 $newSection->space_id = $toCourse->id;
839 875 $newSection->priority = (int) $latestPriority + 1;
840 876 $newSection->save();
841 877
878 + do_action('fluent_community/section/created', $newSection, $toCourse);
879 +
842 880 $originalLessons = CourseLesson::where('parent_id', $originalSection->id)->get();
843 881 foreach ($originalLessons as $lesson) {
844 882 /** @var CourseLesson $lesson */
845 883 $newLesson = $lesson->replicate();
@@ -846,8 +884,10 @@
846 884 $newLesson->space_id = $toCourse->id;
847 885 $newLesson->parent_id = $newSection->id;
848 886 $newLesson->save();
849 887 CourseHelper::copyLessonDocuments($lesson, $newLesson);
888 +
889 + do_action('fluent_community/lesson/created', $newLesson, $newSection);
850 890 }
851 891
852 892 $newSection->load('lessons');
853 893
@@ -921,8 +961,11 @@
921 961
922 962 return apply_filters('fluent_community/admin_course_lesson_api_response', $data, $request->all());
923 963 }
924 964
965 + /**
966 + * Expects `title` and `section_id` at the top level of the request.
967 + */
925 968 public function createLesson(Request $request, $courseId)
926 969 {
927 970 $this->validate($request->all(), [
928 971 'title' => 'required',
@@ -956,8 +999,10 @@
956 999 $lesson = CourseLesson::create($lessonData);
957 1000
958 1001 $lesson = CourseLesson::findOrFail($lesson->id);
959 1002
1003 + do_action('fluent_community/lesson/created', $lesson, $topic);
1004 +
960 1005 return [
961 1006 'message' => __('Lesson has been created successfully.', 'fluent-community'),
962 1007 'lesson' => $lesson,
963 1008 ];
@@ -962,13 +1007,16 @@
962 1007 'lesson' => $lesson,
963 1008 ];
964 1009 }
965 1010
1011 + /**
1012 + * Expects the fields nested under `lesson`, referencing the section as `parent_id`.
1013 + */
966 1014 public function updateLesson(Request $request, $courseId, $lessionId)
967 1015 {
968 1016 Course::findOrFail($courseId);
969 1017
970 - $lessonData = $request->get('lesson');
1018 + $lessonData = (array)$request->get('lesson');
971 1019
972 1020 $this->validate($lessonData, [
973 1021 'title' => 'required',
974 1022 'parent_id' => 'required',
@@ -1032,8 +1080,12 @@
1032 1080 if ($dirtyFields) {
1033 1081 $lesson->save();
1034 1082 $isNewlyPublished = $lesson->status === 'published' && $previousStatus !== 'published';
1035 1083 do_action('fluent_community/lesson/updated', $lesson, $dirtyFields, $isNewlyPublished);
1084 +
1085 + if ($isNewlyPublished) {
1086 + do_action('fluent_community/lesson/published', $lesson);
1087 + }
1036 1088 }
1037 1089
1038 1090 do_action('fluent_community/lesson/additional_media_updated', $request->all(), $lesson, $updateData);
1039 1091
@@ -1050,15 +1102,39 @@
1050 1102 })
1051 1103 ->where('id', $lessionId)
1052 1104 ->firstOrFail();
1053 1105
1106 + $previousStatus = $lesson->status;
1107 +
1054 1108 $acceptedFields = [ 'title', 'status', 'slug' ];
1055 1109
1056 1110 // empty title/slug/status must not overwrite, but a literal "0" is a valid value
1111 + // Request::only() is a plain array pick with no sanitization, so each field is
1112 + // sanitized here the same way createLesson/updateLesson do it. The title is
1113 + // rendered with v-html in the course views, so it must not carry markup.
1057 1114 $lessonData = array_filter($request->only($acceptedFields), function ($value) {
1058 1115 return $value !== null && $value !== '';
1059 1116 });
1060 1117
1118 + if (isset($lessonData['title'])) {
1119 + $lessonData['title'] = sanitize_text_field($lessonData['title']);
1120 + }
1121 +
1122 + if (isset($lessonData['slug'])) {
1123 + // sanitize_title() alone let an author set a slug already taken by a
1124 + // sibling lesson, which getLessonBySlug() then resolves arbitrarily.
1125 + $lessonData['slug'] = CourseLesson::uniqueSlug(
1126 + $lessonData['slug'],
1127 + $lesson->space_id,
1128 + $lesson->id,
1129 + Arr::get($lessonData, 'title', $lesson->title)
1130 + );
1131 + }
1132 +
1133 + if (isset($lessonData['status']) && !in_array($lessonData['status'], ['draft', 'published', 'archived'], true)) {
1134 + unset($lessonData['status']);
1135 + }
1136 +
1061 1137 if (Arr::get($lessonData, 'status') === 'published' && $lesson->status !== 'published') {
1062 1138 if (empty($lesson->scheduled_at)) {
1063 1139 $lessonData['scheduled_at'] = current_time('mysql');
1064 1140 }
@@ -1070,10 +1146,19 @@
1070 1146 }
1071 1147
1072 1148 if (!empty($lessonData)) {
1073 1149 $lesson->fill($lessonData);
1074 - if ($lesson->isDirty()) {
1150 + $dirtyFields = $lesson->getDirty();
1151 +
1152 + if ($dirtyFields) {
1075 1153 $lesson->save();
1154 +
1155 + $isNewlyPublished = $lesson->status === 'published' && $previousStatus !== 'published';
1156 + do_action('fluent_community/lesson/updated', $lesson, $dirtyFields, $isNewlyPublished);
1157 +
1158 + if ($isNewlyPublished) {
1159 + do_action('fluent_community/lesson/published', $lesson);
1160 + }
1076 1161 }
1077 1162 }
1078 1163
1079 1164 return [
@@ -1148,8 +1233,9 @@
1148 1233 $newLesson = CourseLesson::findOrFail($newLesson->id);
1149 1234
1150 1235 CourseHelper::copyLessonDocuments($lesson, $newLesson);
1151 1236
1237 + do_action('fluent_community/lesson/created', $newLesson, $newLesson->topic);
1152 1238 do_action('fluent_community/lesson/duplicated', $newLesson, $lesson);
1153 1239
1154 1240 return [
1155 1241 'message' => __('Lesson has been duplicated successfully.', 'fluent-community'),