post_status ) { return; } if ( $post->post_name === $requested_slug ) { return; } wp_update_post( [ 'ID' => $post_id, 'post_name' => $requested_slug, ] ); } /** * Permalink notice when lesson/quiz is not assigned to a course. * * @return string */ protected function get_item_permalink_unavailable_message(): string { return __( 'Permalink is only available if the item is already assigned to a course.', 'learnpress' ); } /** * Check whether a post status is public. * * @param string $status * * @return bool */ protected function is_public_post_status( string $status ): bool { $status_object = get_post_status_object( $status ); return (bool) ( $status_object && ! empty( $status_object->public ) ); } /** * Normalize target course status based on role/capability and current status. * Mirrors wp-admin behavior for instructors without publish capability. * * @param string $requested_status * @param bool $insert * @param CourseModel|null $courseModel * * @return string */ protected function normalize_course_status_for_save( string $requested_status, bool $insert, ?CourseModel $courseModel ): string { $allowed_statuses = [ 'publish', 'future', 'draft', 'pending', 'private' ]; if ( ! in_array( $requested_status, $allowed_statuses, true ) ) { $requested_status = 'draft'; } $is_admin_user = current_user_can( ADMIN_ROLE ); if ( $is_admin_user ) { return $requested_status; } $is_instructor_user = current_user_can( LP_TEACHER_ROLE ); $required_review = LP_Settings::get_option( 'required_review', 'yes' ) === 'yes'; $can_publish_course = current_user_can( 'publish_' . LP_COURSE_CPT . 's' ); $current_status = $insert ? 'auto-draft' : (string) ( $courseModel->post_status ?? 'draft' ); $is_public_state = $this->is_public_post_status( $current_status ); // Require moderation for instructors when review is enabled. if ( $is_instructor_user && $required_review && ! $is_public_state && in_array( $requested_status, [ 'publish', 'private', 'future' ], true ) ) { return 'pending'; } // Keep wp-admin behavior: users without publish capability submit for review. if ( ! $can_publish_course && ! $is_public_state && in_array( $requested_status, [ 'publish', 'private', 'future' ], true ) ) { return 'pending'; } return $requested_status; } /** * Normalize publish date from Course Builder UI datetime value. * * @param string $datetime_value * * @return array|null */ protected function normalize_course_post_date_for_save( string $datetime_value ): ?array { $datetime_value = trim( $datetime_value ); if ( '' === $datetime_value ) { return null; } $timezone = wp_timezone(); $formats = [ 'Y-m-d\TH:i', 'Y-m-d\TH:i:s', 'Y-m-d H:i:s' ]; $parsed = null; foreach ( $formats as $format ) { $date_candidate = DateTimeImmutable::createFromFormat( $format, $datetime_value, $timezone ); if ( ! $date_candidate instanceof DateTimeImmutable ) { continue; } $errors = DateTimeImmutable::getLastErrors(); if ( is_array( $errors ) && ( ! empty( $errors['warning_count'] ) || ! empty( $errors['error_count'] ) ) ) { continue; } $parsed = $date_candidate; break; } if ( ! $parsed instanceof DateTimeImmutable ) { return null; } $gmt_timezone = new DateTimeZone( 'UTC' ); return [ 'post_date' => $parsed->format( 'Y-m-d H:i:s' ), 'post_date_gmt' => $parsed->setTimezone( $gmt_timezone )->format( 'Y-m-d H:i:s' ), ]; } /** * Save Course. * * @since 4.3 * @version 1.0.1 */ public function save_courses() { $response = new LP_REST_Response(); $response->data = new stdClass(); try { $data = self::check_valid_course(); $course_id = $data['course_id'] ?? 0; $settings = $data['course_settings'] ?? false; $insert = $data['insert']; $course_title = isset( $data['course_title'] ) ? trim( sanitize_text_field( wp_unslash( (string) $data['course_title'] ) ) ) : ''; $course_status_requested = ! empty( $data['course_status'] ) ? sanitize_text_field( $data['course_status'] ) : 'publish'; $course_visibility = ! empty( $data['course_visibility'] ) ? sanitize_key( $data['course_visibility'] ) : ''; $course_password_raw = isset( $data['course_password'] ) ? wp_unslash( (string) $data['course_password'] ) : ''; $course_password = sanitize_text_field( $course_password_raw ); $course_post_date_raw = ! empty( $data['course_post_date'] ) ? sanitize_text_field( $data['course_post_date'] ) : ''; $course_post_date_data = $this->normalize_course_post_date_for_save( $course_post_date_raw ); $post_password_to_update = null; if ( '' === $course_title ) { throw new Exception( __( 'Course title is required.', 'learnpress' ) ); } if ( $insert ) { // Check user capability before insert if ( ! current_user_can( 'edit_lp_courses' ) ) { throw new Exception( __( 'You are not allowed to create courses', 'learnpress' ) ); } $course_status = $this->normalize_course_status_for_save( $course_status_requested, true, null ); $categories = ! empty( $data['course_categories'] ) ? array_map( 'absint', explode( ',', $data['course_categories'] ) ) : array(); $tags = ! empty( $data['course_tags'] ) ? array_map( 'absint', explode( ',', $data['course_tags'] ) ) : array(); $insert_post_data = array( 'post_type' => LP_COURSE_CPT, 'post_title' => $course_title, 'post_content' => Template::sanitize_html_content( $data['course_description'] ?? '' ), 'post_status' => $course_status, 'tax_input' => array( 'course_category' => $categories, 'course_tag' => $tags, ), ); if ( ! empty( $course_post_date_data['post_date'] ) ) { $insert_post_data['post_date'] = $course_post_date_data['post_date']; $insert_post_data['post_date_gmt'] = $course_post_date_data['post_date_gmt']; } if ( in_array( $course_visibility, [ 'public', 'private' ], true ) ) { $insert_post_data['post_password'] = ''; } elseif ( 'password' === $course_visibility ) { if ( '' === $course_password ) { throw new Exception( __( 'Password is required for password protected visibility.', 'learnpress' ) ); } $insert_post_data['post_password'] = $course_password; } $course_id = wp_insert_post( $insert_post_data, true ); if ( is_wp_error( $course_id ) ) { throw new Exception( $course_id->get_error_message() ); } // Load the newly created course as CourseModel $courseModel = CourseModel::find( $course_id, true ); if ( ! $courseModel ) { throw new Exception( __( 'Failed to load course model', 'learnpress' ) ); } } else { $courseModel = $data['course_model']; $course_status = $this->normalize_course_status_for_save( $course_status_requested, false, $courseModel ); $co_instructor_ids = $courseModel->get_meta_value_by_key( '_lp_co_teacher', [] ); if ( absint( $courseModel->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) && ! in_array( get_current_user_id(), $co_instructor_ids ) ) { throw new Exception( __( 'You are not allowed to update this course', 'learnpress' ) ); } $courseModel->post_status = $course_status; if ( ! empty( $course_post_date_data['post_date'] ) ) { $courseModel->post_date = $course_post_date_data['post_date']; $courseModel->post_date_gmt = $course_post_date_data['post_date_gmt']; } if ( in_array( $course_visibility, [ 'public', 'private' ], true ) ) { $post_password_to_update = ''; } elseif ( 'password' === $course_visibility ) { if ( '' !== $course_password ) { $post_password_to_update = $course_password; } else { $existing_post = get_post( $courseModel->ID ); if ( $existing_post && ! empty( $existing_post->post_password ) ) { $post_password_to_update = (string) $existing_post->post_password; } else { throw new Exception( __( 'Password is required for password protected visibility.', 'learnpress' ) ); } } } if ( '' !== $course_title ) { $categories = ! empty( $data['course_categories'] ) ? array_map( 'absint', explode( ',', $data['course_categories'] ) ) : array(); $tags = ! empty( $data['course_tags'] ) ? array_map( 'absint', explode( ',', $data['course_tags'] ) ) : array(); $courseModel->post_title = $course_title; $courseModel->post_content = Template::sanitize_html_content( $data['course_description'] ?? '' ); wp_set_post_terms( $courseModel->ID, $categories, 'course_category' ); wp_set_post_terms( $courseModel->ID, $tags, 'course_tag' ); } // Update permalink/slug if provided if ( ! empty( $data['course_permalink'] ) ) { $new_slug = sanitize_title( $data['course_permalink'] ); if ( $new_slug && $new_slug !== $courseModel->post_name ) { wp_update_post( array( 'ID' => $courseModel->ID, 'post_name' => $new_slug, ) ); $courseModel->post_name = $new_slug; } } $course_id = $courseModel->ID; } if ( $settings ) { $this->save_course_settings_to_model( $courseModel, $data ); } if ( ! empty( $courseModel->meta_data ) ) { $coursePostModel = new CoursePostModel( $courseModel ); foreach ( $courseModel->meta_data as $meta_key => $meta_value ) { $coursePostModel->save_meta_value_by_key( $meta_key, $meta_value ); } $coursePostModel->save(); } // Handle course thumbnail - AFTER coursePostModel->save() to avoid being overwritten if ( isset( $data['course_thumbnail_id'] ) ) { $thumbnail_id = absint( $data['course_thumbnail_id'] ); if ( $thumbnail_id > 0 ) { $result = set_post_thumbnail( $course_id, $thumbnail_id ); } else { $result = delete_post_thumbnail( $course_id ); } $current_thumbnail = get_post_thumbnail_id( $course_id ); } if ( null !== $post_password_to_update ) { wp_update_post( array( 'ID' => $course_id, 'post_password' => $post_password_to_update, ) ); } // Use persisted status after save to avoid UI drift when WP normalizes status by capability. $saved_course_status = get_post_status( $course_id ); if ( ! is_string( $saved_course_status ) || '' === $saved_course_status ) { $saved_course_status = $course_status; } $response->status = 'success'; $response->message = $insert ? __( 'Insert course successfully!', 'learnpress' ) : __( 'Update course successfully!', 'learnpress' ); $response->data->status = $saved_course_status; $response->data->button_title = 'publish' === $saved_course_status ? __( 'Update', 'learnpress' ) : ( 'pending' === $saved_course_status ? __( 'Submit for Review', 'learnpress' ) : __( 'Publish', 'learnpress' ) ); $response->data->course_id_new = $insert ? $course_id : ''; // Return the actual saved permalink data (important if WordPress auto-generated a unique slug) $saved_post = get_post( $course_id ); if ( $saved_post ) { $visibility = 'public'; if ( 'private' === $saved_post->post_status ) { $visibility = 'private'; } elseif ( ! empty( $saved_post->post_password ) ) { $visibility = 'password'; } $response->data->visibility = $visibility; $response->data->course_slug = $saved_post->post_name; $response->data->course_permalink = get_permalink( $course_id ); } // Return full redirect URL for new courses if ( $insert && $course_id ) { $response->data->redirect_url = CourseBuilder::get_link_course_builder( "courses/{$course_id}" ); } wp_send_json( $response ); } catch ( Throwable $th ) { $response->message = $th->getMessage(); wp_send_json( $response ); } } /** * Save Course Settings only (from Settings tab). * * @since 4.3 * @version 1.0.0 */ public function save_course_settings() { $response = new LP_REST_Response(); $response->data = new stdClass(); try { $data = self::check_valid_course(); $course_id = $data['course_id'] ?? 0; $insert = $data['insert']; $courseModel = $data['course_model']; if ( $insert || empty( $courseModel ) ) { throw new Exception( __( 'Course not found. Please save the course first.', 'learnpress' ) ); } $co_instructor_ids = $courseModel->get_meta_value_by_key( '_lp_co_teacher', [] ); if ( absint( $courseModel->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) && ! in_array( get_current_user_id(), $co_instructor_ids ) ) { throw new Exception( __( 'You are not allowed to update this course', 'learnpress' ) ); } // Save course settings $this->save_course_settings_to_model( $courseModel, $data ); // Save meta data if ( ! empty( $courseModel->meta_data ) ) { $coursePostModel = new CoursePostModel( $courseModel ); foreach ( $courseModel->meta_data as $meta_key => $meta_value ) { $coursePostModel->save_meta_value_by_key( $meta_key, $meta_value ); } $coursePostModel->save(); } $response->status = 'success'; $response->message = __( 'Settings saved successfully!', 'learnpress' ); wp_send_json( $response ); } catch ( Throwable $th ) { $response->status = 'error'; $response->message = $th->getMessage(); wp_send_json( $response ); } } /** * Save all course settings to CourseModel * * @param CoursePostModel $courseModel * @param array $data * * @throws Exception */ public function save_course_settings_to_model( CoursePostModel &$courseModel, array $data ) { // General settings $this->save_general_settings_to_model( $courseModel, $data ); // Offline course settings $this->save_offline_settings_to_model( $courseModel, $data ); // Price settings $this->save_price_settings_to_model( $courseModel, $data ); // Extra info settings $this->save_extra_settings_to_model( $courseModel, $data ); // Assessment settings $this->save_assessment_settings_to_model( $courseModel, $data ); // Author settings $this->save_author_settings_to_model( $courseModel, $data ); /** * Allow addons to persist their own course settings fields added to the * settings metabox tabs (e.g. Co-Instructor) when saving from Course Builder. * * @param CoursePostModel $courseModel Course model being saved. * @param array $data Submitted settings data. * * @since 4.3.8 */ do_action( 'learn-press/course-builder/save-course-settings', $courseModel, $data ); } /** * Save general settings to CourseModel * * @param CoursePostModel $courseModel * @param array $data */ protected function save_general_settings_to_model( CoursePostModel &$courseModel, array $data ) { if ( isset( $data['_lp_duration'] ) ) { $duration_value = ! empty( $data['_lp_duration'] ) ? str_replace( ',', ' ', $data['_lp_duration'] ) : '0 minute'; $explode = explode( ' ', $duration_value ); $number = (float) $explode[0] < 0 ? 0 : absint( $explode[0] ); $unit = $explode[1] ?? 'minute'; $courseModel->meta_data->{CoursePostModel::META_KEY_DURATION} = $number . ' ' . $unit; } $checkbox_fields = [ '_lp_block_expire_duration' => CoursePostModel::META_KEY_BLOCK_EXPIRE_DURATION, '_lp_block_finished' => CoursePostModel::META_KEY_BLOCK_FINISH, '_lp_allow_course_repurchase' => CoursePostModel::META_KEY_ALLOW_COURSE_REPURCHASE, '_lp_has_finish' => CoursePostModel::META_KEY_HAS_FINISH, '_lp_featured' => CoursePostModel::META_KEY_FEATURED, ]; foreach ( $checkbox_fields as $key => $meta_key ) { if ( isset( $data[ $key ] ) ) { $courseModel->meta_data->{$meta_key} = $data[ $key ] === 'yes' ? 'yes' : ''; } } $simple_fields = [ '_lp_course_repurchase_option' => CoursePostModel::META_KEY_COURSE_REPURCHASE_OPTION, '_lp_level' => CoursePostModel::META_KEY_LEVEL, '_lp_featured_review' => CoursePostModel::META_KEY_FEATURED_REVIEW, '_lp_external_link_buy_course' => CoursePostModel::META_KEY_EXTERNAL_LINK_BY_COURSE, ]; foreach ( $simple_fields as $key => $meta_key ) { if ( isset( $data[ $key ] ) ) { $courseModel->meta_data->{$meta_key} = sanitize_text_field( $data[ $key ] ); } } $numeric_fields = [ '_lp_students' => CoursePostModel::META_KEY_STUDENTS, '_lp_max_students' => CoursePostModel::META_KEY_MAX_STUDENTS, '_lp_retake_count' => CoursePostModel::META_KEY_RETAKE_COUNT, ]; foreach ( $numeric_fields as $key => $meta_key ) { if ( isset( $data[ $key ] ) ) { $value = absint( $data[ $key ] ); $courseModel->meta_data->{$meta_key} = $value; } } } /** * Save offline course settings to CourseModel * * @param CoursePostModel $courseModel * @param array $data */ protected function save_offline_settings_to_model( CoursePostModel &$courseModel, array $data ) { if ( isset( $data['_lp_offline_course'] ) ) { $courseModel->meta_data->{CoursePostModel::META_KEY_OFFLINE_COURSE} = $data['_lp_offline_course'] === 'yes' ? 'yes' : ''; } if ( isset( $data['_lp_offline_lesson_count'] ) ) { $courseModel->meta_data->{CoursePostModel::META_KEY_OFFLINE_LESSON_COUNT} = absint( $data['_lp_offline_lesson_count'] ); } if ( isset( $data['_lp_deliver_type'] ) ) { $courseModel->meta_data->{CoursePostModel::META_KEY_DELIVER} = sanitize_text_field( $data['_lp_deliver_type'] ); } if ( isset( $data['_lp_address'] ) ) { $courseModel->meta_data->{CoursePostModel::META_KEY_ADDRESS} = sanitize_text_field( $data['_lp_address'] ); } } /** * Save price settings to CourseModel * * @param CoursePostModel $courseModel * @param array $data */ protected function save_price_settings_to_model( CoursePostModel &$courseModel, array $data ) { // Regular price if ( isset( $data['_lp_regular_price'] ) ) { $regular_price = floatval( $data['_lp_regular_price'] ); if ( $regular_price < 0 ) { $regular_price = ''; } $courseModel->meta_data->{CoursePostModel::META_KEY_REGULAR_PRICE} = $regular_price; } // Sale price if ( isset( $data['_lp_sale_price'] ) ) { $sale_price = $data['_lp_sale_price'] !== '' ? floatval( $data['_lp_sale_price'] ) : ''; $regular_price = $courseModel->get_regular_price(); if ( $sale_price !== '' && $sale_price > $regular_price ) { $sale_price = ''; } $courseModel->meta_data->{CoursePostModel::META_KEY_SALE_PRICE} = $sale_price; } // Sale dates if ( isset( $data['_lp_sale_start'] ) ) { $courseModel->meta_data->{CoursePostModel::META_KEY_SALE_START} = sanitize_text_field( $data['_lp_sale_start'] ); } if ( isset( $data['_lp_sale_end'] ) ) { $courseModel->meta_data->{CoursePostModel::META_KEY_SALE_END} = sanitize_text_field( $data['_lp_sale_end'] ); } // Price prefix/suffix if ( isset( $data['_lp_price_prefix'] ) ) { $courseModel->meta_data->{CoursePostModel::META_KEY_PRICE_PREFIX} = sanitize_text_field( $data['_lp_price_prefix'] ); } if ( isset( $data['_lp_price_suffix'] ) ) { $courseModel->meta_data->{CoursePostModel::META_KEY_PRICE_SUFFIX} = sanitize_text_field( $data['_lp_price_suffix'] ); } // No required enroll if ( isset( $data['_lp_no_required_enroll'] ) ) { $courseModel->meta_data->{CoursePostModel::META_KEY_NO_REQUIRED_ENROLL} = $data['_lp_no_required_enroll'] === 'yes' ? 'yes' : ''; } } /** * Normalize an extra-info field value to an array. * * The Course Builder sends these fields as arrays (one entry per input); * older paths may send a comma-separated string. Splitting on comma must * never be applied to array input, or values that legitimately contain a * comma get broken into multiple entries. * * @param mixed $value * @return array */ protected function extra_field_to_array( $value ): array { if ( is_array( $value ) ) { return $value; } return $value !== '' ? explode( ',', (string) $value ) : []; } /** * Save extra info settings to CourseModel * * @param CoursePostModel $courseModel * @param array $data */ protected function save_extra_settings_to_model( CoursePostModel &$courseModel, array $data ) { // Requirements if ( isset( $data['_lp_requirements'] ) ) { $requirements = array_filter( $this->extra_field_to_array( $data['_lp_requirements'] ), function ( $item ) { return ! is_null( $item ) && $item !== ''; } ); $courseModel->meta_data->{CoursePostModel::META_KEY_REQUIREMENTS} = array_map( 'sanitize_text_field', array_values( $requirements ) ); } if ( isset( $data['_lp_target_audiences'] ) ) { $target_audiences = array_filter( $this->extra_field_to_array( $data['_lp_target_audiences'] ), function ( $item ) { return ! is_null( $item ) && $item !== ''; } ); $courseModel->meta_data->{CoursePostModel::META_KEY_TARGET} = array_map( 'sanitize_text_field', array_values( $target_audiences ) ); } if ( isset( $data['_lp_key_features'] ) ) { $key_features = array_filter( $this->extra_field_to_array( $data['_lp_key_features'] ), function ( $item ) { return ! is_null( $item ) && $item !== ''; } ); $courseModel->meta_data->{CoursePostModel::META_KEY_FEATURES} = array_map( 'sanitize_text_field', array_values( $key_features ) ); } // FAQs if ( isset( $data['_lp_faqs_question'] ) ) { $questions = $this->extra_field_to_array( $data['_lp_faqs_question'] ); $answers = ! empty( $data['_lp_faqs_answer'] ) ? $this->extra_field_to_array( $data['_lp_faqs_answer'] ) : []; $faqs = []; if ( ! empty( $questions ) ) { foreach ( $questions as $index => $question ) { $clean_question = trim( $question ); if ( ! empty( $clean_question ) ) { $answer_content = $answers[ $index ] ?? ''; $faqs[] = [ sanitize_text_field( $clean_question ), wp_kses_post( $answer_content ) ]; } } } $courseModel->meta_data->{CoursePostModel::META_KEY_FAQS} = $faqs; } } /** * Save assessment settings to CourseModel * * @param CoursePostModel $courseModel * @param array $data */ protected function save_assessment_settings_to_model( CoursePostModel &$courseModel, array $data ) { // Course result evaluation type if ( isset( $data['_lp_course_result'] ) ) { $courseModel->meta_data->{CoursePostModel::META_KEY_EVALUATION_TYPE} = sanitize_text_field( $data['_lp_course_result'] ); } // Passing condition if ( isset( $data['_lp_passing_condition'] ) ) { $passing_condition = floatval( $data['_lp_passing_condition'] ); if ( $passing_condition < 0 ) { $passing_condition = 0; } elseif ( $passing_condition > 100 ) { $passing_condition = 100; } $courseModel->meta_data->{CoursePostModel::META_KEY_PASSING_CONDITION} = $passing_condition; } } /** * Save author settings to CourseModel * * @param CoursePostModel $courseModel * @param array $data */ protected function save_author_settings_to_model( CoursePostModel &$courseModel, array $data ) { if ( ! isset( $data['_post_author'] ) ) { return; } $new_author_id = absint( $data['_post_author'] ); if ( $new_author_id <= 0 || $new_author_id === (int) $courseModel->post_author ) { return; } $courseModel->meta_data->_post_author = $new_author_id; $courseModel->post_author = $new_author_id; } /** * Duplicate for course. * */ public function duplicate_course() { $response = new LP_REST_Response(); $response->data = new stdClass(); try { $data = self::check_valid_course(); $course_id = $data['course_id'] ?? 0; $courseModel = $data['course_model']; if ( absint( $courseModel->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) { throw new Exception( __( 'You are not allowed to duplicate this course', 'learnpress' ) ); } if ( ! function_exists( 'learn_press_duplicate_post' ) ) { require_once LP_PLUGIN_PATH . 'inc/admin/lp-admin-functions.php'; } $curd = new LP_Course_CURD(); $new_item_id = $curd->duplicate( $course_id, array( 'exclude_meta' => array( 'order-pending', 'order-processing', 'order-completed', 'order-cancelled', 'order-failed', 'count_enrolled_users', '_lp_sample_data', '_lp_retake_count', ), ) ); if ( is_wp_error( $new_item_id ) ) { throw new Exception( $new_item_id->get_error_message() ); } $courseModel = CourseModel::find( $new_item_id, true ); $html = BuilderListCoursesTemplate::render_course( $courseModel ); $response->status = 'success'; $response->data->html = $html; $response->data->course_id_new = $new_item_id; $response->data->redirect_url = CourseBuilder::get_link_course_builder( "courses/{$new_item_id}" ); $response->message = __( 'Course duplicated successfully', 'learnpress' ); wp_send_json( $response ); } catch ( Throwable $th ) { $response->status = 'error'; $response->message = $th->getMessage(); wp_send_json( $response ); } } public function move_trash_course() { $response = new LP_REST_Response(); $response->data = new stdClass(); try { $data = self::check_valid_course(); $course_id = $data['course_id'] ?? 0; $courseModel = $data['course_model']; $status = $data['status'] ?? 'trash'; $co_instructor_ids = get_post_meta( $course_id, '_lp_co_teacher', false ); $co_instructor_ids = ! empty( $co_instructor_ids ) ? $co_instructor_ids : array(); if ( absint( $courseModel->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) && ! in_array( get_current_user_id(), $co_instructor_ids ) ) { throw new Exception( __( 'You are not allowed to delete this course', 'learnpress' ) ); } if ( $status === 'delete' ) { if ( ! current_user_can( 'manage_options' ) ) { throw new Exception( __( 'You are not allowed to delete this course', 'learnpress' ) ); } wp_delete_post( $course_id, true ); $message = __( 'Course has been deleted', 'learnpress' ); } elseif ( $status === 'draft' ) { $update = wp_update_post( array( 'ID' => $course_id, 'post_type' => LP_COURSE_CPT, 'post_status' => 'draft', ) ); if ( ! $update ) { throw new Exception( __( 'Course cannot be moved to draft', 'learnpress' ) ); } $message = __( 'Course has been moved to draft', 'learnpress' ); } else { // Store original slug before trashing (wp_trash_post adds __trashed suffix) $original_slug = $courseModel->post_name; $delete = wp_trash_post( $course_id ); if ( ! $delete ) { throw new Exception( __( 'Course cannot be moved to trash', 'learnpress' ) ); } // Restore original slug after trashing if ( $original_slug ) { wp_update_post( array( 'ID' => $course_id, 'post_name' => $original_slug, ) ); } $message = __( 'Course moved to trash', 'learnpress' ); } $response->status = 'success'; $response->data->button_title = __( 'Publish', 'learnpress' ); $response->data->status = $status; $response->message = $message; wp_send_json( $response ); } catch ( Throwable $th ) { $response->status = 'error'; $response->message = $th->getMessage(); wp_send_json( $response ); } } public function add_course_category() { $response = new LP_REST_Response(); $response->data = new stdClass(); try { $data = self::check_valid_course(); if ( ! current_user_can( 'edit_lp_courses' ) ) { throw new Exception( __( 'You are not allowed to create categories', 'learnpress' ) ); } $name = sanitize_text_field( $data['name'] ?? '' ); $parent = isset( $data['parent'] ) ? (int) $data['parent'] : 0; if ( $parent < 0 ) { $parent = 0; } $term = wp_insert_term( $name, 'course_category', array( 'parent' => $parent ) ); if ( is_wp_error( $term ) ) { throw new Exception( $term->get_error_message() ); } $term_id = $term['term_id']; $html = sprintf( '', esc_attr( $term_id ), esc_html( $name ) ); $response->status = 'success'; $response->data->html = $html; $response->data->term_id = $term_id; $response->data->parent = $parent; $response->message = __( 'Insert category successfully!', 'learnpress' ); wp_send_json( $response ); } catch ( Throwable $th ) { $response->status = 'error'; $response->message = $th->getMessage(); wp_send_json( $response ); } } public function add_course_tag() { $response = new LP_REST_Response(); $response->data = new stdClass(); try { $data = self::check_valid_course(); if ( ! current_user_can( 'edit_lp_courses' ) ) { throw new Exception( __( 'You are not allowed to create tags', 'learnpress' ) ); } $name = sanitize_text_field( wp_unslash( $data['name'] ?? '' ) ); $term = wp_insert_term( $name, 'course_tag', array() ); if ( is_wp_error( $term ) ) { throw new Exception( $term->get_error_message() ); } $html = BuilderEditCourseTemplate::instance()->input_checkbox_tag_item( $term['term_id'], $name, false ); $response->status = 'success'; $response->data->html = $html; $response->message = __( 'Insert term successfully!', 'learnpress' ); wp_send_json( $response ); } catch ( Throwable $th ) { $response->status = 'error'; $response->message = $th->getMessage(); wp_send_json( $response ); } } /** * Duplicate for lesson. * */ public function duplicate_lesson() { $response = new LP_REST_Response(); $response->data = new stdClass(); try { $data = self::check_valid_lesson(); $lesson_id = $data['lesson_id'] ?? 0; $lesson_model = $data['lesson_model']; if ( absint( $lesson_model->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) { throw new Exception( __( 'You are not allowed to duplicate this lesson', 'learnpress' ) ); } if ( ! function_exists( 'learn_press_duplicate_post' ) ) { require_once LP_PLUGIN_PATH . 'inc/admin/lp-admin-functions.php'; } $duplicate_args = apply_filters( 'learn-press/duplicate-post-args', array( 'post_status' => 'publish' ) ); $curd = new LP_Lesson_CURD(); $new_item_id = $curd->duplicate( $lesson_id, $duplicate_args ); if ( is_wp_error( $new_item_id ) ) { throw new Exception( $new_item_id->get_error_message() ); } $lesson_model_new = LessonPostModel::find( $new_item_id, true ); $html = BuilderListLessonsTemplate::render_lesson( $lesson_model_new ); $response->status = 'success'; $response->data->html = $html; $response->message = __( 'Lesson duplicated successfully', 'learnpress' ); wp_send_json( $response ); } catch ( Throwable $th ) { $response->status = 'error'; $response->message = $th->getMessage(); wp_send_json( $response ); } } /** * Update Lesson from Course Builder. * Supports partial updates (title only, description only, or settings only). * * @since 4.3 * @version 1.0.3 */ public function builder_update_lesson() { $response = new Response(); $response->data = new stdClass(); try { $data = self::check_valid_lesson(); $lesson_id = $data['lesson_id'] ?? 0; $title = LP_Helper::sanitize_params_submitted( $data['lesson_title'] ?? '' ); $description = LP_Helper::sanitize_params_submitted( $data['lesson_description'] ?? '', 'html' ); $settings = $data['lesson_settings'] ?? false; $is_elementor = $data['is_elementor'] ?? false; $return_html = ( $data['return_html'] ?? 'no' ) === 'yes'; $insert = $data['insert']; $lesson_slug = LP_Helper::sanitize_params_submitted( $data['lesson_permalink'] ?? '', 'key' ); $course_id = absint( $data['course_id'] ?? 0 ); // Determine target status (draft or publish) $target_status = LP_Helper::sanitize_params_submitted( $data['lesson_status'] ?? '', 'key' ); $restore_with_custom_slug = false; if ( empty( $title ) ) { throw new Exception( __( 'Lesson title is required', 'learnpress' ) ); } if ( $insert ) { $insert_arg = array( 'post_title' => $title, 'post_content' => $description, 'post_status' => $target_status, 'post_author' => get_current_user_id(), ); $lessonPostModelNew = new LessonPostModel( $insert_arg ); $lessonPostModelNew->check_capabilities_create_item_course(); $lessonPostModelNew->save(); $lesson_id = $lessonPostModelNew->ID; $lesson_model = $lessonPostModelNew; $response->message = __( 'Create lesson successfully', 'learnpress' ); } else { $lesson_model = $data['lesson_model'] ?? null; if ( ! $lesson_model instanceof LessonPostModel ) { throw new Exception( __( 'Lesson not found', 'learnpress' ) ); } $lesson_model->check_capabilities_update_item_course(); if ( ! $course_id ) { $course_id = absint( $this->get_course_by_item_id( $lesson_id ) ); } // Support for co-instructor. /*$co_instructor_ids = get_post_meta( $course_id, '_lp_co_teacher', false ); $co_instructor_ids = ! empty( $co_instructor_ids ) ? $co_instructor_ids : array(); if ( absint( $lesson_model->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) && ! in_array( get_current_user_id(), $co_instructor_ids ) ) { throw new Exception( __( 'You are not allowed to update this lesson', 'learnpress' ) ); }*/ /*$update_arg = array( 'ID' => $lesson_id, 'post_type' => LP_LESSON_CPT, 'post_status' => $target_status, );*/ if ( defined( 'ELEMENTOR_VERSION' ) ) { \Elementor\Plugin::$instance->documents->get( $lesson_id )->set_is_built_with_elementor( ! empty( $is_elementor ) ); } $lesson_model->post_title = $title; $lesson_model->post_content = $description; $lesson_model->post_status = $target_status; if ( ! empty( $lesson_slug ) ) { $lesson_model->post_name = $lesson_slug; } //$restore_with_custom_slug = $this->prepare_desired_slug_for_restore( $lesson_id, $target_status, $lesson_slug ); //$update = wp_update_post( $update_arg ); $lesson_model->save(); /*if ( $restore_with_custom_slug ) { $this->sync_slug_after_restore( $lesson_id, $lesson_slug ); }*/ if ( $course_id ) { $courseModelCache = CourseModel::find( $course_id, true ); if ( $courseModelCache ) { $courseModelCache->sections_items = null; $courseModelCache->save(); } } $response->message = __( 'Update lesson successfully', 'learnpress' ); } if ( $settings ) { $this->save_lesson_settings_to_model( $lesson_model, $data ); } // Remove lesson from curriculum if status is not public if ( $target_status !== 'publish' ) { $this->remove_course_item_from_curriculum( $lesson_id, $course_id ); } $response->status = 'success'; $response->data->status = $lesson_model->post_status; $response->data->button_title = $lesson_model->post_status === 'publish' ? __( 'Update', 'learnpress' ) : __( 'Publish', 'learnpress' ); $response->data->lesson_id_new = $insert ? $lesson_id : ''; $response->message = $target_status === 'draft' ? esc_html__( 'Lesson saved as draft', 'learnpress' ) : ( $insert ? esc_html__( 'Insert lesson successfully', 'learnpress' ) : esc_html__( 'Update lesson successfully', 'learnpress' ) ); $response->data->lesson_slug = $lesson_model->post_name; $course_id_of_item = $this->get_course_by_item_id( $lesson_id ); $response->data->permalink_available = false; $response->data->permalink_notice = $this->get_item_permalink_unavailable_message(); if ( 'publish' === $lesson_model->post_status && $course_id_of_item ) { $course = learn_press_get_course( $course_id_of_item ); if ( $course ) { $response->data->permalink_available = true; $response->data->lesson_permalink = urldecode( $course->get_item_link( $lesson_id ) ); } } $lesson_model_for_html = LessonPostModel::find( $lesson_id, true ); if ( $return_html ) { $response->data->list_item_html = $lesson_model_for_html ? BuilderListLessonsTemplate::render_lesson( $lesson_model_for_html ) : ''; if ( $course_id ) { $courseModelForHtml = CourseModel::find( $course_id, true ); if ( $courseModelForHtml && $lesson_model_for_html ) { $item = new stdClass(); $item->item_id = $lesson_id; $item->title = $lesson_model_for_html->post_title; $item->item_type = LP_LESSON_CPT; AdminEditCurriculumTemplate::instance()->context_data = [ 'is_course_builder' => true ]; $response->data->section_item_html = AdminEditCurriculumTemplate::instance()->html_section_item( $courseModelForHtml, $item ); } } } $response->status = Response::STATUS_SUCCESS; } catch ( Throwable $e ) { $response->message = $e->getMessage(); } wp_send_json( $response ); } public function move_trash_lesson() { $response = new LP_REST_Response(); $response->data = new stdClass(); try { $data = self::check_valid_lesson(); $lesson_id = $data['lesson_id'] ?? 0; $status = $data['status'] ?? 'trash'; $lesson_model = $data['lesson_model'] ?? []; $lesson_slug = ! empty( $data['lesson_permalink'] ) ? sanitize_title( wp_unslash( (string) $data['lesson_permalink'] ) ) : ''; if ( ! $lesson_model ) { throw new Exception( __( 'Lesson not found', 'learnpress' ) ); } if ( absint( $lesson_model->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) { throw new Exception( __( 'You are not allowed to delete this lesson', 'learnpress' ) ); } $course_id = absint( $data['course_id'] ?? 0 ); if ( ! $course_id ) { $course_id = absint( $this->get_course_by_item_id( $lesson_id ) ); } if ( $status === 'trash' ) { $original_slug = (string) get_post_field( 'post_name', $lesson_id ); $move_trash = wp_trash_post( $lesson_id ); if ( is_wp_error( $move_trash ) ) { throw new Exception( esc_html__( 'Cannot move this lesson to trash', 'learnpress' ) ); } if ( '' !== $original_slug ) { wp_update_post( [ 'ID' => $lesson_id, 'post_name' => $original_slug, ] ); } $message = esc_html__( 'This lesson has been moved to trash.', 'learnpress' ); } elseif ( $status === 'delete' ) { if ( $lesson_model->post_status !== 'trash' ) { throw new Exception( esc_html__( 'Lesson must be trashed before deleting.', 'learnpress' ) ); } $delete = wp_delete_post( $lesson_id ); if ( is_wp_error( $delete ) ) { throw new Exception( esc_html__( 'Cannot delete this lesson.', 'learnpress' ) ); } $message = esc_html__( 'Delete this lesson successfully', 'learnpress' ); } elseif ( in_array( $status, [ 'publish', 'draft' ], true ) ) { $restore_with_custom_slug = $this->prepare_desired_slug_for_restore( $lesson_id, $status, $lesson_slug ); $update_args = [ 'ID' => $lesson_id, 'post_type' => LP_LESSON_CPT, 'post_status' => $status, ]; if ( '' !== $lesson_slug ) { $update_args['post_name'] = $lesson_slug; } $update = wp_update_post( $update_args ); if ( ! $update ) { if ( 'draft' === $status ) { throw new Exception( __( 'Lesson cannot be moved to draft', 'learnpress' ) ); } throw new Exception( __( 'Lesson cannot be moved to publish', 'learnpress' ) ); } if ( $restore_with_custom_slug ) { $this->sync_slug_after_restore( $lesson_id, $lesson_slug ); } $message = 'draft' === $status ? __( 'Lesson has been moved to draft', 'learnpress' ) : __( 'Lesson has been moved to publish', 'learnpress' ); } else { throw new Exception( __( 'Invalid lesson status transition', 'learnpress' ) ); } $saved_lesson_status = get_post_status( $lesson_id ); if ( ! is_string( $saved_lesson_status ) || '' === $saved_lesson_status ) { $saved_lesson_status = $status; } if ( $saved_lesson_status !== 'publish' ) { $this->remove_course_item_from_curriculum( $lesson_id, $course_id ); } $response->data->status = $saved_lesson_status; $response->data->button_title = 'publish' === $saved_lesson_status ? __( 'Update', 'learnpress' ) : __( 'Publish', 'learnpress' ); $response->data->lesson_slug = (string) get_post_field( 'post_name', $lesson_id ); $response->data->permalink_available = false; $response->data->permalink_notice = $this->get_item_permalink_unavailable_message(); $course_id_of_item = $this->get_course_by_item_id( $lesson_id ); if ( 'publish' === $saved_lesson_status && $course_id_of_item ) { $course = learn_press_get_course( $course_id_of_item ); if ( $course ) { $response->data->permalink_available = true; $response->data->lesson_permalink = urldecode( $course->get_item_link( $lesson_id ) ); } } if ( 'delete' !== $status ) { $lesson_model_new = LessonPostModel::find( $lesson_id, true ); $response->data->html = $lesson_model_new ? BuilderListLessonsTemplate::render_lesson( $lesson_model_new ) : ''; } $response->status = 'success'; $response->message = $message; wp_send_json( $response ); } catch ( Throwable $th ) { $response->status = 'error'; $response->message = $th->getMessage(); wp_send_json( $response ); } } /** * Save Lesson Settings */ protected function save_lesson_settings_to_model( LessonPostModel $lessonModel, array $data ) { if ( isset( $data['_lp_duration'] ) ) { $duration = ! empty( $data['_lp_duration'] ) ? str_replace( ',', ' ', $data['_lp_duration'] ) : '0 minute'; $explode = explode( ' ', $duration ); $number = (float) $explode[0] < 0 ? 0 : absint( $explode[0] ); $unit = $explode[1] ?? 'minute'; $lessonModel->save_meta_value_by_key( '_lp_duration', $number . ' ' . $unit ); } if ( isset( $data['_lp_preview'] ) ) { $enable = $data['_lp_preview'] === 'yes'; $lessonModel->set_preview( $enable ); } } /** * Save Quiz Settings to QuizPostModel * * @param QuizPostModel $quizModel * @param array $data * * @since 4.3 * @version 1.0.0 */ protected function save_quiz_settings_to_model( QuizPostModel $quizModel, array $data ) { if ( isset( $data['_lp_duration'] ) ) { $duration = ! empty( $data['_lp_duration'] ) ? str_replace( ',', ' ', $data['_lp_duration'] ) : '0 minute'; $explode = explode( ' ', $duration ); $number = (float) $explode[0] < 0 ? 0 : absint( $explode[0] ); $unit = $explode[1] ?? 'minute'; $quizModel->save_meta_value_by_key( '_lp_duration', $number . ' ' . $unit ); } $checkbox_keys = [ '_lp_instant_check', '_lp_negative_marking', '_lp_minus_skip_questions', '_lp_review', '_lp_show_correct_review', '_lp_show_check_answer', '_lp_show_hint', ]; foreach ( $checkbox_keys as $key ) { if ( isset( $data[ $key ] ) ) { $value = $data[ $key ] === 'yes' ? 'yes' : 'no'; $quizModel->save_meta_value_by_key( $key, $value ); } } $numeric_keys = [ '_lp_passing_grade', '_lp_retake_count', '_lp_pagination', ]; foreach ( $numeric_keys as $key ) { if ( isset( $data[ $key ] ) ) { $value = absint( $data[ $key ] ); if ( '_lp_passing_grade' === $key && $value > 100 ) { $value = 100; } $quizModel->save_meta_value_by_key( $key, $value ); } } } /** * Update Quiz from Course Builder. * Supports partial updates (title only, description only, or settings only). * * @since 4.3 * @version 1.0.2 */ public function builder_update_quiz() { $response = new Response(); $response->data = new stdClass(); try { $data = self::check_valid_quiz(); $quiz_id = $data['quiz_id'] ?? 0; $title = LP_Helper::sanitize_params_submitted( $data['quiz_title'] ?? '' ); $description = LP_Helper::sanitize_params_submitted( $data['quiz_description'] ?? '', 'html' ); $settings = $data['quiz_settings'] ?? false; $is_elementor = $data['is_elementor'] ?? false; $return_html = ( $data['return_html'] ?? 'no' ) === 'yes'; $insert = $data['insert']; $quiz_slug = LP_Helper::sanitize_params_submitted( $data['quiz_permalink'] ?? '', 'key' ); $course_id = absint( $data['course_id'] ?? 0 ); // Determine target status (draft or publish) $target_status = LP_Helper::sanitize_params_submitted( $data['quiz_status'] ?? '', 'key' ); if ( empty( $title ) ) { throw new Exception( __( 'Quiz title is required', 'learnpress' ) ); } if ( $insert ) { $insert_arg = array( 'post_title' => $title, 'post_content' => $description, 'post_status' => $target_status, 'post_author' => get_current_user_id(), ); $quizPostModelNew = new QuizPostModel( $insert_arg ); $quizPostModelNew->check_capabilities_create_item_course(); $quizPostModelNew->save(); $quiz_id = $quizPostModelNew->ID; $quiz_model = $quizPostModelNew; $response->message = __( 'Create quiz successfully', 'learnpress' ); } else { $quiz_model = $data['quiz_model'] ?? null; if ( ! $quiz_model instanceof QuizPostModel ) { throw new Exception( __( 'Quiz not found', 'learnpress' ) ); } $quiz_model->check_capabilities_update_item_course(); if ( ! $course_id ) { $course_id = absint( $this->get_course_by_item_id( $quiz_id ) ); } if ( defined( 'ELEMENTOR_VERSION' ) ) { \Elementor\Plugin::$instance->documents->get( $quiz_id )->set_is_built_with_elementor( ! empty( $is_elementor ) ); } $quiz_model->post_title = $title; $quiz_model->post_content = $description; $quiz_model->post_status = $target_status; if ( ! empty( $quiz_slug ) ) { $quiz_model->post_name = $quiz_slug; } $quiz_model->save(); if ( $course_id ) { $courseModelCache = CourseModel::find( $course_id, true ); if ( $courseModelCache ) { $courseModelCache->sections_items = null; $courseModelCache->save(); } } $response->message = __( 'Update quiz successfully', 'learnpress' ); } if ( $settings ) { $this->save_quiz_settings_to_model( $quiz_model, $data ); } // Remove quiz from curriculum if status is not public if ( $target_status !== 'publish' ) { $this->remove_course_item_from_curriculum( $quiz_id, $course_id ); } $response->status = 'success'; $response->data->status = $quiz_model->post_status; $response->data->button_title = $quiz_model->post_status === 'publish' ? __( 'Update', 'learnpress' ) : __( 'Publish', 'learnpress' ); $response->data->quiz_id_new = $insert ? $quiz_id : ''; if ( $insert && $quiz_id ) { $response->data->redirect_url = CourseBuilder::get_link_course_builder( CourseBuilderTemplate::MENU_QUIZZES . "/{$quiz_id}" ); } $response->message = $target_status === 'draft' ? esc_html__( 'Quiz saved as draft', 'learnpress' ) : ( $insert ? esc_html__( 'Insert quiz successfully', 'learnpress' ) : esc_html__( 'Update quiz successfully', 'learnpress' ) ); $response->data->quiz_slug = $quiz_model->post_name; $course_id_of_item = $this->get_course_by_item_id( $quiz_id ); $response->data->permalink_available = false; $response->data->permalink_notice = $this->get_item_permalink_unavailable_message(); if ( 'publish' === $quiz_model->post_status && $course_id_of_item ) { $course = learn_press_get_course( $course_id_of_item ); if ( $course ) { $response->data->permalink_available = true; $response->data->quiz_permalink = urldecode( $course->get_item_link( $quiz_id ) ); } } $quiz_model_for_html = QuizPostModel::find( $quiz_id, true ); if ( $return_html ) { $response->data->list_item_html = $quiz_model_for_html ? BuilderListQuizzesTemplate::render_quiz( $quiz_model_for_html ) : ''; if ( $course_id ) { $courseModelForHtml = CourseModel::find( $course_id, true ); if ( $courseModelForHtml && $quiz_model_for_html ) { $item = new stdClass(); $item->item_id = $quiz_id; $item->title = $quiz_model_for_html->post_title; $item->item_type = LP_QUIZ_CPT; AdminEditCurriculumTemplate::instance()->context_data = [ 'is_course_builder' => true ]; $response->data->section_item_html = AdminEditCurriculumTemplate::instance()->html_section_item( $courseModelForHtml, $item ); } } } $response->status = Response::STATUS_SUCCESS; } catch ( Throwable $e ) { $response->message = $e->getMessage(); } wp_send_json( $response ); } public function duplicate_quiz() { $response = new LP_REST_Response(); $response->data = new stdClass(); try { $data = self::check_valid_quiz(); $quiz_id = $data['quiz_id'] ?? 0; $quiz_model = $data['quiz_model']; if ( absint( $quiz_model->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) { throw new Exception( __( 'You are not allowed to duplicate this quiz', 'learnpress' ) ); } if ( ! function_exists( 'learn_press_duplicate_post' ) ) { require_once LP_PLUGIN_PATH . 'inc/admin/lp-admin-functions.php'; } $duplicate_args = apply_filters( 'learn-press/duplicate-post-args', array( 'post_status' => 'publish' ) ); $curd = new LP_Quiz_CURD(); $new_item_id = $curd->duplicate( $quiz_id, $duplicate_args ); if ( is_wp_error( $new_item_id ) ) { throw new Exception( $new_item_id->get_error_message() ); } $response->status = 'success'; $response->data->redirect_url = CourseBuilder::get_link_course_builder( CourseBuilderTemplate::MENU_QUIZZES . "/{$new_item_id}" ); $response->message = __( 'Quiz duplicated successfully', 'learnpress' ); wp_send_json( $response ); } catch ( Throwable $th ) { $response->status = 'error'; $response->message = $th->getMessage(); wp_send_json( $response ); } } public function move_trash_quiz() { $response = new LP_REST_Response(); $response->data = new stdClass(); try { $data = self::check_valid_quiz(); $quiz_id = $data['quiz_id'] ?? 0; $status = $data['status'] ?? 'trash'; $quiz_model = $data['quiz_model'] ?? []; $quiz_slug = ! empty( $data['quiz_permalink'] ) ? sanitize_title( wp_unslash( (string) $data['quiz_permalink'] ) ) : ''; if ( ! $quiz_model ) { throw new Exception( __( 'Quiz not found', 'learnpress' ) ); } if ( absint( $quiz_model->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) { throw new Exception( __( 'You are not allowed to delete this quiz', 'learnpress' ) ); } $course_id = absint( $data['course_id'] ?? 0 ); if ( ! $course_id ) { $course_id = absint( $this->get_course_by_item_id( $quiz_id ) ); } if ( $status === 'trash' ) { $original_slug = (string) get_post_field( 'post_name', $quiz_id ); $move_trash = wp_trash_post( $quiz_id ); if ( is_wp_error( $move_trash ) ) { throw new Exception( esc_html__( 'Cannot move this quiz to trash', 'learnpress' ) ); } if ( '' !== $original_slug ) { wp_update_post( [ 'ID' => $quiz_id, 'post_name' => $original_slug, ] ); } $message = esc_html__( 'This quiz has been moved to trash.', 'learnpress' ); } elseif ( $status === 'delete' ) { if ( $quiz_model->post_status !== 'trash' ) { throw new Exception( esc_html__( 'Quiz must be trashed before deleting.', 'learnpress' ) ); } $delete = wp_delete_post( $quiz_id ); if ( is_wp_error( $delete ) ) { throw new Exception( esc_html__( 'Cannot delete this quiz.', 'learnpress' ) ); } $message = esc_html__( 'Delete this quiz successfully', 'learnpress' ); } elseif ( in_array( $status, [ 'publish', 'draft' ], true ) ) { $restore_with_custom_slug = $this->prepare_desired_slug_for_restore( $quiz_id, $status, $quiz_slug ); $update_args = [ 'ID' => $quiz_id, 'post_type' => LP_QUIZ_CPT, 'post_status' => $status, ]; if ( '' !== $quiz_slug ) { $update_args['post_name'] = $quiz_slug; } $update = wp_update_post( $update_args ); if ( ! $update ) { if ( 'draft' === $status ) { throw new Exception( __( 'Quiz cannot be moved to draft', 'learnpress' ) ); } throw new Exception( __( 'Quiz cannot be moved to publish', 'learnpress' ) ); } if ( $restore_with_custom_slug ) { $this->sync_slug_after_restore( $quiz_id, $quiz_slug ); } $message = 'draft' === $status ? __( 'Quiz has been moved to draft', 'learnpress' ) : __( 'Quiz has been moved to publish', 'learnpress' ); } else { throw new Exception( __( 'Invalid quiz status transition', 'learnpress' ) ); } $saved_quiz_status = get_post_status( $quiz_id ); if ( ! is_string( $saved_quiz_status ) || '' === $saved_quiz_status ) { $saved_quiz_status = $status; } if ( $saved_quiz_status !== 'publish' ) { $this->remove_course_item_from_curriculum( $quiz_id, $course_id ); } $response->data->status = $saved_quiz_status; $response->data->button_title = 'publish' === $saved_quiz_status ? __( 'Update', 'learnpress' ) : __( 'Publish', 'learnpress' ); $response->data->quiz_slug = (string) get_post_field( 'post_name', $quiz_id ); $response->data->permalink_available = false; $response->data->permalink_notice = $this->get_item_permalink_unavailable_message(); $course_id_of_item = $this->get_course_by_item_id( $quiz_id ); if ( 'publish' === $saved_quiz_status && $course_id_of_item ) { $course = learn_press_get_course( $course_id_of_item ); if ( $course ) { $response->data->permalink_available = true; $response->data->quiz_permalink = urldecode( $course->get_item_link( $quiz_id ) ); } } if ( 'delete' !== $status ) { $fresh_quiz_model = QuizPostModel::find( $quiz_id, true ); $response->data->html = $fresh_quiz_model ? BuilderListQuizzesTemplate::render_quiz( $fresh_quiz_model ) : ''; } $response->status = 'success'; $response->message = $message; wp_send_json( $response ); } catch ( Throwable $th ) { $response->status = 'error'; $response->message = $th->getMessage(); wp_send_json( $response ); } } public function duplicate_question() { $response = new LP_REST_Response(); $response->data = new stdClass(); try { $data = self::check_valid_question(); $question_id = $data['question_id'] ?? 0; $question_model = $data['question_model']; if ( absint( $question_model->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) { throw new Exception( __( 'You are not allowed to duplicate this question', 'learnpress' ) ); } if ( ! function_exists( 'learn_press_duplicate_post' ) ) { require_once LP_PLUGIN_PATH . 'inc/admin/lp-admin-functions.php'; } $duplicate_args = apply_filters( 'learn-press/duplicate-post-args', array( 'post_status' => 'publish' ) ); $curd = new LP_Question_CURD(); $new_item_id = $curd->duplicate( $question_id, $duplicate_args ); if ( is_wp_error( $new_item_id ) ) { throw new Exception( $new_item_id->get_error_message() ); } $response->status = 'success'; $response->data->redirect_url = CourseBuilder::get_link_course_builder( CourseBuilderTemplate::MENU_QUESTIONS . "/{$new_item_id}" ); $response->message = __( 'Question duplicated successfully', 'learnpress' ); wp_send_json( $response ); } catch ( Throwable $th ) { $response->status = 'error'; $response->message = $th->getMessage(); wp_send_json( $response ); } } public function builder_update_question() { $response = new Response(); $response->data = new stdClass(); try { $data = self::check_valid_question(); $question_id = $data['question_id'] ?? 0; $title = LP_Helper::sanitize_params_submitted( $data['question_title'] ?? '' ); $description = LP_Helper::sanitize_params_submitted( $data['question_description'] ?? '', 'html' ); $is_elementor = $data['is_elementor'] ?? false; $return_html = ( $data['return_html'] ?? 'no' ) === 'yes'; $insert = $data['insert']; $question_slug = LP_Helper::sanitize_params_submitted( $data['question_permalink'] ?? '', 'key' ); // Determine target status (draft or publish) $target_status = LP_Helper::sanitize_params_submitted( $data['question_status'] ?? '', 'key' ); if ( empty( $title ) ) { throw new Exception( __( 'Question title is required', 'learnpress' ) ); } if ( $insert ) { $insert_arg = array( 'post_title' => $title, 'post_content' => $description, 'post_status' => $target_status, 'post_author' => get_current_user_id(), ); $questionPostModelNew = new QuestionPostModel( $insert_arg ); $questionPostModelNew->check_capabilities_create_item_course(); $questionPostModelNew->save(); $question_id = $questionPostModelNew->ID; $question_model = $questionPostModelNew; $response->message = __( 'Create question successfully', 'learnpress' ); } else { $question_model = $data['question_model'] ?? null; if ( ! $question_model instanceof QuestionPostModel ) { throw new Exception( __( 'Question not found', 'learnpress' ) ); } $question_model->check_capabilities_update_item_course(); if ( defined( 'ELEMENTOR_VERSION' ) ) { \Elementor\Plugin::$instance->documents->get( $question_id )->set_is_built_with_elementor( ! empty( $is_elementor ) ); } $question_model->post_title = $title; $question_model->post_content = $description; $question_model->post_status = $target_status; if ( ! empty( $question_slug ) ) { $question_model->post_name = $question_slug; } $question_model->save(); $response->message = __( 'Update question successfully', 'learnpress' ); } // Remove question from quizzes if status is not public if ( $target_status !== 'publish' ) { $this->remove_question_from_assigned_quizzes( $question_id ); } do_action( 'learn-press/course-builder/update-question', $data, $question_model ); $response->status = 'success'; $response->data->status = $question_model->post_status; $response->data->button_title = $question_model->post_status === 'publish' ? __( 'Update', 'learnpress' ) : __( 'Publish', 'learnpress' ); $response->data->question_id_new = $insert ? $question_id : ''; if ( $insert && $question_id ) { $response->data->redirect_url = CourseBuilder::get_link_course_builder( CourseBuilderTemplate::MENU_QUESTIONS . "/{$question_id}" ); } $response->message = $target_status === 'draft' ? esc_html__( 'Question saved as draft', 'learnpress' ) : ( $insert ? esc_html__( 'Insert question successfully', 'learnpress' ) : esc_html__( 'Update question successfully', 'learnpress' ) ); $response->data->question_slug = $question_model->post_name; $response->data->question_permalink = get_permalink( $question_id ); $question_model_for_html = QuestionPostModel::find( $question_id, true ); if ( $return_html ) { $response->data->list_item_html = $question_model_for_html ? BuilderListQuestionsTemplate::render_question( $question_model_for_html ) : ''; } $response->status = Response::STATUS_SUCCESS; } catch ( Throwable $e ) { $response->message = $e->getMessage(); } wp_send_json( $response ); } public function move_trash_question() { $response = new LP_REST_Response(); $response->data = new stdClass(); try { $data = self::check_valid_question(); $question_id = $data['question_id'] ?? 0; $status = $data['status'] ?? 'trash'; $question_model = $data['question_model'] ?? []; if ( ! $question_model ) { throw new Exception( __( 'Question not found', 'learnpress' ) ); } if ( absint( $question_model->post_author ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) { throw new Exception( __( 'You are not allowed to delete this question', 'learnpress' ) ); } if ( $status === 'trash' ) { $move_trash = wp_trash_post( $question_id ); if ( is_wp_error( $move_trash ) ) { throw new Exception( esc_html__( 'Cannot move this question to trash', 'learnpress' ) ); } $message = esc_html__( 'This question has been moved to trash.', 'learnpress' ); } elseif ( $status === 'delete' ) { if ( $question_model->post_status !== 'trash' ) { throw new Exception( esc_html__( 'Question must be trashed before deleting.', 'learnpress' ) ); } $delete = wp_delete_post( $question_id ); if ( is_wp_error( $delete ) ) { throw new Exception( esc_html__( 'Cannot delete this question.', 'learnpress' ) ); } $message = esc_html__( 'Delete this question successfully', 'learnpress' ); } elseif ( $status === 'publish' ) { $update = wp_update_post( array( 'ID' => $question_id, 'post_type' => LP_QUESTION_CPT, 'post_status' => 'publish', ) ); if ( ! $update ) { throw new Exception( __( 'Question cannot be moved to publish', 'learnpress' ) ); } $message = __( 'Question has been moved to publish', 'learnpress' ); } elseif ( $status === 'draft' ) { $update = wp_update_post( array( 'ID' => $question_id, 'post_type' => LP_QUESTION_CPT, 'post_status' => 'draft', ) ); if ( ! $update ) { throw new Exception( __( 'Question cannot be restored to draft', 'learnpress' ) ); } $message = __( 'Question has been restored to draft', 'learnpress' ); } if ( $status !== 'publish' ) { $this->remove_question_from_assigned_quizzes( $question_id ); } $response->data->status = $status; $response->data->button_title = __( 'Publish', 'learnpress' ); if ( 'delete' !== $status ) { $fresh_question_model = QuestionPostModel::find( $question_id, true ); $response->data->html = $fresh_question_model ? BuilderListQuestionsTemplate::render_question( $fresh_question_model ) : ''; } $response->status = 'success'; $response->message = $message; wp_send_json( $response ); } catch ( Throwable $th ) { $response->status = 'error'; $response->message = $th->getMessage(); wp_send_json( $response ); } } /** * Remove lesson/quiz assignment from curriculum sections. * * @param int $item_id * @param int $course_id_fallback * * @return void */ protected function remove_course_item_from_curriculum( int $item_id, int $course_id_fallback = 0 ) { if ( $item_id <= 0 ) { return; } global $wpdb; $section_items = $wpdb->get_results( $wpdb->prepare( " SELECT si.section_id, s.section_course_id FROM {$wpdb->learnpress_section_items} si INNER JOIN {$wpdb->learnpress_sections} s ON s.section_id = si.section_id WHERE si.item_id = %d ", $item_id ) ); if ( ! $section_items ) { if ( $course_id_fallback > 0 ) { $this->clean_course_curriculum_cache( $course_id_fallback ); } return; } foreach ( $section_items as $section_item ) { $section_id = absint( $section_item->section_id ?? 0 ); $course_id = absint( $section_item->section_course_id ?? 0 ); if ( ! $section_id || ! $course_id ) { continue; } $course_section_item_model = CourseSectionItemModel::find( $section_id, $item_id ); if ( ! $course_section_item_model ) { continue; } $course_section_item_model->section_course_id = $course_id; $course_section_item_model->delete(); } } /** * Keep course curriculum caches in sync after direct section item removal. * * @param int $course_id * * @return void */ protected function clean_course_curriculum_cache( int $course_id ) { if ( $course_id <= 0 ) { return; } try { $courseModel = CourseModel::find( $course_id, true ); if ( ! $courseModel ) { return; } $courseModel->sections_items = null; $courseModel->total_items = null; $courseModel->save( true ); } catch ( Throwable $e ) { error_log( __METHOD__ . ': ' . $e->getMessage() ); } } /** * Remove question assignment from all quizzes. * * @param int $question_id * * @return void */ protected function remove_question_from_assigned_quizzes( int $question_id ) { if ( $question_id <= 0 ) { return; } global $wpdb; $wpdb->delete( $wpdb->prefix . 'learnpress_quiz_questions', array( 'question_id' => $question_id ), array( '%d' ) ); } protected function get_course_by_item_id( $item_id ) { static $cache = []; global $wpdb; if ( empty( $item_id ) ) { return false; } $item_id = absint( $item_id ); if ( isset( $cache[ $item_id ] ) ) { return $cache[ $item_id ] ? $cache[ $item_id ] : false; } $course_id = $wpdb->get_var( $wpdb->prepare( "SELECT c.ID FROM {$wpdb->posts} c INNER JOIN {$wpdb->learnpress_sections} s ON c.ID = s.section_course_id INNER JOIN {$wpdb->learnpress_section_items} si ON si.section_id = s.section_id WHERE si.item_id = %d ORDER BY si.section_id DESC LIMIT 1 ", $item_id ) ); $cache[ $item_id ] = absint( $course_id ); if ( $cache[ $item_id ] ) { return $cache[ $item_id ]; } return false; } /** * Save global settings for Course Builder. * * @since 4.3.6 * @version 1.0.0 */ public function save_global_settings() { $response = new LP_REST_Response(); $response->data = new stdClass(); try { $params = wp_unslash( $_REQUEST['data'] ?? '' ); if ( empty( $params ) ) { throw new Exception( 'Error: params invalid!' ); } $data = LP_Helper::json_decode( $params, true ); if ( ! current_user_can( ADMIN_ROLE ) ) { throw new Exception( __( 'Permission denied', 'learnpress' ) ); } $hide_instructor_access_admin_screen = ! empty( $data['hide_instructor_access_admin_screen'] ) && $data['hide_instructor_access_admin_screen'] === 'yes' ? 'yes' : 'no'; $logo_remove = ! empty( $data['course_builder_logo_remove'] ) && $data['course_builder_logo_remove'] === 'yes'; $logo_id = absint( $data['course_builder_logo_id'] ?? 0 ); if ( $logo_remove ) { $logo_id = 0; } LP_Settings::update_option( 'hide_instructor_access_admin_screen', $hide_instructor_access_admin_screen ); LP_Settings::update_option( 'course_builder_logo_id', $logo_id ); $logo_url = ''; if ( $logo_id ) { $logo_url = wp_get_attachment_image_url( $logo_id, 'full' ); } $response->status = 'success'; $response->message = __( 'Course Builder settings updated.', 'learnpress' ); $response->data->hide_instructor_access_admin_screen = $hide_instructor_access_admin_screen; $response->data->course_builder_logo_id = $logo_id; $response->data->course_builder_logo_url = $logo_url; wp_send_json( $response ); } catch ( Throwable $th ) { $response->status = 'error'; $response->message = $th->getMessage(); wp_send_json( $response ); } } }