PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.0
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.0
4.4.8 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 All 139 releases
learnpress / inc / Ajax / CourseBuilder / CourseBuilderAjax.php

CourseBuilderAjax.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.4.0, at inc/Ajax/CourseBuilder/CourseBuilderAjax.php

2,171 lines 71.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * class CourseBuilderAjax
4 *
5 * @since 4.3
6 * @version 1.0.0
7 */
8
9 namespace LearnPress\Ajax\CourseBuilder;
10
11 use DateTimeImmutable;
12 use DateTimeZone;
13 use Exception;
14 use LearnPress\Ajax\AbstractAjax;
15 use LearnPress\CourseBuilder\CourseBuilder;
16 use LearnPress\CourseBuilder\CourseBuilderAccessPolicy;
17 use LearnPress\Helpers\Response;
18 use LearnPress\Helpers\Template;
19 use LearnPress\Models\CourseModel;
20 use LearnPress\Models\CoursePostModel;
21 use LearnPress\Models\CourseSectionItemModel;
22 use LearnPress\Models\LessonPostModel;
23 use LearnPress\Models\Question\QuestionPostModel;
24 use LearnPress\Models\QuizPostModel;
25 use LearnPress\TemplateHooks\Course\AdminEditCurriculumTemplate;
26 use LearnPress\TemplateHooks\CourseBuilder\Course\BuilderEditCourseTemplate;
27 use LearnPress\TemplateHooks\CourseBuilder\Course\BuilderListCoursesTemplate;
28 use LearnPress\TemplateHooks\CourseBuilder\CourseBuilderTemplate;
29 use LearnPress\TemplateHooks\CourseBuilder\Lesson\BuilderListLessonsTemplate;
30 use LearnPress\TemplateHooks\CourseBuilder\Question\BuilderListQuestionsTemplate;
31 use LearnPress\TemplateHooks\CourseBuilder\Question\BuilderQuestionTemplate;
32 use LearnPress\TemplateHooks\CourseBuilder\Quiz\BuilderListQuizzesTemplate;
33 use LearnPress\TemplateHooks\CourseBuilder\Quiz\BuilderQuizTemplate;
34 use LP_Course_CURD;
35 use LP_Helper;
36 use LP_Lesson_CURD;
37 use LP_Question_CURD;
38 use LP_Quiz_CURD;
39 use LP_REST_Response;
40 use LP_Settings;
41 use stdClass;
42 use Throwable;
43
44 class CourseBuilderAjax extends AbstractAjax {
45 /**
46 * Check permissions and validate parameters.
47 *
48 * @throws Exception
49 *
50 * @since 4.3
51 * @version 1.0.0
52 */
53 public static function check_valid_course() {
54 $params = wp_unslash( $_REQUEST['data'] ?? '' );
55 if ( empty( $params ) ) {
56 throw new Exception( 'Error: params invalid!' );
57 }
58
59 $params = LP_Helper::json_decode( $params, true );
60 $course_id = ! empty( $params['course_id'] ) ? (int) $params['course_id'] : 0;
61 $courseModel = CourseModel::find( $course_id, true );
62 if ( empty( $courseModel ) ) {
63 $params['insert'] = true;
64 $params['course_model'] = '';
65 } else {
66 $params['insert'] = false;
67 $params['course_model'] = $courseModel;
68 }
69
70 return $params;
71 }
72
73 /**
74 * @throws Exception
75 */
76 public static function check_valid_lesson() {
77 $params = wp_unslash( $_REQUEST['data'] ?? '' );
78 if ( empty( $params ) ) {
79 throw new Exception( 'Error: params invalid!' );
80 }
81
82 $params = LP_Helper::json_decode( $params, true );
83 $lesson_id = absint( $params['lesson_id'] ?? 0 );
84 $lesson_model = LessonPostModel::find( $lesson_id, true );
85 if ( empty( $lesson_model ) ) {
86 $params['insert'] = true;
87 $params['lesson_model'] = '';
88 } else {
89 $params['insert'] = false;
90 $params['lesson_model'] = $lesson_model;
91 }
92
93 return $params;
94 }
95
96 public static function check_valid_quiz() {
97 $params = wp_unslash( $_REQUEST['data'] ?? '' );
98 if ( empty( $params ) ) {
99 throw new Exception( 'Error: params invalid!' );
100 }
101
102 $params = LP_Helper::json_decode( $params, true );
103 $quiz_id = ! empty( $params['quiz_id'] ) ? (int) $params['quiz_id'] : 0;
104 $quiz_model = QuizPostModel::find( $quiz_id, true );
105 if ( empty( $quiz_model ) ) {
106 $params['insert'] = true;
107 } else {
108 $params['insert'] = false;
109 $params['quiz_model'] = $quiz_model;
110 }
111
112 return $params;
113 }
114
115 public static function check_valid_question() {
116 $params = wp_unslash( $_REQUEST['data'] ?? '' );
117 if ( empty( $params ) ) {
118 throw new Exception( 'Error: params invalid!' );
119 }
120
121 $params = LP_Helper::json_decode( $params, true );
122 $question_id = ! empty( $params['question_id'] ) ? (int) $params['question_id'] : 0;
123 $question_model = QuestionPostModel::find( $question_id, true );
124 if ( empty( $question_model ) ) {
125 $params['insert'] = true;
126 } else {
127 $params['insert'] = false;
128 $params['question_model'] = $question_model;
129 }
130
131 return $params;
132 }
133
134 /**
135 * Prepare desired slug when restoring a trashed post with a new permalink.
136 *
137 * WordPress stores `_wp_desired_post_slug` on trash and may restore that value
138 * when status changes from `trash` to a non-trash status.
139 *
140 * @param int $post_id
141 * @param string $target_status
142 * @param string $requested_slug
143 *
144 * @return bool
145 */
146 protected function prepare_desired_slug_for_restore( int $post_id, string $target_status, string $requested_slug ): bool {
147 if ( $post_id <= 0 || '' === $requested_slug ) {
148 return false;
149 }
150
151 if ( ! in_array( $target_status, [ 'publish', 'draft' ], true ) ) {
152 return false;
153 }
154
155 if ( 'trash' !== get_post_status( $post_id ) ) {
156 return false;
157 }
158
159 update_post_meta( $post_id, '_wp_desired_post_slug', $requested_slug );
160
161 return true;
162 }
163
164 /**
165 * Ensure restored post keeps requested slug after trash status transition.
166 *
167 * @param int $post_id
168 * @param string $requested_slug
169 *
170 * @return void
171 */
172 protected function sync_slug_after_restore( int $post_id, string $requested_slug ): void {
173 if ( $post_id <= 0 || '' === $requested_slug ) {
174 return;
175 }
176
177 $post = get_post( $post_id );
178 if ( ! $post || 'trash' === $post->post_status ) {
179 return;
180 }
181
182 if ( $post->post_name === $requested_slug ) {
183 return;
184 }
185
186 wp_update_post(
187 [
188 'ID' => $post_id,
189 'post_name' => $requested_slug,
190 ]
191 );
192 }
193
194 /**
195 * Permalink notice when lesson/quiz is not assigned to a course.
196 *
197 * @return string
198 */
199 protected function get_item_permalink_unavailable_message(): string {
200 return __( 'Permalink is only available if the item is already assigned to a course.', 'learnpress' );
201 }
202
203 /**
204 * Check whether a post status is public.
205 *
206 * @param string $status
207 *
208 * @return bool
209 */
210 protected function is_public_post_status( string $status ): bool {
211 $status_object = get_post_status_object( $status );
212
213 return (bool) ( $status_object && ! empty( $status_object->public ) );
214 }
215
216 /**
217 * Normalize target course status based on role/capability and current status.
218 * Mirrors wp-admin behavior for instructors without publish capability.
219 *
220 * @param string $requested_status
221 * @param bool $insert
222 * @param CourseModel|null $courseModel
223 *
224 * @return string
225 */
226 protected function normalize_course_status_for_save( string $requested_status, bool $insert, ?CourseModel $courseModel ): string {
227 $allowed_statuses = [ 'publish', 'future', 'draft', 'pending', 'private' ];
228 if ( ! in_array( $requested_status, $allowed_statuses, true ) ) {
229 $requested_status = 'draft';
230 }
231
232 $is_admin_user = current_user_can( ADMIN_ROLE );
233 if ( $is_admin_user ) {
234 return $requested_status;
235 }
236
237 $is_instructor_user = current_user_can( LP_TEACHER_ROLE );
238 $required_review = LP_Settings::get_option( 'required_review', 'yes' ) === 'yes';
239 $can_publish_course = current_user_can( 'publish_' . LP_COURSE_CPT . 's' );
240
241 $current_status = $insert ? 'auto-draft' : (string) ( $courseModel->post_status ?? 'draft' );
242 $is_public_state = $this->is_public_post_status( $current_status );
243
244 // Require moderation for instructors when review is enabled.
245 if ( $is_instructor_user && $required_review && ! $is_public_state && in_array( $requested_status, [ 'publish', 'private', 'future' ], true ) ) {
246 return 'pending';
247 }
248
249 // Keep wp-admin behavior: users without publish capability submit for review.
250 if ( ! $can_publish_course && ! $is_public_state && in_array( $requested_status, [ 'publish', 'private', 'future' ], true ) ) {
251 return 'pending';
252 }
253
254 return $requested_status;
255 }
256
257 /**
258 * Normalize publish date from Course Builder UI datetime value.
259 *
260 * @param string $datetime_value
261 *
262 * @return array<string, string>|null
263 */
264 protected function normalize_course_post_date_for_save( string $datetime_value ): ?array {
265 $datetime_value = trim( $datetime_value );
266 if ( '' === $datetime_value ) {
267 return null;
268 }
269
270 $timezone = wp_timezone();
271 $formats = [ 'Y-m-d\TH:i', 'Y-m-d\TH:i:s', 'Y-m-d H:i:s' ];
272 $parsed = null;
273
274 foreach ( $formats as $format ) {
275 $date_candidate = DateTimeImmutable::createFromFormat( $format, $datetime_value, $timezone );
276 if ( ! $date_candidate instanceof DateTimeImmutable ) {
277 continue;
278 }
279
280 $errors = DateTimeImmutable::getLastErrors();
281 if ( is_array( $errors ) && ( ! empty( $errors['warning_count'] ) || ! empty( $errors['error_count'] ) ) ) {
282 continue;
283 }
284
285 $parsed = $date_candidate;
286 break;
287 }
288
289 if ( ! $parsed instanceof DateTimeImmutable ) {
290 return null;
291 }
292
293 $gmt_timezone = new DateTimeZone( 'UTC' );
294
295 return [
296 'post_date' => $parsed->format( 'Y-m-d H:i:s' ),
297 'post_date_gmt' => $parsed->setTimezone( $gmt_timezone )->format( 'Y-m-d H:i:s' ),
298 ];
299 }
300
301 /**
302 * Save Course.
303 *
304 * @since 4.3
305 * @version 1.0.1
306 */
307 public function save_courses() {
308 $response = new LP_REST_Response();
309 $response->data = new stdClass();
310
311 try {
312 $data = self::check_valid_course();
313 $course_id = $data['course_id'] ?? 0;
314 $settings = $data['course_settings'] ?? false;
315 $insert = $data['insert'];
316 $course_title = isset( $data['course_title'] ) ? trim( sanitize_text_field( wp_unslash( (string) $data['course_title'] ) ) ) : '';
317 $course_status_requested = ! empty( $data['course_status'] ) ? sanitize_text_field( $data['course_status'] ) : 'publish';
318 $course_visibility = ! empty( $data['course_visibility'] ) ? sanitize_key( $data['course_visibility'] ) : '';
319 $course_password_raw = isset( $data['course_password'] ) ? wp_unslash( (string) $data['course_password'] ) : '';
320 $course_password = sanitize_text_field( $course_password_raw );
321 $course_post_date_raw = ! empty( $data['course_post_date'] ) ? sanitize_text_field( $data['course_post_date'] ) : '';
322 $course_post_date_data = $this->normalize_course_post_date_for_save( $course_post_date_raw );
323 $post_password_to_update = null;
324
325 if ( '' === $course_title ) {
326 throw new Exception( __( 'Course title is required.', 'learnpress' ) );
327 }
328
329 if ( $insert ) {
330 // Check user capability before insert
331 if ( ! current_user_can( 'edit_lp_courses' ) ) {
332 throw new Exception( __( 'You are not allowed to create courses', 'learnpress' ) );
333 }
334
335 $course_status = $this->normalize_course_status_for_save( $course_status_requested, true, null );
336
337 $categories = ! empty( $data['course_categories'] ) ? array_map( 'absint', explode( ',', $data['course_categories'] ) ) : array();
338 $tags = ! empty( $data['course_tags'] ) ? array_map( 'absint', explode( ',', $data['course_tags'] ) ) : array();
339
340 $insert_post_data = array(
341 'post_type' => LP_COURSE_CPT,
342 'post_title' => $course_title,
343 'post_content' => Template::sanitize_html_content( $data['course_description'] ?? '' ),
344 'post_status' => $course_status,
345 'tax_input' => array(
346 'course_category' => $categories,
347 'course_tag' => $tags,
348 ),
349 );
350
351 if ( ! empty( $course_post_date_data['post_date'] ) ) {
352 $insert_post_data['post_date'] = $course_post_date_data['post_date'];
353 $insert_post_data['post_date_gmt'] = $course_post_date_data['post_date_gmt'];
354 }
355
356 if ( in_array( $course_visibility, [ 'public', 'private' ], true ) ) {
357 $insert_post_data['post_password'] = '';
358 } elseif ( 'password' === $course_visibility ) {
359 if ( '' === $course_password ) {
360 throw new Exception( __( 'Password is required for password protected visibility.', 'learnpress' ) );
361 }
362 $insert_post_data['post_password'] = $course_password;
363 }
364
365 $course_id = wp_insert_post( $insert_post_data, true );
366
367 if ( is_wp_error( $course_id ) ) {
368 throw new Exception( $course_id->get_error_message() );
369 }
370
371 // Load the newly created course as CourseModel
372 $courseModel = CourseModel::find( $course_id, true );
373 if ( ! $courseModel ) {
374 throw new Exception( __( 'Failed to load course model', 'learnpress' ) );
375 }
376 } else {
377 $courseModel = $data['course_model'];
378 $course_status = $this->normalize_course_status_for_save( $course_status_requested, false, $courseModel );
379
380 $co_instructor_ids = $courseModel->get_meta_value_by_key( '_lp_co_teacher', [] );
381 if ( absint( $courseModel->post_author ) !== get_current_user_id() &&
382 ! current_user_can( 'manage_options' ) &&
383 ! in_array( get_current_user_id(), $co_instructor_ids ) ) {
384 throw new Exception( __( 'You are not allowed to update this course', 'learnpress' ) );
385 }
386
387 $courseModel->post_status = $course_status;
388 if ( ! empty( $course_post_date_data['post_date'] ) ) {
389 $courseModel->post_date = $course_post_date_data['post_date'];
390 $courseModel->post_date_gmt = $course_post_date_data['post_date_gmt'];
391 }
392
393 if ( in_array( $course_visibility, [ 'public', 'private' ], true ) ) {
394 $post_password_to_update = '';
395 } elseif ( 'password' === $course_visibility ) {
396 if ( '' !== $course_password ) {
397 $post_password_to_update = $course_password;
398 } else {
399 $existing_post = get_post( $courseModel->ID );
400 if ( $existing_post && ! empty( $existing_post->post_password ) ) {
401 $post_password_to_update = (string) $existing_post->post_password;
402 } else {
403 throw new Exception( __( 'Password is required for password protected visibility.', 'learnpress' ) );
404 }
405 }
406 }
407
408 if ( '' !== $course_title ) {
409 $categories = ! empty( $data['course_categories'] ) ? array_map( 'absint', explode( ',', $data['course_categories'] ) ) : array();
410 $tags = ! empty( $data['course_tags'] ) ? array_map( 'absint', explode( ',', $data['course_tags'] ) ) : array();
411
412 $courseModel->post_title = $course_title;
413 $courseModel->post_content = Template::sanitize_html_content( $data['course_description'] ?? '' );
414
415 wp_set_post_terms( $courseModel->ID, $categories, 'course_category' );
416 wp_set_post_terms( $courseModel->ID, $tags, 'course_tag' );
417 }
418
419 // Update permalink/slug if provided
420 if ( ! empty( $data['course_permalink'] ) ) {
421 $new_slug = sanitize_title( $data['course_permalink'] );
422 if ( $new_slug && $new_slug !== $courseModel->post_name ) {
423 wp_update_post(
424 array(
425 'ID' => $courseModel->ID,
426 'post_name' => $new_slug,
427 )
428 );
429 $courseModel->post_name = $new_slug;
430 }
431 }
432
433 $course_id = $courseModel->ID;
434 }
435
436 if ( $settings ) {
437 $this->save_course_settings_to_model( $courseModel, $data );
438 }
439
440 if ( ! empty( $courseModel->meta_data ) ) {
441 $coursePostModel = new CoursePostModel( $courseModel );
442 foreach ( $courseModel->meta_data as $meta_key => $meta_value ) {
443 $coursePostModel->save_meta_value_by_key( $meta_key, $meta_value );
444 }
445 $coursePostModel->save();
446 }
447
448 // Handle course thumbnail - AFTER coursePostModel->save() to avoid being overwritten
449 if ( isset( $data['course_thumbnail_id'] ) ) {
450 $thumbnail_id = absint( $data['course_thumbnail_id'] );
451
452 if ( $thumbnail_id > 0 ) {
453 $result = set_post_thumbnail( $course_id, $thumbnail_id );
454 } else {
455 $result = delete_post_thumbnail( $course_id );
456 }
457 $current_thumbnail = get_post_thumbnail_id( $course_id );
458 }
459
460 if ( null !== $post_password_to_update ) {
461 wp_update_post(
462 array(
463 'ID' => $course_id,
464 'post_password' => $post_password_to_update,
465 )
466 );
467 }
468
469 // Use persisted status after save to avoid UI drift when WP normalizes status by capability.
470 $saved_course_status = get_post_status( $course_id );
471 if ( ! is_string( $saved_course_status ) || '' === $saved_course_status ) {
472 $saved_course_status = $course_status;
473 }
474
475 $response->status = 'success';
476 $response->message = $insert ? __( 'Insert course successfully!', 'learnpress' ) : __( 'Update course successfully!', 'learnpress' );
477 $response->data->status = $saved_course_status;
478 $response->data->button_title = 'publish' === $saved_course_status
479 ? __( 'Update', 'learnpress' )
480 : ( 'pending' === $saved_course_status ? __( 'Submit for Review', 'learnpress' ) : __( 'Publish', 'learnpress' ) );
481 $response->data->course_id_new = $insert ? $course_id : '';
482
483 // Return the actual saved permalink data (important if WordPress auto-generated a unique slug)
484 $saved_post = get_post( $course_id );
485 if ( $saved_post ) {
486 $visibility = 'public';
487 if ( 'private' === $saved_post->post_status ) {
488 $visibility = 'private';
489 } elseif ( ! empty( $saved_post->post_password ) ) {
490 $visibility = 'password';
491 }
492
493 $response->data->visibility = $visibility;
494 $response->data->course_slug = $saved_post->post_name;
495 $response->data->course_permalink = get_permalink( $course_id );
496 }
497
498 // Return full redirect URL for new courses
499 if ( $insert && $course_id ) {
500 $response->data->redirect_url = CourseBuilder::get_link_course_builder( "courses/{$course_id}" );
501 }
502
503 wp_send_json( $response );
504 } catch ( Throwable $th ) {
505 $response->message = $th->getMessage();
506 wp_send_json( $response );
507 }
508 }
509
510 /**
511 * Save Course Settings only (from Settings tab).
512 *
513 * @since 4.3
514 * @version 1.0.0
515 */
516 public function save_course_settings() {
517 $response = new LP_REST_Response();
518 $response->data = new stdClass();
519
520 try {
521 $data = self::check_valid_course();
522 $course_id = $data['course_id'] ?? 0;
523 $insert = $data['insert'];
524 $courseModel = $data['course_model'];
525
526 if ( $insert || empty( $courseModel ) ) {
527 throw new Exception( __( 'Course not found. Please save the course first.', 'learnpress' ) );
528 }
529
530 $co_instructor_ids = $courseModel->get_meta_value_by_key( '_lp_co_teacher', [] );
531 if ( absint( $courseModel->post_author ) !== get_current_user_id() &&
532 ! current_user_can( 'manage_options' ) &&
533 ! in_array( get_current_user_id(), $co_instructor_ids ) ) {
534 throw new Exception( __( 'You are not allowed to update this course', 'learnpress' ) );
535 }
536
537 // Save course settings
538 $this->save_course_settings_to_model( $courseModel, $data );
539
540 // Save meta data
541 if ( ! empty( $courseModel->meta_data ) ) {
542 $coursePostModel = new CoursePostModel( $courseModel );
543 foreach ( $courseModel->meta_data as $meta_key => $meta_value ) {
544 $coursePostModel->save_meta_value_by_key( $meta_key, $meta_value );
545 }
546 $coursePostModel->save();
547 }
548
549 $response->status = 'success';
550 $response->message = __( 'Settings saved successfully!', 'learnpress' );
551
552 wp_send_json( $response );
553 } catch ( Throwable $th ) {
554 $response->status = 'error';
555 $response->message = $th->getMessage();
556 wp_send_json( $response );
557 }
558 }
559
560 /**
561 * Save all course settings to CourseModel
562 *
563 * @param CoursePostModel $courseModel
564 * @param array $data
565 *
566 * @throws Exception
567 */
568 public function save_course_settings_to_model( CoursePostModel &$courseModel, array $data ) {
569 // General settings
570 $this->save_general_settings_to_model( $courseModel, $data );
571
572 // Offline course settings
573 $this->save_offline_settings_to_model( $courseModel, $data );
574
575 // Price settings
576 $this->save_price_settings_to_model( $courseModel, $data );
577
578 // Extra info settings
579 $this->save_extra_settings_to_model( $courseModel, $data );
580
581 // Assessment settings
582 $this->save_assessment_settings_to_model( $courseModel, $data );
583
584 // Author settings
585 $this->save_author_settings_to_model( $courseModel, $data );
586 }
587
588 /**
589 * Save general settings to CourseModel
590 *
591 * @param CoursePostModel $courseModel
592 * @param array $data
593 */
594 protected function save_general_settings_to_model( CoursePostModel &$courseModel, array $data ) {
595 if ( isset( $data['_lp_duration'] ) ) {
596 $duration_value = ! empty( $data['_lp_duration'] ) ? str_replace( ',', ' ', $data['_lp_duration'] ) : '0 minute';
597 $explode = explode( ' ', $duration_value );
598 $number = (float) $explode[0] < 0 ? 0 : absint( $explode[0] );
599 $unit = $explode[1] ?? 'minute';
600
601 $courseModel->meta_data->{CoursePostModel::META_KEY_DURATION} = $number . ' ' . $unit;
602 }
603
604 $checkbox_fields = [
605 '_lp_block_expire_duration' => CoursePostModel::META_KEY_BLOCK_EXPIRE_DURATION,
606 '_lp_block_finished' => CoursePostModel::META_KEY_BLOCK_FINISH,
607 '_lp_allow_course_repurchase' => CoursePostModel::META_KEY_ALLOW_COURSE_REPURCHASE,
608 '_lp_has_finish' => CoursePostModel::META_KEY_HAS_FINISH,
609 '_lp_featured' => CoursePostModel::META_KEY_FEATURED,
610 ];
611
612 foreach ( $checkbox_fields as $key => $meta_key ) {
613 if ( isset( $data[ $key ] ) ) {
614 $courseModel->meta_data->{$meta_key} = $data[ $key ] === 'yes' ? 'yes' : '';
615 }
616 }
617
618 $simple_fields = [
619 '_lp_course_repurchase_option' => CoursePostModel::META_KEY_COURSE_REPURCHASE_OPTION,
620 '_lp_level' => CoursePostModel::META_KEY_LEVEL,
621 '_lp_featured_review' => CoursePostModel::META_KEY_FEATURED_REVIEW,
622 '_lp_external_link_buy_course' => CoursePostModel::META_KEY_EXTERNAL_LINK_BY_COURSE,
623 ];
624
625 foreach ( $simple_fields as $key => $meta_key ) {
626 if ( isset( $data[ $key ] ) ) {
627 $courseModel->meta_data->{$meta_key} = sanitize_text_field( $data[ $key ] );
628 }
629 }
630
631 $numeric_fields = [
632 '_lp_students' => CoursePostModel::META_KEY_STUDENTS,
633 '_lp_max_students' => CoursePostModel::META_KEY_MAX_STUDENTS,
634 '_lp_retake_count' => CoursePostModel::META_KEY_RETAKE_COUNT,
635 ];
636
637 foreach ( $numeric_fields as $key => $meta_key ) {
638 if ( isset( $data[ $key ] ) ) {
639 $value = absint( $data[ $key ] );
640 $courseModel->meta_data->{$meta_key} = $value;
641 }
642 }
643 }
644
645 /**
646 * Save offline course settings to CourseModel
647 *
648 * @param CoursePostModel $courseModel
649 * @param array $data
650 */
651 protected function save_offline_settings_to_model( CoursePostModel &$courseModel, array $data ) {
652 if ( isset( $data['_lp_offline_course'] ) ) {
653 $courseModel->meta_data->{CoursePostModel::META_KEY_OFFLINE_COURSE} = $data['_lp_offline_course'] === 'yes' ? 'yes' : '';
654 }
655
656 if ( isset( $data['_lp_offline_lesson_count'] ) ) {
657 $courseModel->meta_data->{CoursePostModel::META_KEY_OFFLINE_LESSON_COUNT} = absint( $data['_lp_offline_lesson_count'] );
658 }
659
660 if ( isset( $data['_lp_deliver_type'] ) ) {
661 $courseModel->meta_data->{CoursePostModel::META_KEY_DELIVER} = sanitize_text_field( $data['_lp_deliver_type'] );
662 }
663
664 if ( isset( $data['_lp_address'] ) ) {
665 $courseModel->meta_data->{CoursePostModel::META_KEY_ADDRESS} = sanitize_text_field( $data['_lp_address'] );
666 }
667 }
668
669 /**
670 * Save price settings to CourseModel
671 *
672 * @param CoursePostModel $courseModel
673 * @param array $data
674 */
675 protected function save_price_settings_to_model( CoursePostModel &$courseModel, array $data ) {
676 // Regular price
677 if ( isset( $data['_lp_regular_price'] ) ) {
678 $regular_price = floatval( $data['_lp_regular_price'] );
679 if ( $regular_price < 0 ) {
680 $regular_price = '';
681 }
682 $courseModel->meta_data->{CoursePostModel::META_KEY_REGULAR_PRICE} = $regular_price;
683 }
684
685 // Sale price
686 if ( isset( $data['_lp_sale_price'] ) ) {
687 $sale_price = $data['_lp_sale_price'] !== '' ? floatval( $data['_lp_sale_price'] ) : '';
688 $regular_price = $courseModel->get_regular_price();
689
690 if ( $sale_price !== '' && $sale_price > $regular_price ) {
691 $sale_price = '';
692 }
693 $courseModel->meta_data->{CoursePostModel::META_KEY_SALE_PRICE} = $sale_price;
694 }
695
696 // Sale dates
697 if ( isset( $data['_lp_sale_start'] ) ) {
698 $courseModel->meta_data->{CoursePostModel::META_KEY_SALE_START} = sanitize_text_field( $data['_lp_sale_start'] );
699 }
700
701 if ( isset( $data['_lp_sale_end'] ) ) {
702 $courseModel->meta_data->{CoursePostModel::META_KEY_SALE_END} = sanitize_text_field( $data['_lp_sale_end'] );
703 }
704
705 // Price prefix/suffix
706 if ( isset( $data['_lp_price_prefix'] ) ) {
707 $courseModel->meta_data->{CoursePostModel::META_KEY_PRICE_PREFIX} = sanitize_text_field( $data['_lp_price_prefix'] );
708 }
709
710 if ( isset( $data['_lp_price_suffix'] ) ) {
711 $courseModel->meta_data->{CoursePostModel::META_KEY_PRICE_SUFFIX} = sanitize_text_field( $data['_lp_price_suffix'] );
712 }
713
714 // No required enroll
715 if ( isset( $data['_lp_no_required_enroll'] ) ) {
716 $courseModel->meta_data->{CoursePostModel::META_KEY_NO_REQUIRED_ENROLL} = $data['_lp_no_required_enroll'] === 'yes' ? 'yes' : '';
717 }
718 }
719
720 /**
721 * Save extra info settings to CourseModel
722 *
723 * @param CoursePostModel $courseModel
724 * @param array $data
725 */
726 protected function save_extra_settings_to_model( CoursePostModel &$courseModel, array $data ) {
727 // Requirements
728 if ( isset( $data['_lp_requirements'] ) ) {
729 $requirements = ! empty( $data['_lp_requirements'] ) ? explode( ',', $data['_lp_requirements'] ) : [];
730 $requirements = array_filter(
731 $requirements,
732 function ( $item ) {
733 return ! is_null( $item ) && $item !== '';
734 }
735 );
736 $courseModel->meta_data->{CoursePostModel::META_KEY_REQUIREMENTS} = array_map( 'sanitize_text_field', array_values( $requirements ) );
737 }
738
739 if ( isset( $data['_lp_target_audiences'] ) ) {
740 $target_audiences = ! empty( $data['_lp_target_audiences'] ) ? explode( ',', $data['_lp_target_audiences'] ) : [];
741 $target_audiences = array_filter(
742 $target_audiences,
743 function ( $item ) {
744 return ! is_null( $item ) && $item !== '';
745 }
746 );
747 $courseModel->meta_data->{CoursePostModel::META_KEY_TARGET} = array_map( 'sanitize_text_field', array_values( $target_audiences ) );
748 }
749
750 if ( isset( $data['_lp_key_features'] ) ) {
751 $key_features = ! empty( $data['_lp_key_features'] ) ? explode( ',', $data['_lp_key_features'] ) : [];
752 $key_features = array_filter(
753 $key_features,
754 function ( $item ) {
755 return ! is_null( $item ) && $item !== '';
756 }
757 );
758 $courseModel->meta_data->{CoursePostModel::META_KEY_FEATURES} = array_map( 'sanitize_text_field', array_values( $key_features ) );
759 }
760
761 // FAQs
762 if ( isset( $data['_lp_faqs_question'] ) ) {
763 $questions = ! empty( $data['_lp_faqs_question'] ) ? explode( ',', $data['_lp_faqs_question'] ) : [];
764 $answers = ! empty( $data['_lp_faqs_answer'] ) ? explode( ',', $data['_lp_faqs_answer'] ) : [];
765 $faqs = [];
766
767 if ( ! empty( $questions ) ) {
768 foreach ( $questions as $index => $question ) {
769 $clean_question = trim( $question );
770 if ( ! empty( $clean_question ) ) {
771 $answer_content = $answers[ $index ] ?? '';
772 $faqs[] = [ sanitize_text_field( $clean_question ), wp_kses_post( $answer_content ) ];
773 }
774 }
775 }
776 $courseModel->meta_data->{CoursePostModel::META_KEY_FAQS} = $faqs;
777 }
778 }
779
780 /**
781 * Save assessment settings to CourseModel
782 *
783 * @param CoursePostModel $courseModel
784 * @param array $data
785 */
786 protected function save_assessment_settings_to_model( CoursePostModel &$courseModel, array $data ) {
787 // Course result evaluation type
788 if ( isset( $data['_lp_course_result'] ) ) {
789 $courseModel->meta_data->{CoursePostModel::META_KEY_EVALUATION_TYPE} = sanitize_text_field( $data['_lp_course_result'] );
790 }
791
792 // Passing condition
793 if ( isset( $data['_lp_passing_condition'] ) ) {
794 $passing_condition = floatval( $data['_lp_passing_condition'] );
795 if ( $passing_condition < 0 ) {
796 $passing_condition = 0;
797 } elseif ( $passing_condition > 100 ) {
798 $passing_condition = 100;
799 }
800 $courseModel->meta_data->{CoursePostModel::META_KEY_PASSING_CONDITION} = $passing_condition;
801 }
802 }
803
804 /**
805 * Save author settings to CourseModel
806 *
807 * @param CoursePostModel $courseModel
808 * @param array $data
809 */
810 protected function save_author_settings_to_model( CoursePostModel &$courseModel, array $data ) {
811 if ( ! isset( $data['_post_author'] ) ) {
812 return;
813 }
814
815 $new_author_id = absint( $data['_post_author'] );
816 if ( $new_author_id <= 0 || $new_author_id === (int) $courseModel->post_author ) {
817 return;
818 }
819 $courseModel->meta_data->_post_author = $new_author_id;
820 $courseModel->post_author = $new_author_id;
821 }
822
823 /**
824 * Duplicate for course.
825 *
826 */
827 public function duplicate_course() {
828 $response = new LP_REST_Response();
829 $response->data = new stdClass();
830
831 try {
832 $data = self::check_valid_course();
833 $course_id = $data['course_id'] ?? 0;
834 $courseModel = $data['course_model'];
835
836 if ( absint( $courseModel->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) {
837 throw new Exception( __( 'You are not allowed to duplicate this course', 'learnpress' ) );
838 }
839
840 if ( ! function_exists( 'learn_press_duplicate_post' ) ) {
841 require_once LP_PLUGIN_PATH . 'inc/admin/lp-admin-functions.php';
842 }
843
844 $curd = new LP_Course_CURD();
845 $new_item_id = $curd->duplicate(
846 $course_id,
847 array(
848 'exclude_meta' => array(
849 'order-pending',
850 'order-processing',
851 'order-completed',
852 'order-cancelled',
853 'order-failed',
854 'count_enrolled_users',
855 '_lp_sample_data',
856 '_lp_retake_count',
857 ),
858 )
859 );
860
861 if ( is_wp_error( $new_item_id ) ) {
862 throw new Exception( $new_item_id->get_error_message() );
863 }
864
865 $courseModel = CourseModel::find( $new_item_id, true );
866 $html = BuilderListCoursesTemplate::render_course( $courseModel );
867 $response->status = 'success';
868 $response->data->html = $html;
869 $response->data->course_id_new = $new_item_id;
870 $response->data->redirect_url = CourseBuilder::get_link_course_builder( "courses/{$new_item_id}" );
871 $response->message = __( 'Course duplicated successfully', 'learnpress' );
872 wp_send_json( $response );
873 } catch ( Throwable $th ) {
874 $response->status = 'error';
875 $response->message = $th->getMessage();
876 wp_send_json( $response );
877 }
878 }
879
880 public function move_trash_course() {
881 $response = new LP_REST_Response();
882 $response->data = new stdClass();
883
884 try {
885 $data = self::check_valid_course();
886 $course_id = $data['course_id'] ?? 0;
887 $courseModel = $data['course_model'];
888 $status = $data['status'] ?? 'trash';
889
890 $co_instructor_ids = get_post_meta( $course_id, '_lp_co_teacher', false );
891 $co_instructor_ids = ! empty( $co_instructor_ids ) ? $co_instructor_ids : array();
892
893 if ( absint( $courseModel->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) && ! in_array( get_current_user_id(), $co_instructor_ids ) ) {
894 throw new Exception( __( 'You are not allowed to delete this course', 'learnpress' ) );
895 }
896
897 if ( $status === 'delete' ) {
898 if ( ! current_user_can( 'manage_options' ) ) {
899 throw new Exception( __( 'You are not allowed to delete this course', 'learnpress' ) );
900 }
901
902 wp_delete_post( $course_id, true );
903
904 $message = __( 'Course has been deleted', 'learnpress' );
905 } elseif ( $status === 'draft' ) {
906 $update = wp_update_post(
907 array(
908 'ID' => $course_id,
909 'post_type' => LP_COURSE_CPT,
910 'post_status' => 'draft',
911 )
912 );
913
914 if ( ! $update ) {
915 throw new Exception( __( 'Course cannot be moved to draft', 'learnpress' ) );
916 }
917
918 $message = __( 'Course has been moved to draft', 'learnpress' );
919 } else {
920 // Store original slug before trashing (wp_trash_post adds __trashed suffix)
921 $original_slug = $courseModel->post_name;
922
923 $delete = wp_trash_post( $course_id );
924
925 if ( ! $delete ) {
926 throw new Exception( __( 'Course cannot be moved to trash', 'learnpress' ) );
927 }
928
929 // Restore original slug after trashing
930 if ( $original_slug ) {
931 wp_update_post(
932 array(
933 'ID' => $course_id,
934 'post_name' => $original_slug,
935 )
936 );
937 }
938
939 $message = __( 'Course moved to trash', 'learnpress' );
940 }
941
942 $response->status = 'success';
943 $response->data->button_title = __( 'Publish', 'learnpress' );
944 $response->data->status = $status;
945 $response->message = $message;
946 wp_send_json( $response );
947 } catch ( Throwable $th ) {
948 $response->status = 'error';
949 $response->message = $th->getMessage();
950 wp_send_json( $response );
951 }
952 }
953
954 public function add_course_category() {
955 $response = new LP_REST_Response();
956 $response->data = new stdClass();
957
958 try {
959 $data = self::check_valid_course();
960 if ( ! current_user_can( 'edit_lp_courses' ) ) {
961 throw new Exception( __( 'You are not allowed to create categories', 'learnpress' ) );
962 }
963
964 $name = sanitize_text_field( $data['name'] ?? '' );
965 $parent = isset( $data['parent'] ) ? (int) $data['parent'] : 0;
966
967 if ( $parent < 0 ) {
968 $parent = 0;
969 }
970
971 $term = wp_insert_term( $name, 'course_category', array( 'parent' => $parent ) );
972
973 if ( is_wp_error( $term ) ) {
974 throw new Exception( $term->get_error_message() );
975 }
976
977 $term_id = $term['term_id'];
978
979 $html = sprintf(
980 '<li id="in-course_category-%1$s" class="popular-category">
981 <label class="selectit">
982 <input value="%1$s" type="checkbox" name="tax_input[course_category][]" id="in-course_category-%1$s" checked="checked">
983 %2$s
984 </label>
985 </li>',
986 esc_attr( $term_id ),
987 esc_html( $name )
988 );
989
990 $response->status = 'success';
991 $response->data->html = $html;
992 $response->data->term_id = $term_id;
993 $response->data->parent = $parent;
994 $response->message = __( 'Insert category successfully!', 'learnpress' );
995 wp_send_json( $response );
996 } catch ( Throwable $th ) {
997 $response->status = 'error';
998 $response->message = $th->getMessage();
999 wp_send_json( $response );
1000 }
1001 }
1002
1003 public function add_course_tag() {
1004 $response = new LP_REST_Response();
1005 $response->data = new stdClass();
1006
1007 try {
1008 $data = self::check_valid_course();
1009 if ( ! current_user_can( 'edit_lp_courses' ) ) {
1010 throw new Exception( __( 'You are not allowed to create tags', 'learnpress' ) );
1011 }
1012
1013 $name = sanitize_text_field( wp_unslash( $data['name'] ?? '' ) );
1014 $term = wp_insert_term( $name, 'course_tag', array() );
1015
1016 if ( is_wp_error( $term ) ) {
1017 throw new Exception( $term->get_error_message() );
1018 }
1019
1020 $html = BuilderEditCourseTemplate::instance()->input_checkbox_tag_item( $term['term_id'], $name, false );
1021 $response->status = 'success';
1022 $response->data->html = $html;
1023 $response->message = __( 'Insert term successfully!', 'learnpress' );
1024 wp_send_json( $response );
1025 } catch ( Throwable $th ) {
1026 $response->status = 'error';
1027 $response->message = $th->getMessage();
1028 wp_send_json( $response );
1029 }
1030 }
1031
1032 /**
1033 * Duplicate for lesson.
1034 *
1035 */
1036 public function duplicate_lesson() {
1037 $response = new LP_REST_Response();
1038 $response->data = new stdClass();
1039
1040 try {
1041 $data = self::check_valid_lesson();
1042 $lesson_id = $data['lesson_id'] ?? 0;
1043 $lesson_model = $data['lesson_model'];
1044
1045 if ( absint( $lesson_model->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) {
1046 throw new Exception( __( 'You are not allowed to duplicate this lesson', 'learnpress' ) );
1047 }
1048
1049 if ( ! function_exists( 'learn_press_duplicate_post' ) ) {
1050 require_once LP_PLUGIN_PATH . 'inc/admin/lp-admin-functions.php';
1051 }
1052
1053 $duplicate_args = apply_filters( 'learn-press/duplicate-post-args', array( 'post_status' => 'publish' ) );
1054 $curd = new LP_Lesson_CURD();
1055 $new_item_id = $curd->duplicate( $lesson_id, $duplicate_args );
1056
1057 if ( is_wp_error( $new_item_id ) ) {
1058 throw new Exception( $new_item_id->get_error_message() );
1059 }
1060 $lesson_model_new = LessonPostModel::find( $new_item_id, true );
1061 $html = BuilderListLessonsTemplate::render_lesson( $lesson_model_new );
1062 $response->status = 'success';
1063 $response->data->html = $html;
1064 $response->message = __( 'Lesson duplicated successfully', 'learnpress' );
1065 wp_send_json( $response );
1066 } catch ( Throwable $th ) {
1067 $response->status = 'error';
1068 $response->message = $th->getMessage();
1069 wp_send_json( $response );
1070 }
1071 }
1072
1073 /**
1074 * Update Lesson from Course Builder.
1075 * Supports partial updates (title only, description only, or settings only).
1076 *
1077 * @since 4.3
1078 * @version 1.0.3
1079 */
1080 public function builder_update_lesson() {
1081 $response = new Response();
1082 $response->data = new stdClass();
1083
1084 try {
1085 $data = self::check_valid_lesson();
1086 $lesson_id = $data['lesson_id'] ?? 0;
1087 $title = LP_Helper::sanitize_params_submitted( $data['lesson_title'] ?? '' );
1088 $description = LP_Helper::sanitize_params_submitted(
1089 $data['lesson_description'] ?? '',
1090 'html'
1091 );
1092 $settings = $data['lesson_settings'] ?? false;
1093 $is_elementor = $data['is_elementor'] ?? false;
1094 $return_html = ( $data['return_html'] ?? 'no' ) === 'yes';
1095 $insert = $data['insert'];
1096 $lesson_slug = LP_Helper::sanitize_params_submitted(
1097 $data['lesson_permalink'] ?? '',
1098 'key'
1099 );
1100 $course_id = absint( $data['course_id'] ?? 0 );
1101
1102 // Determine target status (draft or publish)
1103 $target_status = LP_Helper::sanitize_params_submitted(
1104 $data['lesson_status'] ?? '',
1105 'key'
1106 );
1107 $restore_with_custom_slug = false;
1108
1109 if ( empty( $title ) ) {
1110 throw new Exception( __( 'Lesson title is required', 'learnpress' ) );
1111 }
1112
1113 if ( $insert ) {
1114 $insert_arg = array(
1115 'post_title' => $title,
1116 'post_content' => $description,
1117 'post_status' => $target_status,
1118 );
1119
1120 $lessonPostModelNew = new LessonPostModel( $insert_arg );
1121 $lessonPostModelNew->check_capabilities_create_item_course();
1122
1123 $lessonPostModelNew->save();
1124 $lesson_id = $lessonPostModelNew->ID;
1125
1126 $lesson_model = $lessonPostModelNew;
1127 $response->message = __( 'Create lesson successfully', 'learnpress' );
1128 } else {
1129 $lesson_model = $data['lesson_model'] ?? null;
1130 if ( ! $lesson_model instanceof LessonPostModel ) {
1131 throw new Exception( __( 'Lesson not found', 'learnpress' ) );
1132 }
1133
1134 $lesson_model->check_capabilities_update_item_course();
1135
1136 if ( ! $course_id ) {
1137 $course_id = absint( $this->get_course_by_item_id( $lesson_id ) );
1138 }
1139
1140 // Support for co-instructor.
1141 /*$co_instructor_ids = get_post_meta( $course_id, '_lp_co_teacher', false );
1142 $co_instructor_ids = ! empty( $co_instructor_ids ) ? $co_instructor_ids : array();
1143
1144 if ( absint( $lesson_model->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) && ! in_array( get_current_user_id(), $co_instructor_ids ) ) {
1145 throw new Exception( __( 'You are not allowed to update this lesson', 'learnpress' ) );
1146 }*/
1147
1148 /*$update_arg = array(
1149 'ID' => $lesson_id,
1150 'post_type' => LP_LESSON_CPT,
1151 'post_status' => $target_status,
1152 );*/
1153
1154 if ( defined( 'ELEMENTOR_VERSION' ) ) {
1155 \Elementor\Plugin::$instance->documents->get( $lesson_id )->set_is_built_with_elementor( ! empty( $is_elementor ) );
1156 }
1157
1158 $lesson_model->post_title = $title;
1159 $lesson_model->post_content = $description;
1160 $lesson_model->post_status = $target_status;
1161 if ( ! empty( $lesson_slug ) ) {
1162 $lesson_model->post_name = $lesson_slug;
1163 }
1164
1165 //$restore_with_custom_slug = $this->prepare_desired_slug_for_restore( $lesson_id, $target_status, $lesson_slug );
1166
1167 //$update = wp_update_post( $update_arg );
1168 $lesson_model->save();
1169
1170 /*if ( $restore_with_custom_slug ) {
1171 $this->sync_slug_after_restore( $lesson_id, $lesson_slug );
1172 }*/
1173
1174 if ( $course_id ) {
1175 $courseModelCache = CourseModel::find( $course_id, true );
1176 if ( $courseModelCache ) {
1177 $courseModelCache->sections_items = null;
1178 $courseModelCache->save();
1179 }
1180 }
1181
1182 $response->message = __( 'Update lesson successfully', 'learnpress' );
1183 }
1184
1185 if ( $settings ) {
1186 $this->save_lesson_settings_to_model( $lesson_model, $data );
1187 }
1188
1189 // Remove lesson from curriculum if status is not public
1190 if ( $target_status !== 'publish' ) {
1191 $this->remove_course_item_from_curriculum( $lesson_id, $course_id );
1192 }
1193
1194 $response->status = 'success';
1195 $response->data->status = $lesson_model->post_status;
1196 $response->data->button_title = $lesson_model->post_status === 'publish' ?
1197 __( 'Update', 'learnpress' ) :
1198 __( 'Publish', 'learnpress' );
1199 $response->data->lesson_id_new = $insert ? $lesson_id : '';
1200 $response->message = $target_status === 'draft'
1201 ? esc_html__( 'Lesson saved as draft', 'learnpress' )
1202 : ( $insert ? esc_html__( 'Insert lesson successfully', 'learnpress' ) : esc_html__( 'Update lesson successfully', 'learnpress' ) );
1203
1204 $response->data->lesson_slug = $lesson_model->post_name;
1205
1206 $course_id_of_item = $this->get_course_by_item_id( $lesson_id );
1207 $response->data->permalink_available = false;
1208 $response->data->permalink_notice = $this->get_item_permalink_unavailable_message();
1209 if ( 'publish' === $lesson_model->post_status && $course_id_of_item ) {
1210 $course = learn_press_get_course( $course_id_of_item );
1211 if ( $course ) {
1212 $response->data->permalink_available = true;
1213 $response->data->lesson_permalink = urldecode( $course->get_item_link( $lesson_id ) );
1214 }
1215 }
1216
1217 $lesson_model_for_html = LessonPostModel::find( $lesson_id, true );
1218 if ( $return_html ) {
1219 $response->data->list_item_html = $lesson_model_for_html
1220 ? BuilderListLessonsTemplate::render_lesson( $lesson_model_for_html )
1221 : '';
1222
1223 if ( $course_id ) {
1224 $courseModelForHtml = CourseModel::find( $course_id, true );
1225 if ( $courseModelForHtml && $lesson_model_for_html ) {
1226 $item = new stdClass();
1227 $item->item_id = $lesson_id;
1228 $item->title = $lesson_model_for_html->post_title;
1229 $item->item_type = LP_LESSON_CPT;
1230 AdminEditCurriculumTemplate::instance()->context_data = [ 'is_course_builder' => true ];
1231 $response->data->section_item_html = AdminEditCurriculumTemplate::instance()->html_section_item( $courseModelForHtml, $item );
1232 }
1233 }
1234 }
1235
1236 $response->status = Response::STATUS_SUCCESS;
1237 } catch ( Throwable $e ) {
1238 $response->message = $e->getMessage();
1239 }
1240
1241 wp_send_json( $response );
1242 }
1243
1244 public function move_trash_lesson() {
1245 $response = new LP_REST_Response();
1246 $response->data = new stdClass();
1247
1248 try {
1249 $data = self::check_valid_lesson();
1250 $lesson_id = $data['lesson_id'] ?? 0;
1251 $status = $data['status'] ?? 'trash';
1252 $lesson_model = $data['lesson_model'] ?? [];
1253 $lesson_slug = ! empty( $data['lesson_permalink'] )
1254 ? sanitize_title( wp_unslash( (string) $data['lesson_permalink'] ) )
1255 : '';
1256
1257 if ( ! $lesson_model ) {
1258 throw new Exception( __( 'Lesson not found', 'learnpress' ) );
1259 }
1260
1261 if ( absint( $lesson_model->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) {
1262 throw new Exception( __( 'You are not allowed to delete this lesson', 'learnpress' ) );
1263 }
1264 $course_id = absint( $data['course_id'] ?? 0 );
1265 if ( ! $course_id ) {
1266 $course_id = absint( $this->get_course_by_item_id( $lesson_id ) );
1267 }
1268
1269 if ( $status === 'trash' ) {
1270 $original_slug = (string) get_post_field( 'post_name', $lesson_id );
1271 $move_trash = wp_trash_post( $lesson_id );
1272
1273 if ( is_wp_error( $move_trash ) ) {
1274 throw new Exception( esc_html__( 'Cannot move this lesson to trash', 'learnpress' ) );
1275 }
1276
1277 if ( '' !== $original_slug ) {
1278 wp_update_post(
1279 [
1280 'ID' => $lesson_id,
1281 'post_name' => $original_slug,
1282 ]
1283 );
1284 }
1285
1286 $message = esc_html__( 'This lesson has been moved to trash.', 'learnpress' );
1287 } elseif ( $status === 'delete' ) {
1288
1289 if ( $lesson_model->post_status !== 'trash' ) {
1290 throw new Exception( esc_html__( 'Lesson must be trashed before deleting.', 'learnpress' ) );
1291 }
1292
1293 $delete = wp_delete_post( $lesson_id );
1294
1295 if ( is_wp_error( $delete ) ) {
1296 throw new Exception( esc_html__( 'Cannot delete this lesson.', 'learnpress' ) );
1297 }
1298 $message = esc_html__( 'Delete this lesson successfully', 'learnpress' );
1299 } elseif ( in_array( $status, [ 'publish', 'draft' ], true ) ) {
1300 $restore_with_custom_slug = $this->prepare_desired_slug_for_restore( $lesson_id, $status, $lesson_slug );
1301 $update_args = [
1302 'ID' => $lesson_id,
1303 'post_type' => LP_LESSON_CPT,
1304 'post_status' => $status,
1305 ];
1306
1307 if ( '' !== $lesson_slug ) {
1308 $update_args['post_name'] = $lesson_slug;
1309 }
1310
1311 $update = wp_update_post( $update_args );
1312 if ( ! $update ) {
1313 if ( 'draft' === $status ) {
1314 throw new Exception( __( 'Lesson cannot be moved to draft', 'learnpress' ) );
1315 }
1316
1317 throw new Exception( __( 'Lesson cannot be moved to publish', 'learnpress' ) );
1318 }
1319
1320 if ( $restore_with_custom_slug ) {
1321 $this->sync_slug_after_restore( $lesson_id, $lesson_slug );
1322 }
1323
1324 $message = 'draft' === $status
1325 ? __( 'Lesson has been moved to draft', 'learnpress' )
1326 : __( 'Lesson has been moved to publish', 'learnpress' );
1327 } else {
1328 throw new Exception( __( 'Invalid lesson status transition', 'learnpress' ) );
1329 }
1330
1331 $saved_lesson_status = get_post_status( $lesson_id );
1332 if ( ! is_string( $saved_lesson_status ) || '' === $saved_lesson_status ) {
1333 $saved_lesson_status = $status;
1334 }
1335
1336 if ( $saved_lesson_status !== 'publish' ) {
1337 $this->remove_course_item_from_curriculum( $lesson_id, $course_id );
1338 }
1339
1340 $response->data->status = $saved_lesson_status;
1341 $response->data->button_title = 'publish' === $saved_lesson_status ? __( 'Update', 'learnpress' ) : __( 'Publish', 'learnpress' );
1342 $response->data->lesson_slug = (string) get_post_field( 'post_name', $lesson_id );
1343 $response->data->permalink_available = false;
1344 $response->data->permalink_notice = $this->get_item_permalink_unavailable_message();
1345
1346 $course_id_of_item = $this->get_course_by_item_id( $lesson_id );
1347 if ( 'publish' === $saved_lesson_status && $course_id_of_item ) {
1348 $course = learn_press_get_course( $course_id_of_item );
1349 if ( $course ) {
1350 $response->data->permalink_available = true;
1351 $response->data->lesson_permalink = urldecode( $course->get_item_link( $lesson_id ) );
1352 }
1353 }
1354
1355 if ( 'delete' !== $status ) {
1356 $lesson_model_new = LessonPostModel::find( $lesson_id, true );
1357 $response->data->html = $lesson_model_new ? BuilderListLessonsTemplate::render_lesson( $lesson_model_new ) : '';
1358 }
1359
1360 $response->status = 'success';
1361 $response->message = $message;
1362 wp_send_json( $response );
1363 } catch ( Throwable $th ) {
1364 $response->status = 'error';
1365 $response->message = $th->getMessage();
1366 wp_send_json( $response );
1367 }
1368 }
1369
1370 /**
1371 * Save Lesson Settings
1372 */
1373 protected function save_lesson_settings_to_model( LessonPostModel $lessonModel, array $data ) {
1374 if ( isset( $data['_lp_duration'] ) ) {
1375 $duration = ! empty( $data['_lp_duration'] ) ? str_replace( ',', ' ', $data['_lp_duration'] ) : '0 minute';
1376 $explode = explode( ' ', $duration );
1377 $number = (float) $explode[0] < 0 ? 0 : absint( $explode[0] );
1378 $unit = $explode[1] ?? 'minute';
1379
1380 $lessonModel->save_meta_value_by_key( '_lp_duration', $number . ' ' . $unit );
1381 }
1382
1383 if ( isset( $data['_lp_preview'] ) ) {
1384 $enable = $data['_lp_preview'] === 'yes';
1385 $lessonModel->set_preview( $enable );
1386 }
1387 }
1388
1389 /**
1390 * Save Quiz Settings to QuizPostModel
1391 *
1392 * @param QuizPostModel $quizModel
1393 * @param array $data
1394 *
1395 * @since 4.3
1396 * @version 1.0.0
1397 */
1398 protected function save_quiz_settings_to_model( QuizPostModel $quizModel, array $data ) {
1399 if ( isset( $data['_lp_duration'] ) ) {
1400 $duration = ! empty( $data['_lp_duration'] ) ? str_replace( ',', ' ', $data['_lp_duration'] ) : '0 minute';
1401 $explode = explode( ' ', $duration );
1402 $number = (float) $explode[0] < 0 ? 0 : absint( $explode[0] );
1403 $unit = $explode[1] ?? 'minute';
1404
1405 $quizModel->save_meta_value_by_key( '_lp_duration', $number . ' ' . $unit );
1406 }
1407
1408 $checkbox_keys = [
1409 '_lp_instant_check',
1410 '_lp_negative_marking',
1411 '_lp_minus_skip_questions',
1412 '_lp_review',
1413 '_lp_show_correct_review',
1414 '_lp_show_check_answer',
1415 '_lp_show_hint',
1416 ];
1417
1418 foreach ( $checkbox_keys as $key ) {
1419 if ( isset( $data[ $key ] ) ) {
1420 $value = $data[ $key ] === 'yes' ? 'yes' : 'no';
1421 $quizModel->save_meta_value_by_key( $key, $value );
1422 }
1423 }
1424
1425 $numeric_keys = [
1426 '_lp_passing_grade',
1427 '_lp_retake_count',
1428 '_lp_pagination',
1429 ];
1430
1431 foreach ( $numeric_keys as $key ) {
1432 if ( isset( $data[ $key ] ) ) {
1433 $value = absint( $data[ $key ] );
1434 if ( '_lp_passing_grade' === $key && $value > 100 ) {
1435 $value = 100;
1436 }
1437 $quizModel->save_meta_value_by_key( $key, $value );
1438 }
1439 }
1440 }
1441
1442 /**
1443 * Update Quiz from Course Builder.
1444 * Supports partial updates (title only, description only, or settings only).
1445 *
1446 * @since 4.3
1447 * @version 1.0.2
1448 */
1449 public function builder_update_quiz() {
1450 $response = new Response();
1451 $response->data = new stdClass();
1452
1453 try {
1454 $data = self::check_valid_quiz();
1455 $quiz_id = $data['quiz_id'] ?? 0;
1456 $title = LP_Helper::sanitize_params_submitted( $data['quiz_title'] ?? '' );
1457 $description = LP_Helper::sanitize_params_submitted(
1458 $data['quiz_description'] ?? '',
1459 'html'
1460 );
1461 $settings = $data['quiz_settings'] ?? false;
1462 $is_elementor = $data['is_elementor'] ?? false;
1463 $return_html = ( $data['return_html'] ?? 'no' ) === 'yes';
1464 $insert = $data['insert'];
1465 $quiz_slug = LP_Helper::sanitize_params_submitted(
1466 $data['quiz_permalink'] ?? '',
1467 'key'
1468 );
1469 $course_id = absint( $data['course_id'] ?? 0 );
1470
1471 // Determine target status (draft or publish)
1472 $target_status = LP_Helper::sanitize_params_submitted(
1473 $data['quiz_status'] ?? '',
1474 'key'
1475 );
1476
1477 if ( empty( $title ) ) {
1478 throw new Exception( __( 'Quiz title is required', 'learnpress' ) );
1479 }
1480
1481 if ( $insert ) {
1482 $insert_arg = array(
1483 'post_title' => $title,
1484 'post_content' => $description,
1485 'post_status' => $target_status,
1486 );
1487
1488 $quizPostModelNew = new QuizPostModel( $insert_arg );
1489 $quizPostModelNew->check_capabilities_create_item_course();
1490
1491 $quizPostModelNew->save();
1492 $quiz_id = $quizPostModelNew->ID;
1493
1494 $quiz_model = $quizPostModelNew;
1495 $response->message = __( 'Create quiz successfully', 'learnpress' );
1496 } else {
1497 $quiz_model = $data['quiz_model'] ?? null;
1498 if ( ! $quiz_model instanceof QuizPostModel ) {
1499 throw new Exception( __( 'Quiz not found', 'learnpress' ) );
1500 }
1501
1502 $quiz_model->check_capabilities_update_item_course();
1503
1504 if ( ! $course_id ) {
1505 $course_id = absint( $this->get_course_by_item_id( $quiz_id ) );
1506 }
1507
1508 if ( defined( 'ELEMENTOR_VERSION' ) ) {
1509 \Elementor\Plugin::$instance->documents->get( $quiz_id )->set_is_built_with_elementor( ! empty( $is_elementor ) );
1510 }
1511
1512 $quiz_model->post_title = $title;
1513 $quiz_model->post_content = $description;
1514 $quiz_model->post_status = $target_status;
1515 if ( ! empty( $quiz_slug ) ) {
1516 $quiz_model->post_name = $quiz_slug;
1517 }
1518
1519 $quiz_model->save();
1520
1521 if ( $course_id ) {
1522 $courseModelCache = CourseModel::find( $course_id, true );
1523 if ( $courseModelCache ) {
1524 $courseModelCache->sections_items = null;
1525 $courseModelCache->save();
1526 }
1527 }
1528
1529 $response->message = __( 'Update quiz successfully', 'learnpress' );
1530 }
1531
1532 if ( $settings ) {
1533 $this->save_quiz_settings_to_model( $quiz_model, $data );
1534 }
1535
1536 // Remove quiz from curriculum if status is not public
1537 if ( $target_status !== 'publish' ) {
1538 $this->remove_course_item_from_curriculum( $quiz_id, $course_id );
1539 }
1540
1541 $response->status = 'success';
1542 $response->data->status = $quiz_model->post_status;
1543 $response->data->button_title = $quiz_model->post_status === 'publish' ?
1544 __( 'Update', 'learnpress' ) :
1545 __( 'Publish', 'learnpress' );
1546 $response->data->quiz_id_new = $insert ? $quiz_id : '';
1547 if ( $insert && $quiz_id ) {
1548 $response->data->redirect_url = CourseBuilder::get_link_course_builder( CourseBuilderTemplate::MENU_QUIZZES . "/{$quiz_id}" );
1549 }
1550 $response->message = $target_status === 'draft'
1551 ? esc_html__( 'Quiz saved as draft', 'learnpress' )
1552 : ( $insert ? esc_html__( 'Insert quiz successfully', 'learnpress' ) : esc_html__( 'Update quiz successfully', 'learnpress' ) );
1553
1554 $response->data->quiz_slug = $quiz_model->post_name;
1555
1556 $course_id_of_item = $this->get_course_by_item_id( $quiz_id );
1557 $response->data->permalink_available = false;
1558 $response->data->permalink_notice = $this->get_item_permalink_unavailable_message();
1559 if ( 'publish' === $quiz_model->post_status && $course_id_of_item ) {
1560 $course = learn_press_get_course( $course_id_of_item );
1561 if ( $course ) {
1562 $response->data->permalink_available = true;
1563 $response->data->quiz_permalink = urldecode( $course->get_item_link( $quiz_id ) );
1564 }
1565 }
1566
1567 $quiz_model_for_html = QuizPostModel::find( $quiz_id, true );
1568 if ( $return_html ) {
1569 $response->data->list_item_html = $quiz_model_for_html
1570 ? BuilderListQuizzesTemplate::render_quiz( $quiz_model_for_html )
1571 : '';
1572
1573 if ( $course_id ) {
1574 $courseModelForHtml = CourseModel::find( $course_id, true );
1575 if ( $courseModelForHtml && $quiz_model_for_html ) {
1576 $item = new stdClass();
1577 $item->item_id = $quiz_id;
1578 $item->title = $quiz_model_for_html->post_title;
1579 $item->item_type = LP_QUIZ_CPT;
1580 AdminEditCurriculumTemplate::instance()->context_data = [ 'is_course_builder' => true ];
1581 $response->data->section_item_html = AdminEditCurriculumTemplate::instance()->html_section_item( $courseModelForHtml, $item );
1582 }
1583 }
1584 }
1585
1586 $response->status = Response::STATUS_SUCCESS;
1587 } catch ( Throwable $e ) {
1588 $response->message = $e->getMessage();
1589 }
1590
1591 wp_send_json( $response );
1592 }
1593
1594 public function duplicate_quiz() {
1595 $response = new LP_REST_Response();
1596 $response->data = new stdClass();
1597
1598 try {
1599 $data = self::check_valid_quiz();
1600 $quiz_id = $data['quiz_id'] ?? 0;
1601 $quiz_model = $data['quiz_model'];
1602
1603 if ( absint( $quiz_model->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) {
1604 throw new Exception( __( 'You are not allowed to duplicate this quiz', 'learnpress' ) );
1605 }
1606
1607 if ( ! function_exists( 'learn_press_duplicate_post' ) ) {
1608 require_once LP_PLUGIN_PATH . 'inc/admin/lp-admin-functions.php';
1609 }
1610
1611 $duplicate_args = apply_filters( 'learn-press/duplicate-post-args', array( 'post_status' => 'publish' ) );
1612 $curd = new LP_Quiz_CURD();
1613 $new_item_id = $curd->duplicate( $quiz_id, $duplicate_args );
1614
1615 if ( is_wp_error( $new_item_id ) ) {
1616 throw new Exception( $new_item_id->get_error_message() );
1617 }
1618 $response->status = 'success';
1619 $response->data->redirect_url = CourseBuilder::get_link_course_builder( CourseBuilderTemplate::MENU_QUIZZES . "/{$new_item_id}" );
1620 $response->message = __( 'Quiz duplicated successfully', 'learnpress' );
1621 wp_send_json( $response );
1622 } catch ( Throwable $th ) {
1623 $response->status = 'error';
1624 $response->message = $th->getMessage();
1625 wp_send_json( $response );
1626 }
1627 }
1628
1629 public function move_trash_quiz() {
1630 $response = new LP_REST_Response();
1631 $response->data = new stdClass();
1632
1633 try {
1634 $data = self::check_valid_quiz();
1635 $quiz_id = $data['quiz_id'] ?? 0;
1636 $status = $data['status'] ?? 'trash';
1637 $quiz_model = $data['quiz_model'] ?? [];
1638 $quiz_slug = ! empty( $data['quiz_permalink'] )
1639 ? sanitize_title( wp_unslash( (string) $data['quiz_permalink'] ) )
1640 : '';
1641
1642 if ( ! $quiz_model ) {
1643 throw new Exception( __( 'Quiz not found', 'learnpress' ) );
1644 }
1645
1646 if ( absint( $quiz_model->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) {
1647 throw new Exception( __( 'You are not allowed to delete this quiz', 'learnpress' ) );
1648 }
1649 $course_id = absint( $data['course_id'] ?? 0 );
1650 if ( ! $course_id ) {
1651 $course_id = absint( $this->get_course_by_item_id( $quiz_id ) );
1652 }
1653
1654 if ( $status === 'trash' ) {
1655 $original_slug = (string) get_post_field( 'post_name', $quiz_id );
1656 $move_trash = wp_trash_post( $quiz_id );
1657
1658 if ( is_wp_error( $move_trash ) ) {
1659 throw new Exception( esc_html__( 'Cannot move this quiz to trash', 'learnpress' ) );
1660 }
1661
1662 if ( '' !== $original_slug ) {
1663 wp_update_post(
1664 [
1665 'ID' => $quiz_id,
1666 'post_name' => $original_slug,
1667 ]
1668 );
1669 }
1670
1671 $message = esc_html__( 'This quiz has been moved to trash.', 'learnpress' );
1672 } elseif ( $status === 'delete' ) {
1673
1674 if ( $quiz_model->post_status !== 'trash' ) {
1675 throw new Exception( esc_html__( 'Quiz must be trashed before deleting.', 'learnpress' ) );
1676 }
1677
1678 $delete = wp_delete_post( $quiz_id );
1679
1680 if ( is_wp_error( $delete ) ) {
1681 throw new Exception( esc_html__( 'Cannot delete this quiz.', 'learnpress' ) );
1682 }
1683 $message = esc_html__( 'Delete this quiz successfully', 'learnpress' );
1684 } elseif ( in_array( $status, [ 'publish', 'draft' ], true ) ) {
1685 $restore_with_custom_slug = $this->prepare_desired_slug_for_restore( $quiz_id, $status, $quiz_slug );
1686 $update_args = [
1687 'ID' => $quiz_id,
1688 'post_type' => LP_QUIZ_CPT,
1689 'post_status' => $status,
1690 ];
1691
1692 if ( '' !== $quiz_slug ) {
1693 $update_args['post_name'] = $quiz_slug;
1694 }
1695
1696 $update = wp_update_post( $update_args );
1697 if ( ! $update ) {
1698 if ( 'draft' === $status ) {
1699 throw new Exception( __( 'Quiz cannot be moved to draft', 'learnpress' ) );
1700 }
1701
1702 throw new Exception( __( 'Quiz cannot be moved to publish', 'learnpress' ) );
1703 }
1704
1705 if ( $restore_with_custom_slug ) {
1706 $this->sync_slug_after_restore( $quiz_id, $quiz_slug );
1707 }
1708
1709 $message = 'draft' === $status
1710 ? __( 'Quiz has been moved to draft', 'learnpress' )
1711 : __( 'Quiz has been moved to publish', 'learnpress' );
1712 } else {
1713 throw new Exception( __( 'Invalid quiz status transition', 'learnpress' ) );
1714 }
1715
1716 $saved_quiz_status = get_post_status( $quiz_id );
1717 if ( ! is_string( $saved_quiz_status ) || '' === $saved_quiz_status ) {
1718 $saved_quiz_status = $status;
1719 }
1720
1721 if ( $saved_quiz_status !== 'publish' ) {
1722 $this->remove_course_item_from_curriculum( $quiz_id, $course_id );
1723 }
1724
1725 $response->data->status = $saved_quiz_status;
1726 $response->data->button_title = 'publish' === $saved_quiz_status ? __( 'Update', 'learnpress' ) : __( 'Publish', 'learnpress' );
1727 $response->data->quiz_slug = (string) get_post_field( 'post_name', $quiz_id );
1728 $response->data->permalink_available = false;
1729 $response->data->permalink_notice = $this->get_item_permalink_unavailable_message();
1730
1731 $course_id_of_item = $this->get_course_by_item_id( $quiz_id );
1732 if ( 'publish' === $saved_quiz_status && $course_id_of_item ) {
1733 $course = learn_press_get_course( $course_id_of_item );
1734 if ( $course ) {
1735 $response->data->permalink_available = true;
1736 $response->data->quiz_permalink = urldecode( $course->get_item_link( $quiz_id ) );
1737 }
1738 }
1739
1740 if ( 'delete' !== $status ) {
1741 $fresh_quiz_model = QuizPostModel::find( $quiz_id, true );
1742 $response->data->html = $fresh_quiz_model
1743 ? BuilderListQuizzesTemplate::render_quiz( $fresh_quiz_model )
1744 : '';
1745 }
1746
1747 $response->status = 'success';
1748 $response->message = $message;
1749 wp_send_json( $response );
1750 } catch ( Throwable $th ) {
1751 $response->status = 'error';
1752 $response->message = $th->getMessage();
1753 wp_send_json( $response );
1754 }
1755 }
1756
1757 public function duplicate_question() {
1758 $response = new LP_REST_Response();
1759 $response->data = new stdClass();
1760
1761 try {
1762 $data = self::check_valid_question();
1763 $question_id = $data['question_id'] ?? 0;
1764 $question_model = $data['question_model'];
1765
1766 if ( absint( $question_model->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) {
1767 throw new Exception( __( 'You are not allowed to duplicate this question', 'learnpress' ) );
1768 }
1769
1770 if ( ! function_exists( 'learn_press_duplicate_post' ) ) {
1771 require_once LP_PLUGIN_PATH . 'inc/admin/lp-admin-functions.php';
1772 }
1773
1774 $duplicate_args = apply_filters( 'learn-press/duplicate-post-args', array( 'post_status' => 'publish' ) );
1775 $curd = new LP_Question_CURD();
1776 $new_item_id = $curd->duplicate( $question_id, $duplicate_args );
1777
1778 if ( is_wp_error( $new_item_id ) ) {
1779 throw new Exception( $new_item_id->get_error_message() );
1780 }
1781 $response->status = 'success';
1782 $response->data->redirect_url = CourseBuilder::get_link_course_builder( CourseBuilderTemplate::MENU_QUESTIONS . "/{$new_item_id}" );
1783 $response->message = __( 'Question duplicated successfully', 'learnpress' );
1784 wp_send_json( $response );
1785 } catch ( Throwable $th ) {
1786 $response->status = 'error';
1787 $response->message = $th->getMessage();
1788 wp_send_json( $response );
1789 }
1790 }
1791
1792 public function builder_update_question() {
1793 $response = new Response();
1794 $response->data = new stdClass();
1795
1796 try {
1797 $data = self::check_valid_question();
1798 $question_id = $data['question_id'] ?? 0;
1799 $title = LP_Helper::sanitize_params_submitted( $data['question_title'] ?? '' );
1800 $description = LP_Helper::sanitize_params_submitted(
1801 $data['question_description'] ?? '',
1802 'html'
1803 );
1804 $is_elementor = $data['is_elementor'] ?? false;
1805 $return_html = ( $data['return_html'] ?? 'no' ) === 'yes';
1806 $insert = $data['insert'];
1807 $question_slug = LP_Helper::sanitize_params_submitted(
1808 $data['question_permalink'] ?? '',
1809 'key'
1810 );
1811
1812 // Determine target status (draft or publish)
1813 $target_status = LP_Helper::sanitize_params_submitted(
1814 $data['question_status'] ?? '',
1815 'key'
1816 );
1817
1818 if ( empty( $title ) ) {
1819 throw new Exception( __( 'Question title is required', 'learnpress' ) );
1820 }
1821
1822 if ( $insert ) {
1823 $insert_arg = array(
1824 'post_title' => $title,
1825 'post_content' => $description,
1826 'post_status' => $target_status,
1827 );
1828
1829 $questionPostModelNew = new QuestionPostModel( $insert_arg );
1830 $questionPostModelNew->check_capabilities_create_item_course();
1831
1832 $questionPostModelNew->save();
1833 $question_id = $questionPostModelNew->ID;
1834
1835 $question_model = $questionPostModelNew;
1836 $response->message = __( 'Create question successfully', 'learnpress' );
1837 } else {
1838 $question_model = $data['question_model'] ?? null;
1839 if ( ! $question_model instanceof QuestionPostModel ) {
1840 throw new Exception( __( 'Question not found', 'learnpress' ) );
1841 }
1842
1843 $question_model->check_capabilities_update_item_course();
1844
1845 if ( defined( 'ELEMENTOR_VERSION' ) ) {
1846 \Elementor\Plugin::$instance->documents->get( $question_id )->set_is_built_with_elementor( ! empty( $is_elementor ) );
1847 }
1848
1849 $question_model->post_title = $title;
1850 $question_model->post_content = $description;
1851 $question_model->post_status = $target_status;
1852 if ( ! empty( $question_slug ) ) {
1853 $question_model->post_name = $question_slug;
1854 }
1855
1856 $question_model->save();
1857
1858 $response->message = __( 'Update question successfully', 'learnpress' );
1859 }
1860
1861 // Remove question from quizzes if status is not public
1862 if ( $target_status !== 'publish' ) {
1863 $this->remove_question_from_assigned_quizzes( $question_id );
1864 }
1865
1866 do_action( 'learn-press/course-builder/update-question', $data, $question_model );
1867
1868 $response->status = 'success';
1869 $response->data->status = $question_model->post_status;
1870 $response->data->button_title = $question_model->post_status === 'publish' ?
1871 __( 'Update', 'learnpress' ) :
1872 __( 'Publish', 'learnpress' );
1873 $response->data->question_id_new = $insert ? $question_id : '';
1874 if ( $insert && $question_id ) {
1875 $response->data->redirect_url = CourseBuilder::get_link_course_builder( CourseBuilderTemplate::MENU_QUESTIONS . "/{$question_id}" );
1876 }
1877 $response->message = $target_status === 'draft'
1878 ? esc_html__( 'Question saved as draft', 'learnpress' )
1879 : ( $insert ? esc_html__( 'Insert question successfully', 'learnpress' ) : esc_html__( 'Update question successfully', 'learnpress' ) );
1880
1881 $response->data->question_slug = $question_model->post_name;
1882 $response->data->question_permalink = get_permalink( $question_id );
1883
1884 $question_model_for_html = QuestionPostModel::find( $question_id, true );
1885 if ( $return_html ) {
1886 $response->data->list_item_html = $question_model_for_html
1887 ? BuilderListQuestionsTemplate::render_question( $question_model_for_html )
1888 : '';
1889 }
1890
1891 $response->status = Response::STATUS_SUCCESS;
1892 } catch ( Throwable $e ) {
1893 $response->message = $e->getMessage();
1894 }
1895
1896 wp_send_json( $response );
1897 }
1898
1899 public function move_trash_question() {
1900 $response = new LP_REST_Response();
1901 $response->data = new stdClass();
1902
1903 try {
1904 $data = self::check_valid_question();
1905 $question_id = $data['question_id'] ?? 0;
1906 $status = $data['status'] ?? 'trash';
1907 $question_model = $data['question_model'] ?? [];
1908
1909 if ( ! $question_model ) {
1910 throw new Exception( __( 'Question not found', 'learnpress' ) );
1911 }
1912
1913 if ( absint( $question_model->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) {
1914 throw new Exception( __( 'You are not allowed to delete this question', 'learnpress' ) );
1915 }
1916
1917 if ( $status === 'trash' ) {
1918 $move_trash = wp_trash_post( $question_id );
1919
1920 if ( is_wp_error( $move_trash ) ) {
1921 throw new Exception( esc_html__( 'Cannot move this question to trash', 'learnpress' ) );
1922 }
1923 $message = esc_html__( 'This question has been moved to trash.', 'learnpress' );
1924 } elseif ( $status === 'delete' ) {
1925
1926 if ( $question_model->post_status !== 'trash' ) {
1927 throw new Exception( esc_html__( 'Question must be trashed before deleting.', 'learnpress' ) );
1928 }
1929
1930 $delete = wp_delete_post( $question_id );
1931
1932 if ( is_wp_error( $delete ) ) {
1933 throw new Exception( esc_html__( 'Cannot delete this question.', 'learnpress' ) );
1934 }
1935 $message = esc_html__( 'Delete this question successfully', 'learnpress' );
1936
1937 } elseif ( $status === 'publish' ) {
1938 $update = wp_update_post(
1939 array(
1940 'ID' => $question_id,
1941 'post_type' => LP_QUESTION_CPT,
1942 'post_status' => 'publish',
1943 )
1944 );
1945 if ( ! $update ) {
1946 throw new Exception( __( 'Question cannot be moved to publish', 'learnpress' ) );
1947 }
1948
1949 $message = __( 'Question has been moved to publish', 'learnpress' );
1950 } elseif ( $status === 'draft' ) {
1951 $update = wp_update_post(
1952 array(
1953 'ID' => $question_id,
1954 'post_type' => LP_QUESTION_CPT,
1955 'post_status' => 'draft',
1956 )
1957 );
1958 if ( ! $update ) {
1959 throw new Exception( __( 'Question cannot be restored to draft', 'learnpress' ) );
1960 }
1961
1962 $message = __( 'Question has been restored to draft', 'learnpress' );
1963 }
1964
1965 if ( $status !== 'publish' ) {
1966 $this->remove_question_from_assigned_quizzes( $question_id );
1967 }
1968
1969 $response->data->status = $status;
1970 $response->data->button_title = __( 'Publish', 'learnpress' );
1971
1972 if ( 'delete' !== $status ) {
1973 $fresh_question_model = QuestionPostModel::find( $question_id, true );
1974 $response->data->html = $fresh_question_model
1975 ? BuilderListQuestionsTemplate::render_question( $fresh_question_model )
1976 : '';
1977 }
1978
1979 $response->status = 'success';
1980 $response->message = $message;
1981 wp_send_json( $response );
1982 } catch ( Throwable $th ) {
1983 $response->status = 'error';
1984 $response->message = $th->getMessage();
1985 wp_send_json( $response );
1986 }
1987 }
1988
1989 /**
1990 * Remove lesson/quiz assignment from curriculum sections.
1991 *
1992 * @param int $item_id
1993 * @param int $course_id_fallback
1994 *
1995 * @return void
1996 */
1997 protected function remove_course_item_from_curriculum( int $item_id, int $course_id_fallback = 0 ) {
1998 if ( $item_id <= 0 ) {
1999 return;
2000 }
2001
2002 global $wpdb;
2003
2004 $section_items = $wpdb->get_results(
2005 $wpdb->prepare(
2006 "
2007 SELECT si.section_id, s.section_course_id
2008 FROM {$wpdb->learnpress_section_items} si
2009 INNER JOIN {$wpdb->learnpress_sections} s ON s.section_id = si.section_id
2010 WHERE si.item_id = %d
2011 ",
2012 $item_id
2013 )
2014 );
2015 if ( ! $section_items ) {
2016 if ( $course_id_fallback > 0 ) {
2017 $this->clean_course_curriculum_cache( $course_id_fallback );
2018 }
2019
2020 return;
2021 }
2022
2023 foreach ( $section_items as $section_item ) {
2024 $section_id = absint( $section_item->section_id ?? 0 );
2025 $course_id = absint( $section_item->section_course_id ?? 0 );
2026 if ( ! $section_id || ! $course_id ) {
2027 continue;
2028 }
2029
2030 $course_section_item_model = CourseSectionItemModel::find( $section_id, $item_id );
2031 if ( ! $course_section_item_model ) {
2032 continue;
2033 }
2034
2035 $course_section_item_model->section_course_id = $course_id;
2036 $course_section_item_model->delete();
2037 }
2038 }
2039
2040 /**
2041 * Keep course curriculum caches in sync after direct section item removal.
2042 *
2043 * @param int $course_id
2044 *
2045 * @return void
2046 */
2047 protected function clean_course_curriculum_cache( int $course_id ) {
2048 if ( $course_id <= 0 ) {
2049 return;
2050 }
2051
2052 try {
2053 $courseModel = CourseModel::find( $course_id, true );
2054 if ( ! $courseModel ) {
2055 return;
2056 }
2057
2058 $courseModel->sections_items = null;
2059 $courseModel->total_items = null;
2060 $courseModel->save( true );
2061 } catch ( Throwable $e ) {
2062 error_log( __METHOD__ . ': ' . $e->getMessage() );
2063 }
2064 }
2065
2066 /**
2067 * Remove question assignment from all quizzes.
2068 *
2069 * @param int $question_id
2070 *
2071 * @return void
2072 */
2073 protected function remove_question_from_assigned_quizzes( int $question_id ) {
2074 if ( $question_id <= 0 ) {
2075 return;
2076 }
2077
2078 global $wpdb;
2079 $wpdb->delete(
2080 $wpdb->prefix . 'learnpress_quiz_questions',
2081 array( 'question_id' => $question_id ),
2082 array( '%d' )
2083 );
2084 }
2085
2086 protected function get_course_by_item_id( $item_id ) {
2087 static $cache = [];
2088
2089 global $wpdb;
2090
2091 if ( empty( $item_id ) ) {
2092 return false;
2093 }
2094
2095 $item_id = absint( $item_id );
2096 if ( isset( $cache[ $item_id ] ) ) {
2097 return $cache[ $item_id ] ? $cache[ $item_id ] : false;
2098 }
2099
2100 $course_id = $wpdb->get_var(
2101 $wpdb->prepare(
2102 "SELECT c.ID FROM {$wpdb->posts} c
2103 INNER JOIN {$wpdb->learnpress_sections} s ON c.ID = s.section_course_id
2104 INNER JOIN {$wpdb->learnpress_section_items} si ON si.section_id = s.section_id
2105 WHERE si.item_id = %d ORDER BY si.section_id DESC LIMIT 1
2106 ",
2107 $item_id
2108 )
2109 );
2110 $cache[ $item_id ] = absint( $course_id );
2111
2112 if ( $cache[ $item_id ] ) {
2113 return $cache[ $item_id ];
2114 }
2115
2116 return false;
2117 }
2118
2119 /**
2120 * Save global settings for Course Builder.
2121 *
2122 * @since 4.3.6
2123 * @version 1.0.0
2124 */
2125 public function save_global_settings() {
2126 $response = new LP_REST_Response();
2127 $response->data = new stdClass();
2128
2129 try {
2130 $params = wp_unslash( $_REQUEST['data'] ?? '' );
2131 if ( empty( $params ) ) {
2132 throw new Exception( 'Error: params invalid!' );
2133 }
2134
2135 $data = LP_Helper::json_decode( $params, true );
2136
2137 if ( ! current_user_can( ADMIN_ROLE ) ) {
2138 throw new Exception( __( 'Permission denied', 'learnpress' ) );
2139 }
2140
2141 $hide_instructor_access_admin_screen = ! empty( $data['hide_instructor_access_admin_screen'] ) && $data['hide_instructor_access_admin_screen'] === 'yes' ? 'yes' : 'no';
2142 $logo_remove = ! empty( $data['course_builder_logo_remove'] ) && $data['course_builder_logo_remove'] === 'yes';
2143 $logo_id = absint( $data['course_builder_logo_id'] ?? 0 );
2144
2145 if ( $logo_remove ) {
2146 $logo_id = 0;
2147 }
2148
2149 LP_Settings::update_option( 'hide_instructor_access_admin_screen', $hide_instructor_access_admin_screen );
2150 LP_Settings::update_option( 'course_builder_logo_id', $logo_id );
2151
2152 $logo_url = '';
2153 if ( $logo_id ) {
2154 $logo_url = wp_get_attachment_image_url( $logo_id, 'full' );
2155 }
2156
2157 $response->status = 'success';
2158 $response->message = __( 'Course Builder settings updated.', 'learnpress' );
2159 $response->data->hide_instructor_access_admin_screen = $hide_instructor_access_admin_screen;
2160 $response->data->course_builder_logo_id = $logo_id;
2161 $response->data->course_builder_logo_url = $logo_url;
2162
2163 wp_send_json( $response );
2164 } catch ( Throwable $th ) {
2165 $response->status = 'error';
2166 $response->message = $th->getMessage();
2167 wp_send_json( $response );
2168 }
2169 }
2170 }
2171