PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.7
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.7
4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 4.2.1 All 138 releases
learnpress / inc / MCP / Domain / LessonTools.php

LessonTools.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.4.7, at inc/MCP/Domain/LessonTools.php

410 lines 12.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace LearnPress\MCP\Domain;
4
5 use LearnPress\MCP\Mappers\ResponseMapper;
6 use LearnPress\MCP\Support\Curriculum;
7 use LearnPress\MCP\Support\Errors;
8 use LearnPress\MCP\Support\Pagination;
9 use LearnPress\MCP\Support\Permissions;
10 use LearnPress\MCP\Support\Sanitizer;
11 use LearnPress\MCP\Support\Validator;
12 use LearnPress\Models\CourseModel;
13 use LearnPress\Models\CoursePostModel;
14 use LearnPress\Models\CourseSectionModel;
15 use LearnPress\Models\LessonPostModel;
16 use LP_Material_Files_DB;
17 use Throwable;
18 use WP_Error;
19
20 defined( 'ABSPATH' ) || exit;
21
22 /**
23 * Lesson create/update/delete executors.
24 *
25 * Creates lessons through the LearnPress section curriculum API and persists
26 * lesson metadata. Delete is reversible (trash + relationship removal with
27 * recovery metadata).
28 */
29 class LessonTools {
30
31 const VIDEO_INTRO_META = '_lp_lesson_video_intro';
32
33 /**
34 * Execute `learnpress/create-lesson`.
35 *
36 * @param mixed $input Ability input.
37 *
38 * @return array|WP_Error
39 */
40 public static function create_lesson( $input ) {
41 $args = is_array( $input ) ? $input : array();
42 $course_id = Validator::require_id( $args, 'course_id' );
43 if ( is_wp_error( $course_id ) ) {
44 return $course_id;
45 }
46 $section_id = Validator::require_id( $args, 'section_id' );
47 if ( is_wp_error( $section_id ) ) {
48 return $section_id;
49 }
50
51 $title = Sanitizer::text( $args['title'] ?? '' );
52 if ( '' === $title ) {
53 return Errors::invalid( __( 'title is required.', 'learnpress' ) );
54 }
55
56 $course = Validator::find_course( $course_id );
57 if ( is_wp_error( $course ) ) {
58 return $course;
59 }
60 $section = Validator::find_section( $section_id, $course_id );
61 if ( is_wp_error( $section ) ) {
62 return $section;
63 }
64
65 if ( ! Permissions::can_edit_post( $course_id ) ) {
66 return Errors::forbidden();
67 }
68
69 $status = Validator::status( $args['status'] ?? '' );
70 if ( is_wp_error( $status ) ) {
71 return $status;
72 }
73
74 try {
75 $section_item = $section->create_item_and_add(
76 array(
77 'item_type' => LP_LESSON_CPT,
78 'item_title' => $title,
79 'item_content' => Sanitizer::html( $args['content'] ?? '' ),
80 )
81 );
82
83 $lesson_id = absint( $section_item->item_id );
84 $lesson = LessonPostModel::find( $lesson_id, true );
85 if ( ! $lesson instanceof LessonPostModel ) {
86 return Errors::internal();
87 }
88
89 // Default to draft on create (create_item_and_add publishes by default).
90 self::apply_fields( $lesson, $args, '' !== $status ? $status : 'draft' );
91
92 if ( array_key_exists( 'order', $args ) ) {
93 $course_post = CoursePostModel::find( $course_id, true );
94 if ( $course_post instanceof CoursePostModel ) {
95 Curriculum::place_item( $course_post, $course_id, $lesson_id, $section_id, $section_id, absint( $args['order'] ) );
96 }
97 }
98
99 $lesson = LessonPostModel::find( $lesson_id, true );
100
101 return array(
102 'lesson' => ResponseMapper::lesson_write_result(
103 $lesson,
104 array(
105 'course_id' => $course_id,
106 'section_id' => $section_id,
107 )
108 ),
109 );
110 } catch ( Throwable $e ) {
111 return Errors::from_throwable( $e );
112 }
113 }
114
115 /**
116 * Execute `learnpress/update-lesson`.
117 *
118 * @param mixed $input Ability input.
119 *
120 * @return array|WP_Error
121 */
122 public static function update_lesson( $input ) {
123 $args = is_array( $input ) ? $input : array();
124 $lesson_id = Validator::require_id( $args, 'lesson_id' );
125 if ( is_wp_error( $lesson_id ) ) {
126 return $lesson_id;
127 }
128
129 $lesson = Validator::find_lesson( $lesson_id );
130 if ( is_wp_error( $lesson ) ) {
131 return $lesson;
132 }
133
134 if ( ! Permissions::can_edit_post( $lesson_id ) ) {
135 return Errors::forbidden();
136 }
137
138 $status = Validator::status( $args['status'] ?? '' );
139 if ( is_wp_error( $status ) ) {
140 return $status;
141 }
142
143 $location = Curriculum::resolve_location( $lesson_id, LP_LESSON_CPT );
144
145 try {
146 if ( array_key_exists( 'title', $args ) ) {
147 $lesson->post_title = Sanitizer::text( $args['title'] );
148 }
149 if ( array_key_exists( 'content', $args ) ) {
150 $lesson->post_content = Sanitizer::html( $args['content'] );
151 }
152 self::apply_fields( $lesson, $args, $status );
153
154 $move = self::move_within_curriculum( $lesson_id, $location, $args );
155 if ( is_wp_error( $move ) ) {
156 return $move;
157 }
158 $location = $move;
159
160 $lesson = LessonPostModel::find( $lesson_id, true );
161
162 return array( 'lesson' => ResponseMapper::lesson( $lesson, $location ) );
163 } catch ( Throwable $e ) {
164 return Errors::from_throwable( $e );
165 }
166 }
167
168 /**
169 * Execute `learnpress/delete-lesson` (reversible trash).
170 *
171 * @param mixed $input Ability input.
172 *
173 * @return array|WP_Error
174 */
175 public static function delete_lesson( $input ) {
176 $args = is_array( $input ) ? $input : array();
177 $lesson_id = Validator::require_id( $args, 'lesson_id' );
178 if ( is_wp_error( $lesson_id ) ) {
179 return $lesson_id;
180 }
181
182 $lesson = Validator::find_lesson( $lesson_id );
183 if ( is_wp_error( $lesson ) ) {
184 return $lesson;
185 }
186
187 if ( ! Permissions::can_delete_post( $lesson_id ) ) {
188 return Errors::forbidden();
189 }
190
191 $hint_course = absint( $args['course_id'] ?? 0 );
192 $location = Curriculum::resolve_location( $lesson_id, LP_LESSON_CPT, $hint_course );
193
194 if ( $hint_course > 0 && $location['course_id'] !== $hint_course ) {
195 return Errors::not_found( __( 'Lesson does not belong to the provided course.', 'learnpress' ) );
196 }
197 if ( ! empty( $args['section_id'] ) && absint( $args['section_id'] ) !== $location['section_id'] ) {
198 return Errors::not_found( __( 'Lesson does not belong to the provided section.', 'learnpress' ) );
199 }
200
201 try {
202 $removed_from_curriculum = false;
203 if ( $location['section_id'] > 0 ) {
204 $removed_from_curriculum = Curriculum::remove_item_from_section( $location['section_id'], $lesson_id, $location['course_id'] );
205 }
206
207 $trashed = wp_trash_post( $lesson_id );
208 if ( ! $trashed ) {
209 return Errors::internal();
210 }
211
212 return array(
213 'trashed' => true,
214 'lesson_id' => $lesson_id,
215 'removed_from_curriculum' => $removed_from_curriculum,
216 'recovery' => array(
217 'course_id' => $location['course_id'],
218 'section_id' => $location['section_id'],
219 'previous_order' => $location['item_order'],
220 'item_type' => LP_LESSON_CPT,
221 'note' => __( 'Lesson moved to trash. Restore from WP admin and re-add to the section to recover.', 'learnpress' ),
222 ),
223 );
224 } catch ( Throwable $e ) {
225 return Errors::from_throwable( $e );
226 }
227 }
228
229 /**
230 * Execute `learnpress/list-lessons`.
231 *
232 * @param mixed $input Ability input.
233 *
234 * @return array|WP_Error
235 */
236 public static function list_lessons( $input ) {
237 $args = Sanitizer::input_array( $input, 'learnpress/list-lessons' );
238 if ( is_wp_error( $args ) ) {
239 return $args;
240 }
241
242 $course_id = Validator::require_id( $args, 'course_id' );
243 if ( is_wp_error( $course_id ) ) {
244 return $course_id;
245 }
246
247 $course = CourseModel::find( $course_id, true );
248 if ( ! $course instanceof CourseModel ) {
249 return Errors::not_found( __( 'Course not found.', 'learnpress' ) );
250 }
251
252 $section_id = absint( $args['section_id'] ?? 0 );
253 $page = Pagination::page( $args['page'] ?? 1 );
254 $per_page = Pagination::per_page( $args['per_page'] ?? 10 );
255 $statuses = Sanitizer::status_list( $args['status'] ?? null );
256 $refs = Curriculum::collect_items( $course, LP_LESSON_CPT, $section_id );
257
258 $all = array();
259 foreach ( $refs as $ref ) {
260 $lesson = LessonPostModel::find( (int) $ref['item_id'], true );
261 if ( ! $lesson instanceof LessonPostModel ) {
262 continue;
263 }
264 if ( ! empty( $statuses ) && ! in_array( $lesson->post_status, $statuses, true ) ) {
265 continue;
266 }
267 $all[] = ResponseMapper::lesson_summary( $lesson, $ref );
268 }
269
270 $total = count( $all );
271 $items = array_slice( $all, ( $page - 1 ) * $per_page, $per_page );
272
273 return array(
274 'items' => array_values( $items ),
275 'pagination' => array(
276 'page' => $page,
277 'per_page' => $per_page,
278 'total_items' => $total,
279 'total_pages' => Pagination::total_pages( $total, $per_page ),
280 ),
281 );
282 }
283
284 /**
285 * Execute `learnpress/get-lesson-details`.
286 *
287 * @param mixed $input Ability input.
288 *
289 * @return array|WP_Error
290 */
291 public static function get_lesson_details( $input ) {
292 $args = Sanitizer::input_array( $input, 'learnpress/get-lesson-details' );
293 if ( is_wp_error( $args ) ) {
294 return $args;
295 }
296
297 $lesson_id = Validator::require_id( $args, 'lesson_id' );
298 if ( is_wp_error( $lesson_id ) ) {
299 return $lesson_id;
300 }
301
302 $lesson = LessonPostModel::find( $lesson_id, true );
303 if ( ! $lesson instanceof LessonPostModel ) {
304 return Errors::not_found( __( 'Lesson not found.', 'learnpress' ) );
305 }
306
307 $materials = array();
308 if ( class_exists( 'LP_Material_Files_DB' ) ) {
309 $materials_rs = LP_Material_Files_DB::getInstance()->get_material_by_item_id( $lesson_id, 0, 0, false );
310 $materials_rs = is_array( $materials_rs ) ? $materials_rs : array();
311 foreach ( $materials_rs as $material ) {
312 $materials[] = array(
313 'file_id' => absint( $material->file_id ?? 0 ),
314 'name' => (string) ( $material->file_name ?? '' ),
315 'type' => (string) ( $material->file_type ?? '' ),
316 'method' => (string) ( $material->method ?? '' ),
317 'file_path' => (string) ( $material->file_path ?? '' ),
318 'url' => (string) ( $material->file_url ?? '' ),
319 );
320 }
321 }
322
323 return array(
324 'lesson' => array(
325 'lesson_id' => $lesson->get_id(),
326 'title' => (string) $lesson->get_the_title(),
327 'content' => (string) $lesson->get_the_content(),
328 'excerpt' => (string) $lesson->get_the_excerpt(),
329 'duration' => (string) $lesson->get_duration(),
330 'video_intro' => (string) $lesson->get_meta_value_by_key( self::VIDEO_INTRO_META, '' ),
331 'preview' => (bool) $lesson->has_preview(),
332 'status' => (string) $lesson->post_status,
333 'permalink' => (string) $lesson->get_permalink(),
334 'materials' => $materials,
335 'materials_no' => count( $materials ),
336 ),
337 );
338 }
339
340 /**
341 * Apply common lesson post-status and metadata fields.
342 *
343 * @param LessonPostModel $lesson Lesson model.
344 * @param array $args Input args.
345 * @param string $status Validated status ('' when not provided).
346 *
347 * @return void
348 */
349 protected static function apply_fields( LessonPostModel $lesson, array $args, string $status ): void {
350 if ( array_key_exists( 'excerpt', $args ) ) {
351 $lesson->post_excerpt = Sanitizer::html( $args['excerpt'] );
352 }
353 if ( '' !== $status ) {
354 $lesson->post_status = $status;
355 }
356
357 $lesson->save();
358
359 if ( array_key_exists( 'duration', $args ) ) {
360 $lesson->save_meta_value_by_key( LessonPostModel::META_KEY_DURATION, Sanitizer::text( $args['duration'] ) );
361 }
362 if ( array_key_exists( 'preview', $args ) ) {
363 $lesson->save_meta_value_by_key( LessonPostModel::META_KEY_PREVIEW, Sanitizer::boolean( $args['preview'] ) ? 'yes' : 'no' );
364 }
365 if ( array_key_exists( 'video_intro', $args ) ) {
366 $lesson->save_meta_value_by_key( self::VIDEO_INTRO_META, Sanitizer::url( $args['video_intro'] ) );
367 }
368 }
369
370 /**
371 * Move/reorder the lesson within the curriculum when requested.
372 *
373 * @param int $lesson_id Lesson ID.
374 * @param array $location Current location.
375 * @param array $args Input args.
376 *
377 * @return array|WP_Error Updated location.
378 */
379 protected static function move_within_curriculum( int $lesson_id, array $location, array $args ) {
380 $has_section = array_key_exists( 'section_id', $args );
381 $has_order = array_key_exists( 'order', $args );
382 if ( ! $has_section && ! $has_order ) {
383 return $location;
384 }
385
386 $course_id = $location['course_id'];
387 if ( $course_id <= 0 ) {
388 return Errors::invalid( __( 'Lesson is not assigned to a course; cannot move or reorder.', 'learnpress' ) );
389 }
390
391 $old_section_id = $location['section_id'];
392 $new_section_id = $has_section ? absint( $args['section_id'] ) : $old_section_id;
393
394 if ( $has_section ) {
395 $section = Validator::find_section( $new_section_id, $course_id );
396 if ( is_wp_error( $section ) ) {
397 return $section;
398 }
399 }
400
401 $position = $has_order ? absint( $args['order'] ) : 0;
402 $course_post = CoursePostModel::find( $course_id, true );
403 if ( $course_post instanceof CoursePostModel ) {
404 Curriculum::place_item( $course_post, $course_id, $lesson_id, $new_section_id, $old_section_id, $position );
405 }
406
407 return Curriculum::resolve_location( $lesson_id, LP_LESSON_CPT, $course_id );
408 }
409 }
410